diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-07 09:08:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-07 09:08:10 +0300 |
commit | f23de8014c9104ab62c68e88b4c8e924469cd996 (patch) | |
tree | a513f5c77fe54ff68309f1b0508422c5d2d3b39f | |
parent | 896eadaa13c8852efa907d2555de883d07c38e48 (diff) |
Add latest changes from gitlab-org/gitlab@master
32 files changed, 798 insertions, 64 deletions
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index 31500b919f3..d84a9a4a4b5 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -8,17 +8,22 @@ import { GlFormGroup, GlFormInput, GlFormTextarea, + GlLoadingIcon, } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { createAlert } from '~/alert'; -import { visitUrl } from '~/lib/utils/url_utility'; +import { visitUrl, queryToObject } from '~/lib/utils/url_utility'; import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; import RefSelector from '~/ref/components/ref_selector.vue'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql'; +import updatePipelineScheduleMutation from '../graphql/mutations/update_pipeline_schedule.mutation.graphql'; +import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql'; import { VARIABLE_TYPE, FILE_TYPE } from '../constants'; +const scheduleId = queryToObject(window.location.search).id; + export default { components: { GlButton, @@ -29,20 +34,12 @@ export default { GlFormGroup, GlFormInput, GlFormTextarea, + GlLoadingIcon, RefSelector, TimezoneDropdown, IntervalPatternInput, }, - inject: [ - 'fullPath', - 'projectId', - 'defaultBranch', - 'cron', - 'cronTimezone', - 'dailyLimit', - 'settingsLink', - 'schedulesPath', - ], + inject: ['fullPath', 'projectId', 'defaultBranch', 'dailyLimit', 'settingsLink', 'schedulesPath'], props: { timezoneData: { type: Array, @@ -58,24 +55,74 @@ export default { required: true, }, }, + apollo: { + schedule: { + query: getPipelineSchedulesQuery, + variables() { + return { + projectPath: this.fullPath, + ids: scheduleId, + }; + }, + update(data) { + return data.project?.pipelineSchedules?.nodes[0] || {}; + }, + result({ data }) { + if (data) { + const { + project: { + pipelineSchedules: { nodes }, + }, + } = data; + + const schedule = nodes[0]; + const variables = schedule.variables?.nodes || []; + + this.description = schedule.description; + this.cron = schedule.cron; + this.cronTimezone = schedule.cronTimezone; + this.scheduleRef = schedule.ref; + this.variables = variables.map((variable) => { + return { + id: variable.id, + variableType: variable.variableType, + key: variable.key, + value: variable.value, + destroy: false, + }; + }); + this.addEmptyVariable(); + this.activated = schedule.active; + } + }, + skip() { + return !this.editing; + }, + error() { + createAlert({ message: this.$options.i18n.scheduleFetchError }); + }, + }, + }, data() { return { - cronValue: this.cron, + cron: '', description: '', scheduleRef: this.defaultBranch, activated: true, - timezone: this.cronTimezone, + cronTimezone: '', variables: [], + schedule: {}, }; }, i18n: { activated: __('Activated'), - cronTimezone: s__('PipelineSchedules|Cron timezone'), + cronTimezoneText: s__('PipelineSchedules|Cron timezone'), description: s__('PipelineSchedules|Description'), shortDescriptionPipeline: s__( 'PipelineSchedules|Provide a short description for this pipeline', ), - savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'), + editScheduleBtnText: s__('PipelineSchedules|Edit pipeline schedule'), + createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'), cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), intervalPattern: s__('PipelineSchedules|Interval Pattern'), @@ -87,6 +134,12 @@ export default { scheduleCreateError: s__( 'PipelineSchedules|An error occurred while creating the pipeline schedule.', ), + scheduleUpdateError: s__( + 'PipelineSchedules|An error occurred while updating the pipeline schedule.', + ), + scheduleFetchError: s__( + 'PipelineSchedules|An error occurred while trying to fetch the pipeline schedule.', + ), }, typeOptions: { [VARIABLE_TYPE]: __('Variable'), @@ -114,9 +167,26 @@ export default { getEnabledRefTypes() { return [REF_TYPE_BRANCHES, REF_TYPE_TAGS]; }, - preparedVariables() { + preparedVariablesUpdate() { return this.variables.filter((variable) => variable.key !== ''); }, + preparedVariablesCreate() { + return this.preparedVariablesUpdate.map((variable) => { + return { + key: variable.key, + value: variable.value, + variableType: variable.variableType, + }; + }); + }, + loading() { + return this.$apollo.queries.schedule.loading; + }, + buttonText() { + return this.editing + ? this.$options.i18n.editScheduleBtnText + : this.$options.i18n.createScheduleBtnText; + }, }, created() { this.addEmptyVariable(); @@ -133,6 +203,7 @@ export default { variableType: VARIABLE_TYPE, key: '', value: '', + destroy: false, }); }, setVariableAttribute(key, attribute, value) { @@ -140,16 +211,11 @@ export default { variable[attribute] = value; }, removeVariable(index) { - this.variables.splice(index, 1); + this.variables[index].destroy = true; }, canRemove(index) { return index < this.variables.length - 1; }, - scheduleHandler() { - if (!this.editing) { - this.createPipelineSchedule(); - } - }, async createPipelineSchedule() { try { const { @@ -161,10 +227,10 @@ export default { variables: { input: { description: this.description, - cron: this.cronValue, - cronTimezone: this.timezone, + cron: this.cron, + cronTimezone: this.cronTimezone, ref: this.scheduleRef, - variables: this.preparedVariables, + variables: this.preparedVariablesCreate, active: this.activated, projectPath: this.fullPath, }, @@ -180,11 +246,48 @@ export default { createAlert({ message: this.$options.i18n.scheduleCreateError }); } }, + async updatePipelineSchedule() { + try { + const { + data: { + pipelineScheduleUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: updatePipelineScheduleMutation, + variables: { + input: { + id: this.schedule.id, + description: this.description, + cron: this.cron, + cronTimezone: this.cronTimezone, + ref: this.scheduleRef, + variables: this.preparedVariablesUpdate, + active: this.activated, + }, + }, + }); + + if (errors.length > 0) { + createAlert({ message: errors[0] }); + } else { + visitUrl(this.schedulesPath); + } + } catch { + createAlert({ message: this.$options.i18n.scheduleUpdateError }); + } + }, + scheduleHandler() { + if (this.editing) { + this.updatePipelineSchedule(); + } else { + this.createPipelineSchedule(); + } + }, setCronValue(cron) { - this.cronValue = cron; + this.cron = cron; }, setTimezone(timezone) { - this.timezone = timezone.identifier || ''; + this.cronTimezone = timezone.identifier || ''; }, }, }; @@ -192,7 +295,8 @@ export default { <template> <div class="col-lg-8 gl-pl-0"> - <gl-form> + <gl-loading-icon v-if="loading && editing" size="lg" /> + <gl-form v-else> <!--Description--> <gl-form-group :label="$options.i18n.description" label-for="schedule-description"> <gl-form-input @@ -215,10 +319,10 @@ export default { /> </gl-form-group> <!--Timezone--> - <gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone"> + <gl-form-group :label="$options.i18n.cronTimezoneText" label-for="schedule-timezone"> <timezone-dropdown id="schedule-timezone" - :value="timezone" + :value="cronTimezone" :timezone-data="timezoneData" name="schedule-timezone" @input="setTimezone" @@ -242,12 +346,12 @@ export default { <div v-for="(variable, index) in variables" :key="`var-${index}`" - class="gl-mb-3 gl-pb-2" - data-testid="ci-variable-row" data-qa-selector="ci_variable_row_container" > <div - class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row" + v-if="!variable.destroy" + class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2" + data-testid="ci-variable-row" > <gl-dropdown :text="$options.typeOptions[variable.variableType]" @@ -308,7 +412,7 @@ export default { </gl-form-checkbox> <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler"> - {{ $options.i18n.savePipelineSchedule }} + {{ buttonText }} </gl-button> <gl-button :href="schedulesPath" data-testid="schedule-cancel-button"> {{ $options.i18n.cancel }} diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql new file mode 100644 index 00000000000..a6a937af74a --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql @@ -0,0 +1,6 @@ +mutation updatePipelineSchedule($input: PipelineScheduleUpdateInput!) { + pipelineScheduleUpdate(input: $input) { + clientMutationId + errors + } +} diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql index 0e091afb9d7..29a26be0344 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql +++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql @@ -1,15 +1,22 @@ -query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStatus) { +query getPipelineSchedulesQuery( + $projectPath: ID! + $status: PipelineScheduleStatus + $ids: [ID!] = null +) { currentUser { id username } project(fullPath: $projectPath) { id - pipelineSchedules(status: $status) { + pipelineSchedules(status: $status, ids: $ids) { count nodes { id description + cron + cronTimezone + ref forTag editPath refPath @@ -35,6 +42,14 @@ query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStat name webPath } + variables { + nodes { + id + variableType + key + value + } + } userPermissions { playPipelineSchedule updatePipelineSchedule diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js index 749e3d0a69f..6bf121d39b6 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js +++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js @@ -18,10 +18,8 @@ export default (selector, editing = false) => { const { fullPath, - cron, dailyLimit, timezoneData, - cronTimezone, projectId, defaultBranch, settingsLink, @@ -37,8 +35,6 @@ export default (selector, editing = false) => { projectId, defaultBranch, dailyLimit: dailyLimit ?? '', - cronTimezone: cronTimezone ?? '', - cron: cron ?? '', settingsLink, schedulesPath, }, diff --git a/app/graphql/mutations/ci/pipeline_schedule/update.rb b/app/graphql/mutations/ci/pipeline_schedule/update.rb index a0b5e793ecb..aff0a5494e7 100644 --- a/app/graphql/mutations/ci/pipeline_schedule/update.rb +++ b/app/graphql/mutations/ci/pipeline_schedule/update.rb @@ -43,7 +43,7 @@ module Mutations def resolve(id:, variables: [], **pipeline_schedule_attrs) schedule = authorized_find!(id: id) - params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h)) + params = pipeline_schedule_attrs.merge(variables_attributes: variable_attributes_for(variables)) service_response = ::Ci::PipelineSchedules::UpdateService .new(schedule, current_user, params) @@ -54,6 +54,18 @@ module Mutations errors: service_response.errors } end + + private + + def variable_attributes_for(variables) + variables.map do |variable| + variable.to_h.tap do |hash| + hash[:id] = GlobalID::Locator.locate(hash[:id]).id if hash[:id] + + hash[:_destroy] = hash.delete(:destroy) + end + end + end end end end diff --git a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb index 54a6ad92448..eb6a78eb67a 100644 --- a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb +++ b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb @@ -8,11 +8,18 @@ module Mutations description 'Attributes for the pipeline schedule variable.' + PipelineScheduleVariableID = ::Types::GlobalIDType[::Ci::PipelineScheduleVariable] + + argument :id, PipelineScheduleVariableID, required: false, description: 'ID of the variable to mutate.' + argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.' argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.' argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.' + + argument :destroy, GraphQL::Types::Boolean, required: false, + description: 'Boolean option to destroy the variable.' end end end diff --git a/app/helpers/ci/pipeline_schedules_helper.rb b/app/helpers/ci/pipeline_schedules_helper.rb new file mode 100644 index 00000000000..e5125353b99 --- /dev/null +++ b/app/helpers/ci/pipeline_schedules_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedulesHelper + def js_pipeline_schedules_form_data(project, schedule) + { + full_path: project.full_path, + daily_limit: schedule.daily_limit, + timezone_data: timezone_data.to_json, + project_id: project.id, + default_branch: project.default_branch, + settings_link: project_settings_ci_cd_path(project), + schedules_path: pipeline_schedules_path(project) + } + end + end +end + +Ci::PipelineSchedulesHelper.prepend_mod_with('Ci::PipelineSchedulesHelper') diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml index 3f843ce6aec..4e1ae53a101 100644 --- a/app/views/projects/pipeline_schedules/edit.html.haml +++ b/app/views/projects/pipeline_schedules/edit.html.haml @@ -5,9 +5,8 @@ %h1.page-title.gl-font-size-h-display = _("Edit Pipeline Schedule") -%hr - if Feature.enabled?(:pipeline_schedules_vue, @project) - #pipeline-schedules-form-edit{ data: { full_path: @project.full_path } } + #pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) } - else = render "form" diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml index 89836e6d091..ef99a79b06f 100644 --- a/app/views/projects/pipeline_schedules/new.html.haml +++ b/app/views/projects/pipeline_schedules/new.html.haml @@ -9,6 +9,6 @@ = _("Schedule a new pipeline") - if Feature.enabled?(:pipeline_schedules_vue, @project) - #pipeline-schedules-form-new{ data: { full_path: @project.full_path, cron: @schedule.cron, daily_limit: @schedule.daily_limit, timezone_data: timezone_data.to_json, cron_timezone: @schedule.cron_timezone, project_id: @project.id, default_branch: @project.default_branch, settings_link: project_settings_ci_cd_path(@project), schedules_path: pipeline_schedules_path(@project) } } + #pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) } - else = render "form" diff --git a/db/docs/batched_background_migrations/backfill_missing_ci_cd_settings.yml b/db/docs/batched_background_migrations/backfill_missing_ci_cd_settings.yml new file mode 100644 index 00000000000..aa6ba2684af --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_missing_ci_cd_settings.yml @@ -0,0 +1,6 @@ +--- +migration_job_name: BackfillMissingCiCdSettings +description: Backfills ci_cd_settings for projects that do not have them +feature_category: source_code_management +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393502 +milestone: 16.2 diff --git a/db/migrate/20230703115902_add_relay_state_allowlist_application_settings.rb b/db/migrate/20230703115902_add_relay_state_allowlist_application_settings.rb new file mode 100644 index 00000000000..3de7470f113 --- /dev/null +++ b/db/migrate/20230703115902_add_relay_state_allowlist_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddRelayStateAllowlistApplicationSettings < Gitlab::Database::Migration[2.1] + def change + add_column :application_settings, :relay_state_domain_allowlist, + :text, + array: true, + default: [], + null: false + end +end diff --git a/db/migrate/20230703121859_add_relay_state_allowlist_saml_providers.rb b/db/migrate/20230703121859_add_relay_state_allowlist_saml_providers.rb new file mode 100644 index 00000000000..b05059d1d61 --- /dev/null +++ b/db/migrate/20230703121859_add_relay_state_allowlist_saml_providers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddRelayStateAllowlistSamlProviders < Gitlab::Database::Migration[2.1] + def change + add_column :saml_providers, :relay_state_domain_allowlist, + :text, + array: true, + default: [], + null: false + end +end diff --git a/db/post_migrate/20230628023103_queue_backfill_missing_ci_cd_settings.rb b/db/post_migrate/20230628023103_queue_backfill_missing_ci_cd_settings.rb new file mode 100644 index 00000000000..0fc39e96e18 --- /dev/null +++ b/db/post_migrate/20230628023103_queue_backfill_missing_ci_cd_settings.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class QueueBackfillMissingCiCdSettings < Gitlab::Database::Migration[2.1] + MIGRATION = "BackfillMissingCiCdSettings" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 500 + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :projects, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :projects, :id, []) + end +end diff --git a/db/schema_migrations/20230628023103 b/db/schema_migrations/20230628023103 new file mode 100644 index 00000000000..57a9e342467 --- /dev/null +++ b/db/schema_migrations/20230628023103 @@ -0,0 +1 @@ +59e4b358359514dbb49b2b73c829a99f646100442f02aa36287935d6e8fa76ab
\ No newline at end of file diff --git a/db/schema_migrations/20230703115902 b/db/schema_migrations/20230703115902 new file mode 100644 index 00000000000..471eb5becbb --- /dev/null +++ b/db/schema_migrations/20230703115902 @@ -0,0 +1 @@ +8a16b05cd573528b6e8baa2d86e761a2b431584c026918e3eda9a630b30ec727
\ No newline at end of file diff --git a/db/schema_migrations/20230703121859 b/db/schema_migrations/20230703121859 new file mode 100644 index 00000000000..f72e3201352 --- /dev/null +++ b/db/schema_migrations/20230703121859 @@ -0,0 +1 @@ +149cdb7863460246fb89d02d3c8e1709bdb1d38378304d44c9a916c4bd4ee4ed
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1c4cd81a119..c1d3a6325aa 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11771,6 +11771,7 @@ CREATE TABLE application_settings ( gitlab_shell_operation_limit integer DEFAULT 600, elasticsearch_requeue_workers boolean DEFAULT false NOT NULL, elasticsearch_worker_number_of_shards integer DEFAULT 2 NOT NULL, + relay_state_domain_allowlist text[] DEFAULT '{}'::text[] NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), @@ -22230,7 +22231,8 @@ CREATE TABLE saml_providers ( enforced_group_managed_accounts boolean DEFAULT false NOT NULL, prohibited_outer_forks boolean DEFAULT true NOT NULL, default_membership_role smallint DEFAULT 10 NOT NULL, - git_check_enforced boolean DEFAULT false NOT NULL + git_check_enforced boolean DEFAULT false NOT NULL, + relay_state_domain_allowlist text[] DEFAULT '{}'::text[] NOT NULL ); CREATE SEQUENCE saml_providers_id_seq diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 75c2776ae47..65c3072fa8b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -27181,6 +27181,12 @@ A `CiPipelineScheduleID` is a global ID. It is encoded as a string. An example `CiPipelineScheduleID` is: `"gid://gitlab/Ci::PipelineSchedule/1"`. +### `CiPipelineScheduleVariableID` + +A `CiPipelineScheduleVariableID` is a global ID. It is encoded as a string. + +An example `CiPipelineScheduleVariableID` is: `"gid://gitlab/Ci::PipelineScheduleVariable/1"`. + ### `CiRunnerID` A `CiRunnerID` is a global ID. It is encoded as a string. @@ -29032,6 +29038,8 @@ Attributes for the pipeline schedule variable. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="pipelineschedulevariableinputdestroy"></a>`destroy` | [`Boolean`](#boolean) | Boolean option to destroy the variable. | +| <a id="pipelineschedulevariableinputid"></a>`id` | [`CiPipelineScheduleVariableID`](#cipipelineschedulevariableid) | ID of the variable to mutate. | | <a id="pipelineschedulevariableinputkey"></a>`key` | [`String!`](#string) | Name of the variable. | | <a id="pipelineschedulevariableinputvalue"></a>`value` | [`String!`](#string) | Value of the variable. | | <a id="pipelineschedulevariableinputvariabletype"></a>`variableType` | [`CiVariableType!`](#civariabletype) | Type of the variable. | diff --git a/doc/ci/index.md b/doc/ci/index.md index 75e668290c8..1196ecc8779 100644 --- a/doc/ci/index.md +++ b/doc/ci/index.md @@ -83,6 +83,7 @@ GitLab CI/CD features, grouped by DevOps stage, include: | [Connect to cloud services](cloud_services/index.md) | Connect to cloud providers using OpenID Connect (OIDC) to retrieve temporary credentials to access services or secrets. | | **Verify** | | | [CI services](services/index.md) | Link Docker containers with your base image. | +| [Code Quality](testing/code_quality.md) | Analyze your source code quality. | | [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. | | [Interactive Web Terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | | [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | @@ -98,11 +99,19 @@ GitLab CI/CD features, grouped by DevOps stage, include: | [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. | | [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. | | **Secure** | | -| [Code Quality](testing/code_quality.md) | Analyze your source code quality. | -| [Container Scanning](../user/application_security/container_scanning/index.md) | Check your Docker containers for known vulnerabilities. | +| [Container Scanning](../user/application_security/container_scanning/index.md) | Scan your container images for known vulnerabilities. | +| [Coverage-guided fuzz testing](../user/application_security/coverage_fuzzing/index.md) | Test your application's behavior by providing randomized input. | +| [Dynamic Application Security Testing](../user/application_security/dast/index.md) | Test your application's runtime behavior for vulnerabilities. | | [Dependency Scanning](../user/application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. | +| [Infrastructure as Code scanning](../user/application_security/iac_scanning/index.md) | Scan your IaC configuration files for known vulnerabilities. | | [License Compliance](../user/compliance/license_compliance/index.md) | Search your project dependencies for their licenses. | -| [Security Test reports](../user/application_security/index.md) | Check for app vulnerabilities. | +| [Secret Detection](../user/application_security/secret_detection/index.md) | Search your application's source code for secrets. | +| [Static Application Security Testing](../user/application_security/sast/index.md) | Test your application's source code for known vulnerabilities. | +| [Web API fuzz testing](../user/application_security/api_fuzzing/index.md) | Test your application's API behavior by providing randomized input. | +| **Govern** | | +| [Compliance frameworks](../user/group/compliance_frameworks.md) | Enforce a GitLab CI/CD configuration on all projects in a group. | +| [Scan execution policies](../user/application_security/policies/scan-execution-policies.md) | Enforce security scans run on a specified schedule or with the project pipeline. | +| [Scan results policies](../user/application_security/policies/scan-result-policies.md) | Enforce action based on results of a pipeline security scan. | ## Examples diff --git a/doc/ci/runners/runners_scope.md b/doc/ci/runners/runners_scope.md index 83fc163791a..89051d8950f 100644 --- a/doc/ci/runners/runners_scope.md +++ b/doc/ci/runners/runners_scope.md @@ -5,9 +5,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# The scope of runners **(FREE)** +# Manage runners -Runners are available based on who you want to have access: +GitLab Runner has the following types of runners, which are available based on who you want to have access: - [Shared runners](#shared-runners) are available to all groups and projects in a GitLab instance. - [Group runners](#group-runners) are available to all projects and subgroups in a group. diff --git a/doc/user/application_security/vulnerabilities/severities.md b/doc/user/application_security/vulnerabilities/severities.md index ab90ac18b8e..c199ea2e00d 100644 --- a/doc/user/application_security/vulnerabilities/severities.md +++ b/doc/user/application_security/vulnerabilities/severities.md @@ -18,6 +18,32 @@ most to least severe: - `Info` - `Unknown` +GitLab analyzers make an effort to fit the severity descriptions below, but they may not always be correct. Analyzers and scanners provided by third-party vendors may not follow the same classification. + +## Critical severity + +Vulnerabilities identified at the Critical Severity level should be investigated immediately. Vulnerabilities at this level assume exploitation of the flaw could lead to full system or data compromise. Examples of critical severity flaws are Command/Code Injection and SQL Injection. Typically these flaws are rated with CVSS 3.1 between 9.0-10.0. + +## High severity + +High severity vulnerabilities can be characterized as flaws that may lead to an attacker accessing application resources or unintended exposure of data. Examples of high severity flaws are External XML Entity Injection (XXE), Server Side Request Forgery (SSRF), Local File Include/Path Traversal and certain forms of Cross-Site Scripting (XSS). Typically these flaws are rated with CVSS 3.1 between 7.0-8.9. + +## Medium severity + +Medium severity vulnerabilities usually arise from misconfiguration of systems or lack of security controls. Exploitation of these vulnerabilities may lead to accessing a restricted amount of data or could be used in conjunction with other flaws to gain unintended access to systems or resources. Examples of medium severity flaws are reflected XSS, incorrect HTTP session handling, and missing security controls. Typically these flaws are rated with CVSS 3.1 between 4.0-6.9. + +## Low severity + +Low severity vulnerabilities contain flaws that may not be directly exploitable but introduce unnecessary weakness to an application or system. These flaws are usually due to missing security controls, or unnecessary disclose information about the application environment. Examples of low severity vulnerabilities are missing cookie security directives, verbose error or exception messages. Typically these flaws are rated with CVSS 3.1 between 1.0-3.9. + +## Info severity + +Info level severity vulnerabilities contain information that may have value, but are not necessarily associated to a particular flaw or weakness. Typically these issues do not have a CVSS rating. + +## Unknown severity + +Issues identified at this level do not have enough context to clearly demonstrate severity. + Most GitLab vulnerability analyzers are wrappers around popular open source scanning tools. Each open source scanning tool provides their own native vulnerability severity level value. These values can be one of the following: diff --git a/lib/gitlab/background_migration/backfill_missing_ci_cd_settings.rb b/lib/gitlab/background_migration/backfill_missing_ci_cd_settings.rb new file mode 100644 index 00000000000..e3ad63aac2e --- /dev/null +++ b/lib/gitlab/background_migration/backfill_missing_ci_cd_settings.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # backfills project_ci_cd_settings + class BackfillMissingCiCdSettings < BatchedMigrationJob + # migrations only version of `project_ci_cd_settings` table + class ProjectCiCdSetting < ::ApplicationRecord + self.table_name = 'project_ci_cd_settings' + end + + operation_name :backfill_missing_ci_cd_settings + feature_category :source_code_management + + def perform + each_sub_batch do |sub_batch| + sub_batch = sub_batch.where(%{ + NOT EXISTS ( + SELECT 1 + FROM project_ci_cd_settings + WHERE project_ci_cd_settings.project_id = projects.id + ) + }) + next unless sub_batch.present? + + ci_cd_attributes = sub_batch.map do |project| + { + project_id: project.id, + default_git_depth: 20, + forward_deployment_enabled: true + } + end + + ProjectCiCdSetting.insert_all(ci_cd_attributes) + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7bb25f37a6e..6d2c46303bd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33395,6 +33395,12 @@ msgstr "" msgid "PipelineSchedules|An error occurred while creating the pipeline schedule." msgstr "" +msgid "PipelineSchedules|An error occurred while trying to fetch the pipeline schedule." +msgstr "" + +msgid "PipelineSchedules|An error occurred while updating the pipeline schedule." +msgstr "" + msgid "PipelineSchedules|Are you sure you want to delete this pipeline schedule?" msgstr "" @@ -33404,6 +33410,9 @@ msgstr "" msgid "PipelineSchedules|Create a new pipeline schedule" msgstr "" +msgid "PipelineSchedules|Create pipeline schedule" +msgstr "" + msgid "PipelineSchedules|Cron timezone" msgstr "" @@ -33461,9 +33470,6 @@ msgstr "" msgid "PipelineSchedules|Runs with the same project permissions as the schedule owner." msgstr "" -msgid "PipelineSchedules|Save pipeline schedule" -msgstr "" - msgid "PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. " msgstr "" diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 1697533803a..bb48d4dc38d 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -1,5 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; -import { GlForm } from '@gitlab/ui'; +import { GlForm, GlLoadingIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -14,8 +14,14 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql'; +import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql'; +import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql'; import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers'; -import { createScheduleMutationResponse } from '../mock_data'; +import { + createScheduleMutationResponse, + updateScheduleMutationResponse, + mockSinglePipelineScheduleNode, +} from '../mock_data'; Vue.use(VueApollo); @@ -23,8 +29,20 @@ jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), joinPaths: jest.fn().mockReturnValue(''), + queryToObject: jest.fn().mockReturnValue({ id: '1' }), })); +const { + data: { + project: { + pipelineSchedules: { nodes }, + }, + }, +} = mockSinglePipelineScheduleNode; + +const schedule = nodes[0]; +const variables = schedule.variables.nodes; + describe('Pipeline schedules form', () => { let wrapper; const defaultBranch = 'main'; @@ -32,8 +50,13 @@ describe('Pipeline schedules form', () => { const cron = ''; const dailyLimit = ''; + const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode); + const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse); const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse); + const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); const createMockApolloProvider = ( requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]], @@ -52,8 +75,6 @@ describe('Pipeline schedules form', () => { fullPath: 'gitlab-org/gitlab', projectId, defaultBranch, - cron, - cronTimezone: '', dailyLimit, settingsLink: '', schedulesPath: '/root/ci-project/-/pipeline_schedules', @@ -69,6 +90,7 @@ describe('Pipeline schedules form', () => { const findRefSelector = () => wrapper.findComponent(RefSelector); const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button'); const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); // Variables const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row'); const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key'); @@ -187,7 +209,38 @@ describe('Pipeline schedules form', () => { }); }); - describe('schedule creation', () => { + describe('Button text', () => { + it.each` + editing | expectedText + ${true} | ${'Edit pipeline schedule'} + ${false} | ${'Create pipeline schedule'} + `( + 'button text is $expectedText when editing is $editing', + async ({ editing, expectedText }) => { + createComponent(shallowMountExtended, editing, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + await waitForPromises(); + + expect(findSubmitButton().text()).toBe(expectedText); + }, + ); + }); + + describe('Schedule creation', () => { + it('when creating a schedule the query is not called', () => { + createComponent(); + + expect(querySuccessHandler).not.toHaveBeenCalled(); + }); + + it('does not show loading state when creating new schedule', () => { + createComponent(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + describe('schedule creation success', () => { let mock; @@ -259,4 +312,125 @@ describe('Pipeline schedules form', () => { }); }); }); + + describe('Schedule editing', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('shows loading state when editing', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + describe('schedule fetch success', () => { + it('fetches schedule and sets form data correctly', async () => { + createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]); + + expect(querySuccessHandler).toHaveBeenCalled(); + + await waitForPromises(); + + expect(findDescription().element.value).toBe(schedule.description); + expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron); + expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone); + expect(findRefSelector().props('value')).toBe(schedule.ref); + expect(findVariableRows()).toHaveLength(3); + expect(findKeyInputs().at(0).element.value).toBe(variables[0].key); + expect(findKeyInputs().at(1).element.value).toBe(variables[1].key); + expect(findValueInputs().at(0).element.value).toBe(variables[0].value); + expect(findValueInputs().at(1).element.value).toBe(variables[1].value); + }); + }); + + it('schedule fetch failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, queryFailedHandler], + ]); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while trying to fetch the pipeline schedule.', + }); + }); + + it('edit schedule success', async () => { + createComponent(mountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findDescription().element.value = 'Updated schedule'; + findDescription().trigger('change'); + + findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *'); + + // Ensures variable is sent with destroy property set true + findRemoveIcons().at(0).vm.$emit('click'); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({ + input: { + active: schedule.active, + cron: '0 22 16 * *', + cronTimezone: schedule.cronTimezone, + id: schedule.id, + ref: schedule.ref, + description: 'Updated schedule', + variables: [ + { + destroy: true, + id: variables[0].id, + key: variables[0].key, + value: variables[0].value, + variableType: variables[0].variableType, + }, + { + destroy: false, + id: variables[1].id, + key: variables[1].key, + value: variables[1].value, + variableType: variables[1].variableType, + }, + ], + }, + }); + }); + + it('edit schedule failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerFailed], + ]); + + await waitForPromises(); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while updating the pipeline schedule.', + }); + }); + }); }); diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js index 58fc4a616e4..81283a7170b 100644 --- a/spec/frontend/ci/pipeline_schedules/mock_data.js +++ b/spec/frontend/ci/pipeline_schedules/mock_data.js @@ -2,6 +2,7 @@ import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json'; import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json'; import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json'; +import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json'; const { data: { @@ -30,10 +31,10 @@ const { export const mockPipelineScheduleNodes = nodes; export const mockPipelineScheduleCurrentUser = currentUser; - export const mockPipelineScheduleAsGuestNodes = guestNodes; - export const mockTakeOwnershipNodes = takeOwnershipNodes; +export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse; + export const emptyPipelineSchedulesResponse = { data: { project: { @@ -89,4 +90,14 @@ export const createScheduleMutationResponse = { }, }; +export const updateScheduleMutationResponse = { + data: { + pipelineScheduleUpdate: { + clientMutationId: null, + errors: [], + __typename: 'PipelineScheduleUpdatePayload', + }, + }, +}; + export { mockGetPipelineSchedulesGraphQLResponse }; diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb index 3bfe9113e83..7bba7910b87 100644 --- a/spec/frontend/fixtures/pipeline_schedules.rb +++ b/spec/frontend/fixtures/pipeline_schedules.rb @@ -63,6 +63,12 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end + it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do + post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id }) + + expect_graphql_errors_to_be_empty + end + it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do guest = create(:user) project.add_guest(user) diff --git a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb index 564bc95b352..a932002d614 100644 --- a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb +++ b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') } - it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) } + it { expect(described_class.arguments.keys).to match_array(%w[id key value variableType destroy]) } end diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb new file mode 100644 index 00000000000..1ba24a08b58 --- /dev/null +++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integration do + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:pipeline_schedule) { build_stubbed(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:timezones) { [{ identifier: "Pacific/Honolulu", name: "Hawaii" }] } + + let_it_be(:pipeline_schedule_variable) do + build_stubbed(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + + describe '#js_pipeline_schedules_form_data' do + before do + allow(helper).to receive(:timezone_data).and_return(timezones) + end + + it 'returns pipeline schedule form data' do + expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)).to include({ + full_path: project.full_path, + daily_limit: nil, + project_id: project.id, + schedules_path: pipeline_schedules_path(project), + settings_link: project_settings_ci_cd_path(project), + timezone_data: timezones.to_json + }) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb new file mode 100644 index 00000000000..8f7d5f25a80 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillMissingCiCdSettings, schema: 20230628023103, feature_category: :source_code_management do # rubocop:disable Layout/LineLength + let(:projects_table) { table(:projects) } + let(:namespaces_table) { table(:namespaces) } + let(:ci_cd_settings_table) { table(:project_ci_cd_settings) } + + let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') } + + let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } + let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') } + let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let!(:project_1) do + projects_table + .create!( + name: 'project1', + path: 'path1', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_2.id, + visibility_level: 0 + ) + end + + let!(:project_2) do + projects_table + .create!( + name: 'project2', + path: 'path2', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_3.id, + visibility_level: 0 + ) + end + + let!(:project_3) do + projects_table + .create!( + name: 'project3', + path: 'path3', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_4.id, + visibility_level: 0 + ) + end + + let!(:ci_cd_settings_3) do + ci_cd_settings_table.create!(project_id: project_3.id) + end + + let!(:project_4) do + projects_table + .create!( + name: 'project4', + path: 'path4', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_5.id, + visibility_level: 0 + ) + end + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: projects_table.connection) + .perform + end + + it 'creates ci_cd_settings for projects without ci_cd_settings' do + expect { subject }.to change { ci_cd_settings_table.count }.from(1).to(4) + end + + it 'creates ci_cd_settings with default values' do + ci_cd_settings_table.where.not(project_id: ci_cd_settings_3.project_id).each do |ci_cd_setting| + expect(ci_cd_setting.attributes.except('id', 'project_id')).to eq({ + "group_runners_enabled" => true, + "merge_pipelines_enabled" => nil, + "default_git_depth" => 20, + "forward_deployment_enabled" => true, + "merge_trains_enabled" => false, + "auto_rollback_enabled" => false, + "keep_latest_artifact" => false, + "restrict_user_defined_variables" => false, + "job_token_scope_enabled" => false, + "runner_token_expiration_interval" => nil, + "separated_caches" => true, + "allow_fork_pipelines_to_run_in_parent_project" => true, + "inbound_job_token_scope_enabled" => true + }) + end + end +end diff --git a/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb new file mode 100644 index 00000000000..f6c470260ff --- /dev/null +++ b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillMissingCiCdSettings, feature_category: :source_code_management do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :projects, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb index c1da231a4a6..3c3dcfc0a2d 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb @@ -9,6 +9,14 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:variable_one) do + create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + + let_it_be(:variable_two) do + create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule) + end + let(:mutation) do variables = { id: pipeline_schedule.to_global_id.to_s, @@ -30,6 +38,7 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati nodes { key value + variableType } } } @@ -88,8 +97,37 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123') + end + end + + context 'when updating and removing variables' do + let(:pipeline_schedule_parameters) do + { + variables: [ + { key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false }, + { id: variable_one.to_global_id.to_s, + key: 'foo', value: "foovalue", + variableType: 'ENV_VAR', + destroy: true }, + { id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' } + ] + } + end + + it 'processes variables correctly' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['pipelineSchedule']['variables']['nodes']) + .to match_array( + [ + { "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' }, + { "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' } + ] + ) end end diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb index 6899f6c7d63..c31a652ed93 100644 --- a/spec/services/ci/pipeline_schedules/update_service_spec.rb +++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb @@ -8,9 +8,16 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + before_all do project.add_maintainer(user) project.add_reporter(reporter) + + pipeline_schedule.reload end describe "execute" do @@ -35,7 +42,10 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo description: 'updated_desc', ref: 'patch-x', active: false, - cron: '*/1 * * * *' + cron: '*/1 * * * *', + variables_attributes: [ + { id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' } + ] } end @@ -47,6 +57,42 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo .and change { pipeline_schedule.ref }.from('master').to('patch-x') .and change { pipeline_schedule.active }.from(true).to(false) .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *') + .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar') + .and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue') + end + + context 'when creating a variable' do + let(:params) do + { + variables_attributes: [ + { key: 'ABC', secret_value: 'ABC123' } + ] + } + end + + it 'creates the new variable' do + expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + expect(pipeline_schedule.variables.last.key).to eq('ABC') + expect(pipeline_schedule.variables.last.value).to eq('ABC123') + end + end + + context 'when deleting a variable' do + let(:params) do + { + variables_attributes: [ + { + id: pipeline_schedule_variable.id, + _destroy: true + } + ] + } + end + + it 'deletes the existing variable' do + expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1) + end end it 'returns ServiceResponse.success' do |