Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-09 09:14:13 +00:00
parent 377c02f959
commit 6021fa2fc6
80 changed files with 1461 additions and 689 deletions

View File

@ -2,21 +2,6 @@
# Cop supports --auto-correct.
Layout/FirstArrayElementIndentation:
Exclude:
- 'app/controllers/abuse_reports_controller.rb'
- 'app/controllers/admin/application_settings_controller.rb'
- 'app/controllers/admin/broadcast_messages_controller.rb'
- 'app/controllers/admin/plan_limits_controller.rb'
- 'app/controllers/boards/issues_controller.rb'
- 'app/controllers/groups_controller.rb'
- 'app/controllers/projects/issues_controller.rb'
- 'app/controllers/projects/merge_requests_controller.rb'
- 'app/controllers/projects/pipelines_controller.rb'
- 'app/controllers/projects_controller.rb'
- 'app/finders/issuable_finder.rb'
- 'app/finders/merge_requests/by_approvals_finder.rb'
- 'app/finders/user_groups_counter.rb'
- 'app/helpers/diff_helper.rb'
- 'app/helpers/search_helper.rb'
- 'config/initializers/postgres_partitioning.rb'
- 'db/post_migrate/20210812013042_remove_duplicate_project_authorizations.rb'
- 'ee/app/controllers/groups/settings/reporting_controller.rb'

View File

@ -1,9 +1,11 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual, get, isEmpty } from 'lodash';
import { GlAlert, GlSprintf, GlLink, GlCard, GlButton } from '@gitlab/ui';
import {
CONTAINER_CLEANUP_POLICY_TITLE,
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
CONTAINER_CLEANUP_POLICY_EDIT_RULES,
CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
CONTAINER_CLEANUP_POLICY_SET_RULES,
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
@ -13,20 +15,29 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
export default {
components: {
SettingsBlock,
GlAlert,
GlSprintf,
GlLink,
ContainerExpirationPolicyForm,
GlCard,
GlButton,
},
inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
inject: [
'projectPath',
'isAdmin',
'adminSettingsPath',
'enableHistoricEntries',
'helpPagePath',
'cleanupSettingsPath',
],
i18n: {
CONTAINER_CLEANUP_POLICY_TITLE,
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
CONTAINER_CLEANUP_POLICY_EDIT_RULES,
CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
CONTAINER_CLEANUP_POLICY_SET_RULES,
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
FETCH_SETTINGS_ERROR_MESSAGE,
@ -40,9 +51,6 @@ export default {
};
},
update: (data) => data.project?.containerExpirationPolicy,
result({ data }) {
this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
},
error(e) {
this.fetchSettingsError = e;
},
@ -52,29 +60,25 @@ export default {
return {
fetchSettingsError: false,
containerExpirationPolicy: null,
workingCopy: {},
};
},
computed: {
isDisabled() {
return !(this.containerExpirationPolicy || this.enableHistoricEntries);
isCleanupEnabled() {
return this.containerExpirationPolicy?.enabled ?? false;
},
isEnabled() {
return this.containerExpirationPolicy || this.enableHistoricEntries;
},
showDisabledFormMessage() {
return this.isDisabled && !this.fetchSettingsError;
return !this.isEnabled && !this.fetchSettingsError;
},
unavailableFeatureMessage() {
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
},
isEdited() {
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
return false;
}
return !isEqual(this.containerExpirationPolicy, this.workingCopy);
},
},
methods: {
restoreOriginal() {
this.workingCopy = { ...this.containerExpirationPolicy };
cleanupRulesButtonText() {
return this.isCleanupEnabled
? this.$options.i18n.CONTAINER_CLEANUP_POLICY_EDIT_RULES
: this.$options.i18n.CONTAINER_CLEANUP_POLICY_SET_RULES;
},
},
};
@ -93,13 +97,19 @@ export default {
</span>
</template>
<template #default>
<container-expiration-policy-form
v-if="!isDisabled"
v-model="workingCopy"
:is-loading="$apollo.queries.containerExpirationPolicy.loading"
:is-edited="isEdited"
@reset="restoreOriginal"
/>
<gl-card v-if="isEnabled">
<p data-testid="description">
{{ $options.i18n.CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION }}
</p>
<gl-button
data-testid="rules-button"
:href="cleanupSettingsPath"
category="secondary"
variant="confirm"
>
{{ cleanupRulesButtonText }}
</gl-button>
</gl-card>
<template v-else>
<gl-alert
v-if="showDisabledFormMessage"

View File

@ -4,6 +4,12 @@ export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up im
export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__(
`ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`,
);
export const CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION = s__(
'ContainerRegistry|Set rules to automatically remove unused packages to save storage space.',
);
export const CONTAINER_CLEANUP_POLICY_EDIT_RULES = s__('ContainerRegistry|Edit cleanup rules');
export const CONTAINER_CLEANUP_POLICY_SET_RULES = s__('ContainerRegistry|Set cleanup rules');
export const SET_CLEANUP_POLICY_BUTTON = __('Save changes');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,

View File

@ -18,6 +18,7 @@ export default () => {
enableHistoricEntries,
projectPath,
adminSettingsPath,
cleanupSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showContainerRegistrySettings,
@ -34,6 +35,7 @@ export default () => {
enableHistoricEntries: parseBoolean(enableHistoricEntries),
projectPath,
adminSettingsPath,
cleanupSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),

View File

@ -19,6 +19,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-namespace-storage-alert',
'.js-web-hook-disabled-callout',
'.js-merge-request-settings-callout',
'.js-ultimate-feature-removal-banner',
];
const initCallouts = () => {

View File

@ -7,14 +7,14 @@
@return $string;
}
@mixin dropzone-background($stroke-color, $stroke-width: 4, $stroke-linecap: 'butt') {
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='#{encodecolor($stroke-color)}' stroke-width='#{$stroke-width}' stroke-dasharray='6%2c4' stroke-dashoffset='0' stroke-linecap='#{encodecolor($stroke-linecap)}'/%3e%3c/svg%3e");
@mixin dropzone-background($stroke-color, $stroke-width: 4) {
background-image: url("data:image/svg+xml, %3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='#{$border-radius-default}' ry='#{$border-radius-default}' stroke='#{encodecolor($stroke-color)}' stroke-width='#{$stroke-width}' stroke-dasharray='6%2c4' stroke-dashoffset='0' stroke-linecap='butt' /%3e %3c/svg%3e");
}
.upload-dropzone-border {
border: 0;
@include dropzone-background($gray-400, 2, 'round');
border-radius: 8px;
@include dropzone-background($gray-400, 2);
border-radius: $border-radius-default;
}
.upload-dropzone-card {

View File

@ -30,10 +30,7 @@ class AbuseReportsController < ApplicationController
private
def report_params
params.require(:abuse_report).permit(%i(
message
user_id
))
params.require(:abuse_report).permit(:message, :user_id)
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -18,23 +18,23 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token
]
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token
]
feature_category :metrics, [
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
urgency :low, [
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project,
:reset_error_tracking_access_token
]
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project,
:reset_error_tracking_access_token
]
feature_category :source_code_management, [:repository, :clear_repository_check_states]
feature_category :continuous_integration, [:ci_cd, :reset_registration_token]

View File

@ -57,14 +57,15 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def broadcast_message_params
params.require(:broadcast_message).permit(%i(
theme
ends_at
message
starts_at
target_path
broadcast_type
dismissable
), target_access_levels: []).reverse_merge!(target_access_levels: [])
params.require(:broadcast_message)
.permit(%i(
theme
ends_at
message
starts_at
target_path
broadcast_type
dismissable
), target_access_levels: []).reverse_merge!(target_access_levels: [])
end
end

View File

@ -28,24 +28,25 @@ class Admin::PlanLimitsController < Admin::ApplicationController
end
def plan_limits_params
params.require(:plan_limits).permit(%i[
plan_id
conan_max_file_size
helm_max_file_size
maven_max_file_size
npm_max_file_size
nuget_max_file_size
pypi_max_file_size
terraform_module_max_file_size
generic_packages_max_file_size
ci_pipeline_size
ci_active_jobs
ci_active_pipelines
ci_project_subscriptions
ci_pipeline_schedules
ci_needs_size_limit
ci_registered_group_runners
ci_registered_project_runners
])
params.require(:plan_limits)
.permit(%i[
plan_id
conan_max_file_size
helm_max_file_size
maven_max_file_size
npm_max_file_size
nuget_max_file_size
pypi_max_file_size
terraform_module_max_file_size
generic_packages_max_file_size
ci_pipeline_size
ci_active_jobs
ci_active_pipelines
ci_project_subscriptions
ci_pipeline_schedules
ci_needs_size_limit
ci_registered_group_runners
ci_registered_project_runners
])
end
end

View File

@ -77,10 +77,10 @@ module Boards
:milestone,
:assignees,
project: [
:route,
{
namespace: [:route]
}
:route,
{
namespace: [:route]
}
],
labels: [:priorities],
notes: [:award_emoji, :author]

View File

@ -49,9 +49,9 @@ class GroupsController < Groups::ApplicationController
layout :determine_layout
feature_category :subgroups, [
:index, :new, :create, :show, :edit, :update,
:destroy, :details, :transfer, :activity
]
:index, :new, :create, :show, :edit, :update,
:destroy, :details, :transfer, :activity
]
feature_category :team_planning, [:issues, :issues_calendar, :preview_markdown]
feature_category :code_review, [:merge_requests, :unfoldered_environment_names]

View File

@ -65,19 +65,19 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :designs, :show
feature_category :team_planning, [
:index, :calendar, :show, :new, :create, :edit, :update,
:destroy, :move, :reorder, :designs, :toggle_subscription,
:discussions, :bulk_update, :realtime_changes,
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
:index, :calendar, :show, :new, :create, :edit, :update,
:destroy, :move, :reorder, :designs, :toggle_subscription,
:discussions, :bulk_update, :realtime_changes,
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
urgency :low, [
:index, :calendar, :show, :new, :create, :edit, :update,
:destroy, :move, :reorder, :designs, :toggle_subscription,
:discussions, :bulk_update, :realtime_changes,
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
:index, :calendar, :show, :new, :create, :edit, :update,
:destroy, :move, :reorder, :designs, :toggle_subscription,
:discussions, :bulk_update, :realtime_changes,
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
feature_category :service_desk, [:service_desk]
urgency :low, [:service_desk]

View File

@ -57,11 +57,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
after_action :log_merge_request_show, only: [:show]
feature_category :code_review, [
:assign_related_issues, :bulk_update, :cancel_auto_merge,
:commit_change_content, :commits, :context_commits, :destroy,
:discussions, :edit, :index, :merge, :rebase, :remove_wip,
:show, :toggle_award_emoji, :toggle_subscription, :update
]
:assign_related_issues, :bulk_update, :cancel_auto_merge,
:commit_change_content, :commits, :context_commits, :destroy,
:discussions, :edit, :index, :merge, :rebase, :remove_wip,
:show, :toggle_award_emoji, :toggle_subscription, :update
]
feature_category :code_testing, [:test_reports, :coverage_reports]
feature_category :code_quality, [:codequality_reports, :codequality_mr_diff_reports]

View File

@ -51,10 +51,10 @@ class Projects::PipelinesController < Projects::ApplicationController
POLLING_INTERVAL = 10_000
feature_category :continuous_integration, [
:charts, :show, :config_variables, :stage, :cancel, :retry,
:builds, :dag, :failures, :status,
:index, :create, :new, :destroy
]
:charts, :show, :config_variables, :stage, :cancel, :retry,
:builds, :dag, :failures, :status,
:index, :create, :new, :destroy
]
feature_category :code_testing, [:test_report]
feature_category :build_artifacts, [:downloadable_artifacts]

View File

@ -56,9 +56,9 @@ class ProjectsController < Projects::ApplicationController
layout :determine_layout
feature_category :projects, [
:index, :show, :new, :create, :edit, :update, :transfer,
:destroy, :archive, :unarchive, :toggle_star, :activity
]
:index, :show, :new, :create, :edit, :update, :transfer,
:destroy, :archive, :unarchive, :toggle_star, :activity
]
feature_category :source_code_management, [:remove_fork, :housekeeping, :refs]
feature_category :team_planning, [:preview_markdown, :new_issuable_address]

View File

@ -31,7 +31,7 @@ module IncidentManagement
end
def sort(collection)
collection.order_occurred_at_asc
collection.order_occurred_at_asc_id_asc
end
end
end

View File

@ -58,19 +58,19 @@ class IssuableFinder
class << self
def scalar_params
@scalar_params ||= %i[
assignee_id
assignee_username
author_id
author_username
crm_contact_id
crm_organization_id
label_name
milestone_title
release_tag
my_reaction_emoji
search
in
]
assignee_id
assignee_username
author_id
author_username
crm_contact_id
crm_organization_id
label_name
milestone_title
release_tag
my_reaction_emoji
search
in
]
end
def array_params

