Commit 7b3aadca authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/gb/pipeline-processing-quotas' into 'master'

Introduce CI/CD pipeline processing quotas

Closes #3493

See merge request gitlab-org/gitlab-ee!2986
parents 5dcf9f18 f69c62e9
......@@ -73,6 +73,13 @@
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
class="js-pipeline-url-failure label label-danger"
:title="pipeline.failure_reason">
error
</span>
<a
v-if="pipeline.flags.auto_devops"
class="js-pipeline-url-autodevops label label-info autodevops-badge"
......
......@@ -6,7 +6,7 @@ class Groups::BillingsController < Groups::ApplicationController
def index
@top_most_group = @group.root_ancestor if @group.has_parent?
current_plan = (@top_most_group || @group).actual_plan
current_plan = (@top_most_group || @group).actual_plan_name
@plans_data = FetchSubscriptionPlansService.new(plan: current_plan).execute
end
end
......@@ -2,6 +2,8 @@ class Profiles::BillingsController < Profiles::ApplicationController
before_action :verify_namespace_plan_check_enabled
def index
@plans_data = FetchSubscriptionPlansService.new(plan: current_user.namespace.actual_plan).execute
@plans_data = FetchSubscriptionPlansService
.new(plan: current_user.namespace.actual_plan_name)
.execute
end
end
......@@ -5,6 +5,7 @@ module Ci
include Importable
include AfterCommitQueue
include Presentable
include Gitlab::OptimisticLocking
prepend ::EE::Ci::Pipeline
......@@ -71,6 +72,11 @@ module Ci
auto_devops_source: 2
}
enum failure_reason: {
unknown_failure: 0,
config_error: 1
}.merge(EE_FAILURE_REASONS)
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
......@@ -122,6 +128,12 @@ module Ci
pipeline.auto_canceled_by = nil
end
before_transition any => :failed do |pipeline, transition|
transition.args.first.try do |reason|
pipeline.failure_reason = reason
end
end
after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
......@@ -276,7 +288,7 @@ module Ci
end
def cancel_running
Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
retry_optimistic_lock(cancelable_statuses) do |cancelable|
cancelable.find_each do |job|
yield(job) if block_given?
job.cancel
......@@ -325,6 +337,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
def seeds_size
@seeds_size ||= stage_seeds.sum(&:size)
end
def has_kubernetes_active?
project.kubernetes_service&.active?
end
......@@ -416,7 +432,7 @@ module Ci
end
def update_status
Gitlab::OptimisticLocking.retry_lock(self) do
retry_optimistic_lock(self) do
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
......
......@@ -81,6 +81,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
......
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
prepend ::EE::Ci::PipelinePresenter
FAILURE_REASONS = {
config_error: 'CI/CD YAML configuration error!'
}.merge(EE_FAILURE_REASONS)
presents :pipeline
def failure_reason
return unless pipeline.failure_reason?
FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
pipeline.failure_reason
end
def status_title
if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
......
......@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
end
expose :details do
......@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity
end
expose :commit, using: CommitEntity
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
expose :failure_reason, if: -> (pipeline, _) { pipeline.failure_reason? } do |pipeline|
pipeline.present.failure_reason
end
expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline)
......@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
private
alias_method :pipeline, :object
......
......@@ -6,7 +6,9 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze
EE::Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Create,
EE::Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new(
......
- page_title "Billing"
- if @top_most_group
- top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan)
- top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan_name)
= render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
- else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group
- current_plan = subscription_plan_info(plans_data, namespace.actual_plan)
- current_plan = subscription_plan_info(plans_data, namespace.actual_plan_name)
- if current_plan
= render 'shared/billings/billing_plan_header', namespace: namespace, plan: current_plan
......
---
title: Add suport for CI/CD pipeline policy management
merge_request: 2986
author:
type: added
......@@ -45,6 +45,7 @@ module Gitlab
#{config.root}/ee/app/models/concerns
#{config.root}/ee/app/policies
#{config.root}/ee/app/serializers
#{config.root}/ee/app/presenters
#{config.root}/ee/app/services
#{config.root}/ee/app/workers
])
......
require './spec/support/sidekiq'
Plan.create!(name: EE::Namespace::FREE_PLAN,
title: EE::Namespace::FREE_PLAN.titleize)
EE::Namespace::EE_PLANS.each_key do |plan|
Plan.create!(name: plan, title: plan.titleize)
end
class AddPipelineQuotasToPlan < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :plans, :active_pipelines_limit, :integer
add_column :plans, :pipeline_size_limit, :integer
end
end
class AddFailureReasonToPipelines < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_pipelines, :failure_reason, :integer
end
end
class CreateMissingFreePlan < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
class Plan < ActiveRecord::Base
self.table_name = 'plans'
end
def up
Plan.create!(name: 'free', title: 'Free')
end
def down
Plan.find_by(name: 'free')&.destroy!
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170928100231) do
ActiveRecord::Schema.define(version: 20171002105019) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -403,6 +403,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do
t.integer "source"
t.integer "config_source"
t.boolean "protected"
t.integer "failure_reason"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
......@@ -1367,6 +1368,8 @@ ActiveRecord::Schema.define(version: 20170928100231) do
t.datetime_with_timezone "updated_at", null: false
t.string "name"
t.string "title"
t.integer "active_pipelines_limit"
t.integer "pipeline_size_limit"
end
add_index "plans", ["name"], name: "index_plans_on_name", using: :btree
......
module EE
module Ci
module Pipeline
EE_FAILURE_REASONS = {
activity_limit_exceeded: 20,
size_limit_exceeded: 21
}.freeze
def predefined_variables
result = super
result << { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
......
......@@ -78,8 +78,13 @@ module EE
# The main difference between the "plan" column and this method is that "plan"
# returns nil / "" when it has no plan. Having no plan means it's a "free" plan.
#
def actual_plan
plan&.name || FREE_PLAN
self.plan || Plan.find_by(name: FREE_PLAN)
end
def actual_plan_name
actual_plan&.name || FREE_PLAN
end
def actual_shared_runners_minutes_limit
......@@ -108,6 +113,16 @@ module EE
end
end
# TODO, CI/CD Quotas feature check
#
def max_active_pipelines
actual_plan&.active_pipelines_limit.to_i
end
def max_pipeline_size
actual_plan&.pipeline_size_limit.to_i
end
private
def validate_plan_name
......
module EE
module Ci
module PipelinePresenter
EE_FAILURE_REASONS = {
activity_limit_exceeded: 'Pipeline activity limit exceeded!',
size_limit_exceeded: 'Pipeline size limit exceeded!'
}.freeze
end
end
end
module EE
module Gitlab
module Ci
##
# Abstract base class for CI/CD Quotas
#
class Limit
def initialize(_context, _resource)
end
def enabled?
raise NotImplementedError
end
def exceeded?
raise NotImplementedError
end
def message
raise NotImplementedError
end
end
end
end
end
module EE
module Gitlab
module Ci
module Pipeline
module Chain
module Limit
class Activity < ::Gitlab::Ci::Pipeline::Chain::Base
include ::Gitlab::Ci::Pipeline::Chain::Helpers
include ::Gitlab::OptimisticLocking
def initialize(*)
super
@limit = Pipeline::Quota::Activity
.new(project.namespace, pipeline.project)
end
def perform!
return unless @limit.exceeded?
retry_optimistic_lock(@pipeline) do
@pipeline.drop!(:activity_limit_exceeded)
end
end
def break?
@limit.exceeded?
end
end
end
end
end
end
end
end
module EE
module Gitlab
module Ci
module Pipeline
module Chain
module Limit
class Size < ::Gitlab::Ci::Pipeline::Chain::Base
include ::Gitlab::Ci::Pipeline::Chain::Helpers
def initialize(*)
super
@limit = Pipeline::Quota::Size
.new(project.namespace, pipeline)
end
def perform!
return unless @limit.exceeded?
if @command.save_incompleted
@pipeline.drop!(:size_limit_exceeded)
end
error(@limit.message)
end
def break?
@limit.exceeded?
end
end
end
end
end
end
end
end
module EE
module Gitlab
module Ci
module Pipeline
module Quota
class Activity < Ci::Limit
include ActionView::Helpers::TextHelper
def initialize(namespace, project)
@namespace = namespace
@project = project
end
def enabled?
@namespace.max_active_pipelines > 0
end
def exceeded?
return false unless enabled?
excessive_pipelines_count > 0
end
def message
return unless exceeded?
'Active pipelines limit exceeded by ' \
"#{pluralize(excessive_pipelines_count, 'pipeline')}!"
end
private
def excessive_pipelines_count
@excessive ||= alive_pipelines_count - max_active_pipelines_count
end
def alive_pipelines_count
@project.pipelines.alive.count
end
def max_active_pipelines_count
@namespace.max_active_pipelines
end
end
end
end
end
end
end
module EE
module Gitlab
module Ci
module Pipeline
module Quota
class Size < Ci::Limit
include ActionView::Helpers::TextHelper
def initialize(namespace, pipeline)
@namespace = namespace
@pipeline = pipeline
end
def enabled?
@namespace.max_pipeline_size > 0
end
def exceeded?
return false unless enabled?
excessive_seeds_count > 0
end
def message
return unless exceeded?
'Pipeline size limit exceeded by ' \
"#{pluralize(excessive_seeds_count, 'job')}!"
end
private
def excessive_seeds_count
@excessive ||= @pipeline.seeds_size - @namespace.max_pipeline_size
end
end
end
end
end
end
end
......@@ -13,7 +13,7 @@ module Gitlab
end
if @command.save_incompleted && @pipeline.has_yaml_errors?
@pipeline.drop
@pipeline.drop!(:config_error)
end
return error(@pipeline.yaml_errors)
......
......@@ -3,7 +3,9 @@ module Gitlab
module Stage
class Seed
attr_reader :pipeline
delegate :project, to: :pipeline
delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
......
require 'spec_helper'
describe EE::Gitlab::Ci::Pipeline::Chain::Limit::Activity do
set(:namespace) { create(:namespace, plan: Namespace::GOLD_PLAN) }
set(:project) { create(:project, namespace: namespace) }
set(:user) { create(:user) }
let(:command) do
double('command', project: project, current_user: user)
end
let(:pipeline) do
create(:ci_pipeline, project: project)
end
let(:step) { described_class.new(pipeline, command) }
context 'when active pipelines limit is exceeded' do
before do
project.namespace.plan.update_column(:active_pipelines_limit, 1)
create(:ci_pipeline, project: project, status: 'pending')
create(:ci_pipeline, project: project, status: 'running')
step.perform!
end
it 'drops the pipeline' do
expect(pipeline.reload).to be_failed
end
it 'persists the pipeline' do
expect(pipeline).to be_persisted
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'sets a valid failure reason' do
expect(pipeline.activity_limit_exceeded?).to be true
end
end
context 'when pipeline size limit is not exceeded' do
before do
step.perform!
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'does not invalidate the pipeline' do
expect(pipeline.errors).to be_empty
end
end
end
require 'spec_helper'
describe EE::Gitlab::Ci::Pipeline::Chain::Limit::Size do
set(:namespace) { create(:namespace, plan: Namespace::GOLD_PLAN) }
set(:project) { create(:project, namespace: namespace) }
set(:user) { create(:user) }
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
ref: 'master')
end
let(:command) do
double('command', project: project,
current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
context 'when pipeline size limit is exceeded' do
before do
project.namespace.plan.update_column(:pipeline_size_limit, 1)
step.perform!
end
let(:pipeline) do
config = { rspec: { script: 'rspec' },
spinach: { script: 'spinach' } }
create(:ci_pipeline, project: project, config: config)
end
context 'when saving incomplete pipelines' do
let(:command) do
double('command', project: project,
current_user: user,
save_incompleted: true)
end
it 'drops the pipeline' do
expect(pipeline.reload).to be_failed
end
it 'persists the pipeline' do
expect(pipeline).to be_persisted
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'sets a valid failure reason' do
expect(pipeline.size_limit_exceeded?).to be true
end
it 'appends validation error' do
expect(pipeline.errors.to_a)
.to include 'Pipeline size limit exceeded by 1 job!'
end
end
context 'when not saving incomplete pipelines' do
let(:command) do
double('command', project: project,
current_user: user,
save_incompleted: false)
end
it 'does not drop the pipeline' do
expect(pipeline).not_to be_failed
end
it 'breaks the chain' do
expect(step.break?).to be true
end
end
end
context 'when pipeline size limit is not exceeded' do
before do
step.perform!
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'does not persist the pipeline' do
expect(pipeline).not_to be_persisted
end
end
end
require 'spec_helper'
describe EE::Gitlab::Ci::Pipeline::Quota::Activity do
set(:namespace) { create(:namespace, plan: EE::Namespace::GOLD_PLAN) }
set(:project) { create(:project, namespace: namespace) }
let(:limit) { described_class.new(namespace, project) }
shared_context 'pipeline activity limit exceeded' do
before do
create(:ci_pipeline, project: project, status: 'created')
create(:ci_pipeline, project: project, status: 'pending')
create(:ci_pipeline, project: project, status: 'running')
namespace.plan.update_column(:active_pipelines_limit, 1)
end
end
shared_context 'pipeline activity limit not exceeded' do
before do
namespace.plan.update_column(:active_pipelines_limit, 2)
end
end
describe '#enabled?' do
context 'when limit is enabled in plan' do
before do
namespace.plan.update_column(:active_pipelines_limit, 10)
end
it 'is enabled' do
expect(limit).to be_enabled
end
end
context 'when limit is not enabled' do
before do
namespace.plan.update_column(:active_pipelines_limit, 0)
end
it 'is not enabled' do
expect(limit).not_to be_enabled
end
end
end
describe '#exceeded?' do
context 'when limit is exceeded' do
include_context 'pipeline activity limit exceeded'
it 'is exceeded' do
expect(limit).to be_exceeded
end
end
context 'when limit is not exceeded' do
include_context 'pipeline activity limit not exceeded'
it 'is not exceeded' do
expect(limit).not_to be_exceeded
end
end
end
describe '#message' do
context 'when limit is exceeded' do
include_context 'pipeline activity limit exceeded'
it 'returns info about pipeline activity limit exceeded' do
expect(limit.message)
.to eq "Active pipelines limit exceeded by 2 pipelines!"
end
end
end
end
require 'spec_helper'
describe EE::Gitlab::Ci::Pipeline::Quota::Size do
set(:namespace) { create(:namespace, plan: EE::Namespace::GOLD_PLAN) }
set(:project) { create(:project, namespace: namespace) }
let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
let(:limit) { described_class.new(namespace, pipeline) }
shared_context 'pipeline size limit exceeded' do
let(:pipeline) do
config = { rspec: { script: 'rspec' },
spinach: { script: 'spinach' } }
build(:ci_pipeline, project: project, config: config)
end
before do
namespace.plan.update_column(:pipeline_size_limit, 1)
end
end
shared_context 'pipeline size limit not exceeded' do
let(:pipeline) { build(:ci_pipeline_with_one_job, project: project) }
before do
namespace.plan.update_column(:pipeline_size_limit, 2)
end
end
describe '#enabled?' do
context 'when limit is enabled in plan' do
before do
namespace.plan.update_column(:pipeline_size_limit, 10)
end
it 'is enabled' do
expect(limit).to be_enabled
end
end
context 'when limit is not enabled' do
before do
namespace.plan.update_column(:pipeline_size_limit, 0)
end
it 'is not enabled' do
expect(limit).not_to be_enabled
end
end
end
describe '#exceeded?' do
context 'when limit is exceeded' do
include_context 'pipeline size limit exceeded'
it 'is exceeded' do
expect(limit).to be_exceeded
end
end
context 'when limit is not exceeded' do
include_context 'pipeline size limit not exceeded'
it 'is not exceeded' do
expect(limit).not_to be_exceeded
end
end
end
describe '#message' do
context 'when limit is exceeded' do
include_context 'pipeline size limit exceeded'
it 'returns infor about pipeline size limit exceeded' do
expect(limit.message)
.to eq "Pipeline size limit exceeded by 1 job!"
end
end
end
end
require 'spec_helper'
describe Ci::Pipeline do
describe '.failure_reasons' do
it 'contains failure reasons about exceeded limits' do
expect(described_class.failure_reasons)
.to include 'activity_limit_exceeded', 'size_limit_exceeded'
end
end
end
......@@ -156,6 +156,86 @@ describe Namespace do
end
end
describe '#max_active_pipelines' do
context 'when there is no limit defined' do
it 'returns zero' do
expect(namespace.max_active_pipelines).to be_zero
end
end
context 'when free plan has limit defined' do
before do
Plan.find_by(name: Namespace::FREE_PLAN)
.update_column(:active_pipelines_limit, 40)
end
it 'returns a free plan limits' do
expect(namespace.max_active_pipelines).to be 40
end
end
context 'when associated plan has no limit defined' do
before do
namespace.plan = Namespace::GOLD_PLAN
end
it 'returns zero' do
expect(namespace.max_active_pipelines).to be_zero
end
end
context 'when limit is defined' do
before do
namespace.plan = Namespace::GOLD_PLAN
namespace.plan.update_column(:active_pipelines_limit, 10)
end
it 'returns a number of maximum active pipelines' do
expect(namespace.max_active_pipelines).to eq 10
end
end
end
describe '#max_pipeline_size' do
context 'when there are no limits defined' do
it 'returns zero' do
expect(namespace.max_pipeline_size).to be_zero
end
end
context 'when free plan has limit defined' do
before do
Plan.find_by(name: Namespace::FREE_PLAN)
.update_column(:pipeline_size_limit, 40)
end
it 'returns a free plan limits' do
expect(namespace.max_pipeline_size).to be 40
end
end
context 'when associated plan has no limits defined' do
before do
namespace.plan = Namespace::GOLD_PLAN
end
it 'returns zero' do
expect(namespace.max_pipeline_size).to be_zero
end
end
context 'when limit is defined' do
before do
namespace.plan = Namespace::GOLD_PLAN
namespace.plan.update_column(:pipeline_size_limit, 15)
end
it 'returns a number of maximum pipeline size' do
expect(namespace.max_pipeline_size).to eq 15
end
end
end
describe '#shared_runners_enabled?' do
subject { namespace.shared_runners_enabled? }
......@@ -294,4 +374,42 @@ describe Namespace do
expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end
end
describe '#actual_plan' do
context 'when namespace has a plan associated' do
before do
namespace.plan = Namespace::GOLD_PLAN
end
it 'returns an associated plan' do
expect(namespace.plan).not_to be_nil
expect(namespace.actual_plan.name).to eq 'gold'
end
end
context 'when namespace does not have plan associated' do
it 'returns a free plan object' do
expect(namespace.plan).to be_nil
expect(namespace.actual_plan.name).to eq 'free'
end
end
end
describe '#actual_plan_name' do
context 'when namespace has a plan associated' do
before do
namespace.plan = Namespace::GOLD_PLAN
end
it 'returns an associated plan name' do
expect(namespace.actual_plan_name).to eq 'gold'
end
end
context 'when namespace does not have plan associated' do
it 'returns a free plan name' do
expect(namespace.actual_plan_name).to eq 'free'
end
end
end
end
require 'spec_helper'
describe Ci::PipelinePresenter do
set(:project) { create(:project) }
set(:pipeline) { create(:ci_pipeline, project: project) }
subject(:presenter) do
described_class.new(pipeline)
end
context '#failure_reason' do
context 'when pipeline has failure reason' do
it 'represents a failure reason sentence' do
pipeline.failure_reason = :activity_limit_exceeded
expect(presenter.failure_reason)
.to eq 'Pipeline activity limit exceeded!'
end
end
context 'when pipeline does not have failure reason' do
it 'returns nil' do
expect(presenter.failure_reason).to be_nil
end
end
end
end
require 'spec_helper'
describe Ci::CreatePipelineService, '#execute' do
set(:namespace) { create(:namespace, plan: EE::Namespace::GOLD_PLAN) }
set(:project) { create(:project, :repository, namespace: namespace) }
set(:user) { create(:user) }
let(:service) do
params = { ref: 'master',
before: '00000000',
after: project.commit.id,
commits: [{ message: 'some commit' }] }
described_class.new(project, user, params)
end
before do
project.add_developer(user)
stub_ci_pipeline_to_return_yaml_file
end
describe 'CI/CD Quotas / Limits' do
context 'when there are not limits enabled' do
it 'enqueues a new pipeline' do
pipeline = create_pipeline!
expect(pipeline).to be_persisted
expect(pipeline).to be_pending
end
end
context 'when pipeline activity limit is exceeded' do
before do
namespace.plan.update_column(:active_pipelines_limit, 2)
create(:ci_pipeline, project: project, status: 'pending')
create(:ci_pipeline, project: project, status: 'running')
end
it 'drops the pipeline and does not process jobs' do
pipeline = create_pipeline!
expect(pipeline).to be_persisted
expect(pipeline).to be_failed
expect(pipeline.statuses).not_to be_empty
expect(pipeline.statuses).to all(be_created)
expect(pipeline.activity_limit_exceeded?).to be true
end
end
context 'when pipeline size limit is exceeded' do
before do
namespace.plan.update_column(:pipeline_size_limit, 2)
end
it 'drops pipeline without creating jobs' do
pipeline = create_pipeline!
expect(pipeline).to be_persisted
expect(pipeline).to be_failed
expect(pipeline.seeds_size).to be > 2
expect(pipeline.statuses).to be_empty
expect(pipeline.size_limit_exceeded?).to be true
end
end
end
def create_pipeline!
service.execute(:push)
end
end
......@@ -47,6 +47,7 @@ FactoryGirl.define do
trait :invalid do
config(rspec: nil)
failure_reason :config_error
end
trait :blocked do
......
......@@ -162,6 +162,16 @@ describe 'Pipelines', :js do
expect(page).to have_selector(
%Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end
it 'contains badge that indicates failure reason' do
expect(page).to have_content 'error'
end
it 'contains badge with tooltip which contains failure reason' do
expect(pipeline.failure_reason?).to eq true
expect(page).to have_selector(
%Q{span[data-original-title="#{pipeline.present.failure_reason}"]})
end
end
context 'with manual actions' do
......
......@@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => {
component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
).toEqual('Auto DevOps');
});
it('should render error badge when pipeline has a failure reason set', () => {
const component = new PipelineUrlComponent({
propsData: {
pipeline: {
id: 1,
path: 'foo',
flags: {
failure_reason: true,
},
failure_reason: 'some reason',
},
autoDevopsHelpPath: 'foo',
},
}).$mount();
expect(component.$el.querySelector('.js-pipeline-url-failure').textContent).toContain('error');
expect(component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title')).toContain('some reason');
});
});
......@@ -55,6 +55,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
it 'fails the pipeline' do
expect(pipeline.reload).to be_failed
end
it 'sets a config error failure reason' do
expect(pipeline.reload.config_error?).to eq true
end
end
context 'when saving incomplete pipeline is not allowed' do
......
......@@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do
described_class.new(pipeline, 'test', builds)
end
describe '#size' do
it 'returns a number of jobs in the stage' do
expect(subject.size).to eq 2
end
end
describe '#stage' do
it 'returns hash attributes of a stage' do
expect(subject.stage).to be_a Hash
......
......@@ -228,6 +228,7 @@ Ci::Pipeline:
- auto_canceled_by_id
- pipeline_schedule_id
- config_source
- failure_reason
- protected
Ci::Stage:
- id
......
......@@ -242,7 +242,7 @@ describe Ci::Pipeline, :mailer do
describe '#stage_seeds' do
let(:pipeline) do
create(:ci_pipeline, config: { rspec: { script: 'rake' } })
build(:ci_pipeline, config: { rspec: { script: 'rake' } })
end
it 'returns preseeded stage seeds object' do
......@@ -251,6 +251,14 @@ describe Ci::Pipeline, :mailer do
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
subject { pipeline.legacy_stages }
......
......@@ -231,6 +231,18 @@ describe HasStatus do
end
end
describe '.alive' do
subject { CommitStatus.alive }
%i[running pending created].each do |status|
it_behaves_like 'containing the job', status
end
%i[failed success].each do |status|
it_behaves_like 'not containing the job', status
end
end
describe '.created_or_pending' do
subject { CommitStatus.created_or_pending }
......
......@@ -51,4 +51,21 @@ describe Ci::PipelinePresenter do
end
end
end
context '#failure_reason' do
context 'when pipeline has failure reason' do
it 'represents a failure reason sentence' do
pipeline.failure_reason = :config_error
expect(presenter.failure_reason)
.to eq 'CI/CD YAML configuration error!'
end
end
context 'when pipeline does not have failure reason' do
it 'returns nil' do
expect(presenter.failure_reason).to be_nil
end
end
end
end
......@@ -108,5 +108,18 @@ describe PipelineEntity do
expect(subject[:ref][:path]).to be_nil
end
end
context 'when pipeline has a failure reason set' do
let(:pipeline) { create(:ci_empty_pipeline) }
before do
pipeline.drop!(:config_error)
end
it 'has a correct failure reason' do
expect(subject[:failure_reason])
.to eq 'CI/CD YAML configuration error!'
end
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