|
module Katello
|
|
class Api::Registry::RegistryProxiesController < Api::V2::ApiController
|
|
before_action :disable_strong_params
|
|
before_action :confirm_settings
|
|
skip_before_action :authorize, except: [:token]
|
|
before_action :registry_authorize, except: [:token]
|
|
before_action :authorize_repository_read, only: [:pull_manifest, :tags_list]
|
|
before_action :authorize_repository_write, only: [:push_manifest]
|
|
skip_before_action :check_content_type, :only => [:start_upload_blob, :upload_blob, :finish_upload_blob,
|
|
:chunk_upload_blob, :push_manifest]
|
|
skip_after_action :log_response_body, :only => [:pull_blob]
|
|
|
|
wrap_parameters false
|
|
|
|
around_action :repackage_message
|
|
|
|
def repackage_message
|
|
yield
|
|
ensure
|
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
end
|
|
|
|
rescue_from RestClient::Exception do |e|
|
|
Rails.logger.error pp_exception(e)
|
|
if request_from_katello_cli?
|
|
render json: { errors: [e.http_body] }, status: e.http_code
|
|
else
|
|
render plain: e.http_body, status: e.http_code
|
|
end
|
|
end
|
|
|
|
def redirect_authorization_headers
|
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
response.headers['Www-Authenticate'] = "Bearer realm=\"#{request_url}/v2/token\"," \
|
|
"service=\"#{request.host}\"," \
|
|
"scope=\"repository:registry:pull,push\""
|
|
end
|
|
|
|
def registry_authorize
|
|
token = request.headers['Authorization']
|
|
if token
|
|
token_type, token = token.split
|
|
if token_type == 'Bearer' && token
|
|
personal_token = PersonalAccessToken.find_by_token(token)
|
|
if personal_token && !personal_token.expired?
|
|
User.current = User.unscoped.find(personal_token.user_id)
|
|
return true if User.current
|
|
end
|
|
elsif token_type == 'Basic' && token
|
|
return true if authorize
|
|
redirect_authorization_headers
|
|
return false
|
|
end
|
|
end
|
|
redirect_authorization_headers
|
|
render_error('unauthorized', :status => :unauthorized)
|
|
return false
|
|
end
|
|
|
|
def authorize_repository_write
|
|
@repository = Repository.syncable.find_by_container_repository_name(params[:repository])
|
|
unless @repository
|
|
not_found params[:repository]
|
|
return false
|
|
end
|
|
true
|
|
end
|
|
|
|
# Reduce visible repos to include lifecycle env permissions
|
|
# http://projects.theforeman.org/issues/22914
|
|
def readable_repositories
|
|
table_name = Repository.table_name
|
|
in_products = Repository.where(:product_id => Katello::Product.authorized(:view_products)).select(:id)
|
|
in_environments = Repository.where(:environment_id => Katello::KTEnvironment.authorized(:view_lifecycle_environments)).select(:id)
|
|
in_content_views = Repository.joins(:content_view_repositories).where("#{ContentViewRepository.table_name}.content_view_id" => Katello::ContentView.readable).select(:id)
|
|
in_versions = Repository.joins(:content_view_version).where("#{Katello::ContentViewVersion.table_name}.content_view_id" => Katello::ContentView.readable).select(:id)
|
|
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)
|
|
end
|
|
|
|
def find_repository
|
|
readable_repositories.find_by_container_repository_name(params[:repository])
|
|
end
|
|
|
|
def authorize_repository_read
|
|
@repository = find_repository
|
|
unless @repository
|
|
not_found params[:repository]
|
|
return false
|
|
end
|
|
|
|
if params[:tag]
|
|
if params[:tag][0..6] == 'sha256:'
|
|
manifest = Katello::DockerManifestList.where(digest: params[:tag]).first || Katello::DockerManifest.where(digest: params[:tag]).first
|
|
not_found params[:tag] unless manifest
|
|
else
|
|
tag = DockerMetaTag.where(repository_id: @repository.id, name: params[:tag]).first
|
|
not_found params[:tag] unless tag
|
|
end
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
def token
|
|
personal_token = PersonalAccessToken.where(user_id: User.current.id, name: 'registry').first
|
|
if personal_token.nil?
|
|
personal_token = PersonalAccessToken.new(user: User.current, name: 'registry', expires_at: 6.minutes.from_now)
|
|
personal_token.generate_token
|
|
personal_token.save!
|
|
else
|
|
personal_token.expires_at = 6.minutes.from_now
|
|
personal_token.save!
|
|
end
|
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
render json: { token: personal_token.token, expires_at: personal_token.expires_at, issued_at: personal_token.created_at }
|
|
end
|
|
|
|
def pull_manifest
|
|
headers = {}
|
|
env = request.env.select do |key, _value|
|
|
key.match("^HTTP.*")
|
|
end
|
|
env.each do |header|
|
|
headers[header[0].split('_')[1..-1].join('-')] = header[1]
|
|
end
|
|
|
|
r = Resources::Registry::Proxy.get(@_request.fullpath, headers)
|
|
logger.debug r
|
|
results = JSON.parse(r)
|
|
|
|
response.header['Docker-Content-Digest'] = "sha256:#{Digest::SHA256.hexdigest(r)}"
|
|
render json: r, content_type: results['mediaType']
|
|
end
|
|
|
|
def check_blob
|
|
begin
|
|
r = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
|
|
response.header['Content-Length'] = "#{r.body.size}"
|
|
rescue RestClient::NotFound
|
|
digest_file = tmp_file("#{params[:digest][7..-1]}.tar")
|
|
raise unless File.exist? digest_file
|
|
response.header['Content-Length'] = "#{File.size digest_file}"
|
|
end
|
|
render json: {}
|
|
end
|
|
|
|
def pull_blob
|
|
r = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
|
|
render json: r
|
|
end
|
|
|
|
def push_manifest
|
|
repository = params[:repository]
|
|
tag = params[:tag]
|
|
|
|
manifest = create_manifest
|
|
return if manifest.nil?
|
|
|
|
begin
|
|
files = get_manifest_files(repository, manifest)
|
|
return if files.nil?
|
|
|
|
tar_file = create_tar_file(files, repository, tag)
|
|
return if tar_file.nil?
|
|
|
|
digest = upload_manifest(tar_file)
|
|
return if digest.nil?
|
|
|
|
tag = upload_tag(digest, tag)
|
|
return if tag.nil?
|
|
ensure
|
|
File.delete(tmp_file('manifest.json')) if File.exist? tmp_file('manifest.json')
|
|
end
|
|
|
|
render json: {}
|
|
end
|
|
|
|
def pulp_content
|
|
Katello.pulp_server.resources.content
|
|
end
|
|
|
|
def start_upload_blob
|
|
uuid = SecureRandom.hex(16)
|
|
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{uuid}"
|
|
response.header['Docker-Upload-UUID'] = uuid
|
|
response.header['Range'] = '0-0'
|
|
head 202
|
|
end
|
|
|
|
def status_upload_blob
|
|
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
|
|
response.header['Range'] = "123"
|
|
response.header['Docker-Upload-UUID'] = "123"
|
|
render plain: '', status: 204
|
|
end
|
|
|
|
def chunk_upload_blob
|
|
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
|
|
render plain: '', status: 202
|
|
end
|
|
|
|
def upload_blob
|
|
File.open(tmp_file("#{params[:uuid]}.tar"), 'ab', 0600) do |file|
|
|
file.write request.body.read
|
|
end
|
|
|
|
# ???? true chunked data?
|
|
if request.headers['Content-Range']
|
|
render_error 'unprocessable_entity', :status => :unprocessable_entity
|
|
end
|
|
|
|
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
|
|
response.header['Range'] = "1-#{request.body.size}"
|
|
response.header['Docker-Upload-UUID'] = params[:uuid]
|
|
head 204
|
|
end
|
|
|
|
def finish_upload_blob
|
|
# error by client if no params[:digest]
|
|
|
|
uuid_file = tmp_file("#{params[:uuid]}.tar")
|
|
digest_file = tmp_file("#{params[:digest][7..-1]}.tar")
|
|
|
|
File.delete(digest_file) if File.exist? digest_file
|
|
File.rename(uuid_file, digest_file)
|
|
|
|
response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/#{params[:digest]}"
|
|
response.header['Docker-Content-Digest'] = params[:digest]
|
|
response.header['Content-Range'] = "1-#{File.size(digest_file)}"
|
|
response.header['Content-Length'] = "0"
|
|
response.header['Docker-Upload-UUID'] = params[:uuid]
|
|
head 201
|
|
end
|
|
|
|
def cancel_upload_blob
|
|
render plain: '', status: 200
|
|
end
|
|
|
|
def ping
|
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
render json: {}, status: 200
|
|
end
|
|
|
|
def v1_ping
|
|
head 200
|
|
end
|
|
|
|
def v1_search
|
|
options = {
|
|
resource_class: Katello::Repository
|
|
}
|
|
params[:per_page] = params[:n] || 25
|
|
params[:search] = params[:q]
|
|
search_results = scoped_search(readable_repositories.where(content_type: 'docker').distinct,
|
|
:container_repository_name, :asc, options)
|
|
results = {
|
|
num_results: search_results[:subtotal],
|
|
query: params[:search]
|
|
}
|
|
results[:results] = search_results[:results].collect do |repository|
|
|
{ name: repository[:container_repository_name], description: repository[:description] }
|
|
end
|
|
render json: results, status: 200
|
|
end
|
|
|
|
def catalog
|
|
repositories = readable_repositories.where(content_type: 'docker').collect do |repository|
|
|
repository.container_repository_name
|
|
end
|
|
render json: { repositories: repositories }
|
|
end
|
|
|
|
def tags_list
|
|
tags = @repository.docker_tags.collect do |tag|
|
|
tag.name
|
|
end
|
|
tags.uniq!
|
|
tags.sort!
|
|
render json: {
|
|
name: @repository.container_repository_name,
|
|
tags: tags
|
|
}
|
|
end
|
|
|
|
def create_manifest
|
|
filename = tmp_file('manifest.json')
|
|
if File.exist? filename
|
|
render_error('custom_error', :status => :unprocessable_entity,
|
|
:locals => { :message => "Upload already in progress" })
|
|
return nil
|
|
end
|
|
manifest = request.body.read
|
|
File.open(tmp_file('manifest.json'), 'wb', 0600) do |file|
|
|
file.write manifest
|
|
end
|
|
manifest = JSON.parse(manifest)
|
|
rescue
|
|
File.delete(tmp_file('manifest.json')) if File.exist? tmp_file('manifest.json')
|
|
end
|
|
|
|
def get_manifest_files(repository, manifest)
|
|
files = ['manifest.json']
|
|
if manifest['schemaVersion'] == 1
|
|
if manifest['fsLayers']
|
|
files += manifest['fsLayers'].collect do |layer|
|
|
layerfile = "#{layer['blobSum'][7..-1]}.tar"
|
|
force_include_layer(repository, layer['blobSum'], layerfile)
|
|
layerfile
|
|
end
|
|
end
|
|
elsif manifest['schemaVersion'] == 2
|
|
if manifest['layers']
|
|
files += manifest['layers'].collect do |layer|
|
|
layerfile = "#{layer['digest'][7..-1]}.tar"
|
|
force_include_layer(repository, layer['digest'], layerfile)
|
|
layerfile
|
|
end
|
|
end
|
|
files << "#{manifest['config']['digest'][7..-1]}.tar"
|
|
else
|
|
render_error 'custom_error', :status => :internal_server_error,
|
|
:locals => { :message => "Unsupported schema #{manifest['schemaVersion']}" }
|
|
return nil
|
|
end
|
|
files
|
|
end
|
|
|
|
def create_tar_file(files, repository, tag)
|
|
tar_file = "#{repository}_#{tag}.tar"
|
|
`/usr/bin/tar cf #{tmp_file(tar_file)} -C #{tmp_dir} #{files.join(' ')}`
|
|
|
|
files.each do |file|
|
|
filename = tmp_file(file)
|
|
File.delete(filename) if File.exist? filename
|
|
end
|
|
tar_file
|
|
end
|
|
|
|
def upload_manifest(tar_file)
|
|
upload_id = pulp_content.create_upload_request['upload_id']
|
|
filename = tmp_file(tar_file)
|
|
File.open(filename, 'rb') do |file|
|
|
pulp_content.upload_bits(upload_id, 0, file.read)
|
|
|
|
file.rewind
|
|
content = file.read
|
|
unit_keys = [{
|
|
name: filename,
|
|
size: file.size,
|
|
checksum: Digest::SHA256.hexdigest(content)
|
|
}]
|
|
unit_type_id = 'docker_manifest'
|
|
task = sync_task(::Actions::Katello::Repository::ImportUpload,
|
|
@repository, [upload_id], :unit_type_id => unit_type_id,
|
|
:unit_keys => unit_keys,
|
|
:generate_metadata => true, :sync_capsule => true)
|
|
digest = task.output['upload_results'][0]['digest']
|
|
|
|
File.delete(filename)
|
|
|
|
digest
|
|
end
|
|
ensure
|
|
pulp_content.delete_upload_request(upload_id) if upload_id
|
|
end
|
|
|
|
def upload_tag(digest, tag)
|
|
upload_id = pulp_content.create_upload_request['upload_id']
|
|
unit_keys = [{
|
|
name: tag,
|
|
digest: digest
|
|
}]
|
|
unit_type_id = 'docker_tag'
|
|
sync_task(::Actions::Katello::Repository::ImportUpload,
|
|
@repository, [upload_id], :unit_type_id => unit_type_id,
|
|
:unit_keys => unit_keys,
|
|
:generate_metadata => true, :sync_capsule => true)
|
|
|
|
tag
|
|
ensure
|
|
pulp_content.delete_upload_request(upload_id) if upload_id
|
|
end
|
|
|
|
def tmp_dir
|
|
"#{Rails.root}/tmp"
|
|
end
|
|
|
|
def tmp_file(filename)
|
|
File.join(tmp_dir, filename)
|
|
end
|
|
|
|
# TODO: Until pulp supports optional upload of layers, include all layers
|
|
# https://pulp.plan.io/issues/3497
|
|
def force_include_layer(repository, digest, layer)
|
|
unless File.exist? tmp_file(layer)
|
|
logger.debug "Getting blob #{digest} to write to #{layer}"
|
|
fullpath = "/v2/#{repository}/blobs/#{digest}"
|
|
request = Resources::Registry::Proxy.get(fullpath)
|
|
File.open(tmp_file(layer), 'wb', 0600) do |file|
|
|
file.write request.body
|
|
end
|
|
logger.debug "Wrote blob #{digest} to #{layer}"
|
|
end
|
|
end
|
|
|
|
def disable_strong_params
|
|
params.permit!
|
|
end
|
|
|
|
def confirm_settings
|
|
return true if SETTINGS[:katello][:registry]
|
|
render_error('custom_error', :status => :not_found,
|
|
:locals => { :message => "Registry not configured" })
|
|
false
|
|
end
|
|
|
|
def request_url
|
|
request.protocol + request.host_with_port
|
|
end
|
|
|
|
def logger
|
|
::Foreman::Logging.logger('katello/registry_proxy')
|
|
end
|
|
|
|
def route_name
|
|
Engine.routes.router.recognize(request) do |_, params|
|
|
break params[:action] if params[:action]
|
|
end
|
|
end
|
|
|
|
def process_action(method_name, *args)
|
|
::Api::V2::BaseController.instance_method(:process_action).bind(self).call(method_name, *args)
|
|
Rails.logger.debug "With body: #{response.body}\n" unless route_name == 'pull_blob'
|
|
end
|
|
end
|
|
end
|