Implement pull endpoint for Container Virtual Registry
What does this MR do and why?
References
Screenshots or screen recordings
NA
🔬 How to set up and validate locally
🛠️ 1. Setup
Enable Dependency Proxy, if not enabled.
Enable the feature flag:
Feature.enable(:container_virtual_registries)
Prepare a user, upstream, and its registry
current_user = User.first # root or any user with read_virtual_registry and write_virtual_registry permissions
upstream = VirtualRegistries::Container::Upstream.find(14) # adjust ID as needed
registry = VirtualRegistries::Container::Registry.find(5) # adjust ID as needed
We'll use DockerHub as the upstream. Thus, update the upstream
record to point to DockerHub and set it up with valid DockerHub credentials
upstream.url = 'https://registry-1.docker.io'
upstream.username = 'myusername'
upstream.password = 'mypassword'
upstream.save
🧑🍳 Create a cache entry for testing
2. require 'net/http'
require 'json'
# Fetch a real hello-world manifest from Docker Hub (smallest image)
puts "Fetching real hello-world manifest from Docker Hub..."
uri = URI('https://registry-1.docker.io/v2/library/hello-world/manifests/latest')
request = Net::HTTP::Get.new(uri)
request['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
manifest_content = response.body
manifest_json = JSON.parse(manifest_content)
manifest_etag = response['etag']&.gsub('"', '') || 'manifest-etag-12345'
puts "Manifest digest: #{manifest_etag}"
puts "Manifest content:"
puts JSON.pretty_generate(manifest_json)
# Create manifest cache entry
temp_file = Tempfile.new(['manifest', '.json'])
temp_file.write(manifest_content)
temp_file.rewind
uploaded_file = UploadedFile.new(
temp_file.path,
filename: 'latest',
sha1: Digest::SHA1.hexdigest(manifest_content),
md5: Digest::MD5.hexdigest(manifest_content)
)
service_response = VirtualRegistries::Container::Cache::Entries::CreateOrUpdateService.new(
upstream: upstream,
current_user: current_user,
params: {
path: 'hello-world/manifests/latest',
file: uploaded_file,
etag: manifest_etag,
content_type: 'application/vnd.docker.distribution.manifest.v2+json'
}
).execute
temp_file.close
temp_file.unlink
if service_response.success?
puts "Manifest cache entry created successfully!"
else
puts "Error creating manifest: #{service_response.message}"
next
end
# Extract the blob digest from the manifest
if manifest_json['layers'] && manifest_json['layers'].any?
digest = manifest_json['layers'].first['digest']
elsif manifest_json['fsLayers'] && manifest_json['fsLayers'].any?
digest = manifest_json['fsLayers'].first['blobSum']
else
puts "Could not find layer digest in manifest"
next
end
puts "\nFetching blob: #{digest}..."
# Fetch the blob from Docker Hub
blob_uri = URI("https://registry-1.docker.io/v2/library/hello-world/blobs/#{digest}")
blob_request = Net::HTTP::Get.new(blob_uri)
blob_response = Net::HTTP.start(blob_uri.hostname, blob_uri.port, use_ssl: true) do |http|
http.request(blob_request)
end
if blob_response.code != '200'
puts "Failed to fetch blob: #{blob_response.code}"
next
end
blob_content = blob_response.body
blob_etag = blob_response['etag']&.gsub('"', '') || 'blob-etag'
# Create blob cache entry
blob_temp_file = Tempfile.new(['blob', '.tar.gz'])
blob_temp_file.write(blob_content)
blob_temp_file.rewind
blob_uploaded_file = UploadedFile.new(
blob_temp_file.path,
filename: digest.split(':').last,
sha1: Digest::SHA1.hexdigest(blob_content),
md5: Digest::MD5.hexdigest(blob_content)
)
blob_service_response = VirtualRegistries::Container::Cache::Entries::CreateOrUpdateService.new(
upstream: upstream,
current_user: current_user,
params: {
path: "hello-world/blobs/#{digest}",
file: blob_uploaded_file,
etag: blob_etag,
content_type: 'application/octet-stream'
}
).execute
blob_temp_file.close
blob_temp_file.unlink
if blob_service_response.success?
puts "Blob cache entry created (#{(blob_content.size / 1024.0).round(2)} KB)"
else
puts "Error creating blob: #{blob_service_response.message}"
end
# Create cache entry for the config blob
if manifest_json['config']
config_digest = manifest_json['config']['digest']
puts "\nFetching config blob: #{config_digest}..."
config_uri = URI("https://registry-1.docker.io/v2/library/hello-world/blobs/#{config_digest}")
config_request = Net::HTTP::Get.new(config_uri)
config_response = Net::HTTP.start(config_uri.hostname, config_uri.port, use_ssl: true) do |http|
http.request(config_request)
end
if config_response.code == '200'
config_content = config_response.body
config_etag = config_response['etag']&.gsub('"', '') || 'config-etag'
config_temp_file = Tempfile.new(['config', '.json'])
config_temp_file.write(config_content)
config_temp_file.rewind
config_uploaded_file = UploadedFile.new(
config_temp_file.path,
filename: config_digest.split(':').last,
sha1: Digest::SHA1.hexdigest(config_content),
md5: Digest::MD5.hexdigest(config_content)
)
config_service_response = VirtualRegistries::Container::Cache::Entries::CreateOrUpdateService.new(
upstream: upstream,
current_user: current_user,
params: {
path: "hello-world/blobs/#{config_digest}",
file: config_uploaded_file,
etag: config_etag,
content_type: 'application/vnd.docker.container.image.v1+json'
}
).execute
config_temp_file.close
config_temp_file.unlink
if config_service_response.success?
puts "Config blob cache entry created"
else
puts "Error creating config blob: #{config_service_response.message}"
end
end
end
# Verify all cache entries were created
puts "\n" + "="*60
puts "Summary:"
puts " Total cache entries: #{upstream.reload.cache_entries.count}"
puts " Manifest: #{upstream.cache_entries.where('relative_path LIKE ?', '%/manifests/%').count}"
puts " Blobs: #{upstream.cache_entries.where('relative_path LIKE ?', '%/blobs/%').count}"
puts "="*60
🔓 Docker login to the virtual registry
3. docker login gdk.test:3000/virtual_registries/containers/<registry_id>
When the docker
client asks for the password, paste a personal access token of the user, with read_virtual_registry
and write_virtual_registry
permissions
🔽 Docker pull from the virtual registry
4. docker pull gdk.test:3000/virtual_registries/containers/<registry_id>/busybox:latest
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #549131
Edited by Radamanthus Batnag