Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-04-08 21:09:50 +00:00
parent 80e9fdc968
commit 76358aee81
38 changed files with 1368 additions and 297 deletions

View file

@ -0,0 +1,96 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import csrf from '~/lib/utils/csrf';
import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
import DeleteCustomMetricModal from './delete_custom_metric_modal.vue';
import { formDataValidator } from '../constants';
export default {
components: {
CustomMetricsFormFields,
DeleteCustomMetricModal,
GlDeprecatedButton,
},
props: {
customMetricsPath: {
type: String,
required: false,
default: '',
},
metricPersisted: {
type: Boolean,
required: true,
},
editProjectServicePath: {
type: String,
required: true,
},
validateQueryPath: {
type: String,
required: true,
},
formData: {
type: Object,
required: true,
validator: formDataValidator,
},
},
data() {
return {
formIsValid: null,
errorMessage: '',
};
},
computed: {
saveButtonText() {
return this.metricPersisted ? __('Save Changes') : s__('Metrics|Create metric');
},
titleText() {
return this.metricPersisted ? s__('Metrics|Edit metric') : s__('Metrics|New metric');
},
},
created() {
this.csrf = csrf.token != null ? csrf.token : '';
this.formOperation = this.metricPersisted ? 'patch' : 'post';
},
methods: {
formValidation(isValid) {
this.formIsValid = isValid;
},
submit() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<div class="row my-3">
<h4 class="prepend-top-0 col-lg-8 offset-lg-2">{{ titleText }}</h4>
<form ref="form" class="col-lg-8 offset-lg-2" :action="customMetricsPath" method="post">
<custom-metrics-form-fields
:form-operation="formOperation"
:form-data="formData"
:metric-persisted="metricPersisted"
:validate-query-path="validateQueryPath"
@formValidation="formValidation"
/>
<div class="form-actions">
<gl-deprecated-button variant="success" :disabled="!formIsValid" @click="submit">
{{ saveButtonText }}
</gl-deprecated-button>
<gl-deprecated-button
variant="secondary"
class="float-right"
:href="editProjectServicePath"
>{{ __('Cancel') }}</gl-deprecated-button
>
<delete-custom-metric-modal
v-if="metricPersisted"
:delete-metric-url="customMetricsPath"
:csrf-token="csrf"
/>
</div>
</form>
</div>
</template>

View file

@ -0,0 +1,294 @@
<script>
import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import csrf from '~/lib/utils/csrf';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import { queryTypes, formDataValidator } from '../constants';
const VALIDATION_REQUEST_TIMEOUT = 10000;
const axiosCancelToken = axios.CancelToken;
let cancelTokenSource;
function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then(resp => {
if (resp.status === statusCodes.OK) {
stop(resp);
} else {
next();
}
})
// If the request is cancelled by axios
// then consider it as noop so that its not
// caught by subsequent catches
.catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
}, VALIDATION_REQUEST_TIMEOUT);
}
export default {
components: {
GlFormInput,
GlLink,
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
Icon,
},
props: {
formOperation: {
type: String,
required: true,
},
formData: {
type: Object,
required: false,
default: () => ({
title: '',
yLabel: '',
query: '',
unit: '',
group: '',
legend: '',
}),
validator: formDataValidator,
},
metricPersisted: {
type: Boolean,
required: false,
default: false,
},
validateQueryPath: {
type: String,
required: true,
},
},
data() {
const group = this.formData.group.length ? this.formData.group : queryTypes.business;
return {
queryIsValid: null,
queryValidateInFlight: false,
...this.formData,
group,
};
},
computed: {
formIsValid() {
return Boolean(
this.queryIsValid &&
this.title.length &&
this.yLabel.length &&
this.unit.length &&
this.group.length,
);
},
validQueryMsg() {
return this.queryIsValid ? s__('Metrics|PromQL query is valid') : '';
},
invalidQueryMsg() {
return !this.queryIsValid ? this.errorMessage : '';
},
},
watch: {
formIsValid(value) {
this.$emit('formValidation', value);
},
},
beforeMount() {
if (this.metricPersisted) {
this.validateQuery();
}
},
methods: {
requestValidation(query, cancelToken) {
return backOffRequest(() =>
axios.post(
this.validateQueryPath,
{
query,
},
{
cancelToken,
},
),
);
},
setFormState(isValid, inFlight, message) {
this.queryIsValid = isValid;
this.queryValidateInFlight = inFlight;
this.errorMessage = message;
},
validateQuery() {
if (!this.query) {
this.setFormState(null, false, '');
return;
}
this.setFormState(null, true, '');
// cancel previously dispatched backoff request
if (cancelTokenSource) {
cancelTokenSource.cancel();
}
// Creating a new token for each request because
// if a single token is used it can cancel existing requests
// as well.
cancelTokenSource = axiosCancelToken.source();
this.requestValidation(this.query, cancelTokenSource.token)
.then(res => {
const response = res.data;
const { valid, error } = response.query;
if (response.success) {
this.setFormState(valid, false, valid ? '' : error);
} else {
throw new Error(__('There was an error trying to validate your query'));
}
})
.catch(() => {
this.setFormState(
false,
false,
s__('Metrics|There was an error trying to validate your query'),
);
});
},
debouncedValidateQuery: debounce(function checkQuery() {
this.validateQuery();
}, 500),
},
csrfToken: csrf.token || '',
formGroupOptions: [
{ text: __('Business'), value: queryTypes.business },
{ text: __('Response'), value: queryTypes.response },
{ text: __('System'), value: queryTypes.system },
],
};
</script>
<template>
<div>
<input ref="method" type="hidden" name="_method" :value="formOperation" />
<input :value="$options.csrfToken" type="hidden" name="authenticity_token" />
<gl-form-group :label="__('Name')" label-for="prometheus_metric_title" label-class="label-bold">
<gl-form-input
id="prometheus_metric_title"
v-model="title"
name="prometheus_metric[title]"
class="form-control"
:placeholder="s__('Metrics|e.g. Throughput')"
data-qa-selector="custom_metric_prometheus_title_field"
required
/>
<span class="form-text text-muted">{{ s__('Metrics|Used as a title for the chart') }}</span>
</gl-form-group>
<gl-form-group :label="__('Type')" label-for="prometheus_metric_group" label-class="label-bold">
<gl-form-radio-group
id="metric-group"
v-model="group"
:options="$options.formGroupOptions"
:checked="group"
name="prometheus_metric[group]"
/>
<span class="form-text text-muted">{{ s__('Metrics|For grouping similar metrics') }}</span>
</gl-form-group>
<gl-form-group
:label="__('Query')"
label-for="prometheus_metric_query"
label-class="label-bold"
:state="queryIsValid"
>
<gl-form-input
id="prometheus_metric_query"
v-model.trim="query"
data-qa-selector="custom_metric_prometheus_query_field"
name="prometheus_metric[query]"
class="form-control"
:placeholder="s__('Metrics|e.g. rate(http_requests_total[5m])')"
required
:state="queryIsValid"
@input="debouncedValidateQuery"
/>
<span v-if="queryValidateInFlight" class="form-text text-muted">
<gl-loading-icon :inline="true" class="mr-1 align-middle" />
{{ s__('Metrics|Validating query') }}
</span>
<slot v-if="!queryValidateInFlight" name="valid-feedback">
<span class="form-text cgreen">
{{ validQueryMsg }}
</span>
</slot>
<slot v-if="!queryValidateInFlight" name="invalid-feedback">
<span class="form-text cred">
{{ invalidQueryMsg }}
</span>
</slot>
<span v-show="query.length === 0" class="form-text text-muted">
{{ s__('Metrics|Must be a valid PromQL query.') }}
<gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1">
{{ s__('Metrics|Prometheus Query Documentation') }}
<icon name="external-link" :size="12" />
</gl-link>
</span>
</gl-form-group>
<gl-form-group
:label="s__('Metrics|Y-axis label')"
label-for="prometheus_metric_y_label"
label-class="label-bold"
>
<gl-form-input
id="prometheus_metric_y_label"
v-model="yLabel"
data-qa-selector="custom_metric_prometheus_y_label_field"
name="prometheus_metric[y_label]"
class="form-control"
:placeholder="s__('Metrics|e.g. Requests/second')"
required
/>
<span class="form-text text-muted">
{{
s__('Metrics|Label of the y-axis (usually the unit). The x-axis always represents time.')
}}
</span>
</gl-form-group>
<gl-form-group
:label="s__('Metrics|Unit label')"
label-for="prometheus_metric_unit"
label-class="label-bold"
>
<gl-form-input
id="prometheus_metric_unit"
v-model="unit"
data-qa-selector="custom_metric_prometheus_unit_label_field"
name="prometheus_metric[unit]"
class="form-control"
:placeholder="s__('Metrics|e.g. req/sec')"
required
/>
</gl-form-group>
<gl-form-group
:label="s__('Metrics|Legend label (optional)')"
label-for="prometheus_metric_legend"
label-class="label-bold"
>
<gl-form-input
id="prometheus_metric_legend"
v-model="legend"
data-qa-selector="custom_metric_prometheus_legend_label_field"
name="prometheus_metric[legend]"
class="form-control"
:placeholder="s__('Metrics|e.g. HTTP requests')"
required
/>
<span class="form-text text-muted">
{{
s__(
'Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response.',
)
}}
</span>
</gl-form-group>
</div>
</template>

