2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-07-07 11:18:12 +05:30
class ContainerRepository < ApplicationRecord
2019-03-02 22:35:43 +05:30
include Gitlab :: Utils :: StrongMemoize
2020-05-24 23:13:21 +05:30
include Gitlab :: SQL :: Pattern
2021-01-29 00:20:46 +05:30
include EachBatch
2021-03-11 19:13:27 +05:30
include Sortable
2022-04-04 11:22:00 +05:30
include AfterCommitQueue
2021-01-29 00:20:46 +05:30
WAITING_CLEANUP_STATUSES = % i [ cleanup_scheduled cleanup_unfinished ] . freeze
2021-06-08 01:23:25 +05:30
REQUIRING_CLEANUP_STATUSES = % i [ cleanup_unscheduled cleanup_scheduled ] . freeze
2022-06-21 17:19:12 +05:30
2022-04-04 11:22:00 +05:30
IDLE_MIGRATION_STATES = %w[ default pre_import_done import_done import_aborted import_skipped ] . freeze
ACTIVE_MIGRATION_STATES = %w[ pre_importing importing ] . freeze
MIGRATION_STATES = ( IDLE_MIGRATION_STATES + ACTIVE_MIGRATION_STATES ) . freeze
2022-06-21 17:19:12 +05:30
ABORTABLE_MIGRATION_STATES = ( ACTIVE_MIGRATION_STATES + %w[ pre_import_done default ] ) . freeze
SKIPPABLE_MIGRATION_STATES = ( ABORTABLE_MIGRATION_STATES + %w[ import_aborted ] ) . freeze
2022-04-04 11:22:00 +05:30
2022-05-07 20:08:51 +05:30
MIGRATION_PHASE_1_STARTED_AT = Date . new ( 2021 , 11 , 4 ) . freeze
2022-06-21 17:19:12 +05:30
MIGRATION_PHASE_1_ENDED_AT = Date . new ( 2022 , 01 , 23 ) . freeze
2022-05-07 20:08:51 +05:30
2022-08-27 11:52:29 +05:30
MAX_TAGS_PAGES = 2000
2022-04-04 11:22:00 +05:30
TooManyImportsError = Class . new ( StandardError )
2019-03-02 22:35:43 +05:30
2017-08-17 22:00:37 +05:30
belongs_to :project
validates :name , length : { minimum : 0 , allow_nil : false }
validates :name , uniqueness : { scope : :project_id }
2022-04-04 11:22:00 +05:30
validates :migration_state , presence : true , inclusion : { in : MIGRATION_STATES }
validates :migration_aborted_in_state , inclusion : { in : ABORTABLE_MIGRATION_STATES } , allow_nil : true
2022-03-02 08:16:31 +05:30
validates :migration_retries_count , presence : true ,
numericality : { greater_than_or_equal_to : 0 } ,
allow_nil : false
2017-08-17 22:00:37 +05:30
2020-04-22 19:07:51 +05:30
enum status : { delete_scheduled : 0 , delete_failed : 1 }
2021-01-29 00:20:46 +05:30
enum expiration_policy_cleanup_status : { cleanup_unscheduled : 0 , cleanup_scheduled : 1 , cleanup_unfinished : 2 , cleanup_ongoing : 3 }
2022-06-21 17:19:12 +05:30
enum migration_skipped_reason : {
not_in_plan : 0 ,
too_many_retries : 1 ,
too_many_tags : 2 ,
root_namespace_in_deny_list : 3 ,
migration_canceled : 4 ,
not_found : 5 ,
native_import : 6 ,
2022-07-16 23:28:13 +05:30
migration_forced_canceled : 7 ,
migration_canceled_by_registry : 8
2022-06-21 17:19:12 +05:30
}
2020-04-22 19:07:51 +05:30
2022-04-04 11:22:00 +05:30
delegate :client , :gitlab_api_client , to : :registry
2017-08-17 22:00:37 +05:30
2019-03-02 22:35:43 +05:30
scope :ordered , - > { order ( :name ) }
2019-12-26 22:10:19 +05:30
scope :with_api_entity_associations , - > { preload ( project : [ :route , { namespace : :route } ] ) }
scope :for_group_and_its_subgroups , - > ( group ) do
2020-06-23 00:09:42 +05:30
project_scope = Project
. for_group_and_its_subgroups ( group )
2021-09-30 23:02:18 +05:30
. with_feature_enabled ( :container_registry )
. select ( :id )
2020-06-23 00:09:42 +05:30
2021-02-22 17:27:13 +05:30
joins ( " INNER JOIN ( #{ project_scope . to_sql } ) projects on projects.id=container_repositories.project_id " )
2019-12-26 22:10:19 +05:30
end
2021-01-29 00:20:46 +05:30
scope :for_project_id , - > ( project_id ) { where ( project_id : project_id ) }
2020-05-24 23:13:21 +05:30
scope :search_by_name , - > ( query ) { fuzzy_search ( query , [ :name ] , use_minimum_char_limit : false ) }
2021-01-29 00:20:46 +05:30
scope :waiting_for_cleanup , - > { where ( expiration_policy_cleanup_status : WAITING_CLEANUP_STATUSES ) }
2021-06-08 01:23:25 +05:30
scope :expiration_policy_started_at_nil_or_before , - > ( timestamp ) { where ( 'expiration_policy_started_at < ? OR expiration_policy_started_at IS NULL' , timestamp ) }
2022-04-04 11:22:00 +05:30
scope :with_migration_import_started_at_nil_or_before , - > ( timestamp ) { where ( " COALESCE(migration_import_started_at, '01-01-1970') < ? " , timestamp ) }
scope :with_migration_pre_import_started_at_nil_or_before , - > ( timestamp ) { where ( " COALESCE(migration_pre_import_started_at, '01-01-1970') < ? " , timestamp ) }
scope :with_migration_pre_import_done_at_nil_or_before , - > ( timestamp ) { where ( " COALESCE(migration_pre_import_done_at, '01-01-1970') < ? " , timestamp ) }
2021-09-04 01:27:46 +05:30
scope :with_stale_ongoing_cleanup , - > ( threshold ) { cleanup_ongoing . where ( 'expiration_policy_started_at < ?' , threshold ) }
2022-04-04 11:22:00 +05:30
scope :import_in_process , - > { where ( migration_state : %w[ pre_importing pre_import_done importing ] ) }
scope :recently_done_migration_step , - > do
2022-06-21 17:19:12 +05:30
where ( migration_state : %w[ import_done pre_import_done import_aborted import_skipped ] )
. order ( Arel . sql ( 'GREATEST(migration_pre_import_done_at, migration_import_done_at, migration_aborted_at, migration_skipped_at) DESC' ) )
2022-04-04 11:22:00 +05:30
end
scope :ready_for_import , - > do
# There is no yaml file for the container_registry_phase_2_deny_list
# feature flag since it is only accessed in this query.
# https://gitlab.com/gitlab-org/gitlab/-/issues/350543 tracks the rollout and
# removal of this feature flag.
2022-05-07 20:08:51 +05:30
joins ( project : [ :namespace ] ) . where (
2022-04-04 11:22:00 +05:30
migration_state : [ :default ] ,
created_at : ... ContainerRegistry :: Migration . created_before
) . with_target_import_tier
. where (
" NOT EXISTS (
SELECT 1
FROM feature_gates
WHERE feature_gates . feature_key = 'container_registry_phase_2_deny_list'
AND feature_gates . key = 'actors'
2022-05-07 20:08:51 +05:30
AND feature_gates . value = concat ( 'Group:' , namespaces . traversal_ids [ 1 ] )
2022-04-04 11:22:00 +05:30
) "
)
end
state_machine :migration_state , initial : :default , use_transactions : false do
state :pre_importing do
validates :migration_pre_import_started_at , presence : true
validates :migration_pre_import_done_at , presence : false
end
state :pre_import_done do
validates :migration_pre_import_done_at , presence : true
end
state :importing do
validates :migration_import_started_at , presence : true
validates :migration_import_done_at , presence : false
end
state :import_done
state :import_skipped do
validates :migration_skipped_reason ,
:migration_skipped_at ,
presence : true
end
state :import_aborted do
validates :migration_aborted_at , presence : true
validates :migration_retries_count , presence : true , numericality : { greater_than_or_equal_to : 1 }
end
event :start_pre_import do
2022-06-21 17:19:12 +05:30
transition % i [ default pre_importing importing import_aborted ] = > :pre_importing
2022-04-04 11:22:00 +05:30
end
event :finish_pre_import do
2022-06-21 17:19:12 +05:30
transition % i [ pre_importing importing import_aborted ] = > :pre_import_done
2022-04-04 11:22:00 +05:30
end
event :start_import do
2022-06-21 17:19:12 +05:30
transition % i [ pre_import_done pre_importing importing import_aborted ] = > :importing
2022-04-04 11:22:00 +05:30
end
event :finish_import do
2022-06-21 17:19:12 +05:30
transition % i [ default pre_importing importing import_aborted ] = > :import_done
2022-04-04 11:22:00 +05:30
end
event :already_migrated do
transition default : :import_done
end
event :abort_import do
transition ABORTABLE_MIGRATION_STATES . map ( & :to_sym ) = > :import_aborted
end
event :skip_import do
2022-06-21 17:19:12 +05:30
transition SKIPPABLE_MIGRATION_STATES . map ( & :to_sym ) = > :import_skipped
2022-04-04 11:22:00 +05:30
end
event :retry_pre_import do
2022-06-21 17:19:12 +05:30
transition % i [ pre_importing importing import_aborted ] = > :pre_importing
2022-04-04 11:22:00 +05:30
end
event :retry_import do
2022-06-21 17:19:12 +05:30
transition % i [ pre_importing importing import_aborted ] = > :importing
2022-04-04 11:22:00 +05:30
end
before_transition any = > :pre_importing do | container_repository |
container_repository . migration_pre_import_started_at = Time . zone . now
container_repository . migration_pre_import_done_at = nil
end
2022-06-21 17:19:12 +05:30
after_transition any = > :pre_importing do | container_repository , transition |
forced = transition . args . first . try ( :[] , :forced )
next if forced
2022-04-04 11:22:00 +05:30
container_repository . try_import do
container_repository . migration_pre_import
end
end
2022-06-21 17:19:12 +05:30
before_transition any = > :pre_import_done do | container_repository |
2022-04-04 11:22:00 +05:30
container_repository . migration_pre_import_done_at = Time . zone . now
end
before_transition any = > :importing do | container_repository |
container_repository . migration_import_started_at = Time . zone . now
container_repository . migration_import_done_at = nil
end
2022-06-21 17:19:12 +05:30
after_transition any = > :importing do | container_repository , transition |
forced = transition . args . first . try ( :[] , :forced )
next if forced
2022-04-04 11:22:00 +05:30
container_repository . try_import do
container_repository . migration_import
end
end
2022-06-21 17:19:12 +05:30
before_transition any = > :import_done do | container_repository |
2022-04-04 11:22:00 +05:30
container_repository . migration_import_done_at = Time . zone . now
end
before_transition any = > :import_aborted do | container_repository |
container_repository . migration_aborted_in_state = container_repository . migration_state
container_repository . migration_aborted_at = Time . zone . now
container_repository . migration_retries_count += 1
end
2022-06-21 17:19:12 +05:30
after_transition any = > :import_aborted do | container_repository |
if container_repository . retried_too_many_times?
container_repository . skip_import ( reason : :too_many_retries )
end
end
2022-04-04 11:22:00 +05:30
before_transition import_aborted : any do | container_repository |
container_repository . migration_aborted_at = nil
container_repository . migration_aborted_in_state = nil
end
before_transition any = > :import_skipped do | container_repository |
container_repository . migration_skipped_at = Time . zone . now
end
2022-07-16 23:28:13 +05:30
before_transition any = > % i [ import_done import_aborted import_skipped ] do | container_repository |
2022-04-04 11:22:00 +05:30
container_repository . run_after_commit do
2022-07-16 23:28:13 +05:30
:: ContainerRegistry :: Migration :: EnqueuerWorker . enqueue_a_job
2022-04-04 11:22:00 +05:30
end
end
end
2019-03-02 22:35:43 +05:30
2020-04-22 19:07:51 +05:30
def self . exists_by_path? ( path )
where (
project : path . repository_project ,
name : path . repository_name
) . exists?
end
2022-06-21 17:19:12 +05:30
def self . all_migrated?
# check that the set of non migrated repositories is empty
where ( created_at : ... MIGRATION_PHASE_1_ENDED_AT )
. where . not ( migration_state : 'import_done' )
. empty?
end
2021-06-08 01:23:25 +05:30
def self . with_enabled_policy
2021-09-04 01:27:46 +05:30
joins ( 'INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id' )
2021-06-08 01:23:25 +05:30
. where ( container_expiration_policies : { enabled : true } )
end
def self . requiring_cleanup
2021-09-04 01:27:46 +05:30
with_enabled_policy
. where ( container_repositories : { expiration_policy_cleanup_status : REQUIRING_CLEANUP_STATUSES } )
. where ( 'container_repositories.expiration_policy_started_at IS NULL OR container_repositories.expiration_policy_started_at < container_expiration_policies.next_run_at' )
. where ( 'container_expiration_policies.next_run_at < ?' , Time . zone . now )
2021-06-08 01:23:25 +05:30
end
def self . with_unfinished_cleanup
with_enabled_policy . cleanup_unfinished
end
2022-04-04 11:22:00 +05:30
def self . with_stale_migration ( before_timestamp )
stale_pre_importing = with_migration_states ( :pre_importing )
. with_migration_pre_import_started_at_nil_or_before ( before_timestamp )
stale_pre_import_done = with_migration_states ( :pre_import_done )
. with_migration_pre_import_done_at_nil_or_before ( before_timestamp )
stale_importing = with_migration_states ( :importing )
. with_migration_import_started_at_nil_or_before ( before_timestamp )
union = :: Gitlab :: SQL :: Union . new ( [
stale_pre_importing ,
stale_pre_import_done ,
stale_importing
] )
from ( " ( #{ union . to_sql } ) #{ ContainerRepository . table_name } " )
end
def self . with_target_import_tier
# overridden in ee
#
# Repositories are being migrated by tier on Saas, so we need to
# filter by plan/subscription which is not available in FOSS
all
end
def skip_import ( reason : )
self . migration_skipped_reason = reason
super
end
2022-06-21 17:19:12 +05:30
def start_pre_import ( * args )
2022-04-04 11:22:00 +05:30
return false unless ContainerRegistry :: Migration . enabled?
2022-06-21 17:19:12 +05:30
super ( * args )
2022-04-04 11:22:00 +05:30
end
def retry_pre_import
return false unless ContainerRegistry :: Migration . enabled?
super
end
def retry_import
return false unless ContainerRegistry :: Migration . enabled?
super
end
def finish_pre_import_and_start_import
# nothing to do between those two transitions for now.
finish_pre_import && start_import
end
def retry_aborted_migration
return unless migration_state == 'import_aborted'
2022-06-21 17:19:12 +05:30
reconcile_import_status ( external_import_status ) do
# If the import_status request fails, use the timestamp to guess current state
migration_pre_import_done_at ? retry_import : retry_pre_import
end
end
def reconcile_import_status ( status )
case status
2022-04-04 11:22:00 +05:30
when 'native'
2022-06-21 17:19:12 +05:30
finish_import_as ( :native_import )
when 'pre_import_in_progress'
return if pre_importing?
start_pre_import ( forced : true )
2022-04-04 11:22:00 +05:30
when 'import_in_progress'
2022-06-21 17:19:12 +05:30
return if importing?
start_import ( forced : true )
2022-04-04 11:22:00 +05:30
when 'import_complete'
finish_import
2022-07-16 23:28:13 +05:30
when 'import_failed' , 'import_canceled'
2022-04-04 11:22:00 +05:30
retry_import
when 'pre_import_complete'
finish_pre_import_and_start_import
2022-07-16 23:28:13 +05:30
when 'pre_import_failed' , 'pre_import_canceled'
2022-04-04 11:22:00 +05:30
retry_pre_import
else
2022-06-21 17:19:12 +05:30
yield
2022-04-04 11:22:00 +05:30
end
end
def try_import
raise ArgumentError , 'block not given' unless block_given?
try_count = 0
begin
try_count += 1
2022-06-21 17:19:12 +05:30
case yield
when :ok
return true
when :not_found
finish_import_as ( :not_found )
when :already_imported
finish_import_as ( :native_import )
else
abort_import
end
2022-04-04 11:22:00 +05:30
false
rescue TooManyImportsError
if try_count < = :: ContainerRegistry :: Migration . start_max_retries
sleep 0 . 1 * try_count
retry
else
abort_import
false
end
end
end
2022-06-21 17:19:12 +05:30
def retried_too_many_times?
migration_retries_count > = ContainerRegistry :: Migration . max_retries
end
2022-07-16 23:28:13 +05:30
def nearing_or_exceeded_retry_limit?
migration_retries_count > = ContainerRegistry :: Migration . max_retries - 1
end
2022-08-27 11:52:29 +05:30
def migrated?
MIGRATION_PHASE_1_ENDED_AT < self . created_at || import_done?
end
2022-04-04 11:22:00 +05:30
def last_import_step_done_at
2022-06-21 17:19:12 +05:30
[ migration_pre_import_done_at , migration_import_done_at , migration_aborted_at , migration_skipped_at ] . compact . max
2022-04-04 11:22:00 +05:30
end
def external_import_status
strong_memoize ( :import_status ) do
gitlab_api_client . import_status ( self . path )
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
def registry
@registry || = begin
token = Auth :: ContainerRegistryAuthenticationService . full_access_token ( path )
url = Gitlab . config . registry . api_url
host_port = Gitlab . config . registry . host_port
ContainerRegistry :: Registry . new ( url , token : token , path : host_port )
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
def path
@path || = [ project . full_path , name ]
. select ( & :present? ) . join ( '/' ) . downcase
end
def location
File . join ( registry . path , path )
end
def tag ( tag )
ContainerRegistry :: Tag . new ( self , tag )
end
def manifest
@manifest || = client . repository_tags ( path )
end
def tags
return [ ] unless manifest && manifest [ 'tags' ]
2019-03-02 22:35:43 +05:30
strong_memoize ( :tags ) do
manifest [ 'tags' ] . sort . map do | tag |
ContainerRegistry :: Tag . new ( self , tag )
end
2017-08-17 22:00:37 +05:30
end
end
2022-08-27 11:52:29 +05:30
def each_tags_page ( page_size : 100 , & block )
raise ArgumentError , 'not a migrated repository' unless migrated?
raise ArgumentError , 'block not given' unless block
# dummy uri to initialize the loop
next_page_uri = URI ( '' )
page_count = 0
while next_page_uri && page_count < MAX_TAGS_PAGES
last = Rack :: Utils . parse_nested_query ( next_page_uri . query ) [ 'last' ]
current_page = gitlab_api_client . tags ( self . path , page_size : page_size , last : last )
if current_page & . key? ( :response_body )
yield transform_tags_page ( current_page [ :response_body ] )
next_page_uri = current_page . dig ( :pagination , :next , :uri )
else
# no current page. Break the loop
next_page_uri = nil
end
page_count += 1
end
raise 'too many pages requested' if page_count > = MAX_TAGS_PAGES
end
2020-06-23 00:09:42 +05:30
def tags_count
return 0 unless manifest && manifest [ 'tags' ]
manifest [ 'tags' ] . size
end
2017-08-17 22:00:37 +05:30
def blob ( config )
ContainerRegistry :: Blob . new ( self , config )
end
def has_tags?
tags . any?
end
def root_repository?
name . empty?
end
def delete_tags!
return unless has_tags?
2019-12-21 20:55:43 +05:30
digests = tags . map { | tag | tag . digest } . compact . to_set
2017-08-17 22:00:37 +05:30
2019-12-21 20:55:43 +05:30
digests . map ( & method ( :delete_tag_by_digest ) ) . all?
2017-08-17 22:00:37 +05:30
end
2019-10-12 21:52:04 +05:30
def delete_tag_by_digest ( digest )
2020-03-13 15:44:24 +05:30
client . delete_repository_tag_by_digest ( self . path , digest )
end
def delete_tag_by_name ( name )
client . delete_repository_tag_by_name ( self . path , name )
2019-10-12 21:52:04 +05:30
end
2021-01-03 14:25:43 +05:30
def start_expiration_policy!
2022-07-16 23:28:13 +05:30
update! ( expiration_policy_started_at : Time . zone . now , last_cleanup_deleted_tags_count : nil )
2021-01-03 14:25:43 +05:30
end
2022-05-07 20:08:51 +05:30
def size
strong_memoize ( :size ) do
next unless Gitlab . com?
2022-08-13 15:12:31 +05:30
next if self . created_at . before? ( MIGRATION_PHASE_1_STARTED_AT ) && self . migration_state != 'import_done'
2022-05-07 20:08:51 +05:30
next unless gitlab_api_client . supports_gitlab_api?
2022-06-21 17:19:12 +05:30
gitlab_api_client . repository_details ( self . path , sizing : :self ) [ 'size_bytes' ]
2022-05-07 20:08:51 +05:30
end
end
2022-04-04 11:22:00 +05:30
def migration_in_active_state?
migration_state . in? ( ACTIVE_MIGRATION_STATES )
end
def migration_importing?
migration_state == 'importing'
end
def migration_pre_importing?
migration_state == 'pre_importing'
end
def migration_pre_import
return :error unless gitlab_api_client . supports_gitlab_api?
response = gitlab_api_client . pre_import_repository ( self . path )
raise TooManyImportsError if response == :too_many_imports
response
end
def migration_import
return :error unless gitlab_api_client . supports_gitlab_api?
response = gitlab_api_client . import_repository ( self . path )
raise TooManyImportsError if response == :too_many_imports
response
end
2022-06-21 17:19:12 +05:30
def migration_cancel
return :error unless gitlab_api_client . supports_gitlab_api?
gitlab_api_client . cancel_repository_import ( self . path )
end
# This method is not meant for consumption by the code
# It is meant for manual use in the case that a migration needs to be
# cancelled by an admin or SRE
def force_migration_cancel
return :error unless gitlab_api_client . supports_gitlab_api?
response = gitlab_api_client . cancel_repository_import ( self . path , force : true )
skip_import ( reason : :migration_forced_canceled ) if response [ :status ] == :ok
response
end
2017-08-17 22:00:37 +05:30
def self . build_from_path ( path )
self . new ( project : path . repository_project ,
name : path . repository_name )
end
2022-01-26 12:08:38 +05:30
def self . find_or_create_from_path ( path )
repository = safe_find_or_create_by (
project : path . repository_project ,
name : path . repository_name
)
return repository if repository . persisted?
find_by_path! ( path )
2017-08-17 22:00:37 +05:30
end
def self . build_root_repository ( project )
self . new ( project : project , name : '' )
end
2019-10-12 21:52:04 +05:30
def self . find_by_path! ( path )
self . find_by! ( project : path . repository_project ,
name : path . repository_name )
end
2022-04-04 11:22:00 +05:30
def self . find_by_path ( path )
self . find_by ( project : path . repository_project ,
2022-08-27 11:52:29 +05:30
name : path . repository_name )
2022-04-04 11:22:00 +05:30
end
2022-06-21 17:19:12 +05:30
private
def finish_import_as ( reason )
self . migration_skipped_reason = reason
finish_import
end
2022-08-27 11:52:29 +05:30
def transform_tags_page ( tags_response_body )
return [ ] unless tags_response_body
tags_response_body . map do | raw_tag |
tag = ContainerRegistry :: Tag . new ( self , raw_tag [ 'name' ] )
tag . force_created_at_from_iso8601 ( raw_tag [ 'created_at' ] )
tag
end
end
2017-08-17 22:00:37 +05:30
end
2019-12-04 20:38:33 +05:30
2021-06-08 01:23:25 +05:30
ContainerRepository . prepend_mod_with ( 'ContainerRepository' )