Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
589ee0e419
commit
8bf2e2b73e
61 changed files with 604 additions and 348 deletions
28
app/assets/javascripts/issuable/bulk_update_sidebar/index.js
Normal file
28
app/assets/javascripts/issuable/bulk_update_sidebar/index.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Vue from 'vue';
|
||||
import StatusSelect from './components/status_select.vue';
|
||||
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||
|
||||
export function initBulkUpdateSidebar(prefixId) {
|
||||
const el = document.querySelector('.issues-bulk-update');
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
issuableBulkUpdateActions.init({ prefixId });
|
||||
new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
export function initIssueStatusSelect() {
|
||||
const el = document.querySelector('.js-issue-status');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render: (createElement) => createElement(StatusSelect),
|
||||
});
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import StatusSelect from './components/status_select.vue';
|
||||
|
||||
export default function initIssueStatusSelect() {
|
||||
const el = document.querySelector('.js-issue-status');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(h) {
|
||||
return h(StatusSelect);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -6,7 +6,6 @@ import { property } from 'lodash';
|
|||
import issuableEventHub from '~/issues_list/eventhub';
|
||||
import LabelsSelect from '~/labels/labels_select';
|
||||
import MilestoneSelect from '~/milestones/milestone_select';
|
||||
import initIssueStatusSelect from './init_issue_status_select';
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import subscriptionSelect from './subscription_select';
|
||||
|
||||
|
@ -57,7 +56,6 @@ export default class IssuableBulkUpdateSidebar {
|
|||
initDropdowns() {
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
initIssueStatusSelect();
|
||||
subscriptionSelect();
|
||||
|
||||
if (IS_EE) {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||
|
||||
export default {
|
||||
bulkUpdateSidebar: null,
|
||||
|
||||
init(prefixId) {
|
||||
const bulkUpdateEl = document.querySelector('.issues-bulk-update');
|
||||
const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
|
||||
|
||||
if (bulkUpdateEl && !alreadyInitialized) {
|
||||
issuableBulkUpdateActions.init({ prefixId });
|
||||
|
||||
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
|
||||
}
|
||||
|
||||
return this.bulkUpdateSidebar;
|
||||
},
|
||||
};
|
|
@ -11,7 +11,9 @@ import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
|
|||
export function initCsvImportExportButtons() {
|
||||
const el = document.querySelector('.js-csv-import-export-buttons');
|
||||
|
||||
if (!el) return null;
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
showExportButton,
|
||||
|
@ -42,23 +44,24 @@ export function initCsvImportExportButtons() {
|
|||
maxAttachmentSize,
|
||||
showLabel,
|
||||
},
|
||||
render(h) {
|
||||
return h(CsvImportExportButtons, {
|
||||
render: (createElement) =>
|
||||
createElement(CsvImportExportButtons, {
|
||||
props: {
|
||||
exportCsvPath,
|
||||
issuableCount: parseInt(issuableCount, 10),
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function initIssuableByEmail() {
|
||||
Vue.use(GlToast);
|
||||
|
||||
const el = document.querySelector('.js-issuable-by-email');
|
||||
|
||||
if (!el) return null;
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
||||
const {
|
||||
initialEmail,
|
||||
|
@ -79,9 +82,7 @@ export function initIssuableByEmail() {
|
|||
markdownHelpPath,
|
||||
resetPath,
|
||||
},
|
||||
render(h) {
|
||||
return h(IssuableByEmail);
|
||||
},
|
||||
render: (createElement) => createElement(IssuableByEmail),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -89,7 +90,7 @@ export function initIssuableHeaderWarnings(store) {
|
|||
const el = document.getElementById('js-issuable-header-warnings');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const { hidden } = el.dataset;
|
||||
|
@ -98,18 +99,18 @@ export function initIssuableHeaderWarnings(store) {
|
|||
el,
|
||||
store,
|
||||
provide: { hidden: parseBoolean(hidden) },
|
||||
render(createElement) {
|
||||
return createElement(IssuableHeaderWarnings);
|
||||
},
|
||||
render: (createElement) => createElement(IssuableHeaderWarnings),
|
||||
});
|
||||
}
|
||||
|
||||
export function initIssuableSidebar() {
|
||||
const sidebarOptEl = document.querySelector('.js-sidebar-options');
|
||||
const el = document.querySelector('.js-sidebar-options');
|
||||
|
||||
if (!sidebarOptEl) return;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sidebarOptions = getSidebarOptions(sidebarOptEl);
|
||||
const sidebarOptions = getSidebarOptions(el);
|
||||
|
||||
new IssuableContext(sidebarOptions.currentUser); // eslint-disable-line no-new
|
||||
Sidebar.initialize();
|
||||
|
|
|
@ -517,10 +517,9 @@ export default {
|
|||
},
|
||||
async handleBulkUpdateClick() {
|
||||
if (!this.hasInitBulkEdit) {
|
||||
const initBulkUpdateSidebar = await import(
|
||||
'~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar'
|
||||
);
|
||||
initBulkUpdateSidebar.default.init('issuable_');
|
||||
const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
|
||||
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
|
||||
bulkUpdateSidebar.initIssueStatusSelect();
|
||||
|
||||
const usersSelect = await import('~/users_select');
|
||||
const UsersSelect = usersSelect.default;
|
||||
|
|
|
@ -172,8 +172,12 @@ export default class Todos {
|
|||
|
||||
updateBadges(data) {
|
||||
$(document).trigger('todo:toggle', data.count);
|
||||
document.querySelector('.js-todos-pending .badge').innerHTML = addDelimiter(data.count);
|
||||
document.querySelector('.js-todos-done .badge').innerHTML = addDelimiter(data.done_count);
|
||||
document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
|
||||
data.count,
|
||||
);
|
||||
document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
|
||||
data.done_count,
|
||||
);
|
||||
}
|
||||
|
||||
goToTodoUrl(e) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
||||
import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
|
||||
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
|
@ -13,7 +13,7 @@ if (gon.features?.vueIssuesList) {
|
|||
|
||||
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
|
||||
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
|
||||
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
|
||||
initBulkUpdateSidebar(ISSUE_BULK_UPDATE_PREFIX);
|
||||
|
||||
initFilteredSearch({
|
||||
page: FILTERED_SEARCH.ISSUES,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
||||
import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import projectSelect from '~/project_select';
|
||||
|
@ -8,7 +8,7 @@ import projectSelect from '~/project_select';
|
|||
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
|
||||
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_BULK_UPDATE_PREFIX);
|
||||
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
|
||||
|
||||
initFilteredSearch({
|
||||
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
||||
import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
|
||||
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
|
@ -20,7 +20,8 @@ if (gon.features?.vueIssuesList) {
|
|||
useDefaultState: true,
|
||||
});
|
||||
|
||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.ISSUE);
|
||||
initBulkUpdateSidebar(ISSUABLE_INDEX.ISSUE);
|
||||
initIssueStatusSelect();
|
||||
new UsersSelect(); // eslint-disable-line no-new
|
||||
|
||||
initCsvImportExportButtons();
|
||||
|
|
|
@ -2,13 +2,14 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra
|
|||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
||||
import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import { ISSUABLE_INDEX } from '~/issuable/constants';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import UsersSelect from '~/users_select';
|
||||
|
||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.MERGE_REQUEST);
|
||||
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
|
||||
initIssueStatusSelect();
|
||||
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
|
||||
|
|
|
@ -54,7 +54,7 @@ class GroupsFinder < UnionFinder
|
|||
groups = []
|
||||
|
||||
if current_user
|
||||
if Feature.enabled?(:use_traversal_ids_groups_finder, default_enabled: :yaml)
|
||||
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
|
||||
groups << current_user.authorized_groups.self_and_ancestors
|
||||
groups << current_user.groups.self_and_descendants
|
||||
else
|
||||
|
@ -81,7 +81,7 @@ class GroupsFinder < UnionFinder
|
|||
.groups
|
||||
.where('members.access_level >= ?', params[:min_access_level])
|
||||
|
||||
if Feature.enabled?(:use_traversal_ids_groups_finder, default_enabled: :yaml)
|
||||
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
|
||||
groups.self_and_descendants
|
||||
else
|
||||
Gitlab::ObjectHierarchy
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
module Admin
|
||||
module BackgroundMigrationsHelper
|
||||
def batched_migration_status_badge_class_name(migration)
|
||||
class_names = {
|
||||
'active' => 'badge-info',
|
||||
'paused' => 'badge-warning',
|
||||
'failed' => 'badge-danger',
|
||||
'finished' => 'badge-success'
|
||||
def batched_migration_status_badge_variant(migration)
|
||||
variants = {
|
||||
'active' => :info,
|
||||
'paused' => :warning,
|
||||
'failed' => :danger,
|
||||
'finished' => :success
|
||||
}
|
||||
|
||||
class_names[migration.status]
|
||||
variants[migration.status]
|
||||
end
|
||||
|
||||
# The extra logic here is needed because total_tuple_count is just
|
||||
|
|
|
@ -42,16 +42,19 @@ module Ci
|
|||
check_access!(build)
|
||||
|
||||
new_build = clone_build(build)
|
||||
|
||||
if create_deployment_in_separate_transaction?
|
||||
new_build.run_after_commit do |new_build|
|
||||
::Deployments::CreateForBuildService.new.execute(new_build)
|
||||
end
|
||||
end
|
||||
|
||||
::Ci::Pipelines::AddJobService.new(build.pipeline).execute!(new_build) do |job|
|
||||
BulkInsertableAssociations.with_bulk_insert do
|
||||
job.save!
|
||||
end
|
||||
end
|
||||
|
||||
if create_deployment_in_separate_transaction?
|
||||
clone_deployment!(new_build, build)
|
||||
end
|
||||
|
||||
build.reset # refresh the data to get new values of `retried` and `processed`.
|
||||
|
||||
new_build
|
||||
|
@ -95,20 +98,6 @@ module Ci
|
|||
.deployment_attributes_for(new_build, old_build.persisted_environment)
|
||||
end
|
||||
|
||||
def clone_deployment!(new_build, old_build)
|
||||
return unless old_build.deployment.present?
|
||||
|
||||
# We should clone the previous deployment attributes instead of initializing
|
||||
# new object with `Seed::Deployment`.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/347206
|
||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||
.new(new_build, old_build.persisted_environment).to_resource
|
||||
|
||||
return unless deployment
|
||||
|
||||
new_build.create_deployment!(deployment.attributes)
|
||||
end
|
||||
|
||||
def create_deployment_in_separate_transaction?
|
||||
strong_memoize(:create_deployment_in_separate_transaction) do
|
||||
::Feature.enabled?(:create_deployment_in_separate_transaction, project, default_enabled: :yaml)
|
||||
|
|
25
app/services/deployments/create_for_build_service.rb
Normal file
25
app/services/deployments/create_for_build_service.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Deployments
|
||||
# This class creates a deployment record for a build (a pipeline job).
|
||||
class CreateForBuildService
|
||||
DeploymentCreationError = Class.new(StandardError)
|
||||
|
||||
def execute(build)
|
||||
return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
|
||||
|
||||
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
|
||||
# `create_deployment_in_separate_transaction` feature flag has been removed.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/348778
|
||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||
.new(build, build.persisted_environment).to_resource
|
||||
|
||||
return unless deployment
|
||||
|
||||
build.create_deployment!(deployment.attributes)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
||||
DeploymentCreationError.new(e.message), build_id: build.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@
|
|||
- else
|
||||
= _('Unknown')
|
||||
%td{ role: 'cell', data: { label: _('Status') } }
|
||||
%span.badge.badge-pill.gl-badge.sm{ class: batched_migration_status_badge_class_name(migration) }= migration.status.humanize
|
||||
= gl_badge_tag migration.status.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
|
||||
%td{ role: 'cell', data: { label: _('Action') } }
|
||||
- if migration.active?
|
||||
= button_to pause_admin_background_migration_path(migration),
|
||||
|
|
|
@ -8,18 +8,15 @@
|
|||
%li.nav-item{ role: 'presentation' }
|
||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path, class: (active_tab_classes if @current_tab == 'queued'), role: 'tab' }
|
||||
= _('Queued')
|
||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= limited_counter_with_delimiter(@relations_by_tab['queued'])
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['queued'])
|
||||
%li.nav-item{ role: 'presentation' }
|
||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'failed'), class: (active_tab_classes if @current_tab == 'failed'), role: 'tab' }
|
||||
= _('Failed')
|
||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= limited_counter_with_delimiter(@relations_by_tab['failed'])
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['failed'])
|
||||
%li.nav-item{ role: 'presentation' }
|
||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'finished'), class: (active_tab_classes if @current_tab == 'finished'), role: 'tab' }
|
||||
= _('Finished')
|
||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= limited_counter_with_delimiter(@relations_by_tab['finished'])
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['finished'])
|
||||
|
||||
.tab-content.gl-tab-content
|
||||
.tab-pane.active{ role: 'tabpanel' }
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
|
||||
= gl_tab_link_to todos_filter_path(state: 'pending'), item_active: params[:state].blank? || params[:state] == 'pending', class: "js-todos-pending" do
|
||||
= _("To Do")
|
||||
= gl_tab_counter_badge number_with_delimiter(todos_pending_count)
|
||||
= gl_tab_counter_badge(number_with_delimiter(todos_pending_count), { class: 'js-todos-badge' })
|
||||
= gl_tab_link_to todos_filter_path(state: 'done'), item_active: params[:state] == 'done', class: "js-todos-done" do
|
||||
= _("Done")
|
||||
= gl_tab_counter_badge number_with_delimiter(todos_done_count)
|
||||
= gl_tab_counter_badge(number_with_delimiter(todos_done_count), { class: 'js-todos-badge' })
|
||||
|
||||
.nav-controls
|
||||
- if @allowed_todos.any?(&:pending?)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: redis_hll_counters.testing.users_clicking_license_testing_visiting_external_website_monthly
|
||||
description: Count of users clicking licence to visit external information website
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: group::static analysis
|
||||
product_category: dependency_scanning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '14.7'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76917
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- users_clicking_license_testing_visiting_external_website
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: redis_hll_counters.testing.users_clicking_license_testing_visiting_external_website_weekly
|
||||
description: Count of users clicking licence to visit external information website
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: group::static analysis
|
||||
product_category: dependency_scanning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '14.7'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76917
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- users_clicking_license_testing_visiting_external_website
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -23,7 +23,7 @@ Side effects:
|
|||
|
||||
## `ci_queueing_disaster_recovery_disable_quota`
|
||||
|
||||
This feature flag, if temporarily enabled, disables enforcing CI minutes quota
|
||||
This feature flag, if temporarily enabled, disables enforcing CI/CD minutes quota
|
||||
on shared runners. This can help to reduce system resource usage on the
|
||||
`jobs/request` endpoint by significantly reducing the computations being
|
||||
performed.
|
||||
|
|
|
@ -34,6 +34,7 @@ The following API resources are available in the project context:
|
|||
| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) |
|
||||
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
|
||||
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
|
||||
| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) |
|
||||
| [Deployments](deployments.md) | `/projects/:id/deployments` |
|
||||
| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) |
|
||||
| [Environments](environments.md) | `/projects/:id/environments` |
|
||||
|
@ -101,6 +102,7 @@ The following API resources are available in the group context:
|
|||
| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
|
||||
| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
|
||||
| [Debian distributions](packages/debian_group_distributions.md) | `/groups/:id/-/packages/debian` (also available for projects) |
|
||||
| [Deploy tokens](deploy_tokens.md) | `/groups/:id/deploy_tokens` (also available for projects and standalone) |
|
||||
| [Discussions](discussions.md) (threaded comments) **(ULTIMATE)** | `/groups/:id/epics/.../discussions` (also available for projects) |
|
||||
| [Epic issues](epic_issues.md) **(ULTIMATE)** | `/groups/:id/epics/.../issues` |
|
||||
| [Epic links](epic_links.md) **(ULTIMATE)** | `/groups/:id/epics/.../epics` |
|
||||
|
@ -137,6 +139,7 @@ The following API resources are available outside of project and group contexts
|
|||
| [Code snippets](snippets.md) | `/snippets` |
|
||||
| [Custom attributes](custom_attributes.md) | `/users/:id/custom_attributes` (also available for groups and projects) |
|
||||
| [Deploy keys](deploy_keys.md) | `/deploy_keys` (also available for projects) |
|
||||
| [Deploy tokens](deploy_tokens.md) | `/deploy_tokens` (also available for projects and groups) |
|
||||
| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) |
|
||||
| [Feature flags](features.md) | `/features` |
|
||||
| [Geo Nodes](geo_nodes.md) **(PREMIUM SELF)** | `/geo_nodes` |
|
||||
|
|
|
@ -385,7 +385,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
|
||||
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
|
||||
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
|
||||
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
|
||||
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of CI/CD minutes that a group can use on shared runners per month. |
|
||||
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
|
||||
| `sidekiq_job_limiter_mode` | string | no | `track` or `compress`. Sets the behavior for [Sidekiq job size limits](../user/admin_area/settings/sidekiq_job_limits.md). Default: 'compress'. |
|
||||
| `sidekiq_job_limiter_compression_threshold_bytes` | integer | no | The threshold in bytes at which Sidekiq jobs are compressed before being stored in Redis. Default: 100 000 bytes (100KB). |
|
||||
|
|
|
@ -257,14 +257,33 @@ WARNING:
|
|||
Deleting a pipeline expires all pipeline caches, and deletes all related objects,
|
||||
such as builds, logs, artifacts, and triggers. **This action cannot be undone.**
|
||||
|
||||
### Quotas of CI/CD minutes
|
||||
### Pipeline security on protected branches
|
||||
|
||||
Each user has a personal pipeline quota that tracks the usage of shared runners in all personal projects.
|
||||
Each group has a [quota of CI/CD minutes](cicd_minutes.md) that tracks the usage of shared runners for all projects created within the group.
|
||||
A strict security model is enforced when pipelines are executed on
|
||||
[protected branches](../../user/project/protected_branches.md).
|
||||
|
||||
When a pipeline is triggered, regardless of who triggered it, the quota of CI/CD minutes for the project owner's [namespace](../../user/group/index.md#namespaces) is used. In this case, the namespace can be the user or group that owns the project.
|
||||
The following actions are allowed on protected branches only if the user is
|
||||
[allowed to merge or push](../../user/project/protected_branches.md)
|
||||
on that specific branch:
|
||||
|
||||
#### How pipeline duration is calculated
|
||||
- Run manual pipelines (using the [Web UI](#run-a-pipeline-manually) or [pipelines API](#pipelines-api)).
|
||||
- Run scheduled pipelines.
|
||||
- Run pipelines using triggers.
|
||||
- Run on-demand DAST scan.
|
||||
- Trigger manual actions on existing pipelines.
|
||||
- Retry or cancel existing jobs (using the Web UI or pipelines API).
|
||||
|
||||
**Variables** marked as **protected** are accessible only to jobs that
|
||||
run on protected branches, preventing untrusted users getting unintended access to
|
||||
sensitive information like deployment credentials and tokens.
|
||||
|
||||
**Runners** marked as **protected** can run jobs only on protected
|
||||
branches, preventing untrusted code from executing on the protected runner and
|
||||
preserving deployment keys and other credentials from being unintentionally
|
||||
accessed. In order to ensure that jobs intended to be executed on protected
|
||||
runners do not use regular runners, they must be tagged accordingly.
|
||||
|
||||
### How pipeline duration is calculated
|
||||
|
||||
Total running time for a given pipeline excludes retries and pending
|
||||
(queued) time.
|
||||
|
@ -301,44 +320,6 @@ The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time
|
|||
(4 - 1) + (7 - 6) => 4
|
||||
```
|
||||
|
||||
#### How pipeline quota usage is calculated
|
||||
|
||||
Pipeline quota usage is calculated as the sum of the duration of each individual job. This is slightly different to how pipeline _duration_ is [calculated](#how-pipeline-duration-is-calculated). Pipeline quota usage doesn't consider any overlap of jobs running in parallel.
|
||||
|
||||
For example, a pipeline consists of the following jobs:
|
||||
|
||||
- Job A takes 3 minutes.
|
||||
- Job B takes 3 minutes.
|
||||
- Job C takes 2 minutes.
|
||||
|
||||
The pipeline quota usage is the sum of each job's duration. In this example, 8 runner minutes would be used, calculated as: 3 + 3 + 2.
|
||||
|
||||
### Pipeline security on protected branches
|
||||
|
||||
A strict security model is enforced when pipelines are executed on
|
||||
[protected branches](../../user/project/protected_branches.md).
|
||||
|
||||
The following actions are allowed on protected branches only if the user is
|
||||
[allowed to merge or push](../../user/project/protected_branches.md)
|
||||
on that specific branch:
|
||||
|
||||
- Run manual pipelines (using the [Web UI](#run-a-pipeline-manually) or [pipelines API](#pipelines-api)).
|
||||
- Run scheduled pipelines.
|
||||
- Run pipelines using triggers.
|
||||
- Run on-demand DAST scan.
|
||||
- Trigger manual actions on existing pipelines.
|
||||
- Retry or cancel existing jobs (using the Web UI or pipelines API).
|
||||
|
||||
**Variables** marked as **protected** are accessible only to jobs that
|
||||
run on protected branches, preventing untrusted users getting unintended access to
|
||||
sensitive information like deployment credentials and tokens.
|
||||
|
||||
**Runners** marked as **protected** can run jobs only on protected
|
||||
branches, preventing untrusted code from executing on the protected runner and
|
||||
preserving deployment keys and other credentials from being unintentionally
|
||||
accessed. In order to ensure that jobs intended to be executed on protected
|
||||
runners do not use regular runners, they must be tagged accordingly.
|
||||
|
||||
## Visualize pipelines
|
||||
|
||||
Pipelines can be complex structures with many sequential and parallel jobs.
|
||||
|
|
|
@ -28,7 +28,7 @@ If you are using a self-managed instance of GitLab:
|
|||
going to your project's **Settings > CI/CD**, expanding the **Runners** section,
|
||||
and clicking **Show runner installation instructions**.
|
||||
These instructions are also available [in the documentation](https://docs.gitlab.com/runner/install/index.html).
|
||||
- The administrator can also configure a maximum number of shared runner [pipeline minutes for
|
||||
- The administrator can also configure a maximum number of shared runner [CI/CD minutes for
|
||||
each group](../pipelines/cicd_minutes.md#set-the-quota-of-cicd-minutes-for-a-specific-namespace).
|
||||
|
||||
If you are using GitLab.com:
|
||||
|
|
|
@ -180,7 +180,7 @@ feature and its components work.
|
|||
Watch a walkthrough of this feature in details in the video below.
|
||||
|
||||
<div class="video-fallback">
|
||||
See the video: <a href="https://www.youtube.com/watch?v=NmdWRGT8kZg">CI Minutes - architectural overview</a>.
|
||||
See the video: <a href="https://www.youtube.com/watch?v=NmdWRGT8kZg">CI/CD minutes - architectural overview</a>.
|
||||
</div>
|
||||
<figure class="video-container">
|
||||
<iframe src="https://www.youtube.com/embed/NmdWRGT8kZg" frameborder="0" allowfullscreen="true"> </iframe>
|
||||
|
|
|
@ -182,6 +182,11 @@ Use **check out** as a verb. For the Git command, use `checkout`.
|
|||
|
||||
CI/CD is always uppercase. No need to spell it out on first use.
|
||||
|
||||
## CI/CD minutes
|
||||
|
||||
Use **CI/CD minutes** instead of **CI minutes**, **pipeline minutes**, **pipeline minutes quota**, or
|
||||
**CI pipeline minutes**. This decision was made in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342813).
|
||||
|
||||
## click
|
||||
|
||||
Do not use **click**. Instead, use **select** with buttons, links, menu items, and lists.
|
||||
|
|
|
@ -703,10 +703,10 @@ Example response:
|
|||
|
||||
- CustomersDot
|
||||
|
||||
## CI minute provisioning
|
||||
## CI/CD minutes provisioning
|
||||
|
||||
The CI Minute endpoints are used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
|
||||
to apply additional packs of CI minutes, for personal namespaces or top-level groups within GitLab.com.
|
||||
The CI/CD Minutes endpoints are used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
|
||||
to apply additional packs of CI/CD minutes, for personal namespaces or top-level groups within GitLab.com.
|
||||
|
||||
### Creating an additional pack
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ To change the password for this customers portal account:
|
|||
### GitLab for Education
|
||||
|
||||
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
|
||||
the top GitLab tier, plus 50,000 CI minutes per month.
|
||||
the top GitLab tier, plus 50,000 CI/CD minutes per month.
|
||||
|
||||
The GitLab for Education license can only be used for instructional-use or
|
||||
non-commercial academic research.
|
||||
|
@ -188,7 +188,7 @@ Find more information on how to apply and renew at
|
|||
### GitLab for Open Source
|
||||
|
||||
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
|
||||
the top GitLab tier, plus 50,000 CI minutes per month.
|
||||
the top GitLab tier, plus 50,000 CI/CD minutes per month.
|
||||
|
||||
You can find more information about the [program requirements](https://about.gitlab.com/solutions/open-source/join/#requirements),
|
||||
[renewals](https://about.gitlab.com/solutions/open-source/join/#renewals),
|
||||
|
@ -253,7 +253,7 @@ if a project holds sensitive data. Email `opensource@gitlab.com` with details of
|
|||
### GitLab for Startups
|
||||
|
||||
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
|
||||
the top GitLab tier, plus 50,000 CI minutes per month for 12 months.
|
||||
the top GitLab tier, plus 50,000 CI/CD minutes per month for 12 months.
|
||||
|
||||
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
|
||||
|
||||
|
|
|
@ -202,6 +202,8 @@ The following data is included in the export:
|
|||
- Access level ([Project](../permissions.md#project-members-permissions) and [Group](../permissions.md#group-members-permissions))
|
||||
- Date of last activity ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345388) in GitLab 14.6). For a list of activities that populate this column, see the [Users API documentation](../../api/users.md#get-user-activities-admin-only).
|
||||
|
||||
Only the first 100,000 user accounts are exported.
|
||||
|
||||
![user permission export button](img/export_permissions_v13_11.png)
|
||||
|
||||
#### Users statistics
|
||||
|
|
|
@ -22,6 +22,10 @@ NOTE:
|
|||
By default, all Git operations are first tried unauthenticated. Because of this, HTTP Git operations
|
||||
may trigger the rate limits configured for unauthenticated requests.
|
||||
|
||||
NOTE:
|
||||
The rate limits for API requests don't affect requests made by the frontend, as these are always
|
||||
counted as web traffic.
|
||||
|
||||
## Enable unauthenticated API request rate limit
|
||||
|
||||
To enable the unauthenticated request rate limit:
|
||||
|
|
|
@ -774,7 +774,7 @@ To view the merge request approval rules for a group:
|
|||
- [Webhooks](../project/integrations/webhooks.md).
|
||||
- [Kubernetes cluster integration](clusters/index.md).
|
||||
- [Audit Events](../../administration/audit_events.md#group-events).
|
||||
- [Pipelines quota](../admin_area/settings/continuous_integration.md): Keep track of the pipeline quota for the group.
|
||||
- [CI/CD minutes quota](../../ci/pipelines/cicd_minutes.md): Keep track of the CI/CD minute quota for the group.
|
||||
- [Integrations](../admin_area/settings/project_integration_management.md).
|
||||
- [Transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace).
|
||||
- [Share a project with a group](../project/members/share_project_with_groups.md): Give all group members access to the project at once.
|
||||
|
|
|
@ -207,7 +207,7 @@ This improvement is [tracked as a follow-up](https://gitlab.com/gitlab-org/gitla
|
|||
|
||||
- When working locally in your branch, add multiple commits and only push when
|
||||
you're done, so GitLab runs only one pipeline for all the commits pushed
|
||||
at once. By doing so, you save pipeline minutes.
|
||||
at once. By doing so, you save CI/CD minutes.
|
||||
- Delete feature branches on merge or after merging them to keep your repository clean.
|
||||
- Take one thing at a time and ship the smallest changes possible. By doing so,
|
||||
reviews are faster and your changes are less prone to errors.
|
||||
|
|
|
@ -5,8 +5,6 @@ module Gitlab
|
|||
module Pipeline
|
||||
module Chain
|
||||
class CreateDeployments < Chain::Base
|
||||
DeploymentCreationError = Class.new(StandardError)
|
||||
|
||||
def perform!
|
||||
return unless pipeline.create_deployment_in_separate_transaction?
|
||||
|
||||
|
@ -24,18 +22,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def create_deployment(build)
|
||||
return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
|
||||
|
||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||
.new(build, build.persisted_environment).to_resource
|
||||
|
||||
return unless deployment
|
||||
|
||||
deployment.deployable = build
|
||||
deployment.save!
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
||||
DeploymentCreationError.new(e.message), build_id: build.id)
|
||||
::Deployments::CreateForBuildService.new.execute(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module Gitlab
|
||||
module RackAttack
|
||||
module Request
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
FILES_PATH_REGEX = %r{^/api/v\d+/projects/[^/]+/repository/files/.+}.freeze
|
||||
GROUP_PATH_REGEX = %r{^/api/v\d+/groups/[^/]+/?$}.freeze
|
||||
|
||||
|
@ -30,15 +32,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
def api_internal_request?
|
||||
path =~ %r{^/api/v\d+/internal/}
|
||||
path.match?(%r{^/api/v\d+/internal/})
|
||||
end
|
||||
|
||||
def health_check_request?
|
||||
path =~ %r{^/-/(health|liveness|readiness|metrics)}
|
||||
path.match?(%r{^/-/(health|liveness|readiness|metrics)})
|
||||
end
|
||||
|
||||
def container_registry_event?
|
||||
path =~ %r{^/api/v\d+/container_registry_event/}
|
||||
path.match?(%r{^/api/v\d+/container_registry_event/})
|
||||
end
|
||||
|
||||
def product_analytics_collector_request?
|
||||
|
@ -58,7 +60,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def protected_path_regex
|
||||
path =~ protected_paths_regex
|
||||
path.match?(protected_paths_regex)
|
||||
end
|
||||
|
||||
def throttle?(throttle, authenticated:)
|
||||
|
@ -70,6 +72,7 @@ module Gitlab
|
|||
def throttle_unauthenticated_api?
|
||||
api_request? &&
|
||||
!should_be_skipped? &&
|
||||
!frontend_request? &&
|
||||
!throttle_unauthenticated_packages_api? &&
|
||||
!throttle_unauthenticated_files_api? &&
|
||||
!throttle_unauthenticated_deprecated_api? &&
|
||||
|
@ -78,7 +81,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def throttle_unauthenticated_web?
|
||||
web_request? &&
|
||||
(web_request? || frontend_request?) &&
|
||||
!should_be_skipped? &&
|
||||
# TODO: Column will be renamed in https://gitlab.com/gitlab-org/gitlab/-/issues/340031
|
||||
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
|
||||
|
@ -87,6 +90,7 @@ module Gitlab
|
|||
|
||||
def throttle_authenticated_api?
|
||||
api_request? &&
|
||||
!frontend_request? &&
|
||||
!throttle_authenticated_packages_api? &&
|
||||
!throttle_authenticated_files_api? &&
|
||||
!throttle_authenticated_deprecated_api? &&
|
||||
|
@ -94,7 +98,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def throttle_authenticated_web?
|
||||
web_request? &&
|
||||
(web_request? || frontend_request?) &&
|
||||
!throttle_authenticated_git_lfs? &&
|
||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled
|
||||
end
|
||||
|
@ -178,15 +182,24 @@ module Gitlab
|
|||
end
|
||||
|
||||
def packages_api_path?
|
||||
path =~ ::Gitlab::Regex::Packages::API_PATH_REGEX
|
||||
path.match?(::Gitlab::Regex::Packages::API_PATH_REGEX)
|
||||
end
|
||||
|
||||
def git_lfs_path?
|
||||
path =~ Gitlab::PathRegex.repository_git_lfs_route_regex
|
||||
path.match?(Gitlab::PathRegex.repository_git_lfs_route_regex)
|
||||
end
|
||||
|
||||
def files_api_path?
|
||||
path =~ FILES_PATH_REGEX
|
||||
path.match?(FILES_PATH_REGEX)
|
||||
end
|
||||
|
||||
def frontend_request?
|
||||
strong_memoize(:frontend_request) do
|
||||
next false unless env.include?('HTTP_X_CSRF_TOKEN') && session.include?(:_csrf_token)
|
||||
|
||||
# CSRF tokens are not verified for GET/HEAD requests, so we pretend that we always have a POST request.
|
||||
Gitlab::RequestForgeryProtection.verified?(env.merge('REQUEST_METHOD' => 'POST'))
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_api_request?
|
||||
|
@ -195,7 +208,7 @@ module Gitlab
|
|||
with_projects = params['with_projects']
|
||||
with_projects = true if with_projects.blank?
|
||||
|
||||
path =~ GROUP_PATH_REGEX && Gitlab::Utils.to_boolean(with_projects)
|
||||
path.match?(GROUP_PATH_REGEX) && Gitlab::Utils.to_boolean(with_projects)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -368,6 +368,10 @@
|
|||
redis_slot: testing
|
||||
category: testing
|
||||
aggregation: weekly
|
||||
- name: users_clicking_license_testing_visiting_external_website
|
||||
redis_slot: testing
|
||||
category: testing
|
||||
aggregation: weekly
|
||||
# Container Security - Network Policies
|
||||
- name: clusters_using_network_policies_ui
|
||||
redis_slot: network_policies
|
||||
|
|
|
@ -2768,7 +2768,7 @@ msgstr ""
|
|||
msgid "AdminUsers|Delete user and contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Export permissions as CSV"
|
||||
msgid "AdminUsers|Export permissions as CSV (max 100,000 users)"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|External"
|
||||
|
|
|
@ -179,6 +179,30 @@ module QA
|
|||
QA::Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval, &block)
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::Base] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(self.class) && comparable == other.comparable
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Custom resource comparison logic using resource attributes from api_resource
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable
|
||||
raise("comparable method needs to be implemented in order to compare resources via '=='")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_value(name, block)
|
||||
|
|
|
@ -39,27 +39,12 @@ module QA
|
|||
# @return [String]
|
||||
def resource_web_url(_resource); end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::GroupBadge] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(GroupBadge) && comparable_badge == other.comparable_badge
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_badge)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing badges
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_badge
|
||||
def comparable
|
||||
reload! unless api_response
|
||||
|
||||
api_response.slice(
|
||||
|
|
|
@ -123,18 +123,12 @@ module QA
|
|||
end
|
||||
|
||||
# Object comparison
|
||||
# Override to make sure we are comparing descendands of GroupBase
|
||||
#
|
||||
# @param [QA::Resource::GroupBase] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(GroupBase) && comparable_group == other.comparable_group
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_group)
|
||||
other.is_a?(GroupBase) && comparable == other.comparable
|
||||
end
|
||||
|
||||
protected
|
||||
|
@ -142,7 +136,7 @@ module QA
|
|||
# Return subset of fields for comparing groups
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_group
|
||||
def comparable
|
||||
reload! if api_response.nil?
|
||||
|
||||
api_resource.slice(
|
||||
|
|
|
@ -56,27 +56,12 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::GroupMilestone] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(GroupMilestone) && comparable_milestone == other.comparable_milestone
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_milestone)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing milestones
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_milestone
|
||||
def comparable
|
||||
reload! unless api_response
|
||||
|
||||
api_response.slice(
|
||||
|
|
|
@ -95,27 +95,12 @@ module QA
|
|||
)
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::Issue] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Issue) && comparable_issue == other.comparable_issue
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_issue)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing issues
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_issue
|
||||
def comparable
|
||||
reload! if api_response.nil?
|
||||
|
||||
api_resource.slice(
|
||||
|
|
|
@ -49,27 +49,12 @@ module QA
|
|||
}
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::GroupBase] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(LabelBase) && comparable_label == other.comparable_label
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_label)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing labels
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_label
|
||||
def comparable
|
||||
reload! unless api_response
|
||||
|
||||
api_response.slice(
|
||||
|
|
|
@ -168,27 +168,12 @@ module QA
|
|||
)
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::MergeRequest] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(MergeRequest) && comparable_mr == other.comparable_mr
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_mr)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing merge requests
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_mr
|
||||
def comparable
|
||||
reload! if api_response.nil?
|
||||
|
||||
api_resource.except(
|
||||
|
|
|
@ -372,27 +372,12 @@ module QA
|
|||
api_post_to(api_wikis_path, title: title, content: content)
|
||||
end
|
||||
|
||||
# Object comparison
|
||||
#
|
||||
# @param [QA::Resource::Project] other
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
other.is_a?(Project) && comparable_project == other.comparable_project
|
||||
end
|
||||
|
||||
# Override inspect for a better rspec failure diff output
|
||||
#
|
||||
# @return [String]
|
||||
def inspect
|
||||
JSON.pretty_generate(comparable_project)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Return subset of fields for comparing projects
|
||||
#
|
||||
# @return [Hash]
|
||||
def comparable_project
|
||||
def comparable
|
||||
reload! if api_response.nil?
|
||||
|
||||
api_resource.slice(
|
||||
|
|
|
@ -181,6 +181,15 @@ module QA
|
|||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Compare users by username and password
|
||||
#
|
||||
# @return [Array]
|
||||
def comparable
|
||||
[username, password]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap_post_body
|
||||
|
|
|
@ -84,7 +84,8 @@ module QA
|
|||
retry_attempts: example.metadata[:retry_attempts] || 0,
|
||||
job_url: QA::Runtime::Env.ci_job_url,
|
||||
pipeline_url: env('CI_PIPELINE_URL'),
|
||||
pipeline_id: env('CI_PIPELINE_ID')
|
||||
pipeline_id: env('CI_PIPELINE_ID'),
|
||||
testcase: example.metadata[:testcase]
|
||||
}
|
||||
}
|
||||
rescue StandardError => e
|
||||
|
|
|
@ -56,13 +56,14 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
retry_attempts: 0,
|
||||
job_url: ci_job_url,
|
||||
pipeline_url: ci_pipeline_url,
|
||||
pipeline_id: ci_pipeline_id
|
||||
pipeline_id: ci_pipeline_id,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def run_spec(&spec)
|
||||
spec ||= -> { it('spec') {} }
|
||||
spec ||= -> { it('spec', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} }
|
||||
|
||||
describe_successfully('stats export', &spec).tap do |example_group|
|
||||
example_group.examples.each { |ex| ex.metadata[:file_path] = file_path }
|
||||
|
@ -131,7 +132,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
|
||||
it 'exports data to influxdb with correct reliable tag' do
|
||||
run_spec do
|
||||
it('spec', :reliable) {}
|
||||
it('spec', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||
end
|
||||
|
||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||
|
@ -143,7 +144,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
|
||||
it 'exports data to influxdb with correct quarantine tag' do
|
||||
run_spec do
|
||||
it('spec', :quarantine) {}
|
||||
it('spec', :quarantine, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||
end
|
||||
|
||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||
|
|
|
@ -3,15 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_sessions do
|
||||
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
|
||||
include SessionHelpers
|
||||
|
||||
context 'when session cookie is set' do
|
||||
before do
|
||||
Gitlab::Redis::Sessions.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
stub_session(session_hash)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
|
|
|
@ -94,13 +94,13 @@ describe('Todos', () => {
|
|||
});
|
||||
|
||||
it('updates pending text', () => {
|
||||
expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
|
||||
expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
|
||||
addDelimiter(TEST_COUNT_BIG),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates done text', () => {
|
||||
expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
|
||||
expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
|
||||
addDelimiter(TEST_DONE_COUNT_BIG),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
require "spec_helper"
|
||||
|
||||
RSpec.describe Admin::BackgroundMigrationsHelper do
|
||||
describe '#batched_migration_status_badge_class_name' do
|
||||
describe '#batched_migration_status_badge_variant' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:status, :class_name) do
|
||||
:active | 'badge-info'
|
||||
:paused | 'badge-warning'
|
||||
:failed | 'badge-danger'
|
||||
:finished | 'badge-success'
|
||||
where(:status, :variant) do
|
||||
:active | :info
|
||||
:paused | :warning
|
||||
:failed | :danger
|
||||
:finished | :success
|
||||
end
|
||||
|
||||
subject { helper.batched_migration_status_badge_class_name(migration) }
|
||||
subject { helper.batched_migration_status_badge_variant(migration) }
|
||||
|
||||
with_them do
|
||||
let(:migration) { build(:batched_background_migration, status: status) }
|
||||
|
||||
it { is_expected.to eq(class_name) }
|
||||
it { is_expected.to eq(variant) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -38,20 +38,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do
|
|||
expect(job.deployment.environment).to eq(job.persisted_environment)
|
||||
end
|
||||
|
||||
context 'when creation failure occures' do
|
||||
before do
|
||||
allow_next_instance_of(Deployment) do |deployment|
|
||||
allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
|
||||
end
|
||||
end
|
||||
|
||||
it 'trackes the exception' do
|
||||
expect { subject }.to raise_error(described_class::DeploymentCreationError)
|
||||
|
||||
expect(Deployment.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the corresponding environment does not exist' do
|
||||
let!(:environment) { }
|
||||
|
||||
|
|
|
@ -5,6 +5,19 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::RackAttack::Request do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:env) { {} }
|
||||
let(:session) { {} }
|
||||
let(:request) do
|
||||
::Rack::Attack::Request.new(
|
||||
env.reverse_merge(
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'PATH_INFO' => path,
|
||||
'rack.input' => StringIO.new,
|
||||
'rack.session' => session
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
describe 'FILES_PATH_REGEX' do
|
||||
subject { described_class::FILES_PATH_REGEX }
|
||||
|
||||
|
@ -16,11 +29,63 @@ RSpec.describe Gitlab::RackAttack::Request do
|
|||
it { is_expected.not_to match('/api/v4/projects/some/nested/repo/repository/files/README') }
|
||||
end
|
||||
|
||||
describe '#deprecated_api_request?' do
|
||||
let(:env) { { 'REQUEST_METHOD' => 'GET', 'rack.input' => StringIO.new, 'PATH_INFO' => path, 'QUERY_STRING' => query } }
|
||||
let(:request) { ::Rack::Attack::Request.new(env) }
|
||||
describe '#api_request?' do
|
||||
subject { request.api_request? }
|
||||
|
||||
subject { !!request.__send__(:deprecated_api_request?) }
|
||||
where(:path, :env, :expected) do
|
||||
'/' | {} | false
|
||||
'/groups' | {} | false
|
||||
|
||||
'/api' | {} | true
|
||||
'/api/v4/groups/1' | {} | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#web_request?' do
|
||||
subject { request.web_request? }
|
||||
|
||||
where(:path, :env, :expected) do
|
||||
'/' | {} | true
|
||||
'/groups' | {} | true
|
||||
|
||||
'/api' | {} | false
|
||||
'/api/v4/groups/1' | {} | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#frontend_request?', :allow_forgery_protection do
|
||||
subject { request.send(:frontend_request?) }
|
||||
|
||||
let(:path) { '/' }
|
||||
|
||||
# Define these as local variables so we can use them in the `where` block.
|
||||
valid_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
|
||||
other_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
|
||||
|
||||
where(:session, :env, :expected) do
|
||||
{} | {} | false # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
||||
{} | { 'HTTP_X_CSRF_TOKEN' => valid_token } | false
|
||||
{ _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => other_token } | false
|
||||
{ _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => valid_token } | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#deprecated_api_request?' do
|
||||
subject { request.send(:deprecated_api_request?) }
|
||||
|
||||
let(:env) { { 'QUERY_STRING' => query } }
|
||||
|
||||
where(:path, :query, :expected) do
|
||||
'/' | '' | false
|
||||
|
|
|
@ -2064,6 +2064,31 @@ RSpec.describe Ci::Build do
|
|||
end
|
||||
|
||||
describe 'build auto retry feature' do
|
||||
context 'with deployment job' do
|
||||
let(:build) do
|
||||
create(:ci_build, :deploy_to_production, :with_deployment,
|
||||
user: user, pipeline: pipeline, project: project)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
allow(build).to receive(:auto_retry_allowed?) { true }
|
||||
end
|
||||
|
||||
it 'creates a deployment when a build is dropped' do
|
||||
expect { build.drop!(:script_failure) }.to change { Deployment.count }.by(1)
|
||||
|
||||
retried_deployment = Deployment.last
|
||||
expect(build.deployment.environment).to eq(retried_deployment.environment)
|
||||
expect(build.deployment.ref).to eq(retried_deployment.ref)
|
||||
expect(build.deployment.sha).to eq(retried_deployment.sha)
|
||||
expect(build.deployment.tag).to eq(retried_deployment.tag)
|
||||
expect(build.deployment.user).to eq(retried_deployment.user)
|
||||
expect(build.deployment).to be_failed
|
||||
expect(retried_deployment).to be_created
|
||||
end
|
||||
end
|
||||
|
||||
describe '#retries_count' do
|
||||
subject { create(:ci_build, name: 'test', pipeline: pipeline) }
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'mime/types'
|
|||
|
||||
RSpec.describe API::Commits do
|
||||
include ProjectForksHelper
|
||||
include SessionHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
|
||||
|
@ -378,14 +379,7 @@ RSpec.describe API::Commits do
|
|||
|
||||
context 'when using warden' do
|
||||
it 'increments usage counters', :clean_gitlab_redis_sessions do
|
||||
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
|
||||
session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
|
||||
|
||||
Gitlab::Redis::Sessions.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
stub_session('warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]])
|
||||
|
||||
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
|
||||
include RackAttackSpecHelpers
|
||||
include SessionHelpers
|
||||
|
||||
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
||||
|
||||
|
@ -63,6 +64,22 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
|
|||
end
|
||||
end
|
||||
|
||||
describe 'API requests from the frontend', :api, :clean_gitlab_redis_sessions do
|
||||
context 'when unauthenticated' do
|
||||
it_behaves_like 'rate-limited frontend API requests' do
|
||||
let(:throttle_setting_prefix) { 'throttle_unauthenticated' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it_behaves_like 'rate-limited frontend API requests' do
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token) }
|
||||
|
||||
let(:throttle_setting_prefix) { 'throttle_authenticated' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'API requests authenticated with personal access token', :api do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:token) { create(:personal_access_token, user: user) }
|
||||
|
|
82
spec/services/deployments/create_for_build_service_spec.rb
Normal file
82
spec/services/deployments/create_for_build_service_spec.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Deployments::CreateForBuildService do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:service) { described_class.new }
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute(build) }
|
||||
|
||||
context 'with a deployment job' do
|
||||
let!(:build) { create(:ci_build, :start_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||
|
||||
it 'creates a deployment record' do
|
||||
expect { subject }.to change { Deployment.count }.by(1)
|
||||
|
||||
build.reset
|
||||
expect(build.deployment.project).to eq(build.project)
|
||||
expect(build.deployment.ref).to eq(build.ref)
|
||||
expect(build.deployment.sha).to eq(build.sha)
|
||||
expect(build.deployment.deployable).to eq(build)
|
||||
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
||||
expect(build.deployment.environment).to eq(build.persisted_environment)
|
||||
end
|
||||
|
||||
context 'when creation failure occures' do
|
||||
before do
|
||||
allow(build).to receive(:create_deployment!) { raise ActiveRecord::RecordInvalid }
|
||||
end
|
||||
|
||||
it 'trackes the exception' do
|
||||
expect { subject }.to raise_error(described_class::DeploymentCreationError)
|
||||
|
||||
expect(Deployment.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the corresponding environment does not exist' do
|
||||
let!(:environment) { }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a teardown job' do
|
||||
let!(:build) { create(:ci_build, :stop_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, name: build.expanded_environment_name) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a normal job' do
|
||||
let!(:build) { create(:ci_build, project: project) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a bridge' do
|
||||
let!(:build) { create(:ci_bridge, project: project) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,14 +26,14 @@ module RackAttackSpecHelpers
|
|||
{ 'AUTHORIZATION' => "Basic #{encoded_login}" }
|
||||
end
|
||||
|
||||
def expect_rejection(&block)
|
||||
def expect_rejection(name = nil, &block)
|
||||
yield
|
||||
|
||||
expect(response).to have_gitlab_http_status(:too_many_requests)
|
||||
|
||||
expect(response.headers.to_h).to include(
|
||||
'RateLimit-Limit' => a_string_matching(/^\d+$/),
|
||||
'RateLimit-Name' => a_string_matching(/^throttle_.*$/),
|
||||
'RateLimit-Name' => name || a_string_matching(/^throttle_.*$/),
|
||||
'RateLimit-Observed' => a_string_matching(/^\d+$/),
|
||||
'RateLimit-Remaining' => a_string_matching(/^\d+$/),
|
||||
'Retry-After' => a_string_matching(/^\d+$/)
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SessionHelpers
|
||||
# Stub a session in Redis, for use in request specs where we can't mock the session directly.
|
||||
# This also needs the :clean_gitlab_redis_sessions tag on the spec.
|
||||
def stub_session(session_hash)
|
||||
unless RSpec.current_example.metadata[:clean_gitlab_redis_sessions]
|
||||
raise 'Add :clean_gitlab_redis_sessions to your spec!'
|
||||
end
|
||||
|
||||
session_id = Rack::Session::SessionId.new(SecureRandom.hex)
|
||||
|
||||
Gitlab::Redis::Sessions.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
end
|
||||
|
||||
def expect_single_session_with_authenticated_ttl
|
||||
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
|
||||
end
|
||||
|
|
|
@ -10,6 +10,8 @@ RSpec.shared_examples 'when the snippet is not found' do
|
|||
end
|
||||
|
||||
RSpec.shared_examples 'snippet edit usage data counters' do
|
||||
include SessionHelpers
|
||||
|
||||
context 'when user is sessionless' do
|
||||
it 'does not track usage data actions' do
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
|
||||
|
@ -20,14 +22,7 @@ RSpec.shared_examples 'snippet edit usage data counters' do
|
|||
|
||||
context 'when user is not sessionless', :clean_gitlab_redis_sessions do
|
||||
before do
|
||||
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
|
||||
session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
|
||||
|
||||
Gitlab::Redis::Sessions.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
stub_session('warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]])
|
||||
end
|
||||
|
||||
it 'tracks usage data actions', :clean_gitlab_redis_sessions do
|
||||
|
|
|
@ -580,3 +580,88 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Requires let variables:
|
||||
# * throttle_setting_prefix: "throttle_authenticated", "throttle_unauthenticated"
|
||||
RSpec.shared_examples 'rate-limited frontend API requests' do
|
||||
let(:requests_per_period) { 1 }
|
||||
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
|
||||
let(:csrf_session) { { _csrf_token: csrf_token } }
|
||||
let(:personal_access_token) { nil }
|
||||
|
||||
let(:api_path) { '/projects' }
|
||||
|
||||
# These don't actually exist, so a 404 is the expected response.
|
||||
let(:files_api_path) { '/projects/1/repository/files/ref/path' }
|
||||
let(:packages_api_path) { '/projects/1/packages/foo' }
|
||||
let(:deprecated_api_path) { '/groups/1?with_projects=true' }
|
||||
|
||||
def get_api(path: api_path, csrf: false)
|
||||
headers = csrf ? { 'X-CSRF-Token' => csrf_token } : nil
|
||||
get api(path, personal_access_token: personal_access_token), headers: headers
|
||||
end
|
||||
|
||||
def expect_not_found(&block)
|
||||
yield
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_application_setting(
|
||||
"#{throttle_setting_prefix}_enabled" => true,
|
||||
"#{throttle_setting_prefix}_requests_per_period" => requests_per_period,
|
||||
"#{throttle_setting_prefix}_api_enabled" => true,
|
||||
"#{throttle_setting_prefix}_api_requests_per_period" => requests_per_period,
|
||||
"#{throttle_setting_prefix}_web_enabled" => true,
|
||||
"#{throttle_setting_prefix}_web_requests_per_period" => requests_per_period,
|
||||
"#{throttle_setting_prefix}_files_api_enabled" => true,
|
||||
"#{throttle_setting_prefix}_packages_api_enabled" => true,
|
||||
"#{throttle_setting_prefix}_deprecated_api_enabled" => true
|
||||
)
|
||||
|
||||
stub_session(csrf_session)
|
||||
end
|
||||
|
||||
context 'with a CSRF token' do
|
||||
it 'uses the rate limit for web requests' do
|
||||
requests_per_period.times { get_api csrf: true }
|
||||
|
||||
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true }
|
||||
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: files_api_path }
|
||||
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: packages_api_path }
|
||||
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: deprecated_api_path }
|
||||
|
||||
# API rate limit is not triggered yet
|
||||
expect_ok { get_api }
|
||||
expect_not_found { get_api path: files_api_path }
|
||||
expect_not_found { get_api path: packages_api_path }
|
||||
expect_not_found { get_api path: deprecated_api_path }
|
||||
end
|
||||
|
||||
context 'without a CSRF session' do
|
||||
let(:csrf_session) { nil }
|
||||
|
||||
it 'always uses the rate limit for API requests' do
|
||||
requests_per_period.times { get_api csrf: true }
|
||||
|
||||
expect_rejection("#{throttle_setting_prefix}_api") { get_api csrf: true }
|
||||
expect_rejection("#{throttle_setting_prefix}_api") { get_api }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a CSRF token' do
|
||||
it 'uses the rate limit for API requests' do
|
||||
requests_per_period.times { get_api }
|
||||
|
||||
expect_rejection("#{throttle_setting_prefix}_api") { get_api }
|
||||
|
||||
# Web and custom API rate limits are not triggered yet
|
||||
expect_ok { get_api csrf: true }
|
||||
expect_not_found { get_api path: files_api_path }
|
||||
expect_not_found { get_api path: packages_api_path }
|
||||
expect_not_found { get_api path: deprecated_api_path }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue