- 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:
parent
43555af63d
commit
05bb710830
8 changed files with 197 additions and 79 deletions
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
31
util/aufs.py
Normal 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)
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
Reference in a new issue