View File

@ -71,9 +71,7 @@ module MergeRequests
#
# @param [ActiveRecord::Relation] items the activerecord relation
def with_any_approvals(items)
items.select_from_union([
items.with_approvals
])
items.select_from_union([items.with_approvals])
end
# Merge requests approved by given usernames

View File

@ -8,9 +8,9 @@ class UserGroupsCounter
def execute
Namespace.unscoped do
Namespace.from_union([
groups,
project_groups
]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
groups,
project_groups
]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
end
end

View File

@ -140,12 +140,12 @@ module DiffHelper
if compare_url
link_text = [
_('Compare'),
' ',
content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
'...',
content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
].join('').html_safe
_('Compare'),
' ',
content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
'...',
content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
].join('').html_safe
tooltip = _('Compare submodule commit revisions')
link = content_tag(:span, link_to(link_text, compare_url, class: 'btn gl-button has-tooltip', title: tooltip), class: 'submodule-compare')

View File

@ -83,7 +83,8 @@ module PackagesHelper
def settings_data
cleanup_settings_data.merge(
show_container_registry_settings: show_container_registry_settings(@project).to_s,
show_package_registry_settings: show_package_registry_settings(@project).to_s
show_package_registry_settings: show_package_registry_settings(@project).to_s,
cleanup_settings_path: cleanup_image_tags_project_settings_packages_and_registries_path(@project)
)
end
end

View File

