diff --git a/static/css/directives/ui/dockerfile-build-form.css b/static/css/directives/ui/dockerfile-build-form.css index 825a6a717..2b0b95299 100644 --- a/static/css/directives/ui/dockerfile-build-form.css +++ b/static/css/directives/ui/dockerfile-build-form.css @@ -3,6 +3,10 @@ white-space: nowrap; } +.dockerfile-build-form .file-drop { + padding: 0px; +} + .dockerfile-build-form input[type="file"] { margin: 0px; } @@ -10,6 +14,11 @@ .dockerfile-build-form .help-text { font-size: 13px; color: #aaa; - margin-bottom: 20px; - padding-left: 22px; + margin-top: 10px; + margin-bottom: 16px; +} + +.dockerfile-build-form dd { + padding-left: 20px; + padding-top: 14px; } \ No newline at end of file diff --git a/static/directives/dockerfile-build-form.html b/static/directives/dockerfile-build-form.html index 4bb9558f6..02253858c 100644 --- a/static/directives/dockerfile-build-form.html +++ b/static/directives/dockerfile-build-form.html @@ -12,35 +12,35 @@
Dockerfile or .tar.gz or .zip:
- -
If an archive, the Dockerfile must be at the root
+
+ {{ dockerfileError }} +
+ + +
Note: If an archive, the Dockerfile must be in the root directory.
+
+ Reading Dockerfile: +
-
+
Base Image Pull Credentials:
-
- -
- - +
+
+ Warning: Robot account {{ pullEntity.name }} does not have + read permission on repository {{ privateBaseRepository }}, so + this build will fail with an authorization error.
- - -
- + +
+ The selected Dockerfile contains a FROM that refers to the private + repository {{ privateBaseRepository }}. + A robot account with read access to that repository is required for the build.
diff --git a/static/js/directives/file-present.js b/static/js/directives/file-present.js index a692ac167..4789f3b95 100644 --- a/static/js/directives/file-present.js +++ b/static/js/directives/file-present.js @@ -15,4 +15,23 @@ angular.module('quay').directive("filePresent", [function () { }); } } +}]); + +/** + * Raises the 'filesChanged' event on the scope if a file on the marked exists. + */ +angular.module('quay').directive("filesChanged", [function () { + return { + restrict: 'A', + scope: { + 'filesChanged': "&" + }, + link: function (scope, element, attributes) { + element.bind("change", function (changeEvent) { + scope.$apply(function() { + scope.filesChanged({'files': changeEvent.target.files}); + }); + }); + } + } }]); \ No newline at end of file diff --git a/static/js/directives/ui/dockerfile-build-form.js b/static/js/directives/ui/dockerfile-build-form.js index 76e284f8c..5ce6b6aec 100644 --- a/static/js/directives/ui/dockerfile-build-form.js +++ b/static/js/directives/ui/dockerfile-build-form.js @@ -20,10 +20,66 @@ angular.module('quay').directive('dockerfileBuildForm', function () { 'uploading': '=uploading', 'building': '=building' }, - controller: function($scope, $element, ApiService) { - $scope.internal = {'hasDockerfile': false}; - $scope.pull_entity = null; - $scope.is_public = true; + controller: function($scope, $element, ApiService, DockerfileService, Config) { + var MEGABYTE = 1000000; + var MAX_FILE_SIZE = 100 * MEGABYTE; + + $scope.hasDockerFile = false; + $scope.pullEntity = null; + $scope.dockerfileState = 'none'; + $scope.privateBaseRepository = null; + $scope.isReady = false; + + $scope.handleFilesChanged = function(files) { + $scope.dockerfileError = ''; + $scope.privateBaseRepository = null; + $scope.pullEntity = null; + + $scope.dockerfileState = 'loading'; + $scope.hasDockerFile = files.length > 0; + + var checkPrivateImage = function(baseImage) { + var params = { + 'repository': baseImage + }; + + ApiService.getRepo(null, params).then(function(repository) { + $scope.privateBaseRepository = repository.is_public ? null : baseImage; + $scope.dockerfileState = 'ready'; + }, function() { + $scope.privateBaseRepository = baseImage; + $scope.dockerfileState = 'ready'; + }); + }; + + var loadError = function(msg) { + $scope.$apply(function() { + $scope.dockerfileError = msg || 'Could not read uploaded Dockerfile'; + $scope.dockerfileState = 'error'; + }); + }; + + var gotDockerfile = function(df) { + $scope.$apply(function() { + var baseImage = df.getRegistryBaseImage(); + if (baseImage) { + checkPrivateImage(baseImage); + } else { + $scope.dockerfileState = 'ready'; + } + }); + }; + + if (files.length > 0) { + if (files[0].size < MAX_FILE_SIZE) { + DockerfileService.getDockerfile(files[0], gotDockerfile, loadError); + } else { + $scope.dockerfileState = 'ready'; + } + } else { + $scope.dockerfileState = 'none'; + } + }; var handleBuildFailed = function(message) { message = message || 'Dockerfile build failed to start'; @@ -97,8 +153,8 @@ angular.module('quay').directive('dockerfileBuildForm', function () { 'file_id': fileId }; - if (!$scope.is_public && $scope.pull_entity) { - data['pull_robot'] = $scope.pull_entity['name']; + if ($scope.pullEntity) { + data['pull_robot'] = $scope.pullEntity['name']; } var params = { @@ -180,13 +236,28 @@ angular.module('quay').directive('dockerfileBuildForm', function () { }); }; - var checkIsReady = function() { - $scope.isReady = $scope.internal.hasDockerfile && ($scope.is_public || $scope.pull_entity); + var checkReady = function() { + $scope.isReady = ($scope.dockerfileState == 'ready' && + (!$scope.privateBaseRepository || $scope.pullEntity)); }; - $scope.$watch('pull_entity', checkIsReady); - $scope.$watch('is_public', checkIsReady); - $scope.$watch('internal.hasDockerfile', checkIsReady); + var checkEntity = function() { + $scope.currentRobotHasPermission = null; + if (!$scope.pullEntity) { return; } + + var permParams = { + 'repository': $scope.privateBaseRepository, + 'username': $scope.pullEntity.name + }; + + ApiService.getUserTransitivePermission(null, permParams).then(function(resp) { + $scope.currentRobotHasPermission = resp['permissions'].length > 0; + }); + }; + + $scope.$watch('pullEntity', checkEntity); + $scope.$watch('pullEntity', checkReady); + $scope.$watch('dockerfileState', checkReady); $scope.$watch('startNow', function() { if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) { diff --git a/static/js/services/dockerfile-service.js b/static/js/services/dockerfile-service.js new file mode 100644 index 000000000..c6f1d199f --- /dev/null +++ b/static/js/services/dockerfile-service.js @@ -0,0 +1,98 @@ +/** + * Service which provides helper methods for extracting information out from a Dockerfile + * or an archive containing a Dockerfile. + */ +angular.module('quay').factory('DockerfileService', ['DataFileService', 'Config', function(DataFileService, Config) { + var dockerfileService = {}; + + function DockerfileInfo(contents) { + this.contents = contents; + } + + DockerfileInfo.prototype.getRegistryBaseImage = function() { + var baseImage = this.getBaseImage(); + if (!baseImage) { + return; + } + + if (baseImage.indexOf(Config.getDomain() + '/') != 0) { + return; + } + + return baseImage.substring(Config.getDomain().length + 1); + }; + + DockerfileInfo.prototype.getBaseImage = function() { + var fromIndex = this.contents.indexOf('FROM '); + if (fromIndex < 0) { + return; + } + + var newline = this.contents.indexOf('\n', fromIndex); + if (newline < 0) { + newline = this.contents.length; + } + + return $.trim(this.contents.substring(fromIndex + 'FROM '.length, newline)); + }; + + DockerfileInfo.forData = function(contents) { + if (contents.indexOf('FROM ') < 0) { + return; + } + + return new DockerfileInfo(contents); + }; + + var processFiles = function(files, dataArray, success, failure) { + // The files array will be empty if the submitted file was not an archive. We therefore + // treat it as a single Dockerfile. + if (files.length == 0) { + DataFileService.arrayToString(dataArray, function(c) { + var result = DockerfileInfo.forData(c); + if (!result) { + failure('File chosen is not a valid Dockerfile'); + return; + } + + success(result); + }); + return; + } + + var found = false; + files.forEach(function(file) { + if (file['name'] == 'Dockerfile') { + DataFileService.blobToString(file.toBlob(), function(c) { + var result = DockerfileInfo.forData(c); + if (!result) { + failure('Dockerfile inside archive is not a valid Dockerfile'); + return; + } + + success(result); + }); + found = true; + } + }); + + if (!found) { + failure('No Dockerfile found in root of archive'); + } + }; + + dockerfileService.getDockerfile = function(file, success, failure) { + var reader = new FileReader(); + reader.onload = function(e) { + var dataArray = reader.result; + DataFileService.readDataArrayAsPossibleArchive(dataArray, function(files) { + processFiles(files, dataArray, success, failure); + }, failure); + }; + + reader.onerror = failure; + reader.readAsArrayBuffer(file); + }; + + return dockerfileService; +}]); \ No newline at end of file