View file

@ -0,0 +1,54 @@
<script>
import { GlModal, GlModalDirective, GlDeprecatedButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlModal,
GlDeprecatedButton,
},
directives: {
'gl-modal': GlModalDirective,
},
props: {
deleteMetricUrl: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
},
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
},
descriptionText: s__(
`Metrics|You're about to permanently delete this metric. This cannot be undone.`,
),
modalId: 'delete-custom-metric-modal',
};
</script>
<template>
<div class="d-inline-block float-right mr-3">
<gl-deprecated-button v-gl-modal="$options.modalId" variant="danger">
{{ __('Delete') }}
</gl-deprecated-button>
<gl-modal
:title="s__('Metrics|Delete metric?')"
:ok-title="s__('Metrics|Delete metric')"
:modal-id="$options.modalId"
ok-variant="danger"
@ok="onSubmit"
>
{{ $options.descriptionText }}
<form ref="form" :action="deleteMetricUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
</form>
</gl-modal>
</div>
</template>

View file

@ -0,0 +1,12 @@
export const queryTypes = {
business: 'business',
response: 'response',
system: 'system',
};
export const formDataValidator = val => {
const fieldNames = Object.keys(val);
const requiredFields = ['title', 'query', 'yLabel', 'unit', 'group', 'legend'];
return requiredFields.every(name => fieldNames.includes(name));
};

View file

@ -0,0 +1,47 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import CustomMetricsForm from './components/custom_metrics_form.vue';
export default () => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-custom-metrics',
components: {
CustomMetricsForm,
},
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const {
customMetricsPath,
editProjectServicePath,
validateQueryPath,
title,
query,
yLabel,
unit,
group,
legend,
} = domEl.dataset;
let { metricPersisted } = domEl.dataset;
metricPersisted = parseBoolean(metricPersisted);
return createElement('custom-metrics-form', {
props: {
customMetricsPath,
metricPersisted,
editProjectServicePath,
validateQueryPath,
formData: {
title,
query,
yLabel,
unit,
group,
legend,
},
},
});
},
});
};

View file

@ -23,7 +23,7 @@ function getErrorMessage(res) {
return res.message;
}
export default function dropzoneInput(form) {
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
const $attachButton = form.find('.button-attach-file');
@ -69,6 +69,7 @@ export default function dropzoneInput(form) {
uploadMultiple: false,
headers: csrf.headers,
previewContainer: false,
...config,
processing: () => $('.div-dropzone-alert').alert('close'),
dragover: () => {
$mdArea.addClass('is-dropzone-hover');

View file

@ -45,7 +45,7 @@ export default class GLForm {
);
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
dropzoneInput(this.form, { parallelUploads: 1 });
autosize(this.textarea);
}
// form and textarea event listeners

View file

@ -109,3 +109,9 @@ export const initialStateKeys = [...endpointKeys, 'currentEnvironmentName'];
* Constant to indicate if a metric exists in the database
*/
export const NOT_IN_DB_PREFIX = 'NO_DB';
/**
* graphQL environments API value for active environments.
* Used as a value for the 'states' query filter
*/
export const ENVIRONMENT_AVAILABLE_STATE = 'available';

View file

@ -1,6 +1,6 @@
query getEnvironments($projectPath: ID!, $search: String) {
query getEnvironments($projectPath: ID!, $search: String, $states: [String!]) {
project(fullPath: $projectPath) {
data: environments(search: $search) {
data: environments(search: $search, states: $states) {
environments: nodes {
name
id

View file

@ -10,7 +10,7 @@ import statusCodes from '../../lib/utils/http_status';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT } from '../constants';
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@ -238,6 +238,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
variables: {
projectPath: removeLeadingSlash(state.projectPath),
search: state.environmentsSearchTerm,
states: [ENVIRONMENT_AVAILABLE_STATE],
},
})
.then(resp =>

View file

@ -0,0 +1,62 @@
<script>
import { GlLink } from '@gitlab/ui';
export default {
name: 'AccessibilityIssueBody',
components: {
GlLink,
},
props: {
issue: {
type: Object,
required: true,
},
isNew: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
parsedTECHSCode() {
/*
* In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
* or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
*
* The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
* Here we simply split the string on `.` and get the code in the 5th position
*/
if (this.issue.code === undefined) {
return null;
}
return this.issue.code.split('.')[4] || null;
},
learnMoreUrl() {
if (this.parsedTECHSCode === null) {
return 'https://www.w3.org/TR/WCAG20-TECHS/Overview.html';
}
return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode}.html`;
},
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
<div
v-if="isNew"
ref="accessibility-issue-is-new-badge"
class="badge badge-danger append-right-5"
>
{{ s__('AccessibilityReport|New') }}
</div>
{{ issue.name }}
<gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
s__('AccessibilityReport|Learn More')
}}</gl-link>
{{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
</div>
</div>
</template>

View file

@ -1,9 +1,12 @@
import TestIssueBody from './test_issue_body.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
export const components = {
AccessibilityIssueBody,
TestIssueBody,
};
export const componentNames = {
AccessibilityIssueBody: AccessibilityIssueBody.name,
TestIssueBody: TestIssueBody.name,
};

View file

@ -41,16 +41,6 @@ html [type="button"],
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $gl-text-color;
font-weight: 600;
}
h1,
.h1,
h2,

View file

@ -567,16 +567,6 @@ body {
font-weight: $gl-font-weight-bold;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $gl-text-color;
font-weight: $gl-font-weight-bold;
}
.light-header {
font-weight: $gl-font-weight-bold;
}

View file

@ -35,6 +35,8 @@ $h3-font-size: 14px * 1.75;
$h4-font-size: 14px * 1.5;
$h5-font-size: 14px * 1.25;
$h6-font-size: 14px;
$headings-color: $gl-text-color;
$headings-font-weight: $gl-font-weight-bold;
$spacer: $grid-size;
$spacers: (
0: 0,

View file

@ -10,7 +10,7 @@ module GroupsHelper
]
end
def group_nav_link_paths
def group_settings_nav_link_paths
%w[
groups#projects
groups#edit

View file

@ -14,4 +14,12 @@ class ImportExportUploader < AttachmentUploader
def move_to_cache
false
end
def work_dir
File.join(Settings.shared['path'], 'tmp', 'work')
end
def cache_dir
File.join(Settings.shared['path'], 'tmp', 'cache')
end
end

View file

@ -133,7 +133,7 @@
= _('Members')
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= nav_link(path: group_settings_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')

View file

@ -0,0 +1,5 @@
---
title: Show only active environments in monitoring dropdown
merge_request: 28456
author:
type: changed

View file

@ -0,0 +1,6 @@
---
title: Align color and font-weight styles of heading elements and their typography
classes
merge_request: 28422
author:
type: other

View file

@ -17,7 +17,7 @@ how GitLab can be scaled out and made highly available. These examples progress
from simple to complex as scaling or highly-available components are added.
For larger setups serving 2,000 or more users, we provide
[reference architectures](#reference-architectures) based on GitLab's
[reference architectures](../scaling/index.md#reference-architectures) based on GitLab's
experience with GitLab.com and internal scale testing that aim to achieve the
right balance of scalability and availability.
@ -93,147 +93,6 @@ them.
In some cases, components can be combined on the same nodes to reduce complexity as well.
## Recommended setups based on number of users
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [requirements page](../../install/requirements.md) for further details of the specs you will require.
- 1000 - 10000 Users: A scaled environment based on one of our [Reference Architectures](#reference-architectures), without the HA components applied. This can be a reasonable step towards a fully HA environment.
- 2000 - 50000+ Users: A scaled HA environment based on one of our [Reference Architectures](#reference-architectures) below.
## Reference architectures
In this section we'll detail the Reference Architectures that can support large numbers
of users. These were built, tested and verified by our Quality and Support teams.
Testing was done with our GitLab Performance Tool at specific coded workloads, and the
throughputs used for testing were calculated based on sample customer data. We
test each endpoint type with the following number of requests per second (RPS)
per 1000 users:
- API: 20 RPS
- Web: 2 RPS
- Git: 2 RPS
NOTE: **Note:** Note that depending on your workflow the below recommended
reference architectures may need to be adapted accordingly. Your workload
is influenced by factors such as - but not limited to - how active your users are,
how much automation you use, mirroring, and repo/change size. Additionally the
shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
On different cloud vendors a best effort like for like can be used.
### 2,000 user configuration
- **Supported users (approximate):** 2,000
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|-----------------------|---------------|--------------|
| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 5,000 user configuration
- **Supported users (approximate):** 5,000
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|---------------|--------------|
| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 10,000 user configuration
- **Supported users (approximate):** 10,000
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | GCP Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 25,000 user configuration
- **Supported users (approximate):** 25,000
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
### 50,000 user configuration
- **Supported users (approximate):** 50,000
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
and have its number of workers set to 90% of available CPUs along with 4 threads.

View file

@ -71,3 +71,190 @@ References:
- [Configure your NFS server to work with GitLab](../high_availability/nfs.md)
- [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip)
- [Setting up a Redis-only server](https://docs.gitlab.com/omnibus/settings/redis.html#setting-up-a-redis-only-server)
## Recommended setups based on number of users
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [requirements page](../../install/requirements.md) for further details of the specs you will require.
- 1000 - 10000 Users: A scaled environment based on one of our [Reference Architectures](#reference-architectures), without the HA components applied. This can be a reasonable step towards a fully HA environment.
- 2000 - 50000+ Users: A scaled HA environment based on one of our [Reference Architectures](#reference-architectures) below.
## Reference architectures
In this section we'll detail the Reference Architectures that can support large numbers
of users. These were built, tested and verified by our Quality and Support teams.
Testing was done with our GitLab Performance Tool at specific coded workloads, and the
throughputs used for testing were calculated based on sample customer data. We
test each endpoint type with the following number of requests per second (RPS)
per 1000 users:
- API: 20 RPS
- Web: 2 RPS
- Git: 2 RPS
NOTE: **Note:** Note that depending on your workflow the below recommended
reference architectures may need to be adapted accordingly. Your workload
is influenced by factors such as - but not limited to - how active your users are,
how much automation you use, mirroring, and repo/change size. Additionally the
shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
On different cloud vendors a best effort like for like can be used.
### 2,000 user configuration
- **Supported users (approximate):** 2,000
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|-----------------------|---------------|--------------|
| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 5,000 user configuration
- **Supported users (approximate):** 5,000
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|---------------|--------------|
| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 10,000 user configuration
- **Supported users (approximate):** 10,000
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | GCP Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
### 25,000 user configuration
- **Supported users (approximate):** 25,000
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
### 50,000 user configuration
- **Supported users (approximate):** 50,000
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
| ----------------------------|-------|------------------------|----------------|--------------|
| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge |
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| Cloud Object Storage[^4] | - | - | - | - |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
| Internal load balancing node[^6] | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
and have its number of workers set to 90% of available CPUs along with 4 threads.
[^2]: Gitaly node requirements are dependent on customer data, specifically the number of
projects and their sizes. We recommend 2 nodes as an absolute minimum for HA environments
and at least 4 nodes should be used when supporting 50,000 or more users.
We also recommend that each Gitaly node should store no more than 5TB of data
and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
set to 20% of available CPUs. Additional nodes should be considered in conjunction
with a review of expected data size and spread based on the recommendations above.
[^3]: Recommended Redis setup differs depending on the size of the architecture.
For smaller architectures (up to 5,000 users) we suggest one Redis cluster for all
classes and that Redis Sentinel is hosted alongside Consul.
For larger architectures (10,000 users or more) we suggest running a separate
[Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
and another for the Queues and Shared State classes respectively. We also recommend
that you run the Redis Sentinel clusters separately as well for each Redis Cluster.
[^4]: For data objects such as LFS, Uploads, Artifacts, etc. We recommend a [Cloud Object Storage service](../object_storage.md)
over NFS where possible, due to better performance and availability.
[^5]: NFS can be used as an alternative for both repository data (replacing Gitaly) and
object storage but this isn't typically recommended for performance reasons. Note however it is required for
[GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
[^6]: Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
as the load balancer. However other reputable load balancers with similar feature sets
should also work instead but be aware these aren't validated.
[^7]: We strongly recommend that any Gitaly and / or NFS nodes are set up with SSD disks over
HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
as these components have heavy I/O. These IOPS values are recommended only as a starter
as with time they may be adjusted higher or lower depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider
you may need to refer to their documentation on how configure IOPS correctly.
[^8]: The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform on GCP. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
[^9]: AWS-equivalent configurations are rough suggestions and may change in the
future. They have not yet been tested and validated.

View file

@ -715,8 +715,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_SERVER_VERSION_PATCH=0
++ export CI_SERVER_REVISION=f4cc00ae823
++ CI_SERVER_REVISION=f4cc00ae823
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,export_issues,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,export_issues,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ export CI_PROJECT_ID=17893
++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace

View file

@ -173,7 +173,7 @@ To see the status of your GitLab.com subscription, log into GitLab.com and go to
1. Go to **User Avatar > Settings**.
1. Click **Billing**.
- For groups:
1. From the group page (*not* from a project within the group), go to **Administration > Billing**.
1. From the group page (*not* from a project within the group), go to **Settings > Billing**.
The following table describes details of your subscription for groups:
@ -430,7 +430,7 @@ CI pipeline minutes are the execution time for your [pipelines](../ci/pipelines/
Quotas apply to:
- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group, then **{settings}** **Administration > Usage Quotas**.
- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group, then **{settings}** **Settings > Usage Quotas**.
- Your personal account, where the minutes are available for your personal projects. To view and buy personal minutes, click your avatar, then **{settings}** **Settings > Pipeline quota**.
Only pipeline minutes for GitLab shared runners are restricted. If you have a specific runner set up for your projects, there is no limit to your build time on GitLab.com.
@ -451,10 +451,10 @@ main quota. Additional minutes:
To purchase additional minutes for your group on GitLab.com:
1. From your group, go to **{settings}** **Administration > Usage Quotas**.
1. From your group, go to **{settings}** **Settings > Usage Quotas**.
1. Locate the subscription card that's linked to your group on GitLab.com, click **Buy more CI minutes**, and complete the details about the transaction.
1. Once we have processed your payment, the extra CI minutes will be synced to your group.
1. To confirm the available CI minutes, go to your group, then **{settings}** **Administration > Usage Quotas**.
1. To confirm the available CI minutes, go to your group, then **{settings}** **Settings > Usage Quotas**.
The **Additional minutes** displayed now includes the purchased additional CI minutes, plus any minutes rolled over from last month.
To purchase additional minutes for your personal namespace:

View file

@ -121,6 +121,8 @@ License Compliance can be configured using environment variables.
| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
| `SETUP_CMD` | no | Custom setup for the dependency installation. (experimental) |
| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Python projects). |
### Installing custom dependencies
@ -215,6 +217,37 @@ license_scanning:
LM_PYTHON_VERSION: 2
```
### Custom root certificates for Python
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
To bypass TLS verification, you can use a custom [`pip.conf`](https://pip.pypa.io/en/stable/user_guide/#config-file)
file to configure trusted hosts.
The following `gitlab-ci.yml` file uses a [`before_script`](../../../ci/yaml/README.md#before_script-and-after_script)
to inject a custom [`pip.conf`](https://pip.pypa.io/en/stable/user_guide/#config-file):
```yaml
include:
- template: License-Scanning.gitlab-ci.yml
license_scanning:
variables:
PIP_INDEX_URL: 'https://pypi.example.com/simple/'
before_script:
- mkdir -p ~/.config/pip/
- cp pip.conf ~/.config/pip/pip.conf
```
The [`pip.conf`](https://pip.pypa.io/en/stable/reference/pip/) allows you to specify a list of
[trusted hosts](https://pip.pypa.io/en/stable/reference/pip/#cmdoption-trusted-host):
```text
[global]
trusted-host = pypi.example.com
```
### Migration from `license_management` to `license_scanning`
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.

View file

@ -24,7 +24,7 @@ Note the following:
## Configuring your Identity Provider
1. Navigate to the group and click **Administration > SAML SSO**.
1. Navigate to the group and click **Settings > SAML SSO**.
1. Configure your SAML server using the **Assertion consumer service URL** and **Identifier**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [your identity provider's documentation](#providers) for more details.
1. Configure the SAML response to include a NameID that uniquely identifies each user.
1. Configure required assertions using the [table below](#assertions).
@ -116,7 +116,7 @@ This feature is similar to the [Credentials inventory for self-managed instances
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.
Groups with group-managed accounts can disallow forking of projects to destinations outside the group.
To do so, enable the "Prohibit outer forks" option in **Administration > SAML SSO**.
To do so, enable the "Prohibit outer forks" option in **Settings > SAML SSO**.
When enabled, projects within the group can only be forked to other destinations within the group (including its subgroups).
##### Other restrictions for Group-managed accounts
@ -146,7 +146,7 @@ assertions to be able to create a user.
GitLab provides metadata XML that can be used to configure your Identity Provider.
1. Navigate to the group and click **Administration > SAML SSO**.
1. Navigate to the group and click **Settings > SAML SSO**.
1. Copy the provided **GitLab metadata URL**.
1. Follow your Identity Provider's documentation and paste the metadata URL when it is requested.
@ -154,7 +154,7 @@ GitLab provides metadata XML that can be used to configure your Identity Provide
Once you've set up your identity provider to work with GitLab, you'll need to configure GitLab to use it for authentication:
1. Navigate to the group's **Administration > SAML SSO**.
1. Navigate to the group's **Settings > SAML SSO**.
1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign on URL** field.
1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
1. Click the **Enable SAML authentication for this group** toggle switch.
@ -288,14 +288,14 @@ If the information you need isn't listed above you may wish to check our [troubl
To link SAML to your existing GitLab.com account:
1. Sign in to your GitLab.com account.
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on the group's **Administration > SAML SSO** page.
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on the group's **Settings > SAML SSO** page.
1. Visit the SSO URL and click **Authorize**.
1. Enter your credentials on the Identity Provider if prompted.
1. You will be redirected back to GitLab.com and should now have access to the group. In the future, you can use SAML to sign in to GitLab.com.
## Signing in to GitLab.com with SAML
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on a group's **Administration > SAML SSO** page. If configured, it might also be possible to sign in to GitLab starting from your Identity Provider.
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on a group's **Settings > SAML SSO** page. If configured, it might also be possible to sign in to GitLab starting from your Identity Provider.
1. Visit the SSO URL and click the **Sign in with Single Sign-On** button.
1. Enter your credentials on the Identity Provider if prompted.
1. You will be signed in to GitLab.com and redirected to the group.

View file

@ -30,7 +30,7 @@ The following identity providers are supported:
Once [Single sign-on](index.md) has been configured, we can:
1. Navigate to the group and click **Administration > SAML SSO**.
1. Navigate to the group and click **Settings > SAML SSO**.
1. Click on the **Generate a SCIM token** button.
1. Save the token and URL so they can be used in the next step.

View file

@ -1,4 +1,4 @@
# Export Issues to CSV **(STARTER)**
# Export Issues to CSV
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).

View file

@ -197,7 +197,7 @@ Feature.disable(:save_issuable_health_status)
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
in order to change their status, assignee, milestone, or labels in bulk.
- [Import issues](csv_import.md)
- [Export issues](csv_export.md) **(STARTER)**
- [Export issues](csv_export.md)
- [Issues API](../../../api/issues.md)
- Configure an [external issue tracker](../../../integration/external-issue-tracker.md)
such as Jira, Redmine, or Bugzilla.

View file

@ -205,16 +205,6 @@ module API
post '/notify_post_receive' do
status 200
# TODO: Re-enable when Gitaly is processing the post-receive notification
# return unless Gitlab::GitalyClient.enabled?
#
# begin
# repository = wiki? ? project.wiki.repository : project.repository
# Gitlab::GitalyClient::NotificationService.new(repository.raw_repository).post_receive
# rescue GRPC::Unavailable => e
# render_api_error!(e, 500)
# end
end
post '/post_receive' do

View file

@ -1024,6 +1024,15 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
msgid "AccessibilityReport|Learn More"
msgstr ""
msgid "AccessibilityReport|Message: %{message}"
msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
msgid "Account"
msgstr ""

View file

@ -0,0 +1,334 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
const { CancelToken } = axios;
describe('custom metrics form fields component', () => {
let component;
let mockAxios;
const getNamedInput = name => component.element.querySelector(`input[name="${name}"]`);
const validateQueryPath = `${TEST_HOST}/mock/path`;
const validQueryResponse = { data: { success: true, query: { valid: true, error: '' } } };
const csrfToken = 'mockToken';
const formOperation = 'post';
const debouncedValidateQueryMock = jest.fn();
const makeFormData = (data = {}) => ({
formData: {
title: '',
yLabel: '',
query: '',
unit: '',
group: '',
legend: '',
...data,
},
});
const mountComponent = (props, methods = {}) => {
component = mount(CustomMetricsFormFields, {
propsData: {
formOperation,
validateQueryPath,
...props,
},
csrfToken,
methods,
});
};
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onPost(validateQueryPath).reply(validQueryResponse);
});
afterEach(() => {
component.destroy();
mockAxios.restore();
});
it('checks form validity', done => {
mountComponent({
metricPersisted: true,
...makeFormData({
title: 'title',
yLabel: 'yLabel',
unit: 'unit',
group: 'group',
}),
});
component.vm.$nextTick(() => {
expect(component.vm.formIsValid).toBe(false);
done();
});
});
describe('hidden inputs', () => {
beforeEach(() => {
mountComponent();
});
it('specifies form operation _method', () => {
expect(getNamedInput('_method', 'input').value).toBe('post');
});
it('specifies authenticity token', () => {
expect(getNamedInput('authenticity_token', 'input').value).toBe(csrfToken);
});
});
describe('name input', () => {
const name = 'prometheus_metric[title]';
it('is empty by default', () => {
mountComponent();
expect(getNamedInput(name).value).toBe('');
});
it('receives a persisted value', () => {
const title = 'mockTitle';
mountComponent(makeFormData({ title }));
expect(getNamedInput(name).value).toBe(title);
});
});
describe('group input', () => {
it('has a default value', () => {
mountComponent();
expect(getNamedInput('prometheus_metric[group]', 'glformradiogroup-stub').value).toBe(
'business',
);
});
});
describe('query input', () => {
const queryInputName = 'prometheus_metric[query]';
beforeEach(() => {
mockAxios.onPost(validateQueryPath).reply(validQueryResponse);
});
it('is empty by default', () => {
mountComponent();
expect(getNamedInput(queryInputName).value).toBe('');
});
it('receives and validates a persisted value', () => {
const query = 'persistedQuery';
const axiosPost = jest.spyOn(axios, 'post');
const source = CancelToken.source();
mountComponent({ metricPersisted: true, ...makeFormData({ query }) });
expect(axiosPost).toHaveBeenCalledWith(
validateQueryPath,
{ query },
{ cancelToken: source.token },
);
expect(getNamedInput(queryInputName).value).toBe(query);
jest.runAllTimers();
});
it('checks validity on user input', () => {
const query = 'changedQuery';
mountComponent(
{},
{
debouncedValidateQuery: debouncedValidateQueryMock,
},
);
const queryInput = component.find(`input[name="${queryInputName}"]`);
queryInput.setValue(query);
queryInput.trigger('input');
expect(debouncedValidateQueryMock).toHaveBeenCalledWith(query);
});
describe('when query validation is in flight', () => {
beforeEach(() => {
jest.useFakeTimers();
mountComponent(
{ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) },
{
requestValidation: jest.fn().mockImplementation(
() =>
new Promise(resolve =>
setTimeout(() => {
resolve(validQueryResponse);
}, 4000),
),
),
},
);
});
afterEach(() => {
jest.clearAllTimers();
});
it('expect queryValidateInFlight is in flight', done => {
const queryInput = component.find(`input[name="${queryInputName}"]`);
queryInput.setValue('query');
queryInput.trigger('input');
component.vm.$nextTick(() => {
expect(component.vm.queryValidateInFlight).toBe(true);
jest.runOnlyPendingTimers();
waitForPromises()
.then(() => {
component.vm.$nextTick(() => {
expect(component.vm.queryValidateInFlight).toBe(false);
expect(component.vm.queryIsValid).toBe(true);
done();
});
})
.catch(done.fail);
});
});
it('expect loading message to display', done => {
const queryInput = component.find(`input[name="${queryInputName}"]`);
queryInput.setValue('query');
queryInput.trigger('input');
component.vm.$nextTick(() => {
expect(component.text()).toContain('Validating query');
jest.runOnlyPendingTimers();
done();
});
});
it('expect loading message to disappear', done => {
const queryInput = component.find(`input[name="${queryInputName}"]`);
queryInput.setValue('query');
queryInput.trigger('input');
component.vm.$nextTick(() => {
jest.runOnlyPendingTimers();
waitForPromises()
.then(() => {
component.vm.$nextTick(() => {
expect(component.vm.queryValidateInFlight).toBe(false);
expect(component.vm.queryIsValid).toBe(true);
expect(component.vm.errorMessage).toBe('');
done();
});
})
.catch(done.fail);
});
});
});
describe('when query is invalid', () => {
const errorMessage = 'mockErrorMessage';
const invalidQueryResponse = {
data: { success: true, query: { valid: false, error: errorMessage } },
};
beforeEach(() => {
mountComponent(
{ metricPersisted: true, ...makeFormData({ query: 'invalidQuery' }) },
{
requestValidation: jest
.fn()
.mockImplementation(() => Promise.resolve(invalidQueryResponse)),
},
);
});
it('sets queryIsValid to false', done => {
component.vm.$nextTick(() => {
expect(component.vm.queryValidateInFlight).toBe(false);
expect(component.vm.queryIsValid).toBe(false);
done();
});
});
it('shows invalid query message', done => {
component.vm.$nextTick(() => {
expect(component.text()).toContain(errorMessage);
done();
});
});
});
describe('when query is valid', () => {
beforeEach(() => {
mountComponent(
{ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) },
{
requestValidation: jest
.fn()
.mockImplementation(() => Promise.resolve(validQueryResponse)),
},
);
});
it('sets queryIsValid to true when query is valid', done => {
component.vm.$nextTick(() => {
expect(component.vm.queryIsValid).toBe(true);
done();
});
});
it('shows valid query message', () => {
expect(component.text()).toContain('PromQL query is valid');
});
});
});
describe('yLabel input', () => {
const name = 'prometheus_metric[y_label]';
it('is empty by default', () => {
mountComponent();
expect(getNamedInput(name).value).toBe('');
});
it('receives a persisted value', () => {
const yLabel = 'mockYLabel';
mountComponent(makeFormData({ yLabel }));
expect(getNamedInput(name).value).toBe(yLabel);
});
});
describe('unit input', () => {
const name = 'prometheus_metric[unit]';
it('is empty by default', () => {
mountComponent();
expect(getNamedInput(name).value).toBe('');
});
it('receives a persisted value', () => {
const unit = 'mockUnit';
mountComponent(makeFormData({ unit }));
expect(getNamedInput(name).value).toBe(unit);
});
});
describe('legend input', () => {
const name = 'prometheus_metric[legend]';
it('is empty by default', () => {
mountComponent();
expect(getNamedInput(name).value).toBe('');
});
it('receives a persisted value', () => {
const legend = 'mockLegend';
mountComponent(makeFormData({ legend }));
expect(getNamedInput(name).value).toBe(legend);
});
});
});

View file

@ -0,0 +1,48 @@
import { shallowMount } from '@vue/test-utils';
import CustomMetricsForm from '~/custom_metrics/components/custom_metrics_form.vue';
describe('CustomMetricsForm', () => {
let wrapper;
function mountComponent({
metricPersisted = false,
formData = {
title: '',
query: '',
yLabel: '',
unit: '',
group: '',
legend: '',
},
}) {
wrapper = shallowMount(CustomMetricsForm, {
propsData: {
customMetricsPath: '',
editProjectServicePath: '',
metricPersisted,
validateQueryPath: '',
formData,
},
});
}
afterEach(() => {
wrapper.destroy();
});
describe('Computed', () => {
it('Form button and title text indicate the custom metric is being edited', () => {
mountComponent({ metricPersisted: true });
expect(wrapper.vm.saveButtonText).toBe('Save Changes');
expect(wrapper.vm.titleText).toBe('Edit metric');
});
it('Form button and title text indicate the custom metric is being created', () => {
mountComponent({ metricPersisted: false });
expect(wrapper.vm.saveButtonText).toBe('Create metric');
expect(wrapper.vm.titleText).toBe('New metric');
});
});
});

View file

@ -6,6 +6,7 @@ import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
@ -157,17 +158,21 @@ describe('Monitoring store actions', () => {
variables: {
projectPath: state.projectPath,
search: searchTerm,
states: [ENVIRONMENT_AVAILABLE_STATE],
},
};
state.environmentsSearchTerm = searchTerm;
mockMutate.mockReturnValue(Promise.resolve());
mockMutate.mockResolvedValue({});
return testAction(
fetchEnvironmentsData,
null,
state,
[],
[{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
[
{ type: 'requestEnvironmentsData' },
{ type: 'receiveEnvironmentsDataSuccess', payload: [] },
],
() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
},

View file

@ -0,0 +1,112 @@
import { shallowMount } from '@vue/test-utils';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
const issue = {
name:
'The accessibility scanning found 2 errors of the following type: WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
message: 'This element has insufficient contrast at this conformance level.',
status: 'failed',
className: 'spec.test_spec',
learnMoreUrl: 'https://www.w3.org/TR/WCAG20-TECHS/H91.html',
};
describe('CustomMetricsForm', () => {
let wrapper;
const mountComponent = ({ name, code, message, status, className }, isNew = false) => {
wrapper = shallowMount(AccessibilityIssueBody, {
propsData: {
issue: {
name,
code,
message,
status,
className,
},
isNew,
},
});
};
const findIsNewBadge = () => wrapper.find({ ref: 'accessibility-issue-is-new-badge' });
beforeEach(() => {
mountComponent(issue);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('Displays the issue message', () => {
const description = wrapper.find({ ref: 'accessibility-issue-description' }).text();
expect(description).toContain(`Message: ${issue.message}`);
});
describe('When an issue code is present', () => {
it('Creates the correct URL for learning more about the issue code', () => {
const learnMoreUrl = wrapper
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toEqual(issue.learnMoreUrl);
});
});
describe('When an issue code is not present', () => {
beforeEach(() => {
mountComponent({
...issue,
code: undefined,
});
});
it('Creates a URL leading to the overview documentation page', () => {
const learnMoreUrl = wrapper
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
});
});
describe('When an issue code does not contain the TECHS code', () => {
beforeEach(() => {
mountComponent({
...issue,
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2',
});
});
it('Creates a URL leading to the overview documentation page', () => {
const learnMoreUrl = wrapper
.find({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
});
});
describe('When issue is new', () => {
beforeEach(() => {
mountComponent(issue, true);
});
it('Renders the new badge', () => {
expect(findIsNewBadge().exists()).toEqual(true);
});
});
describe('When issue is not new', () => {
beforeEach(() => {
mountComponent(issue, false);
});
it('Does not render the new badge', () => {
expect(findIsNewBadge().exists()).toEqual(false);
});
});
});

View file

@ -11,7 +11,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
let(:shared) { project.import_export_shared }
RSpec.shared_examples 'project tree restorer work properly' do |reader|
RSpec.shared_examples 'project tree restorer work properly' do |reader, ndjson_enabled|
describe 'restore project tree' do
before_all do
# Using an admin for import, so we can check assignment of existing members
@ -25,6 +25,9 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
allow(Feature).to receive(:enabled?).and_call_original
stub_feature_flags(project_import_ndjson: ndjson_enabled)
setup_import_export_config('complex')
setup_reader(reader)
@ -999,23 +1002,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
context 'enable ndjson import' do
before_all do
# Test suite `restore project tree` run `project_tree_restorer.restore` in `before_all`.
# `Enable all features by default for testing` happens in `before(:each)`
# So it requires manually enable feature flag to allow ndjson_reader
Feature.enable(:project_import_ndjson)
end
it_behaves_like 'project tree restorer work properly', :legacy_reader, true
it_behaves_like 'project tree restorer work properly', :legacy_reader
it_behaves_like 'project tree restorer work properly', :ndjson_reader
it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
end
context 'disable ndjson import' do
before do
stub_feature_flags(project_import_ndjson: false)
end
it_behaves_like 'project tree restorer work properly', :legacy_reader
it_behaves_like 'project tree restorer work properly', :legacy_reader, false
end
end

View file

@ -16,7 +16,6 @@ describe Gitlab::ImportExport::Project::TreeSaver do
let_it_be(:group) { create(:group) }
let_it_be(:project) { setup_project }
let_it_be(:shared) { project.import_export_shared }
let_it_be(:project_tree_saver ) { described_class.new(project: project, current_user: user, shared: shared) }
let(:relation_name) { :projects }
@ -29,9 +28,17 @@ describe Gitlab::ImportExport::Project::TreeSaver do
end
before_all do
Feature.enable(:project_export_as_ndjson) if ndjson_enabled
project.add_maintainer(user)
project_tree_saver.save
RSpec::Mocks.with_temporary_scope do
allow(Feature).to receive(:enabled?).and_call_original
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project.add_maintainer(user)
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project_tree_saver = described_class.new(project: project, current_user: user, shared: shared)
project_tree_saver.save
end
end
after :all do

View file

@ -882,88 +882,6 @@ describe API::Internal::Base do
end
end
# TODO: Uncomment when the end-point is reenabled
# describe 'POST /notify_post_receive' do
# let(:valid_params) do
# { project: project.repository.path, secret_token: secret_token }
# end
#
# let(:valid_wiki_params) do
# { project: project.wiki.repository.path, secret_token: secret_token }
# end
#
# before do
# allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
# end
#
# it "calls the Gitaly client with the project's repository" do
# expect(Gitlab::GitalyClient::NotificationService).
# to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
# and_call_original
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
# to receive(:post_receive)
#
# post api("/internal/notify_post_receive"), valid_params
#
# expect(response).to have_gitlab_http_status(:ok)
# end
#
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
# expect(Gitlab::GitalyClient::NotificationService).
# to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
# and_call_original
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
# to receive(:post_receive)
#
# post api("/internal/notify_post_receive"), valid_wiki_params
#
# expect(response).to have_gitlab_http_status(:ok)
# end
#
# it "returns 500 if the gitaly call fails" do
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
# to receive(:post_receive).and_raise(GRPC::Unavailable)
#
# post api("/internal/notify_post_receive"), valid_params
#
# expect(response).to have_gitlab_http_status(:internal_server_error)
# end
#
# context 'with a gl_repository parameter' do
# let(:valid_params) do
# { gl_repository: "project-#{project.id}", secret_token: secret_token }
# end
#
# let(:valid_wiki_params) do
# { gl_repository: "wiki-#{project.id}", secret_token: secret_token }
# end
#
# it "calls the Gitaly client with the project's repository" do
# expect(Gitlab::GitalyClient::NotificationService).
# to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
# and_call_original
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
# to receive(:post_receive)
#
# post api("/internal/notify_post_receive"), valid_params
#
# expect(response).to have_gitlab_http_status(:ok)
# end
#
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
# expect(Gitlab::GitalyClient::NotificationService).
# to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
# and_call_original
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
# to receive(:post_receive)
#
# post api("/internal/notify_post_receive"), valid_wiki_params
#
# expect(response).to have_gitlab_http_status(:ok)
# end
# end
# end
describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
let(:identifier) { 'key-123' }
let(:branch_name) { 'feature' }