Skip to content

Commit f09d17a

Browse files
author
Tom McKay
committed
fixes #22951 - support docker v2 api
1 parent bf02310 commit f09d17a

File tree

8 files changed

+631
-0
lines changed

8 files changed

+631
-0
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
module Katello
2+
class Api::Registry::RegistryProxiesController < Api::V2::ApiController
3+
before_action :disable_strong_params
4+
5+
skip_before_action :authorize, except: [:token]
6+
before_action :registry_authorize, except: [:token]
7+
before_action :authorize_repository_read, only: [:pull_manifest, :tags_list]
8+
before_action :authorize_repository_write, only: [:push_manifest]
9+
skip_before_action :check_content_type, :only => [:start_upload_blob, :upload_blob, :finish_upload_blob,
10+
:chunk_upload_blob, :push_manifest]
11+
skip_after_action :log_response_body, :only => [:pull_blob]
12+
13+
wrap_parameters false
14+
15+
around_action :repackage_message
16+
17+
def repackage_message
18+
yield
19+
ensure
20+
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
21+
end
22+
23+
rescue_from RestClient::Exception do |e|
24+
Rails.logger.error pp_exception(e)
25+
if request_from_katello_cli?
26+
render json: { errors: [e.http_body] }, status: e.http_code
27+
else
28+
render plain: e.http_body, status: e.http_code
29+
end
30+
end
31+
32+
def redirect_authorization_headers
33+
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
34+
response.headers['Www-Authenticate'] = "Bearer realm=\"#{request_url}/v2/token\"," \
35+
"service=\"#{request.host}\"," \
36+
"scope=\"repository:registry:pull,push\""
37+
end
38+
39+
def registry_authorize
40+
token = request.headers['Authorization']
41+
if token
42+
token_type, token = token.split
43+
if token_type == 'Bearer' && token
44+
personal_token = PersonalAccessToken.find_by_token(token)
45+
if personal_token && !personal_token.expired?
46+
User.current = User.unscoped.find(personal_token.user_id)
47+
return true if User.current
48+
end
49+
elsif token_type == 'Basic' && token
50+
return true if authorize
51+
redirect_authorization_headers
52+
return false
53+
end
54+
end
55+
redirect_authorization_headers
56+
render_error('unauthorized', :status => :unauthorized)
57+
return false
58+
end
59+
60+
def authorize_repository_write
61+
@repository = Repository.syncable.find_by_container_repository_name(params[:repository])
62+
unless @repository
63+
not_found params[:repository]
64+
return false
65+
end
66+
true
67+
end
68+
69+
# Reduce visible repos to include lifecycle env permissions
70+
# http://projects.theforeman.org/issues/22914
71+
def readable_repositories
72+
table_name = Repository.table_name
73+
in_products = Repository.where(:product_id => Katello::Product.authorized(:view_products)).select(:id)
74+
in_environments = Repository.where(:environment_id => Katello::KTEnvironment.authorized(:view_lifecycle_environments)).select(:id)
75+
in_content_views = Repository.joins(:content_view_repositories).where("#{ContentViewRepository.table_name}.content_view_id" => Katello::ContentView.readable).select(:id)
76+
in_versions = Repository.joins(:content_view_version).where("#{Katello::ContentViewVersion.table_name}.content_view_id" => Katello::ContentView.readable).select(:id)
77+
Repository.where("#{table_name}.id in (?) or #{table_name}.id in (?) or #{table_name}.id in (?) or #{table_name}.id in (?)", in_products, in_content_views, in_versions, in_environments)
78+
end
79+
80+
def authorize_repository_read
81+
@repository = readable_repositories.find_by_container_repository_name(params[:repository])
82+
unless @repository
83+
not_found params[:repository]
84+
return false
85+
end
86+
87+
if params[:tag]
88+
if params[:tag][0..6] == 'sha256:'
89+
manifest = Katello::DockerManifestList.where(digest: params[:tag]).first || Katello::DockerManifest.where(digest: params[:tag]).first
90+
not_found params[:tag] unless manifest
91+
else
92+
tag = DockerMetaTag.where(repository_id: @repository.id, name: params[:tag]).first
93+
not_found params[:tag] unless tag
94+
end
95+
end
96+
97+
true
98+
end
99+
100+
def token
101+
personal_token = PersonalAccessToken.where(user_id: User.current.id, name: 'registry').first
102+
if personal_token.nil?
103+
personal_token = PersonalAccessToken.new(user: User.current, name: 'registry', expires_at: 6.minutes.from_now)
104+
personal_token.generate_token
105+
personal_token.save!
106+
else
107+
personal_token.expires_at = 6.minutes.from_now
108+
personal_token.save!
109+
end
110+
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
111+
render json: { token: personal_token.token, expires_at: personal_token.expires_at, issued_at: personal_token.created_at }
112+
end
113+
114+
def pull_manifest
115+
headers = {}
116+
env = request.env.select do |key, _value|
117+
key.match("^HTTP.*")
118+
end
119+
env.each do |header|
120+
headers[header[0].split('_')[1..-1].join('-')] = header[1]
121+
end
122+
123+
r = Resources::Registry::Proxy.get(@_request.fullpath, headers)
124+
logger.debug r
125+
render json: r
126+
end
127+
128+
def check_blob
129+
begin
130+
r = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
131+
response.header['Content-Length'] = "#{r.body.size}"
132+
rescue RestClient::NotFound
133+
tmp_dir = "#{Rails.root}/tmp/registry_upload"
134+
digest_file = "#{tmp_dir}/#{params[:digest][7..-1]}.tar"
135+
raise unless File.exist? digest_file
136+
response.header['Content-Length'] = "#{File.size digest_file}"
137+
end
138+
render json: {}
139+
end
140+
141+
def pull_blob
142+
r = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
143+
render json: r
144+
end
145+
146+
def push_manifest
147+
repository = params[:repository]
148+
tag = params[:tag]
149+
manifest = request.body.read
150+
File.open(tmp_file('manifest.json'), 'wb', 0600) do |file|
151+
file.write manifest
152+
end
153+
manifest = JSON.parse(manifest)
154+
155+
files = []
156+
if manifest['schemaVersion'] == 1
157+
if manifest['fsLayers']
158+
files += manifest['fsLayers'].collect do |layer|
159+
layerfile = "#{layer['blobSum'][7..-1]}.tar"
160+
force_include_layer(repository, layer['blobSum'], layerfile)
161+
layerfile
162+
end
163+
end
164+
elsif manifest['schemaVersion'] == 2
165+
if manifest['layers']
166+
files += manifest['layers'].collect do |layer|
167+
layerfile = "#{layer['digest'][7..-1]}.tar"
168+
force_include_layer(repository, layer['digest'], layerfile)
169+
layerfile
170+
end
171+
end
172+
files << "#{manifest['config']['digest'][7..-1]}.tar"
173+
else
174+
render_error 'custom_error', :status => :internal_server_error,
175+
:locals => { :message => "Unsupported schema #{manifest['schemaVersion']}" }
176+
return
177+
end
178+
files << "manifest.json"
179+
180+
tar_file = "#{repository}_#{tag}.tar"
181+
`/usr/bin/tar cf #{tmp_dir}/#{tar_file} -C #{tmp_dir} #{files.join(' ')}`
182+
183+
files.each do |file|
184+
filename = "#{tmp_dir}/#{file}"
185+
File.delete(filename) if File.exist? filename
186+
end
187+
188+
#???? change ::Actions::Katello::Repository::ImportUpload to allow both manifest
189+
# and tag types (:unit_keys param) so single call
190+
# MANIFEST
191+
digest = nil
192+
begin
193+
upload_id = pulp_content.create_upload_request['upload_id']
194+
filename = "#{tmp_dir}/#{tar_file}"
195+
File.open(filename, 'rb') do |file|
196+
pulp_content.upload_bits(upload_id, 0, file.read)
197+
198+
file.rewind
199+
content = file.read
200+
unit_keys = [{
201+
name: filename,
202+
size: file.size,
203+
checksum: Digest::SHA256.hexdigest(content)
204+
}]
205+
unit_type_id = 'docker_manifest'
206+
sync_task(::Actions::Katello::Repository::ImportUpload,
207+
@repository, [upload_id], :unit_type_id => unit_type_id,
208+
:unit_keys => unit_keys,
209+
:generate_metadata => true, :sync_capsule => true)
210+
digest = task.output['upload_results'][0]['digest']
211+
end
212+
rescue
213+
render_error 'custom_error', :status => :internal_server_error,
214+
:locals => { :message => 'Failed to upload manifest' }
215+
return
216+
ensure
217+
pulp_content.delete_upload_request(upload_id) if upload_id
218+
end
219+
220+
# TAG
221+
begin
222+
upload_id = pulp_content.create_upload_request['upload_id']
223+
unit_keys = [{
224+
name: tag,
225+
digest: digest
226+
}]
227+
unit_type_id = 'docker_tag'
228+
sync_task(::Actions::Katello::Repository::ImportUpload,
229+
@repository, [upload_id], :unit_type_id => unit_type_id,
230+
:unit_keys => unit_keys,
231+
:generate_metadata => true, :sync_capsule => true)
232+
rescue
233+
render_error 'custom_error', :status => :internal_server_error,
234+
:locals => { :message => 'Failed to upload tag' }
235+
return
236+
ensure
237+
pulp_content.delete_upload_request(upload_id) if upload_id
238+
end
239+
240+
File.delete("#{tmp_dir}/#{tar_file}")
241+
242+
render json: {}
243+
end
244+
245+
def pulp_content
246+
Katello.pulp_server.resources.content
247+
end
248+
249+
def start_upload_blob
250+
tmp_dir = "#{Rails.root}/tmp/registry_upload"
251+
Dir.mkdir(tmp_dir, mode: 0700) unless Dir.exist? tmp_dir
252+
253+
uuid = SecureRandom.hex(16)
254+
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{uuid}"
255+
response.header['Docker-Upload-UUID'] = uuid
256+
response.header['Range'] = '0-0'
257+
head 202
258+
end
259+
260+
def status_upload_blob
261+
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
262+
response.header['Range'] = "123"
263+
response.header['Docker-Upload-UUID'] = "123"
264+
render plain: '', status: 204
265+
end
266+
267+
def chunk_upload_blob
268+
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
269+
render plain: '', status: 202
270+
end
271+
272+
def upload_blob
273+
File.open("#{Rails.root}/tmp/registry_upload/#{params[:uuid]}.tar", 'ab', 0600) do |file|
274+
file.write request.body.read
275+
end
276+
277+
# ???? true chunked data?
278+
if request.headers['Content-Range']
279+
render_error 'unprocessable_entity', :status => :unprocessable_entity
280+
end
281+
282+
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
283+
response.header['Range'] = "1-#{request.body.size}"
284+
response.header['Docker-Upload-UUID'] = params[:uuid]
285+
head 204
286+
end
287+
288+
def finish_upload_blob
289+
# error by client if no params[:digest]
290+
291+
tmp_dir = "#{Rails.root}/tmp/registry_upload"
292+
uuid_file = "#{tmp_dir}/#{params[:uuid]}.tar"
293+
digest_file = "#{tmp_dir}/#{params[:digest][7..-1]}.tar"
294+
if File.exist? digest_file
295+
File.unlink(uuid_file)
296+
else
297+
File.rename(uuid_file, digest_file)
298+
end
299+
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/#{params[:digest]}"
300+
response.header['Docker-Content-Digest'] = params[:digest]
301+
response.header['Content-Range'] = "1-#{File.size(digest_file)}"
302+
response.header['Content-Length'] = "0"
303+
response.header['Docker-Upload-UUID'] = params[:uuid]
304+
head 201
305+
end
306+
307+
def cancel_upload_blob
308+
render plain: '', status: 200
309+
end
310+
311+
def ping
312+
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
313+
render json: {}, status: 200
314+
end
315+
316+
def v1_ping
317+
head 200
318+
end
319+
320+
def v1_search
321+
options = {
322+
resource_class: Katello::Repository
323+
}
324+
params[:per_page] = params[:n] || 25
325+
params[:search] = params[:q]
326+
search_results = scoped_search(readable_repositories.where(content_type: 'docker').distinct,
327+
:container_repository_name, :asc, options)
328+
results = {
329+
num_results: search_results[:subtotal],
330+
query: params[:search]
331+
}
332+
results[:results] = search_results[:results].collect do |repository|
333+
{ name: repository[:container_repository_name], description: repository[:description] }
334+
end
335+
render json: results, status: 200
336+
end
337+
338+
def catalog
339+
repositories = readable_repositories.where(content_type: 'docker').collect do |repository|
340+
repository.container_repository_name
341+
end
342+
render json: { repositories: repositories }
343+
end
344+
345+
def tags_list
346+
tags = @repository.docker_tags.collect do |tag|
347+
tag.name
348+
end
349+
tags.uniq!
350+
tags.sort!
351+
render json: {
352+
name: @repository.container_repository_name,
353+
tags: tags
354+
}
355+
end
356+
357+
def tmp_dir
358+
"#{Rails.root}/tmp/registry_upload"
359+
end
360+
361+
def tmp_file(filename)
362+
File.join(tmp_dir, filename)
363+
end
364+
365+
# TODO: Until pulp supports optional upload of layers, include all layers
366+
# https://pulp.plan.io/issues/3497
367+
def force_include_layer(repository, digest, layer)
368+
unless File.exist? tmp_file(layer)
369+
logger.debug "Getting blob #{digest} to write to #{layer}"
370+
fullpath = "/v2/#{repository}/blobs/#{digest}"
371+
request = Resources::Registry::Proxy.get(fullpath)
372+
File.open(tmp_file(layer), 'wb', 0600) do |file|
373+
file.write request.body
374+
end
375+
logger.debug "Wrote blob #{digest} to #{layer}"
376+
end
377+
end
378+
379+
def disable_strong_params
380+
params.permit!
381+
end
382+
383+
def request_url
384+
request.protocol + request.host_with_port
385+
end
386+
387+
def logger
388+
::Foreman::Logging.logger('katello/registry_proxy')
389+
end
390+
end
391+
end

0 commit comments

Comments
 (0)