Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-03 03:14:42 +00:00
parent 498ba9dc41
commit c657078ecb
70 changed files with 791 additions and 356 deletions

View File

@ -58,6 +58,9 @@ update-qa-cache:
- tooling/bin/find_change_diffs ${CHANGES_DIFFS_DIR}
script:
- |
tooling/bin/qa/check_if_qa_only_spec_changes ${CHANGES_FILE} ${ONLY_QA_CHANGES_FILE}
[ -f $ONLY_QA_CHANGES_FILE ] && export QA_TESTS="`cat $ONLY_QA_CHANGES_FILE`"
echo "QA_TESTS: $QA_TESTS"
tooling/bin/qa/package_and_qa_check ${CHANGES_DIFFS_DIR} && exit_code=$?
if [ $exit_code -eq 0 ]; then
./scripts/trigger-build omnibus
@ -80,9 +83,11 @@ update-qa-cache:
expire_in: 7d
paths:
- ${CHANGES_FILE}
- ${ONLY_QA_CHANGES_FILE}
- ${CHANGES_DIFFS_DIR}/*
variables:
CHANGES_FILE: tmp/changed_files.txt
ONLY_QA_CHANGES_FILE: tmp/qa_only_changed_files.txt
CHANGES_DIFFS_DIR: tmp/diffs
.package-and-qa-ff-base:

View File

@ -0,0 +1,59 @@
<script>
import { __ } from '~/locale';
import Home from './home.vue';
import IncubationBanner from './incubation_banner.vue';
import ServiceAccountsForm from './service_accounts_form.vue';
import NoGcpProjects from './errors/no_gcp_projects.vue';
import GcpError from './errors/gcp_error.vue';
const SCREEN_GCP_ERROR = 'gcp_error';
const SCREEN_HOME = 'home';
const SCREEN_NO_GCP_PROJECTS = 'no_gcp_projects';
const SCREEN_SERVICE_ACCOUNTS_FORM = 'service_accounts_form';
export default {
components: {
IncubationBanner,
},
inheritAttrs: false,
props: {
screen: {
required: true,
type: String,
},
},
computed: {
mainComponent() {
switch (this.screen) {
case SCREEN_HOME:
return Home;
case SCREEN_GCP_ERROR:
return GcpError;
case SCREEN_NO_GCP_PROJECTS:
return NoGcpProjects;
case SCREEN_SERVICE_ACCOUNTS_FORM:
return ServiceAccountsForm;
default:
throw new Error(__('Unknown screen'));
}
},
},
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
},
},
};
</script>
<template>
<div>
<incubation-banner
:share-feedback-url="feedbackUrl('general_feedback')"
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<component :is="mainComponent" v-bind="$attrs" />
</div>
</template>

View File

@ -0,0 +1,41 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import ServiceAccountsList from './service_accounts_list.vue';
export default {
components: {
GlTabs,
GlTab,
ServiceAccountsList,
},
props: {
serviceAccounts: {
type: Array,
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-tabs>
<gl-tab :title="__('Configuration')">
<service-accounts-list
class="gl-mx-4"
:list="serviceAccounts"
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
</gl-tab>
<gl-tab :title="__('Deployments')" disabled />
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</template>

View File

@ -1,50 +0,0 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
import IncubationBanner from '../incubation_banner.vue';
import ServiceAccountsList from '../service_accounts_list.vue';
export default {
components: { GlTab, GlTabs, IncubationBanner, ServiceAccountsList },
props: {
serviceAccounts: {
type: Array,
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
},
},
};
</script>
<template>
<div>
<incubation-banner
:share-feedback-url="feedbackUrl('general_feedback')"
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<gl-tabs>
<gl-tab :title="__('Configuration')">
<service-accounts-list
class="gl-mx-3"
:list="serviceAccounts"
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
</gl-tab>
<gl-tab :title="__('Deployments')" disabled />
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</div>
</template>

View File

@ -1,20 +1,14 @@
<script>
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
import IncubationBanner from '../incubation_banner.vue';
export default {
components: { GlButton, GlFormGroup, GlFormSelect, IncubationBanner },
components: { GlButton, GlFormGroup, GlFormSelect },
props: {
gcpProjects: { required: true, type: Array },
environments: { required: true, type: Array },
cancelPath: { required: true, type: String },
},
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
},
},
i18n: {
title: __('Create service account'),
gcpProjectLabel: __('Google Cloud project'),
@ -31,11 +25,6 @@ export default {
<template>
<div>
<incubation-banner
:share-feedback-url="feedbackUrl('general_feedback')"
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<header class="gl-my-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
<h2 class="gl-font-size-h1">{{ $options.i18n.title }}</h2>
</header>

View File

@ -1,40 +1,12 @@
import Vue from 'vue';
import { __ } from '~/locale';
import App from './components/screens/app.vue';
import ServiceAccountsForm from './components/screens/service_accounts_form.vue';
import ErrorNoGcpProjects from './components/errors/no_gcp_projects.vue';
import ErrorGcpError from './components/errors/gcp_error.vue';
const elementRenderer = (element, props = {}) => (createElement) =>
createElement(element, { props });
const rootComponentMap = [
{
root: '#js-google-cloud-error-no-gcp-projects',
component: ErrorNoGcpProjects,
},
{
root: '#js-google-cloud-error-gcp-error',
component: ErrorGcpError,
},
{
root: '#js-google-cloud-service-accounts',
component: ServiceAccountsForm,
},
{
root: '#js-google-cloud',
component: App,
},
];
import App from './components/app.vue';
export default () => {
for (let i = 0; i < rootComponentMap.length; i += 1) {
const { root, component } = rootComponentMap[i];
const element = document.querySelector(root);
if (element) {
const props = JSON.parse(element.getAttribute('data'));
return new Vue({ el: root, render: elementRenderer(component, props) });
}
}
throw new Error(__('Unknown root'));
const root = '#js-google-cloud';
const element = document.querySelector(root);
const { screen, ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(App, { props: { screen }, attrs }),
});
};

View File

@ -2,7 +2,6 @@ import { s__, __ } from '~/locale';
export const TEST_INTEGRATION_EVENT = 'testIntegration';
export const SAVE_INTEGRATION_EVENT = 'saveIntegration';
export const GET_JIRA_ISSUE_TYPES_EVENT = 'getJiraIssueTypes';
export const TOGGLE_INTEGRATION_EVENT = 'toggleIntegration';
export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm';

View File

@ -0,0 +1,9 @@
import axios from '~/lib/utils/axios_utils';
/**
* Test the validity of [integrationFormData].
* @return Promise<{ issuetypes: []String }> - issuetypes contains valid Jira issue types.
*/
export const testIntegrationSettings = (testPath, integrationFormData) => {
return axios.put(testPath, integrationFormData);
};

View File

@ -69,6 +69,10 @@ export default {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
},
mounted() {
// this form element is defined in Haml
this.form = document.querySelector('.js-integration-settings-form');
},
methods: {
...mapActions([
'setOverride',
@ -76,6 +80,7 @@ export default {
'setIsTesting',
'setIsResetting',
'fetchResetIntegration',
'requestJiraIssueTypes',
]),
onSaveClick() {
this.setIsSaving(true);
@ -88,6 +93,10 @@ export default {
onResetClick() {
this.fetchResetIntegration();
},
onRequestJiraIssueTypes() {
const formData = new FormData(this.form);
this.requestJiraIssueTypes(formData);
},
},
helpHtmlConfig: {
ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
@ -135,6 +144,7 @@ export default {
v-if="isJira && !isInstanceOrGroupLevel"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">

View File

@ -1,10 +1,7 @@
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import {
VALIDATE_INTEGRATION_FORM_EVENT,
GET_JIRA_ISSUE_TYPES_EVENT,
} from '~/integrations/constants';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__, __ } from '~/locale';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
@ -91,9 +88,6 @@ export default {
validateForm() {
this.validated = true;
},
getJiraIssueTypes() {
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
},
},
i18n: {
sectionTitle: s__('JiraService|View Jira issues in GitLab'),
@ -136,7 +130,7 @@ export default {
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesIntegration"
data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes"
@request-jira-issue-types="$emit('request-jira-issue-types')"
/>
<jira-upgrade-cta
v-if="!showJiraVulnerabilitiesIntegration"

View File

@ -1,5 +1,12 @@
import axios from 'axios';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import {
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { testIntegrationSettings } from '../api';
import eventHub from '../event_hub';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
@ -27,10 +34,28 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.catch(() => dispatch('receiveResetIntegrationError'));
};
export const requestJiraIssueTypes = ({ commit }) => {
export const requestJiraIssueTypes = ({ commit, dispatch, getters }, formData) => {
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
return testIntegrationSettings(getters.propsSource.testPath, formData)
.then(
({
data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
throw new Error(message);
}
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
.catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
dispatch('receiveJiraIssueTypesError', message);
});
};
export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);

View File

@ -1,18 +1,16 @@
import { delay } from 'lodash';
import toast from '~/vue_shared/plugins/global_toast';
import axios from '../lib/utils/axios_utils';
import initForm from './edit';
import eventHub from './edit/event_hub';
import {
TEST_INTEGRATION_EVENT,
SAVE_INTEGRATION_EVENT,
GET_JIRA_ISSUE_TYPES_EVENT,
TOGGLE_INTEGRATION_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
} from './constants';
import { testIntegrationSettings } from './edit/api';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@ -41,9 +39,6 @@ export default class IntegrationSettingsForm {
eventHub.$on(SAVE_INTEGRATION_EVENT, () => {
this.saveIntegration();
});
eventHub.$on(GET_JIRA_ISSUE_TYPES_EVENT, () => {
this.getJiraIssueTypes(new FormData(this.$form));
});
}
saveIntegration() {
@ -96,43 +91,12 @@ export default class IntegrationSettingsForm {
*
* @return {Promise}
*/
getJiraIssueTypes(formData) {
const {
$store: { dispatch },
} = this.vue;
dispatch('requestJiraIssueTypes');
return this.fetchTestSettings(formData)
.then(
({
data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
throw new Error(message);
}
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
.catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
dispatch('receiveJiraIssueTypesError', message);
});
}
/**
* Send request to the test endpoint which checks if the current config is valid
*/
fetchTestSettings(formData) {
return axios.put(this.testEndPoint, formData);
}
/**
* Test Integration config
*/
testSettings(formData) {
return this.fetchTestSettings(formData)
return testIntegrationSettings(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);

View File

@ -21,6 +21,6 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
end
def feature_flag_enabled!
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud)
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud, project)
end
end

View File

@ -9,10 +9,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
gcp_projects = google_api_client.list_projects
if gcp_projects.empty?
@js_data = {}.to_json
@js_data = { screen: 'no_gcp_projects' }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects'
else
@js_data = {
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
environments: project.environments,
cancelPath: project_google_cloud_index_path(project)
@ -78,7 +79,7 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
def handle_gcp_error(error, project)
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
@js_data = { error: error.to_s }.to_json
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
end

View File

@ -3,6 +3,7 @@
class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
def index
@js_data = {
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')

View File

@ -2,6 +2,7 @@
module Ci
class JobArtifact < Ci::ApplicationRecord
include IgnorableColumns
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
@ -120,6 +121,9 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
# We will start using this column once we complete https://gitlab.com/gitlab-org/gitlab/-/issues/285597
ignore_column :original_filename, remove_with: '14.7', remove_after: '2022-11-22'
mount_file_store_uploader JobArtifactUploader
skip_callback :save, :after, :store_file!, if: :store_after_commit?

View File

@ -3,4 +3,4 @@
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-error-gcp-error{ data: @js_data }
#js-google-cloud{ data: @js_data }

View File

@ -3,4 +3,4 @@
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-error-no-gcp-projects{ data: @js_data }
#js-google-cloud{ data: @js_data }

View File

@ -5,4 +5,4 @@
- @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_service_accounts_path(@project), method: 'post' do
#js-google-cloud-service-accounts{ data: @js_data }
#js-google-cloud{ data: @js_data }

15
bin/metrics-server Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative '../metrics_server/metrics_server'
begin
target = ENV['METRICS_SERVER_TARGET']
raise "Required: METRICS_SERVER_TARGET=[sidekiq]" unless target == 'sidekiq'
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
# Re-raise exceptions in threads on the main thread.
Thread.abort_on_exception = true
MetricsServer.new(target, metrics_dir).start
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddOrignalFilenameToCiJobArtifact < Gitlab::Database::Migration[1.0]
enable_lock_retries!
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20211119085036_add_text_limit_to_job_artifact_original_filename.rb
def up
add_column :ci_job_artifacts, :original_filename, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
def down
remove_column :ci_job_artifacts, :original_filename, :text
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddTextLimitToJobArtifactOriginalFilename < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_text_limit :ci_job_artifacts, :original_filename, 512
end
def down
remove_text_limit :ci_job_artifacts, :original_filename
end
end

View File

@ -0,0 +1 @@
88b289d724f98f75e0340cde4c6e2bc3cb55df2a979934fb2bc544d22e4c032d

View File

@ -0,0 +1 @@
2b2c28e0370ae1bb84bee5ff769c9b313902d1f1afc50fa54e23a1627b1121f3

View File

@ -11699,7 +11699,9 @@ CREATE TABLE ci_job_artifacts (
id bigint NOT NULL,
job_id bigint NOT NULL,
locked smallint DEFAULT 2,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
original_filename text,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL)),
CONSTRAINT check_85573000db CHECK ((char_length(original_filename) <= 512))
);
CREATE SEQUENCE ci_job_artifacts_id_seq

View File

@ -60,7 +60,7 @@ assumptions are made:
Make sure to follow all steps below:
1. (Optional) If you run short on resources, you can temporarily free up some
1. Optional. If you run short on resources, you can temporarily free up some
memory by shutting down the GitLab service with the following command:
```shell

View File

@ -42,7 +42,7 @@ to the end of the Bitbucket authorization callback URL.
- **Name:** This can be anything. Consider something like `<Organization>'s GitLab`
or `<Your Name>'s GitLab` or something else descriptive.
- **Application description:** *(Optional)* Fill this in if you wish.
- **Application description:** Optional. Fill this in if you wish.
- **Callback URL:** (Required in GitLab versions 8.15 and greater)
The URL to your GitLab installation, such as
`https://gitlab.example.com/users/auth`.

View File

@ -32,15 +32,15 @@ project, group, or instance level:
1. Scroll to **Add an integration**, and select **Datadog**.
1. Select **Active** to enable the integration.
1. Specify the [**Datadog site**](https://docs.datadoghq.com/getting_started/site/) to send data to.
1. (Optional) To override the API URL used to send data directly, provide an **API URL**.
1. Optional. To override the API URL used to send data directly, provide an **API URL**.
Used only in advanced scenarios.
1. Provide your Datadog **API key**.
1. (Optional) If you use more than one GitLab instance, provide a unique **Service** name
1. Optional. If you use more than one GitLab instance, provide a unique **Service** name
to differentiate between your GitLab instances.
1. (Optional) If you use groups of GitLab instances (such as staging and production
1. Optional. If you use groups of GitLab instances (such as staging and production
environments), provide an **Env** name. This value is attached to each span
the integration generates.
1. (Optional) Select **Test settings** to test your integration.
1. Optional. Select **Test settings** to test your integration.
1. Select **Save changes**.
When the integration sends data, you can view it in the [CI Visibility](https://app.datadoghq.com/ci)

View File

@ -279,8 +279,8 @@ To disable the Elasticsearch integration:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Advanced Search**.
1. Uncheck **Elasticsearch indexing** and **Search with Elasticsearch enabled**.
1. Click **Save changes** for the changes to take effect.
1. (Optional) Delete the existing indexes:
1. Select **Save changes**.
1. Optional. Delete the existing indexes:
```shell
# Omnibus installations

View File

@ -65,11 +65,11 @@ and you can [customize the payload](#customize-the-alert-payload-outside-of-gitl
1. Toggle the **Active** alert setting. The **URL** and **Authorization Key** for the webhook
configuration are available in the **View credentials** tab after you save the integration.
You must also input the URL and Authorization Key in your external service.
1. _(Optional)_ To map fields from your monitoring tool's alert to GitLab fields, enter a sample
1. Optional. To map fields from your monitoring tool's alert to GitLab fields, enter a sample
payload and click **Parse payload for custom mapping**. Valid JSON is required. If you update
a sample payload, you must also remap the fields.
1. _(Optional)_ If you provided a valid sample payload, select each value in
1. Optional. If you provided a valid sample payload, select each value in
**Payload alert key** to [map to a **GitLab alert key**](#map-fields-in-custom-alerts).
1. To save your integration, click **Save Integration**. If desired, you can send a test alert
from your integration's **Send test alert** tab after the integration is created.

View File

@ -27,7 +27,7 @@ The following table is an example of how to configure the three different cluste
| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` variable value | Variable environment scope | Notes |
|--------------|---------------------------|-------------------------------------------|----------------------------|---|
| review | `review/*` | `review.example.com` | `review/*` | The review cluster which runs all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, used by every environment name starting with `review/`. |
| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which runs the deployments of the staging environments. You must [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
| staging | `staging` | `staging.example.com` | `staging` | Optional. The staging cluster that runs the deployments of the staging environments. You must [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
| production | `production` | `example.com` | `production` | The production cluster which runs the production environment deployments. You can use [incremental rollouts](customize.md#incremental-rollout-to-production). |
To add a different cluster for each environment:

View File

@ -69,6 +69,7 @@ The browser-based crawler can be configured using CI/CD variables.
| `DAST_BROWSER_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or navigations. |
| `DAST_BROWSER_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. |
| `DAST_BROWSER_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. |
| `DAST_BROWSER_PAGE_READY_SELECTOR` | selector | `css:#page-is-ready` | Selector that when detected as visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Note: When this selector is set, but the element is not found, the scanner waits for the period defined in `DAST_BROWSER_STABILITY_TIMEOUT` before continuing the scan. This can significantly increase scanning time if the element is not present on multiple pages within the site. |
The [DAST variables](index.md#available-cicd-variables) `SECURE_ANALYZERS_PREFIX`, `DAST_FULL_SCAN_ENABLED`, `DAST_AUTO_UPDATE_ADDONS`, `DAST_EXCLUDE_RULES`, `DAST_REQUEST_HEADERS`, `DAST_HTML_REPORT`, `DAST_MARKDOWN_REPORT`, `DAST_XML_REPORT`,
`DAST_AUTH_URL`, `DAST_USERNAME`, `DAST_PASSWORD`, `DAST_USERNAME_FIELD`, `DAST_PASSWORD_FIELD`, `DAST_FIRST_SUBMIT_FIELD`, `DAST_SUBMIT_FIELD`, `DAST_EXCLUDE_URLS`, `DAST_AUTH_VERIFICATION_URL`, `DAST_BROWSER_AUTH_VERIFICATION_SELECTOR`, `DAST_BROWSER_AUTH_VERIFICATION_LOGIN_FORM`, `DAST_BROWSER_AUTH_REPORT`,

View File

@ -321,7 +321,7 @@ To share a group after enabling this feature:
1. Go to your group's page.
1. On the left sidebar, go to **Group information > Members**, and then select **Invite a group**.
1. Select a group, and select a **Max role**.
1. (Optional) Select an **Access expiration date**.
1. Optional. Select an **Access expiration date**.
1. Select **Invite**.
## Manage group memberships via LDAP **(PREMIUM SELF)**

View File

@ -37,7 +37,7 @@ Complete these steps in GitLab:
1. Select **Asana**.
1. Ensure that the **Active** toggle is enabled.
1. Paste the token you generated in Asana.
1. (Optional) To restrict this setting to specific branches, list them in the **Restrict to branch**
1. Optional. To restrict this setting to specific branches, list them in the **Restrict to branch**
field, separated with commas.
1. Select **Save changes** or optionally select **Test settings**.

View File

@ -32,7 +32,7 @@ Select a room and create a webhook:
1. Enter the room where you want to receive notifications from GitLab.
1. Open the room dropdown menu on the top-left and select **Manage webhooks**.
1. Enter the name for your webhook, for example "GitLab integration".
1. (Optional) Add an avatar for your bot.
1. Optional. Add an avatar for your bot.
1. Select **Save**.
1. Copy the webhook URL.
@ -46,7 +46,7 @@ Enable the Google Chat integration in GitLab:
1. Scroll down to the end of the page where you find a **Webhook** field.
1. Enter the webhook URL you copied from Google Chat.
1. Select the events you want to be notified about in your Google Chat room.
1. (Optional) Select **Test settings** to verify the connection.
1. Optional. Select **Test settings** to verify the connection.
1. Select **Save changes**.
To test the integration, make a change based on the events you selected and

View File

@ -50,12 +50,12 @@ Then fill in the integration configuration:
- **Webhook**: The incoming webhook URL on Mattermost, similar to
`http://mattermost.example/hooks/5xo…`.
- **Username**: (Optional) The username shown in messages sent to Mattermost.
- **Username**: Optional. The username shown in messages sent to Mattermost.
To change the bot's username, provide a value.
- **Notify only broken pipelines**: If you enable the **Pipeline** event, and you want
notifications about failed pipelines only.
- **Branches for which notifications are to be sent**: The branches to send notifications for.
- **Labels to be notified**: (Optional) Labels required for the issue or merge request
- **Labels to be notified**: Optional. Labels required for the issue or merge request
to trigger a notification. Leave blank to notify for all issues and merge requests.
- **Labels to be notified behavior**: When you use the **Labels to be notified** filter,
messages are sent when an issue or merge request contains _any_ of the labels specified

View File

@ -42,6 +42,6 @@ Complete these steps in GitLab:
1. Select **Pivotal Tracker**.
1. Ensure that the **Active** toggle is enabled.
1. Paste the token you generated in Pivotal Tracker.
1. (Optional) To restrict this setting to specific branches, list them in the **Restrict to branch**
1. Optional. To restrict this setting to specific branches, list them in the **Restrict to branch**
field, separated with commas.
1. Select **Save changes** or optionally select **Test settings**.

View File

@ -31,7 +31,7 @@ to control GitLab from Slack. Slash commands are configured separately.
[Triggers for Slack notifications](#triggers-for-slack-notifications).
By default, messages are sent to the channel you configured during
[Slack configuration](#configure-slack).
1. (Optional) To send messages to a different channel, multiple channels, or as
1. Optional. To send messages to a different channel, multiple channels, or as
a direct message:
- *To send messages to channels,* enter the Slack channel names, separated by
commas.
@ -42,7 +42,7 @@ to control GitLab from Slack. Slash commands are configured separately.
1. In **Webhook**, enter the webhook URL you copied in the
[Slack configuration](#configure-slack) step.
1. (Optional) In **Username**, enter the username of the Slack bot that sends
1. Optional. In **Username**, enter the username of the Slack bot that sends
the notifications.
1. Select the **Notify only broken pipelines** checkbox to notify only on failures.
1. In the **Branches for which notifications are to be sent** dropdown, select which types of branches

View File

@ -72,9 +72,9 @@ To create a new project label:
1. Select the **New label** button.
1. In the **Title** field, enter a short, descriptive name for the label. You
can also use this field to create [scoped, mutually exclusive labels](#scoped-labels).
1. (Optional) In the **Description** field, you can enter additional
1. Optional. In the **Description** field, you can enter additional
information about how and when to use this label.
1. (Optional) Select a background color for the label by selecting one of the
1. Optional. Select a background color for the label by selecting one of the
available colors, or by entering a hex color value in the **Background color**
field.
1. Select **Create label**.
@ -86,7 +86,7 @@ label section of the right sidebar of an issue or a merge request:
1. Click **Create project label**.
- Fill in the name field. Note that you can't specify a description if creating a label
this way. You can add a description later by editing the label (see below).
- (Optional) Select a color by clicking on the available colors, or input a hex
- Optional. Select a color by clicking on the available colors, or input a hex
color value for a specific color.
1. Click **Create**.

View File

@ -63,7 +63,7 @@ To edit a merge request approval rule:
1. Go to your project and select **Settings > General**.
1. Expand **Merge request (MR) approvals**, and then select **Edit**.
1. (Optional) Change the **Rule name**.
1. Optional. Change the **Rule name**.
1. Set the number of required approvals in **Approvals required**. The minimum value is `0`.
1. Add or remove eligible approvers, as needed:
- *To add users or groups as approvers,* search for users or groups that are

View File

@ -76,7 +76,7 @@ merge request is from a fork:
1. Click on the **Options** dropdown and select **Cherry-pick** to show the cherry-pick modal.
1. In **Pick into project** and **Pick into branch**, select the destination project and branch:
![Cherry-pick commit](img/cherry_pick_into_project_v13_11.png)
1. (Optional) Select **Start a new merge request** if you're ready to create a merge request.
1. Optional. Select **Start a new merge request** if you're ready to create a merge request.
1. Click **Cherry-pick**.
## Related topics

View File

@ -43,7 +43,7 @@ To start your review:
- **Add to review**: Keep this comment private and add to the current review.
These review comments are marked **Pending** and are visible only to you.
- **Add comment now**: Submits the specific comment as a regular comment instead of as part of the review.
1. (Optional) You can use [quick actions](../../quick_actions.md) inside review comments.
1. Optional. You can use [quick actions](../../quick_actions.md) inside review comments.
The comment shows the actions to perform after publication, but does not perform them
until you submit your review.
1. When your review is complete, you can [submit the review](#submit-a-review). Your comments

View File

@ -18,9 +18,9 @@ configured to generate a Pages site.
To fork a sample project and create a Pages website:
1. View the sample projects by navigating to the [GitLab Pages examples](https://gitlab.com/pages) group.
1. Click the name of the project you want to [fork](../../../../user/project/working_with_projects.md#fork-a-project).
1. In the top right, click the **Fork** button, and then choose a namespace to fork to.
1. Go to your project's **CI/CD > Pipelines** and click **Run pipeline**.
1. Select the name of the project you want to [fork](../../../../user/project/working_with_projects.md#fork-a-project).
1. In the top right, select **Fork** and then choose a namespace to fork to.
1. For your project, on the left sidebar, select **CI/CD > Pipelines** and then **Run pipeline**.
GitLab CI/CD builds and deploys your site.
The site can take approximately 30 minutes to deploy.

View File

@ -101,7 +101,7 @@ To use it, follow the instructions at [Creating a fork](#creating-a-fork) and pr
- The project name.
- The project URL.
- The project slug.
- *(Optional)* The project description.
- Optional. The project description.
- The visibility level for your fork.
### Enable or disable the fork project form **(FREE SELF)**

View File

@ -194,7 +194,7 @@ To push a new project:
remote: The private project namespace/myproject was created.
```
1. (Optional) To configure the remote, alter the command
1. Optional. To configure the remote, alter the command
`git remote add origin https://gitlab.example.com/namespace/myproject.git`
to match your namespace and project names.

View File

@ -11,6 +11,13 @@ module API
default_format :json
rescue_from(
::ActiveRecord::RecordNotUnique,
::PG::UniqueViolation
) do |e|
render_api_error!(e.message, 422)
end
before do
authenticate!
authorize! :read_terraform_state, user_project

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require_dependency 'gitlab/utils'
module Gitlab
module Utils
module StrongMemoize

View File

@ -90,7 +90,7 @@ module Sidebars
end
def google_cloud_menu_item
feature_is_enabled = Feature.enabled?(:incubation_5mp_google_cloud)
feature_is_enabled = Feature.enabled?(:incubation_5mp_google_cloud, context.project)
user_has_permissions = can?(context.current_user, :admin_project_google_cloud, context.project)
unless feature_is_enabled && user_has_permissions
@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project),
active_routes: { controller: :google_cloud },
active_routes: { controller: [:google_cloud, :service_accounts] },
item_id: :google_cloud
)
end

View File

@ -37172,7 +37172,7 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
msgid "Unknown root"
msgid "Unknown screen"
msgstr ""
msgid "Unknown user"

View File

@ -0,0 +1,25 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
require 'shellwords'
require 'fileutils'
require 'active_support/concern'
require 'active_support/inflector'
require 'prometheus/client'
require 'rack'
require_relative 'settings_overrides'
require_relative '../lib/gitlab/daemon'
require_relative '../lib/gitlab/utils/strong_memoize'
require_relative '../lib/prometheus/cleanup_multiproc_dir_service'
require_relative '../lib/gitlab/metrics/prometheus'
require_relative '../lib/gitlab/metrics'
require_relative '../lib/gitlab/metrics/exporter/base_exporter'
require_relative '../lib/gitlab/metrics/exporter/sidekiq_exporter'
require_relative '../lib/gitlab/health_checks/probes/collection'
require_relative '../lib/gitlab/health_checks/probes/status'
# rubocop:enable Naming/FileName

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require_relative '../config/bundler_setup'
require_relative 'dependencies'
class MetricsServer # rubocop:disable Gitlab/NamespacedClass
class << self
def spawn(target, gitlab_config: nil)
cmd = "#{Rails.root}/bin/metrics-server"
env = {
'METRICS_SERVER_TARGET' => target,
'GITLAB_CONFIG' => gitlab_config
}
Process.spawn(env, cmd, err: $stderr, out: $stdout).tap do |pid|
Process.detach(pid)
end
end
end
def initialize(target, metrics_dir)
@target = target
@metrics_dir = metrics_dir
end
def start
::Prometheus::Client.configure do |config|
config.multiprocess_files_dir = @metrics_dir
end
FileUtils.mkdir_p(@metrics_dir, mode: 0700)
::Prometheus::CleanupMultiprocDirService.new.execute
settings = Settings.monitoring.sidekiq_exporter
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
server = exporter_class.instance(settings, synchronous: true)
server.start
end
end

View File

@ -0,0 +1,14 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
# Sidekiq-cluster code is loaded both inside a Rails/Rspec
# context as well as outside of it via CLI invocation. When it
# is loaded outside of a Rails/Rspec context we do not have access
# to all necessary constants. For example, we need Rails.root to
# determine the location of bin/metrics-server.
# Here we make the necessary constants available conditionally.
require_relative '../scripts/override_rails_constants' unless Object.const_defined?('Rails')
require_relative '../config/settings'
# rubocop:enable Naming/FileName

View File

@ -0,0 +1,20 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
require 'active_support/environment_inquirer'
module Rails # rubocop:disable Gitlab/NamespacedClass
extend self
def env
@env ||= ActiveSupport::EnvironmentInquirer.new(
ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "test"
)
end
def root
Pathname.new(File.expand_path('..', __dir__))
end
end
# rubocop:enable Naming/FileName

View File

@ -13,17 +13,7 @@ require 'active_support/string_inquirer'
ENV['SKIP_RAILS_ENV_IN_RAKE'] = 'true'
module Rails
extend self
def root
Pathname.new(File.expand_path('..', __dir__))
end
def env
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
end
require_relative 'override_rails_constants'
ActiveSupport::Dependencies.autoload_paths << 'lib'

View File

@ -155,7 +155,8 @@ module Trigger
'ee' => Trigger.ee? ? 'true' : 'false',
'QA_BRANCH' => ENV['QA_BRANCH'] || 'master',
'CACHE_UPDATE' => ENV['OMNIBUS_GITLAB_CACHE_UPDATE'],
'GITLAB_QA_OPTIONS' => ENV['GITLAB_QA_OPTIONS']
'GITLAB_QA_OPTIONS' => ENV['GITLAB_QA_OPTIONS'],
'QA_TESTS' => ENV['QA_TESTS']
}
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../../metrics_server/metrics_server'
# End-to-end tests for the metrics server process we use to serve metrics
# from forking applications (Sidekiq, Puma) to the Prometheus scraper.
RSpec.describe 'bin/metrics-server', :aggregate_failures do
let(:config_file) { Tempfile.new('gitlab.yml') }
let(:config) do
{
'test' => {
'monitoring' => {
'sidekiq_exporter' => {
'address' => 'localhost',
'enabled' => true,
'port' => 3807
}
}
}
}
end
context 'with a running server' do
before do
# We need to send a request to localhost
WebMock.allow_net_connect!
config_file.write(YAML.dump(config))
config_file.close
@pid = MetricsServer.spawn('sidekiq', gitlab_config: config_file.path)
end
after do
webmock_enable!
if @pid
Timeout.timeout(5) do
Process.kill('TERM', @pid)
Process.waitpid(@pid)
end
end
rescue Errno::ESRCH => _
# 'No such process' means the process died before
ensure
config_file.unlink
end
it 'serves /metrics endpoint' do
expect do
Timeout.timeout(5) do
http_ok = false
until http_ok
sleep 1
response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true)
http_ok = response&.success?
end
end
end.not_to raise_error
end
end
end

View File

@ -0,0 +1,115 @@
import { shallowMount } from '@vue/test-utils';
import App from '~/google_cloud/components/app.vue';
import Home from '~/google_cloud/components/home.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
const BASE_FEEDBACK_URL =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new';
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findGcpError = () => wrapper.findComponent(GcpError);
const findNoGcpProjects = () => wrapper.findComponent(NoGcpProjects);
const findServiceAccountsForm = () => wrapper.findComponent(ServiceAccountsForm);
const findHome = () => wrapper.findComponent(Home);
afterEach(() => {
wrapper.destroy();
});
describe('for gcp_error screen', () => {
beforeEach(() => {
const propsData = {
screen: 'gcp_error',
error: 'mock_gcp_client_error',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the gcp_error screen', () => {
expect(findGcpError().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for no_gcp_projects screen', () => {
beforeEach(() => {
const propsData = {
screen: 'no_gcp_projects',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the no_gcp_projects screen', () => {
expect(findNoGcpProjects().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for service_accounts_form screen', () => {
beforeEach(() => {
const propsData = {
screen: 'service_accounts_form',
gcpProjects: [1, 2, 3],
environments: [4, 5, 6],
cancelPath: '',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the service_accounts_form screen', () => {
expect(findServiceAccountsForm().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for home screen', () => {
beforeEach(() => {
const propsData = {
screen: 'home',
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the home screen', () => {
expect(findHome().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
});

View File

@ -0,0 +1,61 @@
import { shallowMount } from '@vue/test-utils';
import { GlTab, GlTabs } from '@gitlab/ui';
import Home from '~/google_cloud/components/home.vue';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
describe('google_cloud Home component', () => {
let wrapper;
const findTabs = () => wrapper.findComponent(GlTabs);
const findTabItems = () => findTabs().findAllComponents(GlTab);
const findTabItemsModel = () =>
findTabs()
.findAllComponents(GlTab)
.wrappers.map((x) => ({
title: x.attributes('title'),
disabled: x.attributes('disabled'),
}));
const TEST_HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
beforeEach(() => {
const propsData = {
screen: 'home',
...TEST_HOME_PROPS,
};
wrapper = shallowMount(Home, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
describe('google_cloud App tabs', () => {
it('should contain tabs', () => {
expect(findTabs().exists()).toBe(true);
});
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
{ title: 'Deployments', disabled: '' },
{ title: 'Services', disabled: '' },
]);
});
describe('configuration tab', () => {
it('should contain service accounts component', () => {
const serviceAccounts = findTabItems().at(0).findComponent(ServiceAccountsList);
expect(serviceAccounts.props()).toEqual({
list: TEST_HOME_PROPS.serviceAccounts,
createUrl: TEST_HOME_PROPS.createServiceAccountUrl,
emptyIllustrationUrl: TEST_HOME_PROPS.emptyIllustrationUrl,
});
});
});
});
});

View File

@ -1,66 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlTab, GlTabs } from '@gitlab/ui';
import App from '~/google_cloud/components/screens/app.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findTabs = () => wrapper.findComponent(GlTabs);
const findTabItems = () => findTabs().findAllComponents(GlTab);
const findConfigurationTab = () => findTabItems().at(0);
const findDeploymentTab = () => findTabItems().at(1);
const findServicesTab = () => findTabItems().at(2);
const findServiceAccountsList = () => findConfigurationTab().findComponent(ServiceAccountsList);
beforeEach(() => {
const propsData = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
wrapper = shallowMount(App, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().exists()).toBe(true);
});
describe('google_cloud App tabs', () => {
it('should contain tabs', () => {
expect(findTabs().exists()).toBe(true);
});
it('should contain three tab items', () => {
expect(findTabItems().length).toBe(3);
});
describe('configuration tab', () => {
it('should exist', () => {
expect(findConfigurationTab().exists()).toBe(true);
});
it('should contain service accounts component', () => {
expect(findServiceAccountsList().exists()).toBe(true);
});
});
describe('deployments tab', () => {
it('should exist', () => {
expect(findDeploymentTab().exists()).toBe(true);
});
});
describe('services tab', () => {
it('should exist', () => {
expect(findServicesTab().exists()).toBe(true);
});
});
});
});

View File

@ -1,12 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsForm from '~/google_cloud/components/screens/service_accounts_form.vue';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
describe('ServiceAccountsForm component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findHeader = () => wrapper.find('header');
const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findAllFormSelects = () => wrapper.findAllComponents(GlFormSelect);
@ -22,10 +20,6 @@ describe('ServiceAccountsForm component', () => {
wrapper.destroy();
});
it('contains incubation banner', () => {
expect(findIncubationBanner().exists()).toBe(true);
});
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});

View File

@ -16,6 +16,7 @@ import { createStore } from '~/integrations/edit/store';
describe('IntegrationForm', () => {
let wrapper;
let dispatch;
const createComponent = ({
customStateProps = {},
@ -23,12 +24,15 @@ describe('IntegrationForm', () => {
initialState = {},
props = {},
} = {}) => {
const store = createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
...initialState,
});
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMountExtended(IntegrationForm, {
propsData: { ...props },
store: createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
...initialState,
}),
store,
stubs: {
OverrideDropdown,
ActiveCheckbox,
@ -195,13 +199,29 @@ describe('IntegrationForm', () => {
});
describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => {
createComponent({
customStateProps: { type: 'jira' },
});
beforeEach(() => {
jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
createComponent({
customStateProps: { type: 'jira', testPath: '/test' },
});
});
it('renders JiraTriggerFields', () => {
expect(findJiraTriggerFields().exists()).toBe(true);
});
it('renders JiraIssuesFields', () => {
expect(findJiraIssuesFields().exists()).toBe(true);
});
describe('when JiraIssueFields emits `request-jira-issue-types` event', () => {
it('dispatches `requestJiraIssueTypes` action', () => {
findJiraIssuesFields().vm.$emit('request-jira-issue-types');
expect(dispatch).toHaveBeenCalledWith('requestJiraIssueTypes', expect.any(FormData));
});
});
});
describe('triggerEvents is present', () => {

View File

@ -1,10 +1,7 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
GET_JIRA_ISSUE_TYPES_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
} from '~/integrations/constants';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
@ -216,13 +213,11 @@ describe('JiraIssuesFields', () => {
);
});
it('emits "getJiraIssueTypes" to the eventHub when the jira-vulnerabilities component requests to fetch issue types', async () => {
const eventHubEmitSpy = jest.spyOn(eventHub, '$emit');
it('emits "request-jira-issue-types` when the jira-vulnerabilities component requests to fetch issue types', async () => {
await setEnableCheckbox(true);
await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
await findJiraForVulnerabilities().vm.$emit('request-jira-issue-types');
expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT);
expect(wrapper.emitted('request-jira-issue-types')).toHaveLength(1);
});
});

View File

@ -14,3 +14,9 @@ export const mockIntegrationProps = {
type: '',
inheritFromId: 25,
};
export const mockJiraIssueTypes = [
{ id: '1', name: 'issue', description: 'issue' },
{ id: '2', name: 'bug', description: 'bug' },
{ id: '3', name: 'epic', description: 'epic' },
];

View File

@ -1,4 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } from '~/integrations/constants';
import {
setOverride,
setIsSaving,
@ -14,14 +17,21 @@ import {
import * as types from '~/integrations/edit/store/mutation_types';
import createState from '~/integrations/edit/store/state';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockJiraIssueTypes } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
describe('Integration form store actions', () => {
let state;
let mockAxios;
beforeEach(() => {
state = createState();
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
mockAxios.restore();
});
describe('setOverride', () => {
@ -75,11 +85,28 @@ describe('Integration form store actions', () => {
});
describe('requestJiraIssueTypes', () => {
it('should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations', () => {
return testAction(requestJiraIssueTypes, null, state, [
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
]);
describe.each`
scenario | responseCode | response | action
${'when successful'} | ${200} | ${{ issuetypes: mockJiraIssueTypes }} | ${{ type: 'receiveJiraIssueTypesSuccess', payload: mockJiraIssueTypes }}
${'when response has no issue types'} | ${200} | ${{ issuetypes: [] }} | ${{ type: 'receiveJiraIssueTypesError', payload: I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE }}
${'when response includes error'} | ${200} | ${{ error: new Error() }} | ${{ type: 'receiveJiraIssueTypesError', payload: I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE }}
${'when error occurs'} | ${500} | ${{}} | ${{ type: 'receiveJiraIssueTypesError', payload: expect.any(String) }}
`('$scenario', ({ responseCode, response, action }) => {
it(`should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations, and dispatch ${action.type}`, () => {
mockAxios.onPut('/test').replyOnce(responseCode, response);
return testAction(
requestJiraIssueTypes,
new FormData(),
{ propsSource: { testPath: '/test' } },
[
// should clear the error messages and set the loading state
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
],
[action],
);
});
});
});

View File

@ -4,10 +4,8 @@ import eventHub from '~/integrations/edit/event_hub';
import axios from '~/lib/utils/axios_utils';
import toast from '~/vue_shared/plugins/global_toast';
import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
GET_JIRA_ISSUE_TYPES_EVENT,
TOGGLE_INTEGRATION_EVENT,
TEST_INTEGRATION_EVENT,
SAVE_INTEGRATION_EVENT,
@ -154,62 +152,6 @@ describe('IntegrationSettingsForm', () => {
});
});
describe('when event hub receives `GET_JIRA_ISSUE_TYPES_EVENT`', () => {
it('should always dispatch `requestJiraIssueTypes`', () => {
const dispatchSpy = mockStoreDispatch();
mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
});
it('should make an ajax request with provided `formData`', () => {
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
expect(axios.put).toHaveBeenCalledWith(
integrationSettingsForm.testEndPoint,
new FormData(integrationSettingsForm.$form),
);
});
it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
const dispatchSpy = mockStoreDispatch();
const mockData = ['ISSUE', 'EPIC'];
mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
issuetypes: mockData,
});
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
});
it.each(['Custom error message here', undefined])(
'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
async (responseErrorMessage) => {
const dispatchSpy = mockStoreDispatch();
const expectedErrorMessage =
responseErrorMessage || I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE;
mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: responseErrorMessage,
});
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith(
'receiveJiraIssueTypesError',
expectedErrorMessage,
);
},
);
});
describe('when event hub receives `SAVE_INTEGRATION_EVENT`', () => {
describe('when form is valid', () => {
beforeEach(() => {

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../metrics_server/metrics_server'
require_relative '../support/helpers/next_instance_of'
RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
include NextInstanceOf
describe '.spawn' do
let(:env) do
{
'METRICS_SERVER_TARGET' => 'sidekiq',
'GITLAB_CONFIG' => nil
}
end
it 'spawns a process with the correct environment variables and detaches it' do
expect(Process).to receive(:spawn).with(env, anything, err: $stderr, out: $stdout).and_return(99)
expect(Process).to receive(:detach).with(99)
described_class.spawn('sidekiq')
end
end
describe '#start' do
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
let(:exporter_double) { double('fake_exporter', start: true) }
let(:prometheus_client_double) { double(::Prometheus::Client) }
let(:prometheus_config) { ::Prometheus::Client::Configuration.new }
let(:metrics_dir) { Dir.mktmpdir }
let(:settings_double) { double(:settings, sidekiq_exporter: {}) }
subject(:metrics_server) { described_class.new('fake', metrics_dir)}
before do
stub_env('prometheus_multiproc_dir', metrics_dir)
stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
allow(exporter_class).to receive(:instance).with({}, synchronous: true).and_return(exporter_double)
allow(Settings).to receive(:monitoring).and_return(settings_double)
end
after do
Dir.rmdir(metrics_dir)
end
it 'configures ::Prometheus::Client' do
allow(prometheus_client_double).to receive(:configuration).and_return(prometheus_config)
metrics_server.start
expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
end
it 'ensures that metrics directory exists in correct mode (0700)' do
expect(FileUtils).to receive(:mkdir_p).with(metrics_dir, mode: 0700)
metrics_server.start
end
it 'removes any old metrics files' do
FileUtils.touch("#{metrics_dir}/remove_this.db")
expect { metrics_server.start }.to change { Dir.empty?(metrics_dir) }.from(false).to(true)
end
it 'starts a metrics server' do
expect(exporter_double).to receive(:start)
metrics_server.start
end
it 'sends the correct Settings to the exporter instance' do
expect(Settings).to receive(:monitoring).and_return(settings_double)
expect(settings_double).to receive(:sidekiq_exporter)
metrics_server.start
end
end
end

View File

@ -152,6 +152,16 @@ RSpec.describe API::Terraform::State do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end
context 'when serial already exists' do
let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version } }
it 'returns unprocessable entity' do
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
end
context 'without body' do

View File

@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
.to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
.to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
.to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
.to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
end
end

View File

@ -0,0 +1,18 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# This script assumes the first argument is a path to a file containing a list of changed files and the second argument
# is the path of a file where a list of end-to-end spec files with the leading 'qa/' trimmed will be written to if
# all the files are end-to-end test spec files.
abort("ERROR: Please specify the file containing the list of changed files and a file where the qa only spec files will be written") if ARGV.size != 2
file_contents = File.read(ARGV.shift).split(' ')
all_files_are_qa_specs = file_contents.all? { |file_path| file_path =~ %r{^qa\/qa\/specs\/features\/} }
output_file = ARGV.shift
if all_files_are_qa_specs
qa_spec_paths_trimmed = file_contents.map { |path| path.sub('qa/', '') }
File.write(output_file, qa_spec_paths_trimmed.join(' '))
end

View File

@ -33,6 +33,7 @@ module Quality
initializers
javascripts
lib
metrics_server
models
policies
presenters