Skip to content
Snippets Groups Projects
Commit 606a1d2d authored by Alessio Caiazza's avatar Alessio Caiazza Committed by Mayra Cabrera
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 256 additions and 4 deletions
Loading
Loading
@@ -19,6 +19,11 @@ module Types
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?
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,
Types::ProjectType.connection_type,
null: false,
Loading
Loading
# 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
Loading
Loading
@@ -8,6 +8,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord
belongs_to :namespace
has_one :route, through: :namespace
 
scope :for_namespace_ids, ->(namespace_ids) { where(namespace_id: namespace_ids) }
delegate :all_projects, to: :namespace
 
def recalculate!
Loading
Loading
Loading
Loading
@@ -124,6 +124,8 @@ class GroupPolicy < BasePolicy
rule { developer & developer_maintainer_access }.enable :create_projects
rule { create_projects_disabled }.prevent :create_projects
 
rule { owner | admin }.enable :read_statistics
def access_level
return GroupMember::NO_ACCESS if @user.nil?
 
Loading
Loading
# frozen_string_literal: true
class Namespace::RootStorageStatisticsPolicy < BasePolicy
delegate { @subject.namespace }
end
Loading
Loading
@@ -11,6 +11,7 @@ class NamespacePolicy < BasePolicy
enable :create_projects
enable :admin_namespace
enable :read_namespace
enable :read_statistics
end
 
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
Loading
Loading
---
title: Expose namespace storage statistics with GraphQL
merge_request: 32012
author:
type: added
Loading
Loading
@@ -109,6 +109,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `visibility` | String | |
| `lfsEnabled` | 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 |
| `webUrl` | String! | |
| `avatarUrl` | String | |
Loading
Loading
@@ -453,6 +454,17 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `exists` | Boolean! | |
| `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
 
| Name | Type | Description |
Loading
Loading
# 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
Loading
Loading
@@ -8,7 +8,7 @@ describe GitlabSchema.types['Namespace'] do
it 'has the expected fields' do
expected_fields = %w[
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)
Loading
Loading
# 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
Loading
Loading
@@ -8,6 +8,19 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
 
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
let(:namespace) { create(:group) }
let(:root_storage_statistics) { create(:namespace_root_storage_statistics, namespace: namespace) }
Loading
Loading
# 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
Loading
Loading
@@ -6,7 +6,7 @@ describe NamespacePolicy do
let(:admin) { create(:admin) }
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) }
 
Loading
Loading
# 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
Loading
Loading
@@ -2,7 +2,7 @@
 
require 'spec_helper'
 
describe 'rendering namespace statistics' do
describe 'rendering project statistics' do
include GraphqlHelpers
 
let(:project) { create(:project) }
Loading
Loading
Loading
Loading
@@ -31,7 +31,8 @@ RSpec.shared_context 'GroupPolicy context' do
:admin_group_member,
:change_visibility_level,
:set_note_created_at,
:create_subgroup
:create_subgroup,
:read_statistics
].compact
end
 
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment