Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-19 12:12:07 +00:00
parent 811f549164
commit 6b19945915
71 changed files with 1106 additions and 429 deletions

View File

@ -14,6 +14,7 @@
GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITLAB_ADMIN_USERNAME: "root"
GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITLAB_QA_ADMIN_ACCESS_TOKEN: "${REVIEW_APPS_ROOT_TOKEN}"
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
SIGNUP_DISABLED: "true"

View File

@ -54,6 +54,7 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
this.isGettingRef = false;
this.refCancelToken = null;
this.mergeRequestCreated = false;
this.refDebounce = debounce((value, target) => this.getRef(value, target), 500);
this.refIsValid = true;
@ -101,9 +102,18 @@ export default class CreateMergeRequestDropdown {
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.branchInput.addEventListener('input', this.onChangeInput.bind(this));
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
// Detect for example when user pastes ref using the mouse
this.refInput.addEventListener('input', this.onChangeInput.bind(this));
// Detect for example when user presses right arrow to apply the suggested ref
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
// Detect when user clicks inside the input to apply the suggested ref
this.refInput.addEventListener('click', this.onChangeInput.bind(this));
// Detect when user clicks outside the input to apply the suggested ref
this.refInput.addEventListener('blur', this.onChangeInput.bind(this));
// Detect when user presses tab to apply the suggested ref
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
@ -247,8 +257,12 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
this.refCancelToken = axios.CancelToken.source();
return axios
.get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
.get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, {
cancelToken: this.refCancelToken.token,
})
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@ -267,7 +281,10 @@ export default class CreateMergeRequestDropdown {
return this.updateInputState(target, ref, result);
})
.catch(() => {
.catch((thrown) => {
if (axios.isCancel(thrown)) {
return false;
}
this.unavailable();
this.disable();
createFlash({
@ -325,14 +342,23 @@ export default class CreateMergeRequestDropdown {
let target;
let value;
// User changed input, cancel to prevent previous request from interfering
if (this.refCancelToken !== null) {
this.refCancelToken.cancel();
}
if (event.target === this.branchInput) {
target = 'branch';
({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
value =
event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd);
if (event.target === document.activeElement) {
value =
event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd);
} else {
value = event.target.value;
}
} else {
return false;
}
@ -358,6 +384,7 @@ export default class CreateMergeRequestDropdown {
this.enable();
this.showAvailableMessage(target);
this.refDebounce(value, target);
return true;
}
@ -414,7 +441,8 @@ export default class CreateMergeRequestDropdown {
if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
event.preventDefault();
window.getSelection().removeAllRanges();
const caretPositionEnd = this.refInput.value.length;
this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd);
}
removeMessage(target) {

View File

@ -392,8 +392,6 @@ export default {
diffsApp.instrument();
},
created() {
this.mergeRequestContainers = document.querySelectorAll('.merge-request-container');
this.adjustView();
this.subscribeToEvents();
@ -521,13 +519,6 @@ export default {
} else {
this.removeEventListeners();
}
if (!this.isFluidLayout && this.glFeatures.mrChangesFluidLayout) {
this.mergeRequestContainers.forEach((el) => {
el.classList.toggle('limit-container-width', !this.shouldShow);
el.classList.toggle('container-limited', !this.shouldShow);
});
}
},
setEventListeners() {
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));

View File

@ -1,42 +1,58 @@
import $ from 'jquery';
import eventHub from '~/projects/new/event_hub';
function setVisibilityOptions(namespaceSelector) {
if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
return;
}
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
// Values are from lib/gitlab/visibility_level.rb
const visibilityLevel = {
private: 0,
internal: 10,
public: 20,
};
function setVisibilityOptions({ name, visibility, showPath, editPath }) {
document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? optionInput.value : 0;
const optionTitle = option.querySelector('.option-title');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
// Don't change anything if the option is restricted by admin
if (option.classList.contains('restricted')) {
return;
}
// don't change anything if the option is restricted by admin
if (!option.classList.contains('restricted')) {
if (visibilityLevel < optionValue) {
option.classList.add('disabled');
optionInput.disabled = true;
const reason = option.querySelector('.option-disabled-reason');
if (reason) {
reason.innerHTML = `This project cannot be ${optionName} because the visibility of
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0;
if (visibilityLevel[visibility] < optionValue) {
option.classList.add('disabled');
optionInput.disabled = true;
const reason = option.querySelector('.option-disabled-reason');
if (reason) {
const optionTitle = option.querySelector('.option-title');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
reason.innerHTML = `This project cannot be ${optionName} because the visibility of
<a href="${showPath}">${name}</a> is ${visibility}. To make this project
${optionName}, you must first <a href="${editPath}">change the visibility</a>
of the parent group.`;
}
} else {
option.classList.remove('disabled');
optionInput.disabled = false;
}
} else {
option.classList.remove('disabled');
optionInput.disabled = false;
}
});
}
function handleSelect2DropdownChange(namespaceSelector) {
if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
return;
}
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
setVisibilityOptions(selectedNamespace.dataset);
}
export default function initProjectVisibilitySelector() {
eventHub.$on('update-visibility', setVisibilityOptions);
const namespaceSelector = document.querySelector('select.js-select-namespace');
if (namespaceSelector) {
$('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector));
setVisibilityOptions(namespaceSelector);
$('.select2.js-select-namespace').on('change', () =>
handleSelect2DropdownChange(namespaceSelector),
);
handleSelect2DropdownChange(namespaceSelector);
}
}

View File

@ -6,9 +6,9 @@ import {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
@ -24,7 +24,6 @@ export default {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
},
mixins: [Tracking.mixin()],
@ -103,6 +102,15 @@ export default {
focusInput() {
this.$refs.search.focusInput();
},
handleDropdownItemClick(namespace) {
eventHub.$emit('update-visibility', {
name: namespace.name,
visibility: namespace.visibility,
showPath: namespace.webUrl,
editPath: joinPaths(namespace.webUrl, '-', 'edit'),
});
this.setNamespace(namespace);
},
handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
@ -134,23 +142,23 @@ export default {
<gl-search-box-by-type
ref="search"
v-model.trim="search"
:is-loading="$apollo.queries.currentUser.loading"
data-qa-selector="select_namespace_dropdown_search_field"
/>
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<template v-if="!$apollo.queries.currentUser.loading">
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group of filteredGroups"
:key="group.id"
@click="setNamespace(group)"
@click="handleDropdownItemClick(group)"
>
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="setNamespace(userNamespace)">
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
@ -158,6 +166,11 @@ export default {
</template>
</gl-dropdown>
<input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
<input
id="project_namespace_id"
type="hidden"
name="project[namespace_id]"
:value="selectedNamespace.id"
/>
</gl-button-group>
</template>

View File

@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) {
nodes {
id
fullPath
name
visibility
webUrl
}
}
namespace {

View File

@ -69,8 +69,7 @@ export default {
if (isScopedLabel(candidateLabel)) {
const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
const currentActiveScopedLabel = state.labels.find(
({ set, title }) =>
set && title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
);
if (currentActiveScopedLabel) {

View File

@ -1,9 +1,5 @@
<script>
export default {
name: 'WorkItemRoot',
};
</script>
<template>
<div></div>
<div>
<router-view />
</div>
</template>

View File

@ -0,0 +1,3 @@
export const widgetTypes = {
title: 'TITLE',
};

View File

@ -0,0 +1 @@
{"__schema":{"types":[{"kind":"INTERFACE","name":"WorkItemWidget","possibleTypes":[{"name":"TitleWidget"}]}]}}

View File

@ -0,0 +1,54 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import workItemQuery from './work_item.query.graphql';
import introspectionQueryResultData from './fragmentTypes.json';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
export function createApolloProvider() {
Vue.use(VueApollo);
const defaultClient = createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
assumeImmutableResults: true,
},
);
defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: '1',
},
data: {
workItem: {
__typename: 'WorkItem',
id: '1',
type: 'FEATURE',
widgets: {
__typename: 'WorkItemWidgetConnection',
nodes: [
{
__typename: 'TitleWidget',
type: 'TITLE',
enabled: true,
// eslint-disable-next-line @gitlab/require-i18n-strings
contentText: 'Test',
},
],
},
},
},
});
return new VueApollo({
defaultClient,
});
}

View File

@ -0,0 +1,38 @@
enum WorkItemType {
FEATURE
}
enum WidgetType {
TITLE
}
interface WorkItemWidget {
type: WidgetType!
}
# Replicating Relay connection type for client schema
type WorkItemWidgetEdge {
cursor: String!
node: WorkItemWidget
}
type WorkItemWidgetConnection {
edges: [WorkItemWidgetEdge]
nodes: [WorkItemWidget]
pageInfo: PageInfo!
}
type TitleWidget implements WorkItemWidget {
type: WidgetType!
contentText: String!
}
type WorkItem {
id: ID!
type: WorkItemType!
widgets: [WorkItemWidgetConnection]
}
extend type Query {
workItem(id: ID!): WorkItem!
}

View File

@ -0,0 +1,3 @@
fragment WidgetBase on WorkItemWidget {
type
}

View File

@ -0,0 +1,16 @@
#import './widget.fragment.graphql'
query WorkItem($id: ID!) {
workItem(id: $id) @client {
id
type
widgets {
nodes {
...WidgetBase
... on TitleWidget {
contentText
}
}
}
}
}

View File

@ -1,11 +1,15 @@
import Vue from 'vue';
import App from './components/app.vue';
import { createRouter } from './router';
import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
return new Vue({
el,
router: createRouter(el.dataset.fullPath),
apolloProvider: createApolloProvider(),
render(createElement) {
return createElement(App);
},

View File

@ -0,0 +1,44 @@
<script>
import workItemQuery from '../graphql/work_item.query.graphql';
import { widgetTypes } from '../constants';
export default {
props: {
id: {
type: String,
required: true,
},
},
data() {
return {
workItem: null,
};
},
apollo: {
workItem: {
query: workItemQuery,
variables() {
return {
id: this.id,
};
},
},
},
computed: {
titleWidgetData() {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
},
},
};
</script>
<template>
<section>
<!-- Title widget placeholder -->
<div>
<h2 v-if="titleWidgetData" class="title" data-testid="title">
{{ titleWidgetData.contentText }}
</h2>
</div>
</section>
</template>

View File

@ -0,0 +1,14 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import { joinPaths } from '~/lib/utils/url_utility';
import { routes } from './routes';
Vue.use(VueRouter);
export function createRouter(fullPath) {
return new VueRouter({
routes,
mode: 'history',
base: joinPaths(fullPath, '-', 'work_items'),
});
}

View File

@ -0,0 +1,8 @@
export const routes = [
{
path: '/:id',
name: 'work_item',
component: () => import('../pages/work_item_root.vue'),
props: true,
},
];

View File

@ -163,7 +163,8 @@ class ApplicationController < ActionController::Base
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
payload[:metadata] = @current_context
payload[:request_urgency] = urgency&.name
payload[:target_duration_s] = urgency&.duration
logged_user = auth_user
if logged_user.present?
payload[:user_id] = logged_user.try(:id)

View File

@ -38,6 +38,6 @@ module WorkhorseAuthorization
end
def file_extension_whitelist
ImportExportUploader::EXTENSION_WHITELIST
ImportExportUploader::EXTENSION_ALLOWLIST
end
end

View File

@ -780,6 +780,10 @@ module Ci
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
def variables_builder
@variables_builder ||= ::Gitlab::Ci::Variables::Builder.new(self)
end
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless persisted?
@ -1254,6 +1258,12 @@ module Ci
self.builds.latest.build_matchers(project)
end
def predefined_vars_in_builder_enabled?
strong_memoize(:predefined_vars_in_builder_enabled) do
Feature.enabled?(:ci_predefined_vars_in_builder, project, default_enabled: :yaml)
end
end
private
def add_message(severity, content)

View File

@ -10,8 +10,10 @@ module Ci
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name, dependencies: true)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
track_duration do
variables = pipeline.variables_builder.scoped_variables(self, environment: environment, dependencies: dependencies)
variables.concat(predefined_variables) unless pipeline.predefined_vars_in_builder_enabled?
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runnable? && runner
@ -25,9 +27,23 @@ module Ci
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables
end
end
def track_duration
start_time = ::Gitlab::Metrics::System.monotonic_time
result = yield
duration = ::Gitlab::Metrics::System.monotonic_time - start_time
::Gitlab::Ci::Pipeline::Metrics
.pipeline_builder_scoped_variables_histogram
.observe({}, duration.seconds)
result
end
##
# Variables that do not depend on the environment name.
#

View File

@ -2,6 +2,6 @@
module BulkImports
class ExportUploader < ImportExportUploader
EXTENSION_WHITELIST = %w[ndjson.gz].freeze
EXTENSION_ALLOWLIST = %w[ndjson.gz].freeze
end
end

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true
class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[tar.gz gz].freeze
EXTENSION_ALLOWLIST = %w[tar.gz gz].freeze
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def extension_whitelist
EXTENSION_WHITELIST
EXTENSION_ALLOWLIST
end
def move_to_cache

View File

@ -1,3 +1,3 @@
- page_title s_('WorkItem|Work Items')
#js-work-items
#js-work-items{ data: { full_path: @project.full_path } }

View File

@ -10,6 +10,7 @@
= visibility_level_label(level)
.option-description
= visibility_level_description(level, form_model)
.option-disabled-reason
.text-muted
- if all_visibility_levels_restricted?

View File

@ -0,0 +1,8 @@
---
name: ci_predefined_vars_in_builder
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72348
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/231300
milestone: '14.4'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -19,7 +19,10 @@ Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint
# adding :line has some overhead because a regexp on the backtrace has
# to be run on every SQL query. Only enable this in development because
# we've seen it slow things down.
Marginalia::Comment.components << :line if Rails.env.development?
if Rails.env.development?
Marginalia::Comment.components << :line
Marginalia::Comment.lines_to_ignore = Regexp.union(Gitlab::BacktraceCleaner::IGNORE_BACKTRACES + %w(lib/ruby/gems/ lib/gem_extensions/ lib/ruby/))
end
Gitlab::Marginalia.set_application_name

View File

@ -358,7 +358,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'details', on: :member
end
resources :work_items, only: [:index]
get 'work_items/*work_items_path' => 'work_items#index', as: :work_items
resource :tracing, only: [:show]

View File

@ -41,6 +41,7 @@ The following metrics are available:
| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | `controller`, `action` |
| `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | |
| `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller or action | `controller`, `action`, `operation` |
| `gitlab_ci_pipeline_builder_scoped_variables_duration` | Histogram | 14.5 | Time in seconds it takes to create the scoped variables for a CI/CD job
| `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | |
| `gitlab_ci_pipeline_size_builds` | Histogram | 13.1 | Total number of builds within a pipeline grouped by a pipeline source | `source` |
| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |

View File

@ -4042,6 +4042,7 @@ Input type: `ScanExecutionPolicyCommitInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationscanexecutionpolicycommitclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationscanexecutionpolicycommitname"></a>`name` | [`String`](#string) | Name of the policy. If the name is null, the `name` field from `policy_yaml` is used. |
| <a id="mutationscanexecutionpolicycommitoperationmode"></a>`operationMode` | [`MutationOperationMode!`](#mutationoperationmode) | Changes the operation mode. |
| <a id="mutationscanexecutionpolicycommitpolicyyaml"></a>`policyYaml` | [`String!`](#string) | YAML snippet of the policy. |
| <a id="mutationscanexecutionpolicycommitprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. |

View File

@ -94,9 +94,9 @@ Parameters:
| `restrict_to_branch` | string | false | Comma-separated list of branches to be are automatically inspected. Leave blank to include all branches. |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Asana integration
### Disable Asana integration
Delete Asana integration for a project.
Disable the Asana integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/asana
@ -130,9 +130,9 @@ Parameters:
| `subdomain` | string | false | The subdomain setting |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Assembla integration
### Disable Assembla integration
Delete Assembla integration for a project.
Disable the Assembla integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/assembla
@ -170,9 +170,9 @@ Parameters:
| `password` | string | true | Password of the user |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Atlassian Bamboo CI integration
### Disable Atlassian Bamboo CI integration
Delete Atlassian Bamboo CI integration for a project.
Disable the Atlassian Bamboo CI integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/bamboo
@ -209,9 +209,9 @@ Parameters:
| `title` | string | false | Title |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Bugzilla integration
### Disable Bugzilla integration
Delete Bugzilla integration for a project.
Disable the Bugzilla integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/bugzilla
@ -246,9 +246,9 @@ Parameters:
| `enable_ssl_verification` | boolean | false | DEPRECATED: This parameter has no effect since SSL verification is always enabled |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Buildkite integration
### Disable Buildkite integration
Delete Buildkite integration for a project.
Disable the Buildkite integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/buildkite
@ -284,9 +284,9 @@ Parameters:
| `room` | string | false | Campfire room. The last part of the URL when you're in a room. |
| `push_events` | boolean | false | Enable notifications for push events. |
### Delete Campfire integration
### Disable Campfire integration
Delete Campfire integration for a project.
Disable the Campfire integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/campfire
@ -322,9 +322,9 @@ Parameters:
| `datadog_service` | string | false | Name of this GitLab instance that all data will be tagged with |
| `datadog_env` | string | false | The environment tag that traces will be tagged with |
### Delete Datadog integration
### Disable Datadog integration
Delete Datadog integration for a project.
Disable the Datadog integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/datadog
@ -367,9 +367,9 @@ Parameters:
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Unify Circuit integration
### Disable Unify Circuit integration
Delete Unify Circuit integration for a project.
Disable the Unify Circuit integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/unify-circuit
@ -412,9 +412,9 @@ Parameters:
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Webex Teams integration
### Disable Webex Teams integration
Delete Webex Teams integration for a project.
Disable the Webex Teams integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/webex-teams
@ -451,9 +451,9 @@ Parameters:
| `title` | string | false | Title |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Custom Issue Tracker integration
### Disable Custom Issue Tracker integration
Delete Custom Issue Tracker integration for a project.
Disable the Custom Issue Tracker integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/custom-issue-tracker
@ -485,9 +485,9 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `webhook` | string | true | Discord webhook. For example, `https://discord.com/api/webhooks/…` |
### Delete Discord integration
### Disable Discord integration
Delete Discord integration for a project.
Disable the Discord integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/discord
@ -524,9 +524,9 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
### Delete Drone CI integration
### Disable Drone CI integration
Delete Drone CI integration for a project.
Disable the Drone CI integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/drone-ci
@ -563,9 +563,9 @@ Parameters:
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected". Notifications are always fired for tag pushes. The default value is "all" |
### Delete Emails on Push integration
### Disable Emails on Push integration
Delete Emails on Push integration for a project.
Disable the Emails on Push integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/emails-on-push
@ -599,9 +599,9 @@ Parameters:
| `project_url` | string | true | The URL to the project in EWM |
| `issues_url` | string | true | The URL to view an issue in EWM. Must contain `:id` |
### Delete EWM integration
### Disable EWM integration
Delete EWM integration for a project.
Disable the EWM integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/ewm
@ -635,9 +635,9 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `confluence_url` | string | true | The URL of the Confluence Cloud Workspace hosted on atlassian.net. |
### Delete Confluence integration
### Disable Confluence integration
Delete Confluence integration for a project.
Disable the Confluence integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/confluence
@ -669,9 +669,9 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `external_wiki_url` | string | true | The URL of the external wiki |
### Delete External wiki integration
### Disable External wiki integration
Delete External wiki integration for a project.
Disable the External wiki integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/external-wiki
@ -706,9 +706,9 @@ Parameters:
| `token` | string | true | Flowdock Git source token |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Flowdock integration
### Disable Flowdock integration
Delete Flowdock integration for a project.
Disable the Flowdock integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/flowdock
@ -742,9 +742,9 @@ Parameters:
| `repository_url` | string | true | GitHub repository URL |
| `static_context` | boolean | false | Append instance name instead of branch to [status check name](../user/project/integrations/github.md#static--dynamic-status-check-names) |
### Delete GitHub integration
### Disable GitHub integration
Delete GitHub integration for a project.
Disable the GitHub integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/github
@ -788,9 +788,9 @@ Parameters:
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Hangouts Chat integration
### Disable Hangouts Chat integration
Delete Hangouts Chat integration for a project.
Disable the Hangouts Chat integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/hangouts-chat
@ -829,9 +829,9 @@ Parameters:
| `colorize_messages` | boolean | false | Colorize messages |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Irker (IRC gateway) integration
### Disable Irker (IRC gateway) integration
Delete Irker (IRC gateway) integration for a project.
Disable the Irker (IRC gateway) integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/irker
@ -880,9 +880,9 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) |
### Delete Jira integration
### Disable Jira integration
Remove all previously Jira integrations from a project.
Disable the Jira integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/jira
@ -939,9 +939,9 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Slack token |
### Delete Slack Slash Command integration
### Disable Slack Slash Command integration
Delete Slack Slash Command integration for a project.
Disable the Slack Slash Command integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/slack-slash-commands
@ -974,9 +974,9 @@ Parameters:
| `token` | string | yes | The Mattermost token |
| `username` | string | no | The username to use to post the message |
### Delete Mattermost Slash Command integration
### Disable Mattermost Slash Command integration
Delete Mattermost Slash Command integration for a project.
Disable the Mattermost Slash Command integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/mattermost-slash-commands
@ -1005,9 +1005,9 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
### Delete Packagist integration
### Disable Packagist integration
Delete Packagist integration for a project.
Disable the Packagist integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/packagist
@ -1044,9 +1044,9 @@ Parameters:
| `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28271)) |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
### Delete Pipeline-Emails integration
### Disable Pipeline-Emails integration
Delete Pipeline-Emails integration for a project.
Disable the Pipeline-Emails integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/pipelines-email
@ -1082,9 +1082,9 @@ Parameters:
| `restrict_to_branch` | boolean | false | Comma-separated list of branches to automatically inspect. Leave blank to include all branches. |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Pivotal Tracker integration
### Disable Pivotal Tracker integration
Delete Pivotal Tracker integration for a project.
Disable the Pivotal Tracker integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/pivotaltracker
@ -1118,9 +1118,9 @@ Parameters:
| `google_iap_audience_client_id` | string | false | Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com) |
| `google_iap_service_account_json` | string | false | `credentials.json` file for your service account, like { "type": "service_account", "project_id": ... } |
### Delete Prometheus integration
### Disable Prometheus integration
Delete Prometheus integration for a project.
Disable the Prometheus integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/prometheus
@ -1157,9 +1157,9 @@ Parameters:
| `sound` | string | false | The sound of the notification |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Pushover integration
### Disable Pushover integration
Delete Pushover integration for a project.
Disable the Pushover integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/pushover
@ -1195,9 +1195,9 @@ Parameters:
| `description` | string | false | Description |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Redmine integration
### Disable Redmine integration
Delete Redmine integration for a project.
Disable the Redmine integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/redmine
@ -1256,9 +1256,9 @@ Parameters:
| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Slack integration
### Disable Slack integration
Delete Slack integration for a project.
Disable the Slack integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/slack
@ -1302,9 +1302,9 @@ Parameters:
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Microsoft Teams integration
### Disable Microsoft Teams integration
Delete Microsoft Teams integration for a project.
Disable the Microsoft Teams integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/microsoft-teams
@ -1359,9 +1359,9 @@ Parameters:
| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
### Delete Mattermost notifications integration
### Disable Mattermost notifications integration
Delete Mattermost notifications integration for a project.
Disable the Mattermost notifications integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/mattermost
@ -1399,9 +1399,9 @@ Parameters:
| `password` | string | true | The password of the user |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete JetBrains TeamCity CI integration
### Disable JetBrains TeamCity CI integration
Delete JetBrains TeamCity CI integration for a project.
Disable the JetBrains TeamCity CI integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/teamcity
@ -1439,9 +1439,9 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events. |
| `tag_push_events` | boolean | false | Enable notifications for tag push events. |
### Delete Jenkins CI integration
### Disable Jenkins CI integration
Delete Jenkins CI integration for a project.
Disable the Jenkins CI integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/jenkins
@ -1476,9 +1476,9 @@ Parameters:
- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
- `pass_unstable` (optional) - Unstable builds are treated as passing
### Delete Jenkins CI (Deprecated) integration
### Disable Jenkins CI (Deprecated) integration
Delete Jenkins CI (Deprecated) integration for a project.
Disable the Jenkins CI (Deprecated) integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/jenkins-deprecated
@ -1512,9 +1512,9 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `mock_service_url` | string | true | `http://localhost:4004` |
### Delete MockCI integration
### Disable MockCI integration
Delete MockCI integration for a project.
Disable the MockCI integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/mock-ci
@ -1549,9 +1549,9 @@ Parameters:
| `description` | string | false | Description |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete YouTrack integration
### Disable YouTrack integration
Delete YouTrack integration for a project.
Disable the YouTrack integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/youtrack

View File

@ -27,7 +27,8 @@ module API
Gitlab::GrapeLogging::Loggers::PerfLogger.new,
Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new,
Gitlab::GrapeLogging::Loggers::ContextLogger.new,
Gitlab::GrapeLogging::Loggers::ContentLogger.new
Gitlab::GrapeLogging::Loggers::ContentLogger.new,
Gitlab::GrapeLogging::Loggers::UrgencyLogger.new
]
allow_access_with_scope :api

View File

@ -5,7 +5,7 @@ module API
module FileUploadHelpers
def file_is_valid?
filename = params[:file]&.original_filename
filename && ImportExportUploader::EXTENSION_WHITELIST.include?(File.extname(filename).delete('.'))
filename && ImportExportUploader::EXTENSION_ALLOWLIST.include?(File.extname(filename).delete('.'))
end
def validate_file!

View File

@ -51,6 +51,15 @@ module Gitlab
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
def self.pipeline_builder_scoped_variables_histogram
name = :gitlab_ci_pipeline_builder_scoped_variables_duration
comment = 'Pipeline variables builder scoped_variables duration'
labels = {}
buckets = [0.01, 0.05, 0.1, 0.3, 0.5, 1, 2, 5, 10, 30, 60, 120]
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
def self.pipeline_processing_events_counter
name = :gitlab_ci_pipeline_processing_events_total
comment = 'Total amount of pipeline processing events'

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Variables
class Builder
include ::Gitlab::Utils::StrongMemoize
def initialize(pipeline)
@pipeline = pipeline
end
def scoped_variables(job, environment:, dependencies:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables(job)) if pipeline.predefined_vars_in_builder_enabled?
end
end
private
attr_reader :pipeline
def predefined_variables(job)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_JOB_NAME', value: job.name)
variables.append(key: 'CI_JOB_STAGE', value: job.stage)
variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action?
variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request
variables.append(key: 'CI_NODE_INDEX', value: job.options[:instance].to_s) if job.options&.include?(:instance)
variables.append(key: 'CI_NODE_TOTAL', value: ci_node_total_value(job).to_s)
# legacy variables
variables.append(key: 'CI_BUILD_NAME', value: job.name)
variables.append(key: 'CI_BUILD_STAGE', value: job.stage)
variables.append(key: 'CI_BUILD_TRIGGERED', value: 'true') if job.trigger_request
variables.append(key: 'CI_BUILD_MANUAL', value: 'true') if job.action?
end
end
def ci_node_total_value(job)
parallel = job.options&.dig(:parallel)
parallel = parallel.dig(:total) if parallel.is_a?(Hash)
parallel || 1
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module GrapeLogging
module Loggers
class UrgencyLogger < ::GrapeLogging::Loggers::Base
def parameters(request, _)
endpoint = request.env['api.endpoint']
return {} unless endpoint
urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
return {} unless urgency
{ request_urgency: urgency.name, target_duration_s: urgency.duration }
end
end
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class CacheCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_cache_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::Cache.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class QueuesCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_queues_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::Queues.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class RateLimitingCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_rate_limiting_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::RateLimiting.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module HealthChecks
module Redis
module RedisAbstractCheck
include SimpleAbstractCheck
def check_up
successful?(check)
end
private
def redis_instance_class_name
Gitlab::Redis.const_get(redis_instance_name.camelize, false)
end
def metric_prefix
"redis_#{redis_instance_name}_ping"
end
def redis_instance_name
name.sub(/_check$/, '')
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
redis_instance_class_name.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end

View File

@ -14,16 +14,22 @@ module Gitlab
end
def successful?(result)
result == 'PONG'
result == true
end
def check
::Gitlab::HealthChecks::Redis::CacheCheck.check_up &&
::Gitlab::HealthChecks::Redis::QueuesCheck.check_up &&
::Gitlab::HealthChecks::Redis::SharedStateCheck.check_up &&
::Gitlab::HealthChecks::Redis::TraceChunksCheck.check_up &&
::Gitlab::HealthChecks::Redis::RateLimitingCheck.check_up &&
::Gitlab::HealthChecks::Redis::SessionsCheck.check_up
redis_health_checks.all?(&:check_up)
end
def redis_health_checks
[
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::Redis::TraceChunksCheck,
Gitlab::HealthChecks::Redis::RateLimitingCheck,
Gitlab::HealthChecks::Redis::SessionsCheck
]
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class SessionsCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_sessions_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::Sessions.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class SharedStateCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_shared_state_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::SharedState.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -4,31 +4,7 @@ module Gitlab
module HealthChecks
module Redis
class TraceChunksCheck
extend SimpleAbstractCheck
class << self
def check_up
check
end
private
def metric_prefix
'redis_trace_chunks_ping'
end
def successful?(result)
result == 'PONG'
end
# rubocop: disable CodeReuse/ActiveRecord
def check
catch_timeout 10.seconds do
Gitlab::Redis::TraceChunks.with(&:ping)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
extend RedisAbstractCheck
end
end
end

View File

@ -56,10 +56,20 @@ module Gitlab
end
def download(url, upload_path)
File.open(upload_path, 'w') do |file|
# Download (stream) file from the uploader's location
IO.copy_stream(URI.parse(url).open, file)
File.open(upload_path, 'wb') do |file|
Gitlab::HTTP.get(url, stream_body: true) do |fragment|
if [301, 302, 307].include?(fragment.code)
Gitlab::Import::Logger.warn(message: "received redirect fragment", fragment_code: fragment.code)
elsif fragment.code == 200
file.write(fragment)
else
raise Gitlab::ImportExport::Error, "unsupported response downloading fragment #{fragment.code}"
end
end
end
rescue StandardError => e
@shared.error(e) # rubocop:disable Gitlab/ModuleWithInstanceVariables
raise e
end
def tar_with_options(archive:, dir:, options:)

View File

@ -7,6 +7,8 @@ module Gitlab
LIMITED_ARRAY_SENTINEL = { key: 'truncated', value: '...' }.freeze
IGNORE_PARAMS = Set.new(%w(controller action format)).freeze
KNOWN_PAYLOAD_PARAMS = [:remote_ip, :user_id, :username, :ua, :queue_duration_s,
:etag_route, :request_urgency, :target_duration_s] + CLOUDFLARE_CUSTOM_HEADERS.values
def self.call(event)
params = event
@ -14,24 +16,17 @@ module Gitlab
.each_with_object([]) { |(k, v), array| array << { key: k, value: v } unless IGNORE_PARAMS.include?(k) }
payload = {
time: Time.now.utc.iso8601(3),
params: Gitlab::Utils::LogLimitedArray.log_limited_array(params, sentinel: LIMITED_ARRAY_SENTINEL),
remote_ip: event.payload[:remote_ip],
user_id: event.payload[:user_id],
username: event.payload[:username],
ua: event.payload[:ua]
params: Gitlab::Utils::LogLimitedArray.log_limited_array(params, sentinel: LIMITED_ARRAY_SENTINEL)
}
payload.merge!(event.payload[:metadata]) if event.payload[:metadata]
optional_payload_params = event.payload.slice(*KNOWN_PAYLOAD_PARAMS).compact
payload.merge!(optional_payload_params)
::Gitlab::InstrumentationHelper.add_instrumentation_data(payload)
payload[:queue_duration_s] = event.payload[:queue_duration_s] if event.payload[:queue_duration_s]
payload[:etag_route] = event.payload[:etag_route] if event.payload[:etag_route]
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = event.payload[Labkit::Correlation::CorrelationId::LOG_KEY] || Labkit::Correlation::CorrelationId.current_id
CLOUDFLARE_CUSTOM_HEADERS.each do |_, value|
payload[value] = event.payload[value] if event.payload[value]
end
# https://github.com/roidrage/lograge#logging-errors--exceptions
exception = event.payload[:exception_object]

View File

@ -30,10 +30,12 @@ module Gitlab
endpoint_id = API::Base.endpoint_id_for_route(route)
route_class = route.app.options[:for]
feature_category = route_class.feature_category_for_app(route.app)
request_urgency = route_class.urgency_for_app(route.app)
{
endpoint_id: endpoint_id,
feature_category: feature_category
feature_category: feature_category,
request_urgency: request_urgency.name
}
end
end
@ -42,7 +44,8 @@ module Gitlab
Gitlab::RequestEndpoints.all_controller_actions.map do |controller, action|
{
endpoint_id: controller.endpoint_id_for_action(action),
feature_category: controller.feature_category_for_action(action)
feature_category: controller.feature_category_for_action(action),
request_urgency: controller.urgency_for_action(action).name
}
end
end

View File

@ -116,9 +116,11 @@ module Gitlab
def record_apdex_if_needed(env, elapsed)
return unless Gitlab::Metrics::RailsSlis.request_apdex_counters_enabled?
urgency = urgency_for_env(env)
Gitlab::Metrics::RailsSlis.request_apdex.increment(
labels: labels_from_context,
success: satisfactory?(env, elapsed)
labels: labels_from_context.merge(request_urgency: urgency.name),
success: elapsed < urgency.duration
)
end
@ -129,17 +131,15 @@ module Gitlab
}
end
def satisfactory?(env, elapsed)
target =
def urgency_for_env(env)
endpoint_urgency =
if env['api.endpoint'].present?
env['api.endpoint'].options[:for].try(:urgency_for_app, env['api.endpoint'])
elsif env['action_controller.instance'].present? && env['action_controller.instance'].respond_to?(:urgency)
env['action_controller.instance'].urgency
end
target ||= Gitlab::EndpointAttributes::DEFAULT_URGENCY
elapsed < target.duration
endpoint_urgency || Gitlab::EndpointAttributes::DEFAULT_URGENCY
end
end
end

View File

@ -81,7 +81,7 @@ module QA
result = yield.tap do
fabrication_time = Time.now - start
Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time * 1000)
Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time)
Runtime::Logger.debug do
msg = ["==#{'=' * parents.size}>"]
msg << "Built a #{name}"

View File

@ -27,7 +27,9 @@ module QA
:import_error
attribute :group do
Group.fabricate!
Group.fabricate! do |group|
group.api_client = api_client
end
end
attribute :path_with_namespace do

View File

@ -104,7 +104,10 @@ module QA
source_issue # fabricate source group, project, issue
end
it 'successfully imports issue' do
it(
'successfully imports issue',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2325'
) do
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
aggregate_failures do

View File

@ -501,11 +501,16 @@ RSpec.describe ApplicationController do
describe '#append_info_to_payload' do
controller(described_class) do
attr_reader :last_payload
urgency :high, [:foo]
def index
render html: 'authenticated'
end
def foo
render html: ''
end
def append_info_to_payload(payload)
super
@ -513,6 +518,13 @@ RSpec.describe ApplicationController do
end
end
before do
routes.draw do
get 'index' => 'anonymous#index'
get 'foo' => 'anonymous#foo'
end
end
it 'does not log errors with a 200 response' do
get :index
@ -534,6 +546,22 @@ RSpec.describe ApplicationController do
expect(controller.last_payload[:metadata]).to include('meta.user' => user.username)
end
context 'urgency information' do
it 'adds default urgency information to the payload' do
get :index
expect(controller.last_payload[:request_urgency]).to eq(:default)
expect(controller.last_payload[:target_duration_s]).to eq(1)
end
it 'adds customized urgency information to the payload' do
get :foo
expect(controller.last_payload[:request_urgency]).to eq(:high)
expect(controller.last_payload[:target_duration_s]).to eq(0.25)
end
end
end
describe '#access_denied' do

View File

@ -46,7 +46,10 @@ describe('CreateMergeRequestDropdown', () => {
dropdown
.getRef('contains#hash')
.then(() => {
expect(axios.get).toHaveBeenCalledWith(endpoint);
expect(axios.get).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({ cancelToken: expect.anything() }),
);
})
.then(done)
.catch(done.fail);

View File

@ -702,23 +702,4 @@ describe('diffs/components/app', () => {
);
});
});
describe('fluid layout', () => {
beforeEach(() => {
setFixtures(
'<div><div class="merge-request-container limit-container-width container-limited"></div></div>',
);
});
it('removes limited container classes when on diffs tab', () => {
createComponent({ isFluidLayout: false, shouldShow: true }, () => {}, {
glFeatures: { mrChangesFluidLayout: true },
});
const containerClassList = document.querySelector('.merge-request-container').classList;
expect(containerClassList).not.toContain('container-limited');
expect(containerClassList).not.toContain('limit-container-width');
});
});
});

View File

@ -24,14 +24,23 @@ describe('NewProjectUrlSelect component', () => {
{
id: 'gid://gitlab/Group/26',
fullPath: 'flightjs',
name: 'Flight JS',
visibility: 'public',
webUrl: 'http://127.0.0.1:3000/flightjs',
},
{
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
name: 'H5BP',
visibility: 'public',
webUrl: 'http://127.0.0.1:3000/h5bp',
},
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
name: 'H5BP Subgroup',
visibility: 'private',
webUrl: 'http://127.0.0.1:3000/h5bp/subgroup',
},
],
},
@ -79,6 +88,10 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick();
};
afterEach(() => {
wrapper.destroy();
@ -127,7 +140,6 @@ describe('NewProjectUrlSelect component', () => {
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@ -140,7 +152,6 @@ describe('NewProjectUrlSelect component', () => {
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@ -160,7 +171,6 @@ describe('NewProjectUrlSelect component', () => {
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@ -195,23 +205,38 @@ describe('NewProjectUrlSelect component', () => {
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.find('li').text()).toBe('No matches found');
});
it('updates hidden input with selected namespace', async () => {
it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
const spy = jest.spyOn(eventHub, '$emit');
await clickDropdownItem();
const namespace = data.currentUser.groups.nodes[0];
expect(spy).toHaveBeenCalledWith('update-visibility', {
name: namespace.name,
visibility: namespace.visibility,
showPath: namespace.webUrl,
editPath: `${namespace.webUrl}/-/edit`,
});
});
it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
await clickDropdownItem();
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),

View File

@ -159,9 +159,8 @@ describe('LabelsSelect Mutations', () => {
labels = [
{ id: 1, title: 'scoped' },
{ id: 2, title: 'scoped::one', set: false },
{ id: 3, title: 'scoped::two', set: false },
{ id: 4, title: 'scoped::three', set: true },
{ id: 5, title: '' },
{ id: 3, title: 'scoped::test', set: true },
{ id: 4, title: '' },
];
});
@ -192,9 +191,8 @@ describe('LabelsSelect Mutations', () => {
expect(state.labels).toEqual([
{ id: 1, title: 'scoped' },
{ id: 2, title: 'scoped::one', set: true, touched: true },
{ id: 3, title: 'scoped::two', set: false },
{ id: 4, title: 'scoped::three', set: false },
{ id: 5, title: '' },
{ id: 3, title: 'scoped::test', set: false },
{ id: 4, title: '' },
]);
});
});

View File

@ -0,0 +1,24 @@
import { shallowMount } from '@vue/test-utils';
import App from '~/work_items/components/app.vue';
describe('Work Items Application', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(App, {
stubs: {
'router-view': true,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders a component', () => {
createComponent();
expect(wrapper.exists()).toBe(true);
});
});

View File

@ -0,0 +1,17 @@
export const workItemQueryResponse = {
workItem: {
__typename: 'WorkItem',
id: '1',
type: 'FEATURE',
widgets: {
__typename: 'WorkItemWidgetConnection',
nodes: [
{
__typename: 'TitleWidget',
type: 'TITLE',
contentText: 'Test',
},
],
},
},
};

View File

@ -0,0 +1,70 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { workItemQueryResponse } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
const WORK_ITEM_ID = '1';
describe('Work items root component', () => {
let wrapper;
let fakeApollo;
const findTitle = () => wrapper.find('[data-testid="title"]');
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
fakeApollo = createMockApollo();
fakeApollo.clients.defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: WORK_ITEM_ID,
},
data: queryResponse,
});
wrapper = shallowMount(WorkItemsRoot, {
propsData: {
id: WORK_ITEM_ID,
},
localVue,
apolloProvider: fakeApollo,
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('renders the title if title is in the widgets list', () => {
createComponent();
expect(findTitle().exists()).toBe(true);
expect(findTitle().text()).toBe('Test');
});
it('does not render the title if title is not in the widgets list', () => {
const queryResponse = {
workItem: {
...workItemQueryResponse.workItem,
widgets: {
__typename: 'WorkItemWidgetConnection',
nodes: [
{
__typename: 'SomeOtherWidget',
type: 'OTHER',
contentText: 'Test',
},
],
},
},
};
createComponent({ queryResponse });
expect(findTitle().exists()).toBe(false);
});
});

View File

@ -0,0 +1,30 @@
import { mount } from '@vue/test-utils';
import App from '~/work_items/components/app.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
describe('Work items router', () => {
let wrapper;
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
if (routeArg !== undefined) {
await router.push(routeArg);
}
wrapper = mount(App, {
router,
});
};
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
});
it('renders work item on `/1` route', async () => {
await createComponent('/1');
expect(wrapper.find(WorkItemsRoot).exists()).toBe(true);
});
});

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Builder do
let(:builder) { described_class.new(pipeline) }
let(:pipeline) { create(:ci_pipeline) }
let(:job) { create(:ci_build, pipeline: pipeline) }
describe '#scoped_variables' do
let(:environment) { job.expanded_environment_name }
let(:dependencies) { true }
subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) }
it 'returns the expected variables' do
keys = %w[CI_JOB_NAME
CI_JOB_STAGE
CI_NODE_TOTAL
CI_BUILD_NAME
CI_BUILD_STAGE]
subject.map { |env| env[:key] }.tap do |names|
expect(names).to include(*keys)
end
end
context 'feature flag disabled' do
before do
stub_feature_flags(ci_predefined_vars_in_builder: false)
end
it 'returns no variables' do
expect(subject.map { |env| env[:key] }).to be_empty
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GrapeLogging::Loggers::UrgencyLogger do
def endpoint(options, namespace: '')
Struct.new(:options, :namespace).new(options, namespace)
end
let(:api_class) do
Class.new(API::Base) do
namespace 'testing' do
# rubocop:disable Rails/HttpPositionalArguments
# This is not the get that performs a request, but the one from Grape
get 'test', urgency: :high do
{}
end
# rubocop:enable Rails/HttpPositionalArguments
end
end
end
describe ".parameters" do
where(:request_env, :expected_parameters) do
[
[{}, {}],
[{ 'api.endpoint' => endpoint({}) }, {}],
[{ 'api.endpoint' => endpoint({ for: 'something weird' }) }, {}],
[
{ 'api.endpoint' => endpoint({ for: api_class, path: [] }) },
{ request_urgency: :default, target_duration_s: 1 }
],
[
{ 'api.endpoint' => endpoint({ for: api_class, path: ['test'] }, namespace: '/testing') },
{ request_urgency: :high, target_duration_s: 0.25 }
]
]
end
with_them do
let(:request) { double('request', env: request_env) }
subject { described_class.new.parameters(request, nil) }
it { is_expected.to eq(expected_parameters) }
end
end
end

View File

@ -4,5 +4,5 @@ require 'spec_helper'
require_relative '../simple_check_shared'
RSpec.describe Gitlab::HealthChecks::Redis::RedisCheck do
include_examples 'simple_check', 'redis_ping', 'Redis', 'PONG'
include_examples 'simple_check', 'redis_ping', 'Redis', true
end

View File

@ -17,6 +17,10 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
def initialize
@shared = Gitlab::ImportExport::Shared.new(nil)
end
def download(url, upload_path)
super(url, upload_path)
end
end.new
end
@ -101,4 +105,44 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end
end
end
describe '#download' do
before do
stub_request(:get, loc)
.to_return(
status: 200,
body: content
)
end
context 'a non-localhost uri' do
let(:loc) { 'https://gitlab.com' }
let(:content) { File.open('spec/fixtures/rails_sample.tif') }
it 'gets the contents' do
Tempfile.create("foo") do |f|
subject.download(loc, f.path)
expect(f.read).to eq(File.open('spec/fixtures/rails_sample.tif').read)
end
end
it 'streams the contents' do
expect(Gitlab::HTTP).to receive(:get).with(loc, hash_including(stream_body: true))
Tempfile.create("foo") do |f|
subject.download(loc, f.path)
end
end
end
context 'a localhost uri' do
let(:loc) { 'https://localhost:8081/foo/bar' }
let(:content) { 'foo' }
it 'throws a blocked url error' do
Tempfile.create("foo") do |f|
expect { subject.download(loc, f.path) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
end
end
end
end
end

View File

@ -19,7 +19,13 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
user_id: 'test',
cf_ray: SecureRandom.hex,
cf_request_id: SecureRandom.hex,
metadata: { 'meta.user' => 'jane.doe' }
metadata: { 'meta.user' => 'jane.doe' },
request_urgency: :default,
target_duration_s: 1,
remote_ip: '192.168.1.2',
ua: 'Nyxt',
queue_duration_s: 0.2,
etag_route: '/etag'
}
end
@ -66,6 +72,18 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
end
end
context 'trusted payload' do
it { is_expected.to include(event_payload.slice(*described_class::KNOWN_PAYLOAD_PARAMS)) }
context 'payload with rejected fields' do
let(:event_payload) { { params: {}, request_urgency: :high, something: 'random', username: nil } }
it { is_expected.to include({ request_urgency: :high }) }
it { is_expected.not_to include({ something: 'random' }) }
it { is_expected.not_to include({ username: nil }) }
end
end
context 'when correlation_id is overridden' do
let(:correlation_id_key) { Labkit::Correlation::CorrelationId::LOG_KEY }

