提交 829e846d 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 c7bdf253
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();
import { __ } from '~/locale';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
const tokenKeys = [
{
formattedKey: __('Status'),
key: 'status',
type: 'string',
param: 'status',
symbol: '',
icon: 'messages',
tag: 'status',
},
{
formattedKey: __('Type'),
key: 'type',
type: 'string',
param: 'type',
symbol: '',
icon: 'cube',
tag: 'type',
},
];
const GroupRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
export default GroupRunnersFilteredSearchTokenKeys;
......@@ -294,6 +294,14 @@ export default {
data-qa-selector="alert_threshold_field"
/>
</gl-form-group>
<gl-form-group
v-if="glFeatures.alertRunbooks"
:label="s__('PrometheusAlerts|Runbook')"
label-for="alert-runbook"
data-testid="alertRunbookField"
>
<gl-form-input id="alert-runbook" :disabled="formDisabled" type="text" />
</gl-form-group>
</div>
<template #modal-ok>
<gl-link
......
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();
......@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import initVariableList from '~/ci_variable_list';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
......@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ADMIN_RUNNERS,
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
filteredSearchTokenKeys: GroupRunnersFilteredSearchTokenKeys,
anchor: FILTERED_SEARCH.GROUP_RUNNERS_ANCHOR,
});
......
......@@ -224,12 +224,6 @@
font-size: $gl-font-size-large;
}
}
.notifications-btn {
.fa-bell {
margin-right: 0;
}
}
}
.nav > .project-buttons {
......
......@@ -10,6 +10,7 @@ module Projects
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
push_frontend_feature_flag(:alert_runbooks)
end
def show
......
......@@ -42,8 +42,8 @@ module Projects
# Technical Debt: https://gitlab.com/gitlab-org/gitlab/issues/207267
# name_regex to be removed when container_expiration_policies is updated
# to have both regex columns
regex_delete = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
regex_retain = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
tags.select do |tag|
# regex_retain will override any overlapping matches by regex_delete
......@@ -81,11 +81,11 @@ module Projects
def valid_regex?
%w(name_regex_delete name_regex name_regex_keep).each do |param_name|
regex = params[param_name]
Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
end
true
rescue RegexpError => e
Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
false
end
end
......
......@@ -6,65 +6,35 @@ module Projects
LOG_DATA_BASE = { service_class: self.to_s }.freeze
def execute(container_repository)
@container_repository = container_repository
return error('access denied') unless can?(current_user, :destroy_container_image, project)
tag_names = params[:tags]
return error('not tags specified') if tag_names.blank?
@tag_names = params[:tags]
return error('not tags specified') if @tag_names.blank?
smart_delete(container_repository, tag_names)
delete_tags
end
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')
def delete_tags
delete_service.execute
.tap(&method(:log_response))
end
# Replace a tag on the registry with a dummy tag.
# 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.
# 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
def slow_delete(container_repository, tag_names)
# generates the blobs for the dummy image
dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
return error('could not generate manifest') if dummy_manifest.nil?
deleted_tags = replace_tag_manifests(container_repository, dummy_manifest, tag_names)
def delete_service
fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
success(deleted: deleted_tags.keys)
if fast_delete_enabled && @container_repository.client.supports_tag_delete?
::Projects::ContainerRepository::Gitlab::DeleteTagsService.new(@container_repository, @tag_names)
else
error('could not delete tags')
::Projects::ContainerRepository::ThirdParty::DeleteTagsService.new(@container_repository, @tag_names)
end
end
def smart_delete(container_repository, tag_names)
fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
response = if fast_delete_enabled && container_repository.client.supports_tag_delete?
fast_delete(container_repository, tag_names)
else
slow_delete(container_repository, tag_names)
end
response.tap { |r| log_response(r, container_repository) }
end
def log_response(response, container_repository)
def log_response(response)
log_data = LOG_DATA_BASE.merge(
container_repository_id: container_repository.id,
container_repository_id: @container_repository.id,
message: 'deleted tags'
)
......@@ -76,26 +46,6 @@ module Projects
log_error(log_data)
end
end
# update the manifests of the tags with the new dummy image
def replace_tag_manifests(container_repository, dummy_manifest, tag_names)
deleted_tags = {}
tag_names.each do |name|
digest = container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
next unless digest
deleted_tags[name] = digest
end
# make sure the digests are the same (it should always be)
digests = deleted_tags.values.uniq
# rubocop: disable CodeReuse/ActiveRecord
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many?
deleted_tags
end
end
end
end
# frozen_string_literal: true
module Projects
module ContainerRepository
module Gitlab
class DeleteTagsService
include BaseServiceUtility
def initialize(container_repository, tag_names)
@container_repository = container_repository
@tag_names = tag_names
end
# 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 execute
return success(deleted: []) if @tag_names.empty?
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
end
end
end
end
# frozen_string_literal: true
module Projects
module ContainerRepository
module ThirdParty
class DeleteTagsService
include BaseServiceUtility
def initialize(container_repository, tag_names)
@container_repository = container_repository
@tag_names = tag_names
end
# Replace a tag on the registry with a dummy tag.
# 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.
# 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
def execute
return success(deleted: []) if @tag_names.empty?
# generates the blobs for the dummy image
dummy_manifest = @container_repository.client.generate_empty_manifest(@container_repository.path)
return error('could not generate manifest') if dummy_manifest.nil?
deleted_tags = replace_tag_manifests(dummy_manifest)
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
success(deleted: deleted_tags.keys)
else
error('could not delete tags')
end
end
private
# update the manifests of the tags with the new dummy image
def replace_tag_manifests(dummy_manifest)
deleted_tags = {}
@tag_names.each do |name|
digest = @container_repository.client.put_tag(@container_repository.path, name, dummy_manifest)
next unless digest
deleted_tags[name] = digest
end
# make sure the digests are the same (it should always be)
digests = deleted_tags.values.uniq
# rubocop: disable CodeReuse/ActiveRecord
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many?
deleted_tags
end
end
end
end
end
......@@ -17,16 +17,16 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
%button.dropdown-new.btn.btn-defaul.btn-icon.gl-button.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= sprite_icon("notifications", size: 16, css_class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle.d-flex{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
%button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= sprite_icon("notifications", size: 16, css_class: "js-notification-loading")
= notification_title(notification_setting.level)
.float-right
= icon("caret-down")
......
- if project.archived
%span.d-flex.badge.badge-warning
%span.d-flex.badge-pill.gl-badge.badge-warning.gl-ml-3
= _('archived')
---
title: Replace fa-bell icons with GitLab SVG notifications icon
merge_request: 37676
author:
type: changed
---
title: Fix bug in group runners filtered search
merge_request: 37626
author: Arthur de Lapertosa Lisboa
type: fixed
---
title: Use new badge style for 'archived' project badge
merge_request: 38013
author:
type: changed
......@@ -283,6 +283,7 @@ Settings.sentry['clientside_dsn'] ||= nil
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
Settings['pages'] = ::Gitlab::Pages::Settings.new(Settings.pages) # For path access detection https://gitlab.com/gitlab-org/gitlab/-/issues/230702
Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
Settings.pages['access_control'] = false if Settings.pages['access_control'].nil?
Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
......
# frozen_string_literal: true
module Gitlab
class Pages
module Pages
VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'.freeze
MAX_SIZE = 1.terabyte
......
# frozen_string_literal: true
module Gitlab
module Pages
class Settings < ::SimpleDelegator
DiskAccessDenied = Class.new(StandardError)
def path
if ::Gitlab::Runtime.web_server? && ENV['GITLAB_PAGES_DENY_DISK_ACCESS'] == '1'
begin
raise DiskAccessDenied
rescue DiskAccessDenied => ex
::Gitlab::ErrorTracking.track_exception(ex)
end
end
super
end
end
end
end
......@@ -19033,6 +19033,9 @@ msgstr ""
msgid "PrometheusAlerts|Operator"
msgstr ""
msgid "PrometheusAlerts|Runbook"
msgstr ""
msgid "PrometheusAlerts|Select query"
msgstr ""
......@@ -21153,9 +21156,6 @@ msgstr ""
msgid "SecurityReports|No vulnerabilities found"
msgstr ""
msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
......@@ -21258,9 +21258,6 @@ msgstr ""
msgid "SecurityReports|Undo dismiss"
msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
......
......@@ -8,9 +8,12 @@ module QA
#
module Members
def add_member(user, access_level = AccessLevel::DEVELOPER)
QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}])
Support::Retrier.retry_until do
QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}])
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
response = post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
response.code == QA::Support::Api::HTTP_STATUS_CREATED
end
end
def remove_member(user)
......
......@@ -27,7 +27,10 @@ module QA
# The number of selectors should be able to be reduced after
# migration to the new spinner is complete.
# https://gitlab.com/groups/gitlab-org/-/epics/956
Capybara.page.has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: wait)
# retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485
Support::Retrier.retry_on_exception do
Capybara.page.has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: wait)
end
end
end
end
......
......@@ -274,6 +274,8 @@ module Trigger
def create_remote_branch!
Gitlab.create_branch(downstream_project_path, ref, 'master')
puts "=> Remote branch '#{ref}' created"
rescue Gitlab::Error::BadRequest
puts "=> Remote branch '#{ref}' already exists!"
end
def cancel_latest_pipeline!
......@@ -292,8 +294,6 @@ module Trigger
# Cancel the pipeline
Gitlab.cancel_pipeline(downstream_project_path, pipeline_id)
rescue Gitlab::Error::BadRequest
puts "=> Remote branch '#{ref}' already exists!"
end
def display_success_message
......
......@@ -450,5 +450,19 @@ RSpec.describe 'Runners' do
expect(all(:link, href: group_runner_path(group, runner)).length).to eq(1)
end
end
context 'filtered search' do
it 'allows user to search by status and type', :js do
visit group_settings_ci_cd_path(group)
find('.filtered-search').click
page.within('#js-dropdown-hint') do
expect(page).to have_content('Status')
expect(page).to have_content('Type')
expect(page).not_to have_content('Tag')
end
end
end
end
end
......@@ -29,7 +29,7 @@ describe('AlertWidgetForm', () => {
configuredAlert: metricId,
};
function createComponent(props = {}) {
function createComponent(props = {}, featureFlags = {}) {
const propsData = {
...defaultProps,
...props,
......@@ -37,6 +37,9 @@ describe('AlertWidgetForm', () => {
wrapper = shallowMount(AlertWidgetForm, {
propsData,
provide: {
glFeatures: featureFlags,
},
stubs: {
GlModal: ModalStub,
},
......@@ -46,6 +49,7 @@ describe('AlertWidgetForm', () => {
const modal = () => wrapper.find(ModalStub);
const modalTitle = () => modal().attributes('title');
const submitButton = () => modal().find(GlLink);
const alertRunbookField = () => wrapper.find('[data-testid="alertRunbookField"]');
const submitButtonTrackingOpts = () =>
JSON.parse(submitButton().attributes('data-tracking-options'));
const e = {
......@@ -217,4 +221,18 @@ describe('AlertWidgetForm', () => {
});
});
});
describe('alert runbooks feature flag', () => {
it('hides the runbook field when the flag is disabled', () => {
createComponent(undefined, { alertRunbooks: false });
expect(alertRunbookField().exists()).toBe(false);
});
it('shows the runbook field when the flag is enabled', () => {
createComponent(undefined, { alertRunbooks: true });
expect(alertRunbookField().exists()).toBe(true);
});
});
});
......@@ -70,7 +70,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
.to eq Gitlab::UntrustedRegexp.new('pattern')
end
it 'is a eager scanner for regexp boundaries' do
it 'is an eager scanner for regexp boundaries' do
scanner = StringScanner.new('/some .* / pattern/')
token = described_class.scan(scanner)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pages::Settings do
describe '#path' do
subject { described_class.new(settings).path }
let(:settings) { double(path: 'the path') }
it { is_expected.to eq('the path') }
it 'does not track calls' do
expect(::Gitlab::ErrorTracking).not_to receive(:track_exception)
subject
end
context 'when running under a web server' do
before do
allow(::Gitlab::Runtime).to receive(:web_server?).and_return(true)
end
it { is_expected.to eq('the path') }
it 'does not track calls' do
expect(::Gitlab::ErrorTracking).not_to receive(:track_exception)
subject
end
context 'with the env var' do
before do
stub_env('GITLAB_PAGES_DENY_DISK_ACCESS', '1')
end
it { is_expected.to eq('the path') }
it 'tracks a DiskAccessDenied exception' do
expect(::Gitlab::ErrorTracking).to receive(:track_exception)
.with(instance_of(described_class::DiskAccessDenied)).and_call_original
subject
end
end
end
end
end
......@@ -3,21 +3,15 @@
require 'spec_helper'
RSpec.describe Projects::ContainerRepository::DeleteTagsService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:repository) { create(:container_repository, :root, project: project) }
include_context 'container repository delete tags service shared context'
let(:params) { { tags: tags } }
let(:service) { described_class.new(project, user, params) }
before do
stub_container_registry_config(enabled: true,
api_url: 'http://registry.gitlab',
host_port: 'registry.gitlab')
stub_container_registry_tags(
repository: repository.path,
tags: %w(latest A Ba Bb C D E))
let_it_be(:available_service_classes) do
[
::Projects::ContainerRepository::Gitlab::DeleteTagsService,
::Projects::ContainerRepository::ThirdParty::DeleteTagsService
]
end
RSpec.shared_examples 'logging a success response' do
......@@ -45,8 +39,54 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
end
end
RSpec.shared_examples 'calling the correct delete tags service' do |expected_service_class|
let(:service_response) { { status: :success, deleted: tags } }
let(:excluded_service_class) { available_service_classes.excluding(expected_service_class).first }
before do
service_double = double
expect(expected_service_class).to receive(:new).with(repository, tags).and_return(service_double)
expect(excluded_service_class).not_to receive(:new)
expect(service_double).to receive(:execute).and_return(service_response)
end
it { is_expected.to include(status: :success) }
it_behaves_like 'logging a success response'
context 'with an error service response' do
let(:service_response) { { status: :error, message: 'could not delete tags' } }
it { is_expected.to include(status: :error) }
it_behaves_like 'logging an error response'
end
end
RSpec.shared_examples 'handling invalid params' do
context 'with invalid params' do
before do
expect(::Projects::ContainerRepository::Gitlab::DeleteTagsService).not_to receive(:new)
expect(::Projects::ContainerRepository::ThirdParty::DeleteTagsService).not_to receive(:new)
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
end
context 'when no params are specified' do
let_it_be(:params) { {} }
it { is_expected.to include(status: :error) }
end
context 'with empty tags' do
let_it_be(:tags) { [] }
it { is_expected.to include(status: :error) }
end
end
end
describe '#execute' do
let(:tags) { %w[A] }
let(:tags) { %w[A Ba] }
subject { service.execute(repository) }
......@@ -61,247 +101,58 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
context 'when the registry supports fast delete' do
context 'and the feature is enabled' do
let_it_be(:project) { create(:project, :private) }
let_it_be(:repository) { create(:container_repository, :root, project: project) }
before do
allow(repository.client).to receive(:supports_tag_delete?).and_return(true)
end
context 'with tags to delete' do
let_it_be(:tags) { %w[A Ba] }
it 'deletes the tags by name' do
stub_delete_reference_request('A')
stub_delete_reference_request('Ba')
expect_delete_tag_by_name('A')
expect_delete_tag_by_name('Ba')
is_expected.to include(status: :success)
end
it 'succeeds when tag delete returns 404' do
stub_delete_reference_request('A')
stub_delete_reference_request('Ba', 404)
is_expected.to include(status: :success)
end
it_behaves_like 'logging a success response' do
before do
stub_delete_reference_request('A')
stub_delete_reference_request('Ba')
end
end
context 'with failures' do
context 'when the delete request fails' do
before do
stub_delete_reference_request('A', 500)
stub_delete_reference_request('Ba', 500)
end
it { is_expected.to include(status: :error) }
it_behaves_like 'logging an error response'
end
end
end
context 'when no params are specified' do
let_it_be(:params) { {} }
it_behaves_like 'calling the correct delete tags service', ::Projects::ContainerRepository::Gitlab::DeleteTagsService
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
it_behaves_like 'handling invalid params'
is_expected.to include(status: :error)
context 'with the real service' do
before do
stub_delete_reference_requests(tags)
expect_delete_tag_by_names(tags)
end
end
context 'with empty tags' do
let_it_be(:tags) { [] }
it { is_expected.to include(status: :success) }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
is_expected.to include(status: :error)
end
it_behaves_like 'logging a success response'
end
end
context 'and the feature is disabled' do
let_it_be(:tags) { %w[A Ba] }
before do
stub_feature_flags(container_registry_fast_tag_delete: false)
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A')
stub_put_manifest_request('Ba')
end
it 'fallbacks to slow delete' do
expect(service).not_to receive(:fast_delete)
expect(service).to receive(:slow_delete).with(repository, tags).and_call_original
expect_delete_tag_by_digest('sha256:dummy')
it_behaves_like 'calling the correct delete tags service', ::Projects::ContainerRepository::ThirdParty::DeleteTagsService
subject
end
it_behaves_like 'handling invalid params'
it_behaves_like 'logging a success response' do
context 'with the real service' do
before do
allow(service).to receive(:slow_delete).and_call_original
stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
tags.each { |tag| stub_put_manifest_request(tag) }
expect_delete_tag_by_digest('sha256:dummy')
end
it { is_expected.to include(status: :success) }
it_behaves_like 'logging a success response'
end
end
end
context 'when the registry does not support fast delete' do
let_it_be(:project) { create(:project, :private) }
let_it_be(:repository) { create(:container_repository, :root, project: project) }
before do
stub_tag_digest('latest', 'sha256:configA')
stub_tag_digest('A', 'sha256:configA')
stub_tag_digest('Ba', 'sha256:configB')
allow(repository.client).to receive(:supports_tag_delete?).and_return(false)
end
context 'when no params are specified' do
let_it_be(:params) { {} }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
it_behaves_like 'calling the correct delete tags service', ::Projects::ContainerRepository::ThirdParty::DeleteTagsService
is_expected.to include(status: :error)
end
end
context 'with empty tags' do
let_it_be(:tags) { [] }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to include(status: :error)
end
end
context 'with tags to delete' do
let_it_be(:tags) { %w[A Ba] }
it 'deletes the tags using a dummy image' do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A')
stub_put_manifest_request('Ba')
expect_delete_tag_by_digest('sha256:dummy')
is_expected.to include(status: :success)
end
it 'succeeds when tag delete returns 404' do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A')
stub_put_manifest_request('Ba')
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
.to_return(status: 404, body: "", headers: {})
is_expected.to include(status: :success)
end
it_behaves_like 'logging a success response' do
before do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A')
stub_put_manifest_request('Ba')
expect_delete_tag_by_digest('sha256:dummy')
end
end
context 'with failures' do
context 'when the dummy manifest generation fails' do
before do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false)
end
it { is_expected.to include(status: :error) }
it_behaves_like 'logging an error response', message: 'could not generate manifest'
end
context 'when updating the tags fails' do
before do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A', 500)
stub_put_manifest_request('Ba', 500)
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3")
.to_return(status: 200, body: "", headers: {})
end
it { is_expected.to include(status: :error) }
it_behaves_like 'logging an error response'
end
end
end
it_behaves_like 'handling invalid params'
end
end
end
private
def stub_delete_reference_request(tag, status = 200)
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
.to_return(status: status, body: '')
end
def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '', headers: headers)
end
def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest })
end
def stub_digest_config(digest, created_at)
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:blob)
.with(repository.path, digest, nil) do
{ 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
end
end
def stub_upload(content, digest, success: true)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:upload_blob)
.with(repository.path, content, digest) { double(success?: success ) }
end
def expect_delete_tag_by_digest(digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag_by_digest)
.with(repository.path, digest) { true }
expect_any_instance_of(ContainerRegistry::Client)
.not_to receive(:delete_repository_tag_by_name)
end
def expect_delete_tag_by_name(name)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag_by_name)
.with(repository.path, name) { true }
expect_any_instance_of(ContainerRegistry::Client)
.not_to receive(:delete_repository_tag_by_digest)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
include_context 'container repository delete tags service shared context'
let(:service) { described_class.new(repository, tags) }
describe '#execute' do
let(:tags) { %w[A Ba] }
subject { service.execute }
context 'with tags to delete' do
it 'deletes the tags by name' do
stub_delete_reference_requests(tags)
expect_delete_tag_by_names(tags)
is_expected.to eq(status: :success, deleted: tags)
end
it 'succeeds when tag delete returns 404' do
stub_delete_reference_requests('A' => 200, 'Ba' => 404)
is_expected.to eq(status: :success, deleted: tags)
end
it 'succeeds when a tag delete returns 500' do
stub_delete_reference_requests('A' => 200, 'Ba' => 500)
is_expected.to eq(status: :success, deleted: ['A'])
end
context 'with failures' do
context 'when the delete request fails' do
before do
stub_delete_reference_requests('A' => 500, 'Ba' => 500)
end
it { is_expected.to eq(status: :error, message: 'could not delete tags') }
end
end
end
context 'with empty tags' do
let_it_be(:tags) { [] }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
is_expected.to eq(status: :success, deleted: [])
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do
include_context 'container repository delete tags service shared context'
let(:service) { described_class.new(repository, tags) }
describe '#execute' do
let(:tags) { %w[A Ba] }
subject { service.execute }
context 'with tags to delete' do
it 'deletes the tags by name' do
stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
tags.each { |tag| stub_put_manifest_request(tag) }
expect_delete_tag_by_digest('sha256:dummy')
is_expected.to eq(status: :success, deleted: tags)
end
it 'succeeds when tag delete returns 404' do
stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_put_manifest_request('A')
stub_put_manifest_request('Ba')
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
.to_return(status: 404, body: '', headers: {})
is_expected.to eq(status: :success, deleted: tags)
end
context 'with failures' do
context 'when the dummy manifest generation fails' do
before do
stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false)
end
it { is_expected.to eq(status: :error, message: 'could not generate manifest') }
end
context 'when updating tags fails' do
before do
stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3")
.to_return(status: 200, body: '', headers: {})
end
context 'all tag updates fail' do
before do
stub_put_manifest_request('A', 500, {})
stub_put_manifest_request('Ba', 500, {})
end
it { is_expected.to eq(status: :error, message: 'could not delete tags') }
end
context 'a single tag update fails' do
before do
stub_put_manifest_request('A')
stub_put_manifest_request('Ba', 500, {})
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
.to_return(status: 404, body: '', headers: {})
end
it { is_expected.to eq(status: :success, deleted: ['A']) }
end
end
end
end
context 'with empty tags' do
let_it_be(:tags) { [] }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
is_expected.to eq(status: :success, deleted: [])
end
end
end
end
# frozen_string_literal: true
RSpec.shared_context 'container repository delete tags service shared context' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
let_it_be(:repository) { create(:container_repository, :root, project: project) }
let(:params) { { tags: tags } }
before do
stub_container_registry_config(enabled: true,
api_url: 'http://registry.gitlab',
host_port: 'registry.gitlab')
stub_container_registry_tags(
repository: repository.path,
tags: %w(latest A Ba Bb C D E))
end
def stub_delete_reference_request(tag, status = 200)
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
.to_return(status: status, body: '')
end
def stub_delete_reference_requests(tags)
tags = Hash[Array.wrap(tags).map { |tag| [tag, 200] }] unless tags.is_a?(Hash)
tags.each do |tag, status|
stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
.to_return(status: status, body: '')
end
end
def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '', headers: headers)
end
def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest })
end
def stub_digest_config(digest, created_at)
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:blob)
.with(repository.path, digest, nil) do
{ 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
end
end
def stub_upload(digest, success: true)
content = "{\n \"config\": {\n }\n}"
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:upload_blob)
.with(repository.path, content, digest) { double(success?: success ) }
end
def expect_delete_tag_by_digest(digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag_by_digest)
.with(repository.path, digest) { true }
expect_any_instance_of(ContainerRegistry::Client)
.not_to receive(:delete_repository_tag_by_name)
end
def expect_delete_tag_by_names(names)
Array.wrap(names).each do |name|
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag_by_name)
.with(repository.path, name) { true }
expect_any_instance_of(ContainerRegistry::Client)
.not_to receive(:delete_repository_tag_by_digest)
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册