Class: ContainerRepository

Inherits:
ApplicationRecord show all
Includes:
AfterCommitQueue, EachBatch, Gitlab::SQL::Pattern, Gitlab::Utils::StrongMemoize, Packages::Destructible, Sortable
Defined in:
app/models/container_repository.rb

Constant Summary collapse

WAITING_CLEANUP_STATUSES =
%i[cleanup_scheduled cleanup_unfinished].freeze
REQUIRING_CLEANUP_STATUSES =
%i[cleanup_unscheduled cleanup_scheduled].freeze
MAX_TAGS_PAGES =
2000
MAX_DELETION_FAILURES =
10
AUTH_TOKEN_USAGE_RESERVED_TIME_IN_SECS =

The Registry client uses JWT token to authenticate to Registry. We cache the client using expiration time of JWT token. However it’s possible that the token is valid but by the time the request is made to Regsitry, it’s already expired. To prevent this case, we are subtracting a few seconds, defined by this constant from the cache expiration time.

5

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_TERM

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from Gitlab::SQL::Pattern

split_query_to_search_terms

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, nullable_column?, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#pathObject

rubocop: enable CodeReuse/ServiceClass



106
107
108
109
# File 'app/models/container_repository.rb', line 106

def path
  @path ||= [project.full_path, name]
    .select(&:present?).join('/').downcase
end

Class Method Details

.build_from_path(path) ⇒ Object



287
288
289
# File 'app/models/container_repository.rb', line 287

def self.build_from_path(path)
  self.new(project: path.repository_project, name: path.repository_name)
end

.build_root_repository(project) ⇒ Object



300
301
302
# File 'app/models/container_repository.rb', line 300

def self.build_root_repository(project)
  self.new(project: project, name: '')
end

.exists_by_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


61
62
63
64
65
66
# File 'app/models/container_repository.rb', line 61

def self.exists_by_path?(path)
  where(
    project: path.repository_project,
    name: path.repository_name
  ).exists?
end

.find_by_path(path) ⇒ Object



308
309
310
# File 'app/models/container_repository.rb', line 308

def self.find_by_path(path)
  self.find_by(project: path.repository_project, name: path.repository_name)
end

.find_by_path!(path) ⇒ Object



304
305
306
# File 'app/models/container_repository.rb', line 304

def self.find_by_path!(path)
  self.find_by!(project: path.repository_project, name: path.repository_name)
end

.find_or_create_from_path!(path) ⇒ Object



291
292
293
294
295
296
297
298
# File 'app/models/container_repository.rb', line 291

def self.find_or_create_from_path!(path)
  ContainerRepository.upsert({
    project_id: path.repository_project.id,
    name: path.repository_name
  }, unique_by: %i[project_id name])

  find_by_path!(path)
end

.pending_destructionObject

needed by Packages::Destructible



89
90
91
# File 'app/models/container_repository.rb', line 89

def self.pending_destruction
  delete_scheduled.where('next_delete_attempt_at IS NULL OR next_delete_attempt_at < ?', Time.zone.now)
end

.registry_client_expiration_timeObject



84
85
86
# File 'app/models/container_repository.rb', line 84

def self.registry_client_expiration_time
  (Gitlab::CurrentSettings.container_registry_token_expire_delay * 60) - AUTH_TOKEN_USAGE_RESERVED_TIME_IN_SECS
end

.requiring_cleanupObject



73
74
75
76
77
78
# File 'app/models/container_repository.rb', line 73

def self.requiring_cleanup
  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)
end

.with_enabled_policyObject



68
69
70
71
# File 'app/models/container_repository.rb', line 68

def self.with_enabled_policy
  joins('INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id')
    .where(container_expiration_policies: { enabled: true })
end

.with_unfinished_cleanupObject



80
81
82
# File 'app/models/container_repository.rb', line 80

def self.with_unfinished_cleanup
  with_enabled_policy.cleanup_unfinished
end

Instance Method Details

#blob(config) ⇒ Object



208
209
210
# File 'app/models/container_repository.rb', line 208

def blob(config)
  ContainerRegistry::Blob.new(self, config)
end

#delete_tag(name_or_digest) ⇒ Object



228
229
230
# File 'app/models/container_repository.rb', line 228

def delete_tag(name_or_digest)
  client.delete_repository_tag_by_digest(self.path, name_or_digest)
end

#delete_tags!Object



220
221
222
223
224
225
226
# File 'app/models/container_repository.rb', line 220

def delete_tags!
  return unless has_tags?

  digests = tags.map { |tag| tag.digest }.compact.to_set

  digests.map { |digest| delete_tag(digest) }.all?
end

#each_tags_page(page_size: 100, &block) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'app/models/container_repository.rb', line 156

def each_tags_page(page_size: 100, &block)
  raise ArgumentError, _('GitLab container registry API not supported') unless gitlab_api_client.supports_gitlab_api?
  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

#has_tags?Boolean

Returns:

  • (Boolean)


212
213
214
# File 'app/models/container_repository.rb', line 212

def has_tags?
  tags.any?
end

#image_manifest(reference) ⇒ Object



129
130
131
# File 'app/models/container_repository.rb', line 129

def image_manifest(reference)
  client.repository_manifest(path, reference)
end

#last_published_atObject



248
249
250
251
252
253
254
255
# File 'app/models/container_repository.rb', line 248

def last_published_at
  return unless gitlab_api_client.supports_gitlab_api?

  timestamp_string = gitlab_api_client_repository_details['last_published_at']
  DateTime.iso8601(timestamp_string)
rescue ArgumentError
  nil
end

#locationObject



111
112
113
# File 'app/models/container_repository.rb', line 111

def location
  File.join(registry.path, path)
end

#manifestObject



133
134
135
# File 'app/models/container_repository.rb', line 133

def manifest
  @manifest ||= client.repository_tags(path)
end

#registryObject

rubocop: disable CodeReuse/ServiceClass



94
95
96
97
98
99
100
101
102
103
# File 'app/models/container_repository.rb', line 94

def registry
  strong_memoize_with_expiration(:registry, self.class.registry_client_expiration_time) do
    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

#root_repository?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'app/models/container_repository.rb', line 216

def root_repository?
  name.empty?
end

#set_delete_failed_statusObject



279
280
281
282
283
284
285
# File 'app/models/container_repository.rb', line 279

def set_delete_failed_status
  update_columns(
    status: :delete_failed,
    delete_started_at: nil,
    status_updated_at: Time.zone.now
  )
end

#set_delete_ongoing_statusObject



258
259
260
261
262
263
264
265
266
267
# File 'app/models/container_repository.rb', line 258

def set_delete_ongoing_status
  now = Time.zone.now

  update_columns(
    status: :delete_ongoing,
    delete_started_at: now,
    status_updated_at: now,
    next_delete_attempt_at: nil
  )
end

#set_delete_scheduled_statusObject



269
270
271
272
273
274
275
276
277
# File 'app/models/container_repository.rb', line 269

def set_delete_scheduled_status
  update_columns(
    status: :delete_scheduled,
    delete_started_at: nil,
    status_updated_at: Time.zone.now,
    failed_deletion_count: failed_deletion_count + 1,
    next_delete_attempt_at: next_delete_attempt_with_delay
  )
end

#sizeObject



240
241
242
243
244
245
246
# File 'app/models/container_repository.rb', line 240

def size
  strong_memoize(:size) do
    next unless gitlab_api_client.supports_gitlab_api?

    gitlab_api_client_repository_details['size_bytes']
  end
end

#start_expiration_policy!Object



232
233
234
235
236
237
238
# File 'app/models/container_repository.rb', line 232

def start_expiration_policy!
  update!(
    expiration_policy_started_at: Time.zone.now,
    last_cleanup_deleted_tags_count: nil,
    expiration_policy_cleanup_status: :cleanup_ongoing
  )
end

#tag(tag) ⇒ Object

If the container registry GitLab API is available, the API does a search of tags containing the name and we filter them to find the exact match. Otherwise, we instantiate a tag.



118
119
120
121
122
123
124
125
126
127
# File 'app/models/container_repository.rb', line 118

def tag(tag)
  if gitlab_api_client.supports_gitlab_api?
    page = tags_page(name: tag)
    return if page[:tags].blank?

    page[:tags].find { |result_tag| result_tag.name == tag }
  else
    ContainerRegistry::Tag.new(self, tag)
  end
end

#tagsObject



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/models/container_repository.rb', line 137

def tags
  strong_memoize(:tags) do
    if gitlab_api_client.supports_gitlab_api?
      result = []
      each_tags_page do |array_of_tags|
        result << array_of_tags
      end

      result.flatten
    else
      next [] unless manifest && manifest['tags']

      manifest['tags'].sort.map do |tag|
        ContainerRegistry::Tag.new(self, tag)
      end
    end
  end
end

#tags_countObject



202
203
204
205
206
# File 'app/models/container_repository.rb', line 202

def tags_count
  return 0 unless manifest && manifest['tags']

  manifest['tags'].size
end

#tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100, referrers: nil, referrer_type: nil) ⇒ Object

Raises:

  • (ArgumentError)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'app/models/container_repository.rb', line 182

def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100, referrers: nil, referrer_type: nil)
  raise ArgumentError, _('GitLab container registry API not supported') unless gitlab_api_client.supports_gitlab_api?

  page = gitlab_api_client.tags(
    self.path,
    page_size: page_size,
    before: before,
    last: last,
    sort: sort,
    name: name,
    referrers: referrers,
    referrer_type: referrer_type
  )

  {
    tags: transform_tags_page(page[:response_body]),
    pagination: page[:pagination]
  }
end