Merge remote-tracking branch 'origin/spartan'

Conflicts:
	test/data/test.db
This commit is contained in:
yackob03 2014-01-08 17:04:40 -05:00
commit 4234ceabe6
10 changed files with 246 additions and 10 deletions

View file

@ -755,6 +755,46 @@ def list_repository_tags(namespace_name, repository_name):
return with_image.where(Repository.name == repository_name,
Repository.namespace == namespace_name)
def delete_tag_and_images(namespace_name, repository_name, tag_name):
all_images = get_repository_images(namespace_name, repository_name)
all_tags = list_repository_tags(namespace_name, repository_name)
# Find the tag's information.
found_tag = None
for tag in all_tags:
if tag.name == tag_name:
found_tag = tag
break
if not found_tag:
return
# Build the set of database IDs corresponding to the tag's ancestor images, as well as the
# tag's image itself.
tag_image_ids = set(found_tag.image.ancestors.split('/'))
tag_image_ids.add(str(found_tag.image.id))
# Filter out any images that belong to any other tags.
for tag in all_tags:
if tag.name != tag_name:
# Remove all ancestors of the tag.
tag_image_ids = tag_image_ids - set(tag.image.ancestors.split('/'))
# Remove the current image ID.
tag_image_ids.discard(str(tag.image.id))
# Find all the images that belong to the tag.
tag_images = [image for image in all_images if str(image.id) in tag_image_ids]
# Delete the tag found.
found_tag.delete_instance()
# Delete the images found.
for image in tag_images:
image.delete_instance()
# TODO: Delete the image's layer data as well.
def get_tag_image(namespace_name, repository_name, tag_name):
joined = Image.select().join(RepositoryTag).join(Repository)

View file

@ -1159,6 +1159,24 @@ def get_image_changes(namespace, repository, image_id):
abort(403)
@app.route('/api/repository/<path:repository>/tag/<tag>',
methods=['DELETE'])
@parse_repository_name
def delete_full_tag(namespace, repository, tag):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_tag_and_images(namespace, repository, tag)
username = current_user.db_user().username
log_action('delete_tag', namespace,
{'username': username, 'repo': repository, 'tag': tag},
repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
methods=['GET'])
@parse_repository_name

View file

@ -158,10 +158,6 @@ def create_repository(namespace, repository):
for existing in model.get_repository_images(namespace, repository):
if existing.docker_image_id in new_repo_images:
added_images.pop(existing.docker_image_id)
else:
logger.debug('Deleting existing image with id: %s' %
existing.docker_image_id)
existing.delete_instance(recursive=True)
for image_description in added_images.values():
model.create_image(image_description['id'], repo)

View file

@ -126,6 +126,7 @@ def initialize_database():
LogEntryKind.create(name='push_repo')
LogEntryKind.create(name='pull_repo')
LogEntryKind.create(name='delete_repo')
LogEntryKind.create(name='delete_tag')
LogEntryKind.create(name='add_repo_permission')
LogEntryKind.create(name='change_repo_permission')
LogEntryKind.create(name='delete_repo_permission')
@ -282,6 +283,9 @@ def populate_database():
model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today,
metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'})
model.log_action('delete_tag', org.username, performer=new_user_2, repository=org_repo, timestamp=today,
metadata={'username': new_user_2.username, 'repo': 'orgrepo', 'tag': 'sometag'})
if __name__ == '__main__':
logging.basicConfig(**app.config['LOGGING_CONFIG'])
initialize_database()

View file

