Commit 716154cb authored by Ian Baum's avatar Ian Baum Committed by Michael Kozono

Geo: Replicate multi-arch containers

parent cea3caef
...@@ -25,21 +25,36 @@ module Geo ...@@ -25,21 +25,36 @@ module Geo
private private
def sync_tag(tag) def sync_tag(tag)
file = nil
manifest = client.repository_raw_manifest(repository_path, tag[:name]) manifest = client.repository_raw_manifest(repository_path, tag[:name])
manifest_parsed = Gitlab::Json.parse(manifest) manifest_parsed = Gitlab::Json.parse(manifest)
list_blobs(manifest_parsed).each do |digest| case manifest_parsed['mediaType']
when ContainerRegistry::Client::DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE
push_manifest_blobs(manifest_parsed)
when ContainerRegistry::Client::DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE
manifest_parsed['manifests'].each do |submanifest|
image_info_raw = client.repository_raw_manifest(repository_path, submanifest['digest'])
image_info = Gitlab::Json.parse(image_info_raw)
push_manifest_blobs(image_info)
container_repository.push_manifest(submanifest['digest'], image_info_raw, image_info['mediaType'])
end
else
raise "Unexpected mediaType: #{manifest_parsed['mediaType']}"
end
container_repository.push_manifest(tag[:name], manifest, manifest_parsed['mediaType'])
end
def push_manifest_blobs(manifest)
list_blobs(manifest).each do |digest|
next if container_repository.blob_exists?(digest) next if container_repository.blob_exists?(digest)
file = client.pull_blob(repository_path, digest) file = client.pull_blob(repository_path, digest)
container_repository.push_blob(digest, file.path) begin
file.unlink container_repository.push_blob(digest, file.path)
ensure
file.unlink
end
end end
container_repository.push_manifest(tag[:name], manifest, manifest_parsed['mediaType'])
ensure
file.try(:unlink)
end end
def remove_tag(tag) def remove_tag(tag)
......
...@@ -94,7 +94,7 @@ module EE ...@@ -94,7 +94,7 @@ module EE
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def accept_raw_manifest(conn) def accept_raw_manifest(conn)
conn.headers['Accept'] = ::ContainerRegistry::Client::ACCEPTED_TYPES conn.headers['Accept'] = ::ContainerRegistry::Client::ACCEPTED_TYPES_RAW
end end
end end
end end
......
...@@ -24,6 +24,14 @@ RSpec.describe ContainerRegistry::Client do ...@@ -24,6 +24,14 @@ RSpec.describe ContainerRegistry::Client do
} }
end end
let(:headers_with_accept_types_with_list) do
{
'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json',
'Authorization' => 'bearer 12345',
'User-Agent' => "GitLab/#{Gitlab::VERSION}"
}
end
describe '#push_blob' do describe '#push_blob' do
let(:file) do let(:file) do
file = Tempfile.new('test1') file = Tempfile.new('test1')
...@@ -119,7 +127,7 @@ RSpec.describe ContainerRegistry::Client do ...@@ -119,7 +127,7 @@ RSpec.describe ContainerRegistry::Client do
it 'GET "/v2/:name/manifests/:reference' do it 'GET "/v2/:name/manifests/:reference' do
stub_request(:get, 'http://registry/v2/group/test/manifests/my-tag') stub_request(:get, 'http://registry/v2/group/test/manifests/my-tag')
.with(headers: headers_with_accept_types) .with(headers: headers_with_accept_types_with_list)
.to_return(status: 200, body: manifest, headers: {}) .to_return(status: 200, body: manifest, headers: {})
expect(client.repository_raw_manifest('group/test', 'my-tag')).to eq(manifest) expect(client.repository_raw_manifest('group/test', 'my-tag')).to eq(manifest)
......
...@@ -17,6 +17,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -17,6 +17,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
let(:manifest) do let(:manifest) do
"{" \ "{" \
"\n\"schemaVersion\":2," \ "\n\"schemaVersion\":2," \
"\n\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\"," \
"\n\"layers\":[" \ "\n\"layers\":[" \
"{\n\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\n\"size\":3333,\n\"digest\":\"sha256:3333\"}," \ "{\n\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\n\"size\":3333,\n\"digest\":\"sha256:3333\"}," \
"{\n\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\n\"size\":4444,\n\"digest\":\"sha256:4444\"}," \ "{\n\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\n\"size\":4444,\n\"digest\":\"sha256:4444\"}," \
...@@ -25,6 +26,26 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -25,6 +26,26 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
"}" "}"
end end
let(:manifest_list) do
%Q(
{
"schemaVersion":2,
"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json",
"manifests":[
{
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"size":6666,
"digest":"sha256:6666",
"platform":
{
"architecture":"arm64","os":"linux"
}
}
]
}
)
end
before do before do
stub_container_registry_config(enabled: true, api_url: secondary_api_url) stub_container_registry_config(enabled: true, api_url: secondary_api_url)
stub_registry_replication_config(enabled: true, primary_api_url: primary_api_url) stub_registry_replication_config(enabled: true, primary_api_url: primary_api_url)
...@@ -61,6 +82,16 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -61,6 +82,16 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
.to_return(status: 200, body: manifest, headers: {}) .to_return(status: 200, body: manifest, headers: {})
end end
def stub_secondary_raw_manifest_request(repository_url, tag, manifest)
stub_request(:get, "#{repository_url}/manifests/#{tag}")
.to_return(status: 200, body: manifest, headers: {})
end
def stub_primary_raw_manifest_list_request(repository_url, tag, manifest)
stub_request(:get, "#{repository_url}/manifests/#{tag}")
.to_return(status: 200, body: manifest_list, headers: {})
end
def stub_secondary_push_manifest_request(repository_url, tag, manifest) def stub_secondary_push_manifest_request(repository_url, tag, manifest)
stub_request(:put, "#{repository_url}/manifests/#{tag}") stub_request(:put, "#{repository_url}/manifests/#{tag}")
.with(body: manifest) .with(body: manifest)
...@@ -82,26 +113,46 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -82,26 +113,46 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
describe '#execute' do describe '#execute' do
subject { described_class.new(container_repository) } subject { described_class.new(container_repository) }
it 'determines list of tags to sync and to remove correctly' do context 'single manifest' do
stub_primary_repository_tags_requests(primary_repository_url, { 'tag-to-sync' => 'sha256:1111' }) it 'determines list of tags to sync and to remove correctly' do
stub_secondary_repository_tags_requests(secondary_repository_url, { 'tag-to-remove' => 'sha256:2222' }) stub_primary_repository_tags_requests(primary_repository_url, { 'tag-to-sync' => 'sha256:1111' })
stub_primary_raw_manifest_request(primary_repository_url, 'tag-to-sync', manifest) stub_secondary_repository_tags_requests(secondary_repository_url, { 'tag-to-remove' => 'sha256:2222' })
stub_missing_blobs_requests(primary_repository_url, secondary_repository_url, { 'sha256:3333' => true, 'sha256:4444' => false }) stub_primary_raw_manifest_request(primary_repository_url, 'tag-to-sync', manifest)
stub_secondary_push_manifest_request(secondary_repository_url, 'tag-to-sync', manifest) stub_missing_blobs_requests(primary_repository_url, secondary_repository_url, { 'sha256:3333' => true, 'sha256:4444' => false })
stub_secondary_push_manifest_request(secondary_repository_url, 'tag-to-sync', manifest)
expect(container_repository).to receive(:push_blob).with('sha256:3333', anything) expect(container_repository).to receive(:push_blob).with('sha256:3333', anything)
expect(container_repository).not_to receive(:push_blob).with('sha256:4444', anything) expect(container_repository).not_to receive(:push_blob).with('sha256:4444', anything)
expect(container_repository).not_to receive(:push_blob).with('sha256:5555', anything) expect(container_repository).not_to receive(:push_blob).with('sha256:5555', anything)
expect(container_repository).to receive(:delete_tag_by_digest).with('sha256:2222') expect(container_repository).to receive(:delete_tag_by_digest).with('sha256:2222')
subject.execute subject.execute
end end
context 'when primary repository has no tags' do context 'when primary repository has no tags' do
it 'removes secondary tags and does not fail' do it 'removes secondary tags and does not fail' do
stub_primary_repository_tags_requests(primary_repository_url, {}) stub_primary_repository_tags_requests(primary_repository_url, {})
stub_secondary_repository_tags_requests(secondary_repository_url, { 'tag-to-remove' => 'sha256:2222' }) stub_secondary_repository_tags_requests(secondary_repository_url, { 'tag-to-remove' => 'sha256:2222' })
expect(container_repository).to receive(:delete_tag_by_digest).with('sha256:2222')
subject.execute
end
end
end
context 'manifest list' do
it 'pushes the correct blobs and manifests' do
stub_primary_repository_tags_requests(primary_repository_url, { 'tag-to-sync' => 'sha256:1111' })
stub_secondary_repository_tags_requests(secondary_repository_url, {})
stub_primary_raw_manifest_list_request(primary_repository_url, 'tag-to-sync', manifest_list)
stub_primary_raw_manifest_request(primary_repository_url, 'sha256:6666', manifest)
stub_secondary_raw_manifest_request(secondary_repository_url, 'sha256:6666', manifest)
stub_missing_blobs_requests(primary_repository_url, secondary_repository_url, { 'sha256:3333' => true, 'sha256:4444' => false })
expect(container_repository).to receive(:push_blob).with('sha256:3333', anything)
expect(container_repository).to receive(:push_manifest).with('sha256:6666', anything, anything)
expect(container_repository).to receive(:push_manifest).with('tag-to-sync', anything, anything)
expect(container_repository).to receive(:delete_tag_by_digest).with('sha256:2222') expect(container_repository).to receive(:delete_tag_by_digest).with('sha256:2222')
subject.execute subject.execute
......
...@@ -11,6 +11,7 @@ module ContainerRegistry ...@@ -11,6 +11,7 @@ module ContainerRegistry
attr_accessor :uri attr_accessor :uri
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json' DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json' OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json' CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version' REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
...@@ -19,6 +20,8 @@ module ContainerRegistry ...@@ -19,6 +20,8 @@ module ContainerRegistry
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
ACCEPTED_TYPES_RAW = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE, DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE].freeze
# Taken from: FaradayMiddleware::FollowRedirects # Taken from: FaradayMiddleware::FollowRedirects
REDIRECT_CODES = Set.new [301, 302, 303, 307] REDIRECT_CODES = Set.new [301, 302, 303, 307]
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment