Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-22 09:13:51 +00:00
parent 589ee0e419
commit 8bf2e2b73e
61 changed files with 604 additions and 348 deletions

View 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),
});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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