Commit aa2b9652 authored by Furkan Ayhan's avatar Furkan Ayhan

Add support for manual bridges for CI pipelines

With this commit, bridges can be defined with when:manual.
This is behind a FF: ci_manual_bridges
parent 3ee0a7e1
...@@ -126,7 +126,7 @@ export default { ...@@ -126,7 +126,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="ci-job-component"> <div class="ci-job-component" data-qa-selector="job_item_container">
<gl-link <gl-link
v-if="status.has_details" v-if="status.has_details"
v-gl-tooltip="{ boundary, placement: 'bottom' }" v-gl-tooltip="{ boundary, placement: 'bottom' }"
...@@ -156,6 +156,7 @@ export default { ...@@ -156,6 +156,7 @@ export default {
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</div> </div>
......
...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload include SendFileUpload
include ContinueParams include ContinueParams
before_action :build, except: [:index] before_action :find_job_as_build, except: [:index, :play]
before_action :find_job_as_processable, only: [:play]
before_action :authorize_read_build! before_action :authorize_read_build!
before_action :authorize_update_build!, before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :erase] except: [:index, :show, :status, :raw, :trace, :erase]
...@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def trace def trace
build.trace.read do |stream| @build.trace.read do |stream|
respond_to do |format| respond_to do |format|
format.json do format.json do
build.trace.being_watched! @build.trace.being_watched!
build_trace = Ci::BuildTrace.new( build_trace = Ci::BuildTrace.new(
build: @build, build: @build,
...@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController
def play def play
return respond_422 unless @build.playable? return respond_422 unless @build.playable?
build = @build.play(current_user, play_params[:job_variables_attributes]) job = @build.play(current_user, play_params[:job_variables_attributes])
redirect_to build_path(build)
if job.is_a?(Ci::Bridge)
redirect_to pipeline_path(job.pipeline)
else
redirect_to build_path(job)
end
end end
def cancel def cancel
...@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController
send_params: raw_send_params, send_params: raw_send_params,
redirect_params: raw_redirect_params) redirect_params: raw_redirect_params)
else else
build.trace.read do |stream| @build.trace.read do |stream|
if stream.file? if stream.file?
workhorse_set_content_type! workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
...@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController
private private
def authorize_update_build! def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build) return access_denied! unless can?(current_user, :update_build, @build)
end end
def authorize_erase_build! def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, build) return access_denied! unless can?(current_user, :erase_build, @build)
end end
def authorize_use_build_terminal! def authorize_use_build_terminal!
return access_denied! unless can?(current_user, :create_build_terminal, build) return access_denied! unless can?(current_user, :create_build_terminal, @build)
end end
def authorize_create_proxy_build! def authorize_create_proxy_build!
return access_denied! unless can?(current_user, :create_build_service_proxy, build) return access_denied! unless can?(current_user, :create_build_service_proxy, @build)
end end
def verify_api_request! def verify_api_request!
...@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController
end end
def trace_artifact_file def trace_artifact_file
@trace_artifact_file ||= build.job_artifacts_trace&.file @trace_artifact_file ||= @build.job_artifacts_trace&.file
end end
def build def find_job_as_build
@build ||= project.builds.find(params[:id]) @build = project.builds.find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
end end
def find_job_as_processable
if ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
@build = project.processables.find(params[:id])
else
find_job_as_build
end
end
def build_path(build) def build_path(build)
project_job_path(build.project, build) project_job_path(build.project, build)
end end
...@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController
end end
def build_service_specification def build_service_specification
build.service_specification(service: params['service'], @build.service_specification(service: params['service'],
port: params['port'], port: params['port'],
path: params['path'], path: params['path'],
subprotocols: proxy_subprotocol) subprotocols: proxy_subprotocol)
end end
def proxy_subprotocol def proxy_subprotocol
......
...@@ -27,7 +27,7 @@ module Ci ...@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize # rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do state_machine :status do
after_transition created: :pending do |bridge| after_transition [:created, :manual] => :pending do |bridge|
next unless bridge.downstream_project next unless bridge.downstream_project
bridge.run_after_commit do bridge.run_after_commit do
...@@ -46,6 +46,10 @@ module Ci ...@@ -46,6 +46,10 @@ module Ci
event :scheduled do event :scheduled do
transition all => :scheduled transition all => :scheduled
end end
event :actionize do
transition created: :manual
end
end end
def self.retry(bridge, current_user) def self.retry(bridge, current_user)
...@@ -126,9 +130,27 @@ module Ci ...@@ -126,9 +130,27 @@ module Ci
false false
end end
def playable?
return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
action? && !archived? && manual?
end
def action? def action?
false return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
%w[manual].include?(self.when)
end
# rubocop: disable CodeReuse/ServiceClass
# We don't need it but we are taking `job_variables_attributes` parameter
# to make it consistent with `Ci::Build#play` method.
def play(current_user, job_variables_attributes = nil)
Ci::PlayBridgeService
.new(project, current_user)
.execute(self)
end end
# rubocop: enable CodeReuse/ServiceClass
def artifacts? def artifacts?
false false
...@@ -185,6 +207,10 @@ module Ci ...@@ -185,6 +207,10 @@ module Ci
[] []
end end
def target_revision_ref
downstream_pipeline_params.dig(:target_revision, :ref)
end
private private
def cross_project_params def cross_project_params
......
...@@ -298,6 +298,7 @@ class Project < ApplicationRecord ...@@ -298,6 +298,7 @@ class Project < ApplicationRecord
# bulk that doesn't involve loading the rows into memory. As a result we're # bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here. # still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
......
# frozen_string_literal: true
module Ci
class BridgePolicy < CommitStatusPolicy
condition(:can_update_downstream_branch) do
::Gitlab::UserAccess.new(@user, container: @subject.downstream_project)
.can_update_branch?(@subject.target_revision_ref)
end
rule { can_update_downstream_branch }.enable :play_job
end
end
...@@ -60,6 +60,8 @@ module Ci ...@@ -60,6 +60,8 @@ module Ci
rule { can?(:update_build) & terminal }.enable :create_build_terminal rule { can?(:update_build) & terminal }.enable :create_build_terminal
rule { can?(:update_build) }.enable :play_job
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal enable :read_web_ide_terminal
enable :update_web_ide_terminal enable :update_web_ide_terminal
......
# frozen_string_literal: true
module Ci
class PlayBridgeService < ::BaseService
def execute(bridge)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, bridge)
bridge.tap do |bridge|
bridge.user = current_user
bridge.enqueue!
end
end
end
end
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
module Ci module Ci
class PlayBuildService < ::BaseService class PlayBuildService < ::BaseService
def execute(build, job_variables_attributes = nil) def execute(build, job_variables_attributes = nil)
unless can?(current_user, :update_build, build) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build)
raise Gitlab::Access::AccessDeniedError
end
# Try to enqueue the build, otherwise create a duplicate. # Try to enqueue the build, otherwise create a duplicate.
# #
......
...@@ -9,12 +9,12 @@ module Ci ...@@ -9,12 +9,12 @@ module Ci
end end
def execute(stage) def execute(stage)
stage.builds.manual.each do |build| stage.processables.manual.each do |processable|
next unless build.playable? next unless processable.playable?
build.play(current_user) processable.play(current_user)
rescue Gitlab::Access::AccessDeniedError rescue Gitlab::Access::AccessDeniedError
logger.error(message: 'Unable to play manual action', build_id: build.id) logger.error(message: 'Unable to play manual action', processable_id: processable.id)
end end
end end
......
---
name: ci_manual_bridges
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44011
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263412
type: development
group: group::pipeline authoring
default_enabled: false
...@@ -2390,9 +2390,9 @@ To trigger a manual job, a user must have permission to merge to the assigned br ...@@ -2390,9 +2390,9 @@ To trigger a manual job, a user must have permission to merge to the assigned br
You can use [protected branches](../../user/project/protected_branches.md) to more strictly You can use [protected branches](../../user/project/protected_branches.md) to more strictly
[protect manual deployments](#protecting-manual-jobs) from being run by unauthorized users. [protect manual deployments](#protecting-manual-jobs) from being run by unauthorized users.
`when:manual` and [`trigger`](#trigger) cannot be used together. If you use both in In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
the same job, you receive a `jobs:#{job-name} when should be on_success, on_failure or always` can use `when:manual` in the same job as [`trigger`](#trigger). In GitLab 13.4 and
error. earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
##### Protecting manual jobs **(PREMIUM)** ##### Protecting manual jobs **(PREMIUM)**
...@@ -3643,10 +3643,9 @@ You can use this keyword to create two different types of downstream pipelines: ...@@ -3643,10 +3643,9 @@ You can use this keyword to create two different types of downstream pipelines:
see which job triggered a downstream pipeline by hovering your mouse cursor over see which job triggered a downstream pipeline by hovering your mouse cursor over
the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines). the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines).
NOTE: **Note:** In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
Using a `trigger` with `when:manual` together results in the error `jobs:#{job-name} can use [`when:manual`](#whenmanual) in the same job as `trigger`. In GitLab 13.4 and
when should be on_success, on_failure or always`, because `when:manual` prevents earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
triggers being used.
#### Simple `trigger` syntax for multi-project pipelines #### Simple `trigger` syntax for multi-project pipelines
......
...@@ -11,15 +11,18 @@ module Gitlab ...@@ -11,15 +11,18 @@ module Gitlab
class Bridge < ::Gitlab::Config::Entry::Node class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Ci::Config::Entry::Processable include ::Gitlab::Ci::Config::Entry::Processable
ALLOWED_WHEN = %w[on_success on_failure always manual].freeze
ALLOWED_KEYS = %i[trigger].freeze ALLOWED_KEYS = %i[trigger].freeze
validations do validations do
validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS
with_options allow_nil: true do with_options allow_nil: true do
validates :when, validates :allow_failure, boolean: true
inclusion: { in: %w[on_success on_failure always], validates :when, inclusion: {
message: 'should be on_success, on_failure or always' } in: ALLOWED_WHEN,
message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
}
end end
validate on: :composed do validate on: :composed do
...@@ -57,11 +60,19 @@ module Gitlab ...@@ -57,11 +60,19 @@ module Gitlab
true true
end end
def manual_action?
self.when == 'manual'
end
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
def value def value
super.merge( super.merge(
trigger: (trigger_value if trigger_defined?), trigger: (trigger_value if trigger_defined?),
needs: (needs_value if needs_defined?), needs: (needs_value if needs_defined?),
ignore: !!allow_failure, ignore: ignored?,
when: self.when, when: self.when,
scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage
).compact ).compact
......
...@@ -70,6 +70,10 @@ module Gitlab ...@@ -70,6 +70,10 @@ module Gitlab
def self.one_dimensional_matrix_enabled? def self.one_dimensional_matrix_enabled?
::Feature.enabled?(:one_dimensional_matrix, default_enabled: false) ::Feature.enabled?(:one_dimensional_matrix, default_enabled: false)
end end
def self.manual_bridges_enabled?(project)
::Feature.enabled?(:ci_manual_bridges, project, default_enabled: false)
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Bridge
class Action < Status::Build::Action
end
end
end
end
end
...@@ -6,7 +6,10 @@ module Gitlab ...@@ -6,7 +6,10 @@ module Gitlab
module Bridge module Bridge
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses def self.extended_statuses
[Status::Bridge::Failed] [[Status::Bridge::Failed],
[Status::Bridge::Manual],
[Status::Bridge::Play],
[Status::Bridge::Action]]
end end
def self.common_helpers def self.common_helpers
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Bridge
class Manual < Status::Build::Manual
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Bridge
class Play < Status::Build::Play
def has_action?
can?(user, :play_job, subject)
end
def self.matches?(bridge, user)
bridge.playable?
end
end
end
end
end
end
...@@ -16,8 +16,9 @@ module QA ...@@ -16,8 +16,9 @@ module QA
end end
view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do
element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern element :job_item_container
element :job_link element :job_link
element :action_button
end end
view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
...@@ -40,10 +41,12 @@ module QA ...@@ -40,10 +41,12 @@ module QA
end end
def has_build?(name, status: :success, wait: nil) def has_build?(name, status: :success, wait: nil)
within('.pipeline-graph') do if status
within('.ci-job-component', text: name) do within_element(:job_item_container, text: name) do
has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
end end
else
has_element?(:job_item_container, text: name)
end end
end end
...@@ -78,6 +81,12 @@ module QA ...@@ -78,6 +81,12 @@ module QA
def click_on_first_job def click_on_first_job
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
end end
def click_job_action(job_name)
within_element(:job_item_container, text: job_name) do
click_element(:action_button)
end
end
end end
end end
end end
......
# frozen_string_literal: true
require 'faker'
module QA
RSpec.describe 'Verify', :runner, :requires_admin do
# [TODO]: Developer to remove :requires_admin once FF is removed in follow up issue
describe "Trigger child pipeline with 'when:manual'" do
let(:feature_flag) { :ci_manual_bridges } # [TODO]: Developer to remove when feature flag is removed
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = [executor]
end
end
before do
Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
Flow::Login.sign_in
add_ci_files
project.visit!
view_the_last_pipeline
end
after do
Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
runner.remove_via_api!
end
it 'can trigger bridge job', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1049' do
Page::Project::Pipeline::Show.perform do |parent_pipeline|
expect(parent_pipeline).not_to have_child_pipeline
parent_pipeline.click_job_action('trigger')
Support::Waiter.wait_until { parent_pipeline.has_child_pipeline? }
parent_pipeline.expand_child_pipeline
expect(parent_pipeline).to have_build('child_build', status: nil)
end
end
private
def add_ci_files
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add parent and child pipelines CI files.'
commit.add_files(
[
child_ci_file,
parent_ci_file
]
)
end
end
def view_the_last_pipeline
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
end
def parent_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
build:
stage: build
tags: ["#{executor}"]
script: echo build
trigger:
stage: test
when: manual
trigger:
include: '.child-pipeline.yml'
deploy:
stage: deploy
tags: ["#{executor}"]
script: echo deploy
YAML
}
end
def child_ci_file
{
file_path: '.child-pipeline.yml',
content: <<~YAML
child_build:
stage: build
tags: ["#{executor}"]
script: echo build
YAML
}
end
end
end
end
...@@ -757,19 +757,21 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -757,19 +757,21 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
name: 'master', project: project) name: 'master', project: project)
sign_in(user) sign_in(user)
post_play
end end
context 'when job is playable' do context 'when job is playable' do
let(:job) { create(:ci_build, :playable, pipeline: pipeline) } let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
it 'redirects to the played job page' do it 'redirects to the played job page' do
post_play
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id)) expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end end
it 'transits to pending' do it 'transits to pending' do
post_play
expect(job.reload).to be_pending expect(job.reload).to be_pending
end end
...@@ -777,15 +779,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -777,15 +779,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] } let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] }
it 'assigns the job variables' do it 'assigns the job variables' do
post_play
expect(job.reload.job_variables.map(&:key)).to contain_exactly('first') expect(job.reload.job_variables.map(&:key)).to contain_exactly('first')
end end
end end
context 'when job is bridge' do
let(:downstream_project) { create(:project) }
let(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
before do
downstream_project.add_developer(user)
end
it 'redirects to the pipeline page' do
post_play
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(pipeline_path(pipeline))
builds_namespace_project_pipeline_path(id: pipeline.id)
end
it 'transits to pending' do
post_play
expect(job.reload).to be_pending
end
context 'when FF ci_manual_bridges is disabled' do
before do
stub_feature_flags(ci_manual_bridges: false)
end
it 'returns 404' do
post_play
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end end
context 'when job is not playable' do context 'when job is not playable' do
let(:job) { create(:ci_build, pipeline: pipeline) } let(:job) { create(:ci_build, pipeline: pipeline) }
it 'renders unprocessable_entity' do it 'renders unprocessable_entity' do
post_play
expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(response).to have_gitlab_http_status(:unprocessable_entity)
end end
end end
......
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Pipelines::StagesController do RSpec.describe Projects::Pipelines::StagesController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:downstream_project) { create(:project, :repository) }
before do before do
sign_in(user) sign_in(user)
...@@ -17,6 +18,7 @@ RSpec.describe Projects::Pipelines::StagesController do ...@@ -17,6 +18,7 @@ RSpec.describe Projects::Pipelines::StagesController do
before do before do
create_manual_build(pipeline, 'test', 'rspec 1/2') create_manual_build(pipeline, 'test', 'rspec 1/2')
create_manual_build(pipeline, 'test', 'rspec 2/2') create_manual_build(pipeline, 'test', 'rspec 2/2')
create_manual_bridge(pipeline, 'test', 'trigger')
pipeline.reload pipeline.reload
end end
...@@ -32,6 +34,7 @@ RSpec.describe Projects::Pipelines::StagesController do ...@@ -32,6 +34,7 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when user has access' do context 'when user has access' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
downstream_project.add_maintainer(user)
end end
context 'when the stage does not exists' do context 'when the stage does not exists' do
...@@ -46,12 +49,12 @@ RSpec.describe Projects::Pipelines::StagesController do ...@@ -46,12 +49,12 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when the stage exists' do context 'when the stage exists' do
it 'starts all manual jobs' do it 'starts all manual jobs' do
expect(pipeline.builds.manual.count).to eq(2) expect(pipeline.processables.manual.count).to eq(3)
play_manual_stage! play_manual_stage!
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(pipeline.builds.manual.count).to eq(0) expect(pipeline.processables.manual.count).to eq(0)
end end
end end
end end
...@@ -68,5 +71,9 @@ RSpec.describe Projects::Pipelines::StagesController do ...@@ -68,5 +71,9 @@ RSpec.describe Projects::Pipelines::StagesController do
def create_manual_build(pipeline, stage, name) def create_manual_build(pipeline, stage, name)
create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name) create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name)
end end
def create_manual_bridge(pipeline, stage, name)
create(:ci_bridge, :manual, pipeline: pipeline, stage: stage, name: name, downstream: downstream_project)
end
end end
end end
...@@ -40,6 +40,10 @@ FactoryBot.define do ...@@ -40,6 +40,10 @@ FactoryBot.define do
end end
end end
trait :created do
status { 'created' }
end
trait :started do trait :started do
started_at { '2013-10-29 09:51:28 CET' } started_at { '2013-10-29 09:51:28 CET' }
end end
...@@ -62,5 +66,14 @@ FactoryBot.define do ...@@ -62,5 +66,14 @@ FactoryBot.define do
trait :strategy_depend do trait :strategy_depend do
options { { trigger: { strategy: 'depend' } } } options { { trigger: { strategy: 'depend' } } }
end end
trait :manual do
status { 'manual' }
self.when { 'manual' }
end
trait :playable do
manual
end
end end
end end
...@@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do ...@@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end end
end end
end end
describe '#manual_action?' do
context 'when job is a manual action' do
let(:config) { { script: 'deploy', when: 'manual' } }
it { is_expected.to be_manual_action }
end
context 'when job is not a manual action' do
let(:config) { { script: 'deploy' } }
it { is_expected.not_to be_manual_action }
end
end
describe '#ignored?' do
context 'when job is a manual action' do
context 'when it is not specified if job is allowed to fail' do
let(:config) do
{ script: 'deploy', when: 'manual' }
end
it { is_expected.to be_ignored }
end
context 'when job is allowed to fail' do
let(:config) do
{ script: 'deploy', when: 'manual', allow_failure: true }
end
it { is_expected.to be_ignored }
end
context 'when job is not allowed to fail' do
let(:config) do
{ script: 'deploy', when: 'manual', allow_failure: false }
end
it { is_expected.not_to be_ignored }
end
end
context 'when job is not a manual action' do
context 'when it is not specified if job is allowed to fail' do
let(:config) { { script: 'deploy' } }
it { is_expected.not_to be_ignored }
end
context 'when job is allowed to fail' do
let(:config) { { script: 'deploy', allow_failure: true } }
it { is_expected.to be_ignored }
end
context 'when job is not allowed to fail' do
let(:config) { { script: 'deploy', allow_failure: false } }
it { is_expected.not_to be_ignored }
end
end
end
end end
...@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do ...@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end end
context 'when bridge is created' do context 'when bridge is created' do
let(:bridge) { create(:ci_bridge) } let(:bridge) { create_bridge(:created) }
it 'matches correct core status' do it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Created expect(factory.core_status).to be_a Gitlab::Ci::Status::Created
...@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do ...@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end end
context 'when bridge is failed' do context 'when bridge is failed' do
let(:bridge) { create(:ci_bridge, :failed) } let(:bridge) { create_bridge(:failed) }
it 'matches correct core status' do it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
...@@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do ...@@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end end
end end
end end
context 'when bridge is a manual action' do
let(:bridge) { create_bridge(:playable) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Bridge::Manual,
Gitlab::Ci::Status::Bridge::Play,
Gitlab::Ci::Status::Bridge::Action]
end
it 'fabricates action detailed status' do
expect(status).to be_a Gitlab::Ci::Status::Bridge::Action
end
it 'fabricates status with correct details' do
expect(status.text).to eq s_('CiStatusText|manual')
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.illustration).to include(:image, :size, :title, :content)
expect(status.label).to include 'manual play action'
expect(status).not_to have_details
expect(status.action_path).to include 'play'
end
context 'when user has ability to play action' do
before do
bridge.downstream_project.add_developer(user)
end
it 'fabricates status that has action' do
expect(status).to have_action
end
end
context 'when user does not have ability to play action' do
it 'fabricates status that has no action' do
expect(status).not_to have_action
end
end
end
private
def create_bridge(trait)
upstream_project = create(:project, :repository)
downstream_project = create(:project, :repository)
upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project)
trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } }
create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline)
end
end end
...@@ -413,6 +413,7 @@ project: ...@@ -413,6 +413,7 @@ project:
- stages - stages
- ci_refs - ci_refs
- builds - builds
- processables
- runner_projects - runner_projects
- runners - runners
- variables - variables
......
...@@ -59,30 +59,20 @@ RSpec.describe Ci::Bridge do ...@@ -59,30 +59,20 @@ RSpec.describe Ci::Bridge do
describe 'state machine transitions' do describe 'state machine transitions' do
context 'when bridge points towards downstream' do context 'when bridge points towards downstream' do
it 'schedules downstream pipeline creation' do %i[created manual].each do |status|
expect(bridge).to receive(:schedule_downstream_pipeline!) it "schedules downstream pipeline creation when the status is #{status}" do
bridge.status = status
bridge.enqueue! expect(bridge).to receive(:schedule_downstream_pipeline!)
end
end
end
describe 'state machine transitions' do
context 'when bridge points towards downstream' do
it 'schedules downstream pipeline creation' do
expect(bridge).to receive(:schedule_downstream_pipeline!)
bridge.enqueue! bridge.enqueue!
end
end end
end
end
describe 'state machine transitions' do it 'raises error when the status is failed' do
context 'when bridge points towards downstream' do bridge.status = :failed
it 'schedules downstream pipeline creation' do
expect(bridge).to receive(:schedule_downstream_pipeline!)
bridge.enqueue! expect { bridge.enqueue! }.to raise_error(StateMachines::InvalidTransition)
end end
end end
end end
...@@ -304,4 +294,67 @@ RSpec.describe Ci::Bridge do ...@@ -304,4 +294,67 @@ RSpec.describe Ci::Bridge do
end end
end end
end end
describe '#play' do
let(:downstream_project) { create(:project) }
let(:user) { create(:user) }
let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
subject { bridge.play(user) }
before do
project.add_maintainer(user)
downstream_project.add_maintainer(user)
end
it 'enqueues the bridge' do
subject
expect(bridge).to be_pending
end
end
describe '#playable?' do
context 'when bridge is a manual action' do
subject { build_stubbed(:ci_bridge, :manual).playable? }
it { is_expected.to be_truthy }
context 'when FF ci_manual_bridges is disabled' do
before do
stub_feature_flags(ci_manual_bridges: false)
end
it { is_expected.to be_falsey }
end
end
context 'when build is not a manual action' do
subject { build_stubbed(:ci_bridge, :created).playable? }
it { is_expected.to be_falsey }
end
end
describe '#action?' do
context 'when bridge is a manual action' do
subject { build_stubbed(:ci_bridge, :manual).action? }
it { is_expected.to be_truthy }
context 'when FF ci_manual_bridges is disabled' do
before do
stub_feature_flags(ci_manual_bridges: false)
end
it { is_expected.to be_falsey }
end
end
context 'when build is not a manual action' do
subject { build_stubbed(:ci_bridge, :created).action? }
it { is_expected.to be_falsey }
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::BridgePolicy do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:downstream_project, reload: true) { create(:project, :repository) }
let_it_be(:pipeline, reload: true) { create(:ci_empty_pipeline, project: project) }
let_it_be(:bridge, reload: true) { create(:ci_bridge, pipeline: pipeline, downstream: downstream_project) }
let(:policy) do
described_class.new(user, bridge)
end
describe '#play_job' do
before do
fake_access = double('Gitlab::UserAccess')
expect(fake_access).to receive(:can_update_branch?).with('master').and_return(can_update_branch)
expect(Gitlab::UserAccess).to receive(:new).with(user, container: downstream_project).and_return(fake_access)
end
context 'when user can update the downstream branch' do
let(:can_update_branch) { true }
it 'allows' do
expect(policy).to be_allowed :play_job
end
end
context 'when user can not update the downstream branch' do
let(:can_update_branch) { false }
it 'does not allow' do
expect(policy).not_to be_allowed :play_job
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PlayBridgeService, '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:downstream_project) { create(:project) }
let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
let(:instance) { described_class.new(project, user) }
subject(:execute_service) { instance.execute(bridge) }
context 'when user can run the bridge' do
before do
allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(true)
end
it 'marks the bridge pending' do
execute_service
expect(bridge.reload).to be_pending
end
it 'enqueues Ci::CreateCrossProjectPipelineWorker' do
expect(::Ci::CreateCrossProjectPipelineWorker).to receive(:perform_async).with(bridge.id)
execute_service
end
it "updates bridge's user" do
execute_service
expect(bridge.reload.user).to eq(user)
end
context 'when bridge is not playable' do
let(:bridge) { create(:ci_bridge, :failed, pipeline: pipeline, downstream: downstream_project) }
it 'raises StateMachines::InvalidTransition' do
expect { execute_service }.to raise_error StateMachines::InvalidTransition
end
end
end
context 'when user can not run the bridge' do
before do
allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(false)
end
it 'allows user with developer role to play a bridge' do
expect { execute_service }.to raise_error Gitlab::Access::AccessDeniedError
end
end
end
...@@ -6,6 +6,7 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do ...@@ -6,6 +6,7 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, user: current_user) } let(:pipeline) { create(:ci_pipeline, user: current_user) }
let(:project) { pipeline.project } let(:project) { pipeline.project }
let(:downstream_project) { create(:project) }
let(:service) { described_class.new(project, current_user, pipeline: pipeline) } let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
let(:stage_status) { 'manual' } let(:stage_status) { 'manual' }
...@@ -18,40 +19,42 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do ...@@ -18,40 +19,42 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
before do before do
project.add_maintainer(current_user) project.add_maintainer(current_user)
downstream_project.add_maintainer(current_user)
create_builds_for_stage(status: stage_status) create_builds_for_stage(status: stage_status)
create_bridge_for_stage(status: stage_status)
end end
context 'when pipeline has manual builds' do context 'when pipeline has manual processables' do
before do before do
service.execute(stage) service.execute(stage)
end end
it 'starts manual builds from pipeline' do it 'starts manual processables from pipeline' do
expect(pipeline.builds.manual.count).to eq(0) expect(pipeline.processables.manual.count).to eq(0)
end end
it 'updates manual builds' do it 'updates manual processables' do
pipeline.builds.each do |build| pipeline.processables.each do |processable|
expect(build.user).to eq(current_user) expect(processable.user).to eq(current_user)
end end
end end
end end
context 'when pipeline has no manual builds' do context 'when pipeline has no manual processables' do
let(:stage_status) { 'failed' } let(:stage_status) { 'failed' }
before do before do
service.execute(stage) service.execute(stage)
end end
it 'does not update the builds' do it 'does not update the processables' do
expect(pipeline.builds.failed.count).to eq(3) expect(pipeline.processables.failed.count).to eq(4)
end end
end end
context 'when user does not have permission on a specific build' do context 'when user does not have permission on a specific processable' do
before do before do
allow_next_instance_of(Ci::Build) do |instance| allow_next_instance_of(Ci::Processable) do |instance|
allow(instance).to receive(:play).and_raise(Gitlab::Access::AccessDeniedError) allow(instance).to receive(:play).and_raise(Gitlab::Access::AccessDeniedError)
end end
...@@ -60,12 +63,14 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do ...@@ -60,12 +63,14 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
it 'logs the error' do it 'logs the error' do
expect(Gitlab::AppLogger).to receive(:error) expect(Gitlab::AppLogger).to receive(:error)
.exactly(stage.builds.manual.count) .exactly(stage.processables.manual.count)
service.execute(stage) service.execute(stage)
end end
end end
private
def create_builds_for_stage(options) def create_builds_for_stage(options)
options.merge!({ options.merge!({
when: 'manual', when: 'manual',
...@@ -77,4 +82,17 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do ...@@ -77,4 +82,17 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
create_list(:ci_build, 3, options) create_list(:ci_build, 3, options)
end end
def create_bridge_for_stage(options)
options.merge!({
when: 'manual',
pipeline: pipeline,
stage: stage.name,
stage_id: stage.id,
user: pipeline.user,
downstream: downstream_project
})
create(:ci_bridge, options)
end
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