Skip to content
Snippets Groups Projects
Commit a5aa40c5 authored by Matija Čupić's avatar Matija Čupić Committed by Phil Hughes
Browse files

Add Job specific variables

Adds Job specific variables to facilitate specifying variables when
running manual jobs.
parent 946f7c06
No related branches found
No related tags found
No related merge requests found
Showing
with 362 additions and 16 deletions
<script>
import { GlLink } from '@gitlab/ui';
import ManualVariablesForm from './manual_variables_form.vue';
 
export default {
components: {
GlLink,
ManualVariablesForm,
},
props: {
illustrationPath: {
Loading
Loading
@@ -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,
Loading
Loading
@@ -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>
Loading
Loading
Loading
Loading
@@ -45,6 +45,11 @@ export default {
required: false,
default: null,
},
variablesSettingsUrl: {
type: String,
required: false,
default: null,
},
runnerHelpUrl: {
type: String,
required: false,
Loading
Loading
@@ -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 -->
 
Loading
Loading
<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>
Loading
Loading
@@ -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,
Loading
Loading
Loading
Loading
@@ -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 () => {};
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
Loading
Loading
# 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
# 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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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 } }
Loading
Loading
@@ -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
Loading
Loading
---
title: Allow specifying variables when running manual jobs
merge_request: 30485
author:
type: added
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateJobVariables < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :ci_job_variables do |t|
t.string :key, null: false
t.text :encrypted_value
t.string :encrypted_value_iv
t.references :job, null: false, index: true, foreign_key: { to_table: :ci_builds, on_delete: :cascade }
t.integer :variable_type, null: false, limit: 2, default: 1
end
add_index :ci_job_variables, [:key, :job_id], unique: true
end
end
Loading
Loading
@@ -605,6 +605,16 @@ ActiveRecord::Schema.define(version: 2019_07_25_012225) do
t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id"
end
 
create_table "ci_job_variables", force: :cascade do |t|
t.string "key", null: false
t.text "encrypted_value"
t.string "encrypted_value_iv"
t.bigint "job_id", null: false
t.integer "variable_type", limit: 2, default: 1, null: false
t.index ["job_id"], name: "index_ci_job_variables_on_job_id"
t.index ["key", "job_id"], name: "index_ci_job_variables_on_key_and_job_id", unique: true
end
create_table "ci_pipeline_chat_data", force: :cascade do |t|
t.integer "pipeline_id", null: false
t.integer "chat_name_id", null: false
Loading
Loading
@@ -3637,6 +3647,7 @@ ActiveRecord::Schema.define(version: 2019_07_25_012225) do
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
add_foreign_key "ci_job_variables", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_pipeline_chat_data", "chat_names", on_delete: :cascade
add_foreign_key "ci_pipeline_chat_data", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
Loading
Loading
doc/ci/img/manual_job_variables.png

420 KiB

Loading
Loading
@@ -323,6 +323,20 @@ stage has a job with a manual action.
 
![Pipelines example](img/pipelines.png)
 
### Specifying variables when running manual jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30485) in GitLab 12.2.
When running manual jobs you can supply additional job specific variables.
You can do this from the job page of the manual job you want to run with
additional variables.
This is useful when you want to alter the execution of a job by using
environment variables.
![Manual job variables](img/manual_job_variables.png)
### Delay a job in a pipeline graph
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4.
Loading
Loading
Loading
Loading
@@ -10,7 +10,7 @@ module Gitlab
image: 'illustrations/manual_action.svg',
size: 'svg-394',
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
content: _('This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.')
}
end
 
Loading
Loading
Loading
Loading
@@ -1080,6 +1080,9 @@ msgstr ""
msgid "An error occurred while saving assignees"
msgstr ""
 
msgid "An error occurred while triggering the job."
msgstr ""
msgid "An error occurred while validating username"
msgstr ""
 
Loading
Loading
@@ -2221,6 +2224,9 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
 
msgid "CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default"
msgstr ""
msgid "CiVariables|State"
msgstr ""
 
Loading
Loading
@@ -2230,6 +2236,9 @@ msgstr ""
msgid "CiVariables|Value"
msgstr ""
 
msgid "CiVariables|Variables"
msgstr ""
msgid "CiVariable|* (All environments)"
msgstr ""
 
Loading
Loading
@@ -7697,6 +7706,9 @@ msgstr ""
msgid "Pipeline|Existing branch name or tag"
msgstr ""
 
msgid "Pipeline|Key"
msgstr ""
msgid "Pipeline|Pipeline"
msgstr ""
 
Loading
Loading
@@ -7727,6 +7739,9 @@ msgstr ""
msgid "Pipeline|Triggerer"
msgstr ""
 
msgid "Pipeline|Value"
msgstr ""
msgid "Pipeline|Variables"
msgstr ""
 
Loading
Loading
@@ -11056,9 +11071,6 @@ msgstr ""
msgid "This issue is locked."
msgstr ""
 
msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
 
Loading
Loading
@@ -11113,6 +11125,9 @@ msgstr ""
msgid "This job requires a manual action"
msgstr ""
 
msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes."
msgstr ""
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -676,6 +676,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
 
describe 'POST play' do
let(:variable_attributes) { [] }
before do
project.add_developer(user)
 
Loading
Loading
@@ -698,6 +700,14 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'transits to pending' do
expect(job.reload).to be_pending
end
context 'when job variables are specified' do
let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] }
it 'assigns the job variables' do
expect(job.reload.job_variables.map(&:key)).to contain_exactly('first')
end
end
end
 
context 'when job is not playable' do
Loading
Loading
@@ -712,7 +722,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
post :play, params: {
namespace_id: project.namespace,
project_id: project,
id: job.id
id: job.id,
job_variables_attributes: variable_attributes
}
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment