Commit a77158ff authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/intermediate/32568-adding-variables-to-pipelines-schedules' into 'master'

Add variables to pipelines schedules

Closes #32568

See merge request !12372
parents 89724028 e0c150da
...@@ -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,
};
...@@ -153,6 +153,7 @@ $code_line_height: 1.6; ...@@ -153,6 +153,7 @@ $code_line_height: 1.6;
* Padding * Padding
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-col-padding: 15px;
$gl-btn-padding: 10px; $gl-btn-padding: 10px;
$gl-input-padding: 10px; $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
...@@ -443,6 +444,7 @@ $logs-p-color: #333; ...@@ -443,6 +444,7 @@ $logs-p-color: #333;
/* /*
* Forms * Forms
*/ */
$input-height: 34px;
$input-danger-bg: #f2dede; $input-danger-bg: #f2dede;
$input-danger-border: $red-400; $input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa; $input-group-addon-bg: #f7f8fa;
...@@ -574,6 +576,12 @@ $stage-hover-bg: #eaf3fc; ...@@ -574,6 +576,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,84 @@ ...@@ -74,3 +74,84 @@
margin-right: 3px; margin-right: 3px;
} }
} }
.pipeline-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
clear: both;
}
.pipeline-variable-row {
display: flex;
align-items: flex-end;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
}
@media (max-width: $screen-sm-max) {
padding-right: $gl-col-padding;
}
&:last-child {
& .pipeline-variable-row-remove-button {
display: none;
}
@media (max-width: $screen-sm-max) {
& .pipeline-variable-value-input {
margin-right: $pipeline-variable-remove-button-width;
}
}
@media (max-width: $screen-xs-max) {
.pipeline-variable-row-body {
margin-right: $pipeline-variable-remove-button-width;
}
}
}
}
.pipeline-variable-row-body {
display: flex;
width: calc(75% - #{$gl-col-padding});
padding-left: $gl-col-padding;
@media (max-width: $screen-sm-max) {
width: 100%;
}
@media (max-width: $screen-xs-max) {
display: block;
}
}
.pipeline-variable-key-input {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-bottom: $gl-btn-padding;
}
}
.pipeline-variable-row-remove-button {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $pipeline-variable-remove-button-width;
height: $input-height;
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 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
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,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -66,6 +66,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
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
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end end
end end
...@@ -203,6 +203,7 @@ module Ci ...@@ -203,6 +203,7 @@ module Ci
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
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 += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment variables += persisted_environment_variables if environment
variables variables
......
...@@ -9,17 +9,21 @@ module Ci ...@@ -9,17 +9,21 @@ module Ci
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 @@ module Ci ...@@ -56,5 +60,9 @@ module Ci
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
end
end
module Ci module Ci
class PipelineSchedulePolicy < PipelinePolicy class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
condition(:owner_of_schedule) do
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
rule { can?(:master_access) | owner_of_schedule }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
end end
end end
...@@ -162,7 +162,6 @@ class ProjectPolicy < BasePolicy ...@@ -162,7 +162,6 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline enable :create_pipeline
enable :update_pipeline enable :update_pipeline
enable :create_pipeline_schedule enable :create_pipeline_schedule
enable :update_pipeline_schedule
enable :create_merge_request enable :create_merge_request
enable :create_wiki enable :create_wiki
enable :push_code enable :push_code
...@@ -188,7 +187,6 @@ class ProjectPolicy < BasePolicy ...@@ -188,7 +187,6 @@ class ProjectPolicy < BasePolicy
enable :admin_build enable :admin_build
enable :admin_container_image enable :admin_container_image
enable :admin_pipeline enable :admin_pipeline
enable :admin_pipeline_schedule
enable :admin_environment enable :admin_environment
enable :admin_deployment enable :admin_deployment
enable :admin_pages enable :admin_pages
......
# 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'
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= pipeline_schedule.owner&.name = pipeline_schedule.owner&.name
%td %td
.pull-right.btn-group .pull-right.btn-group
- if can?(current_user, :update_pipeline_schedule, @project) && !pipeline_schedule.owned_by?(current_user) - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership') = s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule) - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
......
- 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?}" } }
.pipeline-variable-row-body
%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
...@@ -253,6 +253,19 @@ ActiveRecord::Schema.define(version: 20170724184243) do ...@@ -253,6 +253,19 @@ ActiveRecord::Schema.define(version: 20170724184243) do
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", null: false
t.datetime "updated_at", null: false
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_group_variables", force: :cascade do |t| create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
...@@ -1566,6 +1579,7 @@ ActiveRecord::Schema.define(version: 20170724184243) do ...@@ -1566,6 +1579,7 @@ ActiveRecord::Schema.define(version: 20170724184243) do
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_builds", "projects", name: "fk_befce0568a", on_delete: :cascade add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", 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_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", 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
......
...@@ -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. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
1. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. Group-level [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)
......
...@@ -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
...@@ -74,9 +74,10 @@ module API ...@@ -74,9 +74,10 @@ module API
optional :active, type: Boolean, desc: 'The activation of pipeline schedule' optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
end end
put ':id/pipeline_schedules/:pipeline_schedule_id' do put ':id/pipeline_schedules/:pipeline_schedule_id' do
authorize! :update_pipeline_schedule, user_project authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.update(declared_params(include_missing: false)) if pipeline_schedule.update(declared_params(include_missing: false))
present pipeline_schedule, with: Entities::PipelineScheduleDetails present pipeline_schedule, with: Entities::PipelineScheduleDetails
...@@ -92,9 +93,10 @@ module API ...@@ -92,9 +93,10 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
authorize! :update_pipeline_schedule, user_project authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.own!(current_user) if pipeline_schedule.own!(current_user)
present pipeline_schedule, with: Entities::PipelineScheduleDetails present pipeline_schedule, with: Entities::PipelineScheduleDetails
...@@ -110,9 +112,10 @@ module API ...@@ -110,9 +112,10 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
delete ':id/pipeline_schedules/:pipeline_schedule_id' do delete ':id/pipeline_schedules/:pipeline_schedule_id' do
authorize! :admin_pipeline_schedule, user_project authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :admin_pipeline_schedule, pipeline_schedule
status :accepted status :accepted
present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails
......
...@@ -619,6 +619,12 @@ msgstr "" ...@@ -619,6 +619,12 @@ msgstr ""
msgid "PipelineSchedules|Inactive" msgid "PipelineSchedules|Inactive"
msgstr "" msgstr ""
msgid "PipelineSchedules|Input variable key"
msgstr ""
msgid "PipelineSchedules|Input variable value"
msgstr ""
msgid "PipelineSchedules|Next Run" msgid "PipelineSchedules|Next Run"
msgstr "" msgstr ""
...@@ -628,12 +634,18 @@ msgstr "" ...@@ -628,12 +634,18 @@ msgstr ""
msgid "PipelineSchedules|Provide a short description for this pipeline" msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "" msgstr ""
msgid "PipelineSchedules|Remove variable row"
msgstr ""
msgid "PipelineSchedules|Take ownership" msgid "PipelineSchedules|Take ownership"
msgstr "" msgstr ""
msgid "PipelineSchedules|Target" msgid "PipelineSchedules|Target"
msgstr "" msgstr ""
msgid "PipelineSchedules|Variables"
msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom" msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "" msgstr ""
...@@ -1056,6 +1068,12 @@ msgstr "" ...@@ -1056,6 +1068,12 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid ""
"You are going to remove %{group_name}.\n"
"Removed groups CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
msgstr ""
msgid "" msgid ""
"You are going to remove %{project_name_with_namespace}.\n" "You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n" "Removed project CANNOT be restored!\n"
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-28 13:32+0200\n" "POT-Creation-Date: 2017-07-05 08:50-0500\n"
"PO-Revision-Date: 2017-06-28 13:32+0200\n" "PO-Revision-Date: 2017-07-05 08:50-0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -620,6 +620,12 @@ msgstr "" ...@@ -620,6 +620,12 @@ msgstr ""
msgid "PipelineSchedules|Inactive" msgid "PipelineSchedules|Inactive"
msgstr "" msgstr ""
msgid "PipelineSchedules|Input variable key"
msgstr ""
msgid "PipelineSchedules|Input variable value"
msgstr ""
msgid "PipelineSchedules|Next Run" msgid "PipelineSchedules|Next Run"
msgstr "" msgstr ""
...@@ -629,12 +635,18 @@ msgstr "" ...@@ -629,12 +635,18 @@ msgstr ""
msgid "PipelineSchedules|Provide a short description for this pipeline" msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "" msgstr ""
msgid "PipelineSchedules|Remove variable row"
msgstr ""
msgid "PipelineSchedules|Take ownership" msgid "PipelineSchedules|Take ownership"
msgstr "" msgstr ""
msgid "PipelineSchedules|Target" msgid "PipelineSchedules|Target"
msgstr "" msgstr ""
msgid "PipelineSchedules|Variables"
msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom" msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "" msgstr ""
...@@ -1057,6 +1069,12 @@ msgstr "" ...@@ -1057,6 +1069,12 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid ""
"You are going to remove %{group_name}.\n"
"Removed groups CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
msgstr ""
msgid "" msgid ""
"You are going to remove %{project_name_with_namespace}.\n" "You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n" "Removed project CANNOT be restored!\n"
......
FactoryGirl.define do
factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
pipeline_schedule factory: :ci_pipeline_schedule
end
end
require 'spec_helper' require 'spec_helper'
feature 'Pipeline Schedules', :feature do feature 'Pipeline Schedules', :feature, js: true do
include PipelineSchedulesHelper include PipelineSchedulesHelper
let!(:project) { create(:project) } let!(:project) { create(:project) }
...@@ -11,27 +11,20 @@ feature 'Pipeline Schedules', :feature do ...@@ -11,27 +11,20 @@ feature 'Pipeline Schedules', :feature do
before do before do
project.add_master(user) project.add_master(user)
gitlab_sign_in(user) gitlab_sign_in(user)
visit_page
end end
describe 'GET /projects/pipeline_schedules' do describe 'GET /projects/pipeline_schedules' do
let(:visit_page) { visit_pipelines_schedules } before do
visit_pipelines_schedules
it 'avoids N + 1 queries' do
control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count
create_list(:ci_pipeline_schedule, 2, project: project)
expect { visit_pipelines_schedules }.not_to exceed_query_limit(control_count)
end end
describe 'The view' do describe 'The view' do
it 'displays the required information description' do it 'displays the required information description' do
page.within('.pipeline-schedule-table-row') do page.within('.pipeline-schedule-table-row') do
expect(page).to have_content('pipeline schedule') expect(page).to have_content('pipeline schedule')
expect(page).to have_content(pipeline_schedule.real_next_run.strftime('%b %d, %Y')) expect(find(".next-run-cell time")['data-original-title'])
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
expect(page).to have_link('master') expect(page).to have_link('master')
expect(page).to have_link("##{pipeline.id}") expect(page).to have_link("##{pipeline.id}")
end end
...@@ -62,7 +55,7 @@ feature 'Pipeline Schedules', :feature do ...@@ -62,7 +55,7 @@ feature 'Pipeline Schedules', :feature do
it 'deletes the pipeline' do it 'deletes the pipeline' do
click_link 'Delete' click_link 'Delete'
expect(page).not_to have_content('pipeline schedule') expect(page).not_to have_css(".pipeline-schedule-table-row")
end end
end end
...@@ -78,8 +71,10 @@ feature 'Pipeline Schedules', :feature do ...@@ -78,8 +71,10 @@ feature 'Pipeline Schedules', :feature do
end end
end end
describe 'POST /projects/pipeline_schedules/new', js: true do describe 'POST /projects/pipeline_schedules/new' do
let(:visit_page) { visit_new_pipeline_schedule } before do
visit_new_pipeline_schedule
end
it 'sets defaults for timezone and target branch' do it 'sets defaults for timezone and target branch' do
expect(page).to have_button('master') expect(page).to have_button('master')
...@@ -100,8 +95,8 @@ feature 'Pipeline Schedules', :feature do ...@@ -100,8 +95,8 @@ feature 'Pipeline Schedules', :feature do
end end
end end
describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do describe 'PATCH /projects/pipelines_schedules/:id/edit' do
let(:visit_page) do before do
edit_pipeline_schedule edit_pipeline_schedule
end end
...@@ -134,6 +129,72 @@ feature 'Pipeline Schedules', :feature do ...@@ -134,6 +129,72 @@ feature 'Pipeline Schedules', :feature do
end end
end end
context 'when user creates a new pipeline schedule with variables' do
background do
visit_pipelines_schedules
click_link 'New schedule'
fill_in_schedule_form
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
all('[name="schedule[variables_attributes][][value]"]')[0].set('AAA123')
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
all('[name="schedule[variables_attributes][][value]"]')[1].set('BBB123')
save_pipeline_schedule
end
scenario 'user sees the new variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.pipeline-variable-list') do
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA')
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123')
expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB')
expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123')
end
end
end
context 'when user edits a variable of a pipeline schedule' do
background do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
all('[name="schedule[variables_attributes][][key]"]')[0].set('foo')
all('[name="schedule[variables_attributes][][value]"]')[0].set('bar')
click_button 'Save pipeline schedule'
end
scenario 'user sees the updated variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.pipeline-variable-list') do
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo')
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar')
end
end
end
context 'when user removes a variable of a pipeline schedule' do
background do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
find('.pipeline-variable-list .pipeline-variable-row-remove-button').click
click_button 'Save pipeline schedule'
end
scenario 'user does not see the removed variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.pipeline-variable-list') do
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('')
expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('')
end
end
end
def visit_new_pipeline_schedule def visit_new_pipeline_schedule
visit new_project_pipeline_schedule_path(project, pipeline_schedule) visit new_project_pipeline_schedule_path(project, pipeline_schedule)
end end
......
import {
setupPipelineVariableList,
insertRow,
removeRow,
} from '~/pipeline_schedules/setup_pipeline_variable_list';
describe('Pipeline Variable List', () => {
let $markup;
describe('insertRow', () => {
it('should insert another row', () => {
$markup = $(`<div>
<li class="js-row">
<input>
<textarea></textarea>
</li>
</div>`);
insertRow($markup.find('.js-row'));
expect($markup.find('.js-row').length).toBe(2);
});
it('should clear `data-is-persisted` on cloned row', () => {
$markup = $(`<div>
<li class="js-row" data-is-persisted="true"></li>
</div>`);
insertRow($markup.find('.js-row'));
const $lastRow = $markup.find('.js-row').last();
expect($lastRow.attr('data-is-persisted')).toBe(undefined);
});
it('should clear inputs on cloned row', () => {
$markup = $(`<div>
<li class="js-row">
<input value="foo">
<textarea>bar</textarea>
</li>
</div>`);
insertRow($markup.find('.js-row'));
const $lastRow = $markup.find('.js-row').last();
expect($lastRow.find('input').val()).toBe('');
expect($lastRow.find('textarea').val()).toBe('');
});
});
describe('removeRow', () => {
it('should remove dynamic row', () => {
$markup = $(`<div>
<li class="js-row">
<input>
<textarea></textarea>
</li>
</div>`);
removeRow($markup.find('.js-row'));
expect($markup.find('.js-row').length).toBe(0);
});
it('should hide and mark to destroy with already persisted rows', () => {
$markup = $(`<div>
<li class="js-row" data-is-persisted="true">
<input class="js-destroy-input">
</li>
</div>`);
const $row = $markup.find('.js-row');
removeRow($row);
expect($row.find('.js-destroy-input').val()).toBe('1');
expect($markup.find('.js-row').length).toBe(1);
});
});
describe('setupPipelineVariableList', () => {
beforeEach(() => {
$markup = $(`<form>
<li class="js-row">
<input class="js-user-input" name="schedule[variables_attributes][][key]">
<textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea>
<button class="js-row-remove-button"></button>
<button class="js-row-add-button"></button>
</li>
</form>`);
setupPipelineVariableList($markup);
});
it('should remove the row when clicking the remove button', () => {
$markup.find('.js-row-remove-button').trigger('click');
expect($markup.find('.js-row').length).toBe(0);
});
it('should add another row when editing the last rows key input', () => {
const $row = $markup.find('.js-row');
$row.find('input.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
});
it('should add another row when editing the last rows value textarea', () => {
const $row = $markup.find('.js-row');
$row.find('textarea.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
});
it('should remove empty row after blurring', () => {
const $row = $markup.find('.js-row');
$row.find('input.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
$row.find('input.js-user-input')
.val('')
.trigger('input')
.trigger('blur');
expect($markup.find('.js-row').length).toBe(1);
});
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $markup.find('.js-row');
expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]');
expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]');
$markup.filter('form').submit();
expect($row.find('input').attr('name')).toBe('');
expect($row.find('textarea').attr('name')).toBe('');
});
});
});
...@@ -134,8 +134,11 @@ pipeline_schedules: ...@@ -134,8 +134,11 @@ pipeline_schedules:
- owner - owner
- pipelines - pipelines
- last_pipeline - last_pipeline
- variables
pipeline_schedule: pipeline_schedule:
- pipelines - pipelines
pipeline_schedule_variables:
- pipeline_schedule
deploy_keys: deploy_keys:
- user - user
- deploy_keys_projects - deploy_keys_projects
......
...@@ -1427,6 +1427,23 @@ describe Ci::Build, :models do ...@@ -1427,6 +1427,23 @@ describe Ci::Build, :models do
it { is_expected.to include(predefined_trigger_variable) } it { is_expected.to include(predefined_trigger_variable) }
end end
context 'when a job was triggered by a pipeline schedule' do
let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
let!(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable,
key: 'SCHEDULE_VARIABLE_KEY',
pipeline_schedule: pipeline_schedule)
end
before do
pipeline_schedule.pipelines << pipeline
pipeline_schedule.reload
end
it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) }
end
context 'when yaml_variables are undefined' do context 'when yaml_variables are undefined' do
before do before do
build.yaml_variables = nil build.yaml_variables = nil
......
...@@ -5,6 +5,7 @@ describe Ci::PipelineSchedule, models: true do ...@@ -5,6 +5,7 @@ describe Ci::PipelineSchedule, models: true do
it { is_expected.to belong_to(:owner) } it { is_expected.to belong_to(:owner) }
it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:variables) }
it { is_expected.to respond_to(:ref) } it { is_expected.to respond_to(:ref) }
it { is_expected.to respond_to(:cron) } it { is_expected.to respond_to(:cron) }
...@@ -117,4 +118,20 @@ describe Ci::PipelineSchedule, models: true do ...@@ -117,4 +118,20 @@ describe Ci::PipelineSchedule, models: true do
end end
end end
end end
describe '#job_variables' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule) }
let!(:pipeline_schedule_variables) do
create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule)
end
subject { pipeline_schedule.job_variables }
before do
pipeline_schedule.reload
end
it { is_expected.to contain_exactly(*pipeline_schedule_variables.map(&:to_runner_variable)) }
end
end end
require 'spec_helper'
describe Ci::PipelineScheduleVariable, models: true do
subject { build(:ci_pipeline_schedule_variable) }
it { is_expected.to include_module(HasVariable) }
end
...@@ -279,6 +279,8 @@ describe API::PipelineSchedules do ...@@ -279,6 +279,8 @@ describe API::PipelineSchedules do
end end
context 'authenticated user with invalid permissions' do context 'authenticated user with invalid permissions' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) }
it 'does not delete pipeline_schedule' do it 'does not delete pipeline_schedule' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
......
...@@ -50,9 +50,24 @@ module AccessMatchersForController ...@@ -50,9 +50,24 @@ module AccessMatchersForController
"be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}"
end end
def update_owner(objects, user)
return unless objects
objects.each do |object|
if object.respond_to?(:owner)
object.update_attribute(:owner, user)
elsif object.respond_to?(:user)
object.update_attribute(:user, user)
else
raise ArgumentError, "cannot own this object #{object}"
end
end
end
matcher :be_allowed_for do |role| matcher :be_allowed_for do |role|
match do |action| match do |action|
emulate_user(role, @membership) user = emulate_user(role, @membership)
update_owner(@objects, user)
action.call action.call
EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) EXPECTED_STATUS_CODE_ALLOWED.include?(response.status)
...@@ -62,13 +77,18 @@ module AccessMatchersForController ...@@ -62,13 +77,18 @@ module AccessMatchersForController
@membership = membership @membership = membership
end end
chain :own do |*objects|
@objects = objects
end
description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) }
supports_block_expectations supports_block_expectations
end end
matcher :be_denied_for do |role| matcher :be_denied_for do |role|
match do |action| match do |action|
emulate_user(role, @membership) user = emulate_user(role, @membership)
update_owner(@objects, user)
action.call action.call
EXPECTED_STATUS_CODE_DENIED.include?(response.status) EXPECTED_STATUS_CODE_DENIED.include?(response.status)
...@@ -78,6 +98,10 @@ module AccessMatchersForController ...@@ -78,6 +98,10 @@ module AccessMatchersForController
@membership = membership @membership = membership
end end
chain :own do |*objects|
@objects = objects
end
description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) }
supports_block_expectations supports_block_expectations
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment