Коммит 3c267c74 создал по автору Eric Eastwood's avatar Eric Eastwood
Просмотр файлов

Port of feature/intermediate/32568-adding-variables-to-pipelines-schedules to EE

See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12372
владелец c321d953
...@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate'; ...@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue'; import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown'; import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown';
import { setupPipelineVariableList } from './setup_pipeline_variable_list';
Vue.use(Translate); Vue.use(Translate);
...@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown(); gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
}); });
function insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
$rowClone.find('input, textarea').val('');
$row.after($rowClone);
}
function removeRow($row) {
const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
.find('.js-destroy-input')
.val(1);
} else {
$row.remove();
}
}
function checkIfRowTouched($row) {
return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
}
function setupPipelineVariableList(parent = document) {
const $parent = $(parent);
$parent.on('click', '.js-row-remove-button', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
removeRow($row);
e.preventDefault();
});
// Remove any empty rows except the last r
$parent.on('blur', '.js-user-input', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
const isTouched = checkIfRowTouched($row);
if ($row.is(':not(:last-child)') && !isTouched) {
removeRow($row);
}
});
// Always make sure there is an empty last row
$parent.on('input', '.js-user-input', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (isTouched) {
insertRow($lastRow);
}
});
// Clear out the empty last row so it
// doesn't get submitted and throw validation errors
$parent.closest('form').on('submit', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
}
});
}
export {
setupPipelineVariableList,
insertRow,
removeRow,
};
...@@ -579,6 +579,12 @@ $stage-hover-bg: #eaf3fc; ...@@ -579,6 +579,12 @@ $stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc; $stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6; $action-icon-color: #d6d6d6;
/*
Pipeline Schedules
*/
$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/* /*
Filtered Search Filtered Search
*/ */
......
...@@ -74,3 +74,65 @@ ...@@ -74,3 +74,65 @@
margin-right: 3px; margin-right: 3px;
} }
} }
.pipeline-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
}
.pipeline-variable-row {
display: flex;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
}
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
&:last-child {
& > .pipeline-variable-row-remove-button {
display: none;
}
& > .pipeline-variable-value-input {
margin-right: $pipeline-variable-remove-button-width;
}
}
}
.pipeline-variable-key-input {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-right: $pipeline-variable-remove-button-width;
margin-bottom: $gl-btn-padding;
}
}
.pipeline-variable-value-input {
@media (max-width: $screen-xs-max) {
flex: 1;
}
}
.pipeline-variable-row-remove-button {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $pipeline-variable-remove-button-width;
padding: 0;
background: transparent;
border: 0;
color: $gl-text-color-secondary;
@include transition(color);
&:hover,
&:focus {
outline: none;
color: $gl-text-color;
}
}
class Projects::PipelineSchedulesController < Projects::ApplicationController class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
before_action :authorize_read_pipeline_schedule! before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create] before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update] before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy] before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
def index def index
@scope = params[:scope] @scope = params[:scope]
@all_schedules = PipelineSchedulesFinder.new(@project).execute @all_schedules = PipelineSchedulesFinder.new(@project).execute
...@@ -53,7 +53,7 @@ def destroy ...@@ -53,7 +53,7 @@ def destroy
redirect_to pipeline_schedules_path(@project), status: 302 redirect_to pipeline_schedules_path(@project), status: 302
else else
redirect_to pipeline_schedules_path(@project), redirect_to pipeline_schedules_path(@project),
status: 302, status: :forbidden,
alert: _("Failed to remove the pipeline schedule") alert: _("Failed to remove the pipeline schedule")
end end
end end
...@@ -66,6 +66,11 @@ def schedule ...@@ -66,6 +66,11 @@ def schedule
def schedule_params def schedule_params
params.require(:schedule) params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active) .permit(:description, :cron, :cron_timezone, :ref, :active,
variables_attributes: [:id, :key, :value, :_destroy] )
end
def authorize_update_pipeline_schedule!
return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
end end
end end
...@@ -204,6 +204,7 @@ def variables(environment: persisted_environment) ...@@ -204,6 +204,7 @@ def variables(environment: persisted_environment)
variables += secret_variables(environment: environment) variables += secret_variables(environment: environment)
variables += trigger_request.user_variables if trigger_request variables += trigger_request.user_variables if trigger_request
variables += persisted_environment_variables if environment variables += persisted_environment_variables if environment
variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables variables
end end
......
...@@ -9,17 +9,21 @@ class PipelineSchedule < ActiveRecord::Base ...@@ -9,17 +9,21 @@ class PipelineSchedule < ActiveRecord::Base
belongs_to :owner, class_name: 'User' belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines has_many :pipelines
has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? }
validates :description, presence: true validates :description, presence: true
validates :variables, variable_duplicates: true
before_save :set_next_run_at before_save :set_next_run_at
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) } scope :inactive, -> { where(active: false) }
accepts_nested_attributes_for :variables, allow_destroy: true
def owned_by?(current_user) def owned_by?(current_user)
owner == current_user owner == current_user
end end
...@@ -56,5 +60,9 @@ def real_next_run( ...@@ -56,5 +60,9 @@ def real_next_run(
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at) .next_time_from(next_run_at)
end end
def job_variables
variables&.map(&:to_runner_variable) || []
end
end end
end end
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model
include HasVariable
belongs_to :pipeline_schedule
validates :key, uniqueness: { scope: :pipeline_schedule_id }
end
end
module Ci module Ci
class PipelineSchedulePolicy < PipelinePolicy class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
condition(:protected_action) do
owned_by_developer? && owned_by_another?
end
rule { protected_action }.prevent :update_pipeline_schedule
private
def owned_by_developer?
return false unless @user
pipeline_schedule.project.team.developer?(@user)
end
def owned_by_another?
return false unless @user
!pipeline_schedule.owned_by?(@user)
end
end end
end end
# VariableDuplicatesValidator
#
# This validtor is designed for especially the following condition
# - Use `accepts_nested_attributes_for :xxx` in a parent model
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}")
end
end
end
...@@ -22,6 +22,14 @@ ...@@ -22,6 +22,14 @@
= f.label :ref, _('Target Branch'), class: 'label-light' = f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
.col-md-9
%label.label-light
#{ s_('PipelineSchedules|Variables') }
%ul.js-pipeline-variable-list.pipeline-variable-list
- @schedule.variables.each do |variable|
= render 'variable_row', id: variable.id, key: variable.key, value: variable.value
= render 'variable_row'
.form-group .form-group
.col-md-9 .col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
......
- id = local_assigns.fetch(:id, nil)
- key = local_assigns.fetch(:key, "")
- value = local_assigns.fetch(:value, "")
%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
%input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id }
%input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" }
%input.js-user-input.pipeline-variable-key-input.form-control{ type: "text",
name: "schedule[variables_attributes][][key]",
value: key,
placeholder: s_('PipelineSchedules|Input variable key') }
%textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1,
name: "schedule[variables_attributes][][value]",
placeholder: s_('PipelineSchedules|Input variable value') }
= value
%button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') }
%i.fa.fa-minus-circle{ 'aria-hidden': "true" }
---
title: Add variables to pipelines schedules
merge_request: 12372
author:
class CreateCiPipelineScheduleVariables < ActiveRecord::Migration
DOWNTIME = false
def up
create_table :ci_pipeline_schedule_variables do |t|
t.string :key, null: false
t.text :value
t.text :encrypted_value
t.string :encrypted_value_salt
t.string :encrypted_value_iv
t.integer :pipeline_schedule_id, null: false
t.timestamps_with_timezone null: true
end
add_index :ci_pipeline_schedule_variables,
[:pipeline_schedule_id, :key],
name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key",
unique: true
end
def down
drop_table :ci_pipeline_schedule_variables
end
end
class AddForeignKeyToCiPipelineScheduleVariables < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key(:ci_pipeline_schedule_variables, :ci_pipeline_schedules, column: :pipeline_schedule_id)
end
def down
remove_foreign_key(:ci_pipeline_schedule_variables, column: :pipeline_schedule_id)
end
end
...@@ -136,12 +136,12 @@ ...@@ -136,12 +136,12 @@
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.boolean "clientside_sentry_enabled", default: false, null: false t.boolean "clientside_sentry_enabled", default: false, null: false
t.string "clientside_sentry_dsn" t.string "clientside_sentry_dsn"
t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "check_namespace_plan", default: false, null: false t.boolean "check_namespace_plan", default: false, null: false
t.integer "mirror_max_delay", default: 5, null: false t.integer "mirror_max_delay", default: 5, null: false
t.integer "mirror_max_capacity", default: 100, null: false t.integer "mirror_max_capacity", default: 100, null: false
t.integer "mirror_capacity_threshold", default: 50, null: false t.integer "mirror_capacity_threshold", default: 50, null: false
t.boolean "authorized_keys_enabled", default: true, null: false t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "help_page_hide_commercial_content", default: false t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url" t.string "help_page_support_url"
end end
...@@ -288,9 +288,9 @@ ...@@ -288,9 +288,9 @@
t.string "coverage_regex" t.string "coverage_regex"
t.integer "auto_canceled_by_id" t.integer "auto_canceled_by_id"
t.boolean "retried" t.boolean "retried"
t.integer "stage_id"
t.integer "artifacts_file_store", default: 1, null: false t.integer "artifacts_file_store", default: 1, null: false
t.integer "artifacts_metadata_store", default: 1, null: false t.integer "artifacts_metadata_store", default: 1, null: false
t.integer "stage_id"
end end
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
...@@ -307,6 +307,19 @@ ...@@ -307,6 +307,19 @@
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
create_table "ci_pipeline_schedule_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "pipeline_schedule_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
create_table "ci_pipeline_schedules", force: :cascade do |t| create_table "ci_pipeline_schedules", force: :cascade do |t|
t.string "description" t.string "description"
t.string "ref" t.string "ref"
...@@ -615,8 +628,8 @@ ...@@ -615,8 +628,8 @@
t.text "deleted_path", null: false t.text "deleted_path", null: false
t.text "deleted_wiki_path" t.text "deleted_wiki_path"
t.text "deleted_project_name", null: false t.text "deleted_project_name", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "geo_repository_deleted_events", ["project_id"], name: "index_geo_repository_deleted_events_on_project_id", using: :btree add_index "geo_repository_deleted_events", ["project_id"], name: "index_geo_repository_deleted_events_on_project_id", using: :btree
...@@ -1805,8 +1818,8 @@ ...@@ -1805,8 +1818,8 @@
t.string "token" t.string "token"
t.boolean "pipeline_events", default: false, null: false t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false
t.boolean "repository_update_events", default: false, null: false
t.boolean "job_events", default: false, null: false t.boolean "job_events", default: false, null: false
t.boolean "repository_update_events", default: false, null: false
end end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
...@@ -1817,6 +1830,7 @@ ...@@ -1817,6 +1830,7 @@
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
......
...@@ -9,7 +9,7 @@ and a list of **user-defined variables**. ...@@ -9,7 +9,7 @@ and a list of **user-defined variables**.
The variables can be overwritten and they take precedence over each other in The variables can be overwritten and they take precedence over each other in
this order: this order:
1. [Trigger variables][triggers] (take precedence over all) 1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all)
1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables) 1. YAML-defined [global variables](../yaml/README.md#variables)
......
...@@ -31,6 +31,15 @@ is installed on. ...@@ -31,6 +31,15 @@ is installed on.
![Schedules list](img/pipeline_schedules_list.png) ![Schedules list](img/pipeline_schedules_list.png)
### Making use of scheduled pipeline variables
> [Introduced][ce-12328] in GitLab 9.4.
You can pass any number of arbitrary variables and they will be available in
GitLab CI so that they can be used in your `.gitlab-ci.yml` file.
![Scheduled pipeline variables](img/pipeline_schedule_variables.png)
## Using only and except ## Using only and except
To configure that a job can be executed only when the pipeline has been To configure that a job can be executed only when the pipeline has been
...@@ -79,4 +88,5 @@ don't have admin access to the server, ask your administrator. ...@@ -79,4 +88,5 @@ don't have admin access to the server, ask your administrator.
[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533 [ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853 [ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853
[ce-12328]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12328
[settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs [settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать