Commit 72360817 authored by Douwe Maan's avatar Douwe Maan

Merge branch '2392-activate-gitlab-com-pricing-plans' into 'master'

Add migrations to enhance the association between Namespaces and GL.com plans

Closes #2391

See merge request !2609
parents 9af04595 39ca951a
...@@ -89,7 +89,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -89,7 +89,7 @@ class Admin::GroupsController < Admin::ApplicationController
[ [
:repository_size_limit, :repository_size_limit,
:shared_runners_minutes_limit, :shared_runners_minutes_limit,
:plan :plan_id
] ]
end end
end end
...@@ -213,7 +213,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -213,7 +213,7 @@ class Admin::UsersController < Admin::ApplicationController
def user_params_ee def user_params_ee
[ [
:note, :note,
namespace_attributes: [:id, :shared_runners_minutes_limit, :plan] namespace_attributes: [:id, :shared_runners_minutes_limit, :plan_id]
] ]
end end
......
class Plan < ActiveRecord::Base
has_many :namespaces
end
.form-group .form-group
= f.label :plan, class: 'control-label' = f.label :plan, class: 'control-label'
.col-sm-10 .col-sm-10
= f.select :plan, options_for_select(Namespace::EE_PLANS.keys.map { |plan| [plan.titleize, plan] }, f.object.plan), = f.select :plan_id, Plan.pluck(:title, :id),
{ include_blank: 'No plan' }, { include_blank: 'No plan' },
class: 'form-control' class: 'form-control'
- namespace = local_assigns[:namespace] - namespace = local_assigns[:namespace]
- if current_application_settings.should_check_namespace_plan? && namespace && namespace.plan.present? - if current_application_settings.should_check_namespace_plan? && namespace && namespace.plan.present?
%span.plan-badge.has-tooltip{ data: { plan: namespace.plan }, title: "#{namespace.plan.titleize} Plan" } %span.plan-badge.has-tooltip{ data: { plan: namespace.plan.name }, title: "#{namespace.plan.title} Plan" }
= custom_icon('icon_premium') = custom_icon('icon_premium')
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
%li %li
%span.light Plan: %span.light Plan:
- if namespace.plan.present? - if namespace.plan.present?
%strong.plan-badge.inline{ data: { plan: namespace.plan } } %strong.plan-badge.inline{ data: { plan: namespace.plan.name } }
= custom_icon('icon_premium') = custom_icon('icon_premium')
= namespace.plan.titleize = namespace.plan.title
- else - else
%strong.plan-badge.inline %strong.plan-badge.inline
= custom_icon('icon_premium') = custom_icon('icon_premium')
......
require './spec/support/sidekiq'
EE::Namespace::EE_PLANS.each_key do |plan|
Plan.create!(name: plan, title: plan.titleize)
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreatePlans < ActiveRecord::Migration
DOWNTIME = false
class Plan < ActiveRecord::Base
self.table_name = 'plans'
end
def up
create_table :plans do |t|
t.timestamps_with_timezone null: false
t.string :name, index: true
t.string :title
end
%w[early_adopter bronze silver gold].each do |plan|
Plan.create!(name: plan, title: plan.titleize)
end
end
def down
drop_table :plans
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPlanIdToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
end
class Plan < ActiveRecord::Base
self.table_name = 'plans'
end
def up
add_reference :namespaces, :plan
add_concurrent_foreign_key :namespaces, :plans, column: :plan_id, on_delete: :nullify
add_concurrent_index :namespaces, :plan_id
Plan.all.each do |plan|
Namespace.where(plan: plan.name).update_all(plan_id: plan.id)
end
end
def down
remove_foreign_key :namespaces, column: :plan_id
remove_concurrent_index :namespaces, :plan_id
remove_reference :namespaces, :plan
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemovePlanFromNamespaces < ActiveRecord::Migration
DOWNTIME = false
def change
remove_column :namespaces, :plan, :string
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: 20170807160457) do ActiveRecord::Schema.define(version: 20170808163512) 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"
...@@ -1164,7 +1164,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do ...@@ -1164,7 +1164,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.boolean "require_two_factor_authentication", default: false, null: false t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false t.integer "two_factor_grace_period", default: 48, null: false
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.string "plan" t.integer "plan_id"
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -1177,6 +1177,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do ...@@ -1177,6 +1177,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["plan_id"], name: "index_namespaces_on_plan_id", using: :btree
add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
...@@ -1329,6 +1330,15 @@ ActiveRecord::Schema.define(version: 20170807160457) do ...@@ -1329,6 +1330,15 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "plans", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "title"
end
add_index "plans", ["name"], name: "index_plans_on_name", using: :btree
create_table "project_authorizations", id: false, force: :cascade do |t| create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "project_id" t.integer "project_id"
...@@ -2035,6 +2045,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do ...@@ -2035,6 +2045,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
add_foreign_key "namespaces", "plans", name: "fk_fdd12e5b80", on_delete: :nullify
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
......
...@@ -21,14 +21,20 @@ module EE ...@@ -21,14 +21,20 @@ module EE
}.freeze }.freeze
prepended do prepended do
include IgnorableColumn
ignore_column :plan
belongs_to :plan
has_one :namespace_statistics has_one :namespace_statistics
scope :with_plan, -> { where.not(plan: [nil, '']) } scope :with_plan, -> { where.not(plan_id: nil) }
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :namespace_statistics, allow_nil: true to: :namespace_statistics, allow_nil: true
validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true validate :validate_plan_name
end end
def root_ancestor def root_ancestor
...@@ -68,7 +74,7 @@ module EE ...@@ -68,7 +74,7 @@ module EE
def feature_available_in_plan?(feature) def feature_available_in_plan?(feature)
@features_available_in_plan ||= Hash.new do |h, feature| @features_available_in_plan ||= Hash.new do |h, feature|
h[feature] = plans.any? { |plan| License.plan_includes_feature?(EE_PLANS[plan], feature) } h[feature] = plans.any? { |plan| License.plan_includes_feature?(EE_PLANS[plan&.name], feature) }
end end
@features_available_in_plan[feature] @features_available_in_plan[feature]
...@@ -77,7 +83,7 @@ module EE ...@@ -77,7 +83,7 @@ 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.presence || FREE_PLAN plan&.name || FREE_PLAN
end end
def actual_shared_runners_minutes_limit def actual_shared_runners_minutes_limit
...@@ -95,8 +101,25 @@ module EE ...@@ -95,8 +101,25 @@ module EE
shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit
end end
# These helper methods are required to not break the Namespace API.
def plan=(plan_name)
if plan_name.is_a?(String)
@plan_name = plan_name
super(Plan.find_by(name: @plan_name))
else
super
end
end
private private
def validate_plan_name
if @plan_name.present? && EE_PLANS.keys.exclude?(@plan_name)
errors.add(:plan, 'is not included in the list')
end
end
def load_feature_available(feature) def load_feature_available(feature)
globally_available = License.feature_available?(feature) globally_available = License.feature_available?(feature)
...@@ -110,7 +133,7 @@ module EE ...@@ -110,7 +133,7 @@ module EE
def plans def plans
@ancestors_plans ||= @ancestors_plans ||=
if parent_id if parent_id
ancestors.with_plan.reorder(nil).pluck('DISTINCT plan') + [plan] Plan.where(id: ancestors.with_plan.reorder(nil).select('plan_id')) + [plan]
else else
[plan] [plan]
end end
......
...@@ -616,7 +616,9 @@ module API ...@@ -616,7 +616,9 @@ module API
# EE-only # EE-only
expose :shared_runners_minutes_limit, if: lambda { |_, options| options[:current_user]&.admin? } expose :shared_runners_minutes_limit, if: lambda { |_, options| options[:current_user]&.admin? }
expose :plan, if: -> (namespace, opts) { Ability.allowed?(opts[:current_user], :admin_namespace, namespace) } expose :plan, if: -> (namespace, opts) { Ability.allowed?(opts[:current_user], :admin_namespace, namespace) } do |namespace, _|
namespace.plan&.name
end
end end
class MemberAccess < Grape::Entity class MemberAccess < Grape::Entity
......
...@@ -4,11 +4,11 @@ describe Namespace do ...@@ -4,11 +4,11 @@ describe Namespace do
let!(:namespace) { create(:namespace) } let!(:namespace) { create(:namespace) }
it { is_expected.to have_one(:namespace_statistics) } it { is_expected.to have_one(:namespace_statistics) }
it { is_expected.to belong_to(:plan) }
it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_seconds).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) }
it { is_expected.to validate_inclusion_of(:plan).in_array(Namespace::EE_PLANS.keys).allow_blank }
context 'scopes' do context 'scopes' do
describe '.with_plan' do describe '.with_plan' do
...@@ -42,6 +42,29 @@ describe Namespace do ...@@ -42,6 +42,29 @@ describe Namespace do
end end
end end
describe 'custom validations' do
describe '#validate_plan_name' do
let(:group) { build(:group) }
context 'with a valid plan name' do
it 'is valid' do
group.plan = Namespace::BRONZE_PLAN
expect(group).to be_valid
end
end
context 'with an invalid plan name' do
it 'is invalid' do
group.plan = 'unknown'
expect(group).not_to be_valid
expect(group.errors[:plan]).to include('is not included in the list')
end
end
end
end
describe '#move_dir' do describe '#move_dir' do
context 'when running on a primary node' do context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) } let!(:geo_node) { create(:geo_node, :primary, :current) }
......
...@@ -115,7 +115,7 @@ describe Project do ...@@ -115,7 +115,7 @@ describe Project do
context 'allowed by Plan License AND Global License' do context 'allowed by Plan License AND Global License' do
let(:allowed_on_global_license) { true } let(:allowed_on_global_license) { true }
let(:plan_license) { Namespace::GOLD_PLAN } let(:plan_license) { Plan.find_by(name: 'gold') }
it 'returns true' do it 'returns true' do
is_expected.to eq(true) is_expected.to eq(true)
...@@ -124,7 +124,7 @@ describe Project do ...@@ -124,7 +124,7 @@ describe Project do
context 'not allowed by Plan License but project and namespace are public' do context 'not allowed by Plan License but project and namespace are public' do
let(:allowed_on_global_license) { true } let(:allowed_on_global_license) { true }
let(:plan_license) { Namespace::BRONZE_PLAN } let(:plan_license) { Plan.find_by(name: 'bronze') }
it 'returns true' do it 'returns true' do
allow(namespace).to receive(:public?) { true } allow(namespace).to receive(:public?) { true }
...@@ -137,7 +137,7 @@ describe Project do ...@@ -137,7 +137,7 @@ describe Project do
unless License.plan_includes_feature?(License::STARTER_PLAN, feature_sym) unless License.plan_includes_feature?(License::STARTER_PLAN, feature_sym)
context 'not allowed by Plan License' do context 'not allowed by Plan License' do
let(:allowed_on_global_license) { true } let(:allowed_on_global_license) { true }
let(:plan_license) { Namespace::BRONZE_PLAN } let(:plan_license) { Plan.find_by(name: 'bronze') }
it 'returns false' do it 'returns false' do
is_expected.to eq(false) is_expected.to eq(false)
...@@ -147,7 +147,7 @@ describe Project do ...@@ -147,7 +147,7 @@ describe Project do
context 'not allowed by Global License' do context 'not allowed by Global License' do
let(:allowed_on_global_license) { false } let(:allowed_on_global_license) { false }
let(:plan_license) { Namespace::GOLD_PLAN } let(:plan_license) { Plan.find_by(name: 'gold') }
it 'returns false' do it 'returns false' do
is_expected.to eq(false) is_expected.to eq(false)
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Billing plan pages', :feature do describe 'Billing plan pages', :feature do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:bronze_plan) { Plan.find_by(name: 'bronze') }
let(:plans_data) do let(:plans_data) do
[ [
{ {
...@@ -101,7 +101,7 @@ describe 'Billing plan pages', :feature do ...@@ -101,7 +101,7 @@ describe 'Billing plan pages', :feature do
context 'users profile billing page' do context 'users profile billing page' do
before do before do
allow_any_instance_of(EE::Namespace).to receive(:plan).and_return('bronze') allow_any_instance_of(EE::Namespace).to receive(:plan).and_return(bronze_plan)
visit profile_billings_path visit profile_billings_path
end end
...@@ -123,7 +123,7 @@ describe 'Billing plan pages', :feature do ...@@ -123,7 +123,7 @@ describe 'Billing plan pages', :feature do
context 'top-most group' do context 'top-most group' do
before do before do
expect_any_instance_of(EE::Group).to receive(:plan).at_least(:once).and_return('bronze') expect_any_instance_of(EE::Group).to receive(:plan).at_least(:once).and_return(bronze_plan)
visit group_billings_path(group) visit group_billings_path(group)
end end
......
require 'spec_helper'
describe Plan do
describe 'associations' do
it { is_expected.to have_many(:namespaces) }
end
end
...@@ -83,6 +83,8 @@ RSpec.configure do |config| ...@@ -83,6 +83,8 @@ RSpec.configure do |config|
config.before(:all) do config.before(:all) do
License.destroy_all License.destroy_all
TestLicense.init TestLicense.init
SeedFu.seed
end end
config.after(:suite) do config.after(:suite) do
......
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