@ -239,26 +239,26 @@ module SearchHelper
if can?(current_user, :download_code, @project)
result.concat([
{ category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
{ category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) }
])
{ category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
{ category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) }
])
end
if can?(current_user, :read_repository_graphs, @project)
result.concat([
{ category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
{ category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) }
])
{ category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
{ category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) }
])
end
result.concat([
{ category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
{ category: "In this project", label: _("Merge requests"), url: project_merge_requests_path(@project) },
{ category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
{ category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
{ category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
{ category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
])
{ category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
{ category: "In this project", label: _("Merge requests"), url: project_merge_requests_path(@project) },
{ category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
{ category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
{ category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
{ category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
])
if can?(current_user, :read_feature_flag, @project)
result << { category: "In this project", label: _("Feature Flags"), url: project_feature_flags_path(@project) }
@ -294,13 +294,13 @@ module SearchHelper
return [] unless issue && Ability.allowed?(current_user, :read_issue, issue)
[
{
category: 'In this project',
id: issue.id,
label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"),
url: issue_path(issue),
avatar_url: issue.project.avatar_url || ''
}
{
category: 'In this project',
id: issue.id,
label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"),
url: issue_path(issue),
avatar_url: issue.project.avatar_url || ''
}
]
end

View File

@ -13,6 +13,7 @@ module Users
MERGE_REQUEST_SETTINGS_MOVED_CALLOUT = 'merge_request_settings_moved_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
ULTIMATE_FEATURE_REMOVAL_BANNER = 'ultimate_feature_removal_banner'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@ -79,6 +80,12 @@ module Users
!user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT)
end
def ultimate_feature_removal_banner_dismissed?(project)
return false unless project
user_dismissed?(ULTIMATE_FEATURE_REMOVAL_BANNER, project: project)
end
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, project: nil)

View File

@ -598,6 +598,7 @@ class ContainerRepository < ApplicationRecord
tags_response_body.map do |raw_tag|
tag = ContainerRegistry::Tag.new(self, raw_tag['name'])
tag.force_created_at_from_iso8601(raw_tag['created_at'])
tag.updated_at = raw_tag['updated_at']
tag
end
end

View File

@ -20,6 +20,6 @@ module IncidentManagement
validates :action, presence: true, length: { maximum: 128 }
validates :note, :note_html, presence: true, length: { maximum: 10_000 }
scope :order_occurred_at_asc, -> { reorder(occurred_at: :asc) }
scope :order_occurred_at_asc_id_asc, -> { reorder(occurred_at: :asc, id: :asc) }
end
end

View File

@ -22,7 +22,8 @@ class Packages::Package < ApplicationRecord
debian: 9,
rubygems: 10,
helm: 11,
terraform_module: 12
terraform_module: 12,
rpm: 13
}
enum status: { default: 0, hidden: 1, processing: 2, error: 3, pending_destruction: 4 }
@ -43,6 +44,7 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum'
has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos, disable_joins: true

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module Packages
module Rpm
def self.table_name_prefix
'packages_rpm_'
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Packages
module Rpm
class Metadatum < ApplicationRecord
self.primary_key = :package_id
belongs_to :package, -> { where(package_type: :rpm) }, inverse_of: :rpm_metadatum
validates :package, presence: true
validates :release,
presence: true,
length: { maximum: 128 }
validates :summary,
presence: true,
length: { maximum: 1000 }
validates :description,
presence: true,
length: { maximum: 5000 }
validates :arch,
presence: true,
length: { maximum: 255 }
validates :license,
allow_nil: true,
length: { maximum: 1000 }
validates :url,
allow_nil: true,
length: { maximum: 1000 }
validate :rpm_package_type
private
def rpm_package_type
return if package&.rpm?
errors.add(:base, _('Package type must be RPM'))
end
end
end
end

View File

@ -10,7 +10,8 @@ module Users
enum feature_name: {
awaiting_members_banner: 1, # EE-only
web_hook_disabled: 2
web_hook_disabled: 2,
ultimate_feature_removal_banner: 3
}
validates :project, presence: true

View File

@ -9,7 +9,7 @@ module Ci
@user.owns_runner?(@subject)
end
condition(:belongs_to_multiple_projects) do
condition(:belongs_to_multiple_projects, scope: :subject) do
@subject.belongs_to_more_than_one_project?
end

View File

@ -317,6 +317,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def autodevops_anchor_data(show_auto_devops_callout: false)
return unless project.feature_available?(:builds, current_user)
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
if auto_devops_enabled?
AnchorData.new(false,

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Projects
module ContainerRepository
module Gitlab
module Timeoutable
extend ActiveSupport::Concern
DISABLED_TIMEOUTS = [nil, 0].freeze
TimeoutError = Class.new(StandardError)
private
def timeout?(start_time)
return false if service_timeout.in?(DISABLED_TIMEOUTS)
(Time.zone.now - start_time) > service_timeout
end
def service_timeout
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
end
end
end
end
end

View File

@ -0,0 +1,118 @@
# frozen_string_literal: true
module Projects
module ContainerRepository
class CleanupTagsBaseService
include BaseServiceUtility
include ::Gitlab::Utils::StrongMemoize
private
def filter_out_latest
@tags.reject!(&:latest?)
end
def filter_by_name
regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_delete || name_regex}\\z")
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_keep}\\z")
@tags.select! do |tag|
# regex_retain will override any overlapping matches by regex_delete
regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
end
end
# Should return [tags_to_delete, tags_to_keep]
def partition_by_keep_n
return [@tags, []] unless keep_n
order_by_date_desc
@tags.partition.with_index { |_, index| index >= keep_n_as_integer }
end
# Should return [tags_to_delete, tags_to_keep]
def partition_by_older_than
return [@tags, []] unless older_than
older_than_timestamp = older_than_in_seconds.ago
@tags.partition do |tag|
timestamp = pushed_at(tag)
timestamp && timestamp < older_than_timestamp
end
end
def order_by_date_desc
now = DateTime.current
@tags.sort_by! { |tag| pushed_at(tag) || now }
.reverse!
end
def delete_tags
return success(deleted: []) unless @tags.any?
service = Projects::ContainerRepository::DeleteTagsService.new(
@project,
@current_user,
tags: @tags.map(&:name),
container_expiration_policy: container_expiration_policy
)
service.execute(@container_repository)
end
def can_destroy?
return true if container_expiration_policy
can?(@current_user, :destroy_container_image, @project)
end
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?
end
true
rescue RegexpError => e
::Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
false
end
def older_than
@params['older_than']
end
def name_regex_delete
@params['name_regex_delete']
end
def name_regex
@params['name_regex']
end
def name_regex_keep
@params['name_regex_keep']
end
def container_expiration_policy
@params['container_expiration_policy']
end
def keep_n
@params['keep_n']
end
def keep_n_as_integer
keep_n.to_i
end
def older_than_in_seconds
strong_memoize(:older_than_in_seconds) do
ChronicDuration.parse(older_than).seconds
end
end
end
end
end

View File

@ -2,10 +2,7 @@
module Projects
module ContainerRepository
class CleanupTagsService
include BaseServiceUtility
include ::Gitlab::Utils::StrongMemoize
class CleanupTagsService < CleanupTagsBaseService
def initialize(container_repository, user = nil, params = {})
@container_repository = container_repository
@current_user = user
@ -43,74 +40,20 @@ module Projects
private
def delete_tags
return success(deleted: []) unless @tags.any?
service = Projects::ContainerRepository::DeleteTagsService.new(
@project,
@current_user,
tags: @tags.map(&:name),
container_expiration_policy: container_expiration_policy
)
service.execute(@container_repository)
end
def filter_out_latest
@tags.reject!(&:latest?)
end
def order_by_date
now = DateTime.current
@tags.sort_by! { |tag| tag.created_at || now }
.reverse!
end
def filter_by_name
regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_delete || name_regex}\\z")
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_keep}\\z")
@tags.select! do |tag|
# regex_retain will override any overlapping matches by regex_delete
regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
end
end
def filter_keep_n
return unless keep_n
order_by_date
cache_tags(@tags.first(keep_n_as_integer))
@tags = @tags.drop(keep_n_as_integer)
end
def filter_by_older_than
return unless older_than
older_than_timestamp = older_than_in_seconds.ago
@tags, tags_to_keep = @tags.partition do |tag|
tag.created_at && tag.created_at < older_than_timestamp
end
@tags, tags_to_keep = partition_by_keep_n
cache_tags(tags_to_keep)
end
def can_destroy?
return true if container_expiration_policy
def filter_by_older_than
@tags, tags_to_keep = partition_by_older_than
can?(@current_user, :destroy_container_image, @project)
cache_tags(tags_to_keep)
end
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?
end
true
rescue RegexpError => e
::Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
false
def pushed_at(tag)
tag.created_at
end
def truncate
@ -153,40 +96,6 @@ module Projects
def max_list_size
::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
end
def keep_n
@params['keep_n']
end
def keep_n_as_integer
keep_n.to_i
end
def older_than_in_seconds
strong_memoize(:older_than_in_seconds) do
ChronicDuration.parse(older_than).seconds
end
end
def older_than
@params['older_than']
end
def name_regex_delete
@params['name_regex_delete']
end
def name_regex
@params['name_regex']
end
def name_regex_keep
@params['name_regex_keep']
end
def container_expiration_policy
@params['container_expiration_policy']
end
end
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
module Projects
module ContainerRepository
module Gitlab
class CleanupTagsService < CleanupTagsBaseService
include ::Projects::ContainerRepository::Gitlab::Timeoutable
TAGS_PAGE_SIZE = 1000
def initialize(container_repository, user = nil, params = {})
@container_repository = container_repository
@current_user = user
@params = params.dup
@project = container_repository.project
end
def execute
return error('access denied') unless can_destroy?
return error('invalid regex') unless valid_regex?
with_timeout do |start_time, result|
@container_repository.each_tags_page(page_size: TAGS_PAGE_SIZE) do |tags|
execute_for_tags(tags, result)
raise TimeoutError if timeout?(start_time)
end
end
end
private
def execute_for_tags(tags, overall_result)
@tags = tags
original_size = @tags.size
filter_out_latest
filter_by_name
filter_by_keep_n
filter_by_older_than
overall_result[:before_delete_size] += @tags.size
overall_result[:original_size] += original_size
result = delete_tags
overall_result[:deleted_size] += result[:deleted]&.size
overall_result[:deleted] += result[:deleted]
overall_result[:status] = result[:status] unless overall_result[:status] == :error
end
def with_timeout
result = {
original_size: 0,
before_delete_size: 0,
deleted_size: 0,
deleted: []
}
yield Time.zone.now, result
result
rescue TimeoutError
result[:status] = :error
result
end
def filter_by_keep_n
@tags, _ = partition_by_keep_n
end
def filter_by_older_than
@tags, _ = partition_by_older_than
end
def pushed_at(tag)
tag.updated_at || tag.created_at
end
end
end
end
end

View File

@ -6,10 +6,7 @@ module Projects
class DeleteTagsService
include BaseServiceUtility
include ::Gitlab::Utils::StrongMemoize
DISABLED_TIMEOUTS = [nil, 0].freeze
TimeoutError = Class.new(StandardError)
include ::Projects::ContainerRepository::Gitlab::Timeoutable
def initialize(container_repository, tag_names)
@container_repository = container_repository
@ -44,16 +41,6 @@ module Projects
@deleted_tags.any? ? success(deleted: @deleted_tags) : error('could not delete tags')
end
def timeout?(start_time)
return false if service_timeout.in?(DISABLED_TIMEOUTS)
(Time.zone.now - start_time) > service_timeout
end
def service_timeout
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
end
end
end
end

View File

@ -3,16 +3,16 @@
= form_errors(@group)
= render 'shared/groups/group_name_and_path_fields', f: f, autofocus: true, new_subgroup: !!parent
- unless parent
.row
.form-group.gl-form-group.col-sm-12
%label.label-bold
= _('Visibility level')
%p
= _('Who will be able to see this group?')
= link_to _('View the documentation'), help_page_path("user/public_access"), target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group, with_label: false
.row
.form-group.gl-form-group.col-sm-12
%label.label-bold
= _('Visibility level')
%p
= _('Who will be able to see this group?')
= link_to _('View the documentation'), help_page_path("user/public_access"), target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group, with_label: false
- unless parent
- if Gitlab.config.mattermost.enabled
.row
= render 'create_chat_team', f: f

View File

@ -1,4 +1,6 @@
- page_title _("Activity")
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= render 'projects/last_push'
= render 'projects/activity'

View File

@ -5,6 +5,8 @@
- expanded = expanded_by_default?
- reduce_visibility_form_id = 'reduce-visibility-form'
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')

View File

@ -3,6 +3,7 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
- if labels_or_filters
#js-promote-label-modal

View File

@ -2,6 +2,7 @@
- page_title _("Members")
= render_if_exists 'projects/free_user_cap_alert', project: @project
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
.row.gl-mt-3
.col-lg-12

View File

@ -7,6 +7,7 @@
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= render_if_exists 'projects/free_user_cap_alert', project: @project
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= render partial: 'flash_messages', locals: { project: @project }
= render 'clusters_deprecation_alert'

View File

@ -1,5 +1,7 @@
- page_title s_("UsageQuota|Usage")
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'),
variant: :info,
alert_options: { class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none' }) do |c|

View File

@ -0,0 +1,8 @@
---
name: ultimate_feature_removal_banner
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94271
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371690
milestone: '15.4'
type: development
group: group::workspace
default_enabled: false

View File

@ -0,0 +1,9 @@
---
table_name: packages_rpm_metadata
classes:
- Packages::Rpm::Metadatum
feature_categories:
- package_registry
description: Rpm package metadata
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96019
milestone: '15.4'

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddRpmMaxFileSizeToPlanLimits < Gitlab::Database::Migration[2.0]
DOWNTIME = false
def change
add_column :plan_limits, :rpm_max_file_size, :bigint, default: 5.gigabytes, null: false
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class CreatePackagesRpmMetadata < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
with_lock_retries do
create_table :packages_rpm_metadata, id: false do |t|
t.references :package,
primary_key: true,
default: nil,
index: true,
foreign_key: { to_table: :packages_packages, on_delete: :cascade },
type: :bigint
t.text :release, default: '1', null: false, limit: 128
t.text :summary, default: '', null: false, limit: 1000
t.text :description, default: '', null: false, limit: 5000
t.text :arch, default: '', null: false, limit: 255
t.text :license, null: true, limit: 1000
t.text :url, null: true, limit: 1000
end
end
end
def down
with_lock_retries do
drop_table :packages_rpm_metadata
end
end
end

View File

@ -0,0 +1 @@
7373697e5064a5ecca5881e7b98a30deba033bf8d79d2121cd17200f72815252

View File

@ -0,0 +1 @@
d38668a9110a69f12c4d60886ace04da4f6dd7f250763a888d3c428a74032b7d

View File

@ -18907,6 +18907,22 @@ CREATE TABLE packages_pypi_metadata (
CONSTRAINT check_379019d5da CHECK ((char_length(required_python) <= 255))
);
CREATE TABLE packages_rpm_metadata (
package_id bigint NOT NULL,
release text DEFAULT '1'::text NOT NULL,
summary text DEFAULT ''::text NOT NULL,
description text DEFAULT ''::text NOT NULL,
arch text DEFAULT ''::text NOT NULL,
license text,
url text,
CONSTRAINT check_3798bae3d6 CHECK ((char_length(arch) <= 255)),
CONSTRAINT check_5d29ba59ac CHECK ((char_length(description) <= 5000)),
CONSTRAINT check_6e8cbd536d CHECK ((char_length(url) <= 1000)),
CONSTRAINT check_845ba4d7d0 CHECK ((char_length(license) <= 1000)),
CONSTRAINT check_b010bf4870 CHECK ((char_length(summary) <= 1000)),
CONSTRAINT check_c3e2fc2e89 CHECK ((char_length(release) <= 128))
);
CREATE TABLE packages_rubygems_metadata (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
@ -19194,7 +19210,8 @@ CREATE TABLE plan_limits (
web_hook_calls_low integer DEFAULT 0 NOT NULL,
project_ci_variables integer DEFAULT 200 NOT NULL,
group_ci_variables integer DEFAULT 200 NOT NULL,
ci_max_artifact_size_cyclonedx integer DEFAULT 1 NOT NULL
ci_max_artifact_size_cyclonedx integer DEFAULT 1 NOT NULL,
rpm_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL
);
CREATE SEQUENCE plan_limits_id_seq
@ -25926,6 +25943,9 @@ ALTER TABLE ONLY packages_packages
ALTER TABLE ONLY packages_pypi_metadata
ADD CONSTRAINT packages_pypi_metadata_pkey PRIMARY KEY (package_id);
ALTER TABLE ONLY packages_rpm_metadata
ADD CONSTRAINT packages_rpm_metadata_pkey PRIMARY KEY (package_id);
ALTER TABLE ONLY packages_rubygems_metadata
ADD CONSTRAINT packages_rubygems_metadata_pkey PRIMARY KEY (package_id);
@ -29620,6 +29640,8 @@ CREATE INDEX index_packages_packages_on_project_id_and_version ON packages_packa
CREATE INDEX index_packages_project_id_name_partial_for_nuget ON packages_packages USING btree (project_id, name) WHERE (((name)::text <> 'NuGet.Temporary.Package'::text) AND (version IS NOT NULL) AND (package_type = 4));
CREATE INDEX index_packages_rpm_metadata_on_package_id ON packages_rpm_metadata USING btree (package_id);
CREATE INDEX index_packages_tags_on_package_id ON packages_tags USING btree (package_id);
CREATE INDEX index_packages_tags_on_package_id_and_updated_at ON packages_tags USING btree (package_id, updated_at DESC);
@ -34491,6 +34513,9 @@ ALTER TABLE ONLY geo_hashed_storage_attachments_events
ALTER TABLE ONLY ml_candidate_params
ADD CONSTRAINT fk_rails_d4a51d1185 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id);
ALTER TABLE ONLY packages_rpm_metadata
ADD CONSTRAINT fk_rails_d79f02264b FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;

View File

@ -4640,6 +4640,27 @@ Input type: `SecurityFindingCreateIssueInput`
| <a id="mutationsecurityfindingcreateissueerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationsecurityfindingcreateissueissue"></a>`issue` | [`Issue`](#issue) | Issue created after mutation. |
### `Mutation.securityFindingDismiss`
Input type: `SecurityFindingDismissInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecurityfindingdismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecurityfindingdismisscomment"></a>`comment` | [`String`](#string) | Comment why finding should be dismissed. |
| <a id="mutationsecurityfindingdismissdismissalreason"></a>`dismissalReason` | [`VulnerabilityDismissalReason`](#vulnerabilitydismissalreason) | Reason why finding should be dismissed. |
| <a id="mutationsecurityfindingdismissuuid"></a>`uuid` | [`String!`](#string) | UUID of the finding to be dismissed. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecurityfindingdismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecurityfindingdismisserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationsecurityfindingdismissuuid"></a>`uuid` | [`String`](#string) | UUID of dismissed finding. |
### `Mutation.securityPolicyProjectAssign`
Assigns the specified project(`security_policy_project_id`) as security policy project for the given project(`full_path`). If the project already has a security policy project, this reassigns the project's security policy project with the given `security_policy_project_id`.
@ -20605,6 +20626,7 @@ Values for sorting package.
| <a id="packagetypeenumnpm"></a>`NPM` | Packages from the npm package manager. |
| <a id="packagetypeenumnuget"></a>`NUGET` | Packages from the Nuget package manager. |
| <a id="packagetypeenumpypi"></a>`PYPI` | Packages from the PyPI package manager. |
| <a id="packagetypeenumrpm"></a>`RPM` | Packages from the Rpm package manager. |
| <a id="packagetypeenumrubygems"></a>`RUBYGEMS` | Packages from the Rubygems package manager. |
| <a id="packagetypeenumterraform_module"></a>`TERRAFORM_MODULE` | Packages from the Terraform Module package manager. |

View File

@ -4,7 +4,7 @@ module ContainerRegistry
class Tag
include Gitlab::Utils::StrongMemoize
attr_reader :repository, :name
attr_reader :repository, :name, :updated_at
attr_writer :created_at
delegate :registry, :client, to: :repository
@ -97,6 +97,17 @@ module ContainerRegistry
instance_variable_set(ivar(:memoized_created_at), date)
end
def updated_at=(string_value)
return unless string_value
@updated_at =
begin
DateTime.iso8601(string_value)
rescue ArgumentError
nil
end
end
def layers
return unless manifest

View File

@ -1,5 +1,5 @@
variables:
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.37.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -383,6 +383,7 @@ packages_events: :gitlab_main
packages_helm_file_metadata: :gitlab_main
packages_maven_metadata: :gitlab_main
packages_npm_metadata: :gitlab_main
packages_rpm_metadata: :gitlab_main
packages_nuget_dependency_link_metadata: :gitlab_main
packages_nuget_metadata: :gitlab_main
packages_package_file_build_infos: :gitlab_main

View File

@ -10175,6 +10175,9 @@ msgstr ""
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Edit cleanup rules"
msgstr ""
msgid "ContainerRegistry|Enable expiration policy"
msgstr ""
@ -10279,6 +10282,12 @@ msgstr ""
msgid "ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|Set cleanup rules"
msgstr ""
msgid "ContainerRegistry|Set rules to automatically remove unused packages to save storage space."
msgstr ""
msgid "ContainerRegistry|Set up cleanup"
msgstr ""
@ -26407,6 +26416,9 @@ msgstr ""
msgid "No credit card required."
msgstr ""
msgid "No data available"
msgstr ""
msgid "No data found"
msgstr ""
@ -27939,6 +27951,9 @@ msgstr ""
msgid "Package type must be PyPi"
msgstr ""
msgid "Package type must be RPM"
msgstr ""
msgid "Package type must be RubyGems"
msgstr ""
@ -45796,6 +45811,9 @@ msgstr ""
msgid "Your profile"
msgstr ""
msgid "Your project is no longer receiving GitLab Ultimate benefits as of 2022-07-01. As notified in-app previously, public open source projects on the Free tier can apply to the GitLab for Open Source Program to receive GitLab Ultimate benefits. Please refer to the %{faq_link_start}FAQ%{link_end} for more details."
msgstr ""
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr ""

View File

@ -337,6 +337,14 @@ FactoryBot.define do
size { 3989.bytes }
end
trait(:rpm) do
package
file_fixture { 'spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm' }
file_name { 'hello-0.0.1-1.fc29.x86_64.rpm' }
file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
size { 115.kilobytes }
end
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end

View File

@ -55,6 +55,12 @@ FactoryBot.define do
end
end
factory :rpm_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "v1.0.#{n}" }
package_type { :rpm }
end
factory :debian_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "1.0-#{n}" }

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :rpm_metadatum, class: 'Packages::Rpm::Metadatum' do
package { association(:rpm_package) }
release { "#{rand(10)}.#{rand(10)}" }
summary { FFaker::Lorem.sentences(2).join }
description { FFaker::Lorem.sentences(4).join }
arch { FFaker::Lorem.word }
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy', :js do
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
@ -19,7 +19,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
stub_container_registry_config(enabled: container_registry_enabled)
end
context 'as owner' do
context 'as owner', :js do
it 'shows available section' do
subject
@ -27,40 +27,14 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
expect(settings_block).to have_text 'Clean up image tags'
end
it 'saves cleanup policy submit the form' do
it 'contains link to clean up image tags page' do
subject
within '[data-testid="container-expiration-policy-project-settings"]' do
select('Every day', from: 'Run cleanup')
select('50 tags per image name', from: 'Keep the most recent:')
fill_in('Keep tags matching:', with: 'stable')
select('7 days', from: 'Remove tags older than:')
fill_in('Remove tags matching:', with: '.*-production')
submit_button = find('[data-testid="save-button"')
expect(submit_button).not_to be_disabled
submit_button.click
end
expect(find('.gl-toast')).to have_content('Cleanup policy successfully saved.')
end
it 'does not save cleanup policy submit form with invalid regex' do
subject
within '[data-testid="container-expiration-policy-project-settings"]' do
fill_in('Remove tags matching:', with: '*-production')
submit_button = find('[data-testid="save-button"')
expect(submit_button).not_to be_disabled
submit_button.click
end
expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.')
expect(page).to have_link('Edit cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
context 'with a project without expiration policy' do
context 'with a project without expiration policy', :js do
before do
project.container_expiration_policy.destroy!
end
@ -74,7 +48,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
subject
within '[data-testid="container-expiration-policy-project-settings"]' do
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
expect(page).to have_link('Set cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
end

View File

@ -288,6 +288,17 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
end
end
it 'no Auto DevOps button if builds feature is disabled' do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
visit project_path(project)
page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
end
it 'no "Enable Auto DevOps" button when .gitlab-ci.yml already exists' do
Files::CreateService.new(
project,

Binary file not shown.

View File

@ -1,12 +1,14 @@
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink, GlCard } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
import {
CONTAINER_CLEANUP_POLICY_EDIT_RULES,
CONTAINER_CLEANUP_POLICY_SET_RULES,
CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
@ -14,11 +16,7 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import {
expirationPolicyPayload,
emptyExpirationPolicyPayload,
containerExpirationPolicyData,
} from '../mock_data';
import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
describe('Container expiration policy project settings', () => {
let wrapper;
@ -28,17 +26,19 @@ describe('Container expiration policy project settings', () => {
projectPath: 'path',
isAdmin: false,
adminSettingsPath: 'settingsPath',
cleanupSettingsPath: 'cleanupSettingsPath',
enableHistoricEntries: false,
helpPagePath: 'helpPagePath',
showCleanupPolicyLink: false,
};
const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm);
const findAlert = () => wrapper.find(GlAlert);
const findFormComponent = () => wrapper.findComponent(GlCard);
const findDescription = () => wrapper.findByTestId('description');
const findButton = () => wrapper.findByTestId('rules-button');
const findAlert = () => wrapper.findComponent(GlAlert);
const findSettingsBlock = () => wrapper.find(SettingsBlock);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMount(component, {
wrapper = shallowMountExtended(component, {
stubs: {
GlSprintf,
SettingsBlock,
@ -63,37 +63,19 @@ describe('Container expiration policy project settings', () => {
wrapper.destroy();
});
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
`('$description', async ({ apiResponse, workingCopy, result }) => {
mountComponentWithApollo({
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
resolver: jest.fn().mockResolvedValue(apiResponse),
});
await waitForPromises();
findFormComponent().vm.$emit('input', workingCopy);
await waitForPromises();
expect(findFormComponent().props('isEdited')).toBe(result);
});
});
it('renders the setting form', async () => {
mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
});
await waitForPromises();
expect(findFormComponent().exists()).toBe(true);
expect(findSettingsBlock().exists()).toBe(true);
expect(findFormComponent().exists()).toBe(true);
expect(findDescription().text()).toMatchInterpolatedText(
CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
);
expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_EDIT_RULES);
expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
});
describe('the form is disabled', () => {
@ -157,6 +139,10 @@ describe('Container expiration policy project settings', () => {
await waitForPromises();
expect(findFormComponent().exists()).toBe(isShown);
if (isShown) {
expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_SET_RULES);
expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
}
});
});
});

