Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2af44d609e
commit
9c6578ed4e
|
@ -105,3 +105,4 @@ ee/changelogs/unreleased-ee
|
|||
tags.lock
|
||||
tags.temp
|
||||
.stylelintcache
|
||||
.solargraph.yml
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
include:
|
||||
- "**/*.rb"
|
||||
exclude:
|
||||
- "**/spec/**/*"
|
||||
- qa/qa/specs/features/**/*
|
||||
- vendor/**/*
|
||||
- ".bundle/**/*"
|
||||
require: []
|
||||
domains: []
|
||||
reporters:
|
||||
- rubocop
|
||||
- require_not_found
|
||||
require_paths: []
|
||||
plugins: []
|
||||
max_files: 15000
|
1
Gemfile
1
Gemfile
|
@ -346,6 +346,7 @@ end
|
|||
|
||||
group :development do
|
||||
gem 'lefthook', '~> 0.7.0', require: false
|
||||
gem 'solargraph', '~> 0.40.4', require: false
|
||||
|
||||
gem 'letter_opener_web', '~> 1.4.0'
|
||||
|
||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -133,10 +133,12 @@ GEM
|
|||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
babosa (1.0.2)
|
||||
backport (1.1.2)
|
||||
base32 (0.3.2)
|
||||
batch-loader (2.0.1)
|
||||
bcrypt (3.1.16)
|
||||
bcrypt_pbkdf (1.0.0)
|
||||
benchmark (0.1.1)
|
||||
benchmark-ips (2.3.0)
|
||||
benchmark-memory (0.1.2)
|
||||
memory_profiler (~> 0.9)
|
||||
|
@ -306,6 +308,7 @@ GEM
|
|||
dry-equalizer (~> 0.3)
|
||||
dry-inflector (~> 0.1, >= 0.1.2)
|
||||
dry-logic (~> 1.0, >= 1.0.2)
|
||||
e2mmap (0.1.0)
|
||||
ecma-re-validator (0.2.1)
|
||||
regexp_parser (~> 1.2)
|
||||
ed25519 (1.2.4)
|
||||
|
@ -633,6 +636,7 @@ GEM
|
|||
jaeger-client (1.1.0)
|
||||
opentracing (~> 0.3)
|
||||
thrift
|
||||
jaro_winkler (1.5.4)
|
||||
jira-ruby (2.1.4)
|
||||
activesupport
|
||||
atlassian-jwt
|
||||
|
@ -1194,6 +1198,20 @@ GEM
|
|||
slop (3.6.0)
|
||||
snowplow-tracker (0.6.1)
|
||||
contracts (~> 0.7, <= 0.11)
|
||||
solargraph (0.40.4)
|
||||
backport (~> 1.1)
|
||||
benchmark
|
||||
bundler (>= 1.17.2)
|
||||
e2mmap
|
||||
jaro_winkler (~> 1.5)
|
||||
kramdown (~> 2.3)
|
||||
kramdown-parser-gfm (~> 1.1)
|
||||
parser (~> 3.0)
|
||||
reverse_markdown (>= 1.0.5, < 3)
|
||||
rubocop (>= 0.52)
|
||||
thor (~> 1.0)
|
||||
tilt (~> 2.0)
|
||||
yard (~> 0.9, >= 0.9.24)
|
||||
spring (2.1.1)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
|
@ -1334,6 +1352,7 @@ GEM
|
|||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yajl-ruby (1.4.1)
|
||||
yard (0.9.26)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
|
@ -1587,6 +1606,7 @@ DEPENDENCIES
|
|||
simplecov-cobertura (~> 1.3.1)
|
||||
slack-messenger (~> 2.3.4)
|
||||
snowplow-tracker (~> 0.6.1)
|
||||
solargraph (~> 0.40.4)
|
||||
spring (~> 2.1.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
sprockets (~> 3.7.0)
|
||||
|
|
|
@ -1 +1,9 @@
|
|||
import '~/snippet/snippet_show';
|
||||
|
||||
const awardEmojiEl = document.getElementById('js-vue-awards-block');
|
||||
|
||||
if (awardEmojiEl) {
|
||||
import('~/emoji/awards_app')
|
||||
.then((m) => m.default(awardEmojiEl))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
|
@ -25,6 +25,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showLinks: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
viewType: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -91,8 +95,8 @@ export default {
|
|||
collectMetrics: true,
|
||||
};
|
||||
},
|
||||
shouldHideLinks() {
|
||||
return this.isStageView;
|
||||
showJobLinks() {
|
||||
return !this.isStageView && this.showLinks;
|
||||
},
|
||||
shouldShowStageName() {
|
||||
return !this.isStageView;
|
||||
|
@ -188,6 +192,7 @@ export default {
|
|||
:config-paths="configPaths"
|
||||
:linked-pipelines="upstreamPipelines"
|
||||
:column-title="__('Upstream')"
|
||||
:show-links="showJobLinks"
|
||||
:type="$options.pipelineTypeConstants.UPSTREAM"
|
||||
:view-type="viewType"
|
||||
@error="onError"
|
||||
|
@ -202,9 +207,8 @@ export default {
|
|||
:container-measurements="measurements"
|
||||
:highlighted-job="hoveredJobName"
|
||||
:metrics-config="metricsConfig"
|
||||
:never-show-links="shouldHideLinks"
|
||||
:show-links="showJobLinks"
|
||||
:view-type="viewType"
|
||||
default-link-color="gl-stroke-transparent"
|
||||
@error="onError"
|
||||
@highlightedJobsChange="updateHighlightedJobs"
|
||||
>
|
||||
|
@ -234,6 +238,7 @@ export default {
|
|||
:config-paths="configPaths"
|
||||
:linked-pipelines="downstreamPipelines"
|
||||
:column-title="__('Downstream')"
|
||||
:show-links="showJobLinks"
|
||||
:type="$options.pipelineTypeConstants.DOWNSTREAM"
|
||||
:view-type="viewType"
|
||||
@downstreamHovered="setSourceJob"
|
||||
|
|
|
@ -48,6 +48,7 @@ export default {
|
|||
pipeline: null,
|
||||
pipelineLayers: null,
|
||||
showAlert: false,
|
||||
showLinks: false,
|
||||
};
|
||||
},
|
||||
errorTexts: {
|
||||
|
@ -182,6 +183,9 @@ export default {
|
|||
}
|
||||
},
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
updateShowLinksState(val) {
|
||||
this.showLinks = val;
|
||||
},
|
||||
updateViewType(type) {
|
||||
this.currentViewType = type;
|
||||
},
|
||||
|
@ -202,7 +206,9 @@ export default {
|
|||
<graph-view-selector
|
||||
v-if="showGraphViewSelector"
|
||||
:type="currentViewType"
|
||||
:show-links="showLinks"
|
||||
@updateViewType="updateViewType"
|
||||
@updateShowLinksState="updateShowLinksState"
|
||||
/>
|
||||
</local-storage-sync>
|
||||
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
|
||||
|
@ -211,6 +217,7 @@ export default {
|
|||
:config-paths="configPaths"
|
||||
:pipeline="pipeline"
|
||||
:pipeline-layers="getPipelineLayers()"
|
||||
:show-links="showLinks"
|
||||
:view-type="currentViewType"
|
||||
@error="reportFailure"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STAGE_VIEW, LAYER_VIEW } from './constants';
|
||||
|
||||
export default {
|
||||
name: 'GraphViewSelector',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
GlLoadingIcon,
|
||||
GlSegmentedControl,
|
||||
GlToggle,
|
||||
},
|
||||
props: {
|
||||
showLinks: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -19,67 +22,119 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
currentViewType: STAGE_VIEW,
|
||||
currentViewType: this.type,
|
||||
showLinksActive: false,
|
||||
isToggleLoading: false,
|
||||
isSwitcherLoading: false,
|
||||
};
|
||||
},
|
||||
i18n: {
|
||||
labelText: __('Order jobs by'),
|
||||
viewLabelText: __('Group jobs by'),
|
||||
linksLabelText: __('Show dependencies'),
|
||||
},
|
||||
views: {
|
||||
[STAGE_VIEW]: {
|
||||
type: STAGE_VIEW,
|
||||
text: {
|
||||
primary: __('Stage'),
|
||||
secondary: __('View the jobs grouped into stages'),
|
||||
},
|
||||
},
|
||||
[LAYER_VIEW]: {
|
||||
type: LAYER_VIEW,
|
||||
text: {
|
||||
primary: __('%{codeStart}needs:%{codeEnd} relationships'),
|
||||
secondary: __('View what jobs are needed for a job to run'),
|
||||
primary: __('Job dependencies'),
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentDropdownText() {
|
||||
return this.$options.views[this.type].text.primary;
|
||||
showLinksToggle() {
|
||||
return this.currentViewType === LAYER_VIEW;
|
||||
},
|
||||
viewTypesList() {
|
||||
return Object.keys(this.$options.views).map((key) => {
|
||||
return {
|
||||
value: key,
|
||||
text: this.$options.views[key].text.primary,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/*
|
||||
How does this reset the loading? As we note in the methods comment below,
|
||||
the loader is set to on before the update work is undertaken (in the parent).
|
||||
Once the work is complete, one of these values will change, since that's the
|
||||
point of the work. When that happens, the related value will update and we are done.
|
||||
|
||||
The bonus for this approach is that it works the same whichever "direction"
|
||||
the work goes in.
|
||||
*/
|
||||
showLinks() {
|
||||
this.isToggleLoading = false;
|
||||
},
|
||||
type() {
|
||||
this.isSwitcherLoading = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
itemClick(type) {
|
||||
this.$emit('updateViewType', type);
|
||||
/*
|
||||
In both toggle methods, we use setTimeout so that the loading indicator displays,
|
||||
then the work is done to update the DOM. The process is:
|
||||
→ user clicks
|
||||
→ call stack: set loading to true
|
||||
→ render: the loading icon appears on the screen
|
||||
→ callback queue: now do the work to calculate the new view / links
|
||||
(note: this work is done in the parent after the event is emitted)
|
||||
|
||||
setTimeout is how we move the work to the callback queue.
|
||||
We can't use nextTick because that is called before the render loop.
|
||||
|
||||
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
|
||||
*/
|
||||
toggleView(type) {
|
||||
this.isSwitcherLoading = true;
|
||||
setTimeout(() => {
|
||||
this.$emit('updateViewType', type);
|
||||
});
|
||||
},
|
||||
toggleShowLinksActive(val) {
|
||||
this.isToggleLoading = true;
|
||||
setTimeout(() => {
|
||||
this.$emit('updateShowLinksState', val);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center gl-my-4">
|
||||
<span>{{ $options.i18n.labelText }}</span>
|
||||
<gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4">
|
||||
<template #button-content>
|
||||
<gl-sprintf :message="currentDropdownText">
|
||||
<template #code="{ content }">
|
||||
<code> {{ content }} </code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<gl-icon class="gl-px-2" name="angle-down" :size="16" />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="view in $options.views"
|
||||
:key="view.type"
|
||||
:secondary-text="view.text.secondary"
|
||||
@click="itemClick(view.type)"
|
||||
>
|
||||
<b>
|
||||
<gl-sprintf :message="view.text.primary">
|
||||
<template #code="{ content }">
|
||||
<code> {{ content }} </code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</b>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
<div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
|
||||
<gl-loading-icon
|
||||
v-if="isSwitcherLoading"
|
||||
data-testid="switcher-loading-state"
|
||||
class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
|
||||
size="lg"
|
||||
/>
|
||||
<span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
|
||||
<gl-segmented-control
|
||||
v-model="currentViewType"
|
||||
:options="viewTypesList"
|
||||
:disabled="isSwitcherLoading"
|
||||
data-testid="pipeline-view-selector"
|
||||
class="gl-mx-4"
|
||||
@input="toggleView"
|
||||
/>
|
||||
|
||||
<div v-if="showLinksToggle">
|
||||
<gl-toggle
|
||||
v-model="showLinksActive"
|
||||
data-testid="show-links-toggle"
|
||||
class="gl-mx-4"
|
||||
:label="$options.i18n.linksLabelText"
|
||||
:is-loading="isToggleLoading"
|
||||
label-position="left"
|
||||
@change="toggleShowLinksActive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -32,6 +32,10 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showLinks: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -217,6 +221,7 @@ export default {
|
|||
:config-paths="configPaths"
|
||||
:pipeline="currentPipeline"
|
||||
:pipeline-layers="getPipelineLayers(pipeline.id)"
|
||||
:show-links="showLinks"
|
||||
:is-linked-pipeline="true"
|
||||
:view-type="viewType"
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
|
@ -19,10 +18,8 @@ import LinksInner from './links_inner.vue';
|
|||
export default {
|
||||
name: 'LinksLayer',
|
||||
components: {
|
||||
GlAlert,
|
||||
LinksInner,
|
||||
},
|
||||
MAX_GROUPS: 200,
|
||||
props: {
|
||||
containerMeasurements: {
|
||||
type: Object,
|
||||
|
@ -37,10 +34,10 @@ export default {
|
|||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
neverShowLinks: {
|
||||
showLinks: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -67,29 +64,8 @@ export default {
|
|||
shouldCollectMetrics() {
|
||||
return this.metricsConfig.collectMetrics && this.metricsConfig.path;
|
||||
},
|
||||
showAlert() {
|
||||
/*
|
||||
This is a hard override that allows us to turn off the links without
|
||||
needing to remove the component entirely for iteration or based on graph type.
|
||||
*/
|
||||
if (this.neverShowLinks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.containerZero && !this.showLinkedLayers && !this.alertDismissed;
|
||||
},
|
||||
showLinkedLayers() {
|
||||
/*
|
||||
This is a hard override that allows us to turn off the links without
|
||||
needing to remove the component entirely for iteration or based on graph type.
|
||||
*/
|
||||
if (this.neverShowLinks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS)
|
||||
);
|
||||
return this.showLinks && !this.containerZero;
|
||||
},
|
||||
},
|
||||
errorCaptured(err, _vm, info) {
|
||||
|
@ -103,7 +79,7 @@ export default {
|
|||
is closed and functionality is enabled by default.
|
||||
*/
|
||||
|
||||
if (this.neverShowLinks && !isEmpty(this.pipelineData)) {
|
||||
if (!this.showLinks && !isEmpty(this.pipelineData)) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.prepareLinkData();
|
||||
});
|
||||
|
@ -151,13 +127,6 @@ export default {
|
|||
reportPerformance(this.metricsConfig.path, data);
|
||||
});
|
||||
},
|
||||
dismissAlert() {
|
||||
this.alertDismissed = true;
|
||||
},
|
||||
overrideShowLinks() {
|
||||
this.dismissAlert();
|
||||
this.showLinksOverride = true;
|
||||
},
|
||||
prepareLinkData() {
|
||||
this.beginPerfMeasure();
|
||||
let numLinks;
|
||||
|
@ -185,15 +154,6 @@ export default {
|
|||
<slot></slot>
|
||||
</links-inner>
|
||||
<div v-else>
|
||||
<gl-alert
|
||||
v-if="showAlert"
|
||||
class="gl-ml-4 gl-mb-4"
|
||||
:primary-button-text="$options.i18n.showLinksAnyways"
|
||||
@primaryAction="overrideShowLinks"
|
||||
@dismiss="dismissAlert"
|
||||
>
|
||||
{{ $options.i18n.tooManyJobs }}
|
||||
</gl-alert>
|
||||
<div class="gl-display-flex gl-relative">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,10 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
before_action :authorize_read_snippet!, except: [:new, :index]
|
||||
before_action :authorize_update_snippet!, only: :edit
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def index
|
||||
@snippet_counts = ::Snippets::CountService
|
||||
.new(current_user, project: @project)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class BaseUpdate < BaseMutation
|
||||
argument :position, GraphQL::INT_TYPE,
|
||||
required: false,
|
||||
description: 'Position of list within the board.'
|
||||
|
||||
argument :collapsed, GraphQL::BOOLEAN_TYPE,
|
||||
required: false,
|
||||
description: 'Indicates if the list is collapsed for this user.'
|
||||
|
||||
def resolve(list: nil, **args)
|
||||
if list.nil? || !can_read_list?(list)
|
||||
raise_resource_not_available_error!
|
||||
end
|
||||
|
||||
update_result = update_list(list, args)
|
||||
|
||||
{
|
||||
list: update_result[:list],
|
||||
errors: list.errors.full_messages
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_list(list, args)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def can_read_list?(list)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class Update < BaseMutation
|
||||
class Update < BaseUpdate
|
||||
graphql_name 'UpdateBoardList'
|
||||
|
||||
argument :list_id, Types::GlobalIDType[List],
|
||||
|
@ -11,29 +11,11 @@ module Mutations
|
|||
loads: Types::BoardListType,
|
||||
description: 'Global ID of the list.'
|
||||
|
||||
argument :position, GraphQL::INT_TYPE,
|
||||
required: false,
|
||||
description: 'Position of list within the board.'
|
||||
|
||||
argument :collapsed, GraphQL::BOOLEAN_TYPE,
|
||||
required: false,
|
||||
description: 'Indicates if the list is collapsed for this user.'
|
||||
|
||||
field :list,
|
||||
Types::BoardListType,
|
||||
null: true,
|
||||
description: 'Mutated list.'
|
||||
|
||||
def resolve(list: nil, **args)
|
||||
raise_resource_not_available_error! unless can_read_list?(list)
|
||||
update_result = update_list(list, args)
|
||||
|
||||
{
|
||||
list: update_result[:list],
|
||||
errors: list.errors.full_messages
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_list(list, args)
|
||||
|
@ -42,8 +24,6 @@ module Mutations
|
|||
end
|
||||
|
||||
def can_read_list?(list)
|
||||
return false unless list.present?
|
||||
|
||||
Ability.allowed?(current_user, :read_issue_board_list, list.board)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -310,9 +310,15 @@ module ApplicationSettingsHelper
|
|||
:throttle_authenticated_web_enabled,
|
||||
:throttle_authenticated_web_period_in_seconds,
|
||||
:throttle_authenticated_web_requests_per_period,
|
||||
:throttle_authenticated_packages_api_enabled,
|
||||
:throttle_authenticated_packages_api_period_in_seconds,
|
||||
:throttle_authenticated_packages_api_requests_per_period,
|
||||
:throttle_unauthenticated_enabled,
|
||||
:throttle_unauthenticated_period_in_seconds,
|
||||
:throttle_unauthenticated_requests_per_period,
|
||||
:throttle_unauthenticated_packages_api_enabled,
|
||||
:throttle_unauthenticated_packages_api_period_in_seconds,
|
||||
:throttle_unauthenticated_packages_api_requests_per_period,
|
||||
:throttle_protected_paths_enabled,
|
||||
:throttle_protected_paths_period_in_seconds,
|
||||
:throttle_protected_paths_requests_per_period,
|
||||
|
|
|
@ -72,4 +72,10 @@ module SnippetsHelper
|
|||
concat(file_count)
|
||||
end
|
||||
end
|
||||
|
||||
def project_snippets_award_api_path(snippet)
|
||||
if Feature.enabled?(:improved_emoji_picker, snippet.project, default_enabled: :yaml)
|
||||
api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -434,6 +434,14 @@ class ApplicationSetting < ApplicationRecord
|
|||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_unauthenticated_packages_api_requests_per_period,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_unauthenticated_packages_api_period_in_seconds,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_authenticated_api_requests_per_period,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
@ -450,6 +458,14 @@ class ApplicationSetting < ApplicationRecord
|
|||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_authenticated_packages_api_requests_per_period,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_authenticated_packages_api_period_in_seconds,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
||||
validates :throttle_protected_paths_requests_per_period,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
|
|
@ -156,6 +156,9 @@ module ApplicationSettingImplementation
|
|||
throttle_authenticated_web_enabled: false,
|
||||
throttle_authenticated_web_period_in_seconds: 3600,
|
||||
throttle_authenticated_web_requests_per_period: 7200,
|
||||
throttle_authenticated_packages_api_enabled: false,
|
||||
throttle_authenticated_packages_api_period_in_seconds: 15,
|
||||
throttle_authenticated_packages_api_requests_per_period: 1000,
|
||||
throttle_incident_management_notification_enabled: false,
|
||||
throttle_incident_management_notification_per_period: 3600,
|
||||
throttle_incident_management_notification_period_in_seconds: 3600,
|
||||
|
@ -165,6 +168,9 @@ module ApplicationSettingImplementation
|
|||
throttle_unauthenticated_enabled: false,
|
||||
throttle_unauthenticated_period_in_seconds: 3600,
|
||||
throttle_unauthenticated_requests_per_period: 3600,
|
||||
throttle_unauthenticated_packages_api_enabled: false,
|
||||
throttle_unauthenticated_packages_api_period_in_seconds: 15,
|
||||
throttle_unauthenticated_packages_api_requests_per_period: 800,
|
||||
time_tracking_limit_to_hours: false,
|
||||
two_factor_grace_period: 48,
|
||||
unique_ips_limit_enabled: false,
|
||||
|
|
|
@ -27,7 +27,7 @@ class ConfluenceService < Service
|
|||
end
|
||||
|
||||
def description
|
||||
s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
|
||||
s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
|
||||
end
|
||||
|
||||
def help
|
||||
|
@ -37,11 +37,11 @@ class ConfluenceService < Service
|
|||
wiki_url = project.wiki.web_url
|
||||
|
||||
s_(
|
||||
'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' %
|
||||
'ConfluenceService|Your GitLab wiki is still available at %{wiki_link}. To re-enable the link to the GitLab wiki, disable this integration.' %
|
||||
{ wiki_link: link_to(wiki_url, wiki_url) }
|
||||
).html_safe
|
||||
else
|
||||
s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe
|
||||
s_('ConfluenceService|Link to a Confluence Workspace from the sidebar. Enabling this integration replaces the "Wiki" sidebar link with a link to the Confluence Workspace. The GitLab wiki is still available at the original URL.').html_safe
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -50,8 +50,8 @@ class ConfluenceService < Service
|
|||
{
|
||||
type: 'text',
|
||||
name: 'confluence_url',
|
||||
title: 'Confluence Cloud Workspace URL',
|
||||
placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'),
|
||||
title: s_('Confluence Cloud Workspace URL'),
|
||||
placeholder: 'https://example.atlassian.net/wiki',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -192,16 +192,9 @@ class Wiki
|
|||
def delete_page(page, message = nil)
|
||||
return unless page
|
||||
|
||||
if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml)
|
||||
capture_git_error(:deleted) do
|
||||
repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
|
||||
capture_git_error(:deleted) do
|
||||
repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
|
||||
|
||||
after_wiki_activity
|
||||
|
||||
true
|
||||
end
|
||||
else
|
||||
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
|
||||
after_wiki_activity
|
||||
|
||||
true
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.label :abuse_notification_email, 'Abuse reports notification email', class: 'label-bold'
|
||||
= f.label :abuse_notification_email, _('Abuse reports notification email'), class: 'label-bold'
|
||||
= f.text_field :abuse_notification_email, class: 'form-control gl-form-input'
|
||||
.form-text.text-muted
|
||||
Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
|
||||
= _('Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.')
|
||||
|
||||
= f.submit 'Save changes', class: "gl-button btn btn-confirm"
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-packages-limits-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
%h5
|
||||
= _('Unauthenticated API request rate limit')
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :throttle_unauthenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_packages_api_checkbox' }
|
||||
= f.label :throttle_unauthenticated_packages_api_enabled, class: 'form-check-label label-bold' do
|
||||
= _('Enable unauthenticated API request rate limit')
|
||||
%span.form-text.text-muted
|
||||
= _('Helps reduce request volume (e.g. from crawlers or abusive bots)')
|
||||
.form-group
|
||||
= f.label :throttle_unauthenticated_packages_api_requests_per_period, 'Max unauthenticated API requests per period per IP', class: 'label-bold'
|
||||
= f.number_field :throttle_unauthenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
|
||||
.form-group
|
||||
= f.label :throttle_unauthenticated_packages_api_period_in_seconds, 'Unauthenticated API rate limit period in seconds', class: 'label-bold'
|
||||
= f.number_field :throttle_unauthenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
|
||||
%hr
|
||||
%h5
|
||||
= _('Authenticated API request rate limit')
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :throttle_authenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_packages_api_checkbox' }
|
||||
= f.label :throttle_authenticated_packages_api_enabled, class: 'form-check-label label-bold' do
|
||||
= _('Enable authenticated API request rate limit')
|
||||
%span.form-text.text-muted
|
||||
= _('Helps reduce request volume (e.g. from crawlers or abusive bots)')
|
||||
.form-group
|
||||
= f.label :throttle_authenticated_packages_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold'
|
||||
= f.number_field :throttle_authenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
|
||||
.form-group
|
||||
= f.label :throttle_authenticated_packages_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold'
|
||||
= f.number_field :throttle_authenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
|
||||
|
||||
= f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
|
|
@ -24,6 +24,17 @@
|
|||
.settings-content
|
||||
= render 'ip_limits'
|
||||
|
||||
%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
= _('Package Registry Rate Limits')
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Configure specific limits for Packages API requests that supersede the general user and IP rate limits.')
|
||||
.settings-content
|
||||
= render 'package_registry_limits'
|
||||
|
||||
%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
= page_title
|
||||
|
||||
.bs-callout.clearfix
|
||||
Pass the header
|
||||
%code X-Profile-Token: #{@profile_token}
|
||||
to profile the request
|
||||
= html_escape(_('Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request')) % { profile_token: @profile_token, codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe }
|
||||
|
||||
- if @profiles.present?
|
||||
.gl-mt-3
|
||||
|
@ -21,4 +19,4 @@
|
|||
admin_requests_profile_path(profile)
|
||||
- else
|
||||
%p
|
||||
No profiles found
|
||||
= _('No profiles found')
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
|
||||
|
||||
.row-content-block.top-block.content-component-block
|
||||
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
|
||||
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
|
||||
|
||||
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
%span= milestone.issues_visible_to_user(current_user).count
|
||||
.title.hide-collapsed
|
||||
= s_('MilestoneSidebar|Issues')
|
||||
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.issues_visible_to_user(current_user).count
|
||||
- if show_new_issue_link?(project)
|
||||
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: s_('MilestoneSidebar|New Issue') do
|
||||
= s_('MilestoneSidebar|New issue')
|
||||
|
@ -110,7 +110,7 @@
|
|||
%span= milestone.merge_requests.count
|
||||
.title.hide-collapsed
|
||||
= s_('MilestoneSidebar|Merge requests')
|
||||
%span.badge.badge-pill= milestone.merge_requests.count
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.merge_requests.count
|
||||
.value.hide-collapsed.bold
|
||||
- if !project || can?(current_user, :read_merge_request, project)
|
||||
%span.milestone-stat
|
||||
|
|
|
@ -21,82 +21,34 @@ module ContainerExpirationPolicies
|
|||
cleanup_tags_service_deleted_size
|
||||
].freeze
|
||||
|
||||
def perform_work
|
||||
return unless throttling_enabled?
|
||||
return unless container_repository
|
||||
delegate :perform_work, :remaining_work_count, to: :inner_instance
|
||||
|
||||
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
|
||||
log_extra_metadata_on_done(:project_id, project.id)
|
||||
|
||||
unless allowed_to_run?(container_repository)
|
||||
container_repository.cleanup_unscheduled!
|
||||
log_extra_metadata_on_done(:cleanup_status, :skipped)
|
||||
return
|
||||
def inner_instance
|
||||
strong_memoize(:inner_instance) do
|
||||
if loopless_enabled?
|
||||
Loopless.new(self)
|
||||
else
|
||||
Looping.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
result = ContainerExpirationPolicies::CleanupService.new(container_repository)
|
||||
.execute
|
||||
log_on_done(result)
|
||||
end
|
||||
|
||||
def remaining_work_count
|
||||
cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count
|
||||
cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count
|
||||
total_count = cleanup_scheduled_count + cleanup_unfinished_count
|
||||
|
||||
log_info(
|
||||
cleanup_scheduled_count: cleanup_scheduled_count,
|
||||
cleanup_unfinished_count: cleanup_unfinished_count,
|
||||
cleanup_total_count: total_count
|
||||
)
|
||||
|
||||
total_count
|
||||
end
|
||||
|
||||
def max_running_jobs
|
||||
return 0 unless throttling_enabled?
|
||||
|
||||
::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_worker_capacity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_to_run?(container_repository)
|
||||
return false unless policy&.enabled && policy&.next_run_at
|
||||
|
||||
Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at
|
||||
::Gitlab::CurrentSettings.container_registry_expiration_policies_worker_capacity
|
||||
end
|
||||
|
||||
def throttling_enabled?
|
||||
Feature.enabled?(:container_registry_expiration_policies_throttling)
|
||||
end
|
||||
|
||||
def loopless_enabled?
|
||||
Feature.enabled?(:container_registry_expiration_policies_loopless)
|
||||
end
|
||||
|
||||
def max_cleanup_execution_time
|
||||
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
|
||||
end
|
||||
|
||||
def policy
|
||||
project.container_expiration_policy
|
||||
end
|
||||
|
||||
def project
|
||||
container_repository.project
|
||||
end
|
||||
|
||||
def container_repository
|
||||
strong_memoize(:container_repository) do
|
||||
ContainerRepository.transaction do
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# We need a lock to prevent two workers from picking up the same row
|
||||
container_repository = ContainerRepository.waiting_for_cleanup
|
||||
.order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
|
||||
.limit(1)
|
||||
.lock('FOR UPDATE SKIP LOCKED')
|
||||
.first
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
container_repository&.tap(&:cleanup_ongoing!)
|
||||
end
|
||||
end
|
||||
::Gitlab::CurrentSettings.container_registry_delete_tags_service_timeout
|
||||
end
|
||||
|
||||
def log_info(extra_structure)
|
||||
|
@ -120,5 +72,100 @@ module ContainerExpirationPolicies
|
|||
log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated)
|
||||
log_extra_metadata_on_done(:running_jobs_count, running_jobs_count)
|
||||
end
|
||||
|
||||
# rubocop: disable Scalability/IdempotentWorker
|
||||
# TODO: move the logic from this class to the parent one when container_registry_expiration_policies_loopless is removed
|
||||
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273
|
||||
class Loopless
|
||||
# TODO fill the logic here with the approach documented in
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/267546#limited-worker
|
||||
def initialize(parent)
|
||||
@parent = parent
|
||||
end
|
||||
end
|
||||
# rubocop: enable Scalability/IdempotentWorker
|
||||
|
||||
# rubocop: disable Scalability/IdempotentWorker
|
||||
# TODO remove this class when `container_registry_expiration_policies_loopless` is removed
|
||||
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273
|
||||
class Looping
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
delegate :throttling_enabled?,
|
||||
:log_extra_metadata_on_done,
|
||||
:log_info,
|
||||
:log_on_done,
|
||||
:max_cleanup_execution_time,
|
||||
to: :@parent
|
||||
|
||||
def initialize(parent)
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
def perform_work
|
||||
return unless throttling_enabled?
|
||||
return unless container_repository
|
||||
|
||||
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
|
||||
log_extra_metadata_on_done(:project_id, project.id)
|
||||
|
||||
unless allowed_to_run?(container_repository)
|
||||
container_repository.cleanup_unscheduled!
|
||||
log_extra_metadata_on_done(:cleanup_status, :skipped)
|
||||
return
|
||||
end
|
||||
|
||||
result = ContainerExpirationPolicies::CleanupService.new(container_repository)
|
||||
.execute
|
||||
log_on_done(result)
|
||||
end
|
||||
|
||||
def remaining_work_count
|
||||
cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count
|
||||
cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count
|
||||
total_count = cleanup_scheduled_count + cleanup_unfinished_count
|
||||
|
||||
log_info(
|
||||
cleanup_scheduled_count: cleanup_scheduled_count,
|
||||
cleanup_unfinished_count: cleanup_unfinished_count,
|
||||
cleanup_total_count: total_count
|
||||
)
|
||||
|
||||
total_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_to_run?(container_repository)
|
||||
return false unless policy&.enabled && policy&.next_run_at
|
||||
|
||||
Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at
|
||||
end
|
||||
|
||||
def policy
|
||||
project.container_expiration_policy
|
||||
end
|
||||
|
||||
def project
|
||||
container_repository.project
|
||||
end
|
||||
|
||||
def container_repository
|
||||
strong_memoize(:container_repository) do
|
||||
ContainerRepository.transaction do
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# We need a lock to prevent two workers from picking up the same row
|
||||
container_repository = ContainerRepository.waiting_for_cleanup
|
||||
.order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
|
||||
.limit(1)
|
||||
.lock('FOR UPDATE SKIP LOCKED')
|
||||
.first
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
container_repository&.tap(&:cleanup_ongoing!)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop: enable Scalability/IdempotentWorker
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable new RPC to destroy wiki pages
|
||||
merge_request: 57106
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalise strings in _abuse.html.haml
|
||||
merge_request: 57968
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in requests_profiles/index.html.haml
|
||||
merge_request: 58161
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow access to registry API of the current project using the job token
|
||||
merge_request: 49750
|
||||
author: Mathieu Parent
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove unneeded index on packages_debian_{project,group}_architectures.distribution_id
|
||||
merge_request: 59615
|
||||
author: Mathieu Parent
|
||||
type: removed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add specific rate limits for Package Registry (Package API)
|
||||
merge_request: 57029
|
||||
author: Jonas Wälter @wwwjon
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add gl-badge for badges in milestone drawer
|
||||
merge_request: 57964
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update UI text for confluence integration
|
||||
merge_request: 59839
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_job_token_scope
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300821
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: gitaly_replace_wiki_delete_page
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56495
|
||||
rollout_issue_url:
|
||||
milestone: '13.10'
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: pipeline_graph_layers_view
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56865
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328538
|
||||
milestone: '13.11'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddThrottlePackageRegistryColumns < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :application_settings, :throttle_unauthenticated_packages_api_requests_per_period, :integer, default: 800, null: false
|
||||
add_column :application_settings, :throttle_unauthenticated_packages_api_period_in_seconds, :integer, default: 15, null: false
|
||||
add_column :application_settings, :throttle_authenticated_packages_api_requests_per_period, :integer, default: 1000, null: false
|
||||
add_column :application_settings, :throttle_authenticated_packages_api_period_in_seconds, :integer, default: 15, null: false
|
||||
add_column :application_settings, :throttle_unauthenticated_packages_api_enabled, :boolean, default: false, null: false
|
||||
add_column :application_settings, :throttle_authenticated_packages_api_enabled, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDebianGroupArchitecturesDistributionIdIndex < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INDEX_NAME = 'idx_pkgs_deb_grp_architectures_on_distribution_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index :packages_debian_group_architectures, :distribution_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :packages_debian_group_architectures, :distribution_id, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDebianProjectArchitecturesDistributionIdIndex < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INDEX_NAME = 'idx_pkgs_deb_proj_architectures_on_distribution_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index :packages_debian_project_architectures, :distribution_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :packages_debian_project_architectures, :distribution_id, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
28b1e8add8ac7249be55ccd25e60c8a181d2ff036a7d69ac861bcdb5bf5e84e1
|
|
@ -0,0 +1 @@
|
|||
da9c3d764a5750a40e0f6edd2e713efd77620ba3e684e48d47c7f855e47b2984
|
|
@ -0,0 +1 @@
|
|||
7a7b0eaa67851aa9300e4750fd05c6d2d0b49ca7077099a0208a89c74ac03a2c
|
|
@ -9445,6 +9445,12 @@ CREATE TABLE application_settings (
|
|||
encrypted_external_pipeline_validation_service_token text,
|
||||
encrypted_external_pipeline_validation_service_token_iv text,
|
||||
external_pipeline_validation_service_url text,
|
||||
throttle_unauthenticated_packages_api_requests_per_period integer DEFAULT 800 NOT NULL,
|
||||
throttle_unauthenticated_packages_api_period_in_seconds integer DEFAULT 15 NOT NULL,
|
||||
throttle_authenticated_packages_api_requests_per_period integer DEFAULT 1000 NOT NULL,
|
||||
throttle_authenticated_packages_api_period_in_seconds integer DEFAULT 15 NOT NULL,
|
||||
throttle_unauthenticated_packages_api_enabled boolean DEFAULT false NOT NULL,
|
||||
throttle_authenticated_packages_api_enabled boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
|
||||
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
|
||||
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
|
||||
|
@ -21786,10 +21792,6 @@ CREATE INDEX idx_packages_debian_project_component_files_on_architecture_id ON p
|
|||
|
||||
CREATE INDEX idx_packages_packages_on_project_id_name_version_package_type ON packages_packages USING btree (project_id, name, version, package_type);
|
||||
|
||||
CREATE INDEX idx_pkgs_deb_grp_architectures_on_distribution_id ON packages_debian_group_architectures USING btree (distribution_id);
|
||||
|
||||
CREATE INDEX idx_pkgs_deb_proj_architectures_on_distribution_id ON packages_debian_project_architectures USING btree (distribution_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_pkgs_dep_links_on_pkg_id_dependency_id_dependency_type ON packages_dependency_links USING btree (package_id, dependency_id, dependency_type);
|
||||
|
||||
CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL);
|
||||
|
|
|
@ -69,6 +69,15 @@ Read more on [protected path rate limits](../user/admin_area/settings/protected_
|
|||
|
||||
- **Default rate limit** - After 10 requests, the client must wait 60 seconds before trying again
|
||||
|
||||
### Package Registry
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57029) in GitLab 13.12.
|
||||
|
||||
This setting limits the request rate on the Packages API per user or IP. For more information, see
|
||||
[Package Registry Rate Limits](../user/admin_area/settings/package_registry_rate_limits.md).
|
||||
|
||||
- **Default rate limit:** Disabled by default
|
||||
|
||||
### Import/Export
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2.
|
||||
|
|
|
@ -98,6 +98,12 @@ To enable the Packages feature:
|
|||
|
||||
1. [Restart GitLab](../restart_gitlab.md#helm-chart-installations "How to reconfigure Helm GitLab") for the changes to take effect.
|
||||
|
||||
## Rate limits
|
||||
|
||||
When downloading packages as dependencies in downstream projects, many requests are made through the
|
||||
Packages API. You may therefore reach enforced user and IP rate limits. To address this issue, you
|
||||
can define specific rate limits for the Packages API. For more details, see [Package Registry Rate Limits](../../user/admin_area/settings/package_registry_rate_limits.md).
|
||||
|
||||
## Changing the storage path
|
||||
|
||||
By default, the packages are stored locally, but you can change the default
|
||||
|
|
|
@ -208,6 +208,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
|
|||
Package Registry, you can use [deploy tokens](../user/project/deploy_tokens/index.md).
|
||||
- [Container Registry](../user/packages/container_registry/index.md)
|
||||
(the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
|
||||
- [Container Registry API](container_registry.md) (scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled)
|
||||
- [Get job artifacts](job_artifacts.md#get-job-artifacts).
|
||||
- [Get job token's job](jobs.md#get-job-tokens-job).
|
||||
- [Pipeline triggers](pipeline_triggers.md), using the `token=` parameter.
|
||||
|
|
|
@ -6,10 +6,30 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Container Registry API
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/55978) in GitLab 11.8.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/55978) in GitLab 11.8.
|
||||
> - The use of `CI_JOB_TOKEN` scoped to the current project was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12.
|
||||
|
||||
This is the API documentation of the [GitLab Container Registry](../user/packages/container_registry/index.md).
|
||||
|
||||
When the `ci_job_token_scope` feature flag is enabled (it is **disabled by default**), you can use the below endpoints
|
||||
from a CI/CD job, by passing the `$CI_JOB_TOKEN` variable as the `JOB-TOKEN` header.
|
||||
The job token will only have access to its own project.
|
||||
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can opt to enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:ci_job_token_scope)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:ci_job_token_scope)
|
||||
```
|
||||
|
||||
## List registry repositories
|
||||
|
||||
### Within a project
|
||||
|
|
|
@ -6777,6 +6777,16 @@ Autogenerated return type of UpdateContainerExpirationPolicy.
|
|||
| `containerExpirationPolicy` | [`ContainerExpirationPolicy`](#containerexpirationpolicy) | The container expiration policy after mutation. |
|
||||
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `UpdateEpicBoardListPayload`
|
||||
|
||||
Autogenerated return type of UpdateEpicBoardList.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| `list` | [`EpicList`](#epiclist) | Mutated epic list. |
|
||||
|
||||
### `UpdateEpicPayload`
|
||||
|
||||
Autogenerated return type of UpdateEpic.
|
||||
|
|
|
@ -100,6 +100,9 @@ Our codebase style is defined and enforced by [RuboCop](https://github.com/ruboc
|
|||
You can check for any offenses locally with `bundle exec rubocop --parallel`.
|
||||
On the CI, this is automatically checked by the `static-analysis` jobs.
|
||||
|
||||
In addition, you can [integrate RuboCop](../developing_with_solargraph.md) into
|
||||
supported IDEs using the [solargraph](https://github.com/castwide/solargraph) gem.
|
||||
|
||||
For RuboCop rules that we have not taken a decision on yet, we follow the
|
||||
[Ruby Style Guide](https://github.com/rubocop-hq/ruby-style-guide),
|
||||
[Rails Style Guide](https://github.com/rubocop-hq/rails-style-guide), and
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Using Solargraph
|
||||
|
||||
Gemfile packages [Solargraph](https://github.com/castwide/solargraph) language server for additional IntelliSense and code formatting capabilities with editors that support it.
|
||||
|
||||
Example configuration for solargraph can be found in [.solargraph.yml.example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.solargraph.yml.example) file. Copy the contents of this file to `.solargraph.yml` file for language server to pick this configuration up. Since `.solargraph.yml` configuration file is ignored by Git, it's possible to adjust configuration according to your needs.
|
||||
|
||||
Refer to particular IDE plugin documentation on how to integrate it with solargraph language server:
|
||||
|
||||
- **Visual Studio Code**
|
||||
- GitHub: [vscode-solargraph](https://github.com/castwide/vscode-solargraph)
|
||||
|
||||
- **Atom**
|
||||
- GitHub: [atom-solargraph](https://github.com/castwide/atom-solargraph)
|
||||
|
||||
- **Vim**
|
||||
- GitHub: [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim)
|
||||
|
||||
- **Emacs**
|
||||
- GitHub: [emacs-solargraph](https://github.com/guskovd/emacs-solargraph)
|
||||
|
||||
- **Eclipse**
|
||||
- GitHub: [eclipse-solargraph](https://github.com/PyvesB/eclipse-solargraph)
|
|
@ -33,6 +33,7 @@ These are rate limits you can set in the Admin Area of your instance:
|
|||
- [Protected paths](../user/admin_area/settings/protected_paths.md)
|
||||
- [Raw endpoints rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md)
|
||||
- [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md)
|
||||
- [Package registry rate limits](../user/admin_area/settings/package_registry_rate_limits.md)
|
||||
|
||||
## Non-configurable limits
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@ Access the default page for admin area settings by navigating to **Admin Area >
|
|||
| ------ | ----------- |
|
||||
| Performance optimization | [Write to "authorized_keys" file](../../../administration/operations/fast_ssh_key_lookup.md#setting-up-fast-lookup-via-gitlab-shell) and [Push event activities limit and bulk push events](push_event_activities_limit.md). Various settings that affect GitLab performance. |
|
||||
| [User and IP rate limits](user_and_ip_rate_limits.md) | Configure limits for web and API requests. |
|
||||
| [Package Registry Rate Limits](package_registry_rate_limits.md) | Configure specific limits for Packages API requests that supersede the user and IP rate limits. |
|
||||
| [Outbound requests](../../../security/webhooks.md) | Allow requests to the local network from hooks and services. |
|
||||
| [Protected Paths](protected_paths.md) | Configure paths to be protected by Rack Attack. |
|
||||
| [Incident Management](../../../operations/incident_management/index.md) Limits | Configure limits on the number of inbound alerts able to be sent to a project. |
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
stage: Package
|
||||
group: Package
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Package Registry Rate Limits **(FREE SELF)**
|
||||
|
||||
Rate limiting is a common technique used to improve the security and durability of a web
|
||||
application. For more details, see [Rate limits](../../../security/rate_limits.md). General user and
|
||||
IP rate limits can be enforced in **Admin Area > Settings > Network > User and IP rate limits**.
|
||||
For more details, see [User and IP rate limits](user_and_ip_rate_limits.md).
|
||||
|
||||
With the [GitLab Package Registry](../../packages/package_registry/index.md),
|
||||
you can use GitLab as a private or public registry for a variety of common package managers. You can
|
||||
publish and share packages, which others can consume as a dependency in downstream projects through
|
||||
the [Packages API](../../../api/packages.md).
|
||||
|
||||
When downloading such dependencies in downstream projects, many requests are made through the
|
||||
Packages API. You may therefore reach enforced user and IP rate limits. To address this issue, you
|
||||
can define specific rate limits for the Packages API in
|
||||
**Admin Area > Settings > Network > Package Registry Rate Limits**:
|
||||
|
||||
- Unauthenticated Packages API requests
|
||||
- Authenticated Packages API requests
|
||||
|
||||
These limits are disabled by default. When enabled, they supersede the general user and IP rate
|
||||
limits for requests to the Packages API. You can therefore keep the general user and IP rate limits,
|
||||
and increase (if necessary) the rate limits for the Packages API.
|
||||
|
||||
Besides this precedence, there are no differences in functionality compared to the general user and
|
||||
IP rate limits. For more details, see [User and IP rate limits](user_and_ip_rate_limits.md).
|
|
@ -133,6 +133,8 @@ The possible names are:
|
|||
- `throttle_unauthenticated_protected_paths`
|
||||
- `throttle_authenticated_protected_paths_api`
|
||||
- `throttle_authenticated_protected_paths_web`
|
||||
- `throttle_unauthenticated_packages_api`
|
||||
- `throttle_authenticated_packages_api`
|
||||
|
||||
For example, to try out throttles for all authenticated requests to
|
||||
non-protected paths can be done by setting
|
||||
|
|
|
@ -56,7 +56,7 @@ There are several components that work in concert for the Agent to accomplish Gi
|
|||
You can use the same GitLab project or separate projects for configuration and manifest files, as follows:
|
||||
|
||||
- Single GitLab project (recommended): when you use a single repository to hold both the manifest and the configuration files, these projects can be either private or public, as you prefer.
|
||||
- Two GitLab projects: when you opt to use two different GitLab projects, one for manifest files, and another for configuration files, the manifests project must be private, while the configuration project can be either private or public. Our backlog contains issues for adding support for
|
||||
- Two GitLab projects: when you opt to use two different GitLab projects, one for manifest files, and another for configuration files, the manifests project must be public, while the configuration project can be either private or public. Our backlog contains issues for adding support for
|
||||
[private manifest repositories outside of the configuration project](https://gitlab.com/gitlab-org/gitlab/-/issues/220912) and
|
||||
[group level agents](https://gitlab.com/gitlab-org/gitlab/-/issues/283885) in the future.
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ module API
|
|||
authorize_read_package!(user_group)
|
||||
end
|
||||
|
||||
namespace ':id/packages/debian' do
|
||||
namespace ':id/-/packages/debian' do
|
||||
include DebianPackageEndpoints
|
||||
end
|
||||
end
|
||||
|
|
|
@ -124,12 +124,22 @@ module API
|
|||
def find_project!(id)
|
||||
project = find_project(id)
|
||||
|
||||
return forbidden! unless authorized_project_scope?(project)
|
||||
|
||||
return project if can?(current_user, :read_project, project)
|
||||
return unauthorized! if authenticate_non_public?
|
||||
|
||||
not_found!('Project')
|
||||
end
|
||||
|
||||
def authorized_project_scope?(project)
|
||||
return true unless job_token_authentication?
|
||||
return true unless route_authentication_setting[:job_token_scope] == :project
|
||||
|
||||
::Feature.enabled?(:ci_job_token_scope, project, default_enabled: :yaml) &&
|
||||
current_authenticated_job.project == project
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_group(id)
|
||||
if id.to_s =~ /^\d+$/
|
||||
|
|
|
@ -15,6 +15,7 @@ module API
|
|||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
route_setting :authentication, job_token_allowed: true, job_token_scope: :project
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc 'Get a project container repositories' do
|
||||
detail 'This feature was introduced in GitLab 11.8.'
|
||||
|
|
|
@ -73,12 +73,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def delete_page(page_path, commit_details)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_delete_page(page_path, commit_details)
|
||||
end
|
||||
end
|
||||
|
||||
def update_page(page_path, title, format, content, commit_details)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_update_page(page_path, title, format, content, commit_details)
|
||||
|
@ -140,10 +134,6 @@ module Gitlab
|
|||
gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
|
||||
end
|
||||
|
||||
def gitaly_delete_page(page_path, commit_details)
|
||||
gitaly_wiki_client.delete_page(page_path, commit_details)
|
||||
end
|
||||
|
||||
def gitaly_find_page(title:, version: nil, dir: nil)
|
||||
return unless title.present?
|
||||
|
||||
|
|
|
@ -64,16 +64,6 @@ module Gitlab
|
|||
GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum, timeout: GitalyClient.medium_timeout)
|
||||
end
|
||||
|
||||
def delete_page(page_path, commit_details)
|
||||
request = Gitaly::WikiDeletePageRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
page_path: encode_binary(page_path),
|
||||
commit_details: gitaly_commit_details(commit_details)
|
||||
)
|
||||
|
||||
GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request, timeout: GitalyClient.medium_timeout)
|
||||
end
|
||||
|
||||
def find_page(title:, version: nil, dir: nil)
|
||||
request = Gitaly::WikiFindPageRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
|
|
|
@ -19,7 +19,8 @@ module Gitlab
|
|||
:throttle_authenticated_api,
|
||||
:throttle_authenticated_web,
|
||||
:throttle_authenticated_protected_paths_api,
|
||||
:throttle_authenticated_protected_paths_web
|
||||
:throttle_authenticated_protected_paths_web,
|
||||
:throttle_authenticated_packages_api
|
||||
].freeze
|
||||
|
||||
PAYLOAD_KEYS = [
|
||||
|
|
|
@ -83,16 +83,13 @@ module Gitlab
|
|||
|
||||
def self.configure_throttles(rack_attack)
|
||||
throttle_or_track(rack_attack, 'throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
|
||||
if !req.should_be_skipped? &&
|
||||
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
|
||||
req.unauthenticated?
|
||||
if req.throttle_unauthenticated?
|
||||
req.ip
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
|
||||
if req.api_request? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_api_enabled
|
||||
if req.throttle_authenticated_api?
|
||||
req.throttled_user_id([:api])
|
||||
end
|
||||
end
|
||||
|
@ -107,40 +104,41 @@ module Gitlab
|
|||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
|
||||
if req.web_request? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled
|
||||
if req.throttle_authenticated_web?
|
||||
req.throttled_user_id([:api, :rss, :ics])
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req|
|
||||
if req.post? &&
|
||||
!req.should_be_skipped? &&
|
||||
req.protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled? &&
|
||||
req.unauthenticated?
|
||||
if req.throttle_unauthenticated_protected_paths?
|
||||
req.ip
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
|
||||
if req.post? &&
|
||||
req.api_request? &&
|
||||
req.protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled?
|
||||
if req.throttle_authenticated_protected_paths_api?
|
||||
req.throttled_user_id([:api])
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
|
||||
if req.post? &&
|
||||
req.web_request? &&
|
||||
req.protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled?
|
||||
if req.throttle_authenticated_protected_paths_web?
|
||||
req.throttled_user_id([:api, :rss, :ics])
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_unauthenticated_packages_api', Gitlab::Throttle.unauthenticated_packages_api_options) do |req|
|
||||
if req.throttle_unauthenticated_packages_api?
|
||||
req.ip
|
||||
end
|
||||
end
|
||||
|
||||
throttle_or_track(rack_attack, 'throttle_authenticated_packages_api', Gitlab::Throttle.authenticated_packages_api_options) do |req|
|
||||
if req.throttle_authenticated_packages_api?
|
||||
req.throttled_user_id([:api])
|
||||
end
|
||||
end
|
||||
|
||||
rack_attack.safelist('throttle_bypass_header') do |req|
|
||||
Gitlab::Throttle.bypass_header.present? &&
|
||||
req.get_header(Gitlab::Throttle.bypass_header) == '1'
|
||||
|
|
|
@ -58,6 +58,57 @@ module Gitlab
|
|||
path =~ protected_paths_regex
|
||||
end
|
||||
|
||||
def throttle_unauthenticated?
|
||||
!should_be_skipped? &&
|
||||
!throttle_unauthenticated_packages_api? &&
|
||||
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
|
||||
unauthenticated?
|
||||
end
|
||||
|
||||
def throttle_authenticated_api?
|
||||
api_request? &&
|
||||
!throttle_authenticated_packages_api? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_api_enabled
|
||||
end
|
||||
|
||||
def throttle_authenticated_web?
|
||||
web_request? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled
|
||||
end
|
||||
|
||||
def throttle_unauthenticated_protected_paths?
|
||||
post? &&
|
||||
!should_be_skipped? &&
|
||||
protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled? &&
|
||||
unauthenticated?
|
||||
end
|
||||
|
||||
def throttle_authenticated_protected_paths_api?
|
||||
post? &&
|
||||
api_request? &&
|
||||
protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled?
|
||||
end
|
||||
|
||||
def throttle_authenticated_protected_paths_web?
|
||||
post? &&
|
||||
web_request? &&
|
||||
protected_path? &&
|
||||
Gitlab::Throttle.protected_paths_enabled?
|
||||
end
|
||||
|
||||
def throttle_unauthenticated_packages_api?
|
||||
packages_api_path? &&
|
||||
Gitlab::Throttle.settings.throttle_unauthenticated_packages_api_enabled &&
|
||||
unauthenticated?
|
||||
end
|
||||
|
||||
def throttle_authenticated_packages_api?
|
||||
packages_api_path? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_packages_api_enabled
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticated_user_id(request_formats)
|
||||
|
@ -75,6 +126,10 @@ module Gitlab
|
|||
def protected_paths_regex
|
||||
Regexp.union(protected_paths.map { |path| /\A#{Regexp.escape(path)}/ })
|
||||
end
|
||||
|
||||
def packages_api_path?
|
||||
path =~ ::Gitlab::Regex::Packages::API_PATH_REGEX
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ module Gitlab
|
|||
CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
|
||||
CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
|
||||
|
||||
API_PATH_REGEX = %r{^/api/v\d+/(projects/[^/]+/|groups?/[^/]+/-/)?packages/[A-Za-z]+}.freeze
|
||||
|
||||
def conan_package_reference_regex
|
||||
@conan_package_reference_regex ||= %r{\A[A-Za-z0-9]+\z}.freeze
|
||||
end
|
||||
|
|
|
@ -49,6 +49,20 @@ module Gitlab
|
|||
{ limit: limit_proc, period: period_proc }
|
||||
end
|
||||
|
||||
def self.unauthenticated_packages_api_options
|
||||
limit_proc = proc { |req| settings.throttle_unauthenticated_packages_api_requests_per_period }
|
||||
period_proc = proc { |req| settings.throttle_unauthenticated_packages_api_period_in_seconds.seconds }
|
||||
|
||||
{ limit: limit_proc, period: period_proc }
|
||||
end
|
||||
|
||||
def self.authenticated_packages_api_options
|
||||
limit_proc = proc { |req| settings.throttle_authenticated_packages_api_requests_per_period }
|
||||
period_proc = proc { |req| settings.throttle_authenticated_packages_api_period_in_seconds.seconds }
|
||||
|
||||
{ limit: limit_proc, period: period_proc }
|
||||
end
|
||||
|
||||
def self.rate_limiting_response_text
|
||||
(settings.rate_limiting_response_text.presence || DEFAULT_RATE_LIMITING_RESPONSE_TEXT) + "\n"
|
||||
end
|
||||
|
|
|
@ -436,18 +436,10 @@ module Gitlab
|
|||
projects_jira_dvcs_server_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false))
|
||||
}
|
||||
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services|
|
||||
counts = services.group_by do |service|
|
||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
|
||||
service_url&.include?('.atlassian.net') ? :cloud : :server
|
||||
end
|
||||
jira_service_data_hash = jira_service_data
|
||||
results[:projects_jira_server_active] = jira_service_data_hash[:projects_jira_server_active]
|
||||
results[:projects_jira_cloud_active] = jira_service_data_hash[:projects_jira_cloud_active]
|
||||
|
||||
results[:projects_jira_server_active] += counts[:server].size if counts[:server]
|
||||
results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
|
||||
end
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
results
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
{ projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK }
|
||||
|
|
|
@ -25,10 +25,17 @@ module Gitlab
|
|||
SQL_METRIC_DEFAULT
|
||||
end
|
||||
|
||||
def maximum_id(model)
|
||||
def maximum_id(model, column = nil)
|
||||
end
|
||||
|
||||
def minimum_id(model)
|
||||
def minimum_id(model, column = nil)
|
||||
end
|
||||
|
||||
def jira_service_data
|
||||
{
|
||||
projects_jira_server_active: 0,
|
||||
projects_jira_cloud_active: 0
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,27 @@ module Gitlab
|
|||
relation.select(relation.all.table[column].sum).to_sql
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def histogram(relation, column, buckets:, bucket_size: buckets.size)
|
||||
count_grouped = relation.group(column).select(Arel.star.count.as('count_grouped'))
|
||||
cte = Gitlab::SQL::CTE.new(:count_cte, count_grouped)
|
||||
|
||||
bucket_segments = bucket_size - 1
|
||||
width_bucket = Arel::Nodes::NamedFunction
|
||||
.new('WIDTH_BUCKET', [cte.table[:count_grouped], buckets.first, buckets.last, bucket_segments])
|
||||
.as('buckets')
|
||||
|
||||
query = cte
|
||||
.table
|
||||
.project(width_bucket, cte.table[:count])
|
||||
.group('buckets')
|
||||
.order('buckets')
|
||||
.with(cte.to_arel)
|
||||
|
||||
query.to_sql
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# For estimated distinct count use exact query instead of hll
|
||||
# buckets query, because it can't be used to obtain estimations without
|
||||
# supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter
|
||||
|
@ -36,10 +57,21 @@ module Gitlab
|
|||
'SELECT ' + args.map {|arg| "(#{arg})" }.join(' + ')
|
||||
end
|
||||
|
||||
def maximum_id(model)
|
||||
def maximum_id(model, column = nil)
|
||||
end
|
||||
|
||||
def minimum_id(model)
|
||||
def minimum_id(model, column = nil)
|
||||
end
|
||||
|
||||
def jira_service_data
|
||||
{
|
||||
projects_jira_server_active: 0,
|
||||
projects_jira_cloud_active: 0
|
||||
}
|
||||
end
|
||||
|
||||
def epics_deepest_relationship_level
|
||||
{ epics_deepest_relationship_level: 0 }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -210,18 +210,52 @@ module Gitlab
|
|||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name.to_s, values: values)
|
||||
end
|
||||
|
||||
def maximum_id(model)
|
||||
key = :"#{model.name.downcase}_maximum_id"
|
||||
def maximum_id(model, column = nil)
|
||||
key = :"#{model.name.downcase.gsub('::', '_')}_maximum_id"
|
||||
column_to_read = column || :id
|
||||
|
||||
strong_memoize(key) do
|
||||
model.maximum(:id)
|
||||
model.maximum(column_to_read)
|
||||
end
|
||||
end
|
||||
|
||||
def minimum_id(model)
|
||||
key = :"#{model.name.downcase}_minimum_id"
|
||||
strong_memoize(key) do
|
||||
model.minimum(:id)
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
def jira_service_data
|
||||
data = {
|
||||
projects_jira_server_active: 0,
|
||||
projects_jira_cloud_active: 0
|
||||
}
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services|
|
||||
counts = services.group_by do |service|
|
||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
|
||||
service_url&.include?('.atlassian.net') ? :cloud : :server
|
||||
end
|
||||
|
||||
data[:projects_jira_server_active] += counts[:server].size if counts[:server]
|
||||
data[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
|
||||
def minimum_id(model, column = nil)
|
||||
key = :"#{model.name.downcase.gsub('::', '_')}_minimum_id"
|
||||
column_to_read = column || :id
|
||||
|
||||
strong_memoize(key) do
|
||||
model.minimum(column_to_read)
|
||||
end
|
||||
end
|
||||
|
||||
def epics_deepest_relationship_level
|
||||
# rubocop: disable UsageData/LargeTable
|
||||
{ epics_deepest_relationship_level: ::Epic.deepest_relationship_level.to_i }
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -416,9 +416,6 @@ msgstr ""
|
|||
msgid "%{board_target} not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{codeStart}needs:%{codeEnd} relationships"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
|
||||
msgstr ""
|
||||
|
||||
|
@ -1649,6 +1646,12 @@ msgstr ""
|
|||
msgid "Abuse reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Abuse reports notification email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area."
|
||||
msgstr ""
|
||||
|
||||
msgid "Accept invitation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8305,6 +8308,9 @@ msgstr ""
|
|||
msgid "Configure repository mirroring."
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure specific limits for Packages API requests that supersede the general user and IP rate limits."
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure storage path settings."
|
||||
msgstr ""
|
||||
|
||||
|
@ -8338,19 +8344,19 @@ msgstr ""
|
|||
msgid "Confluence"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confluence Cloud Workspace URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "ConfluenceService|Confluence Workspace"
|
||||
msgstr ""
|
||||
|
||||
msgid "ConfluenceService|Connect a Confluence Cloud Workspace to GitLab"
|
||||
msgid "ConfluenceService|Link to a Confluence Workspace from the sidebar."
|
||||
msgstr ""
|
||||
|
||||
msgid "ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration"
|
||||
msgid "ConfluenceService|Link to a Confluence Workspace from the sidebar. Enabling this integration replaces the \"Wiki\" sidebar link with a link to the Confluence Workspace. The GitLab wiki is still available at the original URL."
|
||||
msgstr ""
|
||||
|
||||
msgid "ConfluenceService|The URL of the Confluence Workspace"
|
||||
msgstr ""
|
||||
|
||||
msgid "ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration"
|
||||
msgid "ConfluenceService|Your GitLab wiki is still available at %{wiki_link}. To re-enable the link to the GitLab wiki, disable this integration."
|
||||
msgstr ""
|
||||
|
||||
msgid "Congratulations, your free trial is activated."
|
||||
|
@ -12041,6 +12047,9 @@ msgstr ""
|
|||
msgid "Enable two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable unauthenticated API request rate limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable unauthenticated request rate limit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15217,6 +15226,9 @@ msgstr ""
|
|||
msgid "Group is required when cluster_type is :group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group jobs by"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group maintainers can register group runners in the %{link}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18199,6 +18211,9 @@ msgstr ""
|
|||
msgid "Job artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job dependencies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job has been erased"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21655,6 +21670,9 @@ msgstr ""
|
|||
msgid "No prioritized labels with such name or description"
|
||||
msgstr ""
|
||||
|
||||
msgid "No profiles found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No projects found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22502,9 +22520,6 @@ msgstr ""
|
|||
msgid "Or you can choose one of the suggested colors below"
|
||||
msgstr ""
|
||||
|
||||
msgid "Order jobs by"
|
||||
msgstr ""
|
||||
|
||||
msgid "Orphaned member"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22574,6 +22589,9 @@ msgstr ""
|
|||
msgid "Package Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package Registry Rate Limits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package already exists"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22985,6 +23003,9 @@ msgstr ""
|
|||
msgid "Pass job variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Passed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29048,6 +29069,9 @@ msgstr ""
|
|||
msgid "Show complete raw log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show dependencies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show details"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33582,6 +33606,9 @@ msgstr ""
|
|||
msgid "Unassigned"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unauthenticated API request rate limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unauthenticated rate limit period in seconds"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34816,9 +34843,6 @@ msgstr ""
|
|||
msgid "View the documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "View the jobs grouped into stages"
|
||||
msgstr ""
|
||||
|
||||
msgid "View the latest successful deployment to this environment"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34831,9 +34855,6 @@ msgstr ""
|
|||
msgid "View users statistics"
|
||||
msgstr ""
|
||||
|
||||
msgid "View what jobs are needed for a job to run"
|
||||
msgstr ""
|
||||
|
||||
msgid "Viewed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('graph component', () => {
|
|||
|
||||
const defaultProps = {
|
||||
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
|
||||
showLinks: false,
|
||||
viewType: STAGE_VIEW,
|
||||
configPaths: {
|
||||
metricsPath: '',
|
||||
|
|
|
@ -15,6 +15,7 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
|||
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
|
||||
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import * as parsingUtils from '~/pipelines/components/parsing_utils';
|
||||
import { mockPipelineResponse } from './mock_data';
|
||||
|
||||
|
@ -31,7 +32,9 @@ describe('Pipeline graph wrapper', () => {
|
|||
|
||||
let wrapper;
|
||||
const getAlert = () => wrapper.find(GlAlert);
|
||||
const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
|
||||
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const getLinksLayer = () => wrapper.findComponent(LinksLayer);
|
||||
const getGraph = () => wrapper.find(PipelineGraph);
|
||||
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
|
||||
const getAllStageColumnGroupsInColumn = () =>
|
||||
|
@ -59,6 +62,7 @@ describe('Pipeline graph wrapper', () => {
|
|||
};
|
||||
|
||||
const createComponentWithApollo = ({
|
||||
data = {},
|
||||
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
|
||||
mountFn = shallowMount,
|
||||
provide = {},
|
||||
|
@ -66,7 +70,7 @@ describe('Pipeline graph wrapper', () => {
|
|||
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
|
||||
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
createComponent({ apolloProvider, provide, mountFn });
|
||||
createComponent({ apolloProvider, data, provide, mountFn });
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -74,6 +78,15 @@ describe('Pipeline graph wrapper', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('when data is loading', () => {
|
||||
it('displays the loading icon', () => {
|
||||
createComponentWithApollo();
|
||||
|
@ -282,6 +295,36 @@ describe('Pipeline graph wrapper', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when pipelineGraphLayersView feature flag is on and layers view is selected', () => {
|
||||
beforeEach(async () => {
|
||||
createComponentWithApollo({
|
||||
provide: {
|
||||
glFeatures: {
|
||||
pipelineGraphLayersView: true,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
currentViewType: LAYER_VIEW,
|
||||
},
|
||||
mountFn: mount,
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('sets showLinks to true', async () => {
|
||||
/* This spec uses .props for performance reasons. */
|
||||
expect(getLinksLayer().exists()).toBe(true);
|
||||
expect(getLinksLayer().props('showLinks')).toBe(false);
|
||||
expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
|
||||
await getDependenciesToggle().trigger('click');
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature flag is on and local storage is set', () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
|
||||
|
@ -299,10 +342,15 @@ describe('Pipeline graph wrapper', () => {
|
|||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('reads the view type from localStorage when available', () => {
|
||||
expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain(
|
||||
'needs:',
|
||||
);
|
||||
const viewSelectorNeedsSegment = wrapper
|
||||
.findAll('[data-testid="pipeline-view-selector"] > label')
|
||||
.at(1);
|
||||
expect(viewSelectorNeedsSegment.classes()).toContain('active');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import { GlLoadingIcon, GlSegmentedControl } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
|
||||
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
|
||||
|
||||
describe('the graph view selector component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
|
||||
const findViewTypeSelector = () => wrapper.findComponent(GlSegmentedControl);
|
||||
const findStageViewLabel = () => findViewTypeSelector().findAll('label').at(0);
|
||||
const findLayersViewLabel = () => findViewTypeSelector().findAll('label').at(1);
|
||||
const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
|
||||
const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
|
||||
|
||||
const defaultProps = {
|
||||
showLinks: false,
|
||||
type: STAGE_VIEW,
|
||||
};
|
||||
|
||||
const defaultData = {
|
||||
showLinksActive: false,
|
||||
isToggleLoading: false,
|
||||
isSwitcherLoading: false,
|
||||
};
|
||||
|
||||
const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
|
||||
wrapper = mountFn(GraphViewSelector, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
...defaultData,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when showing stage view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ mountFn: mount });
|
||||
});
|
||||
|
||||
it('shows the Stage view label as active in the selector', () => {
|
||||
expect(findStageViewLabel().classes()).toContain('active');
|
||||
});
|
||||
|
||||
it('does not show the Job dependencies (links) toggle', () => {
|
||||
expect(findDependenciesToggle().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showing Job dependencies view', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mount,
|
||||
props: {
|
||||
type: LAYER_VIEW,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the Job dependencies view label as active in the selector', () => {
|
||||
expect(findLayersViewLabel().classes()).toContain('active');
|
||||
});
|
||||
|
||||
it('shows the Job dependencies (links) toggle', () => {
|
||||
expect(findDependenciesToggle().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
createComponent({
|
||||
mountFn: mount,
|
||||
props: {
|
||||
type: LAYER_VIEW,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state and emits updateViewType when view type toggled', async () => {
|
||||
expect(wrapper.emitted().updateViewType).toBeUndefined();
|
||||
expect(findSwitcherLoader().exists()).toBe(false);
|
||||
|
||||
await findStageViewLabel().trigger('click');
|
||||
/*
|
||||
Loading happens before the event is emitted or timers are run.
|
||||
Then we run the timer because the event is emitted in setInterval
|
||||
which is what gives the loader a chace to show up.
|
||||
*/
|
||||
expect(findSwitcherLoader().exists()).toBe(true);
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(wrapper.emitted().updateViewType).toHaveLength(1);
|
||||
expect(wrapper.emitted().updateViewType).toEqual([[STAGE_VIEW]]);
|
||||
});
|
||||
|
||||
it('shows loading state and emits updateShowLinks when show links toggle is clicked', async () => {
|
||||
expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
|
||||
expect(findToggleLoader().exists()).toBe(false);
|
||||
|
||||
await findDependenciesToggle().trigger('click');
|
||||
/*
|
||||
Loading happens before the event is emitted or timers are run.
|
||||
Then we run the timer because the event is emitted in setInterval
|
||||
which is what gives the loader a chace to show up.
|
||||
*/
|
||||
expect(findToggleLoader().exists()).toBe(true);
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(wrapper.emitted().updateShowLinksState).toHaveLength(1);
|
||||
expect(wrapper.emitted().updateShowLinksState).toEqual([[true]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => {
|
|||
const defaultProps = {
|
||||
columnTitle: 'Downstream',
|
||||
linkedPipelines: processedPipeline.downstream,
|
||||
showLinks: false,
|
||||
type: DOWNSTREAM,
|
||||
viewType: STAGE_VIEW,
|
||||
configPaths: {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import { fireEvent, within } from '@testing-library/dom';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
|
||||
|
@ -8,25 +6,18 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
|
|||
describe('links layer component', () => {
|
||||
let wrapper;
|
||||
|
||||
const withinComponent = () => within(wrapper.element);
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findShowAnyways = () =>
|
||||
withinComponent().getByText(wrapper.vm.$options.i18n.showLinksAnyways);
|
||||
const findLinksInner = () => wrapper.find(LinksInner);
|
||||
|
||||
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
|
||||
const containerId = `pipeline-links-container-${pipeline.id}`;
|
||||
const slotContent = "<div>Ceci n'est pas un graphique</div>";
|
||||
|
||||
const tooManyStages = Array(101)
|
||||
.fill(0)
|
||||
.flatMap(() => pipeline.stages);
|
||||
|
||||
const defaultProps = {
|
||||
containerId,
|
||||
containerMeasurements: { width: 400, height: 400 },
|
||||
pipelineId: pipeline.id,
|
||||
pipelineData: pipeline.stages,
|
||||
showLinks: false,
|
||||
};
|
||||
|
||||
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
|
||||
|
@ -49,7 +40,7 @@ describe('links layer component', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('with data under max stages', () => {
|
||||
describe('with show links off', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
@ -58,63 +49,40 @@ describe('links layer component', () => {
|
|||
expect(wrapper.html()).toContain(slotContent);
|
||||
});
|
||||
|
||||
it('does not render inner links component', () => {
|
||||
expect(findLinksInner().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with show links on', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
showLinks: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the default slot', () => {
|
||||
expect(wrapper.html()).toContain(slotContent);
|
||||
});
|
||||
|
||||
it('renders the inner links component', () => {
|
||||
expect(findLinksInner().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with more than the max number of stages', () => {
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { pipelineData: tooManyStages } });
|
||||
});
|
||||
|
||||
it('renders the default slot', () => {
|
||||
expect(wrapper.html()).toContain(slotContent);
|
||||
});
|
||||
|
||||
it('renders the alert component', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render the inner links component', () => {
|
||||
expect(findLinksInner().exists()).toBe(false);
|
||||
});
|
||||
describe('with width or height measurement at 0', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
|
||||
});
|
||||
|
||||
describe('with width or height measurement at 0', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
|
||||
});
|
||||
|
||||
it('renders the default slot', () => {
|
||||
expect(wrapper.html()).toContain(slotContent);
|
||||
});
|
||||
|
||||
it('does not render the alert component', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the inner links component', () => {
|
||||
expect(findLinksInner().exists()).toBe(false);
|
||||
});
|
||||
it('renders the default slot', () => {
|
||||
expect(wrapper.html()).toContain(slotContent);
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } });
|
||||
});
|
||||
|
||||
it('renders the disable button', () => {
|
||||
expect(findShowAnyways()).not.toBe(null);
|
||||
});
|
||||
|
||||
it('shows links when override is clicked', async () => {
|
||||
expect(findLinksInner().exists()).toBe(false);
|
||||
fireEvent(findShowAnyways(), new MouseEvent('click', { bubbles: true }));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findLinksInner().exists()).toBe(true);
|
||||
});
|
||||
it('does not render the inner links component', () => {
|
||||
expect(findLinksInner().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,8 +32,8 @@ describe('Commit component', () => {
|
|||
createComponent({
|
||||
tag: false,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -55,8 +55,8 @@ describe('Commit component', () => {
|
|||
props = {
|
||||
tag: true,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -122,8 +122,8 @@ describe('Commit component', () => {
|
|||
props = {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -145,8 +145,8 @@ describe('Commit component', () => {
|
|||
props = {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -158,7 +158,7 @@ describe('Commit component', () => {
|
|||
createComponent(props);
|
||||
const refEl = wrapper.find('.ref-name');
|
||||
|
||||
expect(refEl.text()).toContain('master');
|
||||
expect(refEl.text()).toContain('main');
|
||||
|
||||
expect(refEl.attributes('href')).toBe(props.commitRef.ref_url);
|
||||
|
||||
|
@ -173,8 +173,8 @@ describe('Commit component', () => {
|
|||
props = {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -206,8 +206,8 @@ describe('Commit component', () => {
|
|||
props = {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
commitUrl:
|
||||
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
|
@ -232,8 +232,8 @@ describe('Commit component', () => {
|
|||
it('should render path as href attribute', () => {
|
||||
props = {
|
||||
commitRef: {
|
||||
name: 'master',
|
||||
path: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
name: 'main',
|
||||
path: 'http://localhost/namespace2/gitlabhq/tree/main',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export const mockAuthor3 = {
|
|||
|
||||
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
|
||||
|
||||
export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }];
|
||||
export const mockBranches = [{ name: 'Main' }, { name: 'v1.x' }, { name: 'my-Branch' }];
|
||||
|
||||
export const mockRegularMilestone = {
|
||||
id: 1,
|
||||
|
|
|
@ -77,7 +77,7 @@ describe('BranchToken', () => {
|
|||
|
||||
describe('currentValue', () => {
|
||||
it('returns lowercase string for `value.data`', () => {
|
||||
expect(wrapper.vm.currentValue).toBe('master');
|
||||
expect(wrapper.vm.currentValue).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,54 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Lists::Update do
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:board) { create(:board, group: group) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:list) { create(:list, board: board, position: 0) }
|
||||
let_it_be(:list2) { create(:list, board: board) }
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
|
||||
let(:list_update_params) { { position: 1, collapsed: true } }
|
||||
context 'on group issue boards' do
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:board) { create(:board, group: group) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:list) { create(:list, board: board, position: 0) }
|
||||
let_it_be(:list2) { create(:list, board: board) }
|
||||
|
||||
before_all do
|
||||
group.add_reporter(reporter)
|
||||
group.add_guest(guest)
|
||||
list.update_preferences_for(reporter, collapsed: false)
|
||||
end
|
||||
|
||||
subject { mutation.resolve(list: list, **list_update_params) }
|
||||
|
||||
describe '#resolve' do
|
||||
context 'with permission to admin board lists' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it 'updates the list position and collapsed state as expected' do
|
||||
subject
|
||||
|
||||
reloaded_list = list.reload
|
||||
expect(reloaded_list.position).to eq(1)
|
||||
expect(reloaded_list.collapsed?(current_user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with permission to read board lists' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it 'updates the list collapsed state but not the list position' do
|
||||
subject
|
||||
|
||||
reloaded_list = list.reload
|
||||
expect(reloaded_list.position).to eq(0)
|
||||
expect(reloaded_list.collapsed?(current_user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without permission to read board lists' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it 'raises Resource Not Found error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
it_behaves_like 'update board list mutation'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Helpers do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
subject { Class.new.include(described_class).new }
|
||||
|
||||
describe '#find_project' do
|
||||
|
@ -99,6 +101,59 @@ RSpec.describe API::Helpers do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#find_project!' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let(:user) { project.owner}
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:current_user).and_return(user)
|
||||
allow(subject).to receive(:authorized_project_scope?).and_return(true)
|
||||
allow(subject).to receive(:job_token_authentication?).and_return(false)
|
||||
allow(subject).to receive(:authenticate_non_public?).and_return(false)
|
||||
end
|
||||
|
||||
shared_examples 'project finder' do
|
||||
context 'when project exists' do
|
||||
it 'returns requested project' do
|
||||
expect(subject.find_project!(existing_id)).to eq(project)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404)
|
||||
expect(subject.find_project!(non_existing_id)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ID is used as an argument' do
|
||||
let(:existing_id) { project.id }
|
||||
let(:non_existing_id) { non_existing_record_id }
|
||||
|
||||
it_behaves_like 'project finder'
|
||||
end
|
||||
|
||||
context 'when PATH is used as an argument' do
|
||||
let(:existing_id) { project.full_path }
|
||||
let(:non_existing_id) { 'something/else' }
|
||||
|
||||
it_behaves_like 'project finder'
|
||||
|
||||
context 'with an invalid PATH' do
|
||||
let(:non_existing_id) { 'undefined' } # path without slash
|
||||
|
||||
it_behaves_like 'project finder'
|
||||
|
||||
it 'does not hit the database' do
|
||||
expect(Project).not_to receive(:find_by_full_path)
|
||||
expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404)
|
||||
|
||||
subject.find_project!(non_existing_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_namespace' do
|
||||
let(:namespace) { create(:namespace) }
|
||||
|
||||
|
@ -191,6 +246,49 @@ RSpec.describe API::Helpers do
|
|||
it_behaves_like 'user namespace finder'
|
||||
end
|
||||
|
||||
describe '#authorized_project_scope?' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:other_project) { create(:project) }
|
||||
let_it_be(:job) { create(:ci_build) }
|
||||
|
||||
let(:send_authorized_project_scope) { subject.authorized_project_scope?(project) }
|
||||
|
||||
where(:job_token_authentication, :route_setting, :feature_flag, :same_job_project, :expected_result) do
|
||||
false | false | false | false | true
|
||||
false | false | false | true | true
|
||||
false | false | true | false | true
|
||||
false | false | true | true | true
|
||||
false | true | false | false | true
|
||||
false | true | false | true | true
|
||||
false | true | true | false | true
|
||||
false | true | true | true | true
|
||||
true | false | false | false | true
|
||||
true | false | false | true | true
|
||||
true | false | true | false | true
|
||||
true | false | true | true | true
|
||||
true | true | false | false | false
|
||||
true | true | false | true | false
|
||||
true | true | true | false | false
|
||||
true | true | true | true | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(subject).to receive(:job_token_authentication?).and_return(job_token_authentication)
|
||||
allow(subject).to receive(:route_authentication_setting).and_return(job_token_scope: route_setting ? :project : nil)
|
||||
allow(subject).to receive(:current_authenticated_job).and_return(job)
|
||||
allow(job).to receive(:project).and_return(same_job_project ? project : other_project)
|
||||
|
||||
stub_feature_flags(ci_job_token_scope: false)
|
||||
stub_feature_flags(ci_job_token_scope: project) if feature_flag
|
||||
end
|
||||
|
||||
it 'returns the expected result' do
|
||||
expect(send_authorized_project_scope).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_git_blob' do
|
||||
let(:repository) { double }
|
||||
let(:blob) { double(name: 'foobar') }
|
||||
|
|
|
@ -58,22 +58,6 @@ RSpec.describe Gitlab::Git::Wiki do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#delete_page' do
|
||||
after do
|
||||
destroy_page('page1')
|
||||
end
|
||||
|
||||
it 'only removes the page with the same path' do
|
||||
create_page('page1', 'content')
|
||||
create_page('*', 'content')
|
||||
|
||||
subject.delete_page('*', commit_details('whatever'))
|
||||
|
||||
expect(subject.list_pages.count).to eq 1
|
||||
expect(subject.list_pages.first.title).to eq 'page1'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#preview_slug' do
|
||||
where(:title, :format, :expected_slug) do
|
||||
'The Best Thing' | :markdown | 'The-Best-Thing'
|
||||
|
|
|
@ -726,4 +726,134 @@ RSpec.describe Gitlab::Regex do
|
|||
it { is_expected.not_to match('v../../../../../1.2.3') }
|
||||
it { is_expected.not_to match('v%2e%2e%2f1.2.3') }
|
||||
end
|
||||
|
||||
describe 'Packages::API_PATH_REGEX' do
|
||||
subject { described_class::Packages::API_PATH_REGEX }
|
||||
|
||||
it { is_expected.to match('/api/v4/group/12345/-/packages/composer/p/123456789') }
|
||||
it { is_expected.to match('/api/v4/group/12345/-/packages/composer/p2/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/group/12345/-/packages/composer/packages') }
|
||||
it { is_expected.to match('/api/v4/group/12345/-/packages/composer/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/groups/1234/-/packages/maven/a/path/file.jar') }
|
||||
it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/index') }
|
||||
it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/metadata/pkg_name/1.3.4') }
|
||||
it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/metadata/pkg_name/index') }
|
||||
it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/query') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/digest') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/download_urls') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/digest') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/download_urls') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/upload_urls') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/upload_urls') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/conans/search') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name/authorize') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name/authorize') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/ping') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/users/authenticate') }
|
||||
it { is_expected.to match('/api/v4/packages/conan/v1/users/check_credentials') }
|
||||
it { is_expected.to match('/api/v4/packages/maven/a/path/file.jar') }
|
||||
it { is_expected.to match('/api/v4/packages/npm/-/package/pkg_name/dist-tags') }
|
||||
it { is_expected.to match('/api/v4/packages/npm/-/package/pkg_name/dist-tags/tag') }
|
||||
it { is_expected.to match('/api/v4/packages/npm/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/composer') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/composer/archives/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/digest') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/download_urls') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/digest') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/download_urls') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/upload_urls') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/upload_urls') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/search') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/ping') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/users/authenticate') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/users/check_credentials') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/compon/binary-x64/Packages') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/InRelease') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/Release') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/Release.gpg') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/file.name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/file.name/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/debian/pool/compon/e/pkg/file.name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/generic/pkg_name/1.3.4/myfile.txt') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/generic/pkg_name/1.3.4/myfile.txt/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.info') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.mod') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.zip') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/list') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/maven/a/path/file.jar') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/maven/a/path/file.jar/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/npm/-/package/pkg_name/dist-tags') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/npm/-/package/pkg_name/dist-tags/tag') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/npm/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/npm/pkg_name/-/tarball.tgz') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/download/pkg_name/1.3.4/pkg.npkg') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/download/pkg_name/index') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/index') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/metadata/pkg_name/1.3.4') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/metadata/pkg_name/index') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/nuget/query') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/pypi') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/pypi/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/pypi/files/1234567890/file.identifier') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/pypi/simple/pkg_name') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/dependencies') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/gems') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/gems/authorize') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/gems/pkg') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/pkg') }
|
||||
it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/quick/Marshal.4.8/pkg') }
|
||||
it { is_expected.not_to match('') }
|
||||
it { is_expected.not_to match('foo') }
|
||||
it { is_expected.not_to match('/api/v4') }
|
||||
it { is_expected.not_to match('/api/v4/version') }
|
||||
it { is_expected.not_to match('/api/v4/packages') }
|
||||
it { is_expected.not_to match('/api/v4/packages/') }
|
||||
it { is_expected.not_to match('/api/v4/group') }
|
||||
it { is_expected.not_to match('/api/v4/group/12345') }
|
||||
it { is_expected.not_to match('/api/v4/group/12345/-') }
|
||||
it { is_expected.not_to match('/api/v4/group/12345/-/packages') }
|
||||
it { is_expected.not_to match('/api/v4/group/12345/-/packages/') }
|
||||
it { is_expected.not_to match('/api/v4/group/12345/-/packages/50') }
|
||||
it { is_expected.not_to match('/api/v4/groups') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/-') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/-/packages') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/-/packages/') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/-/packages/50') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/packages') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/packages/') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/badges') }
|
||||
it { is_expected.not_to match('/api/v4/groups/12345/issues') }
|
||||
it { is_expected.not_to match('/api/v4/projects') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/packages') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/packages/') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/packages/50') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/packages/50/package_files') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/merge_requests') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/registry/repositories') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/issues') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/members') }
|
||||
it { is_expected.not_to match('/api/v4/projects/1234/milestones') }
|
||||
|
||||
# Group level Debian API endpoints are not matched as it's not using the correct prefix (groups/:id/-/packages/)
|
||||
# TODO: Update Debian group level endpoints urls and adjust this specs: https://gitlab.com/gitlab-org/gitlab/-/issues/326805
|
||||
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/compon/binary-compo/Packages') }
|
||||
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/InRelease') }
|
||||
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/Release') }
|
||||
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/Release.gpg') }
|
||||
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/pool/compon/a/pkg/file.name') }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,6 +59,14 @@ RSpec.describe Gitlab::UsageDataQueries do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.histogram' do
|
||||
it 'returns the histogram sql' do
|
||||
expect(described_class.histogram(AlertManagement::HttpIntegration.active,
|
||||
:project_id, buckets: 1..2, bucket_size: 101))
|
||||
.to eq('WITH "count_cte" AS (SELECT COUNT(*) AS count_grouped FROM "alert_management_http_integrations" WHERE "alert_management_http_integrations"."active" = TRUE GROUP BY "alert_management_http_integrations"."project_id") SELECT WIDTH_BUCKET("count_cte"."count_grouped", 1, 2, 100) AS buckets, "count_cte"."count" FROM "count_cte" GROUP BY buckets ORDER BY buckets')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'min/max methods' do
|
||||
it 'returns nil' do
|
||||
# user min/max
|
||||
|
|
|
@ -785,6 +785,10 @@ RSpec.describe ApplicationSetting do
|
|||
throttle_authenticated_api_period_in_seconds
|
||||
throttle_authenticated_web_requests_per_period
|
||||
throttle_authenticated_web_period_in_seconds
|
||||
throttle_unauthenticated_packages_api_requests_per_period
|
||||
throttle_unauthenticated_packages_api_period_in_seconds
|
||||
throttle_authenticated_packages_api_requests_per_period
|
||||
throttle_authenticated_packages_api_period_in_seconds
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -6,32 +6,32 @@ RSpec.describe API::DebianGroupPackages do
|
|||
include WorkhorseHelpers
|
||||
|
||||
include_context 'Debian repository shared context', :group do
|
||||
describe 'GET groups/:id/packages/debian/dists/*distribution/Release.gpg' do
|
||||
let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release.gpg" }
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/packages/debian/dists/*distribution/Release' do
|
||||
let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release" }
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/packages/debian/dists/*distribution/InRelease' do
|
||||
let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/InRelease" }
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
|
||||
let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
|
||||
let(:url) { "/groups/#{group.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
|
||||
describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
|
||||
end
|
||||
|
|
|
@ -14,43 +14,5 @@ RSpec.describe 'Update of an existing board list' do
|
|||
let(:mutation) { graphql_mutation(:update_board_list, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(:update_board_list) }
|
||||
|
||||
context 'the user is not allowed to read board lists' do
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
before do
|
||||
list.update_preferences_for(current_user, collapsed: false)
|
||||
end
|
||||
|
||||
context 'when user has permissions to admin board lists' do
|
||||
before do
|
||||
group.add_reporter(current_user)
|
||||
end
|
||||
|
||||
it 'updates the list position and collapsed state' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list']).to include(
|
||||
'position' => 1,
|
||||
'collapsed' => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has permissions to read board lists' do
|
||||
before do
|
||||
group.add_guest(current_user)
|
||||
end
|
||||
|
||||
it 'updates the list collapsed state but not the list position' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list']).to include(
|
||||
'position' => 0,
|
||||
'collapsed' => true
|
||||
)
|
||||
end
|
||||
end
|
||||
it_behaves_like 'a GraphQL request to update board list'
|
||||
end
|
||||
|
|
|
@ -6,12 +6,14 @@ RSpec.describe API::ProjectContainerRepositories do
|
|||
include ExclusiveLeaseHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:project2) { create(:project, :public) }
|
||||
let_it_be(:maintainer) { create(:user) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let(:root_repository) { create(:container_repository, :root, project: project) }
|
||||
let(:test_repository) { create(:container_repository, project: project) }
|
||||
let(:root_repository2) { create(:container_repository, :root, project: project2) }
|
||||
|
||||
let(:users) do
|
||||
{
|
||||
|
@ -24,315 +26,408 @@ RSpec.describe API::ProjectContainerRepositories do
|
|||
end
|
||||
|
||||
let(:api_user) { maintainer }
|
||||
let(:job) { create(:ci_build, :running, user: api_user, project: project) }
|
||||
let(:job2) { create(:ci_build, :running, user: api_user, project: project2) }
|
||||
|
||||
before do
|
||||
let(:method) { :get }
|
||||
let(:params) { {} }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(maintainer)
|
||||
project.add_developer(developer)
|
||||
project.add_reporter(reporter)
|
||||
project.add_guest(guest)
|
||||
|
||||
stub_container_registry_config(enabled: true)
|
||||
project2.add_maintainer(maintainer)
|
||||
project2.add_developer(developer)
|
||||
project2.add_reporter(reporter)
|
||||
project2.add_guest(guest)
|
||||
end
|
||||
|
||||
before do
|
||||
root_repository
|
||||
test_repository
|
||||
|
||||
stub_container_registry_config(enabled: true)
|
||||
end
|
||||
|
||||
shared_context 'using API user' do
|
||||
subject { public_send(method, api(url, api_user), params: params) }
|
||||
end
|
||||
|
||||
shared_context 'using job token' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: true)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
|
||||
end
|
||||
|
||||
shared_context 'using job token from another project' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: true)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: { job_token: job2.token }) }
|
||||
end
|
||||
|
||||
shared_context 'using job token while ci_job_token_scope feature flag is disabled' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: false)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
|
||||
end
|
||||
|
||||
shared_examples 'rejected job token scopes' do
|
||||
include_context 'using job token from another project' do
|
||||
it_behaves_like 'rejected container repository access', :maintainer, :forbidden
|
||||
end
|
||||
|
||||
include_context 'using job token while ci_job_token_scope feature flag is disabled' do
|
||||
it_behaves_like 'rejected container repository access', :maintainer, :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/registry/repositories' do
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories" }
|
||||
|
||||
subject { get api(url, api_user) }
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
|
||||
|
||||
it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
|
||||
let(:object) { project }
|
||||
it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
|
||||
let(:object) { project }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
|
||||
subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}", api_user) }
|
||||
let(:method) { :delete }
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}" }
|
||||
|
||||
it_behaves_like 'rejected container repository access', :developer, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
context 'for maintainer' do
|
||||
let(:api_user) { maintainer }
|
||||
it_behaves_like 'rejected container repository access', :developer, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
|
||||
|
||||
it 'schedules removal of repository' do
|
||||
expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id)
|
||||
context 'for maintainer' do
|
||||
let(:api_user) { maintainer }
|
||||
|
||||
subject
|
||||
it 'schedules removal of repository' do
|
||||
expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
|
||||
subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user) }
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
|
||||
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
context 'for reporter' do
|
||||
let(:api_user) { reporter }
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
|
||||
end
|
||||
context 'for reporter' do
|
||||
let(:api_user) { reporter }
|
||||
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
|
||||
end
|
||||
|
||||
it 'returns a list of tags' do
|
||||
subject
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
|
||||
|
||||
expect(json_response.length).to eq(2)
|
||||
expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
|
||||
end
|
||||
it 'returns a list of tags' do
|
||||
subject
|
||||
|
||||
it 'returns a matching schema' do
|
||||
subject
|
||||
expect(json_response.length).to eq(2)
|
||||
expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
|
||||
end
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('registry/tags')
|
||||
it 'returns a matching schema' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('registry/tags')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
|
||||
subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user), params: params }
|
||||
let(:method) { :delete }
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
|
||||
|
||||
context 'disallowed' do
|
||||
let(:params) do
|
||||
{ name_regex_delete: 'v10.*' }
|
||||
end
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
it_behaves_like 'rejected container repository access', :developer, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
|
||||
end
|
||||
|
||||
context 'for maintainer' do
|
||||
let(:api_user) { maintainer }
|
||||
|
||||
context 'without required parameters' do
|
||||
let(:params) { }
|
||||
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without name_regex' do
|
||||
let(:params) do
|
||||
{ keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
end
|
||||
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'passes all declared parameters' do
|
||||
let(:params) do
|
||||
{ name_regex_delete: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
end
|
||||
|
||||
let(:worker_params) do
|
||||
{ name_regex: nil,
|
||||
name_regex_delete: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
container_expiration_policy: false }
|
||||
end
|
||||
|
||||
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
|
||||
|
||||
it 'schedules cleanup of tags repository' do
|
||||
stub_last_activity_update
|
||||
stub_exclusive_lease(lease_key, timeout: 1.hour)
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id, worker_params)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
end
|
||||
|
||||
context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
|
||||
it 'returns 400 with an error message' do
|
||||
stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response.body).to include('This request has already been made.')
|
||||
context 'disallowed' do
|
||||
let(:params) do
|
||||
{ name_regex_delete: 'v10.*' }
|
||||
end
|
||||
|
||||
it 'executes service only for the first time' do
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
|
||||
|
||||
2.times { subject }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deprecated name_regex param' do
|
||||
let(:params) do
|
||||
{ name_regex: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
it_behaves_like 'rejected container repository access', :developer, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
|
||||
end
|
||||
|
||||
let(:worker_params) do
|
||||
{ name_regex: 'v10.*',
|
||||
name_regex_delete: nil,
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
container_expiration_policy: false }
|
||||
end
|
||||
context 'for maintainer' do
|
||||
let(:api_user) { maintainer }
|
||||
|
||||
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
|
||||
context 'without required parameters' do
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
it 'schedules cleanup of tags repository' do
|
||||
stub_last_activity_update
|
||||
stub_exclusive_lease(lease_key, timeout: 1.hour)
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id, worker_params)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid regex' do
|
||||
let(:invalid_regex) { '*v10.' }
|
||||
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
|
||||
|
||||
RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
|
||||
it 'does not enqueue a job' do
|
||||
expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :bad_request
|
||||
context 'without name_regex' do
|
||||
let(:params) do
|
||||
{ keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
subject
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
expect(json_response['error']).to include("#{param_name} is an invalid regexp")
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_last_activity_update
|
||||
stub_exclusive_lease(lease_key, timeout: 1.hour)
|
||||
end
|
||||
context 'passes all declared parameters' do
|
||||
let(:params) do
|
||||
{ name_regex_delete: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
end
|
||||
|
||||
%i[name_regex_delete name_regex name_regex_keep].each do |param_name|
|
||||
context "for #{param_name}" do
|
||||
let(:params) { { param_name => invalid_regex } }
|
||||
let(:worker_params) do
|
||||
{ name_regex: nil,
|
||||
name_regex_delete: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
container_expiration_policy: false }
|
||||
end
|
||||
|
||||
it_behaves_like 'rejecting the invalid regex', param_name
|
||||
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
|
||||
|
||||
it 'schedules cleanup of tags repository' do
|
||||
stub_last_activity_update
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id, worker_params)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
end
|
||||
|
||||
context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
|
||||
it 'returns 400 with an error message' do
|
||||
stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response.body).to include('This request has already been made.')
|
||||
end
|
||||
|
||||
it 'executes service only for the first time' do
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
|
||||
|
||||
2.times { subject }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deprecated name_regex param' do
|
||||
let(:params) do
|
||||
{ name_regex: 'v10.*',
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
other: 'some value' }
|
||||
end
|
||||
|
||||
let(:worker_params) do
|
||||
{ name_regex: 'v10.*',
|
||||
name_regex_delete: nil,
|
||||
name_regex_keep: 'v10.1.*',
|
||||
keep_n: 100,
|
||||
older_than: '1 day',
|
||||
container_expiration_policy: false }
|
||||
end
|
||||
|
||||
it 'schedules cleanup of tags repository' do
|
||||
stub_last_activity_update
|
||||
expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
|
||||
.with(maintainer.id, root_repository.id, worker_params)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid regex' do
|
||||
let(:invalid_regex) { '*v10.' }
|
||||
|
||||
RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
|
||||
it 'does not enqueue a job' do
|
||||
expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :bad_request
|
||||
|
||||
it 'returns an error message' do
|
||||
subject
|
||||
|
||||
expect(json_response['error']).to include("#{param_name} is an invalid regexp")
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_last_activity_update
|
||||
end
|
||||
|
||||
%i[name_regex_delete name_regex name_regex_keep].each do |param_name|
|
||||
context "for #{param_name}" do
|
||||
let(:params) { { param_name => invalid_regex } }
|
||||
|
||||
it_behaves_like 'rejecting the invalid regex', param_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
|
||||
subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
|
||||
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
context 'for reporter' do
|
||||
let(:api_user) { reporter }
|
||||
it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
|
||||
end
|
||||
context 'for reporter' do
|
||||
let(:api_user) { reporter }
|
||||
|
||||
it 'returns a details of tag' do
|
||||
subject
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
|
||||
end
|
||||
|
||||
expect(json_response).to include(
|
||||
'name' => 'rootA',
|
||||
'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
|
||||
'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
|
||||
'total_size' => 2319870)
|
||||
end
|
||||
it 'returns a details of tag' do
|
||||
subject
|
||||
|
||||
it 'returns a matching schema' do
|
||||
subject
|
||||
expect(json_response).to include(
|
||||
'name' => 'rootA',
|
||||
'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
|
||||
'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
|
||||
'total_size' => 2319870)
|
||||
end
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('registry/tag')
|
||||
it 'returns a matching schema' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('registry/tag')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
|
||||
let(:method) { :delete }
|
||||
let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
|
||||
let(:service) { double('service') }
|
||||
|
||||
subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
|
||||
['using API user', 'using job token'].each do |context|
|
||||
context context do
|
||||
include_context context
|
||||
|
||||
it_behaves_like 'rejected container repository access', :reporter, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
it_behaves_like 'rejected container repository access', :reporter, :forbidden
|
||||
it_behaves_like 'rejected container repository access', :anonymous, :not_found
|
||||
|
||||
context 'for developer', :snowplow do
|
||||
let(:api_user) { developer }
|
||||
context 'for developer', :snowplow do
|
||||
let(:api_user) { developer }
|
||||
|
||||
context 'when there are multiple tags' do
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
|
||||
end
|
||||
context 'when there are multiple tags' do
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
|
||||
end
|
||||
|
||||
it 'properly removes tag' do
|
||||
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
|
||||
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
|
||||
it 'properly removes tag' do
|
||||
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
|
||||
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
|
||||
|
||||
subject
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect_snowplow_event(category: described_class.name, action: 'delete_tag')
|
||||
end
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect_snowplow_event(category: described_class.name, action: 'delete_tag')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there\'s only one tag' do
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
|
||||
end
|
||||
context 'when there\'s only one tag' do
|
||||
before do
|
||||
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
|
||||
end
|
||||
|
||||
it 'properly removes tag' do
|
||||
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
|
||||
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
|
||||
it 'properly removes tag' do
|
||||
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
|
||||
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
|
||||
|
||||
subject
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect_snowplow_event(category: described_class.name, action: 'delete_tag')
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect_snowplow_event(category: described_class.name, action: 'delete_tag')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'rejected job token scopes'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,11 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
|
|||
throttle_authenticated_web_requests_per_period: 100,
|
||||
throttle_authenticated_web_period_in_seconds: 1,
|
||||
throttle_authenticated_protected_paths_request_per_period: 100,
|
||||
throttle_authenticated_protected_paths_in_seconds: 1
|
||||
throttle_authenticated_protected_paths_in_seconds: 1,
|
||||
throttle_unauthenticated_packages_api_requests_per_period: 100,
|
||||
throttle_unauthenticated_packages_api_period_in_seconds: 1,
|
||||
throttle_authenticated_packages_api_requests_per_period: 100,
|
||||
throttle_authenticated_packages_api_period_in_seconds: 1
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -435,6 +439,186 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Packages API' do
|
||||
let(:request_method) { 'GET' }
|
||||
|
||||
context 'unauthenticated' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
|
||||
let(:throttle_setting_prefix) { 'throttle_unauthenticated_packages_api' }
|
||||
let(:packages_path_that_does_not_require_authentication) { "/api/v4/projects/#{project.id}/packages/conan/v1/ping" }
|
||||
|
||||
def do_request
|
||||
get packages_path_that_does_not_require_authentication
|
||||
end
|
||||
|
||||
before do
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_requests_per_period] = requests_per_period
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_period_in_seconds] = period_in_seconds
|
||||
end
|
||||
|
||||
context 'when unauthenticated packages api throttle is disabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_enabled] = false
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'allows requests over the rate limit' do
|
||||
(1 + requests_per_period).times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated api throttle is enabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
|
||||
settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
|
||||
settings_to_set[:throttle_unauthenticated_enabled] = true
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'rejects requests over the unauthenticated api rate limit' do
|
||||
requests_per_period.times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect_rejection { do_request }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated packages api throttle is enabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_requests_per_period] = requests_per_period # 1
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_period_in_seconds] = period_in_seconds # 10_000
|
||||
settings_to_set[:throttle_unauthenticated_packages_api_enabled] = true
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'rejects requests over the rate limit' do
|
||||
requests_per_period.times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect_rejection { do_request }
|
||||
end
|
||||
|
||||
context 'when unauthenticated api throttle is lower' do
|
||||
before do
|
||||
settings_to_set[:throttle_unauthenticated_requests_per_period] = 0
|
||||
settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
|
||||
settings_to_set[:throttle_unauthenticated_enabled] = true
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'ignores unauthenticated api throttle' do
|
||||
requests_per_period.times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect_rejection { do_request }
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'tracking when dry-run mode is set' do
|
||||
let(:throttle_name) { 'throttle_unauthenticated_packages_api' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticated', :api do
|
||||
let_it_be(:project) { create(:project, :internal) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
let_it_be(:other_user_token) { create(:personal_access_token, user: other_user) }
|
||||
|
||||
let(:throttle_setting_prefix) { 'throttle_authenticated_packages_api' }
|
||||
let(:api_partial_url) { "/projects/#{project.id}/packages/conan/v1/ping" }
|
||||
|
||||
before do
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
context 'with the token in the query string' do
|
||||
let(:request_args) { [api(api_partial_url, personal_access_token: token), {}] }
|
||||
let(:other_user_request_args) { [api(api_partial_url, personal_access_token: other_user_token), {}] }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with the token in the headers' do
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'precedence over authenticated api throttle' do
|
||||
before do
|
||||
settings_to_set[:throttle_authenticated_packages_api_requests_per_period] = requests_per_period
|
||||
settings_to_set[:throttle_authenticated_packages_api_period_in_seconds] = period_in_seconds
|
||||
end
|
||||
|
||||
def do_request
|
||||
get api(api_partial_url, personal_access_token: token)
|
||||
end
|
||||
|
||||
context 'when authenticated packages api throttle is enabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_authenticated_packages_api_enabled] = true
|
||||
end
|
||||
|
||||
context 'when authenticated api throttle is lower' do
|
||||
before do
|
||||
settings_to_set[:throttle_authenticated_api_requests_per_period] = 0
|
||||
settings_to_set[:throttle_authenticated_api_period_in_seconds] = period_in_seconds
|
||||
settings_to_set[:throttle_authenticated_api_enabled] = true
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'ignores authenticated api throttle' do
|
||||
requests_per_period.times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect_rejection { do_request }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated packages api throttle is disabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_authenticated_packages_api_enabled] = false
|
||||
end
|
||||
|
||||
context 'when authenticated api throttle is enabled' do
|
||||
before do
|
||||
settings_to_set[:throttle_authenticated_api_requests_per_period] = requests_per_period
|
||||
settings_to_set[:throttle_authenticated_api_period_in_seconds] = period_in_seconds
|
||||
settings_to_set[:throttle_authenticated_api_enabled] = true
|
||||
stub_application_setting(settings_to_set)
|
||||
end
|
||||
|
||||
it 'rejects requests over the authenticated api rate limit' do
|
||||
requests_per_period.times do
|
||||
do_request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect_rejection { do_request }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'throttle bypass header' do
|
||||
let(:headers) { {} }
|
||||
let(:bypass_header) { 'gitlab-bypass-rate-limiting' }
|
||||
|
|
|
@ -336,6 +336,32 @@ RSpec.describe ApplicationSettings::UpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when package registry rate limits are passed' do
|
||||
let(:params) do
|
||||
{
|
||||
throttle_unauthenticated_packages_api_enabled: 1,
|
||||
throttle_unauthenticated_packages_api_period_in_seconds: 500,
|
||||
throttle_unauthenticated_packages_api_requests_per_period: 20,
|
||||
throttle_authenticated_packages_api_enabled: 1,
|
||||
throttle_authenticated_packages_api_period_in_seconds: 600,
|
||||
throttle_authenticated_packages_api_requests_per_period: 10
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates package registry throttle settings' do
|
||||
subject.execute
|
||||
|
||||
application_settings.reload
|
||||
|
||||
expect(application_settings.throttle_unauthenticated_packages_api_enabled).to be_truthy
|
||||
expect(application_settings.throttle_unauthenticated_packages_api_period_in_seconds).to eq(500)
|
||||
expect(application_settings.throttle_unauthenticated_packages_api_requests_per_period).to eq(20)
|
||||
expect(application_settings.throttle_authenticated_packages_api_enabled).to be_truthy
|
||||
expect(application_settings.throttle_authenticated_packages_api_period_in_seconds).to eq(600)
|
||||
expect(application_settings.throttle_authenticated_packages_api_requests_per_period).to eq(10)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issues_create_limit is passed' do
|
||||
let(:params) do
|
||||
{
|
||||
|
|
|
@ -51,7 +51,7 @@ RSpec.describe Git::WikiPushService, services: true do
|
|||
process_changes do
|
||||
write_new_page
|
||||
update_page(wiki_page_a.title)
|
||||
delete_page(wiki_page_b.page.path)
|
||||
delete_page(wiki_page_b.page)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -198,7 +198,7 @@ RSpec.describe Git::WikiPushService, services: true do
|
|||
context 'when a page we do not know about has been deleted' do
|
||||
def run_service
|
||||
wiki_page = create(:wiki_page, wiki: wiki)
|
||||
process_changes { delete_page(wiki_page.page.path) }
|
||||
process_changes { delete_page(wiki_page.page) }
|
||||
end
|
||||
|
||||
it 'create a new meta-data record' do
|
||||
|
@ -350,8 +350,8 @@ RSpec.describe Git::WikiPushService, services: true do
|
|||
git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details)
|
||||
end
|
||||
|
||||
def delete_page(path)
|
||||
git_wiki.delete_page(path, commit_details)
|
||||
def delete_page(page)
|
||||
wiki.delete_page(page, 'commit message')
|
||||
end
|
||||
|
||||
def commit_details
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'update board list mutation' do
|
||||
describe '#resolve' do
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
|
||||
let(:list_update_params) { { position: 1, collapsed: true } }
|
||||
|
||||
subject { mutation.resolve(list: list, **list_update_params) }
|
||||
|
||||
before_all do
|
||||
group.add_reporter(reporter)
|
||||
group.add_guest(guest)
|
||||
list.update_preferences_for(reporter, collapsed: false)
|
||||
end
|
||||
|
||||
context 'with permission to admin board lists' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it 'updates the list position and collapsed state as expected' do
|
||||
subject
|
||||
|
||||
reloaded_list = list.reload
|
||||
expect(reloaded_list.position).to eq(1)
|
||||
expect(reloaded_list.collapsed?(current_user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with permission to read board lists' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it 'updates the list collapsed state but not the list position' do
|
||||
subject
|
||||
|
||||
reloaded_list = list.reload
|
||||
expect(reloaded_list.position).to eq(0)
|
||||
expect(reloaded_list.collapsed?(current_user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without permission to read board lists' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it 'raises Resource Not Found error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -469,33 +469,29 @@ RSpec.shared_examples 'wiki model' do
|
|||
end
|
||||
|
||||
describe '#delete_page' do
|
||||
shared_examples 'delete_page operations' do
|
||||
let(:page) { create(:wiki_page, wiki: wiki) }
|
||||
let(:page) { create(:wiki_page, wiki: wiki) }
|
||||
|
||||
it 'deletes the page' do
|
||||
subject.delete_page(page)
|
||||
it 'deletes the page' do
|
||||
subject.delete_page(page)
|
||||
|
||||
expect(subject.list_pages.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'sets the correct commit email' do
|
||||
subject.delete_page(page)
|
||||
|
||||
expect(user.commit_email).not_to eq(user.email)
|
||||
expect(commit.author_email).to eq(user.commit_email)
|
||||
expect(commit.committer_email).to eq(user.commit_email)
|
||||
end
|
||||
|
||||
it 'runs after_wiki_activity callbacks' do
|
||||
page
|
||||
|
||||
expect(subject).to receive(:after_wiki_activity)
|
||||
|
||||
subject.delete_page(page)
|
||||
end
|
||||
expect(subject.list_pages.count).to eq(0)
|
||||
end
|
||||
|
||||
it_behaves_like 'delete_page operations'
|
||||
it 'sets the correct commit email' do
|
||||
subject.delete_page(page)
|
||||
|
||||
expect(user.commit_email).not_to eq(user.email)
|
||||
expect(commit.author_email).to eq(user.commit_email)
|
||||
expect(commit.committer_email).to eq(user.commit_email)
|
||||
end
|
||||
|
||||
it 'runs after_wiki_activity callbacks' do
|
||||
page
|
||||
|
||||
expect(subject).to receive(:after_wiki_activity)
|
||||
|
||||
subject.delete_page(page)
|
||||
end
|
||||
|
||||
context 'when an error is raised' do
|
||||
it 'logs the error and returns false' do
|
||||
|
@ -509,14 +505,6 @@ RSpec.shared_examples 'wiki model' do
|
|||
expect(subject.delete_page(page)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag :gitaly_replace_wiki_delete_page is disabled' do
|
||||
before do
|
||||
stub_feature_flags(gitaly_replace_wiki_delete_page: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'delete_page operations'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ensure_repository' do
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a GraphQL request to update board list' do
|
||||
context 'the user is not allowed to read board lists' do
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
before do
|
||||
list.update_preferences_for(current_user, collapsed: false)
|
||||
end
|
||||
|
||||
context 'when user has permissions to admin board lists' do
|
||||
before do
|
||||
group.add_reporter(current_user)
|
||||
end
|
||||
|
||||
it 'updates the list position and collapsed state' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list']).to include(
|
||||
'position' => 1,
|
||||
'collapsed' => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has permissions to read board lists' do
|
||||
before do
|
||||
group.add_guest(current_user)
|
||||
end
|
||||
|
||||
it 'updates the list collapsed state but not the list position' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['list']).to include(
|
||||
'position' => 0,
|
||||
'collapsed' => true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
# Requires let variables:
|
||||
# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths"
|
||||
# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api"
|
||||
# * request_method
|
||||
# * request_args
|
||||
# * other_user_request_args
|
||||
|
@ -13,7 +13,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
|
|||
{
|
||||
"throttle_protected_paths" => "throttle_authenticated_protected_paths_api",
|
||||
"throttle_authenticated_api" => "throttle_authenticated_api",
|
||||
"throttle_authenticated_web" => "throttle_authenticated_web"
|
||||
"throttle_authenticated_web" => "throttle_authenticated_web",
|
||||
"throttle_authenticated_packages_api" => "throttle_authenticated_packages_api"
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -106,96 +106,102 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with repository in cleanup scheduled state' do
|
||||
it_behaves_like 'handling all repository conditions'
|
||||
end
|
||||
|
||||
context 'with repository in cleanup unfinished state' do
|
||||
context 'with loopless disabled' do
|
||||
before do
|
||||
repository.cleanup_unfinished!
|
||||
stub_feature_flags(container_registry_expiration_policies_loopless: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all repository conditions'
|
||||
end
|
||||
|
||||
context 'with another repository in cleanup unfinished state' do
|
||||
let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
|
||||
|
||||
it 'process the cleanup scheduled repository first' do
|
||||
service_response = cleanup_service_response(repository: repository)
|
||||
expect(ContainerExpirationPolicies::CleanupService)
|
||||
.to receive(:new).with(repository).and_return(double(execute: service_response))
|
||||
expect_log_extra_metadata(service_response: service_response)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple repositories in cleanup unfinished state' do
|
||||
let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
|
||||
let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
|
||||
|
||||
before do
|
||||
repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
|
||||
context 'with repository in cleanup scheduled state' do
|
||||
it_behaves_like 'handling all repository conditions'
|
||||
end
|
||||
|
||||
it 'process the repository with the oldest expiration_policy_started_at' do
|
||||
service_response = cleanup_service_response(repository: repository)
|
||||
expect(ContainerExpirationPolicies::CleanupService)
|
||||
.to receive(:new).with(repository).and_return(double(execute: service_response))
|
||||
expect_log_extra_metadata(service_response: service_response)
|
||||
context 'with repository in cleanup unfinished state' do
|
||||
before do
|
||||
repository.cleanup_unfinished!
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with repository in cleanup ongoing state' do
|
||||
before do
|
||||
repository.cleanup_ongoing!
|
||||
it_behaves_like 'handling all repository conditions'
|
||||
end
|
||||
|
||||
it 'does not process it' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
context 'with another repository in cleanup unfinished state' do
|
||||
let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
expect(repository.cleanup_ongoing?).to be_truthy
|
||||
end
|
||||
end
|
||||
it 'process the cleanup scheduled repository first' do
|
||||
service_response = cleanup_service_response(repository: repository)
|
||||
expect(ContainerExpirationPolicies::CleanupService)
|
||||
.to receive(:new).with(repository).and_return(double(execute: service_response))
|
||||
expect_log_extra_metadata(service_response: service_response)
|
||||
|
||||
context 'with no repository in any cleanup state' do
|
||||
before do
|
||||
repository.cleanup_unscheduled!
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not process it' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
context 'with multiple repositories in cleanup unfinished state' do
|
||||
let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
|
||||
let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
expect(repository.cleanup_unscheduled?).to be_truthy
|
||||
end
|
||||
end
|
||||
before do
|
||||
repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
|
||||
end
|
||||
|
||||
context 'with no container repository waiting' do
|
||||
before do
|
||||
repository.destroy!
|
||||
it 'process the repository with the oldest expiration_policy_started_at' do
|
||||
service_response = cleanup_service_response(repository: repository)
|
||||
expect(ContainerExpirationPolicies::CleanupService)
|
||||
.to receive(:new).with(repository).and_return(double(execute: service_response))
|
||||
expect_log_extra_metadata(service_response: service_response)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not execute the cleanup tags service' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
context 'with repository in cleanup ongoing state' do
|
||||
before do
|
||||
repository.cleanup_ongoing!
|
||||
end
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
end
|
||||
end
|
||||
it 'does not process it' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
|
||||
context 'with feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
expect(repository.cleanup_ongoing?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it 'is a no-op' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
context 'with no repository in any cleanup state' do
|
||||
before do
|
||||
repository.cleanup_unscheduled!
|
||||
end
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
it 'does not process it' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
expect(repository.cleanup_unscheduled?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no container repository waiting' do
|
||||
before do
|
||||
repository.destroy!
|
||||
end
|
||||
|
||||
it 'does not execute the cleanup tags service' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||
end
|
||||
|
||||
it 'is a no-op' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -230,37 +236,42 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
describe '#remaining_work_count' do
|
||||
subject { worker.remaining_work_count }
|
||||
|
||||
context 'with container repositoires waiting for cleanup' do
|
||||
let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
|
||||
|
||||
it { is_expected.to eq(3) }
|
||||
|
||||
it 'logs the work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 1,
|
||||
cleanup_unfinished_count: 2,
|
||||
cleanup_total_count: 3
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no container repositories waiting for cleanup' do
|
||||
context 'with loopless disabled' do
|
||||
before do
|
||||
repository.cleanup_ongoing!
|
||||
stub_feature_flags(container_registry_expiration_policies_loopless: false)
|
||||
end
|
||||
context 'with container repositoires waiting for cleanup' do
|
||||
let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
|
||||
|
||||
it { is_expected.to eq(3) }
|
||||
|
||||
it 'logs the work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 1,
|
||||
cleanup_unfinished_count: 2,
|
||||
cleanup_total_count: 3
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to eq(0) }
|
||||
context 'with no container repositories waiting for cleanup' do
|
||||
before do
|
||||
repository.cleanup_ongoing!
|
||||
end
|
||||
|
||||
it 'logs 0 work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 0,
|
||||
cleanup_unfinished_count: 0,
|
||||
cleanup_total_count: 0
|
||||
)
|
||||
it { is_expected.to eq(0) }
|
||||
|
||||
subject
|
||||
it 'logs 0 work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 0,
|
||||
cleanup_unfinished_count: 0,
|
||||
cleanup_total_count: 0
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -274,14 +285,20 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(capacity) }
|
||||
|
||||
context 'with feature flag disabled' do
|
||||
context 'with loopless disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||
stub_feature_flags(container_registry_expiration_policies_loopless: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(0) }
|
||||
it { is_expected.to eq(capacity) }
|
||||
|
||||
context 'with feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(0) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -35,10 +35,16 @@ RSpec.describe ContainerExpirationPolicyWorker do
|
|||
end
|
||||
|
||||
context 'With no container expiration policies' do
|
||||
it 'does not execute any policies' do
|
||||
expect(ContainerRepository).not_to receive(:for_project_id)
|
||||
context 'with loopless disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_loopless: false)
|
||||
end
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
|
||||
it 'does not execute any policies' do
|
||||
expect(ContainerRepository).not_to receive(:for_project_id)
|
||||
|
||||
expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue