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

Merge branch 'backstage/gb/populating-pipeline-refactoring' into 'master'

Improve how we populate pipeline with stages and builds

Closes #43941

See merge request gitlab-org/gitlab-ce!17841
parents 3adbc579 5b019f43
...@@ -6,6 +6,7 @@ module Ci ...@@ -6,6 +6,7 @@ module Ci
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize
belongs_to :project, inverse_of: :pipelines belongs_to :project, inverse_of: :pipelines
belongs_to :user belongs_to :user
...@@ -361,21 +362,23 @@ module Ci ...@@ -361,21 +362,23 @@ module Ci
def stage_seeds def stage_seeds
return [] unless config_processor return [] unless config_processor
@stage_seeds ||= config_processor.stage_seeds(self) strong_memoize(:stage_seeds) do
seeds = config_processor.stages_attributes.map do |attributes|
Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
end
seeds.select(&:included?)
end
end end
def seeds_size def seeds_size
@seeds_size ||= stage_seeds.sum(&:size) stage_seeds.sum(&:size)
end end
def has_kubernetes_active? def has_kubernetes_active?
project.deployment_platform&.active? project.deployment_platform&.active?
end end
def has_stage_seeds?
stage_seeds.any?
end
def has_warnings? def has_warnings?
builds.latest.failed_but_allowed.any? builds.latest.failed_but_allowed.any?
end end
...@@ -388,6 +391,9 @@ module Ci ...@@ -388,6 +391,9 @@ module Ci
end end
end end
##
# TODO, setting yaml_errors should be moved to the pipeline creation chain.
#
def config_processor def config_processor
return unless ci_yaml_file return unless ci_yaml_file
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
...@@ -472,6 +478,14 @@ module Ci ...@@ -472,6 +478,14 @@ module Ci
end end
end end
def protected_ref?
strong_memoize(:protected_ref) { project.protected_for?(ref) }
end
def legacy_trigger
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
def predefined_variables def predefined_variables
Gitlab::Ci::Variables::Collection.new Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PIPELINE_ID', value: id.to_s) .append(key: 'CI_PIPELINE_ID', value: id.to_s)
......
...@@ -7,6 +7,7 @@ module Ci ...@@ -7,6 +7,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository, Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config, Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
......
module Ci
class CreatePipelineStagesService < BaseService
def execute(pipeline)
pipeline.stage_seeds.each do |seed|
seed.user = current_user
seed.create! do |build|
##
# Create the environment before the build starts. This sets its slug and
# makes it available as an environment variable
#
if build.has_environment?
environment_name = build.expanded_environment_name
project.environments.find_or_create_by(name: environment_name)
end
end
end
end
end
end
...@@ -16,8 +16,8 @@ module Ci ...@@ -16,8 +16,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline| .execute(:trigger, ignore_skip_ci: true) do |pipeline|
pipeline.trigger_requests.create!(trigger: trigger) pipeline.trigger_requests.build(trigger: trigger)
create_pipeline_variables!(pipeline) pipeline.variables.build(variables)
end end
if pipeline.persisted? if pipeline.persisted?
...@@ -33,14 +33,10 @@ module Ci ...@@ -33,14 +33,10 @@ module Ci
end end
end end
def create_pipeline_variables!(pipeline) def variables
return unless params[:variables] params[:variables].to_h.map do |key, value|
variables = params[:variables].map do |key, value|
{ key: key, value: value } { key: key, value: value }
end end
pipeline.variables.create!(variables)
end end
end end
end end
...@@ -9,11 +9,16 @@ module Gitlab ...@@ -9,11 +9,16 @@ module Gitlab
::Ci::Pipeline.transaction do ::Ci::Pipeline.transaction do
pipeline.save! pipeline.save!
@command.seeds_block&.call(pipeline) ##
# Create environments before the pipeline starts.
::Ci::CreatePipelineStagesService #
.new(project, current_user) pipeline.builds.each do |build|
.execute(pipeline) if build.has_environment?
project.environments.find_or_create_by(
name: build.expanded_environment_name
)
end
end
end end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}") error("Failed to persist the pipeline: #{e}")
......
module Gitlab
module Ci
module Pipeline
module Chain
class Populate < Chain::Base
include Chain::Helpers
PopulateError = Class.new(StandardError)
def perform!
##
# Populate pipeline with block argument of CreatePipelineService#execute.
#
@command.seeds_block&.call(pipeline)
##
# Populate pipeline with all stages and builds from pipeline seeds.
#
pipeline.stage_seeds.each do |stage|
stage.user = current_user
pipeline.stages << stage.to_resource
stage.seeds.each do |build|
pipeline.builds << build.to_resource
end
end
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
end
if pipeline.invalid?
return error('Failed to build the pipeline!')
end
raise Populate::PopulateError if pipeline.persisted?
end
def break?
pipeline.errors.any?
end
end
end
end
end
end
...@@ -16,11 +16,7 @@ module Gitlab ...@@ -16,11 +16,7 @@ module Gitlab
@pipeline.drop!(:config_error) @pipeline.drop!(:config_error)
end end
return error(@pipeline.yaml_errors) error(@pipeline.yaml_errors)
end
unless @pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end end
end end
......
module Gitlab
module Ci
module Pipeline
module Seed
class Base
def attributes
raise NotImplementedError
end
def included?
raise NotImplementedError
end
def to_resource
raise NotImplementedError
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Seed
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
delegate :dig, to: :@attributes
def initialize(pipeline, attributes)
@pipeline = pipeline
@attributes = attributes
@only = attributes.delete(:only)
@except = attributes.delete(:except)
end
def user=(current_user)
@attributes.merge!(user: current_user)
end
def included?
strong_memoize(:inclusion) do
only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
end
end
def attributes
@attributes.merge(
pipeline: @pipeline,
project: @pipeline.project,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
protected: @pipeline.protected_ref?
)
end
def to_resource
strong_memoize(:resource) do
::Ci::Build.new(attributes)
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Seed
class Stage < Seed::Base
include Gitlab::Utils::StrongMemoize
delegate :size, to: :seeds
delegate :dig, to: :seeds
def initialize(pipeline, attributes)
@pipeline = pipeline
@attributes = attributes
@builds = attributes.fetch(:builds).map do |attributes|
Seed::Build.new(@pipeline, attributes)
end
end
def user=(current_user)
@builds.each { |seed| seed.user = current_user }
end
def attributes
{ name: @attributes.fetch(:name),
pipeline: @pipeline,
project: @pipeline.project }
end
def seeds
strong_memoize(:seeds) do
@builds.select(&:included?)
end
end
def included?
seeds.any?
end
def to_resource
strong_memoize(:stage) do
::Ci::Stage.new(attributes).tap do |stage|
seeds.each { |seed| stage.builds << seed.to_resource }
end
end
end
end
end
end
end
end
module Gitlab
module Ci
module Stage
class Seed
include ::Gitlab::Utils::StrongMemoize
attr_reader :pipeline
delegate :project, to: :pipeline
delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
@stage = { name: stage }
@jobs = jobs.to_a.dup
end
def user=(current_user)
@jobs.map! do |attributes|
attributes.merge(user: current_user)
end
end
def stage
@stage.merge(project: project)
end
def builds
trigger = pipeline.trigger_requests.first
@jobs.map do |attributes|
attributes.merge(project: project,
ref: pipeline.ref,
tag: pipeline.tag,
trigger_request: trigger,
protected: protected_ref?)
end
end
def create!
pipeline.stages.create!(stage).tap do |stage|
builds_attributes = builds.map do |attributes|
attributes.merge(stage_id: stage.id)
end
pipeline.builds.create!(builds_attributes).each do |build|
yield build if block_given?
end
end
end
private
def protected_ref?
strong_memoize(:protected_ref) do
project.protected_for?(pipeline.ref)
end
end
end
end
end
end
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
end end
def build_attributes(name) def build_attributes(name)
job = @jobs[name.to_sym] || {} job = @jobs.fetch(name.to_sym, {})
{ stage_idx: @stages.index(job[:stage]), { stage_idx: @stages.index(job[:stage]),
stage: job[:stage], stage: job[:stage],
...@@ -53,30 +53,24 @@ module Gitlab ...@@ -53,30 +53,24 @@ module Gitlab
}.compact } }.compact }
end end
def pipeline_stage_builds(stage, pipeline) def stage_builds_attributes(stage)
selected_jobs = @jobs.select do |_, job| @jobs.values
next unless job[:stage] == stage .select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
only_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:only, {}))
except_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:except, {}))
only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
end end
selected_jobs.map { |_, job| build_attributes(job[:name]) } def stages_attributes
end @stages.uniq.map do |stage|
seeds = stage_builds_attributes(stage).map do |attributes|
job = @jobs.fetch(attributes[:name].to_sym)
def stage_seeds(pipeline) attributes
seeds = @stages.uniq.map do |stage| .merge(only: job.fetch(:only, {}))
builds = pipeline_stage_builds(stage, pipeline) .merge(except: job.fetch(:except, {}))
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end end
seeds.compact { name: stage, index: @stages.index(stage), builds: seeds }
end
end end
def self.validation_message(content) def self.validation_message(content)
......
...@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:pipeline) do let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project, build(:ci_empty_pipeline, project: project, ref: 'master')
ref: 'master')
end end
let(:command) do let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new( Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, project: project, current_user: user)
current_user: user, seeds_block: nil)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
context 'when pipeline is ready to be saved' do
before do before do
pipeline.stages.build(name: 'test', project: project)
step.perform! step.perform!
end end
context 'when pipeline is ready to be saved' do
it 'saves a pipeline' do it 'saves a pipeline' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
end end
...@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
it 'creates stages' do it 'creates stages' do
expect(pipeline.reload.stages).to be_one expect(pipeline.reload.stages).to be_one
expect(pipeline.stages.first).to be_persisted
end end
end end
...@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
build(:ci_pipeline, project: project, ref: nil) build(:ci_pipeline, project: project, ref: nil)
end end
before do
step.perform!
end
it 'breaks the chain' do it 'breaks the chain' do
expect(step.break?).to be true expect(step.break?).to be true
end end
...@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
.to include /Failed to persist the pipeline/ .to include /Failed to persist the pipeline/
end end
end end
context 'when there is a seed block present' do
let(:seeds) { spy('pipeline seeds') }
let(:command) do
double('command', project: project,
current_user: user,
seeds_block: seeds)
end
it 'executes the block' do
expect(seeds).to have_received(:call).with(pipeline)
end
end
end end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Populate do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
ref: 'master')
end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
context 'when pipeline doesn not have seeds block' do
before do
step.perform!
end
it 'does not persist the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted
end
it 'populates pipeline with builds' do
expect(pipeline.builds).to be_one
expect(pipeline.builds.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
end
context 'when pipeline is empty' do
let(:config) do
{ rspec: {
script: 'ls',
only: ['something']
} }
end
let(:pipeline) do
build(:ci_pipeline, project: project, config: config)
end
before do
step.perform!
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'appends an error about missing stages' do
expect(pipeline.errors.to_a)
.to include 'No stages / jobs for this pipeline.'
end
end
context 'when pipeline has validation errors' do
let(:pipeline) do
build(:ci_pipeline, project: project, ref: nil)
end
before do
step.perform!
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'appends validation error' do
expect(pipeline.errors.to_a)
.to include 'Failed to build the pipeline!'
end
end
context 'when there is a seed blocks present' do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
seeds_block: seeds_block)
end
context 'when seeds block builds some resources' do
let(:seeds_block) do
->(pipeline) { pipeline.variables.build(key: 'VAR', value: '123') }
end
it 'populates pipeline with resources described in the seeds block' do
step.perform!
expect(pipeline).not_to be_persisted
expect(pipeline.variables).not_to be_empty
expect(pipeline.variables.first).not_to be_persisted
expect(pipeline.variables.first.key).to eq 'VAR'
expect(pipeline.variables.first.value).to eq '123'
end
end
context 'when seeds block tries to persist some resources' do
let(:seeds_block) do
->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
end
it 'raises exception' do
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
end
end
end
context 'when pipeline gets persisted during the process' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'raises error' do
expect { step.perform! }.to raise_error(described_class::PopulateError)
end
end
context 'when using only/except build policies' do
let(:config) do
{ rspec: { script: 'rspec', stage: 'test', only: ['master'] },
prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
end
let(:pipeline) do
build(:ci_pipeline, ref: 'master', config: config)
end
it 'populates pipeline according to used policies' do
step.perform!
expect(pipeline.stages.size).to eq 1
expect(pipeline.builds.size).to eq 1
expect(pipeline.builds.first.name).to eq 'rspec'
end
end
end
...@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do ...@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
end end
end end
context 'when pipeline has no stages / jobs' do
let(:config) do
{ rspec: {
script: 'ls',
only: ['something']
} }
end
let(:pipeline) do
build(:ci_pipeline, project: project, config: config)
end
it 'appends an error about missing stages' do
expect(pipeline.errors.to_a)
.to include 'No stages / jobs for this pipeline.'
end
it 'breaks the chain' do
expect(step.break?).to be true
end
end
context 'when pipeline contains configuration validation errors' do context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } } let(:config) { { rspec: {} } }
......
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:attributes) do
{ name: 'rspec',
ref: 'master',
commands: 'rspec' }
end
subject do
described_class.new(pipeline, attributes)
end
describe '#attributes' do
it 'returns hash attributes of a build' do
expect(subject.attributes).to be_a Hash
expect(subject.attributes)
.to include(:name, :project, :ref, :commands)
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes user to a build' do
subject.user = user
expect(subject.attributes).to include(user: user)
end
end
describe '#to_resource' do
it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build)
expect(subject.to_resource).to be_valid
end
it 'memoizes a resource object' do
build = subject.to_resource
expect(build.object_id).to eq subject.to_resource.object_id
end
it 'can not be persisted without explicit assignment' do
build = subject.to_resource
pipeline.save!
expect(build).not_to be_persisted
end
end
describe 'applying only/except policies' do
context 'when no branch policy is specified' do
let(:attributes) { { name: 'rspec' } }
it { is_expected.to be_included }
end
context 'when branch policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['deploy'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['deploy'] } } }
it { is_expected.to be_included }
end
end
context 'when branch regexp policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['/^deploy$/'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['/^deploy$/'] } } }
it { is_expected.to be_included }
end
end
context 'when branch policy matches' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: %w[deploy master] } } }
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: %w[deploy master] } } }
it { is_expected.not_to be_included }
end
end
context 'when keyword policy matches' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['branches'] } } }
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['branches'] } } }
it { is_expected.not_to be_included }
end
end
context 'when keyword policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['tags'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['tags'] } } }
it { is_expected.to be_included }
end
end
context 'when keywords and pipeline source policy matches' do
possibilities = [%w[pushes push],
%w[web web],
%w[triggers trigger],
%w[schedules schedule],
%w[api api],
%w[external external]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
end
context 'when keywords and pipeline source does not match' do
possibilities = [%w[pushes web],
%w[web push],
%w[triggers schedule],
%w[schedules external],
%w[api trigger],
%w[external api]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.to be_included }
end
end
end
end
context 'when repository path matches' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
end
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) do
{ name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
end
it { is_expected.not_to be_included }
end
end
context 'when repository path does not matches' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: ['branches@fork'] } }
end
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) do
{ name: 'rspec', except: { refs: ['branches@fork'] } }
end
it { is_expected.to be_included }
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Stage::Seed do describe Gitlab::Ci::Pipeline::Seed::Stage do
let(:pipeline) { create(:ci_empty_pipeline) } let(:pipeline) { create(:ci_empty_pipeline) }
let(:builds) do let(:attributes) do
[{ name: 'rspec' }, { name: 'spinach' }] { name: 'test',
index: 0,
builds: [{ name: 'rspec' },
{ name: 'spinach' },
{ name: 'deploy', only: { refs: ['feature'] } }] }
end end
subject do subject do
described_class.new(pipeline, 'test', builds) described_class.new(pipeline, attributes)
end end
describe '#size' do describe '#size' do
...@@ -17,20 +21,46 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -17,20 +21,46 @@ describe Gitlab::Ci::Stage::Seed do
end end
end end
describe '#stage' do describe '#attributes' do
it 'returns hash attributes of a stage' do it 'returns hash attributes of a stage' do
expect(subject.stage).to be_a Hash expect(subject.attributes).to be_a Hash
expect(subject.stage).to include(:name, :project) expect(subject.attributes).to include(:name, :project)
end end
end end
describe '#builds' do describe '#included?' do
it 'returns hash attributes of all builds' do context 'when it contains builds seeds' do
expect(subject.builds.size).to eq 2 let(:attributes) do
expect(subject.builds).to all(include(ref: 'master')) { name: 'test',
expect(subject.builds).to all(include(tag: false)) index: 0,
expect(subject.builds).to all(include(project: pipeline.project)) builds: [{ name: 'deploy', only: { refs: ['master'] } }] }
expect(subject.builds) end
it { is_expected.to be_included }
end
context 'when it does not contain build seeds' do
let(:attributes) do
{ name: 'test',
index: 0,
builds: [{ name: 'deploy', only: { refs: ['feature'] } }] }
end
it { is_expected.not_to be_included }
end
end
describe '#seeds' do
it 'returns build seeds' do
expect(subject.seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Build)
end
it 'returns build seeds including valid attributes' do
expect(subject.seeds.size).to eq 2
expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
expect(subject.seeds.map(&:attributes))
.to all(include(trigger_request: pipeline.trigger_requests.first)) .to all(include(trigger_request: pipeline.trigger_requests.first))
end end
...@@ -40,17 +70,27 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -40,17 +70,27 @@ describe Gitlab::Ci::Stage::Seed do
end end
it 'returns protected builds' do it 'returns protected builds' do
expect(subject.builds).to all(include(protected: true)) expect(subject.seeds.map(&:attributes)).to all(include(protected: true))
end end
end end
context 'when a ref is unprotected' do context 'when a ref is not protected' do
before do before do
allow_any_instance_of(Project).to receive(:protected_for?).and_return(false) allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
end end
it 'returns unprotected builds' do it 'returns unprotected builds' do
expect(subject.builds).to all(include(protected: false)) expect(subject.seeds.map(&:attributes)).to all(include(protected: false))
end
end
it 'filters seeds using only/except policies' do
expect(subject.seeds.map(&:attributes)).to satisfy do |seeds|
seeds.any? { |hash| hash.fetch(:name) == 'rspec' }
end
expect(subject.seeds.map(&:attributes)).not_to satisfy do |seeds|
seeds.any? { |hash| hash.fetch(:name) == 'deploy' }
end end
end end
end end
...@@ -61,13 +101,13 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -61,13 +101,13 @@ describe Gitlab::Ci::Stage::Seed do
it 'assignes relevant pipeline attributes' do it 'assignes relevant pipeline attributes' do
subject.user = user subject.user = user
expect(subject.builds).to all(include(user: user)) expect(subject.seeds.map(&:attributes)).to all(include(user: user))
end end
end end
describe '#create!' do describe '#to_resource' do
it 'creates all stages and builds' do it 'builds a valid stage object with all builds' do
subject.create! subject.to_resource.save!
expect(pipeline.reload.stages.count).to eq 1 expect(pipeline.reload.stages.count).to eq 1
expect(pipeline.reload.builds.count).to eq 2 expect(pipeline.reload.builds.count).to eq 2
...@@ -79,5 +119,15 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -79,5 +119,15 @@ describe Gitlab::Ci::Stage::Seed do
expect(pipeline.stages) expect(pipeline.stages)
.to all(satisfy { |stage| stage.project.present? }) .to all(satisfy { |stage| stage.project.present? })
end end
it 'can not be persisted without explicit pipeline assignment' do
stage = subject.to_resource
pipeline.save!
expect(stage).not_to be_persisted
expect(pipeline.reload.stages.count).to eq 0
expect(pipeline.reload.builds.count).to eq 0
end
end end
end end
This diff is collapsed.
...@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do ...@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#protected_ref?' do
it 'delegates method to project' do
expect(pipeline).not_to be_protected_ref
end
end
describe '#legacy_trigger' do
let(:trigger_request) { create(:ci_trigger_request) }
before do
pipeline.trigger_requests << trigger_request
end
it 'returns first trigger request' do
expect(pipeline.legacy_trigger).to eq trigger_request
end
end
describe '#auto_canceled?' do describe '#auto_canceled?' do
subject { pipeline.auto_canceled? } subject { pipeline.auto_canceled? }
...@@ -215,6 +233,139 @@ describe Ci::Pipeline, :mailer do ...@@ -215,6 +233,139 @@ describe Ci::Pipeline, :mailer do
end end
describe 'pipeline stages' do describe 'pipeline stages' do
describe '#stage_seeds' do
let(:pipeline) { build(:ci_pipeline, config: config) }
let(:config) { { rspec: { script: 'rake' } } }
it 'returns preseeded stage seeds object' do
expect(pipeline.stage_seeds)
.to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
expect(pipeline.stage_seeds.count).to eq 1
end
context 'when no refs policy is specified' do
let(:config) do
{ production: { stage: 'deploy', script: 'cap prod' },
rspec: { stage: 'test', script: 'rspec' },
spinach: { stage: 'test', script: 'spinach' } }
end
it 'correctly fabricates a stage seeds object' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 2
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.second.attributes[:name]).to eq 'deploy'
expect(seeds.dig(0, 0, :name)).to eq 'rspec'
expect(seeds.dig(0, 1, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
end
end
context 'when refs policy is specified' do
let(:pipeline) do
build(:ci_pipeline, ref: 'feature', tag: true, config: config)
end
let(:config) do
{ production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
end
it 'returns stage seeds only assigned to master to master' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end
context 'when source policy is specified' do
let(:pipeline) { build(:ci_pipeline, source: :schedule, config: config) }
let(:config) do
{ production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } }
end
it 'returns stage seeds only assigned to schedules' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end
context 'when kubernetes policy is specified' do
let(:config) do
{
spinach: { stage: 'test', script: 'spinach' },
production: {
stage: 'deploy',
script: 'cap',
only: { kubernetes: 'active' }
}
}
end
context 'when kubernetes is active' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
it 'returns seeds for kubernetes dependent job' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 2
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
end
end
context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) }
let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
context 'when kubernetes is not active' do
it 'does not return seeds for kubernetes dependent job' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end
end
end
describe '#seeds_size' do
context 'when refs policy is specified' do
let(:config) do
{ production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
end
let(:pipeline) do
build(:ci_pipeline, ref: 'feature', tag: true, config: config)
end
it 'returns real seeds size' do
expect(pipeline.seeds_size).to eq 1
end
end
end
describe 'legacy stages' do
before do before do
create(:commit_status, pipeline: pipeline, create(:commit_status, pipeline: pipeline,
stage: 'build', stage: 'build',
...@@ -241,25 +392,6 @@ describe Ci::Pipeline, :mailer do ...@@ -241,25 +392,6 @@ describe Ci::Pipeline, :mailer do
status: 'success') status: 'success')
end end
describe '#stage_seeds' do
let(:pipeline) do
build(:ci_pipeline, config: { rspec: { script: 'rake' } })
end
it 'returns preseeded stage seeds object' do
expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed)
expect(pipeline.stage_seeds.count).to eq 1
end
end
describe '#seeds_size' do
let(:pipeline) { build(:ci_pipeline_with_one_job) }
it 'returns number of jobs in stage seeds' do
expect(pipeline.seeds_size).to eq 1
end
end
describe '#legacy_stages' do describe '#legacy_stages' do
subject { pipeline.legacy_stages } subject { pipeline.legacy_stages }
...@@ -354,6 +486,7 @@ describe Ci::Pipeline, :mailer do ...@@ -354,6 +486,7 @@ describe Ci::Pipeline, :mailer do
end end
end end
end end
end
describe 'state machine' do describe 'state machine' do
let(:current) { Time.now.change(usec: 0) } let(:current) { Time.now.change(usec: 0) }
...@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do ...@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#has_stage_seeds?' do
context 'when pipeline has stage seeds' do
subject { build(:ci_pipeline_with_one_job) }
it { is_expected.to have_stage_seeds }
end
context 'when pipeline does not have stage seeds' do
subject { create(:ci_pipeline_without_jobs) }
it { is_expected.not_to have_stage_seeds }
end
end
describe '#has_warnings?' do describe '#has_warnings?' do
subject { pipeline.has_warnings? } subject { pipeline.has_warnings? }
......
...@@ -5,6 +5,7 @@ describe 'ci/lints/show' do ...@@ -5,6 +5,7 @@ describe 'ci/lints/show' do
describe 'XSS protection' do describe 'XSS protection' do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do before do
assign(:status, true) assign(:status, true)
assign(:builds, config_processor.builds) assign(:builds, config_processor.builds)
......
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