View File

@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
it 'exposes all package types' do
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE])
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE RPM])
end
end

View File

@ -240,6 +240,31 @@ RSpec.describe ContainerRegistry::Tag do
it_behaves_like 'setting and caching the created_at value'
end
end
describe 'updated_at=' do
subject do
tag.updated_at = input
tag.updated_at
end
context 'with a valid input' do
let(:input) { 2.days.ago.iso8601 }
it { is_expected.to eq(DateTime.iso8601(input)) }
end
context 'with a nil input' do
let(:input) { nil }
it { is_expected.to eq(nil) }
end
context 'with an invalid input' do
let(:input) { 'not a timestamp' }
it { is_expected.to eq(nil) }
end
end
end
end
end

View File

@ -29,7 +29,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
it { is_expected.to validate_length_of(:action).is_at_most(128) }
end
describe '.order_occurred_at_asc' do
describe '.order_occurred_at_asc_id_asc' do
let_it_be(:occurred_3mins_ago) do
create(:incident_management_timeline_event, project: project, occurred_at: 3.minutes.ago)
end
@ -38,11 +38,23 @@ RSpec.describe IncidentManagement::TimelineEvent do
create(:incident_management_timeline_event, project: project, occurred_at: 2.minutes.ago)
end
subject(:order) { described_class.order_occurred_at_asc }
subject(:order) { described_class.order_occurred_at_asc_id_asc }
it 'sorts timeline events by occurred_at' do
is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, timeline_event])
end
context 'when two events occured at the same time' do
let_it_be(:also_occurred_2mins_ago) do
create(:incident_management_timeline_event, project: project, occurred_at: occurred_2mins_ago.occurred_at)
end
it 'sorts timeline events by occurred_at then sorts by id' do
occurred_2mins_ago.touch # Interact with record of earlier id to switch default DB ordering
is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, also_occurred_2mins_ago, timeline_event])
end
end
end
describe '#cache_markdown_field' do

