summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlessio Caiazza <acaiazza@gitlab.com>2019-08-21 10:40:30 +0200
committerAlessio Caiazza <acaiazza@gitlab.com>2019-08-22 16:23:18 +0200
commit8022775f6343a38598be8b59c77d8a2046e63a19 (patch)
tree11b87104b4bccaeaae032e06ee6703e9f9ecf51d
parentca622a3e13cf88d94c6b3c98554e9782d37d4ad5 (diff)
downloadgitlab-ce-ac-graphql-root-namespace-stats.tar.gz
Expose namespace storage statistics with GraphQLac-graphql-root-namespace-stats
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 } } }
-rw-r--r--app/graphql/types/namespace_type.rb5
-rw-r--r--app/graphql/types/root_storage_statistics_type.rb16
-rw-r--r--app/models/namespace/root_storage_statistics.rb2
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/namespace/root_storage_statistics_policy.rb5
-rw-r--r--app/policies/namespace_policy.rb1
-rw-r--r--changelogs/unreleased/ac-graphql-root-namespace-stats.yml5
-rw-r--r--doc/api/graphql/reference/index.md12
-rw-r--r--lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb23
-rw-r--r--spec/graphql/types/namespace_type_spec.rb2
-rw-r--r--spec/graphql/types/root_storage_statistics_type_spec.rb14
-rw-r--r--spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb18
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb13
-rw-r--r--spec/policies/namespace/root_storage_statistics_policy_spec.rb80
-rw-r--r--spec/policies/namespace_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb55
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb2
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb3
18 files changed, 256 insertions, 4 deletions
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index f105e9e6e28..35a97b5ace0 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -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,
diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb
new file mode 100644
index 00000000000..a7498ee0a2e
--- /dev/null
+++ b/app/graphql/types/root_storage_statistics_type.rb
@@ -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
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 56c430013ee..ae9b2f14343 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -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!
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index c686e7763bb..5d2b74b17a2 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -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?
diff --git a/app/policies/namespace/root_storage_statistics_policy.rb b/app/policies/namespace/root_storage_statistics_policy.rb
new file mode 100644
index 00000000000..63fcaf20dfe
--- /dev/null
+++ b/app/policies/namespace/root_storage_statistics_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Namespace::RootStorageStatisticsPolicy < BasePolicy
+ delegate { @subject.namespace }
+end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 2babcb0a2d9..937666c7e54 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -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
diff --git a/changelogs/unreleased/ac-graphql-root-namespace-stats.yml b/changelogs/unreleased/ac-graphql-root-namespace-stats.yml
new file mode 100644
index 00000000000..9784605ab2e
--- /dev/null
+++ b/changelogs/unreleased/ac-graphql-root-namespace-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Expose namespace storage statistics with GraphQL
+merge_request: 32012
+author:
+type: added
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2d3bec4ff67..d99a4c37d72 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb b/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb
new file mode 100644
index 00000000000..a0312366d66
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb
@@ -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
diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb
index e1153832cc9..f476dd7286f 100644
--- a/spec/graphql/types/namespace_type_spec.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -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)
diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb
new file mode 100644
index 00000000000..8c69c13aa73
--- /dev/null
+++ b/spec/graphql/types/root_storage_statistics_type_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb
new file mode 100644
index 00000000000..38931f7ab5e
--- /dev/null
+++ b/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb
@@ -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
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index 5341278db7c..9e12831a704 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -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) }
diff --git a/spec/policies/namespace/root_storage_statistics_policy_spec.rb b/spec/policies/namespace/root_storage_statistics_policy_spec.rb
new file mode 100644
index 00000000000..8d53050fffb
--- /dev/null
+++ b/spec/policies/namespace/root_storage_statistics_policy_spec.rb
@@ -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
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
index 99fa8b1fe44..216aaae70ee 100644
--- a/spec/policies/namespace_policy_spec.rb
+++ b/spec/policies/namespace_policy_spec.rb
@@ -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) }
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
new file mode 100644
index 00000000000..ac76d991bd4
--- /dev/null
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
index 14a3f37b779..ddee8537454 100644
--- a/spec/requests/api/graphql/project/project_statistics_spec.rb
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'rendering namespace statistics' do
+describe 'rendering project statistics' do
include GraphqlHelpers
let(:project) { create(:project) }
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index fd24c443288..b89723b1e1a 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -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