2018-07-25 05:30:33 -04:00
# frozen_string_literal: true
2019-03-28 09:17:42 -04:00
class ContainerRepository < ApplicationRecord
2019-01-10 09:22:58 -05:00
include Gitlab :: Utils :: StrongMemoize
2020-04-27 20:09:33 -04:00
include Gitlab :: SQL :: Pattern
2020-10-26 14:08:27 -04:00
include EachBatch
2021-02-09 13:09:59 -05:00
include Sortable
2022-02-11 13:18:58 -05:00
include AfterCommitQueue
2020-10-26 14:08:27 -04:00
WAITING_CLEANUP_STATUSES = % i [ cleanup_scheduled cleanup_unfinished ] . freeze
2021-05-12 08:10:24 -04:00
REQUIRING_CLEANUP_STATUSES = % i [ cleanup_unscheduled cleanup_scheduled ] . freeze
2022-03-18 14:07:26 -04:00
2022-01-25 13:11:55 -05:00
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-03-18 14:07:26 -04:00
ABORTABLE_MIGRATION_STATES = ( ACTIVE_MIGRATION_STATES + %w[ pre_import_done default ] ) . freeze
2022-03-23 11:08:38 -04:00
SKIPPABLE_MIGRATION_STATES = ( ABORTABLE_MIGRATION_STATES + %w[ import_aborted ] ) . freeze
2022-03-18 14:07:26 -04:00
2022-02-18 07:15:31 -05:00
MIGRATION_PHASE_1_STARTED_AT = Date . new ( 2021 , 11 , 4 ) . freeze
2022-04-08 20:09:46 -04:00
MIGRATION_PHASE_1_ENDED_AT = Date . new ( 2022 , 01 , 23 ) . freeze
2022-02-18 07:15:31 -05:00
2022-02-11 13:18:58 -05:00
TooManyImportsError = Class . new ( StandardError )
2016-11-23 11:50:30 -05:00
belongs_to :project
2017-03-24 07:31:34 -04:00
validates :name , length : { minimum : 0 , allow_nil : false }
2017-04-03 05:51:13 -04:00
validates :name , uniqueness : { scope : :project_id }
2022-01-25 13:11:55 -05:00
validates :migration_state , presence : true , inclusion : { in : MIGRATION_STATES }
2022-02-10 10:12:42 -05:00
validates :migration_aborted_in_state , inclusion : { in : ABORTABLE_MIGRATION_STATES } , allow_nil : true
2022-01-19 13:14:01 -05:00
validates :migration_retries_count , presence : true ,
numericality : { greater_than_or_equal_to : 0 } ,
allow_nil : false
2017-03-24 07:31:34 -04:00
2020-04-07 08:09:34 -04:00
enum status : { delete_scheduled : 0 , delete_failed : 1 }
2020-10-26 14:08:27 -04:00
enum expiration_policy_cleanup_status : { cleanup_unscheduled : 0 , cleanup_scheduled : 1 , cleanup_unfinished : 2 , cleanup_ongoing : 3 }
2022-04-11 05:09:29 -04:00
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-04-20 11:10:23 -04:00
migration_forced_canceled : 7 ,
migration_canceled_by_registry : 8
2022-04-11 05:09:29 -04:00
}
2020-04-07 08:09:34 -04:00
2022-01-25 13:11:55 -05:00
delegate :client , :gitlab_api_client , to : :registry
2017-03-31 09:10:15 -04:00
2019-01-10 09:22:58 -05:00
scope :ordered , - > { order ( :name ) }
2019-10-24 20:06:14 -04:00
scope :with_api_entity_associations , - > { preload ( project : [ :route , { namespace : :route } ] ) }
2019-11-04 10:07:36 -05:00
scope :for_group_and_its_subgroups , - > ( group ) do
2020-06-11 08:08:54 -04:00
project_scope = Project
. for_group_and_its_subgroups ( group )
2021-07-09 05:09:53 -04:00
. with_feature_enabled ( :container_registry )
. select ( :id )
2020-06-11 08:08:54 -04:00
2020-11-24 10:09:13 -05:00
joins ( " INNER JOIN ( #{ project_scope . to_sql } ) projects on projects.id=container_repositories.project_id " )
2019-11-04 10:07:36 -05:00
end
2020-10-26 14:08:27 -04:00
scope :for_project_id , - > ( project_id ) { where ( project_id : project_id ) }
2020-04-27 20:09:33 -04:00
scope :search_by_name , - > ( query ) { fuzzy_search ( query , [ :name ] , use_minimum_char_limit : false ) }
2020-10-26 14:08:27 -04:00
scope :waiting_for_cleanup , - > { where ( expiration_policy_cleanup_status : WAITING_CLEANUP_STATUSES ) }
2021-05-12 08:10:24 -04:00
scope :expiration_policy_started_at_nil_or_before , - > ( timestamp ) { where ( 'expiration_policy_started_at < ? OR expiration_policy_started_at IS NULL' , timestamp ) }
2022-02-10 10:12:42 -05:00
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-05-20 11:10:13 -04:00
scope :with_stale_ongoing_cleanup , - > ( threshold ) { cleanup_ongoing . where ( 'expiration_policy_started_at < ?' , threshold ) }
2022-02-11 13:18:58 -05:00
scope :import_in_process , - > { where ( migration_state : %w[ pre_importing pre_import_done importing ] ) }
scope :recently_done_migration_step , - > do
2022-04-08 20:09:46 -04:00
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-02-11 13:18:58 -05:00
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-03-10 13:09:14 -05:00
joins ( project : [ :namespace ] ) . where (
2022-02-11 13:18:58 -05:00
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-03-10 13:09:14 -05:00
AND feature_gates . value = concat ( 'Group:' , namespaces . traversal_ids [ 1 ] )
2022-02-11 13:18:58 -05:00
) "
)
end
2019-01-10 09:22:58 -05:00
2022-02-15 16:12:52 -05:00
state_machine :migration_state , initial : :default , use_transactions : false do
2022-01-25 13:11:55 -05:00
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
2022-02-14 13:13:23 -05:00
validates :migration_pre_import_done_at , presence : true
2022-01-25 13:11:55 -05:00
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-04-05 17:08:46 -04:00
transition % i [ default pre_importing importing import_aborted ] = > :pre_importing
2022-01-25 13:11:55 -05:00
end
event :finish_pre_import do
2022-03-18 14:07:26 -04:00
transition % i [ pre_importing importing import_aborted ] = > :pre_import_done
2022-01-25 13:11:55 -05:00
end
event :start_import do
2022-03-18 14:07:26 -04:00
transition % i [ pre_import_done pre_importing importing import_aborted ] = > :importing
2022-01-25 13:11:55 -05:00
end
event :finish_import do
2022-03-29 05:09:05 -04:00
transition % i [ default pre_importing importing import_aborted ] = > :import_done
2022-01-25 13:11:55 -05:00
end
event :already_migrated do
transition default : :import_done
end
event :abort_import do
2022-02-10 10:12:42 -05:00
transition ABORTABLE_MIGRATION_STATES . map ( & :to_sym ) = > :import_aborted
2022-01-25 13:11:55 -05:00
end
event :skip_import do
2022-03-23 11:08:38 -04:00
transition SKIPPABLE_MIGRATION_STATES . map ( & :to_sym ) = > :import_skipped
2022-01-25 13:11:55 -05:00
end
event :retry_pre_import do
2022-03-18 14:07:26 -04:00
transition % i [ pre_importing importing import_aborted ] = > :pre_importing
2022-01-25 13:11:55 -05:00
end
event :retry_import do
2022-03-18 14:07:26 -04:00
transition % i [ pre_importing importing import_aborted ] = > :importing
2022-01-25 13:11:55 -05:00
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-04-05 17:08:46 -04:00
after_transition any = > :pre_importing do | container_repository , transition |
forced = transition . args . first . try ( :[] , :forced )
next if forced
2022-02-11 13:18:58 -05:00
container_repository . try_import do
container_repository . migration_pre_import
end
2022-01-25 13:11:55 -05:00
end
2022-04-08 20:09:46 -04:00
before_transition any = > :pre_import_done do | container_repository |
2022-01-25 13:11:55 -05:00
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-04-05 17:08:46 -04:00
after_transition any = > :importing do | container_repository , transition |
forced = transition . args . first . try ( :[] , :forced )
next if forced
2022-02-11 13:18:58 -05:00
container_repository . try_import do
container_repository . migration_import
end
2022-01-25 13:11:55 -05:00
end
2022-03-31 14:08:39 -04:00
before_transition any = > :import_done do | container_repository |
2022-01-25 13:11:55 -05:00
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-03-23 11:08:38 -04:00
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-01-25 13:11:55 -05:00
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-04-20 11:10:23 -04:00
before_transition any = > % i [ import_done import_aborted import_skipped ] do | container_repository |
2022-02-11 13:18:58 -05:00
container_repository . run_after_commit do
2022-05-09 11:07:50 -04:00
:: ContainerRegistry :: Migration :: EnqueuerWorker . enqueue_a_job
2022-02-11 13:18:58 -05:00
end
2022-01-25 13:11:55 -05:00
end
end
2020-03-30 05:07:58 -04:00
def self . exists_by_path? ( path )
where (
project : path . repository_project ,
name : path . repository_name
) . exists?
end
2022-04-08 20:09:46 -04:00
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-05-12 08:10:24 -04:00
def self . with_enabled_policy
2021-06-02 02:09:48 -04:00
joins ( 'INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id' )
2021-05-12 08:10:24 -04:00
. where ( container_expiration_policies : { enabled : true } )
end
def self . requiring_cleanup
2021-06-02 02:09:48 -04:00
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-05-12 08:10:24 -04:00
end
def self . with_unfinished_cleanup
with_enabled_policy . cleanup_unfinished
end
2022-02-10 10:12:42 -05:00
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
2022-02-11 13:18:58 -05:00
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
2022-01-25 13:11:55 -05:00
def skip_import ( reason : )
self . migration_skipped_reason = reason
super
end
2022-04-05 17:08:46 -04:00
def start_pre_import ( * args )
2022-01-25 13:11:55 -05:00
return false unless ContainerRegistry :: Migration . enabled?
2022-04-05 17:08:46 -04:00
super ( * args )
2022-01-25 13:11:55 -05:00
end
def retry_pre_import
return false unless ContainerRegistry :: Migration . enabled?
super
end
def retry_import
return false unless ContainerRegistry :: Migration . enabled?
super
end
2022-02-01 19:18:16 -05:00
def finish_pre_import_and_start_import
# nothing to do between those two transitions for now.
finish_pre_import && start_import
end
2022-02-14 13:13:23 -05:00
def retry_aborted_migration
return unless migration_state == 'import_aborted'
2022-03-18 14:07:26 -04:00
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-02-14 13:13:23 -05:00
when 'native'
2022-03-29 05:09:05 -04:00
finish_import_as ( :native_import )
2022-04-05 17:08:46 -04:00
when 'pre_import_in_progress'
return if pre_importing?
start_pre_import ( forced : true )
when 'import_in_progress'
return if importing?
start_import ( forced : true )
2022-02-14 13:13:23 -05:00
when 'import_complete'
finish_import
2022-04-28 14:10:01 -04:00
when 'import_failed' , 'import_canceled'
2022-02-11 13:18:58 -05:00
retry_import
2022-02-14 13:13:23 -05:00
when 'pre_import_complete'
finish_pre_import_and_start_import
2022-04-28 14:10:01 -04:00
when 'pre_import_failed' , 'pre_import_canceled'
2022-02-11 13:18:58 -05:00
retry_pre_import
2022-02-14 13:13:23 -05:00
else
2022-03-18 14:07:26 -04:00
yield
2022-02-11 13:18:58 -05:00
end
end
def try_import
raise ArgumentError , 'block not given' unless block_given?
try_count = 0
begin
try_count += 1
2022-03-23 11:08:38 -04:00
case yield
when :ok
return true
when :not_found
2022-03-29 05:09:05 -04:00
finish_import_as ( :not_found )
2022-03-23 14:08:47 -04:00
when :already_imported
2022-03-29 05:09:05 -04:00
finish_import_as ( :native_import )
2022-03-23 11:08:38 -04:00
else
abort_import
end
2022-02-11 13:18:58 -05:00
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-03-23 11:08:38 -04:00
def retried_too_many_times?
migration_retries_count > = ContainerRegistry :: Migration . max_retries
end
2022-04-20 11:10:23 -04:00
def nearing_or_exceeded_retry_limit?
migration_retries_count > = ContainerRegistry :: Migration . max_retries - 1
end
2022-02-11 13:18:58 -05:00
def last_import_step_done_at
2022-04-08 20:09:46 -04:00
[ migration_pre_import_done_at , migration_import_done_at , migration_aborted_at , migration_skipped_at ] . compact . max
2022-02-11 13:18:58 -05:00
end
2022-02-15 10:15:04 -05:00
def external_import_status
strong_memoize ( :import_status ) do
gitlab_api_client . import_status ( self . path )
end
end
2018-08-27 11:31:01 -04:00
# rubocop: disable CodeReuse/ServiceClass
2017-03-22 07:28:23 -04:00
def registry
2017-03-23 09:37:17 -04:00
@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-08-27 11:31:01 -04:00
# rubocop: enable CodeReuse/ServiceClass
2017-03-23 09:37:17 -04:00
def path
2017-04-13 05:54:02 -04:00
@path || = [ project . full_path , name ]
. select ( & :present? ) . join ( '/' ) . downcase
2016-11-01 22:33:35 -04:00
end
2017-04-10 06:57:19 -04:00
def location
File . join ( registry . path , path )
end
2016-11-01 22:33:35 -04:00
def tag ( tag )
ContainerRegistry :: Tag . new ( self , tag )
end
def manifest
2017-04-05 08:44:35 -04:00
@manifest || = client . repository_tags ( path )
2016-11-01 22:33:35 -04:00
end
def tags
return [ ] unless manifest && manifest [ 'tags' ]
2019-01-10 09:22:58 -05:00
strong_memoize ( :tags ) do
manifest [ 'tags' ] . sort . map do | tag |
ContainerRegistry :: Tag . new ( self , tag )
end
2016-11-01 22:33:35 -04:00
end
end
2020-05-25 17:08:00 -04:00
def tags_count
return 0 unless manifest && manifest [ 'tags' ]
manifest [ 'tags' ] . size
end
2016-11-01 22:33:35 -04:00
def blob ( config )
ContainerRegistry :: Blob . new ( self , config )
end
2017-03-29 06:14:29 -04:00
def has_tags?
2017-04-05 08:44:35 -04:00
tags . any?
2017-03-29 06:14:29 -04:00
end
2017-04-03 06:49:54 -04:00
def root_repository?
name . empty?
end
2017-03-31 05:54:09 -04:00
def delete_tags!
return unless has_tags?
2019-10-16 08:06:32 -04:00
digests = tags . map { | tag | tag . digest } . compact . to_set
2016-11-01 22:33:35 -04:00
2019-10-16 11:06:17 -04:00
digests . map ( & method ( :delete_tag_by_digest ) ) . all?
2016-11-01 22:33:35 -04:00
end
2017-03-30 07:31:33 -04:00
2019-08-01 08:03:08 -04:00
def delete_tag_by_digest ( digest )
2020-02-12 10:09:37 -05:00
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-08-01 08:03:08 -04:00
end
2020-09-22 20:09:45 -04:00
def start_expiration_policy!
2022-05-03 14:07:53 -04:00
update! ( expiration_policy_started_at : Time . zone . now , last_cleanup_deleted_tags_count : nil )
2020-09-22 20:09:45 -04:00
end
2022-02-18 07:15:31 -05:00
def size
strong_memoize ( :size ) do
next unless Gitlab . com?
2022-06-24 20:08:03 -04:00
next if self . created_at . before? ( MIGRATION_PHASE_1_STARTED_AT ) && self . migration_state != 'import_done'
2022-02-18 07:15:31 -05:00
next unless gitlab_api_client . supports_gitlab_api?
2022-04-08 20:09:46 -04:00
gitlab_api_client . repository_details ( self . path , sizing : :self ) [ 'size_bytes' ]
2022-02-18 07:15:31 -05:00
end
end
2022-02-01 19:18:16 -05:00
def migration_in_active_state?
migration_state . in? ( ACTIVE_MIGRATION_STATES )
end
2022-01-24 13:14:42 -05:00
def migration_importing?
migration_state == 'importing'
end
2022-02-01 19:18:16 -05:00
def migration_pre_importing?
migration_state == 'pre_importing'
end
2022-01-25 13:11:55 -05:00
def migration_pre_import
return :error unless gitlab_api_client . supports_gitlab_api?
2022-02-11 13:18:58 -05:00
response = gitlab_api_client . pre_import_repository ( self . path )
raise TooManyImportsError if response == :too_many_imports
response
2022-01-25 13:11:55 -05:00
end
def migration_import
return :error unless gitlab_api_client . supports_gitlab_api?
2022-02-11 13:18:58 -05:00
response = gitlab_api_client . import_repository ( self . path )
raise TooManyImportsError if response == :too_many_imports
response
2022-01-25 13:11:55 -05:00
end
2022-03-18 14:07:26 -04:00
def migration_cancel
return :error unless gitlab_api_client . supports_gitlab_api?
gitlab_api_client . cancel_repository_import ( self . path )
end
2022-04-11 05:09:29 -04:00
# 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-04-03 06:49:54 -04:00
def self . build_from_path ( path )
self . new ( project : path . repository_project ,
name : path . repository_name )
end
2021-12-10 10:10:24 -05:00
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-03-30 07:31:33 -04:00
end
2017-04-04 06:57:38 -04:00
def self . build_root_repository ( project )
self . new ( project : project , name : '' )
end
2019-07-09 11:59:52 -04:00
def self . find_by_path! ( path )
self . find_by! ( project : path . repository_project ,
name : path . repository_name )
end
2022-01-24 13:14:42 -05:00
def self . find_by_path ( path )
self . find_by ( project : path . repository_project ,
2022-08-01 11:11:13 -04:00
name : path . repository_name )
2022-01-24 13:14:42 -05:00
end
2022-03-23 14:08:47 -04:00
private
2022-03-29 05:09:05 -04:00
def finish_import_as ( reason )
self . migration_skipped_reason = reason
2022-03-23 14:08:47 -04:00
finish_import
end
2016-11-01 22:33:35 -04:00
end
2019-09-13 09:26:31 -04:00
2021-05-11 17:10:21 -04:00
ContainerRepository . prepend_mod_with ( 'ContainerRepository' )