[go: up one dir, main page]

Skip to content

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

2. 🧑‍🍳 Create a cache entry for testing

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

3. 🔓 Docker login to the virtual registry

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

4. 🔽 Docker pull from the virtual registry

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

Merge request reports

Loading