summaryrefslogtreecommitdiff
path: root/app/services/projects/container_repository/third_party/delete_tags_service.rb
blob: 942df177bea89e1f2d98354ee5ae42544dad1873 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 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 preserve 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: #{@tag_names.join(', ')}".truncate(1000))
          end
        end

        private

        # update the manifests of the tags with the new dummy image
        def replace_tag_manifests(dummy_manifest)
          deleted_tags = @tag_names.map do |name|
            digest = @container_repository.client.put_tag(@container_repository.path, name, dummy_manifest)
            next unless digest

            [name, digest]
          end.compact.to_h

          # 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