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 } } }
This commit is contained in:
parent
c65ea080ba
commit
606a1d2d31
18 changed files with 256 additions and 4 deletions
|
@ -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,
|
||||
|
|
16
app/graphql/types/root_storage_statistics_type.rb
Normal file
16
app/graphql/types/root_storage_statistics_type.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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
|
|||
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!
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
5
app/policies/namespace/root_storage_statistics_policy.rb
Normal file
5
app/policies/namespace/root_storage_statistics_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Namespace::RootStorageStatisticsPolicy < BasePolicy
|
||||
delegate { @subject.namespace }
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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
|
|||
| `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 | |
|
||||
|
@ -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 |
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
|||
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)
|
||||
|
|
14
spec/graphql/types/root_storage_statistics_type_spec.rb
Normal file
14
spec/graphql/types/root_storage_statistics_type_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 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
|
|
@ -0,0 +1,18 @@
|
|||
# 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
|
|||
|
||||
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) }
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# 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
|
|||
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) }
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# 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 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'rendering namespace statistics' do
|
||||
describe 'rendering project statistics' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:project) { create(:project) }
|
||||
|
|
|
@ -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…
Reference in a new issue