summaryrefslogtreecommitdiff
path: root/app/services/projects/container_repository/cleanup_tags_service.rb
blob: 72f3fddb4c302b4bea40b062f4125f7b91337105 (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# frozen_string_literal: true

module Projects
  module ContainerRepository
    class CleanupTagsService
      include BaseServiceUtility
      include ::Gitlab::Utils::StrongMemoize

      def initialize(container_repository, user = nil, params = {})
        @container_repository = container_repository
        @current_user = user
        @params = params.dup

        @project = container_repository.project
        @tags = container_repository.tags
        tags_size = @tags.size
        @counts = {
          original_size: tags_size,
          cached_tags_count: 0
        }
      end

      def execute
        return error('access denied') unless can_destroy?
        return error('invalid regex') unless valid_regex?

        filter_out_latest
        filter_by_name

        truncate
        populate_from_cache

        filter_keep_n
        filter_by_older_than

        delete_tags.merge(@counts).tap do |result|
          result[:before_delete_size] = @tags.size
          result[:deleted_size] = result[:deleted]&.size

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

      private

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

        service = Projects::ContainerRepository::DeleteTagsService.new(
          @project,
          @current_user,
          tags: @tags.map(&:name),
          container_expiration_policy: container_expiration_policy
        )

        service.execute(@container_repository)
      end

      def filter_out_latest
        @tags.reject!(&:latest?)
      end

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

      def filter_by_name
        regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_delete || name_regex}\\z")
        regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{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
        return unless keep_n

        order_by_date
        cache_tags(@tags.first(keep_n_as_integer))
        @tags = @tags.drop(keep_n_as_integer)
      end

      def filter_by_older_than
        return unless older_than

        older_than_timestamp = older_than_in_seconds.ago

        @tags, tags_to_keep = @tags.partition do |tag|
          tag.created_at && tag.created_at < older_than_timestamp
        end

        cache_tags(tags_to_keep)
      end

      def can_destroy?
        return true if 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
        @counts[:before_truncate_size] = @tags.size
        @counts[:after_truncate_size] = @tags.size

        return unless throttling_enabled?
        return 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_as_integer

        return if @tags.size <= truncated_size

        @tags = @tags.sample(truncated_size)
        @counts[:after_truncate_size] = @tags.size
      end

      def populate_from_cache
        @counts[:cached_tags_count] = cache.populate(@tags) if caching_enabled?
      end

      def cache_tags(tags)
        cache.insert(tags, older_than_in_seconds) if caching_enabled?
      end

      def cache
        strong_memoize(:cache) do
          ::Gitlab::ContainerRepository::Tags::Cache.new(@container_repository)
        end
      end

      def caching_enabled?
        result = ::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_caching &&
                 container_expiration_policy &&
                 older_than.present?
        !!result
      end

      def throttling_enabled?
        Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
      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']
      end

      def keep_n_as_integer
        keep_n.to_i
      end

      def older_than_in_seconds
        strong_memoize(:older_than_in_seconds) do
          ChronicDuration.parse(older_than).seconds
        end
      end

      def older_than
        @params['older_than']
      end

      def name_regex_delete
        @params['name_regex_delete']
      end

      def name_regex
        @params['name_regex']
      end

      def name_regex_keep
        @params['name_regex_keep']
      end

      def container_expiration_policy
        @params['container_expiration_policy']
      end
    end
  end
end