diff options
Diffstat (limited to 'app/services/projects/container_repository')
4 files changed, 111 insertions, 71 deletions
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index c5809c11ea9..204a54ff23a 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -39,11 +39,8 @@ module Projects end def filter_by_name(tags) - # Technical Debt: https://gitlab.com/gitlab-org/gitlab/issues/207267 - # name_regex to be removed when container_expiration_policies is updated - # to have both regex columns - regex_delete = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z") - regex_retain = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z") + regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z") + regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z") tags.select do |tag| # regex_retain will override any overlapping matches by regex_delete @@ -81,11 +78,11 @@ module Projects def valid_regex? %w(name_regex_delete name_regex name_regex_keep).each do |param_name| regex = params[param_name] - Gitlab::UntrustedRegexp.new(regex) unless regex.blank? + ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank? end true rescue RegexpError => e - Gitlab::ErrorTracking.log_exception(e, project_id: project.id) + ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id) false end end diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index 5d4059710bb..a23a6a369b2 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -6,65 +6,35 @@ module Projects LOG_DATA_BASE = { service_class: self.to_s }.freeze def execute(container_repository) + @container_repository = container_repository return error('access denied') unless can?(current_user, :destroy_container_image, project) - tag_names = params[:tags] - return error('not tags specified') if tag_names.blank? + @tag_names = params[:tags] + return error('not tags specified') if @tag_names.blank? - smart_delete(container_repository, tag_names) + delete_tags end private - # Delete tags by name with a single DELETE request. This is only supported - # by the GitLab Container Registry fork. See - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details. - def fast_delete(container_repository, tag_names) - deleted_tags = tag_names.select do |name| - container_repository.delete_tag_by_name(name) - end - - deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags') + def delete_tags + delete_service.execute + .tap(&method(:log_response)) end - # Replace a tag on the registry with a dummy tag. - # This is a hack as the registry doesn't support deleting individual - # tags. This code effectively pushes a dummy image and assigns the tag to it. - # This way when the tag is deleted only the dummy image is affected. - # This is used to preverse compatibility with third-party registries that - # don't support fast delete. - # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion - def slow_delete(container_repository, tag_names) - # generates the blobs for the dummy image - dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path) - return error('could not generate manifest') if dummy_manifest.nil? - - deleted_tags = replace_tag_manifests(container_repository, dummy_manifest, tag_names) + def delete_service + fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) - # Deletes the dummy image - # All created tag digests are the same since they all have the same dummy image. - # a single delete is sufficient to remove all tags with it - if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.each_value.first) - success(deleted: deleted_tags.keys) + if fast_delete_enabled && @container_repository.client.supports_tag_delete? + ::Projects::ContainerRepository::Gitlab::DeleteTagsService.new(@container_repository, @tag_names) else - error('could not delete tags') + ::Projects::ContainerRepository::ThirdParty::DeleteTagsService.new(@container_repository, @tag_names) end end - def smart_delete(container_repository, tag_names) - fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) - response = if fast_delete_enabled && container_repository.client.supports_tag_delete? - fast_delete(container_repository, tag_names) - else - slow_delete(container_repository, tag_names) - end - - response.tap { |r| log_response(r, container_repository) } - end - - def log_response(response, container_repository) + def log_response(response) log_data = LOG_DATA_BASE.merge( - container_repository_id: container_repository.id, + container_repository_id: @container_repository.id, message: 'deleted tags' ) @@ -76,26 +46,6 @@ module Projects log_error(log_data) end end - - # update the manifests of the tags with the new dummy image - def replace_tag_manifests(container_repository, dummy_manifest, tag_names) - deleted_tags = {} - - tag_names.each do |name| - digest = container_repository.client.put_tag(container_repository.path, name, dummy_manifest) - next unless digest - - deleted_tags[name] = digest - end - - # make sure the digests are the same (it should always be) - digests = deleted_tags.values.uniq - - # rubocop: disable CodeReuse/ActiveRecord - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many? - - deleted_tags - end end end end diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb new file mode 100644 index 00000000000..18049648e26 --- /dev/null +++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + module Gitlab + class DeleteTagsService + include BaseServiceUtility + + def initialize(container_repository, tag_names) + @container_repository = container_repository + @tag_names = tag_names + end + + # Delete tags by name with a single DELETE request. This is only supported + # by the GitLab Container Registry fork. See + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details. + def execute + return success(deleted: []) if @tag_names.empty? + + deleted_tags = @tag_names.select do |name| + @container_repository.delete_tag_by_name(name) + end + + deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags') + end + end + end + end +end diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb new file mode 100644 index 00000000000..6504172109e --- /dev/null +++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + module ThirdParty + class DeleteTagsService + include BaseServiceUtility + + def initialize(container_repository, tag_names) + @container_repository = container_repository + @tag_names = tag_names + end + + # Replace a tag on the registry with a dummy tag. + # This is a hack as the registry doesn't support deleting individual + # tags. This code effectively pushes a dummy image and assigns the tag to it. + # This way when the tag is deleted only the dummy image is affected. + # This is used to preverse compatibility with third-party registries that + # don't support fast delete. + # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion + def execute + return success(deleted: []) if @tag_names.empty? + + # generates the blobs for the dummy image + dummy_manifest = @container_repository.client.generate_empty_manifest(@container_repository.path) + return error('could not generate manifest') if dummy_manifest.nil? + + deleted_tags = replace_tag_manifests(dummy_manifest) + + # Deletes the dummy image + # All created tag digests are the same since they all have the same dummy image. + # a single delete is sufficient to remove all tags with it + if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first) + success(deleted: deleted_tags.keys) + else + error('could not delete tags') + end + end + + private + + # update the manifests of the tags with the new dummy image + def replace_tag_manifests(dummy_manifest) + deleted_tags = {} + + @tag_names.each do |name| + digest = @container_repository.client.put_tag(@container_repository.path, name, dummy_manifest) + next unless digest + + deleted_tags[name] = digest + end + + # make sure the digests are the same (it should always be) + digests = deleted_tags.values.uniq + + # rubocop: disable CodeReuse/ActiveRecord + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many? + + deleted_tags + end + end + end + end +end |