diff --git a/static/css/quay.css b/static/css/quay.css index c2652c8c1..4b559a104 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2206,37 +2206,55 @@ p.editable:hover i { font-size: 0.8em; position: relative; margin-top: 30px; - margin-right: 26px; } .repo .pull-container { display: inline-block; - width: 300px; + width: 460px; margin-left: 10px; margin-right: 10px; vertical-align: middle; + position: relative; } -.repo .pull-container input { - cursor: default; - background: white; - color: #666; - padding: 4px; - border: 1px solid #ddd; - width: 300px; -} - -.repo-image-view .id-container { +.repo .pull-container .pull-selector { display: inline-block; - margin-top: 10px; + width: 114px; + font-size: 14px; + height: 36px; + vertical-align: top; + border: 1px solid #ddd; + margin-right: -3px; + background: #f8f8f8; + outline: none; } -.repo-image-view .id-container input { - background: #fefefe; +.repo .pull-container .pull-selector i { + display: inline-block; + margin-right: 6px; } -.repo-image-view .id-container .input-group { - width: 542px; + +.repo .pull-container .copy-box { + width: 340px; + display: inline-block; +} + +.repo .pull-container .copy-box .copy-container { + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; + border-left: 0px; +} + +.repo .pull-container .dropdown-menu li i.fa { + text-align: center; + width: 12px; + display: inline-block; +} + +.repo .pull-container sup { + margin-left: 4px; + color: red; } .repo-image-view #clipboardCopied { @@ -2272,25 +2290,45 @@ p.editable:hover i { position: relative; } -.copy-box-element.disabled .input-group-addon { - display: none; +.copy-box-element .copy-container { + border-radius: 4px !important; + border: 1px solid #ddd; + position: relative; +} + +.copy-box-element input { + border: 0px; + padding-right: 32px; +} + +.copy-box-element .copy-container .copy-icon { + position: absolute; + top: 8px; + right: 10px; + display: inline-block; + color: #ddd; + font-size: 16px; + cursor: pointer; + transition: color 0.5s ease-in-out; +} + +.copy-box-element .copy-container .copy-icon.zeroclipboard-is-hover { + color: #444; } .copy-box-element.disabled input { - border-radius: 4px !important; + margin-right: 0px; +} + +.copy-box-element.disabled .copy-icon { + display: none; } .global-zeroclipboard-container embed { cursor: pointer; } -#copyClipboard.zeroclipboard-is-hover, .copy-box-element .zeroclipboard-is-hover { - background: #428bca; - color: white; - cursor: pointer !important; -} - -#clipboardCopied.hovering, .copy-box-element .hovering { +.copy-box-element .hovering { position: absolute; right: 0px; top: 40px; @@ -2298,16 +2336,11 @@ p.editable:hover i { z-index: 100; } -.copy-box-element .id-container { - display: inline-block; - vertical-align: middle; -} - .copy-box-element input { background-color: white !important; } -#clipboardCopied, .clipboard-copied-message { +.clipboard-copied-message { font-size: 0.8em; display: inline-block; margin-right: 10px; @@ -2318,7 +2351,7 @@ p.editable:hover i { border-radius: 4px; } -#clipboardCopied.animated, .clipboard-copied-message { +.clipboard-copied-message { -webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards; -moz-animation: fadeOut 4s ease-in-out 0s 1 forwards; -ms-animation: fadeOut 4s ease-in-out 0s 1 forwards; diff --git a/static/directives/copy-box.html b/static/directives/copy-box.html index 07dea7407..7532a6d68 100644 --- a/static/directives/copy-box.html +++ b/static/directives/copy-box.html @@ -1,9 +1,12 @@
-
+
- - + +
diff --git a/static/js/app.js b/static/js/app.js index 9ebe2a3e1..1adf4d0b3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -812,6 +812,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading return config['SERVER_HOSTNAME']; }; + config.getHost = function(opt_auth) { + var auth = opt_auth; + if (auth) { + auth = auth + '@'; + } + + return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME']; + }; + config.getUrl = function(opt_path) { var path = opt_path || ''; return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path; @@ -2455,7 +2464,7 @@ quayApp.directive('copyBox', function () { restrict: 'C', scope: { 'value': '=value', - 'hoveringMessage': '=hoveringMessage' + 'hoveringMessage': '=hoveringMessage', }, controller: function($scope, $element, $rootScope) { $scope.disabled = false; @@ -2464,7 +2473,7 @@ quayApp.directive('copyBox', function () { $rootScope.__copyBoxIdCounter = number + 1; $scope.inputId = "copy-box-input-" + number; - var button = $($element).find('.input-group-addon'); + var button = $($element).find('.copy-icon'); var input = $($element).find('input'); input.attr('id', $scope.inputId); diff --git a/static/js/controllers.js b/static/js/controllers.js index 7010dc4eb..f781f4dac 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -361,6 +361,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi var namespace = $routeParams.namespace; var name = $routeParams.name; + $scope.pullCommands = []; + $scope.currentPullCommand = null; + $rootScope.title = 'Loading...'; // Watch for the destruction of the scope. @@ -395,6 +398,47 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi $scope.buildDialogShowCounter = 0; $scope.getFormattedCommand = ImageMetadataService.getFormattedCommand; + $scope.setCurrentPullCommand = function(pullCommand) { + $scope.currentPullCommand = pullCommand; + }; + + $scope.updatePullCommand = function() { + $scope.pullCommands = []; + + if ($scope.currentTag) { + $scope.pullCommands.push({ + 'title': 'docker pull (Tag ' + $scope.currentTag.name + ')', + 'shortTitle': 'Pull Tag', + 'icon': 'fa-tag', + 'command': 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name + ':' + $scope.currentTag.name + }); + } + + $scope.pullCommands.push({ + 'title': 'docker pull (Full Repository)', + 'shortTitle': 'Pull Repo', + 'icon': 'fa-code-fork', + 'command': 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name + }); + + if ($scope.currentTag) { + var squash = 'docker import ' + Config.getHost('ACCOUNTNAME:PASSWORDORTOKEN'); + squash += '/verbs/v1/' + namespace + '/' + name + '/' + $scope.currentTag.name + '/squash'; + squash += ' '; + squash += Config.getDomain() + '/' + namespace + '/' + name + '/' + $scope.currentTag.name + '.squash'; + + $scope.pullCommands.push({ + 'title': 'Squashed image (Tag ' + $scope.currentTag.name + ')', + 'shortTitle': 'Squashed', + 'icon': 'fa-file-archive-o', + 'command': squash, + 'experimental': true + }); + } + + $scope.currentPullCommand = $scope.pullCommands[0]; + }; + $scope.showNewBuildDialog = function() { $scope.buildDialogShowCounter++; }; @@ -587,6 +631,8 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi $location.search('tag', null); $location.search('image', imageId.substr(0, 12)); } + + $scope.updatePullCommand(); }; $scope.setTag = function(tagName, opt_updateURL) { @@ -621,6 +667,8 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi $scope.currentTag = null; $scope.currentImage = null; } + + $scope.updatePullCommand(); }; $scope.getFirstTextLine = getFirstTextLine; diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index e5f2cecc6..68be28679 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -56,10 +56,21 @@ -
-
-
-
+
+ + +
diff --git a/util/aufs.py b/util/aufs.py new file mode 100644 index 000000000..e1ffb5b4a --- /dev/null +++ b/util/aufs.py @@ -0,0 +1,31 @@ +import os + +AUFS_METADATA = u'.wh..wh.' +AUFS_WHITEOUT = u'.wh.' +AUFS_WHITEOUT_PREFIX_LENGTH = len(AUFS_WHITEOUT) + +def is_aufs_metadata(filepath): + """ Returns whether the given filepath references an AUFS metadata file. """ + filename = os.path.basename(filepath) + return filename.startswith(AUFS_METADATA) or filepath.startswith(AUFS_METADATA) + +def get_deleted_filename(filepath): + """ Returns the name of the deleted file referenced by the AUFS whiteout file at + the given path or None if the file path does not reference a whiteout file. + """ + filename = os.path.basename(filepath) + if not filename.startswith(AUFS_WHITEOUT): + return None + + return filename[AUFS_WHITEOUT_PREFIX_LENGTH:] + +def get_deleted_prefix(filepath): + """ Returns the path prefix of the deleted file referenced by the AUFS whiteout file at + the given path or None if the file path does not reference a whiteout file. + """ + deleted_filename = get_deleted_filename(filepath) + if deleted_filename is None: + return None + + dirname = os.path.dirname(filepath) + return os.path.join('/', dirname, deleted_filename) diff --git a/util/changes.py b/util/changes.py index eaeec9d83..a6d20041f 100644 --- a/util/changes.py +++ b/util/changes.py @@ -1,16 +1,10 @@ import marisa_trie import os import tarfile - - -AUFS_METADATA = u'.wh..wh.' - -AUFS_WHITEOUT = u'.wh.' -AUFS_WHITEOUT_PREFIX_LENGTH = len(AUFS_WHITEOUT) +from aufs import is_aufs_metadata, get_deleted_prefix ALLOWED_TYPES = {tarfile.REGTYPE, tarfile.AREGTYPE} - def files_and_dirs_from_tar(source_stream, removed_prefix_collector): try: tar_stream = tarfile.open(mode='r|*', fileobj=source_stream) @@ -20,22 +14,19 @@ def files_and_dirs_from_tar(source_stream, removed_prefix_collector): for tar_info in tar_stream: absolute = os.path.relpath(tar_info.name.decode('utf-8'), './') - dirname = os.path.dirname(absolute) - filename = os.path.basename(absolute) - # Skip directories and metadata - if (filename.startswith(AUFS_METADATA) or - absolute.startswith(AUFS_METADATA)): - # Skip + # Skip metadata. + if is_aufs_metadata(absolute): continue - elif filename.startswith(AUFS_WHITEOUT): - removed_filename = filename[AUFS_WHITEOUT_PREFIX_LENGTH:] - removed_prefix = os.path.join('/', dirname, removed_filename) - removed_prefix_collector.add(removed_prefix) + # Add prefixes of removed paths to the collector. + deleted_prefix = get_deleted_prefix(absolute) + if deleted_prefix is not None: + deleted_prefix.add(deleted_prefix) continue - elif tar_info.type in ALLOWED_TYPES: + # Otherwise, yield the path if it is in the allowed types. + if tar_info.type in ALLOWED_TYPES: yield '/' + absolute diff --git a/util/streamlayerformat.py b/util/streamlayerformat.py index c197763f1..757d1b4ef 100644 --- a/util/streamlayerformat.py +++ b/util/streamlayerformat.py @@ -1,8 +1,8 @@ import marisa_trie import os import tarfile -import StringIO -import traceback +from aufs import is_aufs_metadata, get_deleted_prefix + AUFS_METADATA = u'.wh..wh.' @@ -70,19 +70,15 @@ class StreamLayerMerger(object): def process_tar_info(self, tar_info): absolute = os.path.relpath(tar_info.name.decode('utf-8'), './') - dirname = os.path.dirname(absolute) - filename = os.path.basename(absolute) - # Skip directories and metadata - if (filename.startswith(AUFS_METADATA) or - absolute.startswith(AUFS_METADATA)): - # Skip + # Skip metadata. + if is_aufs_metadata(absolute): return None - elif filename.startswith(AUFS_WHITEOUT): - removed_filename = filename[AUFS_WHITEOUT_PREFIX_LENGTH:] - removed_prefix = os.path.join('/', dirname, removed_filename) - self.encountered.append(removed_prefix) + # Add any prefix of deleted paths to the prefix list. + deleted_prefix = get_deleted_prefix(absolute) + if deleted_prefix is not None: + self.encountered.append(deleted_prefix) return None # Check if this file has already been encountered somewhere. If so, @@ -90,10 +86,6 @@ class StreamLayerMerger(object): if unicode(absolute) in self.trie: return None + # Otherwise, add the path to the encountered list and return it. self.encountered.append(absolute) - - if tar_info.isdir() or tar_info.issym() or tar_info.islnk(): - return (tar_info, False) - - elif tar_info.isfile(): - return (tar_info, True) + return (tar_info, tar_info.isfile() or tar_info.isdev())