Commit 76178869 authored by nicolasdular's avatar nicolasdular Committed by Nicolas Dular

Add Namespace API to set additionally purchased storage

This exposes the params `additional_purchased_storage_size` and
`additional_purchased_storage_ends_on` in our Namespace API and
the possibility to set them for admin users.

This API will be used by customer portal when additional storage
packages get bought
parent 9baace02
...@@ -58,6 +58,8 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -58,6 +58,8 @@ class BasePolicy < DeclarativePolicy::Base
rule { admin }.enable :read_all_resources rule { admin }.enable :read_all_resources
rule { default }.enable :read_cross_project rule { default }.enable :read_cross_project
condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? }
end end
BasePolicy.prepend_if_ee('EE::BasePolicy') BasePolicy.prepend_if_ee('EE::BasePolicy')
...@@ -26,10 +26,12 @@ module EE ...@@ -26,10 +26,12 @@ module EE
attr_writer :root_ancestor attr_writer :root_ancestor
has_one :namespace_statistics has_one :namespace_statistics
has_one :namespace_limit, inverse_of: :namespace
has_one :gitlab_subscription, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :gitlab_subscription, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :elasticsearch_indexed_namespace has_one :elasticsearch_indexed_namespace
accepts_nested_attributes_for :gitlab_subscription accepts_nested_attributes_for :gitlab_subscription
accepts_nested_attributes_for :namespace_limit
scope :include_gitlab_subscription, -> { includes(:gitlab_subscription) } scope :include_gitlab_subscription, -> { includes(:gitlab_subscription) }
scope :join_gitlab_subscription, -> { joins("LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id") } scope :join_gitlab_subscription, -> { joins("LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id") }
...@@ -53,6 +55,10 @@ module EE ...@@ -53,6 +55,10 @@ module EE
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
:extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true :extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true
delegate :additional_purchased_storage_size, :additional_purchased_storage_size=,
:additional_purchased_storage_ends_on, :additional_purchased_storage_ends_on=,
to: :namespace_limit, allow_nil: true
delegate :email, to: :owner, allow_nil: true, prefix: true delegate :email, to: :owner, allow_nil: true, prefix: true
# Opportunistically clear the +file_template_project_id+ if invalid # Opportunistically clear the +file_template_project_id+ if invalid
...@@ -72,6 +78,10 @@ module EE ...@@ -72,6 +78,10 @@ module EE
before_save :clear_feature_available_cache before_save :clear_feature_available_cache
end end
def namespace_limit
super.presence || build_namespace_limit
end
class_methods do class_methods do
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
......
# frozen_string_literal: true
class NamespaceLimit < ApplicationRecord
self.primary_key = :namespace_id
belongs_to :namespace, inverse_of: :namespace_limit
end
...@@ -256,6 +256,8 @@ module EE ...@@ -256,6 +256,8 @@ module EE
rule { ~reject_unsigned_commits_available }.prevent :change_reject_unsigned_commits rule { ~reject_unsigned_commits_available }.prevent :change_reject_unsigned_commits
rule { can?(:maintainer_access) & push_rules_available }.enable :change_push_rules rule { can?(:maintainer_access) & push_rules_available }.enable :change_push_rules
rule { admin & is_gitlab_com }.enable :update_subscription_limit
end end
override :lookup_access_level! override :lookup_access_level!
......
...@@ -8,6 +8,8 @@ module EE ...@@ -8,6 +8,8 @@ module EE
rule { owner | admin }.policy do rule { owner | admin }.policy do
enable :create_jira_connect_subscription enable :create_jira_connect_subscription
end end
rule { admin & is_gitlab_com }.enable :update_subscription_limit
end end
end end
end end
---
title: Add Namespace API to set additionally purchased storage
merge_request: 35257
author:
type: added
...@@ -7,10 +7,13 @@ module EE ...@@ -7,10 +7,13 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
can_update_limits = ->(namespace, opts) { ::Ability.allowed?(opts[:current_user], :update_subscription_limit, namespace) }
can_admin_namespace = ->(namespace, opts) { ::Ability.allowed?(opts[:current_user], :admin_namespace, namespace) } can_admin_namespace = ->(namespace, opts) { ::Ability.allowed?(opts[:current_user], :admin_namespace, namespace) }
expose :shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? } expose :shared_runners_minutes_limit, if: can_update_limits
expose :extra_shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? } expose :extra_shared_runners_minutes_limit, if: can_update_limits
expose :additional_purchased_storage_size, if: can_update_limits
expose :additional_purchased_storage_ends_on, if: can_update_limits
expose :billable_members_count do |namespace, options| expose :billable_members_count do |namespace, options|
namespace.billable_members_count(options[:requested_hosted_plan]) namespace.billable_members_count(options[:requested_hosted_plan])
end end
......
...@@ -53,6 +53,8 @@ module EE ...@@ -53,6 +53,8 @@ module EE
params do params do
optional :shared_runners_minutes_limit, type: Integer, desc: "Pipeline minutes quota for this namespace" optional :shared_runners_minutes_limit, type: Integer, desc: "Pipeline minutes quota for this namespace"
optional :extra_shared_runners_minutes_limit, type: Integer, desc: "Extra pipeline minutes for this namespace" optional :extra_shared_runners_minutes_limit, type: Integer, desc: "Extra pipeline minutes for this namespace"
optional :additional_purchased_storage_size, type: Integer, desc: "Additional storage size for this namespace"
optional :additional_purchased_storage_ends_on, type: Date, desc: "End of subscription of the additional purchased storage"
end end
put ':id' do put ':id' do
authenticated_as_admin! authenticated_as_admin!
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe NamespaceLimit do
it { is_expected.to belong_to(:namespace) }
end
...@@ -13,6 +13,7 @@ RSpec.describe Namespace do ...@@ -13,6 +13,7 @@ RSpec.describe Namespace do
let!(:gold_plan) { create(:gold_plan) } let!(:gold_plan) { create(:gold_plan) }
it { is_expected.to have_one(:namespace_statistics) } it { is_expected.to have_one(:namespace_statistics) }
it { is_expected.to have_one(:namespace_limit) }
it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) } it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) }
it { is_expected.to have_one(:elasticsearch_indexed_namespace) } it { is_expected.to have_one(:elasticsearch_indexed_namespace) }
...@@ -24,6 +25,10 @@ RSpec.describe Namespace do ...@@ -24,6 +25,10 @@ RSpec.describe Namespace do
it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) } it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) }
it { is_expected.to delegate_method(:upgradable?).to(:gitlab_subscription) } it { is_expected.to delegate_method(:upgradable?).to(:gitlab_subscription) }
it { is_expected.to delegate_method(:email).to(:owner).with_prefix.allow_nil } it { is_expected.to delegate_method(:email).to(:owner).with_prefix.allow_nil }
it { is_expected.to delegate_method(:additional_purchased_storage_size).to(:namespace_limit) }
it { is_expected.to delegate_method(:additional_purchased_storage_size=).to(:namespace_limit).with_arguments(:args) }
it { is_expected.to delegate_method(:additional_purchased_storage_ends_on).to(:namespace_limit) }
it { is_expected.to delegate_method(:additional_purchased_storage_ends_on=).to(:namespace_limit).with_arguments(:args) }
shared_examples 'plan helper' do |namespace_plan| shared_examples 'plan helper' do |namespace_plan|
let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") } let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") }
...@@ -1462,4 +1467,13 @@ RSpec.describe Namespace do ...@@ -1462,4 +1467,13 @@ RSpec.describe Namespace do
end end
end end
end end
describe 'ensure namespace limit' do
it 'has namespace limit upon namespace initialization' do
namespace = build(:namespace)
expect(namespace.namespace_limit).to be_present
expect(namespace.namespace_limit).not_to be_persisted
end
end
end end
...@@ -1086,4 +1086,6 @@ RSpec.describe GroupPolicy do ...@@ -1086,4 +1086,6 @@ RSpec.describe GroupPolicy do
end end
end end
end end
it_behaves_like 'update namespace limit policy'
end end
...@@ -48,4 +48,6 @@ RSpec.describe NamespacePolicy do ...@@ -48,4 +48,6 @@ RSpec.describe NamespacePolicy do
it { is_expected.to be_disallowed(:create_jira_connect_subscription) } it { is_expected.to be_disallowed(:create_jira_connect_subscription) }
end end
end end
it_behaves_like 'update namespace limit policy'
end end
...@@ -11,6 +11,10 @@ RSpec.describe API::Namespaces do ...@@ -11,6 +11,10 @@ RSpec.describe API::Namespaces do
describe "GET /namespaces" do describe "GET /namespaces" do
context "when authenticated as admin" do context "when authenticated as admin" do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it "returns correct attributes" do it "returns correct attributes" do
get api("/namespaces", admin) get api("/namespaces", admin)
...@@ -23,12 +27,14 @@ RSpec.describe API::Namespaces do ...@@ -23,12 +27,14 @@ RSpec.describe API::Namespaces do
'parent_id', 'members_count_with_descendants', 'parent_id', 'members_count_with_descendants',
'plan', 'shared_runners_minutes_limit', 'plan', 'shared_runners_minutes_limit',
'avatar_url', 'web_url', 'trial_ends_on', 'trial', 'avatar_url', 'web_url', 'trial_ends_on', 'trial',
'extra_shared_runners_minutes_limit', 'billable_members_count') 'extra_shared_runners_minutes_limit', 'billable_members_count',
'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'plan', 'shared_runners_minutes_limit', 'parent_id', 'plan', 'shared_runners_minutes_limit',
'avatar_url', 'web_url', 'trial_ends_on', 'trial', 'avatar_url', 'web_url', 'trial_ends_on', 'trial',
'extra_shared_runners_minutes_limit', 'billable_members_count') 'extra_shared_runners_minutes_limit', 'billable_members_count',
'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
end end
end end
...@@ -111,27 +117,43 @@ RSpec.describe API::Namespaces do ...@@ -111,27 +117,43 @@ RSpec.describe API::Namespaces do
end end
describe 'PUT /namespaces/:id' do describe 'PUT /namespaces/:id' do
let(:params) do
{
shared_runners_minutes_limit: 9001,
additional_purchased_storage_size: 10_000,
additional_purchased_storage_ends_on: Date.today.to_s
}
end
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
context 'when authenticated as admin' do context 'when authenticated as admin' do
it 'updates namespace using full_path when full_path contains dots' do it 'updates namespace using full_path when full_path contains dots' do
put api("/namespaces/#{group1.full_path}", admin), params: { shared_runners_minutes_limit: 9001 } put api("/namespaces/#{group1.full_path}", admin), params: params
aggregate_failures do aggregate_failures do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['shared_runners_minutes_limit']).to eq(9001) expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
expect(json_response['additional_purchased_storage_ends_on']).to eq(params[:additional_purchased_storage_ends_on])
end end
end end
it 'updates namespace using id' do it 'updates namespace using id' do
put api("/namespaces/#{group1.id}", admin), params: { shared_runners_minutes_limit: 9001 } put api("/namespaces/#{group1.id}", admin), params: params
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['shared_runners_minutes_limit']).to eq(9001) expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
expect(json_response['additional_purchased_storage_ends_on']).to eq(params[:additional_purchased_storage_ends_on])
end end
end end
context 'when not authenticated as admin' do context 'when not authenticated as admin' do
it 'retuns 403' do it 'retuns 403' do
put api("/namespaces/#{group1.id}", user), params: { shared_runners_minutes_limit: 9001 } put api("/namespaces/#{group1.id}", user), params: params
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
...@@ -139,7 +161,7 @@ RSpec.describe API::Namespaces do ...@@ -139,7 +161,7 @@ RSpec.describe API::Namespaces do
context 'when namespace not found' do context 'when namespace not found' do
it 'returns 404' do it 'returns 404' do
put api("/namespaces/#{non_existing_record_id}", admin), params: { shared_runners_minutes_limit: 9001 } put api("/namespaces/#{non_existing_record_id}", admin), params: params
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found') expect(json_response).to eq('message' => '404 Namespace Not Found')
...@@ -147,10 +169,20 @@ RSpec.describe API::Namespaces do ...@@ -147,10 +169,20 @@ RSpec.describe API::Namespaces do
end end
context 'when invalid params' do context 'when invalid params' do
it 'returns validation error' do where(:attr) do
put api("/namespaces/#{group1.id}", admin), params: { shared_runners_minutes_limit: 'unknown' } [
:shared_runners_minutes_limit,
:additional_purchased_storage_size,
:additional_purchased_storage_ends_on
]
end
expect(response).to have_gitlab_http_status(:bad_request) with_them do
it "returns validation error for #{attr}" do
put api("/namespaces/#{group1.id}", admin), params: Hash[attr, 'unknown']
expect(response).to have_gitlab_http_status(:bad_request)
end
end end
end end
......
# frozen_string_literal: true
RSpec.shared_examples 'update namespace limit policy' do
describe 'update_subscription_limit' do
using RSpec::Parameterized::TableSyntax
let(:policy) { :update_subscription_limit }
where(:role, :is_com, :allowed) do
:user | true | false
:owner | true | false
:admin | true | true
:user | false | false
:owner | false | false
:admin | false | false
end
with_them do
let(:current_user) { build_stubbed(role) }
before do
allow(Gitlab).to receive(:com?).and_return(is_com)
end
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(policy) }
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