Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-21 15:09:35 +00:00
parent 2af44d609e
commit 9c6578ed4e
97 changed files with 2121 additions and 845 deletions

1
.gitignore vendored
View File

@ -105,3 +105,4 @@ ee/changelogs/unreleased-ee
tags.lock
tags.temp
.stylelintcache
.solargraph.yml

16
.solargraph.yml.example Normal file
View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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(() => {});
}

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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"
/>

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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 }

View File

@ -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,

View File

@ -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
}
]

View File

@ -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

View File

@ -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"

View File

@ -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' }

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Enable new RPC to destroy wiki pages
merge_request: 57106
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Externalise strings in _abuse.html.haml
merge_request: 57968
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Externalize strings in requests_profiles/index.html.haml
merge_request: 58161
author: nuwe1
type: other

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Remove unneeded index on packages_debian_{project,group}_architectures.distribution_id
merge_request: 59615
author: Mathieu Parent
type: removed

View File

@ -0,0 +1,5 @@
---
title: Add specific rate limits for Package Registry (Package API)
merge_request: 57029
author: Jonas Wälter @wwwjon
type: added

View File

@ -0,0 +1,5 @@
---
title: Add gl-badge for badges in milestone drawer
merge_request: 57964
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update UI text for confluence integration
merge_request: 59839
author:
type: other

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
28b1e8add8ac7249be55ccd25e60c8a181d2ff036a7d69ac861bcdb5bf5e84e1

View File

@ -0,0 +1 @@
da9c3d764a5750a40e0f6edd2e713efd77620ba3e684e48d47c7f855e47b2984

View File

@ -0,0 +1 @@
7a7b0eaa67851aa9300e4750fd05c6d2d0b49ca7077099a0208a89c74ac03a2c

View File

@ -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);

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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. |

View File

@ -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).

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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+$/

View File

@ -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.'

View File

@ -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?

View File

@ -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,

View File

@ -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 = [

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -22,6 +22,7 @@ describe('graph component', () => {
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
showLinks: false,
viewType: STAGE_VIEW,
configPaths: {
metricsPath: '',

View File

@ -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');
});
});

View File

@ -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]]);
});
});
});

View File

@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => {
const defaultProps = {
columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
showLinks: false,
type: DOWNSTREAM,
viewType: STAGE_VIEW,
configPaths: {

View File

@ -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);
});
});
});

View File

@ -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',
},
};

View File

@ -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,

View File

@ -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');
});
});

View File

@ -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

View File

@ -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') }

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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