-
Notifications
You must be signed in to change notification settings - Fork 472
Added support for removing images/tags #854
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,11 @@ | ||
.tags .label.label-success { | ||
margin: 0px 2px; | ||
} | ||
|
||
#remove-repo button { | ||
padding: 0px 10px 0px 0px; | ||
} | ||
|
||
.remove-repo:focus, .remove-repo:hover, .remove-tag:focus, .remove-tag:hover { | ||
text-decoration: none; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# DeleteEnabeld redirects the user back if delete support is not enabled. A | ||
# `before_action` will be created for the :destroy method. | ||
module DeleteEnabled | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is some convention to name concerns like polymorph activerecord relations ending with |
||
extend ActiveSupport::Concern | ||
|
||
included do | ||
before_action :delete_enabled?, only: [:destroy] | ||
end | ||
|
||
# Returns true if users can delete images/tags. | ||
def delete_enabled? | ||
redirect_to :back, status: :forbidden unless APP_CONFIG.enabled?("delete") | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
class RepositoriesController < ApplicationController | ||
before_action :set_repository, only: [:show, :toggle_star] | ||
include DeleteEnabled | ||
|
||
before_action :set_repository, only: [:show, :destroy, :toggle_star] | ||
|
||
# GET /repositories | ||
# GET /repositories.json | ||
|
@@ -23,6 +25,28 @@ def toggle_star | |
render template: "repositories/star", locals: { user: current_user } | ||
end | ||
|
||
# Removes all the tags that belong to this repository, and removes it. | ||
def destroy | ||
# First of all we mark the repo and the tags, so we don't have concurrency | ||
# problems when "delete" events come in. | ||
@repository.tags.update_all(marked: true) | ||
@repository.update_attributes(marked: true) | ||
|
||
# Remove all tags, effectively removing them from the registry too. | ||
@repository.groupped_tags.map { |t| t.first.delete_by_digest!(current_user) } | ||
|
||
# Delete this repository if all tags were successfully deleted. | ||
if @repository.reload.tags.any? | ||
redirect_to repository_path(@repository), alert: "Could not remove all the tags" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be useful to dump more information into Rails' log (like which tags could not have been deleted and eventually a reason). Otherwise an admin will have a hard time figuring this out on his own. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is logged, because it ends up calling |
||
else | ||
@repository.delete_and_update!(current_user) | ||
redirect_to namespace_path(@repository.namespace), | ||
notice: "Repository removed with all its tags" | ||
end | ||
end | ||
|
||
protected | ||
|
||
def set_repository | ||
@repository = Repository.find(params[:id]) | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
class TagsController < ApplicationController | ||
include DeleteEnabled | ||
|
||
# Removes all tags the match the digest of the tag with the given ID. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
# Moreover, it will also remove the image if it's left empty after removing | ||
# the tags. | ||
def destroy | ||
tag = Tag.find(params[:id]) | ||
|
||
# And now remove the tag by the digest. If the repository containing said | ||
# tags becomes empty after that, remove it too. | ||
repo = tag.repository | ||
if tag.delete_by_digest!(current_user) | ||
if repo.tags.empty? | ||
repo.delete_and_update!(current_user) | ||
redirect_to namespace_path(repo.namespace), notice: "Image removed with all its tags" | ||
else | ||
redirect_to repository_path(tag.repository), notice: "Tag removed successfully" | ||
end | ||
else | ||
redirect_to repository_path(tag.repository), alert: "Tag could not be removed" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is something logged also in this case, can the admin get a clue about why this failed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,8 @@ | |
# updated_at :datetime not null | ||
# team_id :integer | ||
# public :boolean default("0") | ||
# registry_id :integer not null | ||
# registry_id :integer | ||
# global :boolean default("0") | ||
# description :text(65535) | ||
# | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we really get rid of this field on the db? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leftover... |
||
# Indexes | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
# namespace_id :integer | ||
# created_at :datetime not null | ||
# updated_at :datetime not null | ||
# marked :boolean default("0") | ||
# | ||
# Indexes | ||
# | ||
|
@@ -59,6 +60,33 @@ def groupped_tags | |
end | ||
end | ||
|
||
# Updates the activities related to this repository and adds a new activity | ||
# regarding the removal of this. | ||
def delete_and_update!(actor) | ||
logger.tagged("catalog") { logger.info "Removed the image '#{name}'." } | ||
|
||
# Take care of current activities. | ||
PublicActivity::Activity.where(trackable: self).update_all( | ||
trackable_type: Namespace, | ||
trackable_id: namespace.id, | ||
recipient_type: nil | ||
) | ||
|
||
# Add a "delete" activity" | ||
namespace.create_activity( | ||
:delete, | ||
owner: actor, | ||
recipient: self, | ||
parameters: { | ||
repository_name: name, | ||
namespace_id: namespace.id, | ||
namespace_name: namespace.clean_name | ||
} | ||
) | ||
|
||
destroy | ||
end | ||
|
||
# Handle a push event from the registry. | ||
def self.handle_push_event(event) | ||
registry = Registry.find_from_event(event) | ||
|
@@ -82,11 +110,15 @@ def self.handle_delete_event(event) | |
# Fetch the repo. | ||
ns, repo_name, = registry.get_namespace_from_event(event, false) | ||
repo = ns.repositories.find_by(name: repo_name) | ||
return if repo.nil? | ||
return if repo.nil? || repo.marked? | ||
|
||
# Destroy tags and the repository if it's empty now. | ||
repo.tags.where(digest: event["target"]["digest"]).map(&:delete_and_update!) | ||
repo.destroy if !repo.nil? && repo.tags.empty? | ||
user = User.find_from_event(event) | ||
repo.tags.where(digest: event["target"]["digest"], marked: false).map do |t| | ||
t.delete_and_update!(user) | ||
end | ||
repo = repo.reload | ||
repo.delete_and_update!(user) if !repo.nil? && repo.tags.empty? | ||
end | ||
|
||
# Add the repository with the given `repo` name and the given `tag`. The | ||
|
@@ -169,7 +201,7 @@ def self.create_or_update!(repo) | |
end | ||
|
||
# Finally remove the tags that are left and return the repo. | ||
repository.tags.where(name: to_be_deleted_tags).find_each(&:delete_and_update!) | ||
repository.tags.where(name: to_be_deleted_tags).find_each { |t| t.delete_and_update!(portus) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will mark the deletion event as to be generated by the How is that happening? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because there are two paths when it comes to deleting a repo/tag:
On the second case, we remove images on the registry, and if we are successful we remove them from the DB. The problem with that is that by "remove images on the registry" I mean that there will be a delete notification for this same repository/tag 😄. This is why I've added a This line of code you're pointing at refers to the first case: a delete notification. Since we don't know who performed the action, we set "portus" there. It's a similar situation as when crono creates repositories in the DB when Portus was not properly sync'ed by the web notification. |
||
repository.reload | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
# user_id :integer | ||
# digest :string(255) | ||
# image_id :string(255) default("") | ||
# marked :boolean default("0") | ||
# | ||
# Indexes | ||
# | ||
|
@@ -29,9 +30,74 @@ class Tag < ActiveRecord::Base | |
# and that's guaranteed to have a good format. | ||
validates :name, uniqueness: { scope: "repository_id" } | ||
|
||
# Delete all the tags that match the given digest. Call this method if you | ||
# want to: | ||
# | ||
# - Safely remove tags (with its re-tags) on the DB. | ||
# - Remove the manifest digest on the registry. | ||
# - Preserve the activities related to the tags that are to be removed. | ||
# | ||
# Returns true on success, false otherwise. | ||
def delete_by_digest!(actor) | ||
dig = fetch_digest | ||
return false if dig.blank? | ||
|
||
Tag.where(digest: dig).update_all(marked: true) | ||
|
||
begin | ||
Registry.get.client.delete(repository.name, dig, "manifests") | ||
rescue StandardError => e | ||
Rails.logger.error "Could not delete tag on the registry: #{e.message}" | ||
return false | ||
end | ||
|
||
Tag.where(digest: dig).map { |t| t.delete_and_update!(actor) } | ||
end | ||
|
||
# Delete this tag and update its activity. | ||
def delete_and_update! | ||
def delete_and_update!(actor) | ||
logger.tagged("catalog") { logger.info "Removed the tag '#{name}'." } | ||
|
||
# If the tag is no longer there, ignore this call and return early. | ||
unless Tag.find_by(id: id) | ||
logger.tagged("catalog") { logger.info "Ignoring..." } | ||
return | ||
end | ||
|
||
# Delete tag and create the corresponding activities. | ||
destroy | ||
create_delete_activities!(actor) | ||
end | ||
|
||
protected | ||
|
||
# Fetch the digest for this tag. Usually the digest should already be | ||
# initialized since it's provided by the event notification that created this | ||
# tag. However, it might happen that the digest column is left blank (e.g. | ||
# legacy Portus, unknown error, etc). In these cases, this method will fetch | ||
# the manifest from the registry. | ||
# | ||
# Returns a string containing the digest on success. Otherwise it returns | ||
# nil. | ||
def fetch_digest | ||
if digest.blank? | ||
client = Registry.get.client | ||
|
||
begin | ||
_, dig, = client.manifest(repository.name, name) | ||
update_attributes(digest: dig) | ||
dig | ||
rescue StandardError => e | ||
Rails.logger.error "Could not fetch manifest digest: #{e.message}" | ||
nil | ||
end | ||
else | ||
digest | ||
end | ||
end | ||
|
||
# Create/update the activities for a delete operation. | ||
def create_delete_activities!(actor) | ||
PublicActivity::Activity.where(recipient: self).update_all( | ||
parameters: { | ||
namespace_id: repository.namespace.id, | ||
|
@@ -40,6 +106,18 @@ def delete_and_update! | |
tag_name: name | ||
} | ||
) | ||
destroy | ||
|
||
# Create the delete activity. | ||
repository.create_activity( | ||
:delete, | ||
owner: actor, | ||
recipient: self, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, because the final render will pick up the stuff stored in the parameters. The thing here is that we rely on the |
||
parameters: { | ||
repository_name: repository.name, | ||
namespace_id: repository.namespace.id, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can change in the future because I expect users will request to be able to delete also a namespace and all their contents. Deleting a namespace would cause this activity to break. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, whenever we propose removing namespaces, we would have to work around it in the same way I've done for removed images/tags: put the info in the parameters hash and update the recipient/tracker to point to a parent (e.g. in this case the namespace containing said image). |
||
namespace_name: repository.namespace.clean_name, | ||
tag_name: name | ||
} | ||
) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, should read
DeleteEnabled
.