View File

@ -21,6 +21,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rpm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rpm::Metadatum, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to validate_presence_of(:release) }
it { is_expected.to validate_presence_of(:summary) }
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:arch) }
it { is_expected.to validate_length_of(:release).is_at_most(128) }
it { is_expected.to validate_length_of(:summary).is_at_most(1000) }
it { is_expected.to validate_length_of(:description).is_at_most(5000) }
it { is_expected.to validate_length_of(:arch).is_at_most(255) }
it { is_expected.to validate_length_of(:license).is_at_most(1000) }
it { is_expected.to validate_length_of(:url).is_at_most(1000) }
describe '#rpm_package_type' do
it 'will not allow a package with a different package_type' do
package = build('conan_package')
rpm_metadatum = build('rpm_metadatum', package: package)
expect(rpm_metadatum).not_to be_valid
expect(rpm_metadatum.errors.to_a).to include('Package type must be RPM')
end
end
end
end

View File

@ -484,6 +484,12 @@ RSpec.describe ProjectPresenter do
end
describe '#autodevops_anchor_data' do
it 'returns nil if builds feature is not available' do
allow(project).to receive(:feature_available?).with(:builds, user).and_return(false)
expect(presenter.autodevops_anchor_data).to be_nil
end
context 'when Auto Devops is enabled' do
it 'returns anchor data' do
allow(project).to receive(:auto_devops_enabled?).and_return(true)

View File

@ -7,10 +7,11 @@ RSpec.describe API::Commits do
include ProjectForksHelper
include SessionHelpers
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
let(:project_id) { project.id }
@ -46,7 +47,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'project commits'
end
@ -413,6 +414,9 @@ RSpec.describe API::Commits do
end
describe 'create' do
let_it_be(:sequencer) { FactoryBot::Sequence.new(:new_file_path) { |n| "files/test/#{n}.rb" } }
let(:new_file_path) { sequencer.next }
let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' }
let(:invalid_c_params) do
{
@ -435,7 +439,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
file_path: 'foo/bar/baz.txt',
file_path: new_file_path,
content: 'puts 8'
}
]
@ -449,7 +453,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
file_path: 'foo/bar/baz.txt',
file_path: new_file_path,
content: 'puts 🦊'
}
]
@ -959,6 +963,7 @@ RSpec.describe API::Commits do
end
describe 'multiple operations' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:message) { 'Multiple actions' }
let(:invalid_mo_params) do
{
@ -1028,17 +1033,11 @@ RSpec.describe API::Commits do
}
end
it 'are committed as one in project repo' do
it 'is committed as one in project repo and includes stats' do
post api(url, user), params: valid_mo_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq(message)
end
it 'includes the commit stats' do
post api(url, user), params: valid_mo_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include 'stats'
end
@ -1124,7 +1123,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
@ -1139,6 +1139,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -1228,6 +1230,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -1269,8 +1273,14 @@ RSpec.describe API::Commits do
end
shared_examples_for 'ref with unaccessible pipeline' do
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
let(:pipeline) do
create(:ci_empty_pipeline,
project: project,
status: :created,
source: :push,
ref: 'master',
sha: commit.sha,
protected: false)
end
it 'does not include last_pipeline' do
@ -1308,7 +1318,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
@ -1338,6 +1348,7 @@ RSpec.describe API::Commits do
context 'when builds are disabled' do
before do
project
.reload
.project_feature
.update!(builds_access_level: ProjectFeature::DISABLED)
end
@ -1389,7 +1400,7 @@ RSpec.describe API::Commits do
context 'with private builds' do
before do
project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
project.reload.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with pipeline'
@ -1415,8 +1426,8 @@ RSpec.describe API::Commits do
end
context 'when authenticated', 'as non_member and project is public' do
let(:current_user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref with pipeline'
@ -1469,6 +1480,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -1478,7 +1491,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref diff'
end
@ -1568,6 +1581,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -1577,7 +1592,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref comments'
end
@ -1666,6 +1681,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:commit_id) { commit.id }
let(:branch) { 'master' }
@ -1703,6 +1719,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -1712,7 +1730,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: 'master' } }
@ -1851,6 +1869,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/revert' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:commit) { project.commit(commit_id) }
let(:branch) { 'master' }
@ -1891,7 +1910,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: branch } }
@ -1998,6 +2017,7 @@ RSpec.describe API::Commits do
end
describe 'POST /projects/:id/repository/commits/:sha/comments' do
let(:project) { create(:project, :repository, :private) }
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
let(:note) { 'My comment' }
@ -2018,6 +2038,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
include_context 'disabled repository'
it_behaves_like '404 response' do
@ -2027,7 +2049,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
let(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '400 response' do
let(:request) { post api(route), params: { note: 'My comment' } }
@ -2047,12 +2069,13 @@ RSpec.describe API::Commits do
it_behaves_like 'ref new comment'
it 'returns the inline comment' do
post api(route, current_user), params: { note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' }
path = project.repository.commit.raw_diffs.first.new_path
post api(route, current_user), params: { note: 'My comment', path: path, line: 1, line_type: 'new' }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/commit_note')
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
expect(json_response['path']).to eq(path)
expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
@ -2127,7 +2150,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
let(:project) { create(:project, :repository, :private) }
let_it_be(:project) { create(:project, :repository, :private) }
let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
@ -2159,7 +2183,8 @@ RSpec.describe API::Commits do
end
context 'public project' do
let(:project) { create(:project, :repository, :public, :merge_requests_private) }
let_it_be(:project) { create(:project, :repository, :public, :merge_requests_private) }
let(:non_member) { create(:user) }
it 'responds 403 when only members are allowed to read merge requests' do

View File

@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query returning expected runner'
end
context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
let(:runner_type) { 'PROJECT_TYPE' }
let(:status) { 'NEVER_CONTACTED' }
let!(:expected_runner) { project_runner }
it_behaves_like 'a working graphql query returning expected runner'
end
end
describe 'pagination' do

View File

@ -221,6 +221,15 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
end
context 'with update_stats: false' do
let_it_be(:extra_artifact_with_file) do
create(:ci_job_artifact, :zip, project: artifact_with_file.project)
end
let(:artifacts) do
Ci::JobArtifact.where(id: [artifact_with_file.id, extra_artifact_with_file.id,
artifact_without_file.id, trace_artifact.id])
end
it 'does not update project statistics' do
expect(ProjectStatistics).not_to receive(:increment_statistic)
@ -230,7 +239,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
it 'returns size statistics' do
expected_updates = {
statistics_updates: {
artifact_with_file.project => -artifact_with_file.file.size,
artifact_with_file.project => -(artifact_with_file.file.size + extra_artifact_with_file.file.size),
artifact_without_file.project => 0
}
}

View File

@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_redis_cache do
using RSpec::Parameterized::TableSyntax
include_context 'for a cleanup tags service'
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
@ -39,268 +41,141 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
describe '#execute' do
subject { service.execute }
context 'when no params are specified' do
let(:params) { {} }
it_behaves_like 'handling invalid params',
service_response_extra: {
before_truncate_size: 0,
after_truncate_size: 0,
before_delete_size: 0,
cached_tags_count: 0
},
supports_caching: true
it 'does not remove anything' do
expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:execute)
expect_no_caching
it_behaves_like 'when regex matching everything is specified',
delete_expectations: [%w(A Ba Bb C D E)],
service_response_extra: {
before_truncate_size: 6,
after_truncate_size: 6,
before_delete_size: 6,
cached_tags_count: 0
},
supports_caching: true
is_expected.to eq(expected_service_response(before_truncate_size: 0, after_truncate_size: 0, before_delete_size: 0))
end
end
it_behaves_like 'when delete regex matching specific tags is used',
service_response_extra: {
before_truncate_size: 2,
after_truncate_size: 2,
before_delete_size: 2,
cached_tags_count: 0
},
supports_caching: true
context 'when regex matching everything is specified' do
shared_examples 'removes all matches' do
it 'does remove all tags except latest' do
expect_no_caching
it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex',
service_response_extra: {
before_truncate_size: 1,
after_truncate_size: 1,
before_delete_size: 1,
cached_tags_count: 0
},
supports_caching: true
expect_delete(%w(A Ba Bb C D E))
it_behaves_like 'with allow regex value',
delete_expectations: [%w(A C D E)],
service_response_extra: {
before_truncate_size: 4,
after_truncate_size: 4,
before_delete_size: 4,
cached_tags_count: 0
},
supports_caching: true
is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C D E)))
end
end
it_behaves_like 'when keeping only N tags',
delete_expectations: [%w(Bb Ba C)],
service_response_extra: {
before_truncate_size: 4,
after_truncate_size: 4,
before_delete_size: 3,
cached_tags_count: 0
},
supports_caching: true
let(:params) do
{ 'name_regex_delete' => '.*' }
end
it_behaves_like 'when not keeping N tags',
delete_expectations: [%w(A Ba Bb C)],
service_response_extra: {
before_truncate_size: 4,
after_truncate_size: 4,
before_delete_size: 4,
cached_tags_count: 0
},
supports_caching: true
it_behaves_like 'removes all matches'
it_behaves_like 'when removing keeping only 3',
delete_expectations: [%w(Bb Ba C)],
service_response_extra: {
before_truncate_size: 6,
after_truncate_size: 6,
before_delete_size: 3,
cached_tags_count: 0
},
supports_caching: true
context 'with deprecated name_regex param' do
let(:params) do
{ 'name_regex' => '.*' }
end
it_behaves_like 'when removing older than 1 day',
delete_expectations: [%w(Ba Bb C)],
service_response_extra: {
before_truncate_size: 6,
after_truncate_size: 6,
before_delete_size: 3,
cached_tags_count: 0
},
supports_caching: true
it_behaves_like 'removes all matches'
end
end
it_behaves_like 'when combining all parameters',
delete_expectations: [%w(Bb Ba C)],
service_response_extra: {
before_truncate_size: 6,
after_truncate_size: 6,
before_delete_size: 3,
cached_tags_count: 0
},
supports_caching: true
context 'with invalid regular expressions' do
shared_examples 'handling an invalid regex' do
it 'keeps all tags' do
expect_no_caching
it_behaves_like 'when running a container_expiration_policy',
delete_expectations: [%w(Bb Ba C)],
service_response_extra: {
before_truncate_size: 6,
after_truncate_size: 6,
before_delete_size: 3,
cached_tags_count: 0
},
supports_caching: true
expect(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:new)
subject
end
it { is_expected.to eq(status: :error, message: 'invalid regex') }
it 'calls error tracking service' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
subject
end
end
context 'when name_regex_delete is invalid' do
let(:params) { { 'name_regex_delete' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex is invalid' do
let(:params) { { 'name_regex' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex_keep is invalid' do
let(:params) { { 'name_regex_keep' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
end
context 'when delete regex matching specific tags is used' do
let(:params) do
{ 'name_regex_delete' => 'C|D' }
end
it 'does remove C and D' do
expect_delete(%w(C D))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(C D), before_truncate_size: 2, after_truncate_size: 2, before_delete_size: 2))
end
context 'with overriding allow regex' do
let(:params) do
{ 'name_regex_delete' => 'C|D',
'name_regex_keep' => 'C' }
end
it 'does not remove C' do
expect_delete(%w(D))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
end
end
context 'with name_regex_delete overriding deprecated name_regex' do
let(:params) do
{ 'name_regex' => 'C|D',
'name_regex_delete' => 'D' }
end
it 'does not remove C' do
expect_delete(%w(D))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
end
end
end
context 'with allow regex value' do
let(:params) do
{ 'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*' }
end
it 'does not remove B*' do
expect_delete(%w(A C D E))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(A C D E), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
end
end
context 'when keeping only N tags' do
let(:params) do
{ 'name_regex' => 'A|B.*|C',
'keep_n' => 1 }
end
it 'sorts tags by date' do
expect_delete(%w(Bb Ba C))
expect_no_caching
expect(service).to receive(:order_by_date).and_call_original
is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 3))
end
end
context 'when not keeping N tags' do
let(:params) do
{ 'name_regex' => 'A|B.*|C' }
end
it 'does not sort tags by date' do
expect_delete(%w(A Ba Bb C))
expect_no_caching
expect(service).not_to receive(:order_by_date)
is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
end
end
context 'when removing keeping only 3' do
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 3 }
end
it 'does remove B* and C as they are the oldest' do
expect_delete(%w(Bb Ba C))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
end
end
context 'when removing older than 1 day' do
let(:params) do
{ 'name_regex_delete' => '.*',
'older_than' => '1 day' }
end
it 'does remove B* and C as they are older than 1 day' do
expect_delete(%w(Ba Bb C))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(Ba Bb C), before_delete_size: 3))
end
end
context 'when combining all parameters' do
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day' }
end
it 'does remove B* and C' do
expect_delete(%w(Bb Ba C))
expect_no_caching
is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
end
end
context 'when running a container_expiration_policy' do
context 'when running a container_expiration_policy with caching' do
let(:user) { nil }
context 'with valid container_expiration_policy param' do
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day',
'container_expiration_policy' => true }
end
before do
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
end
it { is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3)) }
context 'caching' do
it 'expects caching to be used' do
expect_caching
subject
end
context 'when setting set to false' do
before do
stub_application_setting(container_registry_expiration_policies_caching: false)
end
it 'does not use caching' do
expect_no_caching
subject
end
end
end
let(:params) do
{
'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day',
'container_expiration_policy' => true
}
end
context 'without container_expiration_policy param' do
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day' }
it 'expects caching to be used' do
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
expect_caching
subject
end
context 'when setting set to false' do
before do
stub_application_setting(container_registry_expiration_policies_caching: false)
end
it 'fails' do
is_expected.to eq(status: :error, message: 'access denied')
it 'does not use caching' do
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
expect_no_caching
subject
end
end
end
@ -322,10 +197,12 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
service_response = expected_service_response(
status: status,
original_size: original_size,
deleted: nil
).merge(
before_truncate_size: before_truncate_size,
after_truncate_size: after_truncate_size,
before_delete_size: before_delete_size,
deleted: nil
cached_tags_count: 0
)
expect(result).to eq(service_response)
@ -483,34 +360,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
end
def expect_delete(tags, container_expiration_policy: nil)
expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:new)
.with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
.and_call_original
expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
.to receive(:execute)
.with(repository) { { status: :success, deleted: tags } }
end
# all those -1 because the default tags on L13 have a "latest" that will be filtered out
def expected_service_response(status: :success, deleted: [], original_size: tags.size, before_truncate_size: tags.size - 1, after_truncate_size: tags.size - 1, before_delete_size: tags.size - 1)
{
status: status,
deleted: deleted,
original_size: original_size,
before_truncate_size: before_truncate_size,
after_truncate_size: after_truncate_size,
before_delete_size: before_delete_size,
cached_tags_count: 0
}.compact.merge(deleted_size: deleted&.size)
end
def expect_no_caching
expect(::Gitlab::Redis::Cache).not_to receive(:with)
end
def expect_caching
::Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:mget).and_call_original

