diff options
39 files changed, 445 insertions, 157 deletions
diff --git a/.gitlab/merge_request_templates/Stable Branch.md b/.gitlab/merge_request_templates/Stable Branch.md new file mode 100644 index 00000000000..e584296cbb1 --- /dev/null +++ b/.gitlab/merge_request_templates/Stable Branch.md @@ -0,0 +1,18 @@ +<!-- +Merging into stable branches is reserved for GitLab patch releases +https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases +--> + +## What does this MR do and why? + +_Describe in detail what merge request is being backported and why_ + +## MR acceptance checklist + +This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability. + +* [ ] This MR is backporting a bug fix, documentation update, or spec fix, previously merged in the default branch. +* [ ] The original MR has been deployed to GitLab.com (not applicable for documentation or spec changes). +* [ ] Ensure the `e2e:package-and-test` job has either succeeded or been approved by a Software Engineer in Test. + +/assign me diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue index 5401c7c1c28..6130b15a3bc 100644 --- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue @@ -1,9 +1,13 @@ <script> import { GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; -import { DEFAULT_PLATFORM, DEFAULT_ACCESS_LEVEL } from '../constants'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM } from '../constants'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; export default { name: 'AdminNewRunnerApp', @@ -12,7 +16,7 @@ export default { GlSprintf, RunnerInstructionsModal, RunnerPlatformsRadioGroup, - RunnerFormFields, + RunnerCreateForm, }, directives: { GlModal: GlModalDirective, @@ -26,17 +30,21 @@ export default { data() { return { platform: DEFAULT_PLATFORM, - runner: { - description: '', - maintenanceNote: '', - paused: false, - accessLevel: DEFAULT_ACCESS_LEVEL, - runUntagged: false, - tagList: '', - maximumTimeout: ' ', - }, }; }, + methods: { + onSaved(runner) { + const registerUrl = setUrlParams( + { [PARAM_KEY_PLATFORM]: this.platform }, + runner.registerAdminUrl, + ); + saveAlertToLocalStorage({ message: __('Runner created.'), variant: VARIANT_SUCCESS }); + redirectTo(registerUrl); + }, + onError(error) { + createAlert({ message: error.message }); + }, + }, modalId: 'runners-legacy-registration-instructions-modal', }; </script> @@ -73,6 +81,6 @@ export default { <hr aria-hidden="true" /> - <runner-form-fields v-model="runner" /> + <runner-create-form @saved="onSaved" @error="onError" /> </div> </template> diff --git a/app/assets/javascripts/ci/runner/admin_register_runner/index.js b/app/assets/javascripts/ci/runner/admin_register_runner/index.js new file mode 100644 index 00000000000..edb2ec65e98 --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_register_runner/index.js @@ -0,0 +1,5 @@ +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; + +export const initAdminRegisterRunner = () => { + showAlertFromLocalStorage(); +}; diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue new file mode 100644 index 00000000000..5d2a3c53842 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue @@ -0,0 +1,71 @@ +<script> +import { GlForm, GlButton } from '@gitlab/ui'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; +import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql'; +import { modelToUpdateMutationVariables } from 'ee_else_ce/ci/runner/runner_update_form_utils'; +import { captureException } from '../sentry_utils'; +import { DEFAULT_ACCESS_LEVEL } from '../constants'; + +export default { + name: 'RunnerCreateForm', + components: { + GlForm, + GlButton, + RunnerFormFields, + }, + data() { + return { + saving: false, + runner: { + description: '', + maintenanceNote: '', + paused: false, + accessLevel: DEFAULT_ACCESS_LEVEL, + runUntagged: false, + tagList: '', + maximumTimeout: '', + }, + }; + }, + methods: { + async onSubmit() { + this.saving = true; + try { + const { + data: { + runnerCreate: { errors, runner }, + }, + } = await this.$apollo.mutate({ + mutation: runnerCreateMutation, + variables: modelToUpdateMutationVariables(this.runner), + }); + + if (errors?.length) { + this.$emit('error', new Error(errors.join(' '))); + } else { + this.onSuccess(runner); + } + } catch (error) { + captureException({ error, component: this.$options.name }); + this.$emit('error', error); + } finally { + this.saving = false; + } + }, + onSuccess(runner) { + this.$emit('saved', runner); + }, + }, +}; +</script> +<template> + <gl-form @submit.prevent="onSubmit"> + <runner-form-fields v-model="runner" /> + + <div class="gl-display-flex"> + <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving"> + {{ __('Submit') }} + </gl-button> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 318eb7e74bd..27c02420036 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -129,6 +129,8 @@ export const PARAM_KEY_SORT = 'sort'; export const PARAM_KEY_AFTER = 'after'; export const PARAM_KEY_BEFORE = 'before'; +export const PARAM_KEY_PLATFORM = 'platform'; + // CiRunnerType export const INSTANCE_TYPE = 'INSTANCE_TYPE'; diff --git a/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql new file mode 100644 index 00000000000..d14a594e378 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql @@ -0,0 +1,9 @@ +mutation runnerCreate($input: RunnerCreateInput!) { + runnerCreate(input: $input) { + runner { + id + registerAdminUrl + } + errors + } +} diff --git a/app/assets/javascripts/pages/admin/runners/register/index.js b/app/assets/javascripts/pages/admin/runners/register/index.js new file mode 100644 index 00000000000..d7ee2ee369a --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/register/index.js @@ -0,0 +1,3 @@ +import { initAdminRegisterRunner } from '~/ci/runner/admin_register_runner'; + +initAdminRegisterRunner(); diff --git a/app/assets/javascripts/token_access/components/token_access_app.vue b/app/assets/javascripts/token_access/components/token_access_app.vue index 59d59757735..089159ac87b 100644 --- a/app/assets/javascripts/token_access/components/token_access_app.vue +++ b/app/assets/javascripts/token_access/components/token_access_app.vue @@ -1,5 +1,4 @@ <script> -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import OutboundTokenAccess from './outbound_token_access.vue'; import InboundTokenAccess from './inbound_token_access.vue'; import OptInJwt from './opt_in_jwt.vue'; @@ -10,17 +9,11 @@ export default { InboundTokenAccess, OptInJwt, }, - mixins: [glFeatureFlagMixin()], - computed: { - inboundTokenAccessEnabled() { - return this.glFeatures.ciInboundJobTokenScope; - }, - }, }; </script> <template> <div> - <inbound-token-access v-if="inboundTokenAccessEnabled" class="gl-pb-5" /> + <inbound-token-access class="gl-pb-5" /> <outbound-token-access class="gl-py-5" /> <opt-in-jwt /> </div> diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 4ca665679c0..b330aacf3e9 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -12,10 +12,6 @@ module Projects before_action :check_builds_available! before_action :define_variables - before_action do - push_frontend_feature_flag(:ci_inbound_job_token_scope, @project) - end - helper_method :highlight_badge feature_category :continuous_integration diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb index d214aa46cfc..fcba729d460 100644 --- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb +++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb @@ -39,8 +39,6 @@ module Mutations def resolve(full_path:, **args) project = authorized_find!(full_path) - args.delete(:inbound_job_token_scope_enabled) unless Feature.enabled?(:ci_inbound_job_token_scope, project) - settings = project.ci_cd_settings settings.update(args) diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index 20775077bd8..f389c642fd8 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -58,8 +58,7 @@ module Ci end def inbound_accessible?(accessed_project) - # if the flag or setting is disabled any project is considered to be in scope. - return true unless Feature.enabled?(:ci_inbound_job_token_scope, accessed_project) + # if the setting is disabled any project is considered to be in scope. return true unless accessed_project.ci_inbound_job_token_scope_enabled? inbound_linked_as_accessible?(accessed_project) diff --git a/app/models/project.rb b/app/models/project.rb index b1bb0ff2bbb..c88ef4fb9db 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2979,7 +2979,7 @@ class Project < ApplicationRecord end def ci_inbound_job_token_scope_enabled? - return false unless ci_cd_settings + return true unless ci_cd_settings ci_cd_settings.inbound_job_token_scope_enabled? end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index 8741a341ad3..cc9003423be 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -20,10 +20,6 @@ class ProjectCiCdSetting < ApplicationRecord attribute :forward_deployment_enabled, default: true attribute :separated_caches, default: true - default_value_for :inbound_job_token_scope_enabled do |settings| - Feature.enabled?(:ci_inbound_job_token_scope, settings.project) - end - chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval def keep_latest_artifacts_available? diff --git a/app/services/ci/job_token_scope/add_project_service.rb b/app/services/ci/job_token_scope/add_project_service.rb index 15553ad6e92..4f745042f07 100644 --- a/app/services/ci/job_token_scope/add_project_service.rb +++ b/app/services/ci/job_token_scope/add_project_service.rb @@ -6,8 +6,6 @@ module Ci include EditScopeValidations def execute(target_project, direction: :outbound) - direction = :outbound if Feature.disabled?(:ci_inbound_job_token_scope) - validate_edit!(project, target_project, current_user) link = allowlist(direction) diff --git a/config/feature_flags/development/ci_inbound_job_token_scope.yml b/config/feature_flags/development/ci_inbound_job_token_scope.yml deleted file mode 100644 index a0e2e09dde5..00000000000 --- a/config/feature_flags/development/ci_inbound_job_token_scope.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_inbound_job_token_scope -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99165 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376063 -milestone: '15.5' -type: development -group: group::pipeline execution -default_enabled: true diff --git a/danger/z_metadata/Dangerfile b/danger/z_metadata/Dangerfile index 9140bb7d988..1c36c02b69e 100644 --- a/danger/z_metadata/Dangerfile +++ b/danger/z_metadata/Dangerfile @@ -17,9 +17,3 @@ has_milestone = !gitlab.mr_json["milestone"].nil? unless has_milestone || (helper.security_mr? && helper.mr_target_branch == default_branch) warn "This merge request does not refer to an existing milestone.", sticky: false end - -has_pick_into_stable_label = helper.mr_labels.find { |label| label.start_with?('Pick into') } - -if helper.mr_target_branch != default_branch && !has_pick_into_stable_label && !helper.security_mr? - warn "Most of the time, merge requests should target `#{default_branch}`. Otherwise, please set the relevant `Pick into X.Y` label." -end diff --git a/db/post_migrate/20230216233937_remove_application_settings_send_user_confirmation_email_column.rb b/db/post_migrate/20230216233937_remove_application_settings_send_user_confirmation_email_column.rb new file mode 100644 index 00000000000..d7720ebccbd --- /dev/null +++ b/db/post_migrate/20230216233937_remove_application_settings_send_user_confirmation_email_column.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveApplicationSettingsSendUserConfirmationEmailColumn < Gitlab::Database::Migration[2.1] + def change + remove_column :application_settings, :send_user_confirmation_email, :boolean, default: false + end +end diff --git a/db/schema_migrations/20230216233937 b/db/schema_migrations/20230216233937 new file mode 100644 index 00000000000..d3c85c7c981 --- /dev/null +++ b/db/schema_migrations/20230216233937 @@ -0,0 +1 @@ +5088eccec1327f61cb80c5fca4f7e7710534179c2d6bf820f7021dfd079d51a5
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 96c344f6fd9..bc18ea90ba6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11293,7 +11293,6 @@ CREATE TABLE application_settings ( metrics_packet_size integer DEFAULT 1, disabled_oauth_sign_in_sources text, health_check_access_token character varying, - send_user_confirmation_email boolean DEFAULT false, container_registry_token_expire_delay integer DEFAULT 5, after_sign_up_text text, user_default_external boolean DEFAULT false NOT NULL, diff --git a/doc/user/application_security/api_security/api_discovery/index.md b/doc/user/application_security/api_security/api_discovery/index.md index 12e67737293..e916bf879cf 100644 --- a/doc/user/application_security/api_security/api_discovery/index.md +++ b/doc/user/application_security/api_security/api_discovery/index.md @@ -39,16 +39,6 @@ API Discovery is tested with and officially supports LTS versions of the Java ru Only applications that are built as Spring Boot [executable JARs](https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.nested-jars.jar-structure) are supported. -### Example configurations - -The following are working example projects: - -- [API Discovery configured as a pipeline job](https://gitlab.com/gitlab-org/security-products/demos/api-discovery/spring-boot-pipeline) -<!-- -- [API Discovery integrated into a Maven build process](http://...TODO) -- [API Discovery integrated into a Gradle build process](http://...TODO) ---> - ### Configure as pipeline job The easiest way to run API Discovery is through a pipeline job based on our CI template. diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 786045684b8..a50208d78d7 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -109,13 +109,15 @@ module API failure [ { code: 422, message: 'Push access levels access level has already been taken' }, { code: 404, message: '404 Project Not Found' }, - { code: 401, message: '401 Unauthorized' } + { code: 401, message: '401 Unauthorized' }, + { code: 400, message: '400 Bad request' } ] end params do requires :name, type: String, desc: 'The name of the branch', documentation: { example: 'main' } optional :allow_force_push, type: Boolean, - desc: 'Allow force push for all users with push access.' + desc: 'Allow force push for all users with push access.', + allow_blank: false use :optional_params_ee end diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb index 50a67a746f8..0962ad9f028 100644 --- a/lib/gitlab/import_export/project/object_builder.rb +++ b/lib/gitlab/import_export/project/object_builder.rb @@ -64,6 +64,7 @@ module Gitlab if label? atts['type'] = 'ProjectLabel' # Always create project labels + atts.delete('group_id') elsif milestone? if atts['group_id'] # Transform new group milestones into project ones atts['iid'] = nil diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 76fbb839683..d67ea731230 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -36883,6 +36883,9 @@ msgstr "" msgid "Runner API" msgstr "" +msgid "Runner created." +msgstr "" + msgid "Runner tokens" msgstr "" diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js index dd7b0796f6e..1d8ae6ebd3f 100644 --- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js +++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js @@ -3,30 +3,42 @@ import VueApollo from 'vue-apollo'; import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue'; +import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; -import { DEFAULT_PLATFORM } from '~/ci/runner/constants'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { runnerCreateResult } from '../mock_data'; const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN'; Vue.use(VueApollo); +jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage'); +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + redirectTo: jest.fn(), +})); + +const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner; + describe('AdminNewRunnerApp', () => { let wrapper; const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link'); const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup); - const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields); + const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm); - const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { - wrapper = mountFn(AdminNewRunnerApp, { + const createComponent = () => { + wrapper = shallowMountExtended(AdminNewRunnerApp, { propsData: { legacyRegistrationToken: mockLegacyRegistrationToken, - ...props, }, directives: { GlModal: createMockDirective('gl-modal'), @@ -34,7 +46,6 @@ describe('AdminNewRunnerApp', () => { stubs: { GlSprintf, }, - ...options, }); }; @@ -56,25 +67,59 @@ describe('AdminNewRunnerApp', () => { }); }); - describe('New runner form fields', () => { - describe('Platform', () => { - it('shows the platforms radio group', () => { - expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM); - }); + describe('Platform', () => { + it('shows the platforms radio group', () => { + expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM); + }); + }); + + describe('Runner form', () => { + it('shows the runner create form', () => { + expect(findRunnerCreateForm().exists()).toBe(true); }); - describe('Runner', () => { - it('shows the runners fields', () => { - expect(findRunnerFormFields().props('value')).toEqual({ - accessLevel: 'NOT_PROTECTED', - paused: false, - description: '', - maintenanceNote: '', - maximumTimeout: ' ', - runUntagged: false, - tagList: '', + describe('When a runner is saved', () => { + beforeEach(() => { + findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner); + }); + + it('pushes an alert to be shown after redirection', () => { + expect(saveAlertToLocalStorage).toHaveBeenCalledWith({ + message: expect.any(String), + variant: VARIANT_SUCCESS, }); }); + + it('redirects to the registration page', () => { + const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`; + + expect(redirectTo).toHaveBeenCalledWith(url); + }); + }); + + describe('When another platform is selected and a runner is saved', () => { + beforeEach(() => { + findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM); + findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner); + }); + + it('redirects to the registration page with the platform', () => { + const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`; + + expect(redirectTo).toHaveBeenCalledWith(url); + }); + }); + + describe('When runner fails to save', () => { + const ERROR_MSG = 'Cannot save!'; + + beforeEach(() => { + findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG)); + }); + + it('shows an error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG }); + }); }); }); }); diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js new file mode 100644 index 00000000000..1123a026a4d --- /dev/null +++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js @@ -0,0 +1,170 @@ +import Vue from 'vue'; +import { GlForm } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; +import { DEFAULT_ACCESS_LEVEL } from '~/ci/runner/constants'; +import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql'; +import { captureException } from '~/ci/runner/sentry_utils'; +import { runnerCreateResult } from '../mock_data'; + +jest.mock('~/ci/runner/sentry_utils'); + +const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner; + +const defaultRunnerModel = { + description: '', + accessLevel: DEFAULT_ACCESS_LEVEL, + paused: false, + maintenanceNote: '', + maximumTimeout: '', + runUntagged: false, + tagList: '', +}; + +Vue.use(VueApollo); + +describe('RunnerCreateForm', () => { + let wrapper; + let runnerCreateHandler; + + const findForm = () => wrapper.findComponent(GlForm); + const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields); + const findSubmitBtn = () => wrapper.find('[type="submit"]'); + + const createComponent = () => { + wrapper = shallowMountExtended(RunnerCreateForm, { + apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]), + }); + }; + + beforeEach(() => { + runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult); + + createComponent(); + }); + + it('shows default runner values', () => { + expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel); + }); + + it('shows a submit button', () => { + expect(findSubmitBtn().exists()).toBe(true); + }); + + describe('when user submits', () => { + let preventDefault; + + beforeEach(() => { + preventDefault = jest.fn(); + + findRunnerFormFields().vm.$emit('input', { + ...defaultRunnerModel, + description: 'My runner', + maximumTimeout: 0, + tagList: 'tag1, tag2', + }); + }); + + describe('immediately after submit', () => { + beforeEach(() => { + findForm().vm.$emit('submit', { preventDefault }); + }); + + it('prevents default form submission', () => { + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + it('shows a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(true); + }); + + it('saves runner', async () => { + expect(runnerCreateHandler).toHaveBeenCalledWith({ + input: { + ...defaultRunnerModel, + description: 'My runner', + maximumTimeout: 0, + tagList: ['tag1', 'tag2'], + }, + }); + }); + }); + + describe('when saved successfully', () => { + beforeEach(async () => { + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "saved" result', async () => { + expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + }); + + describe('when a server error occurs', () => { + const error = new Error('Error!'); + + beforeEach(async () => { + runnerCreateHandler.mockRejectedValue(error); + + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "error" result', async () => { + expect(wrapper.emitted('error')[0]).toEqual([error]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + + it('reports error', () => { + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith({ + component: 'RunnerCreateForm', + error, + }); + }); + }); + + describe('when a validation error occurs', () => { + const errorMsg1 = 'Issue1!'; + const errorMsg2 = 'Issue2!'; + + beforeEach(async () => { + runnerCreateHandler.mockResolvedValue({ + data: { + runnerCreate: { + errors: [errorMsg1, errorMsg2], + runner: null, + }, + }, + }); + + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "error" results', async () => { + expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + + it('does not report error', () => { + expect(captureException).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js index 5cdf0ea4e3b..e16f4fbd3a5 100644 --- a/spec/frontend/ci/runner/mock_data.js +++ b/spec/frontend/ci/runner/mock_data.js @@ -9,6 +9,9 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que // Edit runner queries import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json'; +// New runner queries +import runnerCreateResult from 'test_fixtures/graphql/ci/runner/new/runner_create.mutation.graphql.json'; + // List queries import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json'; import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json'; @@ -321,4 +324,5 @@ export { runnerProjectsData, runnerJobsData, runnerFormData, + runnerCreateResult, }; diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index f60e4991292..25e97334504 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -145,6 +145,24 @@ RSpec.describe 'Runner (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end end + + describe 'runner_create.mutation.graphql', type: :request do + runner_create_mutation = 'new/runner_create.mutation.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{runner_create_mutation}") + end + + it "#{fixtures_path}#{runner_create_mutation}.json" do + post_graphql(query, current_user: admin, variables: { + input: { + description: 'My dummy runner' + } + }) + + expect_graphql_errors_to_be_empty + end + end end describe 'as group owner', GraphQL::Query do diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js index 7f269ee5fda..cff16fd125c 100644 --- a/spec/frontend/token_access/token_access_app_spec.js +++ b/spec/frontend/token_access/token_access_app_spec.js @@ -11,12 +11,8 @@ describe('TokenAccessApp component', () => { const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess); const findOptInJwt = () => wrapper.findComponent(OptInJwt); - const createComponent = (flagState = false) => { - wrapper = shallowMount(TokenAccessApp, { - provide: { - glFeatures: { ciInboundJobTokenScope: flagState }, - }, - }); + const createComponent = () => { + wrapper = shallowMount(TokenAccessApp); }; describe('default', () => { @@ -32,12 +28,6 @@ describe('TokenAccessApp component', () => { expect(findOutboundTokenAccess().exists()).toBe(true); }); - it('does not render the inbound token access component', () => { - expect(findInboundTokenAccess().exists()).toBe(false); - }); - }); - - describe('with feature flag enabled', () => { it('renders the inbound token access component', () => { createComponent(true); diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb index 189b798c2e8..5fa8590e8fd 100644 --- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb @@ -86,13 +86,16 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do 'group' => group)).to eq(group_label) end - it 'creates a new label' do + it 'creates a new project label' do label = described_class.build(Label, 'title' => 'group label', 'project' => project, - 'group' => project.group) + 'group' => project.group, + 'group_id' => project.group.id) expect(label.persisted?).to be true + expect(label).to be_an_instance_of(ProjectLabel) + expect(label.group_id).to be_nil end end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index bb735d6689e..a07fe4fd29c 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -295,6 +295,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i it 'has project labels' do expect(ProjectLabel.count).to eq(3) + expect(ProjectLabel.pluck(:group_id).compact).to be_empty end it 'has merge request approvals' do diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index 9ae061a3702..51f0f4878e7 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -160,13 +160,5 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f include_examples 'enforces outbound scope only' end - - context 'when inbound scope flag disabled' do - before do - stub_feature_flags(ci_inbound_job_token_scope: false) - end - - include_examples 'enforces outbound scope only' - end end end diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb index 2c490c33747..0a818147bfc 100644 --- a/spec/models/project_ci_cd_setting_spec.rb +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -27,22 +27,8 @@ RSpec.describe ProjectCiCdSetting do end end - describe '#set_default_for_inbound_job_token_scope_enabled' do - context 'when feature flag ci_inbound_job_token_scope is enabled' do - before do - stub_feature_flags(ci_inbound_job_token_scope: true) - end - - it { is_expected.to be_inbound_job_token_scope_enabled } - end - - context 'when feature flag ci_inbound_job_token_scope is disabled' do - before do - stub_feature_flags(ci_inbound_job_token_scope: false) - end - - it { is_expected.not_to be_inbound_job_token_scope_enabled } - end + describe '#default_for_inbound_job_token_scope_enabled' do + it { is_expected.to be_inbound_job_token_scope_enabled } end describe '#default_git_depth' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9db1dde6294..ab50a3aa480 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1162,7 +1162,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end describe '#ci_inbound_job_token_scope_enabled?' do - it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_' do + it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_', default: true do let(:delegated_method) { :inbound_job_token_scope_enabled? } end end diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb index 99e55c44773..0951d165d46 100644 --- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb @@ -101,21 +101,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr expect(response).to have_gitlab_http_status(:success) expect(project.ci_inbound_job_token_scope_enabled).to eq(true) end - - context 'when ci_inbound_job_token_scope disabled' do - before do - stub_feature_flags(ci_inbound_job_token_scope: false) - end - - it 'does not update inbound_job_token_scope_enabled' do - post_graphql_mutation(mutation, current_user: user) - - project.reload - - expect(response).to have_gitlab_http_status(:success) - expect(project.ci_inbound_job_token_scope_enabled).to eq(true) - end - end end it 'updates ci_opt_in_jwt' do diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 8e8a25a8dc2..463893afd13 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -266,6 +266,15 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end.to change { protected_branch.reload.allow_force_push }.from(false).to(true) expect(response).to have_gitlab_http_status(:ok) end + + context 'when allow_force_push is not set' do + it 'responds with a bad request error' do + patch api(route, user), params: { allow_force_push: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'allow_force_push is empty' + end + end end context 'when returned protected branch is invalid' do diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb index 3caf58da4d2..f1af1760e8d 100644 --- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do end end -RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| +RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: '', default: false| using RSpec::Parameterized::TableSyntax context 'when ci_cd_settings is nil' do @@ -28,7 +28,7 @@ RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| end it 'returns false' do - expect(project.send("#{prefix}#{delegated_method}")).to be(false) + expect(project.send("#{prefix}#{delegated_method}")).to be(default) end end diff --git a/tooling/lib/tooling/kubernetes_client.rb b/tooling/lib/tooling/kubernetes_client.rb index ab914db5777..27eb4c8151e 100644 --- a/tooling/lib/tooling/kubernetes_client.rb +++ b/tooling/lib/tooling/kubernetes_client.rb @@ -35,6 +35,19 @@ module Tooling delete_namespaces_by_exact_names(resource_names: namespaces, wait: wait) end + def delete_namespaces_by_exact_names(resource_names:, wait:) + command = [ + 'delete', + 'namespace', + '--now', + '--ignore-not-found', + %(--wait=#{wait}), + resource_names.join(' ') + ] + + run_command(command) + end + private def delete_by_selector(release_name:, wait:) @@ -74,19 +87,6 @@ module Tooling run_command(command) end - def delete_namespaces_by_exact_names(resource_names:, wait:) - command = [ - 'delete', - 'namespace', - '--now', - '--ignore-not-found', - %(--wait=#{wait}), - resource_names.join(' ') - ] - - run_command(command) - end - def delete_by_matching_name(release_name:) resource_names = raw_resource_names command = [ diff --git a/workhorse/go.mod b/workhorse/go.mod index 87e8a844d5c..5fc0d1c1679 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -7,7 +7,7 @@ require ( github.com/BurntSushi/toml v1.2.1 github.com/FZambia/sentinel v1.1.1 github.com/alecthomas/chroma/v2 v2.5.0 - github.com/aws/aws-sdk-go v1.44.201 + github.com/aws/aws-sdk-go v1.44.202 github.com/disintegration/imaging v1.6.2 github.com/getsentry/raven-go v0.2.0 github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index e68da6ed585..cb20a361be5 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -544,8 +544,8 @@ github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4 github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.128/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.151/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.44.201 h1:gKtyFyiVGh/uTW7sCQaoyU6XCUsnI8+WWKmbEaABCfw= -github.com/aws/aws-sdk-go v1.44.201/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.202 h1:nk/DtYoAS7zX4SbfiQEJO+C0GBN8ZxXrkD+BozwLvZk= +github.com/aws/aws-sdk-go v1.44.202/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.17.1 h1:02c72fDJr87N8RAC2s3Qu0YuvMRZKNZJ9F+lAehCazk= github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw= |