diff options
70 files changed, 789 insertions, 354 deletions
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 01fdc186602..7505ae1de68 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -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: diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue new file mode 100644 index 00000000000..64784755b66 --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/app.vue @@ -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> diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue new file mode 100644 index 00000000000..05f39de66ee --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/home.vue @@ -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> diff --git a/app/assets/javascripts/google_cloud/components/screens/app.vue b/app/assets/javascripts/google_cloud/components/screens/app.vue deleted file mode 100644 index 52c9b478916..00000000000 --- a/app/assets/javascripts/google_cloud/components/screens/app.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/google_cloud/components/screens/service_accounts_form.vue b/app/assets/javascripts/google_cloud/components/service_accounts_form.vue index 6aead296918..e7a09668473 100644 --- a/app/assets/javascripts/google_cloud/components/screens/service_accounts_form.vue +++ b/app/assets/javascripts/google_cloud/components/service_accounts_form.vue @@ -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> diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js index ba67877e005..ab9e8227812 100644 --- a/app/assets/javascripts/google_cloud/index.js +++ b/app/assets/javascripts/google_cloud/index.js @@ -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 }), + }); }; diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index d214ee4ded6..977811f81a4 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -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'; diff --git a/app/assets/javascripts/integrations/edit/api.js b/app/assets/javascripts/integrations/edit/api.js new file mode 100644 index 00000000000..7bce5604f9d --- /dev/null +++ b/app/assets/javascripts/integrations/edit/api.js @@ -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); +}; diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index ba1aeb28616..767810950b1 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -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"> diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue index 7cbfb35aeaa..cd0624d6b5c 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue @@ -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" diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js index 400397c050c..b81ae1b1cb6 100644 --- a/app/assets/javascripts/integrations/edit/store/actions.js +++ b/app/assets/javascripts/integrations/edit/store/actions.js @@ -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); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index f519fc87c46..2b6959ed1cd 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -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}`); diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb index 8bfe5c9c5f3..aff305ab7d6 100644 --- a/app/controllers/projects/google_cloud/base_controller.rb +++ b/app/controllers/projects/google_cloud/base_controller.rb @@ -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 diff --git a/app/controllers/projects/google_cloud/service_accounts_controller.rb b/app/controllers/projects/google_cloud/service_accounts_controller.rb index 21b096a6c66..d5db4fabf88 100644 --- a/app/controllers/projects/google_cloud/service_accounts_controller.rb +++ b/app/controllers/projects/google_cloud/service_accounts_controller.rb @@ -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 diff --git a/app/controllers/projects/google_cloud_controller.rb b/app/controllers/projects/google_cloud_controller.rb index 6cc67391d6c..1fa8ae60376 100644 --- a/app/controllers/projects/google_cloud_controller.rb +++ b/app/controllers/projects/google_cloud_controller.rb @@ -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') diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index b10837d4e21..e6dd62fab34 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -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? diff --git a/app/views/projects/google_cloud/errors/gcp_error.html.haml b/app/views/projects/google_cloud/errors/gcp_error.html.haml index b91a85250b3..69e481501d5 100644 --- a/app/views/projects/google_cloud/errors/gcp_error.html.haml +++ b/app/views/projects/google_cloud/errors/gcp_error.html.haml @@ -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 } diff --git a/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml b/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml index 743b757de57..69e481501d5 100644 --- a/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml +++ b/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml @@ -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 } diff --git a/app/views/projects/google_cloud/service_accounts/index.html.haml b/app/views/projects/google_cloud/service_accounts/index.html.haml index 69b2123d723..9b82bc0acb5 100644 --- a/app/views/projects/google_cloud/service_accounts/index.html.haml +++ b/app/views/projects/google_cloud/service_accounts/index.html.haml @@ -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 } diff --git a/bin/metrics-server b/bin/metrics-server new file mode 100755 index 00000000000..48d3a6402c3 --- /dev/null +++ b/bin/metrics-server @@ -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 diff --git a/db/migrate/20211119085015_add_orignal_filename_to_ci_job_artifact.rb b/db/migrate/20211119085015_add_orignal_filename_to_ci_job_artifact.rb new file mode 100644 index 00000000000..f52bc346d16 --- /dev/null +++ b/db/migrate/20211119085015_add_orignal_filename_to_ci_job_artifact.rb @@ -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 diff --git a/db/post_migrate/20211119085036_add_text_limit_to_job_artifact_original_filename.rb b/db/post_migrate/20211119085036_add_text_limit_to_job_artifact_original_filename.rb new file mode 100644 index 00000000000..3eccbb1b3a4 --- /dev/null +++ b/db/post_migrate/20211119085036_add_text_limit_to_job_artifact_original_filename.rb @@ -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 diff --git a/db/schema_migrations/20211119085015 b/db/schema_migrations/20211119085015 new file mode 100644 index 00000000000..874bd158e7c --- /dev/null +++ b/db/schema_migrations/20211119085015 @@ -0,0 +1 @@ +88b289d724f98f75e0340cde4c6e2bc3cb55df2a979934fb2bc544d22e4c032d
\ No newline at end of file diff --git a/db/schema_migrations/20211119085036 b/db/schema_migrations/20211119085036 new file mode 100644 index 00000000000..f9d1eeffbcb --- /dev/null +++ b/db/schema_migrations/20211119085036 @@ -0,0 +1 @@ +2b2c28e0370ae1bb84bee5ff769c9b313902d1f1afc50fa54e23a1627b1121f3
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d57b66ad602..cd081495e34 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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 diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md index 569f6e02ea4..43f2414e8f9 100644 --- a/doc/install/relative_url.md +++ b/doc/install/relative_url.md @@ -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 diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 6b7854b6ef9..db7e7d74efe 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -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`. diff --git a/doc/integration/datadog.md b/doc/integration/datadog.md index 1c699bb622e..89e08d330e8 100644 --- a/doc/integration/datadog.md +++ b/doc/integration/datadog.md @@ -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) diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 2f9ec7a41c2..757fb53ec2e 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -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 diff --git a/doc/operations/incident_management/integrations.md b/doc/operations/incident_management/integrations.md index 92f5a50b1c3..de7316b5ae3 100644 --- a/doc/operations/incident_management/integrations.md +++ b/doc/operations/incident_management/integrations.md @@ -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. diff --git a/doc/topics/autodevops/multiple_clusters_auto_devops.md b/doc/topics/autodevops/multiple_clusters_auto_devops.md index c6df5ac9e02..8156ae7c7ac 100644 --- a/doc/topics/autodevops/multiple_clusters_auto_devops.md +++ b/doc/topics/autodevops/multiple_clusters_auto_devops.md @@ -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: diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md index 10ca3430b48..7e3d927f31d 100644 --- a/doc/user/application_security/dast/browser_based.md +++ b/doc/user/application_security/dast/browser_based.md @@ -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`, diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 9ec5343708e..42fc2f532e1 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -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)** diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md index 963fca34827..b4d7790df1d 100644 --- a/doc/user/project/integrations/asana.md +++ b/doc/user/project/integrations/asana.md @@ -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**. diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index bcaedbc4b10..7a96bb74e3f 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -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 diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 119f219499c..f3f8d900e12 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -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 diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md index 93a3490e4b6..8b17f4afaa8 100644 --- a/doc/user/project/integrations/pivotal_tracker.md +++ b/doc/user/project/integrations/pivotal_tracker.md @@ -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**. diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index d399c7f2901..87f38c3482b 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -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 diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 91f12a36070..3a91a33fc4d 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -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**. diff --git a/doc/user/project/merge_requests/approvals/rules.md b/doc/user/project/merge_requests/approvals/rules.md index 1249aa826fa..f4393b2b76d 100644 --- a/doc/user/project/merge_requests/approvals/rules.md +++ b/doc/user/project/merge_requests/approvals/rules.md @@ -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 diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 54e411fa9c9..15ba6e9de98 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -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 diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md index 597dcb3dfb9..be26ce9176f 100644 --- a/doc/user/project/merge_requests/reviews/index.md +++ b/doc/user/project/merge_requests/reviews/index.md @@ -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 diff --git a/doc/user/project/pages/getting_started/pages_forked_sample_project.md b/doc/user/project/pages/getting_started/pages_forked_sample_project.md index 7640bf1f72d..70b3e1543e7 100644 --- a/doc/user/project/pages/getting_started/pages_forked_sample_project.md +++ b/doc/user/project/pages/getting_started/pages_forked_sample_project.md @@ -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. diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md index bf4ef21e31b..13fd9572dc5 100644 --- a/doc/user/project/repository/forking_workflow.md +++ b/doc/user/project/repository/forking_workflow.md @@ -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)** diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 1c95f2bb5ec..03e7ca6db4a 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -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. diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index f6dfbcafbb6..29e71611092 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -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 diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb index 483bfe12c68..255fa0169bf 100644 --- a/lib/gitlab/utils/strong_memoize.rb +++ b/lib/gitlab/utils/strong_memoize.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_dependency 'gitlab/utils' - module Gitlab module Utils module StrongMemoize diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb index ccc4787601a..3a08aeb9116 100644 --- a/lib/sidebars/projects/menus/infrastructure_menu.rb +++ b/lib/sidebars/projects/menus/infrastructure_menu.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d16a6726157..de1793a7e8c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -37172,7 +37172,7 @@ msgstr "" msgid "Unknown response text" msgstr "" -msgid "Unknown root" +msgid "Unknown screen" msgstr "" msgid "Unknown user" diff --git a/metrics_server/dependencies.rb b/metrics_server/dependencies.rb new file mode 100644 index 00000000000..ecfef502feb --- /dev/null +++ b/metrics_server/dependencies.rb @@ -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 diff --git a/metrics_server/metrics_server.rb b/metrics_server/metrics_server.rb new file mode 100644 index 00000000000..09171d8220b --- /dev/null +++ b/metrics_server/metrics_server.rb @@ -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 diff --git a/metrics_server/settings_overrides.rb b/metrics_server/settings_overrides.rb new file mode 100644 index 00000000000..4239f62ad1c --- /dev/null +++ b/metrics_server/settings_overrides.rb @@ -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 diff --git a/scripts/override_rails_constants.rb b/scripts/override_rails_constants.rb new file mode 100644 index 00000000000..1b255dd0011 --- /dev/null +++ b/scripts/override_rails_constants.rb @@ -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 diff --git a/scripts/setup-test-env b/scripts/setup-test-env index a81aaa5cda3..c955a01d769 100755 --- a/scripts/setup-test-env +++ b/scripts/setup-test-env @@ -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' diff --git a/scripts/trigger-build b/scripts/trigger-build index e5fa55f8582..11f9f5cb07c 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -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 diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb new file mode 100644 index 00000000000..4eff9136f2b --- /dev/null +++ b/spec/commands/metrics_server/metrics_server_spec.rb @@ -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 diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js new file mode 100644 index 00000000000..64e5ac8586f --- /dev/null +++ b/spec/frontend/google_cloud/components/app_spec.js @@ -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`, + }); + }); + }); +}); diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js new file mode 100644 index 00000000000..9b4c3a79f11 --- /dev/null +++ b/spec/frontend/google_cloud/components/home_spec.js @@ -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, + }); + }); + }); + }); +}); diff --git a/spec/frontend/google_cloud/components/screens/app_spec.js b/spec/frontend/google_cloud/components/screens/app_spec.js deleted file mode 100644 index bd766a79f51..00000000000 --- a/spec/frontend/google_cloud/components/screens/app_spec.js +++ /dev/null @@ -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); - }); - }); - }); -}); diff --git a/spec/frontend/google_cloud/components/screens/service_accounts_form_spec.js b/spec/frontend/google_cloud/components/service_accounts_form_spec.js index 3899b7d8a9d..5394d0cdaef 100644 --- a/spec/frontend/google_cloud/components/screens/service_accounts_form_spec.js +++ b/spec/frontend/google_cloud/components/service_accounts_form_spec.js @@ -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); }); diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index 0a9cbadb249..6767714d214 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -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', () => { + beforeEach(() => { + jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form')); + createComponent({ - customStateProps: { type: 'jira' }, + 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', () => { diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js index 3a664b652ac..b5a8eed3598 100644 --- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js @@ -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); }); }); diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js index 27ba0768331..3c45ed0fb1b 100644 --- a/spec/frontend/integrations/edit/mock_data.js +++ b/spec/frontend/integrations/edit/mock_data.js @@ -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' }, +]; diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js index e2f4c138ece..3cf69c65126 100644 --- a/spec/frontend/integrations/edit/store/actions_spec.js +++ b/spec/frontend/integrations/edit/store/actions_spec.js @@ -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], + ); + }); }); }); diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js index c35d178e518..dc9a551e078 100644 --- a/spec/frontend/integrations/integration_settings_form_spec.js +++ b/spec/frontend/integrations/integration_settings_form_spec.js @@ -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(() => { diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb new file mode 100644 index 00000000000..58556d2cf5a --- /dev/null +++ b/spec/metrics_server/metrics_server_spec.rb @@ -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 diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 5d2635126e8..24f38b04348 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -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 diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb index 94fa9d682e1..3348e495732 100644 --- a/spec/tooling/quality/test_level_spec.rb +++ b/spec/tooling/quality/test_level_spec.rb @@ -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 diff --git a/tooling/bin/qa/check_if_qa_only_spec_changes b/tooling/bin/qa/check_if_qa_only_spec_changes new file mode 100755 index 00000000000..fd331559dd7 --- /dev/null +++ b/tooling/bin/qa/check_if_qa_only_spec_changes @@ -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 diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb index 5fbaad073c0..3cc1d87eb36 100644 --- a/tooling/quality/test_level.rb +++ b/tooling/quality/test_level.rb @@ -33,6 +33,7 @@ module Quality initializers javascripts lib + metrics_server models policies presenters |