Commit e6f3a65a authored by Marius Bobin's avatar Marius Bobin

Adapt Limitable for system-wide features

 With these changes we can use the `Limitable`
concern for enforcing system-wide features, like instance level CI/CD
variables.
parent a6892cd1
......@@ -2,6 +2,7 @@
module Limitable
extend ActiveSupport::Concern
GLOBAL_SCOPE = :limitable_global_scope
included do
class_attribute :limit_scope
......@@ -14,14 +15,34 @@ module Limitable
private
def validate_plan_limit_not_exceeded
if GLOBAL_SCOPE == limit_scope
validate_global_plan_limit_not_exceeded
else
validate_scoped_plan_limit_not_exceeded
end
end
def validate_scoped_plan_limit_not_exceeded
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
relation = self.class.where(limit_scope => scope_relation)
limits = scope_relation.actual_limits
if scope_relation.actual_limits.exceeded?(limit_name, relation)
errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
{ name: limit_name.humanize(capitalize: false), count: scope_relation.actual_limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
end
check_plan_limit_not_exceeded(limits, relation)
end
def validate_global_plan_limit_not_exceeded
relation = self.class.all
limits = Plan.default.actual_limits
check_plan_limit_not_exceeded(limits, relation)
end
def check_plan_limit_not_exceeded(limits, relation)
return unless limits.exceeded?(limit_name, relation)
errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
{ name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
end
end
---
title: Adapt Limitable for system-wide features
merge_request: 32574
author:
type: added
......@@ -115,6 +115,20 @@ it_behaves_like 'includes Limitable concern' do
end
```
### Testing instance-wide limits
Instance-wide features always use `default` Plan, as instance-wide features
do not have license assigned.
```ruby
class InstanceVariable
include Limitable
self.limit_name = 'instance_variables' # Optional as InstanceVariable corresponds with instance_variables
self.limit_scope = Limitable::GLOBAL_SCOPE
end
```
### Subscription Plans
Self-managed:
......@@ -123,9 +137,10 @@ Self-managed:
GitLab.com:
- `free` - Everyone
- `bronze`- Namespaces with a Bronze subscription
- `silver` - Namespaces with a Silver subscription
- `gold` - Namespaces with a Gold subscription
- `default` - Any system-wide feature
- `free` - Namespaces and projects with a Free subscription
- `bronze`- Namespaces and projects with a Bronze subscription
- `silver` - Namespaces and projects with a Silver subscription
- `gold` - Namespaces and projects with a Gold subscription
NOTE: **Note:** The test environment doesn't have any plans.
......@@ -153,7 +153,7 @@ module EE
subscription = find_or_create_subscription
subscription&.hosted_plan
end
end || super
end || fallback_plan
end
def closest_gitlab_subscription
......@@ -325,6 +325,14 @@ module EE
private
def fallback_plan
if ::Gitlab.com?
::Plan.free
else
::Plan.default
end
end
def validate_shared_runner_minutes_support
return if shared_runner_minutes_supported?
......
......@@ -38,16 +38,6 @@ module EE
EE_DEFAULT_PLANS
end
override :default
def default
# GitLab.com default plan is `free`
if ::Gitlab.com?
free
else
super
end
end
# This always returns an object if running on GitLab.com
def free
return unless ::Gitlab.com?
......
# frozen_string_literal: true
require 'spec_helper'
describe Limitable do
let(:minimal_test_class) do
Class.new do
include ActiveModel::Model
def self.name
'TestClass'
end
include Limitable
end
end
before do
stub_const("MinimalTestClass", minimal_test_class)
end
it { expect(MinimalTestClass.limit_name).to eq('test_classes') }
context 'with scoped limit' do
before do
MinimalTestClass.limit_scope = :project
end
it { expect(MinimalTestClass.limit_scope).to eq(:project) }
it 'triggers scoped validations' do
instance = MinimalTestClass.new
expect(instance).to receive(:validate_scoped_plan_limit_not_exceeded)
instance.valid?(:create)
end
end
context 'with global limit' do
before do
MinimalTestClass.limit_scope = Limitable::GLOBAL_SCOPE
end
it { expect(MinimalTestClass.limit_scope).to eq(Limitable::GLOBAL_SCOPE) }
it 'triggers scoped validations' do
instance = MinimalTestClass.new
expect(instance).to receive(:validate_global_plan_limit_not_exceeded)
instance.valid?(:create)
end
end
end
......@@ -26,7 +26,7 @@ RSpec.shared_examples 'includes Limitable concern' do
subject.dup.save
end
it 'cannot create new models exceding the plan limits' do
it 'cannot create new models exceeding the plan limits' do
expect { subject.save }.not_to change { described_class.count }
expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded")
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