Commit d816489a authored by Stan Hu's avatar Stan Hu

Merge branch '31832-improve-performance-of-the-container-registry-delete-tags-api' into 'master'

Improve performance of the Container Registry delete tags API

See merge request gitlab-org/gitlab!23325
parents 1e32c494 76ab17ae
...@@ -77,7 +77,11 @@ class ContainerRepository < ApplicationRecord ...@@ -77,7 +77,11 @@ class ContainerRepository < ApplicationRecord
end end
def delete_tag_by_digest(digest) def delete_tag_by_digest(digest)
client.delete_repository_tag(self.path, digest) client.delete_repository_tag_by_digest(self.path, digest)
end
def delete_tag_by_name(name)
client.delete_repository_tag_by_name(self.path, name)
end end
def self.build_from_path(path) def self.build_from_path(path)
......
...@@ -14,12 +14,25 @@ module Projects ...@@ -14,12 +14,25 @@ module Projects
private private
# Delete tags by name with a single DELETE request. This is only supported
# by the GitLab Container Registry fork. See
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details.
def fast_delete(container_repository, tag_names)
deleted_tags = tag_names.select do |name|
container_repository.delete_tag_by_name(name)
end
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
end
# Replace a tag on the registry with a dummy tag. # Replace a tag on the registry with a dummy tag.
# This is a hack as the registry doesn't support deleting individual # This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it. # tags. This code effectively pushes a dummy image and assigns the tag to it.
# This way when the tag is deleted only the dummy image is affected. # This way when the tag is deleted only the dummy image is affected.
# This is used to preverse compatibility with third-party registries that
# don't support fast delete.
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
def smart_delete(container_repository, tag_names) def slow_delete(container_repository, tag_names)
# generates the blobs for the dummy image # generates the blobs for the dummy image
dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path) dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
return error('could not generate manifest') if dummy_manifest.nil? return error('could not generate manifest') if dummy_manifest.nil?
...@@ -36,6 +49,15 @@ module Projects ...@@ -36,6 +49,15 @@ module Projects
end end
end end
def smart_delete(container_repository, tag_names)
fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
if fast_delete_enabled && container_repository.client.supports_tag_delete?
fast_delete(container_repository, tag_names)
else
slow_delete(container_repository, tag_names)
end
end
# update the manifests of the tags with the new dummy image # update the manifests of the tags with the new dummy image
def replace_tag_manifests(container_repository, dummy_manifest, tag_names) def replace_tag_manifests(container_repository, dummy_manifest, tag_names)
deleted_tags = {} deleted_tags = {}
......
---
title: Improve performance of the Container Registry delete tags API
merge_request: 23325
author:
type: performance
...@@ -6,6 +6,8 @@ require 'digest' ...@@ -6,6 +6,8 @@ require 'digest'
module ContainerRegistry module ContainerRegistry
class Client class Client
include Gitlab::Utils::StrongMemoize
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'
...@@ -35,10 +37,25 @@ module ContainerRegistry ...@@ -35,10 +37,25 @@ module ContainerRegistry
response.headers['docker-content-digest'] if response.success? response.headers['docker-content-digest'] if response.success?
end end
def delete_repository_tag(name, reference) def delete_repository_tag_by_digest(name, reference)
result = faraday.delete("/v2/#{name}/manifests/#{reference}") delete_if_exists("/v2/#{name}/manifests/#{reference}")
end
result.success? || result.status == 404 def delete_repository_tag_by_name(name, reference)
delete_if_exists("/v2/#{name}/tags/reference/#{reference}")
end
# Check if the registry supports tag deletion. This is only supported by the
# GitLab registry fork. The fastest and safest way to check this is to send
# an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random
# repository name and tag (the registry won't check if they exist).
# Registries that support tag deletion will reply with a 200 OK and include
# the DELETE method in the Allow header. Others reply with an 404 Not Found.
def supports_tag_delete?
strong_memoize(:supports_tag_delete) do
response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {})
response.success? && response.headers['allow']&.include?('DELETE')
end
end end
def upload_raw_blob(path, blob) def upload_raw_blob(path, blob)
...@@ -86,9 +103,7 @@ module ContainerRegistry ...@@ -86,9 +103,7 @@ module ContainerRegistry
end end
def delete_blob(name, digest) def delete_blob(name, digest)
result = faraday.delete("/v2/#{name}/blobs/#{digest}") delete_if_exists("/v2/#{name}/blobs/#{digest}")
result.success? || result.status == 404
end end
def put_tag(name, reference, manifest) def put_tag(name, reference, manifest)
...@@ -163,6 +178,12 @@ module ContainerRegistry ...@@ -163,6 +178,12 @@ module ContainerRegistry
conn.adapter :net_http conn.adapter :net_http
end end
end end
def delete_if_exists(path)
result = faraday.delete(path)
result.success? || result.status == 404
end
end end
end end
......
...@@ -118,7 +118,7 @@ module ContainerRegistry ...@@ -118,7 +118,7 @@ module ContainerRegistry
def unsafe_delete def unsafe_delete
return unless digest return unless digest
client.delete_repository_tag(repository.path, digest) client.delete_repository_tag_by_digest(repository.path, digest)
end end
end end
end end
...@@ -146,4 +146,57 @@ describe ContainerRegistry::Client do ...@@ -146,4 +146,57 @@ describe ContainerRegistry::Client do
expect(subject).to eq 'sha256:123' expect(subject).to eq 'sha256:123'
end end
end end
describe '#delete_repository_tag_by_name' do
subject { client.delete_repository_tag_by_name('group/test', 'a') }
context 'when the tag exists' do
before do
stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
.to_return(status: 200, body: "")
end
it { is_expected.to be_truthy }
end
context 'when the tag does not exist' do
before do
stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
.to_return(status: 404, body: "")
end
it { is_expected.to be_truthy }
end
context 'when an error occurs' do
before do
stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
.to_return(status: 500, body: "")
end
it { is_expected.to be_falsey }
end
end
describe '#supports_tag_delete?' do
subject { client.supports_tag_delete? }
context 'when the server supports tag deletion' do
before do
stub_request(:options, "http://container-registry/v2/name/tags/reference/tag")
.to_return(status: 200, body: "", headers: { 'Allow' => 'DELETE' })
end
it { is_expected.to be_truthy }
end
context 'when the server does not support tag deletion' do
before do
stub_request(:options, "http://container-registry/v2/name/tags/reference/tag")
.to_return(status: 404, body: "")
end
it { is_expected.to be_falsey }
end
end
end end
...@@ -85,7 +85,7 @@ describe ContainerRepository do ...@@ -85,7 +85,7 @@ describe ContainerRepository do
context 'when action succeeds' do context 'when action succeeds' do
it 'returns status that indicates success' do it 'returns status that indicates success' do
expect(repository.client) expect(repository.client)
.to receive(:delete_repository_tag) .to receive(:delete_repository_tag_by_digest)
.twice .twice
.and_return(true) .and_return(true)
...@@ -96,7 +96,7 @@ describe ContainerRepository do ...@@ -96,7 +96,7 @@ describe ContainerRepository do
context 'when action fails' do context 'when action fails' do
it 'returns status that indicates failure' do it 'returns status that indicates failure' do
expect(repository.client) expect(repository.client)
.to receive(:delete_repository_tag) .to receive(:delete_repository_tag_by_digest)
.twice .twice
.and_return(false) .and_return(false)
...@@ -105,6 +105,36 @@ describe ContainerRepository do ...@@ -105,6 +105,36 @@ describe ContainerRepository do
end end
end end
describe '#delete_tag_by_name' do
let(:repository) do
create(:container_repository, name: 'my_image',
tags: { latest: '123', rc1: '234' },
project: project)
end
context 'when action succeeds' do
it 'returns status that indicates success' do
expect(repository.client)
.to receive(:delete_repository_tag_by_name)
.with(repository.path, "latest")
.and_return(true)
expect(repository.delete_tag_by_name('latest')).to be_truthy
end
end
context 'when action fails' do
it 'returns status that indicates failure' do
expect(repository.client)
.to receive(:delete_repository_tag_by_name)
.with(repository.path, "latest")
.and_return(false)
expect(repository.delete_tag_by_name('latest')).to be_falsey
end
end
end
describe '#location' do describe '#location' do
context 'when registry is running on a custom port' do context 'when registry is running on a custom port' do
before do before do
......
...@@ -41,7 +41,7 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -41,7 +41,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
let(:params) { {} } let(:params) { {} }
it 'does not remove anything' do it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag) expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to include(status: :success, deleted: []) is_expected.to include(status: :success, deleted: [])
end end
...@@ -156,7 +156,7 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -156,7 +156,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
def expect_delete(digest) def expect_delete(digest)
expect_any_instance_of(ContainerRegistry::Client) expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag) .to receive(:delete_repository_tag_by_digest)
.with(repository.path, digest) { true } .with(repository.path, digest) { true }
end end
end end
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