Commit 606a1d2d authored by Alessio Caiazza's avatar Alessio Caiazza Committed by Mayra Cabrera

Expose namespace storage statistics with GraphQL

Root namespaces have storage statistics.
This commit allows namespace owners to get those stats via GraphQL
queries like the following one

{
  namespace(fullPath: "a_namespace_path") {
    rootStorageStatistics {
      storageSize
      repositorySize
      lfsObjectsSize
      buildArtifactsSize
      packagesSize
      wikiSize
    }
  }
}
parent c65ea080
...@@ -19,6 +19,11 @@ module Types ...@@ -19,6 +19,11 @@ module Types
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :root_storage_statistics, Types::RootStorageStatisticsType,
null: true,
description: 'The aggregated storage statistics. Only available for root namespaces',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(obj.id).find }
field :projects, field :projects,
Types::ProjectType.connection_type, Types::ProjectType.connection_type,
null: false, null: false,
......
# frozen_string_literal: true
module Types
class RootStorageStatisticsType < BaseObject
graphql_name 'RootStorageStatistics'
authorize :read_statistics
field :storage_size, GraphQL::INT_TYPE, null: false, description: 'The total storage in bytes'
field :repository_size, GraphQL::INT_TYPE, null: false, description: 'The git repository size in bytes'
field :lfs_objects_size, GraphQL::INT_TYPE, null: false, description: 'The LFS objects size in bytes'
field :build_artifacts_size, GraphQL::INT_TYPE, null: false, description: 'The CI artifacts size in bytes'
field :packages_size, GraphQL::INT_TYPE, null: false, description: 'The packages size in bytes'
field :wiki_size, GraphQL::INT_TYPE, null: false, description: 'The wiki size in bytes'
end
end
...@@ -8,6 +8,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord ...@@ -8,6 +8,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord
belongs_to :namespace belongs_to :namespace
has_one :route, through: :namespace has_one :route, through: :namespace
scope :for_namespace_ids, ->(namespace_ids) { where(namespace_id: namespace_ids) }
delegate :all_projects, to: :namespace delegate :all_projects, to: :namespace
def recalculate! def recalculate!
......
...@@ -124,6 +124,8 @@ class GroupPolicy < BasePolicy ...@@ -124,6 +124,8 @@ class GroupPolicy < BasePolicy
rule { developer & developer_maintainer_access }.enable :create_projects rule { developer & developer_maintainer_access }.enable :create_projects
rule { create_projects_disabled }.prevent :create_projects rule { create_projects_disabled }.prevent :create_projects
rule { owner | admin }.enable :read_statistics
def access_level def access_level
return GroupMember::NO_ACCESS if @user.nil? return GroupMember::NO_ACCESS if @user.nil?
......
# frozen_string_literal: true
class Namespace::RootStorageStatisticsPolicy < BasePolicy
delegate { @subject.namespace }
end
...@@ -11,6 +11,7 @@ class NamespacePolicy < BasePolicy ...@@ -11,6 +11,7 @@ class NamespacePolicy < BasePolicy
enable :create_projects enable :create_projects
enable :admin_namespace enable :admin_namespace
enable :read_namespace enable :read_namespace
enable :read_statistics
end end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects rule { personal_project & ~can_create_personal_project }.prevent :create_projects
......
---
title: Expose namespace storage statistics with GraphQL
merge_request: 32012
author:
type: added
...@@ -109,6 +109,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -109,6 +109,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `visibility` | String | | | `visibility` | String | |
| `lfsEnabled` | Boolean | | | `lfsEnabled` | Boolean | |
| `requestAccessEnabled` | Boolean | | | `requestAccessEnabled` | Boolean | |
| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available if the namespace has no parent |
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
| `webUrl` | String! | | | `webUrl` | String! | |
| `avatarUrl` | String | | | `avatarUrl` | String | |
...@@ -453,6 +454,17 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -453,6 +454,17 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `exists` | Boolean! | | | `exists` | Boolean! | |
| `tree` | Tree | | | `tree` | Tree | |
### RootStorageStatistics
| Name | Type | Description |
| --- | ---- | ---------- |
| `storageSize` | Int! | The total storage in Bytes |
| `repositorySize` | Int! | The git repository size in Bytes |
| `lfsObjectsSize` | Int! | The LFS objects size in Bytes |
| `buildArtifactsSize` | Int! | The CI artifacts size in Bytes |
| `packagesSize` | Int! | The packages size in Bytes |
| `wikiSize` | Int! | The wiki size in Bytes |
### Submodule ### Submodule
| Name | Type | Description | | Name | Type | Description |
......
# frozen_string_literal: true
module Gitlab
module Graphql
module Loaders
class BatchRootStorageStatisticsLoader
attr_reader :namespace_id
def initialize(namespace_id)
@namespace_id = namespace_id
end
def find
BatchLoader.for(namespace_id).batch do |namespace_ids, loader|
Namespace::RootStorageStatistics.for_namespace_ids(namespace_ids).each do |statistics|
loader.call(statistics.namespace_id, statistics)
end
end
end
end
end
end
end
...@@ -8,7 +8,7 @@ describe GitlabSchema.types['Namespace'] do ...@@ -8,7 +8,7 @@ describe GitlabSchema.types['Namespace'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
id name path full_name full_path description description_html visibility id name path full_name full_path description description_html visibility
lfs_enabled request_access_enabled projects lfs_enabled request_access_enabled projects root_storage_statistics
] ]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['RootStorageStatistics'] do
it { expect(described_class.graphql_name).to eq('RootStorageStatistics') }
it 'has all the required fields' do
is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
:build_artifacts_size, :packages_size, :wiki_size)
end
it { is_expected.to require_graphql_authorizations(:read_statistics) }
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader do
describe '#find' do
it 'only queries once for project statistics' do
stats = create_list(:namespace_root_storage_statistics, 2)
namespace1 = stats.first.namespace
namespace2 = stats.last.namespace
expect do
described_class.new(namespace1.id).find
described_class.new(namespace2.id).find
end.not_to exceed_query_limit(1)
end
end
end
...@@ -8,6 +8,19 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do ...@@ -8,6 +8,19 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
it { is_expected.to delegate_method(:all_projects).to(:namespace) } it { is_expected.to delegate_method(:all_projects).to(:namespace) }
context 'scopes' do
describe '.for_namespace_ids' do
it 'returns only requested namespaces' do
stats = create_list(:namespace_root_storage_statistics, 3)
namespace_ids = stats[0..1].map { |s| s.namespace_id }
requested_stats = described_class.for_namespace_ids(namespace_ids).pluck(:namespace_id)
expect(requested_stats).to eq(namespace_ids)
end
end
end
describe '#recalculate!' do describe '#recalculate!' do
let(:namespace) { create(:group) } let(:namespace) { create(:group) }
let(:root_storage_statistics) { create(:namespace_root_storage_statistics, namespace: namespace) } let(:root_storage_statistics) { create(:namespace_root_storage_statistics, namespace: namespace) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Namespace::RootStorageStatisticsPolicy do
using RSpec::Parameterized::TableSyntax
describe '#rules' do
let(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace) }
let(:user) { create(:user) }
subject { Ability.allowed?(user, :read_statistics, statistics) }
shared_examples 'deny anonymous users' do
context 'when the users is anonymous' do
let(:user) { nil }
it { is_expected.to be_falsey }
end
end
context 'when the namespace is a personal namespace' do
let(:owner) { create(:user) }
let(:namespace) { owner.namespace }
include_examples 'deny anonymous users'
context 'when the user is not the owner' do
it { is_expected.to be_falsey }
end
context 'when the user is the owner' do
let(:user) { owner }
it { is_expected.to be_truthy }
end
end
context 'when the namespace is a group' do
let(:user) { create(:user) }
let(:external) { create(:user, :external) }
shared_examples 'allows only owners' do |group_type|
let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel.level_value(group_type.to_s)) }
let(:namespace) { group }
include_examples 'deny anonymous users'
where(:user_type, :outcome) do
[
[:non_member, false],
[:guest, false],
[:reporter, false],
[:developer, false],
[:maintainer, false],
[:owner, true]
]
end
with_them do
before do
group.add_user(user, user_type) unless user_type == :non_member
end
it { is_expected.to eq(outcome) }
context 'when the user is external' do
let(:user) { external }
it { is_expected.to eq(outcome) }
end
end
end
include_examples 'allows only owners', :public
include_examples 'allows only owners', :private
include_examples 'allows only owners', :internal
end
end
end
...@@ -6,7 +6,7 @@ describe NamespacePolicy do ...@@ -6,7 +6,7 @@ describe NamespacePolicy do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, owner: owner) } let(:namespace) { create(:namespace, owner: owner) }
let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace] } let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace, :read_statistics] }
subject { described_class.new(current_user, namespace) } subject { described_class.new(current_user, namespace) }
......
# frozen_string_literal: true
require 'spec_helper'
describe 'rendering namespace statistics' do
include GraphqlHelpers
let(:namespace) { user.namespace }
let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.megabytes) }
let(:user) { create(:user) }
let(:query) do
graphql_query_for('namespace',
{ 'fullPath' => namespace.full_path },
"rootStorageStatistics { #{all_graphql_fields_for('RootStorageStatistics')} }")
end
shared_examples 'a working namespace with storage statistics query' do
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: user)
end
end
it 'includes the packages size if the user can read the statistics' do
post_graphql(query, current_user: user)
expect(graphql_data['namespace']['rootStorageStatistics']).not_to be_blank
expect(graphql_data['namespace']['rootStorageStatistics']['packagesSize']).to eq(5.megabytes)
end
end
it_behaves_like 'a working namespace with storage statistics query'
context 'when the namespace is a group' do
let(:group) { create(:group) }
let(:namespace) { group }
before do
group.add_owner(user)
end
it_behaves_like 'a working namespace with storage statistics query'
context 'when the namespace is public' do
let(:group) { create(:group, :public)}
it 'hides statistics for unauthenticated requests' do
post_graphql(query, current_user: nil)
expect(graphql_data['namespace']).to be_blank
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe 'rendering namespace statistics' do describe 'rendering project statistics' do
include GraphqlHelpers include GraphqlHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -31,7 +31,8 @@ RSpec.shared_context 'GroupPolicy context' do ...@@ -31,7 +31,8 @@ RSpec.shared_context 'GroupPolicy context' do
:admin_group_member, :admin_group_member,
:change_visibility_level, :change_visibility_level,
:set_note_created_at, :set_note_created_at,
:create_subgroup :create_subgroup,
:read_statistics
].compact ].compact
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