summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMatija Čupić <matteeyah@gmail.com>2019-07-29 07:43:10 +0000
committerPhil Hughes <me@iamphill.com>2019-07-29 07:43:10 +0000
commita5aa40c5fe93dc3daba8a578f4c09c4e443fcbec (patch)
tree34eb74d209b1919f78ce70d89ee0900cd2602780 /app
parent946f7c0687760ec49aca582a329d0ec3c7ac3037 (diff)
downloadgitlab-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.vue39
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue8
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue179
-rw-r--r--app/assets/javascripts/jobs/index.js1
-rw-r--r--app/assets/javascripts/jobs/store/actions.js14
-rw-r--r--app/controllers/projects/jobs_controller.rb6
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/job_variable.rb14
-rw-r--r--app/models/concerns/new_has_variable.rb14
-rw-r--r--app/services/ci/play_build_service.rb4
-rw-r--r--app/views/projects/jobs/show.html.haml1
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
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