diff options
Diffstat (limited to 'app/services/projects/container_repository/cleanup_tags_service.rb')
-rw-r--r-- | app/services/projects/container_repository/cleanup_tags_service.rb | 176 |
1 files changed, 121 insertions, 55 deletions
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index 793d2fec033..3a60de0f1ee 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -2,116 +2,152 @@ module Projects module ContainerRepository - class CleanupTagsService < BaseService - def execute(container_repository) + 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? - tags = container_repository.tags - original_size = tags.size + filter_out_latest + filter_by_name - tags = without_latest(tags) - tags = filter_by_name(tags) + truncate + populate_from_cache - before_truncate_size = tags.size - tags = truncate(tags) - after_truncate_size = tags.size + filter_keep_n + filter_by_older_than - 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 + delete_tags.merge(@counts).tap do |result| + result[:before_delete_size] = @tags.size result[:deleted_size] = result[:deleted]&.size - result[:status] = :error if before_truncate_size != after_truncate_size + result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size] end end private - def delete_tags(container_repository, tags) - return success(deleted: []) unless tags.any? - - tag_names = tags.map(&:name) + def delete_tags + return success(deleted: []) unless @tags.any? service = Projects::ContainerRepository::DeleteTagsService.new( - container_repository.project, - current_user, - tags: tag_names, - container_expiration_policy: params['container_expiration_policy'] + @project, + @current_user, + tags: @tags.map(&:name), + container_expiration_policy: container_expiration_policy ) - service.execute(container_repository) + service.execute(@container_repository) end - def without_latest(tags) - tags.reject(&:latest?) + def filter_out_latest + @tags.reject!(&:latest?) end - def order_by_date(tags) + def order_by_date now = DateTime.current - tags.sort_by { |tag| tag.created_at || now }.reverse + @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") + 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| + @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'] + def filter_keep_n + return unless keep_n - tags = order_by_date(tags) - tags.drop(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(tags) - return tags unless params['older_than'] + def filter_by_older_than + return unless older_than - older_than = ChronicDuration.parse(params['older_than']).seconds.ago + older_than_timestamp = older_than_in_seconds.ago - tags.select do |tag| - tag.created_at && tag.created_at < older_than + @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 params['container_expiration_policy'] + return true if container_expiration_policy - can?(current_user, :destroy_container_image, project) + 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] + 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) + ::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 + 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 + truncated_size = max_list_size + keep_n_as_integer - return tags if tags.size <= truncated_size + 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 + ::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository) + end + end - tags.sample(truncated_size) + def caching_enabled? + container_expiration_policy && + older_than.present? && + Feature.enabled?(:container_registry_expiration_policies_caching, @project) end def throttling_enabled? @@ -123,7 +159,37 @@ module Projects end def keep_n - params['keep_n'].to_i + @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 |