- Add a shared AUFS utility lib and change both changes and streamlayerformat to use it

- Add UI for selecting whether to pull the tag, the repo, or the squashed tag
This commit is contained in:
Joseph Schorr 2014-09-18 15:56:59 -04:00
parent 43555af63d
commit 05bb710830
8 changed files with 197 additions and 79 deletions

View file

@ -2206,37 +2206,55 @@ p.editable:hover i {
font-size: 0.8em; font-size: 0.8em;
position: relative; position: relative;
margin-top: 30px; margin-top: 30px;
margin-right: 26px;
} }
.repo .pull-container { .repo .pull-container {
display: inline-block; display: inline-block;
width: 300px; width: 460px;
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
vertical-align: middle; vertical-align: middle;
position: relative;
} }
.repo .pull-container input { .repo .pull-container .pull-selector {
cursor: default;
background: white;
color: #666;
padding: 4px;
border: 1px solid #ddd;
width: 300px;
}
.repo-image-view .id-container {
display: inline-block; 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 { .repo .pull-container .pull-selector i {
background: #fefefe; 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 { .repo-image-view #clipboardCopied {
@ -2272,25 +2290,45 @@ p.editable:hover i {
position: relative; position: relative;
} }
.copy-box-element.disabled .input-group-addon { .copy-box-element .copy-container {
display: none; 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 { .copy-box-element.disabled input {
border-radius: 4px !important; margin-right: 0px;
}
.copy-box-element.disabled .copy-icon {
display: none;
} }
.global-zeroclipboard-container embed { .global-zeroclipboard-container embed {
cursor: pointer; cursor: pointer;
} }
#copyClipboard.zeroclipboard-is-hover, .copy-box-element .zeroclipboard-is-hover { .copy-box-element .hovering {
background: #428bca;
color: white;
cursor: pointer !important;
}
#clipboardCopied.hovering, .copy-box-element .hovering {
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 40px; top: 40px;
@ -2298,16 +2336,11 @@ p.editable:hover i {
z-index: 100; z-index: 100;
} }
.copy-box-element .id-container {
display: inline-block;
vertical-align: middle;
}
.copy-box-element input { .copy-box-element input {
background-color: white !important; background-color: white !important;
} }
#clipboardCopied, .clipboard-copied-message { .clipboard-copied-message {
font-size: 0.8em; font-size: 0.8em;
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
@ -2318,7 +2351,7 @@ p.editable:hover i {
border-radius: 4px; border-radius: 4px;
} }
#clipboardCopied.animated, .clipboard-copied-message { .clipboard-copied-message {
-webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards; -webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards;
-moz-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; -ms-animation: fadeOut 4s ease-in-out 0s 1 forwards;

View file

@ -1,9 +1,12 @@
<div class="copy-box-element" ng-class="disabled ? 'disabled' : ''"> <div class="copy-box-element" ng-class="disabled ? 'disabled' : ''">
<div class="id-container"> <div class="id-container">
<div class="input-group"> <div class="copy-container">
<input type="text" class="form-control" value="{{ value }}" readonly> <input type="text" class="form-control" value="{{ value }}" readonly>
<span class="input-group-addon" data-title="Copy to Clipboard"> <span class="copy-icon" data-title="Copy to Clipboard"
<i class="fa fa-copy"></i> data-container="body"
data-placement="bottom"
bs-tooltip>
<i class="fa fa-clipboard"></i>
</span> </span>
</div> </div>
</div> </div>

View file

@ -812,6 +812,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return config['SERVER_HOSTNAME']; 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) { config.getUrl = function(opt_path) {
var path = opt_path || ''; var path = opt_path || '';
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path; return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
@ -2455,7 +2464,7 @@ quayApp.directive('copyBox', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
'value': '=value', 'value': '=value',
'hoveringMessage': '=hoveringMessage' 'hoveringMessage': '=hoveringMessage',
}, },
controller: function($scope, $element, $rootScope) { controller: function($scope, $element, $rootScope) {
$scope.disabled = false; $scope.disabled = false;
@ -2464,7 +2473,7 @@ quayApp.directive('copyBox', function () {
$rootScope.__copyBoxIdCounter = number + 1; $rootScope.__copyBoxIdCounter = number + 1;
$scope.inputId = "copy-box-input-" + number; $scope.inputId = "copy-box-input-" + number;
var button = $($element).find('.input-group-addon'); var button = $($element).find('.copy-icon');
var input = $($element).find('input'); var input = $($element).find('input');
input.attr('id', $scope.inputId); input.attr('id', $scope.inputId);

View file

@ -361,6 +361,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
var namespace = $routeParams.namespace; var namespace = $routeParams.namespace;
var name = $routeParams.name; var name = $routeParams.name;
$scope.pullCommands = [];
$scope.currentPullCommand = null;
$rootScope.title = 'Loading...'; $rootScope.title = 'Loading...';
// Watch for the destruction of the scope. // Watch for the destruction of the scope.
@ -395,6 +398,47 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$scope.buildDialogShowCounter = 0; $scope.buildDialogShowCounter = 0;
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand; $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.showNewBuildDialog = function() {
$scope.buildDialogShowCounter++; $scope.buildDialogShowCounter++;
}; };
@ -587,6 +631,8 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$location.search('tag', null); $location.search('tag', null);
$location.search('image', imageId.substr(0, 12)); $location.search('image', imageId.substr(0, 12));
} }
$scope.updatePullCommand();
}; };
$scope.setTag = function(tagName, opt_updateURL) { $scope.setTag = function(tagName, opt_updateURL) {
@ -621,6 +667,8 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$scope.currentTag = null; $scope.currentTag = null;
$scope.currentImage = null; $scope.currentImage = null;
} }
$scope.updatePullCommand();
}; };
$scope.getFirstTextLine = getFirstTextLine; $scope.getFirstTextLine = getFirstTextLine;

View file

@ -56,10 +56,21 @@
<!-- Pull Command --> <!-- Pull Command -->
<span class="pull-command visible-md-inline"> <span class="pull-command visible-md-inline">
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title"> <div class="pull-container" ng-show="currentPullCommand">
<div class="input-group"> <button class="pull-selector dropdown-toggle" data-toggle="dropdown">
<div class="copy-box" hovering-message="true" value="'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name"></div> <i class="fa" ng-class="currentPullCommand.icon"></i>
</div> {{ currentPullCommand.shortTitle }}
<b class="caret"></b>
</button>
<ul class="dropdown-menu">
<li ng-repeat="pullCommand in pullCommands">
<a href="javascript:void(0)" ng-click="setCurrentPullCommand(pullCommand)"><i class="fa" ng-class="pullCommand.icon"></i>
{{ pullCommand.title }}
<sup ng-if="pullCommand.experimental">Experimental</sup>
</a>
</li>
</ul>
<div class="copy-box" hovering-message="true" value="currentPullCommand.command"></div>
</div> </div>
</span> </span>
</div> </div>

31
util/aufs.py Normal file
View file

@ -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)

View file

@ -1,16 +1,10 @@
import marisa_trie import marisa_trie
import os import os
import tarfile import tarfile
from aufs import is_aufs_metadata, get_deleted_prefix
AUFS_METADATA = u'.wh..wh.'
AUFS_WHITEOUT = u'.wh.'
AUFS_WHITEOUT_PREFIX_LENGTH = len(AUFS_WHITEOUT)
ALLOWED_TYPES = {tarfile.REGTYPE, tarfile.AREGTYPE} ALLOWED_TYPES = {tarfile.REGTYPE, tarfile.AREGTYPE}
def files_and_dirs_from_tar(source_stream, removed_prefix_collector): def files_and_dirs_from_tar(source_stream, removed_prefix_collector):
try: try:
tar_stream = tarfile.open(mode='r|*', fileobj=source_stream) 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: for tar_info in tar_stream:
absolute = os.path.relpath(tar_info.name.decode('utf-8'), './') absolute = os.path.relpath(tar_info.name.decode('utf-8'), './')
dirname = os.path.dirname(absolute)
filename = os.path.basename(absolute)
# Skip directories and metadata # Skip metadata.
if (filename.startswith(AUFS_METADATA) or if is_aufs_metadata(absolute):
absolute.startswith(AUFS_METADATA)):
# Skip
continue continue
elif filename.startswith(AUFS_WHITEOUT): # Add prefixes of removed paths to the collector.
removed_filename = filename[AUFS_WHITEOUT_PREFIX_LENGTH:] deleted_prefix = get_deleted_prefix(absolute)
removed_prefix = os.path.join('/', dirname, removed_filename) if deleted_prefix is not None:
removed_prefix_collector.add(removed_prefix) deleted_prefix.add(deleted_prefix)
continue 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 yield '/' + absolute

View file

@ -1,8 +1,8 @@
import marisa_trie import marisa_trie
import os import os
import tarfile import tarfile
import StringIO from aufs import is_aufs_metadata, get_deleted_prefix
import traceback
AUFS_METADATA = u'.wh..wh.' AUFS_METADATA = u'.wh..wh.'
@ -70,19 +70,15 @@ class StreamLayerMerger(object):
def process_tar_info(self, tar_info): def process_tar_info(self, tar_info):
absolute = os.path.relpath(tar_info.name.decode('utf-8'), './') absolute = os.path.relpath(tar_info.name.decode('utf-8'), './')
dirname = os.path.dirname(absolute)
filename = os.path.basename(absolute)
# Skip directories and metadata # Skip metadata.
if (filename.startswith(AUFS_METADATA) or if is_aufs_metadata(absolute):
absolute.startswith(AUFS_METADATA)):
# Skip
return None return None
elif filename.startswith(AUFS_WHITEOUT): # Add any prefix of deleted paths to the prefix list.
removed_filename = filename[AUFS_WHITEOUT_PREFIX_LENGTH:] deleted_prefix = get_deleted_prefix(absolute)
removed_prefix = os.path.join('/', dirname, removed_filename) if deleted_prefix is not None:
self.encountered.append(removed_prefix) self.encountered.append(deleted_prefix)
return None return None
# Check if this file has already been encountered somewhere. If so, # Check if this file has already been encountered somewhere. If so,
@ -90,10 +86,6 @@ class StreamLayerMerger(object):
if unicode(absolute) in self.trie: if unicode(absolute) in self.trie:
return None return None
# Otherwise, add the path to the encountered list and return it.
self.encountered.append(absolute) self.encountered.append(absolute)
return (tar_info, tar_info.isfile() or tar_info.isdev())
if tar_info.isdir() or tar_info.issym() or tar_info.islnk():
return (tar_info, False)
elif tar_info.isfile():
return (tar_info, True)