View File

@ -17,11 +17,13 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
possible_labels = [
{
endpoint_id: "GET /api/:version/version",
feature_category: :not_owned
feature_category: :not_owned,
request_urgency: :default
},
{
endpoint_id: "ProjectsController#show",
feature_category: :projects
feature_category: :projects,
request_urgency: :default
}
]

View File

@ -36,7 +36,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it 'tracks request count and duration' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
subject.call(env)
end
@ -122,7 +123,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'issue_tracking')
expect(described_class).not_to receive(:http_health_requests_total)
expect(Gitlab::Metrics::RailsSlis.request_apdex)
.to receive(:increment).with(labels: { feature_category: 'issue_tracking', endpoint_id: 'IssuesController#show' }, success: true)
.to receive(:increment).with(labels: { feature_category: 'issue_tracking', endpoint_id: 'IssuesController#show', request_urgency: :default }, success: true)
subject.call(env)
end
@ -156,7 +157,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it 'sets the required labels to unknown' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
expect(described_class).not_to receive(:http_health_requests_total)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
subject.call(env)
end
@ -206,7 +208,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it "captures SLI metrics" do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'hello_world', endpoint_id: 'GET /projects/:id/archive' },
labels: {
feature_category: 'hello_world',
endpoint_id: 'GET /projects/:id/archive',
request_urgency: request_urgency_name
},
success: success
)
subject.call(env)
@ -235,7 +241,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it "captures SLI metrics" do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'hello_world', endpoint_id: 'AnonymousController#index' },
labels: {
feature_category: 'hello_world',
endpoint_id: 'AnonymousController#index',
request_urgency: request_urgency_name
},
success: success
)
subject.call(env)
@ -255,17 +265,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
let(:api_handler) { Class.new(::API::Base) }
it "falls back request's expectation to medium (1 second)" do
it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: false
)
subject.call(env)
@ -281,17 +299,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
{ 'action_controller.instance' => controller_instance, 'REQUEST_METHOD' => 'GET' }
end
it "falls back request's expectation to medium (1 second)" do
it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: false
)
subject.call(env)
@ -303,17 +329,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
{ 'REQUEST_METHOD' => 'GET' }
end
it "falls back request's expectation to medium (1 second)" do
it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown',
request_urgency: :default
},
success: false
)
subject.call(env)

View File

@ -2759,7 +2759,10 @@ RSpec.describe Ci::Build do
let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
allow(builder).to receive(:predefined_variables) { [build_pre_var] }
end
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow(build).to receive(:job_jwt_variables) { [job_jwt_var] }
@ -3411,75 +3414,122 @@ RSpec.describe Ci::Build do
end
describe '#scoped_variables' do
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline,
scheduling_type: :stage
)
end
before do
pipeline.clear_memoization(:predefined_vars_in_builder_enabled)
end
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
it 'records a prometheus metric' do
histogram = double(:histogram)
expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_builder_scoped_variables_histogram)
.and_return(histogram)
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
expect(histogram).to receive(:observe)
.with({}, a_kind_of(ActiveSupport::Duration))
build.scoped_variables
build.scoped_variables
end
expect(build).not_to be_persisted
end
it 'returns static predefined variables' do
keys = %w[CI_JOB_NAME
CI_COMMIT_SHA
CI_COMMIT_SHORT_SHA
CI_COMMIT_REF_NAME
CI_COMMIT_REF_SLUG
CI_JOB_STAGE]
variables = build.scoped_variables
variables.map { |env| env[:key] }.tap do |names|
expect(names).to include(*keys)
shared_examples 'calculates scoped_variables' do
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline,
scheduling_type: :stage
)
end
expect(variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
build.scoped_variables
expect(build).not_to be_persisted
end
it 'returns static predefined variables' do
keys = %w[CI_JOB_NAME
CI_COMMIT_SHA
CI_COMMIT_SHORT_SHA
CI_COMMIT_REF_NAME
CI_COMMIT_REF_SLUG
CI_JOB_STAGE]
variables = build.scoped_variables
variables.map { |env| env[:key] }.tap do |names|
expect(names).to include(*keys)
end
expect(variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
end
it 'does not return prohibited variables' do
keys = %w[CI_JOB_ID
CI_JOB_URL
CI_JOB_TOKEN
CI_BUILD_ID
CI_BUILD_TOKEN
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
CI_ENVIRONMENT_URL
CI_DEPLOY_USER
CI_DEPLOY_PASSWORD]
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
end
end
end
it 'does not return prohibited variables' do
keys = %w[CI_JOB_ID
CI_JOB_URL
CI_JOB_TOKEN
CI_BUILD_ID
CI_BUILD_TOKEN
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
CI_ENVIRONMENT_URL
CI_DEPLOY_USER
CI_DEPLOY_PASSWORD]
context 'with dependency variables' do
let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
it 'inherits dependent variables' do
expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
end
end
end
context 'with dependency variables' do
let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
it_behaves_like 'calculates scoped_variables'
let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
it 'delegates to the variable builders' do
expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
expect(builder)
.to receive(:scoped_variables).with(build, hash_including(:environment, :dependencies))
.and_call_original
it 'inherits dependent variables' do
expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
expect(builder).to receive(:predefined_variables).and_call_original
end
build.scoped_variables
end
context 'when ci builder feature flag is disabled' do
before do
stub_feature_flags(ci_predefined_vars_in_builder: false)
end
it 'does not delegate to the variable builders' do
expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
expect(builder).not_to receive(:predefined_variables)
end
build.scoped_variables
end
it_behaves_like 'calculates scoped_variables'
end
end