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 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
|
# frozen_string_literal: true
module ContainerRegistry
class GitlabApiClient < BaseClient
include Gitlab::Utils::StrongMemoize
JSON_TYPE = 'application/json'
CANCEL_RESPONSE_STATUS_HEADER = 'status'
GITLAB_REPOSITORIES_PATH = '/gitlab/v1/repositories'
RENAME_RESPONSES = {
202 => :accepted,
204 => :ok,
400 => :bad_request,
401 => :unauthorized,
404 => :not_found,
409 => :name_taken,
422 => :too_many_subrepositories
}.freeze
REGISTRY_GITLAB_V1_API_FEATURE = 'gitlab_v1_api'
MAX_TAGS_PAGE_SIZE = 1000
MAX_REPOSITORIES_PAGE_SIZE = 1000
PAGE_SIZE = 1
UnsuccessfulResponseError = Class.new(StandardError)
def self.supports_gitlab_api?
with_dummy_client(return_value_if_disabled: false) do |client|
client.supports_gitlab_api?
end
end
def self.deduplicated_size(path)
downcased_path = path&.downcase
with_dummy_client(token_config: { type: :nested_repositories_token, path: downcased_path }) do |client|
client.repository_details(downcased_path, sizing: :self_with_descendants)['size_bytes']
end
end
def self.one_project_with_container_registry_tag(path)
downcased_path = path&.downcase
with_dummy_client(token_config: { type: :nested_repositories_token, path: downcased_path }) do |client|
page = client.sub_repositories_with_tag(downcased_path, page_size: PAGE_SIZE)
details = page[:response_body]&.first
break unless details
path = ContainerRegistry::Path.new(details["path"])
break unless path.valid?
ContainerRepository.find_by_path(path)&.project
end
end
def self.rename_base_repository_path(path, name:, dry_run: false)
raise ArgumentError, 'incomplete parameters given' unless path.present? && name.present?
downcased_path = path.downcase
with_dummy_client(token_config: { type: :push_pull_nested_repositories_token, path: downcased_path }) do |client|
client.rename_base_repository_path(downcased_path, name: name.downcase, dry_run: dry_run)
end
end
def self.move_repository_to_namespace(path, namespace:, dry_run: false)
raise ArgumentError, 'incomplete parameters given' unless path.present? && namespace.present?
downcased_path = path.downcase
downcased_namespace = namespace.downcase
token_config = {
type: :push_pull_move_repositories_access_token,
path: downcased_path,
new_path: downcased_namespace
}
with_dummy_client(token_config: token_config) do |client|
client.move_repository_to_namespace(downcased_path, namespace: downcased_namespace, dry_run: dry_run)
end
end
def self.each_sub_repositories_with_tag_page(path:, page_size: 100, &block)
raise ArgumentError, 'block not given' unless block
# dummy uri to initialize the loop
next_page_uri = URI('')
page_count = 0
downcased_path = path&.downcase
with_dummy_client(token_config: { type: :nested_repositories_token, path: downcased_path }) do |client|
while next_page_uri
last = Rack::Utils.parse_nested_query(next_page_uri.query)['last']
current_page = client.sub_repositories_with_tag(downcased_path, page_size: page_size, last: last)
if current_page&.key?(:response_body)
yield (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
raise 'too many pages requested' if page_count >= MAX_REPOSITORIES_PAGE_SIZE
end
end
end
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#compliance-check
def supports_gitlab_api?
strong_memoize(:supports_gitlab_api) do
registry_features = Gitlab::CurrentSettings.container_registry_features || []
next true if ::Gitlab.com_except_jh? && registry_features.include?(REGISTRY_GITLAB_V1_API_FEATURE)
with_token_faraday do |faraday_client|
response = faraday_client.get('/gitlab/v1/')
response.success? || response.status == 401
end
end
rescue ::Faraday::Error
false
end
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#get-repository-details
def repository_details(path, sizing: nil)
with_token_faraday do |faraday_client|
req = faraday_client.get("#{GITLAB_REPOSITORIES_PATH}/#{path}/") do |req|
req.params['size'] = sizing if sizing
end
break {} unless req.success?
response_body(req)
end
end
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-repository-tags
def tags(path, page_size: 100, last: nil, before: nil, name: nil, sort: nil, referrers: nil, referrer_type: nil)
limited_page_size = [page_size, MAX_TAGS_PAGE_SIZE].min
with_token_faraday do |faraday_client|
url = "#{GITLAB_REPOSITORIES_PATH}/#{path}/tags/list/"
response = faraday_client.get(url) do |req|
req.params['n'] = limited_page_size
req.params['last'] = last if last
req.params['before'] = before if before
req.params['name'] = name if name.present?
req.params['sort'] = sort if sort
req.params['referrers'] = 'true' if referrers
req.params['referrer_type'] = referrer_type if referrer_type
end
unless response.success?
Gitlab::ErrorTracking.log_exception(
UnsuccessfulResponseError.new,
class: self.class.name,
url: url,
status_code: response.status
)
break {}
end
link_parser = Gitlab::Utils::LinkHeaderParser.new(response.headers['link'])
{
pagination: link_parser.parse,
response_body: response_body(response)
}
end
end
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-sub-repositories
def sub_repositories_with_tag(path, page_size: 100, last: nil)
limited_page_size = [page_size, MAX_REPOSITORIES_PAGE_SIZE].min
with_token_faraday do |faraday_client|
url = "/gitlab/v1/repository-paths/#{path}/repositories/list/"
response = faraday_client.get(url) do |req|
req.params['n'] = limited_page_size
req.params['last'] = last if last
end
unless response.success?
Gitlab::ErrorTracking.log_exception(
UnsuccessfulResponseError.new,
class: self.class.name,
url: url,
status_code: response.status
)
break {}
end
link_parser = Gitlab::Utils::LinkHeaderParser.new(response.headers['link'])
{
pagination: link_parser.parse,
response_body: response_body(response)
}
end
end
# Given a path 'group/subgroup/project' and name 'newname',
# with a successful rename, it will be 'group/subgroup/newname'
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#rename-base-repository
def rename_base_repository_path(path, name:, dry_run: false)
patch_repository(path, { name: name }, dry_run: dry_run)
end
# Given a path 'group/subgroup/project' and a namespace 'group/subgroup_2'
# with a successful move, it will be 'group/subgroup_2/project'
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#renamemove-origin-repository
def move_repository_to_namespace(path, namespace:, dry_run: false)
patch_repository(path, { namespace: namespace }, dry_run: dry_run)
end
private
def patch_repository(path, body, dry_run: false)
with_token_faraday do |faraday_client|
url = "#{GITLAB_REPOSITORIES_PATH}/#{path}/"
response = faraday_client.patch(url) do |req|
req.params['dry_run'] = dry_run
req.body = body
end
unless response.success?
Gitlab::ErrorTracking.log_exception(
UnsuccessfulResponseError.new,
class: self.class.name,
url: url,
status_code: response.status
)
end
RENAME_RESPONSES.fetch(response.status, :error)
end
end
def with_token_faraday
yield faraday
end
# overrides the default configuration
def configure_connection(conn)
conn.headers['Accept'] = [JSON_TYPE]
conn.response :json, content_type: JSON_TYPE
end
end
end
|