View File

@ -0,0 +1,183 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
using RSpec::Parameterized::TableSyntax
include_context 'for a cleanup tags service'
let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
let(:repository) { create(:container_repository, :root, :import_done, project: project) }
let(:service) { described_class.new(repository, user, params) }
let(:tags) { %w[latest A Ba Bb C D E] }
before do
project.add_maintainer(user) if user
stub_container_registry_config(enabled: true)
stub_const("#{described_class}::TAGS_PAGE_SIZE", tags_page_size)
one_hour_ago = 1.hour.ago
five_days_ago = 5.days.ago
six_days_ago = 6.days.ago
one_month_ago = 1.month.ago
stub_tags(
{
'latest' => one_hour_ago,
'A' => one_hour_ago,
'Ba' => five_days_ago,
'Bb' => six_days_ago,
'C' => one_month_ago,
'D' => nil,
'E' => nil
}
)
end
describe '#execute' do
subject { service.execute }
context 'with several tags pages' do
let(:tags_page_size) { 2 }
it_behaves_like 'handling invalid params'
it_behaves_like 'when regex matching everything is specified',
delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]]
it_behaves_like 'when delete regex matching specific tags is used'
it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
it_behaves_like 'with allow regex value',
delete_expectations: [%w[A], %w[C D], %w[E]]
it_behaves_like 'when keeping only N tags',
delete_expectations: [%w[Bb]]
it_behaves_like 'when not keeping N tags',
delete_expectations: [%w[A], %w[Ba Bb], %w[C]]
context 'when removing keeping only 3' do
let(:params) do
{
'name_regex_delete' => '.*',
'keep_n' => 3
}
end
it_behaves_like 'not removing anything'
end
it_behaves_like 'when removing older than 1 day',
delete_expectations: [%w[Ba Bb], %w[C]]
it_behaves_like 'when combining all parameters',
delete_expectations: [%w[Bb], %w[C]]
it_behaves_like 'when running a container_expiration_policy',
delete_expectations: [%w[Bb], %w[C]]
context 'with a timeout' do
let(:params) do
{ 'name_regex_delete' => '.*' }
end
it 'removes the first few pages' do
expect(service).to receive(:timeout?).and_return(false, true)
expect_delete(%w[A])
expect_delete(%w[Ba Bb])
response = expected_service_response(status: :error, deleted: %w[A Ba Bb], original_size: 4)
is_expected.to eq(response)
end
end
end
context 'with a single tags page' do
let(:tags_page_size) { 1000 }
it_behaves_like 'handling invalid params'
it_behaves_like 'when regex matching everything is specified',
delete_expectations: [%w[A Ba Bb C D E]]
it_behaves_like 'when delete regex matching specific tags is used'
it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
it_behaves_like 'with allow regex value',
delete_expectations: [%w[A C D E]]
it_behaves_like 'when keeping only N tags',
delete_expectations: [%w[Ba Bb C]]
it_behaves_like 'when not keeping N tags',
delete_expectations: [%w[A Ba Bb C]]
it_behaves_like 'when removing keeping only 3',
delete_expectations: [%w[Ba Bb C]]
it_behaves_like 'when removing older than 1 day',
delete_expectations: [%w[Ba Bb C]]
it_behaves_like 'when combining all parameters',
delete_expectations: [%w[Ba Bb C]]
it_behaves_like 'when running a container_expiration_policy',
delete_expectations: [%w[Ba Bb C]]
end
end
private
def stub_tags(tags)
chunked = tags_page_size < tags.size
previous_last = nil
max_chunk_index = tags.size / tags_page_size
tags.keys.in_groups_of(tags_page_size, false).each_with_index do |chunked_tag_names, index|
last = index == max_chunk_index
pagination_needed = chunked && !last
response = {
pagination: pagination_needed ? pagination_with(last: chunked_tag_names.last) : {},
response_body: chunked_tag_names.map do |name|
tag_raw_response(name, tags[name])
end
}
allow(repository.gitlab_api_client)
.to receive(:tags)
.with(repository.path, page_size: described_class::TAGS_PAGE_SIZE, last: previous_last)
.and_return(response)
previous_last = chunked_tag_names.last
end
end
def pagination_with(last:)
{
next: {
uri: URI("http://test.org?last=#{last}")
}
}
end
def tag_raw_response(name, timestamp)
timestamp_field = name.start_with?('B') ? 'updated_at' : 'created_at'
{
'name' => name,
'digest' => 'sha256:1234567890',
'media_type' => 'application/vnd.oci.image.manifest.v1+json',
timestamp_field => timestamp&.iso8601
}
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.shared_context 'for a cleanup tags service' do
def expected_service_response(status: :success, deleted: [], original_size: tags.size)
{
status: status,
deleted: deleted,
original_size: original_size,
before_delete_size: deleted&.size
}.compact.merge(deleted_size: deleted&.size)
end
def expect_delete(tags, container_expiration_policy: nil)
service = instance_double('Projects::ContainerRepository::DeleteTagsService')
expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:new)
.with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
.and_return(service)
expect(service).to receive(:execute)
.with(repository) { { status: :success, deleted: tags } }
end
def expect_no_caching
expect(::Gitlab::Redis::Cache).not_to receive(:with)
end
end

