diff options
author | Matija Čupić <matteeyah@gmail.com> | 2019-07-29 07:43:10 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2019-07-29 07:43:10 +0000 |
commit | a5aa40c5fe93dc3daba8a578f4c09c4e443fcbec (patch) | |
tree | 34eb74d209b1919f78ce70d89ee0900cd2602780 /app | |
parent | 946f7c0687760ec49aca582a329d0ec3c7ac3037 (diff) | |
download | gitlab-ce-a5aa40c5fe93dc3daba8a578f4c09c4e443fcbec.tar.gz |
Add Job specific variables
Adds Job specific variables to facilitate specifying variables when
running manual jobs.
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/jobs/components/empty_state.vue | 39 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/components/job_app.vue | 8 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/components/manual_variables_form.vue | 179 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/index.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/actions.js | 14 | ||||
-rw-r--r-- | app/controllers/projects/jobs_controller.rb | 6 | ||||
-rw-r--r-- | app/models/ci/build.rb | 7 | ||||
-rw-r--r-- | app/models/ci/job_variable.rb | 14 | ||||
-rw-r--r-- | app/models/concerns/new_has_variable.rb | 14 | ||||
-rw-r--r-- | app/services/ci/play_build_service.rb | 4 | ||||
-rw-r--r-- | app/views/projects/jobs/show.html.haml | 1 | ||||
-rw-r--r-- | app/views/projects/settings/ci_cd/show.html.haml | 2 |
12 files changed, 278 insertions, 11 deletions
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index 04f910b6b80..275ed80146e 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -1,9 +1,11 @@ <script> import { GlLink } from '@gitlab/ui'; +import ManualVariablesForm from './manual_variables_form.vue'; export default { components: { GlLink, + ManualVariablesForm, }, props: { illustrationPath: { @@ -23,6 +25,21 @@ export default { required: false, default: null, }, + playable: { + type: Boolean, + required: true, + default: false, + }, + scheduled: { + type: Boolean, + required: false, + default: false, + }, + variablesSettingsUrl: { + type: String, + required: false, + default: null, + }, action: { type: Object, required: false, @@ -37,28 +54,40 @@ export default { }, }, }, + computed: { + shouldRenderManualVariables() { + return this.playable && !this.scheduled; + }, + }, }; </script> <template> <div class="row empty-state"> <div class="col-12"> - <div :class="illustrationSizeClass" class="svg-content"><img :src="illustrationPath" /></div> + <div :class="illustrationSizeClass" class="svg-content"> + <img :src="illustrationPath" /> + </div> </div> <div class="col-12"> <div class="text-content"> <h4 class="js-job-empty-state-title text-center">{{ title }}</h4> - <p v-if="content" class="js-job-empty-state-content text-center">{{ content }}</p> - + <p v-if="content" class="js-job-empty-state-content">{{ content }}</p> + </div> + <manual-variables-form + v-if="shouldRenderManualVariables" + :action="action" + :variables-settings-url="variablesSettingsUrl" + /> + <div class="text-content"> <div v-if="action" class="text-center"> <gl-link :href="action.path" :data-method="action.method" class="js-job-empty-state-action btn btn-primary" + >{{ action.button_title }}</gl-link > - {{ action.button_title }} - </gl-link> </div> </div> </div> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 79fb67d38cd..ef9fb6d08d1 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -45,6 +45,11 @@ export default { required: false, default: null, }, + variablesSettingsUrl: { + type: String, + required: false, + default: null, + }, runnerHelpUrl: { type: String, required: false, @@ -313,6 +318,9 @@ export default { :title="emptyStateTitle" :content="emptyStateIllustration.content" :action="emptyStateAction" + :playable="job.playable" + :scheduled="job.scheduled" + :variables-settings-url="variablesSettingsUrl" /> <!-- EO empty state --> diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue new file mode 100644 index 00000000000..c32a3cac7be --- /dev/null +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -0,0 +1,179 @@ +<script> +import _ from 'underscore'; +import { mapActions } from 'vuex'; +import { GlButton } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + name: 'ManualVariablesForm', + components: { + GlButton, + Icon, + }, + props: { + action: { + type: Object, + required: false, + default: null, + validator(value) { + return ( + value === null || + (_.has(value, 'path') && _.has(value, 'method') && _.has(value, 'button_title')) + ); + }, + }, + variablesSettingsUrl: { + type: String, + required: true, + default: '', + }, + }, + inputTypes: { + key: 'key', + value: 'value', + }, + i18n: { + keyPlaceholder: s__('CiVariables|Input variable key'), + valuePlaceholder: s__('CiVariables|Input variable value'), + }, + data() { + return { + variables: [], + key: '', + secretValue: '', + }; + }, + computed: { + helpText() { + return sprintf( + s__( + 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', + ), + { + linkStart: `<a href="${this.variablesSettingsUrl}">`, + linkEnd: '</a>', + }, + false, + ); + }, + }, + watch: { + key(newVal) { + this.handleValueChange(newVal, this.$options.inputTypes.key); + }, + secretValue(newVal) { + this.handleValueChange(newVal, this.$options.inputTypes.value); + }, + }, + methods: { + ...mapActions(['triggerManualJob']), + handleValueChange(newValue, type) { + if (newValue !== '') { + this.createNewVariable(type); + this.resetForm(); + } + }, + createNewVariable(type) { + const newVariable = { + key: this.key, + secret_value: this.secretValue, + id: _.uniqueId(), + }; + + this.variables.push(newVariable); + + return this.$nextTick().then(() => { + this.$refs[`${this.$options.inputTypes[type]}-${newVariable.id}`][0].focus(); + }); + }, + resetForm() { + this.key = ''; + this.secretValue = ''; + }, + deleteVariable(id) { + this.variables.splice(this.variables.findIndex(el => el.id === id), 1); + }, + }, +}; +</script> +<template> + <div class="js-manual-vars-form col-12"> + <label>{{ s__('CiVariables|Variables') }}</label> + + <div class="ci-table"> + <div class="gl-responsive-table-row table-row-header pb-0 pt-0 border-0" role="row"> + <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Key') }}</div> + <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div> + </div> + + <div v-for="variable in variables" :key="variable.id" class="gl-responsive-table-row"> + <div class="table-section section-50"> + <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div> + <div class="table-mobile-content append-right-10"> + <input + :ref="`${$options.inputTypes.key}-${variable.id}`" + v-model="variable.key" + :placeholder="$options.i18n.keyPlaceholder" + class="ci-variable-body-item form-control" + /> + </div> + </div> + + <div class="table-section section-50"> + <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div> + <div class="table-mobile-content append-right-10"> + <input + :ref="`${$options.inputTypes.value}-${variable.id}`" + v-model="variable.secret_value" + :placeholder="$options.i18n.valuePlaceholder" + class="ci-variable-body-item form-control" + /> + </div> + </div> + + <div class="table-section section-10"> + <div class="table-mobile-header" role="rowheader"></div> + <div class="table-mobile-content justify-content-end"> + <gl-button class="btn-transparent btn-blank w-25" @click="deleteVariable(variable.id)"> + <icon name="clear" /> + </gl-button> + </div> + </div> + </div> + <div class="gl-responsive-table-row"> + <div class="table-section section-50"> + <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div> + <div class="table-mobile-content append-right-10"> + <input + ref="inputKey" + v-model="key" + class="js-input-key form-control" + :placeholder="$options.i18n.keyPlaceholder" + /> + </div> + </div> + + <div class="table-section section-50"> + <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div> + <div class="table-mobile-content append-right-10"> + <input + ref="inputSecretValue" + v-model="secretValue" + class="ci-variable-body-item form-control" + :placeholder="$options.i18n.valuePlaceholder" + /> + </div> + </div> + </div> + </div> + <div class="d-flex prepend-top-default justify-content-center"> + <p class="text-muted" v-html="helpText"></p> + </div> + <div class="d-flex justify-content-center"> + <gl-button variant="primary" @click="triggerManualJob(variables)"> + {{ action.button_title }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 25132449458..06514fcce1d 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -15,6 +15,7 @@ export default () => { deploymentHelpUrl: element.dataset.deploymentHelpUrl, runnerHelpUrl: element.dataset.runnerHelpUrl, runnerSettingsUrl: element.dataset.runnerSettingsUrl, + variablesSettingsUrl: element.dataset.variablesSettingsUrl, endpoint: element.dataset.endpoint, pagePath: element.dataset.buildOptionsPagePath, logState: element.dataset.buildOptionsLogState, diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index 12d67a43599..a2daef96a2d 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -209,5 +209,19 @@ export const receiveJobsForStageError = ({ commit }) => { flash(__('An error occurred while fetching the jobs.')); }; +export const triggerManualJob = ({ state }, variables) => { + const parsedVariables = variables.map(variable => { + const copyVar = Object.assign({}, variable); + delete copyVar.id; + return copyVar; + }); + + axios + .post(state.job.status.action.path, { + job_variables_attributes: parsedVariables, + }) + .catch(() => flash(__('An error occurred while triggering the job.'))); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 02ff6e872c9..adbc0159358 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -94,7 +94,7 @@ class Projects::JobsController < Projects::ApplicationController def play return respond_422 unless @build.playable? - build = @build.play(current_user) + build = @build.play(current_user, play_params[:job_variables_attributes]) redirect_to build_path(build) end @@ -190,6 +190,10 @@ class Projects::JobsController < Projects::ApplicationController { query: { 'response-content-type' => 'text/plain; charset=utf-8', 'response-content-disposition' => 'inline' } } end + def play_params + params.permit(job_variables_attributes: %i[key secret_value]) + end + def trace_artifact_file @trace_artifact_file ||= build.job_artifacts_trace&.file end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index da70cb9a9a7..07813e03f3a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -40,6 +40,7 @@ module Ci has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent + has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id Ci::JobArtifact.file_types.each do |key, value| has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id @@ -48,6 +49,7 @@ module Ci has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build accepts_nested_attributes_for :runner_session + accepts_nested_attributes_for :job_variables delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true @@ -331,10 +333,10 @@ module Ci end # rubocop: disable CodeReuse/ServiceClass - def play(current_user) + def play(current_user, job_variables_attributes = nil) Ci::PlayBuildService .new(project, current_user) - .execute(self) + .execute(self, job_variables_attributes) end # rubocop: enable CodeReuse/ServiceClass @@ -432,6 +434,7 @@ module Ci Gitlab::Ci::Variables::Collection.new .concat(persisted_variables) .concat(scoped_variables) + .concat(job_variables) .concat(persisted_environment_variables) .to_runner_variables end diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb new file mode 100644 index 00000000000..862a0bc1299 --- /dev/null +++ b/app/models/ci/job_variable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Ci + class JobVariable < ApplicationRecord + extend Gitlab::Ci::Model + include NewHasVariable + + belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id + + alias_attribute :secret_value, :value + + validates :key, uniqueness: { scope: :job_id } + end +end diff --git a/app/models/concerns/new_has_variable.rb b/app/models/concerns/new_has_variable.rb new file mode 100644 index 00000000000..429bf496872 --- /dev/null +++ b/app/models/concerns/new_has_variable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module NewHasVariable + extend ActiveSupport::Concern + include HasVariable + + included do + attr_encrypted :value, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_32, + insecure_mode: false + end +end diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb index eb0b070657d..9f922ffde81 100644 --- a/app/services/ci/play_build_service.rb +++ b/app/services/ci/play_build_service.rb @@ -2,7 +2,7 @@ module Ci class PlayBuildService < ::BaseService - def execute(build) + def execute(build, job_variables_attributes = nil) unless can?(current_user, :update_build, build) raise Gitlab::Access::AccessDeniedError end @@ -10,7 +10,7 @@ module Ci # Try to enqueue the build, otherwise create a duplicate. # if build.enqueue - build.tap { |action| action.update(user: current_user) } + build.tap { |action| action.update(user: current_user, job_variables_attributes: job_variables_attributes || []) } else Ci::Build.retry(build, current_user) end diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 81a53f22f67..c7fab87a593 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -11,4 +11,5 @@ deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'), + variables_settings_url: project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'), build_options: javascript_build_options } } diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 293f0a241eb..87000e8270b 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -41,7 +41,7 @@ .settings-content = render 'projects/runners/index' -%section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } } +%section.qa-variables-settings.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } } .settings-header = render 'ci/variables/header', expanded: expanded .settings-content |