Commit b1bc399f authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'nicolasdular/set-temporary-storage-increase-in-graphql-api' into 'master'

Add GraphQL API to temporarily increase storage

See merge request gitlab-org/gitlab!36605
parents fe93883e de7a0f25
......@@ -8311,6 +8311,7 @@ type Mutation {
Update attributes of a merge request
"""
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload
......@@ -8503,6 +8504,41 @@ type NamespaceEdge {
node: Namespace
}
"""
Autogenerated input type of NamespaceIncreaseStorageTemporarily
"""
input NamespaceIncreaseStorageTemporarilyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the namespace to mutate
"""
id: ID!
}
"""
Autogenerated return type of NamespaceIncreaseStorageTemporarily
"""
type NamespaceIncreaseStorageTemporarilyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The namespace after mutation
"""
namespace: Namespace
}
type Note implements ResolvableInterface {
"""
User who wrote this note
......
......@@ -24468,6 +24468,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "namespaceIncreaseStorageTemporarily",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "NamespaceIncreaseStorageTemporarilyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "NamespaceIncreaseStorageTemporarilyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "removeAwardEmoji",
"description": null,
......@@ -25386,6 +25413,108 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "NamespaceIncreaseStorageTemporarilyInput",
"description": "Autogenerated input type of NamespaceIncreaseStorageTemporarily",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the namespace to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "NamespaceIncreaseStorageTemporarilyPayload",
"description": "Autogenerated return type of NamespaceIncreaseStorageTemporarily",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "namespace",
"description": "The namespace after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Namespace",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Note",
......@@ -1302,6 +1302,16 @@ Contains statistics about a milestone
| `temporaryStorageIncreaseEndsOn` | Time | Date until the temporary storage increase is active |
| `visibility` | String | Visibility of the namespace |
## NamespaceIncreaseStorageTemporarilyPayload
Autogenerated return type of NamespaceIncreaseStorageTemporarily
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `namespace` | Namespace | The namespace after mutation |
## Note
| Name | Type | Description |
......
......@@ -24,6 +24,7 @@ module EE
mount_mutation ::Mutations::Pipelines::RunDastScan
mount_mutation ::Mutations::DastSiteProfiles::Create
mount_mutation ::Mutations::DastScannerProfiles::Create
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
end
end
end
......
# frozen_string_literal: true
module Mutations
module Namespaces
class Base < ::Mutations::BaseMutation
argument :id, GraphQL::ID_TYPE,
required: true,
description: "The global id of the namespace to mutate"
field :namespace,
Types::NamespaceType,
null: true,
description: 'The namespace after mutation'
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Namespaces
class IncreaseStorageTemporarily < Base
graphql_name "NamespaceIncreaseStorageTemporarily"
authorize :admin_namespace
def resolve(args)
namespace = authorized_find!(id: args[:id])
namespace.enable_temporary_storage_increase!
{ namespace: namespace, errors: namespace.errors.full_messages }
end
end
end
end
......@@ -19,6 +19,7 @@ module EE
LICENSE_PLANS_TO_NAMESPACE_PLANS = NAMESPACE_PLANS_TO_LICENSE_PLANS.invert.freeze
PLANS = (NAMESPACE_PLANS_TO_LICENSE_PLANS.keys + [Plan::FREE]).freeze
TEMPORARY_STORAGE_INCREASE_DAYS = 30
prepended do
include EachBatch
......@@ -51,7 +52,8 @@ module EE
delegate :additional_purchased_storage_size, :additional_purchased_storage_size=,
:additional_purchased_storage_ends_on, :additional_purchased_storage_ends_on=,
:temporary_storage_increase_ends_on, :temporary_storage_increase_ends_on=,
:temporary_storage_increase_enabled?, to: :namespace_limit, allow_nil: true
:temporary_storage_increase_enabled?, :eligible_for_temporary_storage_increase?,
to: :namespace_limit, allow_nil: true
delegate :email, to: :owner, allow_nil: true, prefix: true
......@@ -335,6 +337,10 @@ module EE
::Gitlab::CurrentSettings.elasticsearch_indexes_namespace?(self)
end
def enable_temporary_storage_increase!
update(temporary_storage_increase_ends_on: TEMPORARY_STORAGE_INCREASE_DAYS.days.from_now)
end
private
def fallback_plan
......
# frozen_string_literal: true
class NamespaceLimit < ApplicationRecord
MIN_REQURIED_STORAGE_USAGE_RATIO = 0.5
self.primary_key = :namespace_id
belongs_to :namespace, inverse_of: :namespace_limit
validates :namespace, presence: true
validate :namespace_is_root_namespace
validate :temporary_storage_increase_set_once, if: :temporary_storage_increase_ends_on_changed?
validate :temporary_storage_increase_eligibility, if: :temporary_storage_increase_ends_on_changed?
def temporary_storage_increase_enabled?
return false unless ::Feature.enabled?(:temporary_storage_increase, namespace)
return false if temporary_storage_increase_ends_on.nil?
temporary_storage_increase_ends_on >= Date.today
end
def eligible_for_temporary_storage_increase?
return false unless ::Feature.enabled?(:temporary_storage_increase, namespace)
EE::Namespace::RootStorageSize.new(namespace).usage_ratio >= MIN_REQURIED_STORAGE_USAGE_RATIO
end
private
def namespace_is_root_namespace
return unless namespace
errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
end
def temporary_storage_increase_set_once
if temporary_storage_increase_ends_on_was.present?
errors.add(:temporary_storage_increase_ends_on, s_('TemporaryStorageIncrease|can only be set once'))
end
end
def temporary_storage_increase_eligibility
unless eligible_for_temporary_storage_increase?
errors.add(
:temporary_storage_increase_ends_on,
s_("TemporaryStorageIncrease|can only be set with more than %{percentage}%% usage") %
{
percentage: (MIN_REQURIED_STORAGE_USAGE_RATIO * 100).to_i
}
)
end
end
end
---
title: Add GraphQL API to temporarily increase storage
merge_request: 36605
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Namespaces::IncreaseStorageTemporarily do
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { user.namespace }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
subject do
mutation.resolve(id: namespace.to_global_id.to_s)
end
before do
allow_next_instance_of(EE::Namespace::RootStorageSize, namespace) do |root_storage|
allow(root_storage).to receive(:usage_ratio).and_return(0.5)
end
end
context 'when user is not the admin of the namespace' do
let(:user) { create(:user) }
it 'raises a not accessible error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can admin the namespace' do
it 'sets temporary_storage_increase_ends_on' do
expect(namespace.temporary_storage_increase_ends_on).to be_nil
subject
expect(subject[:namespace]).to be_present
expect(subject[:errors]).to be_empty
expect(namespace.reload.temporary_storage_increase_ends_on).to be_present
end
end
end
end
......@@ -3,11 +3,20 @@
require 'spec_helper'
RSpec.describe NamespaceLimit do
let(:namespace_limit) { build(:namespace_limit) }
let(:usage_ratio) { 0.5 }
subject { namespace_limit }
before do
allow_next_instance_of(EE::Namespace::RootStorageSize, namespace_limit.namespace) do |root_storage|
allow(root_storage).to receive(:usage_ratio).and_return(usage_ratio)
end
end
it { is_expected.to belong_to(:namespace) }
describe '#temporary_storage_increase_enabled?' do
let(:namespace_limit) { build(:namespace_limit) }
subject { namespace_limit.temporary_storage_increase_enabled? }
context 'when date is not set' do
......@@ -38,4 +47,95 @@ RSpec.describe NamespaceLimit do
it { is_expected.to eq(false) }
end
end
describe '#eligible_for_temporary_storage_increase?' do
subject { namespace_limit.eligible_for_temporary_storage_increase? }
context 'when usage ratio is above the threshold' do
let(:usage_ratio) { 0.5 }
it { is_expected.to be_truthy }
context 'when feature is disabled' do
before do
stub_feature_flags(temporary_storage_increase: false)
end
it { is_expected.to eq(false) }
end
end
context 'when usage ratio is below the threshold' do
let(:usage_ratio) { 0.49 }
it { is_expected.to be_falsey }
end
end
describe 'validations' do
it { is_expected.to validate_presence_of(:namespace) }
context 'namespace_is_root_namespace' do
let(:namespace_limit) { build(:namespace_limit, namespace: namespace)}
context 'when associated namespace is root' do
let(:namespace) { build(:group, parent: nil) }
it { is_expected.to be_valid }
end
context 'when associated namespace is not root' do
let(:namespace) { build(:group, :nested) }
it 'is invalid' do
expect(subject).to be_invalid
expect(subject.errors[:namespace]).to include('must be a root namespace')
end
end
end
context 'temporary_storage_increase_set_once' do
context 'when temporary_storage_increase_ends_on was nil' do
it 'can be set' do
namespace_limit.temporary_storage_increase_ends_on = Date.today
expect(namespace_limit).to be_valid
end
end
context 'when temporary_storage_increase_ends_on is already set' do
before do
namespace_limit.update_attribute(:temporary_storage_increase_ends_on, 30.days.ago)
end
it 'can not be set again' do
namespace_limit.temporary_storage_increase_ends_on = Date.today
expect(subject).to be_invalid
expect(subject.errors[:temporary_storage_increase_ends_on]).to include('can only be set once')
end
end
end
context 'temporary_storage_increase_eligibility' do
before do
namespace_limit.temporary_storage_increase_ends_on = Date.today
end
context 'when storage usage is above threshold' do
let(:usage_ratio) { 0.5 }
it { is_expected.to be_valid }
end
context 'when storage usage is below threshold' do
let(:usage_ratio) { 0.49 }
it 'is invalid' do
expect(namespace_limit).to be_invalid
expect(namespace_limit.errors[:temporary_storage_increase_ends_on]).to include("can only be set with more than 50% usage")
end
end
end
end
end
......@@ -31,6 +31,7 @@ RSpec.describe Namespace do
it { is_expected.to delegate_method(:temporary_storage_increase_ends_on).to(:namespace_limit) }
it { is_expected.to delegate_method(:temporary_storage_increase_ends_on=).to(:namespace_limit).with_arguments(:args) }
it { is_expected.to delegate_method(:temporary_storage_increase_enabled?).to(:namespace_limit) }
it { is_expected.to delegate_method(:eligible_for_temporary_storage_increase?).to(:namespace_limit) }
shared_examples 'plan helper' do |namespace_plan|
let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") }
......@@ -1507,4 +1508,26 @@ RSpec.describe Namespace do
expect(namespace.namespace_limit).not_to be_persisted
end
end
describe '#enable_temporary_storage_increase!' do
it 'sets a date' do
namespace = build(:namespace)
Timecop.freeze do
namespace.enable_temporary_storage_increase!
expect(namespace.temporary_storage_increase_ends_on).to eq(30.days.from_now.to_date)
end
end
it 'is invalid when set twice' do
namespace = create(:namespace)
namespace.enable_temporary_storage_increase!
namespace.enable_temporary_storage_increase!
expect(namespace).to be_invalid
expect(namespace.errors[:"namespace_limit.temporary_storage_increase_ends_on"]).to be_present
end
end
end
......@@ -23371,6 +23371,12 @@ msgstr ""
msgid "Templates"
msgstr ""
msgid "TemporaryStorageIncrease|can only be set once"
msgstr ""
msgid "TemporaryStorageIncrease|can only be set with more than %{percentage}%% usage"
msgstr ""
msgid "TemporaryStorage|GitLab allows you a %{strongStart}free, one-time storage increase%{strongEnd}. For 30 days your storage will be unlimited. This gives you time to reduce your storage usage. After 30 days, your original storage limit of %{limit} applies. If you are at maximum storage capacity, your account will be read-only. To continue using GitLab you'll have to purchase additional storage or decrease storage usage."
msgstr ""
......@@ -28829,6 +28835,9 @@ msgstr ""
msgid "mrWidget|to start a merge train when the pipeline succeeds"
msgstr ""
msgid "must be a root namespace"
msgstr ""
msgid "must be greater than start date"
msgstr ""
......
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