View File

@ -0,0 +1,263 @@
# frozen_string_literal: true
RSpec.shared_examples 'handling invalid params' do |service_response_extra: {}, supports_caching: false|
context 'when no params are specified' do
let(:params) { {} }
it_behaves_like 'not removing anything',
service_response_extra: service_response_extra,
supports_caching: supports_caching
end
context 'with invalid regular expressions' do
shared_examples 'handling an invalid regex' do
it 'keeps all tags' do
expect(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:new)
expect_no_caching unless supports_caching
subject
end
it { is_expected.to eq(status: :error, message: 'invalid regex') }
it 'calls error tracking service' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
subject
end
end
context 'when name_regex_delete is invalid' do
let(:params) { { 'name_regex_delete' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex is invalid' do
let(:params) { { 'name_regex' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex_keep is invalid' do
let(:params) { { 'name_regex_keep' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
end
end
RSpec.shared_examples 'when regex matching everything is specified' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{ 'name_regex_delete' => '.*' }
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
context 'with deprecated name_regex param' do
let(:params) do
{ 'name_regex' => '.*' }
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
end
end
RSpec.shared_examples 'when delete regex matching specific tags is used' do
|service_response_extra: {}, supports_caching: false|
let(:params) do
{ 'name_regex_delete' => 'C|D' }
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: [%w[C D]]
end
RSpec.shared_examples 'when delete regex matching specific tags is used with overriding allow regex' do
|service_response_extra: {}, supports_caching: false|
let(:params) do
{
'name_regex_delete' => 'C|D',
'name_regex_keep' => 'C'
}
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: [%w[D]]
context 'with name_regex_delete overriding deprecated name_regex' do
let(:params) do
{
'name_regex' => 'C|D',
'name_regex_delete' => 'D'
}
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: [%w[D]]
end
end
RSpec.shared_examples 'with allow regex value' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{
'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*'
}
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
end
RSpec.shared_examples 'when keeping only N tags' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{
'name_regex' => 'A|B.*|C',
'keep_n' => 1
}
end
it 'sorts tags by date' do
delete_expectations.each { |expectation| expect_delete(expectation) }
expect_no_caching unless supports_caching
expect(service).to receive(:order_by_date_desc).at_least(:once).and_call_original
is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
end
end
RSpec.shared_examples 'when not keeping N tags' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{ 'name_regex' => 'A|B.*|C' }
end
it 'does not sort tags by date' do
delete_expectations.each { |expectation| expect_delete(expectation) }
expect_no_caching unless supports_caching
expect(service).not_to receive(:order_by_date_desc)
is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
end
end
RSpec.shared_examples 'when removing keeping only 3' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 3 }
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
end
RSpec.shared_examples 'when removing older than 1 day' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{
'name_regex_delete' => '.*',
'older_than' => '1 day'
}
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
end
RSpec.shared_examples 'when combining all parameters' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:params) do
{
'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day'
}
end
it_behaves_like 'removing the expected tags',
service_response_extra: service_response_extra,
supports_caching: supports_caching,
delete_expectations: delete_expectations
end
RSpec.shared_examples 'when running a container_expiration_policy' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
let(:user) { nil }
context 'with valid container_expiration_policy param' do
let(:params) do
{
'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day',
'container_expiration_policy' => true
}
end
it 'removes the expected tags' do
delete_expectations.each { |expectation| expect_delete(expectation, container_expiration_policy: true) }
expect_no_caching unless supports_caching
is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
end
end
context 'without container_expiration_policy param' do
let(:params) do
{
'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day'
}
end
it 'fails' do
is_expected.to eq(status: :error, message: 'access denied')
end
end
end
RSpec.shared_examples 'not removing anything' do |service_response_extra: {}, supports_caching: false|
it 'does not remove anything' do
expect(Projects::ContainerRepository::DeleteTagsService).not_to receive(:new)
expect_no_caching unless supports_caching
is_expected.to eq(expected_service_response(deleted: []).merge(service_response_extra))
end
end
RSpec.shared_examples 'removing the expected tags' do
|service_response_extra: {}, supports_caching: false, delete_expectations:|
it 'removes the expected tags' do
delete_expectations.each { |expectation| expect_delete(expectation) }
expect_no_caching unless supports_caching
is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
end
end

View File

@ -227,6 +227,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package10) { create(:rubygems_package, project: project) }
let_it_be(:package11) { create(:helm_package, project: project) }
let_it_be(:package12) { create(:terraform_module_package, project: project) }
let_it_be(:package13) { create(:rpm_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do

View File

@ -25,4 +25,15 @@ RSpec.describe 'groups/new.html.haml' do
expect(rendered).not_to have_checked_field('Just me')
end
end
context 'when a subgroup' do
let_it_be(:group) { create(:group, :nested) }
it 'renders the visibility level section' do
expect(rendered).to have_content('Visibility level')
expect(rendered).to have_field('Private')
expect(rendered).to have_field('Internal')
expect(rendered).to have_field('Public')
end
end
end