@ -1156,6 +1156,34 @@ p.editable:hover i {
font-size: 1.15em;
}
#tagContextMenu {
display: none;
position: absolute;
z-index: 100000;
outline: none;
}
#tagContextMenu ul {
display: block;
position: static;
margin-bottom: 5px;
}
.tag-controls {
display: inline-block;
margin-right: 20px;
margin-top: 2px;
opacity: 0;
float: right;
-webkit-transition: opacity 200ms ease-in-out;
-moz-transition: opacity 200ms ease-in-out;
transition: opacity 200ms ease-in-out;
}
.tag-heading:hover .tag-controls {
opacity: 1;
}
.right-title {
display: inline-block;
float: right;

View file

@ -194,6 +194,38 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}
};
$scope.askDeleteTag = function(tagName) {
if (!$scope.repo.can_admin) { return; }
$scope.tagToDelete = tagName;
$('#confirmdeleteTagModal').modal('show');
};
$scope.deleteTag = function(tagName) {
if (!$scope.repo.can_admin) { return; }
$('#confirmdeleteTagModal').modal('hide');
var params = {
'repository': namespace + '/' + name,
'tag': tagName
};
ApiService.deleteFullTag(null, params).then(function() {
loadViewInfo();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'Could not delete tag',
"title": "Cannot delete tag",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.setTag = function(tagName, opt_updateURL) {
var repo = $scope.repo;
var proposedTag = repo.tags[tagName];
@ -218,6 +250,11 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
$location.search('tag', $scope.currentTag.name);
}
}
if ($scope.currentTag && !repo.tags[$scope.currentTag.name]) {
$scope.currentTag = null;
$scope.currentImage = null;
}
};
$scope.getTagCount = function(repo) {
@ -229,6 +266,40 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
return count;
};
$scope.hideTagMenu = function(tagName, clientX, clientY) {
$scope.currentMenuTag = null;
var tagMenu = $("#tagContextMenu");
tagMenu.hide();
};
$scope.showTagMenu = function(tagName, clientX, clientY) {
if (!$scope.repo.can_admin) { return; }
$scope.currentMenuTag = tagName;
var tagMenu = $("#tagContextMenu");
tagMenu.css({
display: "block",
left: clientX,
top: clientY
});
tagMenu.on("blur", function() {
setTimeout(function() {
tagMenu.hide();
}, 100); // Needed to allow clicking on menu items.
});
tagMenu.on("click", "a", function() {
setTimeout(function() {
tagMenu.hide();
}, 100); // Needed to allow clicking on menu items.
});
tagMenu[0].focus();
};
var getDefaultTag = function() {
if ($scope.repo === undefined) {
return undefined;
@ -343,6 +414,14 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
$scope.$apply(function() { $scope.setImage(e.image); });
});
$($scope.tree).bind('showTagMenu', function(e) {
$scope.$apply(function() { $scope.showTagMenu(e.tag, e.clientX, e.clientY); });
});
$($scope.tree).bind('hideTagMenu', function(e) {
$scope.$apply(function() { $scope.hideTagMenu(); });
});
return resp.images;
});
};

View file

@ -69,6 +69,25 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
};
ImageHistoryTree.prototype.setupOverscroll_ = function() {
var container = this.container_;
var that = this;
var overscroll = $('#' + container).overscroll();
overscroll.on('overscroll:dragstart', function() {
$(that).trigger({
'type': 'hideTagMenu'
});
});
overscroll.on('scroll', function() {
$(that).trigger({
'type': 'hideTagMenu'
});
});
};
/**
* Updates the dimensions of the tree.
*/
@ -88,8 +107,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var boundingBox = document.getElementById(container).getBoundingClientRect();
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 150) + 'px';
$('#' + container).overscroll();
this.setupOverscroll_();
// Update the tree.
var rootSvg = this.rootSvg_;
var tree = this.tree_;
@ -183,8 +202,7 @@ ImageHistoryTree.prototype.draw = function(container) {
this.root_.y0 = 0;
this.setTag_(this.currentTag_);
$('#' + container).overscroll();
this.setupOverscroll_();
};
@ -642,7 +660,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
if (tag == currentTag) {
kind = 'success';
}
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"">' + tag + '</span>';
}
return html;
});
@ -654,6 +672,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
if (tag) {
that.changeTag_(tag);
}
})
.on("contextmenu", function(d, e) {
d3.event.preventDefault();
var tag = this.getAttribute('data-tag');
if (tag) {
$(that).trigger({
'type': 'showTagMenu',
'tag': tag,
'clientX': d3.event.clientX,
'clientY': d3.event.clientY
});
}
});
// Ensure the tags are visible.

View file

@ -93,6 +93,15 @@ Email: my@email.com</pre>
</ul>
</div>
<h3>Deleting a tag <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">
A specific tag and all its images can be deleted by right clicking on the tag in the repository history tree and choosing "Delete Tag". This will delete the tag and any images <b>unique to it</b>. Images will not be deleted until all tags sharing them are deleted.
</div>
</div>
<a name="#post-hook"></a>
<h3>Using push webhooks <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">

View file

@ -1,3 +1,9 @@
<div id="tagContextMenu" class="dropdown clearfix" tabindex="-1">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
<li><a tabindex="-1" href="javascript:void(0)" ng-click="askDeleteTag(currentMenuTag)">Delete Tag</a></li>
</ul>
</div>
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
<div class="container repo">
<!-- Repo Header -->
@ -73,7 +79,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<!-- Tree View container -->
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-heading tag-heading">
<!-- Tag dropdown -->
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
@ -85,6 +91,9 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</ul>
</div>
<span class="right-title">Tags</span>
<span class="tag-controls" ng-show="repo.can_admin">
<a href="javascript:void(0)" ng-click="askDeleteTag(currentTag.name)">Delete Tag</a>
</span>
</div>
<!-- Image history tree -->
@ -185,3 +194,25 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteTagModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete tag <span class="label label-default tag">{{ tagToDelete }}</span>?</h4>
</div>
<div class="modal-body">
Are you sure you want to delete tag <span class="label label-default tag">{{ tagToDelete }}</span>?
<br><br>
Doing so will delete any images no attached to another tag.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="deleteTag(tagToDelete)">Delete Tag</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

Binary file not shown.