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 @@ ...@@ -73,6 +73,13 @@
:title="pipeline.yaml_errors"> :title="pipeline.yaml_errors">
yaml invalid yaml invalid
</span> </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 <a
v-if="pipeline.flags.auto_devops" v-if="pipeline.flags.auto_devops"
class="js-pipeline-url-autodevops label label-info autodevops-badge" class="js-pipeline-url-autodevops label label-info autodevops-badge"
......
...@@ -6,7 +6,7 @@ class Groups::BillingsController < Groups::ApplicationController ...@@ -6,7 +6,7 @@ class Groups::BillingsController < Groups::ApplicationController
def index def index
@top_most_group = @group.root_ancestor if @group.has_parent? @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 @plans_data = FetchSubscriptionPlansService.new(plan: current_plan).execute
end end
end end
...@@ -2,6 +2,8 @@ class Profiles::BillingsController < Profiles::ApplicationController ...@@ -2,6 +2,8 @@ class Profiles::BillingsController < Profiles::ApplicationController
before_action :verify_namespace_plan_check_enabled before_action :verify_namespace_plan_check_enabled
def index 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
end end
...@@ -5,6 +5,7 @@ module Ci ...@@ -5,6 +5,7 @@ module Ci
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Gitlab::OptimisticLocking
prepend ::EE::Ci::Pipeline prepend ::EE::Ci::Pipeline
...@@ -71,6 +72,11 @@ module Ci ...@@ -71,6 +72,11 @@ module Ci
auto_devops_source: 2 auto_devops_source: 2
} }
enum failure_reason: {
unknown_failure: 0,
config_error: 1
}.merge(EE_FAILURE_REASONS)
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
...@@ -122,6 +128,12 @@ module Ci ...@@ -122,6 +128,12 @@ module Ci
pipeline.auto_canceled_by = nil pipeline.auto_canceled_by = nil
end 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| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
...@@ -276,7 +288,7 @@ module Ci ...@@ -276,7 +288,7 @@ module Ci
end end
def cancel_running def cancel_running
Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable| retry_optimistic_lock(cancelable_statuses) do |cancelable|
cancelable.find_each do |job| cancelable.find_each do |job|
yield(job) if block_given? yield(job) if block_given?
job.cancel job.cancel
...@@ -325,6 +337,10 @@ module Ci ...@@ -325,6 +337,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self) @stage_seeds ||= config_processor.stage_seeds(self)
end end
def seeds_size
@seeds_size ||= stage_seeds.sum(&:size)
end
def has_kubernetes_active? def has_kubernetes_active?
project.kubernetes_service&.active? project.kubernetes_service&.active?
end end
...@@ -416,7 +432,7 @@ module Ci ...@@ -416,7 +432,7 @@ module Ci
end end
def update_status def update_status
Gitlab::OptimisticLocking.retry_lock(self) do retry_optimistic_lock(self) do
case latest_builds_status case latest_builds_status
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
......
...@@ -81,6 +81,7 @@ module HasStatus ...@@ -81,6 +81,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') } scope :manual, -> { where(status: 'manual') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
......
module Ci module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated 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 presents :pipeline
def failure_reason
return unless pipeline.failure_reason?
FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
pipeline.failure_reason
end
def status_title def status_title
if auto_canceled? if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
......
...@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity ...@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity
expose :has_yaml_errors?, as: :yaml_errors expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
end end
expose :details do expose :details do
...@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity ...@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity
end end
expose :commit, using: CommitEntity 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| expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline) retry_project_pipeline_path(pipeline.project, pipeline)
...@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity ...@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline) cancel_project_pipeline_path(pipeline.project, pipeline)
end end
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
private private
alias_method :pipeline, :object alias_method :pipeline, :object
......
...@@ -6,7 +6,9 @@ module Ci ...@@ -6,7 +6,9 @@ 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::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) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
......
- page_title "Billing" - page_title "Billing"
- if @top_most_group - 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 = render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
- else - else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group = 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 - if current_plan
= render 'shared/billings/billing_plan_header', namespace: namespace, plan: 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 ...@@ -45,6 +45,7 @@ module Gitlab
#{config.root}/ee/app/models/concerns #{config.root}/ee/app/models/concerns
#{config.root}/ee/app/policies #{config.root}/ee/app/policies
#{config.root}/ee/app/serializers #{config.root}/ee/app/serializers
#{config.root}/ee/app/presenters
#{config.root}/ee/app/services #{config.root}/ee/app/services
#{config.root}/ee/app/workers #{config.root}/ee/app/workers
]) ])
......
require './spec/support/sidekiq' 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| EE::Namespace::EE_PLANS.each_key do |plan|
Plan.create!(name: plan, title: plan.titleize) Plan.create!(name: plan, title: plan.titleize)
end 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -403,6 +403,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do ...@@ -403,6 +403,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do
t.integer "source" t.integer "source"
t.integer "config_source" t.integer "config_source"
t.boolean "protected" t.boolean "protected"
t.integer "failure_reason"
end end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree 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 ...@@ -1367,6 +1368,8 @@ ActiveRecord::Schema.define(version: 20170928100231) do
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
t.string "name" t.string "name"
t.string "title" t.string "title"
t.integer "active_pipelines_limit"
t.integer "pipeline_size_limit"
end end
add_index "plans", ["name"], name: "index_plans_on_name", using: :btree add_index "plans", ["name"], name: "index_plans_on_name", using: :btree
......
module EE module EE
module Ci module Ci
module Pipeline module Pipeline
EE_FAILURE_REASONS = {
activity_limit_exceeded: 20,
size_limit_exceeded: 21
}.freeze
def predefined_variables def predefined_variables
result = super result = super
result << { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true } result << { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
......
...@@ -78,8 +78,13 @@ module EE ...@@ -78,8 +78,13 @@ module EE
# The main difference between the "plan" column and this method is that "plan" # 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. # returns nil / "" when it has no plan. Having no plan means it's a "free" plan.
#
def actual_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 end
def actual_shared_runners_minutes_limit def actual_shared_runners_minutes_limit
...@@ -108,6 +113,16 @@ module EE ...@@ -108,6 +113,16 @@ module EE
end end
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 private
def validate_plan_name 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 ...@@ -13,7 +13,7 @@ module Gitlab
end end
if @command.save_incompleted && @pipeline.has_yaml_errors? if @command.save_incompleted && @pipeline.has_yaml_errors?
@pipeline.drop @pipeline.drop!(:config_error)
end end
return error(@pipeline.yaml_errors) return error(@pipeline.yaml_errors)
......
...@@ -3,7 +3,9 @@ module Gitlab ...@@ -3,7 +3,9 @@ module Gitlab
module Stage module Stage
class Seed class Seed
attr_reader :pipeline attr_reader :pipeline
delegate :project, to: :pipeline delegate :project, to: :pipeline
delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs) def initialize(pipeline, stage, jobs)
@pipeline = pipeline @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 ...@@ -156,6 +156,86 @@ describe Namespace do
end end
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 describe '#shared_runners_enabled?' do
subject { namespace.shared_runners_enabled? } subject { namespace.shared_runners_enabled? }
...@@ -294,4 +374,42 @@ describe Namespace do ...@@ -294,4 +374,42 @@ describe Namespace do
expect(very_deep_nested_group.root_ancestor).to eq(root_group) expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end end
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 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 ...@@ -47,6 +47,7 @@ FactoryGirl.define do
trait :invalid do trait :invalid do
config(rspec: nil) config(rspec: nil)
failure_reason :config_error
end end
trait :blocked do trait :blocked do
......
...@@ -162,6 +162,16 @@ describe 'Pipelines', :js do ...@@ -162,6 +162,16 @@ describe 'Pipelines', :js do
expect(page).to have_selector( expect(page).to have_selector(
%Q{span[data-original-title="#{pipeline.yaml_errors}"]}) %Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end 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 end
context 'with manual actions' do context 'with manual actions' do
......
...@@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => { ...@@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => {
component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(), component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
).toEqual('Auto DevOps'); ).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 ...@@ -55,6 +55,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
it 'fails the pipeline' do it 'fails the pipeline' do
expect(pipeline.reload).to be_failed expect(pipeline.reload).to be_failed
end end
it 'sets a config error failure reason' do
expect(pipeline.reload.config_error?).to eq true
end
end end
context 'when saving incomplete pipeline is not allowed' do context 'when saving incomplete pipeline is not allowed' do
......
...@@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do
described_class.new(pipeline, 'test', builds) described_class.new(pipeline, 'test', builds)
end 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 describe '#stage' 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.stage).to be_a Hash
......
...@@ -228,6 +228,7 @@ Ci::Pipeline: ...@@ -228,6 +228,7 @@ Ci::Pipeline:
- auto_canceled_by_id - auto_canceled_by_id
- pipeline_schedule_id - pipeline_schedule_id
- config_source - config_source
- failure_reason
- protected - protected
Ci::Stage: Ci::Stage:
- id - id
......
...@@ -242,7 +242,7 @@ describe Ci::Pipeline, :mailer do ...@@ -242,7 +242,7 @@ describe Ci::Pipeline, :mailer do
describe '#stage_seeds' do describe '#stage_seeds' do
let(:pipeline) do let(:pipeline) do
create(:ci_pipeline, config: { rspec: { script: 'rake' } }) build(:ci_pipeline, config: { rspec: { script: 'rake' } })
end end
it 'returns preseeded stage seeds object' do it 'returns preseeded stage seeds object' do
...@@ -251,6 +251,14 @@ describe Ci::Pipeline, :mailer do ...@@ -251,6 +251,14 @@ describe Ci::Pipeline, :mailer do
end end
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 }
......
...@@ -231,6 +231,18 @@ describe HasStatus do ...@@ -231,6 +231,18 @@ describe HasStatus do
end end
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 describe '.created_or_pending' do
subject { CommitStatus.created_or_pending } subject { CommitStatus.created_or_pending }
......
...@@ -51,4 +51,21 @@ describe Ci::PipelinePresenter do ...@@ -51,4 +51,21 @@ describe Ci::PipelinePresenter do
end end
end 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 end
...@@ -108,5 +108,18 @@ describe PipelineEntity do ...@@ -108,5 +108,18 @@ describe PipelineEntity do
expect(subject[:ref][:path]).to be_nil expect(subject[:ref][:path]).to be_nil
end end
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
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