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 {
};
</script>
<template>
<div class="ci-job-component">
<div class="ci-job-component" data-qa-selector="job_item_container">
<gl-link
v-if="status.has_details"
v-gl-tooltip="{ boundary, placement: 'bottom' }"
......@@ -156,6 +156,7 @@ export default {
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
......
......@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
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_update_build!,
except: [:index, :show, :status, :raw, :trace, :erase]
......@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def trace
build.trace.read do |stream|
@build.trace.read do |stream|
respond_to do |format|
format.json do
build.trace.being_watched!
@build.trace.being_watched!
build_trace = Ci::BuildTrace.new(
build: @build,
......@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController
def play
return respond_422 unless @build.playable?
build = @build.play(current_user, play_params[:job_variables_attributes])
redirect_to build_path(build)
job = @build.play(current_user, play_params[:job_variables_attributes])
if job.is_a?(Ci::Bridge)
redirect_to pipeline_path(job.pipeline)
else
redirect_to build_path(job)
end
end
def cancel
......@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController
send_params: raw_send_params,
redirect_params: raw_redirect_params)
else
build.trace.read do |stream|
@build.trace.read do |stream|
if stream.file?
workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
......@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController
private
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
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
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
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
def verify_api_request!
......@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController
end
def trace_artifact_file
@trace_artifact_file ||= build.job_artifacts_trace&.file
@trace_artifact_file ||= @build.job_artifacts_trace&.file
end
def build
@build ||= project.builds.find(params[:id])
def find_job_as_build
@build = project.builds.find(params[:id])
.present(current_user: current_user)
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)
project_job_path(build.project, build)
end
......@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def build_service_specification
build.service_specification(service: params['service'],
port: params['port'],
path: params['path'],
subprotocols: proxy_subprotocol)
@build.service_specification(service: params['service'],
port: params['port'],
path: params['path'],
subprotocols: proxy_subprotocol)
end
def proxy_subprotocol
......
......@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do
after_transition created: :pending do |bridge|
after_transition [:created, :manual] => :pending do |bridge|
next unless bridge.downstream_project
bridge.run_after_commit do
......@@ -46,6 +46,10 @@ module Ci
event :scheduled do
transition all => :scheduled
end
event :actionize do
transition created: :manual
end
end
def self.retry(bridge, current_user)
......@@ -126,9 +130,27 @@ module Ci
false
end
def playable?
return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
action? && !archived? && manual?
end
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
# rubocop: enable CodeReuse/ServiceClass
def artifacts?
false
......@@ -185,6 +207,10 @@ module Ci
[]
end
def target_revision_ref
downstream_pipeline_params.dig(:target_revision, :ref)
end
private
def cross_project_params
......
......@@ -298,6 +298,7 @@ class Project < ApplicationRecord
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
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_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
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
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
enable :read_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 @@
module Ci
class PlayBuildService < ::BaseService
def execute(build, job_variables_attributes = nil)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
end
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build)
# Try to enqueue the build, otherwise create a duplicate.
#
......
......@@ -9,12 +9,12 @@ module Ci
end
def execute(stage)
stage.builds.manual.each do |build|
next unless build.playable?
stage.processables.manual.each do |processable|
next unless processable.playable?
build.play(current_user)
processable.play(current_user)
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
......
---
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
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.
`when:manual` and [`trigger`](#trigger) cannot be used together. If you use both in
the same job, you receive a `jobs:#{job-name} when should be on_success, on_failure or always`
error.
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
can use `when:manual` in the same job as [`trigger`](#trigger). In GitLab 13.4 and
earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
##### Protecting manual jobs **(PREMIUM)**
......@@ -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
the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines).
NOTE: **Note:**
Using a `trigger` with `when:manual` together results in the error `jobs:#{job-name}
when should be on_success, on_failure or always`, because `when:manual` prevents
triggers being used.
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
can use [`when:manual`](#whenmanual) in the same job as `trigger`. In GitLab 13.4 and
earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
#### Simple `trigger` syntax for multi-project pipelines
......
......@@ -11,15 +11,18 @@ module Gitlab
class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Ci::Config::Entry::Processable
ALLOWED_WHEN = %w[on_success on_failure always manual].freeze
ALLOWED_KEYS = %i[trigger].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS
with_options allow_nil: true do
validates :when,
inclusion: { in: %w[on_success on_failure always],
message: 'should be on_success, on_failure or always' }
validates :allow_failure, boolean: true
validates :when, inclusion: {
in: ALLOWED_WHEN,
message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
}
end
validate on: :composed do
......@@ -57,11 +60,19 @@ module Gitlab
true
end
def manual_action?
self.when == 'manual'
end
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
def value
super.merge(
trigger: (trigger_value if trigger_defined?),
needs: (needs_value if needs_defined?),
ignore: !!allow_failure,
ignore: ignored?,
when: self.when,
scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage
).compact
......
......@@ -70,6 +70,10 @@ module Gitlab
def self.one_dimensional_matrix_enabled?
::Feature.enabled?(:one_dimensional_matrix, default_enabled: false)
end
def self.manual_bridges_enabled?(project)
::Feature.enabled?(:ci_manual_bridges, project, default_enabled: false)
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
module Bridge
class Factory < Status::Factory
def self.extended_statuses
[Status::Bridge::Failed]
[[Status::Bridge::Failed],
[Status::Bridge::Manual],
[Status::Bridge::Play],
[Status::Bridge::Action]]
end
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
end
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 :action_button
end
view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
......@@ -40,10 +41,12 @@ module QA
end
def has_build?(name, status: :success, wait: nil)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
if status
within_element(:job_item_container, text: name) do
has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
end
else
has_element?(:job_item_container, text: name)
end
end
......@@ -78,6 +81,12 @@ module QA
def click_on_first_job
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
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
......
# 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
name: 'master', project: project)
sign_in(user)
post_play
end
context 'when job is playable' do
let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
it 'redirects to the played job page' do
post_play
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end
it 'transits to pending' do
post_play
expect(job.reload).to be_pending
end
......@@ -777,15 +779,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] }
it 'assigns the job variables' do
post_play
expect(job.reload.job_variables.map(&:key)).to contain_exactly('first')
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
context 'when job is not playable' do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'renders unprocessable_entity' do
post_play
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
......
......@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Pipelines::StagesController do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:downstream_project) { create(:project, :repository) }
before do
sign_in(user)
......@@ -17,6 +18,7 @@ RSpec.describe Projects::Pipelines::StagesController do
before do
create_manual_build(pipeline, 'test', 'rspec 1/2')
create_manual_build(pipeline, 'test', 'rspec 2/2')
create_manual_bridge(pipeline, 'test', 'trigger')
pipeline.reload
end
......@@ -32,6 +34,7 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when user has access' do
before do
project.add_maintainer(user)
downstream_project.add_maintainer(user)
end
context 'when the stage does not exists' do
......@@ -46,12 +49,12 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when the stage exists' 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!
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
......@@ -68,5 +71,9 @@ RSpec.describe Projects::Pipelines::StagesController do
def create_manual_build(pipeline, stage, name)
create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name)
end
def create_manual_bridge(pipeline, stage, name)
create(:ci_bridge, :manual, pipeline: pipeline, stage: stage, name: name, downstream: downstream_project)
end
end
end
......@@ -40,6 +40,10 @@ FactoryBot.define do
end
end
trait :created do
status { 'created' }
end
trait :started do
started_at { '2013-10-29 09:51:28 CET' }
end
......@@ -62,5 +66,14 @@ FactoryBot.define do
trait :strategy_depend do
options { { trigger: { strategy: 'depend' } } }
end
trait :manual do
status { 'manual' }
self.when { 'manual' }
end
trait :playable do
manual
end
end
end
......@@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
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
......@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is created' do
let(:bridge) { create(:ci_bridge) }
let(:bridge) { create_bridge(:created) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Created
......@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is failed' do
let(:bridge) { create(:ci_bridge, :failed) }
let(:bridge) { create_bridge(:failed) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
......@@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
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
......@@ -413,6 +413,7 @@ project:
- stages
- ci_refs
- builds
- processables
- runner_projects
- runners
- variables
......
......@@ -59,30 +59,20 @@ RSpec.describe Ci::Bridge do
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!)
%i[created manual].each do |status|
it "schedules downstream pipeline creation when the status is #{status}" do
bridge.status = status
bridge.enqueue!
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!)
expect(bridge).to receive(:schedule_downstream_pipeline!)
bridge.enqueue!
bridge.enqueue!
end
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!)
it 'raises error when the status is failed' do
bridge.status = :failed
bridge.enqueue!
expect { bridge.enqueue! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
......@@ -304,4 +294,67 @@ RSpec.describe Ci::Bridge do
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
# 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
let(:current_user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, user: current_user) }
let(:project) { pipeline.project }
let(:downstream_project) { create(:project) }
let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
let(:stage_status) { 'manual' }
......@@ -18,40 +19,42 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
before do
project.add_maintainer(current_user)
downstream_project.add_maintainer(current_user)
create_builds_for_stage(status: stage_status)
create_bridge_for_stage(status: stage_status)
end
context 'when pipeline has manual builds' do
context 'when pipeline has manual processables' do
before do
service.execute(stage)
end
it 'starts manual builds from pipeline' do
expect(pipeline.builds.manual.count).to eq(0)
it 'starts manual processables from pipeline' do
expect(pipeline.processables.manual.count).to eq(0)
end
it 'updates manual builds' do
pipeline.builds.each do |build|
expect(build.user).to eq(current_user)
it 'updates manual processables' do
pipeline.processables.each do |processable|
expect(processable.user).to eq(current_user)
end
end
end
context 'when pipeline has no manual builds' do
context 'when pipeline has no manual processables' do
let(:stage_status) { 'failed' }
before do
service.execute(stage)
end
it 'does not update the builds' do
expect(pipeline.builds.failed.count).to eq(3)
it 'does not update the processables' do
expect(pipeline.processables.failed.count).to eq(4)
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
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)
end
......@@ -60,12 +63,14 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
it 'logs the error' do
expect(Gitlab::AppLogger).to receive(:error)
.exactly(stage.builds.manual.count)
.exactly(stage.processables.manual.count)
service.execute(stage)
end
end
private
def create_builds_for_stage(options)
options.merge!({
when: 'manual',
......@@ -77,4 +82,17 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
create_list(:ci_build, 3, options)
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
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