Commit a8545c04 authored by Steve Abrams's avatar Steve Abrams Committed by David Fernandez

Fix manifest workhorse upload

Use find or create logic when uploading a new
manifest rather than only create to prevent
collisions.

Changelog: fixed
parent 78703db4
...@@ -73,13 +73,23 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy ...@@ -73,13 +73,23 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end end
def upload_manifest def upload_manifest
@group.dependency_proxy_manifests.create!( attrs = {
file_name: manifest_file_name, file_name: manifest_file_name,
content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER], content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
digest: request.headers['Docker-Content-Digest'], digest: request.headers[DependencyProxy::Manifest::DIGEST_HEADER],
file: params[:file], file: params[:file],
size: params[:file].size size: params[:file].size
) }
manifest = @group.dependency_proxy_manifests
.active
.find_by_file_name(manifest_file_name)
if manifest
manifest.update!(attrs)
else
@group.dependency_proxy_manifests.create!(attrs)
end
event_name = tracking_event_name(object_type: :manifest, from_cache: false) event_name = tracking_event_name(object_type: :manifest, from_cache: false)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user) track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
...@@ -105,7 +115,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy ...@@ -105,7 +115,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
def send_manifest(manifest, from_cache:) def send_manifest(manifest, from_cache:)
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536 # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
manifest.touch manifest.touch
response.headers['Docker-Content-Digest'] = manifest.digest response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
response.headers['Content-Length'] = manifest.size response.headers['Content-Length'] = manifest.size
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
response.headers['Etag'] = "\"#{manifest.digest}\"" response.headers['Etag'] = "\"#{manifest.digest}\""
......
...@@ -8,6 +8,7 @@ class DependencyProxy::Manifest < ApplicationRecord ...@@ -8,6 +8,7 @@ class DependencyProxy::Manifest < ApplicationRecord
belongs_to :group belongs_to :group
MAX_FILE_SIZE = 10.megabytes.freeze MAX_FILE_SIZE = 10.megabytes.freeze
DIGEST_HEADER = 'Docker-Content-Digest'
validates :group, presence: true validates :group, presence: true
validates :file, presence: true validates :file, presence: true
......
...@@ -14,7 +14,10 @@ module DependencyProxy ...@@ -14,7 +14,10 @@ module DependencyProxy
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS)) response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
if response.success? if response.success?
success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']) success(
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
content_type: response.headers['content-type']
)
else else
error(response.body, response.code) error(response.body, response.code)
end end
......
...@@ -20,7 +20,13 @@ module DependencyProxy ...@@ -20,7 +20,13 @@ module DependencyProxy
file.write(response.body) file.write(response.body)
file.flush file.flush
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])) yield(
success(
file: file,
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
content_type: response.headers['content-type']
)
)
ensure ensure
file.close file.close
file.unlink file.unlink
......
...@@ -60,7 +60,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -60,7 +60,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
tags.each do |tag, digest| tags.each do |tag, digest|
stub_request(:head, "#{repository_url}/manifests/#{tag}") stub_request(:head, "#{repository_url}/manifests/#{tag}")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest }) .to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
end end
end end
...@@ -73,7 +73,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do ...@@ -73,7 +73,7 @@ RSpec.describe Geo::ContainerRepositorySync, :geo do
tags.each do |tag, digest| tags.each do |tag, digest|
stub_request(:head, "#{repository_url}/manifests/#{tag}") stub_request(:head, "#{repository_url}/manifests/#{tag}")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest }) .to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
end end
end end
......
...@@ -90,7 +90,7 @@ module ContainerRegistry ...@@ -90,7 +90,7 @@ module ContainerRegistry
def repository_tag_digest(name, reference) def repository_tag_digest(name, reference)
response = faraday.head("/v2/#{name}/manifests/#{reference}") response = faraday.head("/v2/#{name}/manifests/#{reference}")
response.headers['docker-content-digest'] if response.success? response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
end end
def delete_repository_tag_by_digest(name, reference) def delete_repository_tag_by_digest(name, reference)
...@@ -171,7 +171,7 @@ module ContainerRegistry ...@@ -171,7 +171,7 @@ module ContainerRegistry
req.body = Gitlab::Json.pretty_generate(manifest) req.body = Gitlab::Json.pretty_generate(manifest)
end end
response.headers['docker-content-digest'] if response.success? response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
end end
private private
......
...@@ -425,28 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -425,28 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end end
end end
describe 'GET #authorize_upload_blob' do describe 'POST #authorize_upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' } let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE } let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
subject do subject do
request.headers.merge!(workhorse_internal_api_request_header) request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha } post :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
end end
it_behaves_like 'without permission' it_behaves_like 'without permission'
it_behaves_like 'authorize action with permission' it_behaves_like 'authorize action with permission'
end end
describe 'GET #upload_blob' do describe 'POST #upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' } let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') } let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
subject do subject do
request.headers.merge!(workhorse_internal_api_request_header) request.headers.merge!(workhorse_internal_api_request_header)
get :upload_blob, params: { post :upload_blob, params: {
group_id: group.to_param, group_id: group.to_param,
image: 'alpine', image: 'alpine',
sha: blob_sha, sha: blob_sha,
...@@ -469,31 +469,45 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -469,31 +469,45 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end end
end end
describe 'GET #authorize_upload_manifest' do describe 'POST #authorize_upload_manifest' do
let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE } let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
subject do subject do
request.headers.merge!(workhorse_internal_api_request_header) request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' } post :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
end end
it_behaves_like 'without permission' it_behaves_like 'without permission'
it_behaves_like 'authorize action with permission' it_behaves_like 'authorize action with permission'
end end
describe 'GET #upload_manifest' do describe 'POST #upload_manifest' do
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') } let_it_be(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
let_it_be(:image) { 'alpine' }
let_it_be(:tag) { 'latest' }
let_it_be(:content_type) { 'v2/manifest' }
let_it_be(:digest) { 'foo' }
let_it_be(:file_name) { "#{image}:#{tag}.json" }
subject do subject do
request.headers.merge!(workhorse_internal_api_request_header) request.headers.merge!(
workhorse_internal_api_request_header.merge!(
get :upload_manifest, params: { {
Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER => content_type,
DependencyProxy::Manifest::DIGEST_HEADER => digest
}
)
)
params = {
group_id: group.to_param, group_id: group.to_param,
image: 'alpine', image: image,
tag: 'latest', tag: tag,
file: file file: file,
file_name: file_name
} }
post :upload_manifest, params: params
end end
it_behaves_like 'without permission' it_behaves_like 'without permission'
...@@ -501,13 +515,30 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -501,13 +515,30 @@ RSpec.describe Groups::DependencyProxyForContainersController do
context 'with a valid user' do context 'with a valid user' do
before do before do
group.add_guest(user) group.add_guest(user)
end
expect_next_found_instance_of(Group) do |instance| it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
expect(instance).to receive_message_chain(:dependency_proxy_manifests, :create!)
context 'with no existing manifest' do
it 'creates a manifest' do
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
manifest = group.dependency_proxy_manifests.first.reload
expect(manifest.content_type).to eq(content_type)
expect(manifest.digest).to eq(digest)
expect(manifest.file_name).to eq(file_name)
end end
end end
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest' context 'with existing stale manifest' do
let_it_be(:old_digest) { 'asdf' }
let_it_be_with_reload(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, digest: old_digest, group: group) }
it 'updates the existing manifest' do
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(0)
.and change { manifest.reload.digest }.from(old_digest).to(digest)
end
end
end end
end end
......
...@@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do ...@@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do
it 'uploads the manifest and returns the digest' do it 'uploads the manifest and returns the digest' do
stub_request(:put, "http://container-registry/v2/path/manifests/tagA") stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
.with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers) .with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers)
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' }) .to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:123' })
expect_new_faraday(timeout: false) expect_new_faraday(timeout: false)
......
...@@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do ...@@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do
before do before do
stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag') stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
.with(headers: headers) .with(headers: headers)
.to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) .to_return(status: 200, headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:digest' })
end end
describe '#digest' do describe '#digest' do
......
...@@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do ...@@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:token) { Digest::SHA256.hexdigest('123') } let(:token) { Digest::SHA256.hexdigest('123') }
let(:headers) do let(:headers) do
{ {
'docker-content-digest' => dependency_proxy_manifest.digest, DependencyProxy::Manifest::DIGEST_HEADER => dependency_proxy_manifest.digest,
'content-type' => dependency_proxy_manifest.content_type 'content-type' => dependency_proxy_manifest.content_type
} }
end end
...@@ -100,8 +100,8 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do ...@@ -100,8 +100,8 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:content_type) { 'new-content-type' } let(:content_type) { 'new-content-type' }
before do before do
stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type }) stub_manifest_head(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type }) stub_manifest_download(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
end end
it_behaves_like 'returning no manifest' it_behaves_like 'returning no manifest'
......
...@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do ...@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do
let(:content_type) { 'foo' } let(:content_type) { 'foo' }
let(:headers) do let(:headers) do
{ {
'docker-content-digest' => digest, DependencyProxy::Manifest::DIGEST_HEADER => digest,
'content-type' => content_type 'content-type' => content_type
} }
end end
......
...@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do ...@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do
let(:digest) { '12345' } let(:digest) { '12345' }
let(:content_type) { 'foo' } let(:content_type) { 'foo' }
let(:headers) do let(:headers) do
{ 'docker-content-digest' => digest, 'content-type' => content_type } { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }
end end
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) } subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
......
...@@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d ...@@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d
end end
end end
def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' }) def stub_put_manifest_request(tag, status = 200, headers = { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '', headers: headers) .to_return(status: status, body: '', headers: headers)
end end
def stub_tag_digest(tag, digest) def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest }) .to_return(status: 200, body: '', headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
end end
def stub_digest_config(digest, created_at) def stub_digest_config(digest, created_at)
......
...@@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do ...@@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest) expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size) expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION) expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"") expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
......
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