diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index b424e7f205d43825ecb1a1477330a8e7a6c08776..50c725aa3d54954d5446c976195532b9c9fc075f 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; +import { setupPipelineVariableList } from './setup_pipeline_variable_list'; Vue.use(Translate); @@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + + setupPipelineVariableList($('.js-pipeline-variable-list')); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js new file mode 100644 index 0000000000000000000000000000000000000000..644efd10509b081e21a8d6451f54d2c0e44fb8f4 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -0,0 +1,71 @@ +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, +}; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1b7b286316e888ea4412de613b3fe5dabdb6b4f2..e7ac7a9e00d81957a30af1a5e13997a2c7e53ce0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -159,6 +159,7 @@ $code_line_height: 1.6; * Padding */ $gl-padding: 16px; +$gl-col-padding: 15px; $gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -454,6 +455,7 @@ $logs-p-color: #333; /* * Forms */ +$input-height: 34px; $input-danger-bg: #f2dede; $input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; @@ -585,6 +587,12 @@ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; +/* +Pipeline Schedules +*/ +$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); + + /* Filtered Search */ diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 595eb40fec71d6149ff96640b68f009cf999dcd6..dc719a6ba948be059bf1db1eae7cd1132fe052fc 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -74,3 +74,84 @@ 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; + } +} diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 0d967a7e691a40e61e2e3e0441f424aa8e396664..ec7c645df5aa89351c5a65fdcbdbedcd6babe299 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,11 +1,11 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController + before_action :schedule, except: [:index, :new, :create] + before_action :authorize_read_pipeline_schedule! 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 :schedule, only: [:edit, :update, :destroy, :take_ownership] - def index @scope = params[:scope] @all_schedules = PipelineSchedulesFinder.new(@project).execute @@ -53,7 +53,7 @@ def destroy redirect_to pipeline_schedules_path(@project), status: 302 else redirect_to pipeline_schedules_path(@project), - status: 302, + status: :forbidden, alert: _("Failed to remove the pipeline schedule") end end @@ -66,6 +66,15 @@ def schedule def schedule_params 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 diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4ae9a0e69bab4d70a530274653ff58aae1913c9d..cc0a8b6023dbf2eb8a00ff4826ceb65f8be16d25 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -207,6 +207,7 @@ def variables(environment: persisted_environment) variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group variables += secret_variables(environment: environment) 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 diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 45d8cd34359b8e1811e97bcf2df9b71f7b364813..e4ae1b35f66f6e1782b7dde22ec31d3dea5a3ed4 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -9,17 +9,21 @@ class PipelineSchedule < ActiveRecord::Base belongs_to :owner, class_name: 'User' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_many :pipelines + has_many :variables, class_name: 'Ci::PipelineScheduleVariable' validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :description, presence: true + validates :variables, variable_duplicates: true before_save :set_next_run_at scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } + accepts_nested_attributes_for :variables, allow_destroy: true + def owned_by?(current_user) owner == current_user end @@ -56,5 +60,9 @@ def real_next_run( Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) .next_time_from(next_run_at) end + + def job_variables + variables&.map(&:to_runner_variable) || [] + end end end diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb new file mode 100644 index 0000000000000000000000000000000000000000..1ff177616e8a3a39e7f055b1907d4e958fc413df --- /dev/null +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -0,0 +1,8 @@ +module Ci + class PipelineScheduleVariable < ActiveRecord::Base + extend Ci::Model + include HasVariable + + belongs_to :pipeline_schedule + end +end diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index 1877e89bb23f1cc2c998ce46ae35c5cadd0734e3..6b7598e1821929e209eab2696b5f5f6b42293baf 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -1,4 +1,14 @@ module Ci 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 diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b8edccd07c20856007a568c2e819e86735d33ce3..219fc9961d6e05ca9e23748302c89bf973167e65 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -164,7 +164,6 @@ def self.create_read_update_admin(name) enable :create_pipeline enable :update_pipeline enable :create_pipeline_schedule - enable :update_pipeline_schedule enable :create_merge_request enable :create_wiki enable :push_code @@ -190,7 +189,6 @@ def self.create_read_update_admin(name) enable :admin_build enable :admin_container_image enable :admin_pipeline - enable :admin_pipeline_schedule enable :admin_environment enable :admin_deployment enable :admin_pages diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a9d8892e9bd4026ed3afa3609a078f07e9d2a9e --- /dev/null +++ b/app/validators/variable_duplicates_validator.rb @@ -0,0 +1,13 @@ +# 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 diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index fc7fa5c1876792b560e968dc7dd8415766557476..857ae00d0ab3ab66781127839122e00c77170306 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -22,6 +22,14 @@ = 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 } } ) = 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 .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 08ccd57c81a7453cdd51d312831cc9feff1a85d3..97c0407a01d6d3fdb02fcb9e748a8ead94038e5c 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -26,7 +26,7 @@ = pipeline_schedule.owner&.name %td .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 = s_('PipelineSchedules|Take ownership') - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..564cb5d1ca94c27b30120c133748ba5d4193e35f --- /dev/null +++ b/app/views/projects/pipeline_schedules/_variable_row.html.haml @@ -0,0 +1,17 @@ +- 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" } diff --git a/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml b/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml new file mode 100644 index 0000000000000000000000000000000000000000..d497575b7f3c78fbe16d804c1a6df9452befa062 --- /dev/null +++ b/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml @@ -0,0 +1,4 @@ +--- +title: Add variables to pipelines schedules +merge_request: 12372 +author: diff --git a/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb b/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..92833765a827457bf36f9648f5a385bc96e6f243 --- /dev/null +++ b/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb @@ -0,0 +1,25 @@ +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 diff --git a/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb b/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bbf66e0ac392a4708ec0c29af89923d89491690 --- /dev/null +++ b/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb @@ -0,0 +1,15 @@ +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 diff --git a/db/schema.rb b/db/schema.rb index e31996d8ae0def7946967e265c0014f0426df9f6..dabf9461ddec1ee94b2d68f87ab414d3c8b97614 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -312,6 +312,19 @@ 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 + 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| t.string "key", null: false t.text "value" @@ -1885,6 +1898,7 @@ 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", "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_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 diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index e6d1a08f05679c4d457de49302dd8476ae4ebf2e..38e381ffd047f332f577a9298a224f053e51ce76 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -9,7 +9,7 @@ and a list of **user-defined variables**. The variables can be overwritten and they take precedence over each other in 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. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables) diff --git a/doc/user/project/pipelines/img/pipeline_schedule_variables.png b/doc/user/project/pipelines/img/pipeline_schedule_variables.png new file mode 100644 index 0000000000000000000000000000000000000000..47a0c6f3697337ddf90e78a3ec834f6e736083f0 Binary files /dev/null and b/doc/user/project/pipelines/img/pipeline_schedule_variables.png differ diff --git a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png index ea5394fa8a685e582f0c649edb6cd9309f4e2845..5a0e596599234934ad272e0cba10771e7981f51e 100644 Binary files a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png and b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png differ diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 17cc21238ff3917cc8c534fa493b4468b154c168..258b3a2f9555f771d8746fc5001e5e97a9f1d174 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -31,6 +31,15 @@ is installed on. ![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 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. [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-12328]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12328 [settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index 93d89209934e50234a890d95fb11e13e61872b1c..dbeaf9e17ef2ea97b44b7b56d6c91e839237ebb0 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -74,9 +74,10 @@ class PipelineSchedules < Grape::API optional :active, type: Boolean, desc: 'The activation of pipeline schedule' end 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 + authorize! :update_pipeline_schedule, pipeline_schedule if pipeline_schedule.update(declared_params(include_missing: false)) present pipeline_schedule, with: Entities::PipelineScheduleDetails @@ -92,9 +93,10 @@ class PipelineSchedules < Grape::API requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end 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 + authorize! :update_pipeline_schedule, pipeline_schedule if pipeline_schedule.own!(current_user) present pipeline_schedule, with: Entities::PipelineScheduleDetails @@ -110,9 +112,10 @@ class PipelineSchedules < Grape::API requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end 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 + authorize! :admin_pipeline_schedule, pipeline_schedule status :accepted present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index bda3fc09e851c35156cc2573e64ffbd0241b6cd7..7065b7a635f585fa115a57c42b2fa187e4318123 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -619,6 +619,12 @@ msgstr "" msgid "PipelineSchedules|Inactive" msgstr "" +msgid "PipelineSchedules|Input variable key" +msgstr "" + +msgid "PipelineSchedules|Input variable value" +msgstr "" + msgid "PipelineSchedules|Next Run" msgstr "" @@ -628,12 +634,18 @@ msgstr "" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "" +msgid "PipelineSchedules|Remove variable row" +msgstr "" + msgid "PipelineSchedules|Take ownership" msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSchedules|Variables" +msgstr "" + msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" @@ -1056,6 +1068,12 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" +msgid "" +"You are going to remove %{group_name}.\n" +"Removed groups CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" + msgid "" "You are going to remove %{project_name_with_namespace}.\n" "Removed project CANNOT be restored!\n" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9f1caeddaa7b23b380ce75d8de38a6a875528335..6066314b32e8c29c10e97cfc007cd2f3722e25e6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-28 13:32+0200\n" -"PO-Revision-Date: 2017-06-28 13:32+0200\n" +"POT-Creation-Date: 2017-07-05 08:50-0500\n" +"PO-Revision-Date: 2017-07-05 08:50-0500\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -620,6 +620,12 @@ msgstr "" msgid "PipelineSchedules|Inactive" msgstr "" +msgid "PipelineSchedules|Input variable key" +msgstr "" + +msgid "PipelineSchedules|Input variable value" +msgstr "" + msgid "PipelineSchedules|Next Run" msgstr "" @@ -629,12 +635,18 @@ msgstr "" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "" +msgid "PipelineSchedules|Remove variable row" +msgstr "" + msgid "PipelineSchedules|Take ownership" msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSchedules|Variables" +msgstr "" + msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" @@ -1057,6 +1069,12 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" +msgid "" +"You are going to remove %{group_name}.\n" +"Removed groups CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" + msgid "" "You are going to remove %{project_name_with_namespace}.\n" "Removed project CANNOT be restored!\n" diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index a8c44d5c313ec4a678272a4d25e8ddab98e0e9ee..41bf5580993121b64e0535e0ebf913b79fd2890a 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::PipelineSchedulesController do + include AccessMatchersForController + set(:project) { create(:empty_project, :public) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } @@ -17,6 +19,14 @@ expect(response).to render_template(:index) end + 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 + context 'when the scope is set to active' do let(:scope) { 'active' } @@ -36,104 +46,352 @@ def visit_pipelines_schedules end end - describe 'GET edit' do - let(:user) { create(:user) } + describe 'GET #new' do + set(:user) { create(:user) } before do - project.add_master(user) - + project.add_developer(user) sign_in(user) end - it 'loads the pipeline schedule' do - get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + it 'initializes a pipeline schedule model' do + get :new, namespace_id: project.namespace.to_param, project_id: project expect(response).to have_http_status(:ok) - expect(assigns(:schedule)).to eq(pipeline_schedule) + expect(assigns(:schedule)).to be_a_new(Ci::PipelineSchedule) end end - describe 'DELETE #destroy' do - set(:user) { create(:user) } + describe 'POST #create' do + describe 'functionality' do + set(:user) { create(:user) } - context 'when a developer makes the request' do before do project.add_developer(user) sign_in(user) + end - delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + let(:basic_param) do + attributes_for(:ci_pipeline_schedule) end - it 'does not delete the pipeline schedule' do - expect(response).not_to have_http_status(:ok) + context 'when variables_attributes has one variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'creates a new schedule' do + expect { go } + .to change { Ci::PipelineSchedule.count }.by(1) + .and change { Ci::PipelineScheduleVariable.count }.by(1) + + expect(response).to have_http_status(:found) + + Ci::PipelineScheduleVariable.last.tap do |v| + expect(v.key).to eq("AAA") + expect(v.value).to eq("AAA123") + end + end + end + + context 'when variables_attributes has two variables and duplicted' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }] + }) + end + + it 'returns an error that the keys of variable are duplicated' do + expect { go } + .to change { Ci::PipelineSchedule.count }.by(0) + .and change { Ci::PipelineScheduleVariable.count }.by(0) + + expect(assigns(:schedule).errors['variables']).not_to be_empty + end end end - context 'when a master makes the request' do + describe 'security' do + let(:schedule) { attributes_for(:ci_pipeline_schedule) } + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + end + + def go + post :create, namespace_id: project.namespace.to_param, project_id: project, schedule: schedule + end + end + + describe 'PUT #update' do + describe 'functionality' do + set(:user) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + before do - project.add_master(user) + project.add_developer(user) sign_in(user) end - it 'destroys the pipeline schedule' do - expect do - delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id - end.to change { project.pipeline_schedules.count }.by(-1) + context 'when a pipeline schedule has no variables' do + let(:basic_param) do + { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } + end - expect(response).to have_http_status(302) + context 'when params include one variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'inserts new variable to the pipeline schedule' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + pipeline_schedule.reload + expect(response).to have_http_status(:found) + expect(pipeline_schedule.variables.last.key).to eq('AAA') + expect(pipeline_schedule.variables.last.value).to eq('AAA123') + end + end + + context 'when params include two duplicated variables' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }] + }) + end + + it 'returns an error that variables are duplciated' do + go + + expect(assigns(:schedule).errors['variables']).not_to be_empty + end + end + end + + context 'when a pipeline schedule has one variable' do + let(:basic_param) do + { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } + end + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'CCC', pipeline_schedule: pipeline_schedule) + end + + context 'when adds a new variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'adds the new variable' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + pipeline_schedule.reload + expect(pipeline_schedule.variables.last.key).to eq('AAA') + end + end + + context 'when adds a new duplicated variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'CCC', value: 'AAA123' }] + }) + end + + it 'returns an error' do + expect { go }.not_to change { Ci::PipelineScheduleVariable.count } + + pipeline_schedule.reload + expect(assigns(:schedule).errors['variables']).not_to be_empty + end + end + + context 'when updates a variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ id: pipeline_schedule_variable.id, value: 'new_value' }] + }) + end + + it 'updates the variable' do + expect { go }.not_to change { Ci::PipelineScheduleVariable.count } + + pipeline_schedule_variable.reload + expect(pipeline_schedule_variable.value).to eq('new_value') + end + end + + context 'when deletes a variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true }] + }) + end + + it 'delete the existsed variable' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(-1) + end + end + + context 'when deletes and creates a same key simultaneously' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true }, + { key: 'CCC', value: 'CCC123' }] + }) + end + + it 'updates the variable' do + expect { go }.not_to change { Ci::PipelineScheduleVariable.count } + + pipeline_schedule.reload + expect(pipeline_schedule.variables.last.key).to eq('CCC') + expect(pipeline_schedule.variables.last.value).to eq('CCC123') + end + end end end - end - describe 'security' do - include AccessMatchersForController + describe 'security' do + let(:schedule) { { description: 'updated_desc' } } - describe 'GET edit' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } - def go + context 'when a developer created a pipeline schedule' do + let(:developer_1) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer_1) } + + before do + project.add_developer(developer_1) + end + + it { expect { go }.to be_allowed_for(developer_1) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + end + + context 'when a master created a pipeline schedule' do + let(:master_1) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master_1) } + + before do + project.add_master(master_1) + end + + it { expect { go }.to be_allowed_for(master_1) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + end + end + + def go + put :update, namespace_id: project.namespace.to_param, + project_id: project, id: pipeline_schedule, + schedule: schedule + end + end + + describe 'GET #edit' do + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + it 'loads the pipeline schedule' do get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + + expect(response).to have_http_status(:ok) + expect(assigns(:schedule)).to eq(pipeline_schedule) end end - describe 'GET take_ownership' do + describe 'security' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } + end - def go - post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id - end + def go + get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id end + end - describe 'PUT update' do + describe 'GET #take_ownership' do + describe 'security' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } + end + + def go + post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'DELETE #destroy' do + set(:user) { create(:user) } + + context 'when a developer makes the request' do + before do + project.add_developer(user) + sign_in(user) - def go - put :update, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id, - schedule: { description: 'a' } + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + + it 'does not delete the pipeline schedule' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when a master makes the request' do + before do + project.add_master(user) + sign_in(user) + end + + it 'destroys the pipeline schedule' do + expect do + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end.to change { project.pipeline_schedules.count }.by(-1) + + expect(response).to have_http_status(302) end end end diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca64d1aada0bcd7ef388d4c56f913f3c4738a36b --- /dev/null +++ b/spec/factories/ci/pipeline_schedule_variables.rb @@ -0,0 +1,8 @@ +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 diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 60e6549f7035e9e989097e64063e4aa260d1efd5..fee54466b81922cd1c0979a83c6c359a2b19e059 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Pipeline Schedules', :feature do +feature 'Pipeline Schedules', :feature, js: true do include PipelineSchedulesHelper let!(:project) { create(:project) } @@ -11,27 +11,20 @@ before do project.add_master(user) - - sign_in(user) - visit_page + gitlab_sign_in(user) end describe 'GET /projects/pipeline_schedules' do - let(:visit_page) { 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) + before do + visit_pipelines_schedules end describe 'The view' do it 'displays the required information description' do page.within('.pipeline-schedule-table-row') do 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("##{pipeline.id}") end @@ -62,7 +55,7 @@ it 'deletes the pipeline' do click_link 'Delete' - expect(page).not_to have_content('pipeline schedule') + expect(page).not_to have_css(".pipeline-schedule-table-row") end end @@ -78,8 +71,10 @@ end end - describe 'POST /projects/pipeline_schedules/new', js: true do - let(:visit_page) { visit_new_pipeline_schedule } + describe 'POST /projects/pipeline_schedules/new' do + before do + visit_new_pipeline_schedule + end it 'sets defaults for timezone and target branch' do expect(page).to have_button('master') @@ -100,8 +95,8 @@ end end - describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do - let(:visit_page) do + describe 'PATCH /projects/pipelines_schedules/:id/edit' do + before do edit_pipeline_schedule end @@ -134,6 +129,72 @@ 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 visit new_project_pipeline_schedule_path(project, pipeline_schedule) end diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5b316b319a56155b15c16bc522c8852fbca2b458 --- /dev/null +++ b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js @@ -0,0 +1,145 @@ +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 = $(`
+
  • + + +
  • +
    `); + + insertRow($markup.find('.js-row')); + + expect($markup.find('.js-row').length).toBe(2); + }); + + it('should clear `data-is-persisted` on cloned row', () => { + $markup = $(`
    +
  • +
    `); + + 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 = $(`
    +
  • + + +
  • +
    `); + + 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 = $(`
    +
  • + + +
  • +
    `); + + 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 = $(`
    +
  • + +
  • +
    `); + + 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 = $(`
    +
  • + + + + +
  • +
    `); + + 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(''); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 51e80e20a9d1d525a4d49dd06c520301fcf4afff..5315a462f9ae244422b65a8518bba1a5f06205b4 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -141,8 +141,11 @@ pipeline_schedules: - owner - pipelines - last_pipeline +- variables pipeline_schedule: - pipelines +pipeline_schedule_variables: +- pipeline_schedule deploy_keys: - user - deploy_keys_projects diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 18c8a6c57e9897e1a39919473198e09db3ee8499..b8c94416dc271c51334b0959f661dfeeb5f23067 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1446,6 +1446,23 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) it { is_expected.to include(predefined_trigger_variable) } 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 before do build.yaml_variables = nil diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 56817baf79d65d679a0da8b1e9c729e2c6e0f7b5..6427deda31e6c630b47da84f317133d44bdf8dd1 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -5,6 +5,7 @@ it { is_expected.to belong_to(:owner) } 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(:cron) } @@ -117,4 +118,20 @@ 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 diff --git a/spec/models/ci/pipeline_schedule_variable_spec.rb b/spec/models/ci/pipeline_schedule_variable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0de76a57b7f7440d7f0c87f43cc33050a4410916 --- /dev/null +++ b/spec/models/ci/pipeline_schedule_variable_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe Ci::PipelineScheduleVariable, models: true do + subject { build(:ci_pipeline_schedule_variable) } + + it { is_expected.to include_module(HasVariable) } +end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb index 85d11deb26f2b0888a9166b50d8d1eccd7a8ef66..b34555d2815e442f3e0395f16a5d2c57333462a8 100644 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ b/spec/requests/api/pipeline_schedules_spec.rb @@ -279,6 +279,8 @@ def active?(str) end 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 delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index fb43f51c70c48b84fce249add51fa75cefc1f1e5..ff60bd0c0aeae05c57660adc023c5dc284fb5ba3 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -50,9 +50,24 @@ def description_for(role, type, expected, result) "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" 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| match do |action| - emulate_user(role, @membership) + user = emulate_user(role, @membership) + update_owner(@objects, user) action.call EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) @@ -62,13 +77,18 @@ def description_for(role, type, expected, result) @membership = membership end + chain :own do |*objects| + @objects = objects + end + description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } supports_block_expectations end matcher :be_denied_for do |role| match do |action| - emulate_user(role, @membership) + user = emulate_user(role, @membership) + update_owner(@objects, user) action.call EXPECTED_STATUS_CODE_DENIED.include?(response.status) @@ -78,6 +98,10 @@ def description_for(role, type, expected, result) @membership = membership end + chain :own do |*objects| + @objects = objects + end + description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } supports_block_expectations end