summaryrefslogtreecommitdiff
path: root/app/services/projects/container_repository/cleanup_tags_service.rb
blob: 793d2fec033428e801c17b254ed28b61077beb4d (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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# frozen_string_literal: true

module Projects
  module ContainerRepository
    class CleanupTagsService < BaseService
      def execute(container_repository)
        return error('access denied') unless can_destroy?
        return error('invalid regex') unless valid_regex?

        tags = container_repository.tags
        original_size = tags.size

        tags = without_latest(tags)
        tags = filter_by_name(tags)

        before_truncate_size = tags.size
        tags = truncate(tags)
        after_truncate_size = tags.size

        tags = filter_keep_n(tags)
        tags = filter_by_older_than(tags)

        delete_tags(container_repository, tags).tap do |result|
          result[:original_size] = original_size
          result[:before_truncate_size] = before_truncate_size
          result[:after_truncate_size] = after_truncate_size
          result[:before_delete_size] = tags.size
          result[:deleted_size] = result[:deleted]&.size

          result[:status] = :error if before_truncate_size != after_truncate_size
        end
      end

      private

      def delete_tags(container_repository, tags)
        return success(deleted: []) unless tags.any?

        tag_names = tags.map(&:name)

        service = Projects::ContainerRepository::DeleteTagsService.new(
          container_repository.project,
          current_user,
          tags: tag_names,
          container_expiration_policy: params['container_expiration_policy']
        )

        service.execute(container_repository)
      end

      def without_latest(tags)
        tags.reject(&:latest?)
      end

      def order_by_date(tags)
        now = DateTime.current
        tags.sort_by { |tag| tag.created_at || now }.reverse
      end

      def filter_by_name(tags)
        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
          regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
        end
      end

      def filter_keep_n(tags)
        return tags unless params['keep_n']

        tags = order_by_date(tags)
        tags.drop(keep_n)
      end

      def filter_by_older_than(tags)
        return tags unless params['older_than']

        older_than = ChronicDuration.parse(params['older_than']).seconds.ago

        tags.select do |tag|
          tag.created_at && tag.created_at < older_than
        end
      end

      def can_destroy?
        return true if params['container_expiration_policy']

        can?(current_user, :destroy_container_image, project)
      end

      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?
        end
        true
      rescue RegexpError => e
        ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
        false
      end

      def truncate(tags)
        return tags unless throttling_enabled?
        return tags if max_list_size == 0

        # truncate the list to make sure that after the #filter_keep_n
        # execution, the resulting list will be max_list_size
        truncated_size = max_list_size + keep_n

        return tags if tags.size <= truncated_size

        tags.sample(truncated_size)
      end

      def throttling_enabled?
        Feature.enabled?(:container_registry_expiration_policies_throttling)
      end

      def max_list_size
        ::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
      end

      def keep_n
        params['keep_n'].to_i
      end
    end
  end
end