diff --git a/app/graphql/resolvers/projects/plan_limits_resolver.rb b/app/graphql/resolvers/projects/plan_limits_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..f47a8f3c65e90aa9fdf9b0be997332acc960882e --- /dev/null +++ b/app/graphql/resolvers/projects/plan_limits_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class PlanLimitsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::ProjectPlanLimitsType, null: false + + authorize :read_project + + def resolve + authorize!(object) + + schedule_allowed = Ability.allowed?(current_user, :read_ci_pipeline_schedules_plan_limit, object) + + { + ci_pipeline_schedules: schedule_allowed ? object.actual_limits.ci_pipeline_schedules : nil + } + end + end + end +end diff --git a/app/graphql/types/project_plan_limits_type.rb b/app/graphql/types/project_plan_limits_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..ed7e5bee9270716feeae95d4dec35e525a924461 --- /dev/null +++ b/app/graphql/types/project_plan_limits_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request + class ProjectPlanLimitsType < BaseObject + graphql_name 'ProjectPlanLimits' + description 'Plan limits for the current project.' + + field :ci_pipeline_schedules, GraphQL::Types::Int, null: true, + description: 'Maximum number of pipeline schedules allowed per project.' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 5dbf20cc7252e9123a9da3f3e187a5f2b22c1f32..db850f60910450546df646bf6f4ff2dc6db2ba69 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -698,6 +698,12 @@ class ProjectType < BaseObject calls_gitaly: true, alpha: { milestone: '16.9' } + field :project_plan_limits, Types::ProjectPlanLimitsType, + resolver: Resolvers::Projects::PlanLimitsResolver, + description: 'Plan limits for the current project.', + alpha: { milestone: '16.9' }, + null: true + def protectable_branches ProtectableDropdown.new(project, :branches).protectable_ref_names end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 01c9d94e0e69cb9f48e10e097e4a6b1dcd53ce9a..a686a94b7c66d93bdd1febb57fb6e9af04bbfa56 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -992,6 +992,10 @@ class ProjectPolicy < BasePolicy rule { ~private_project & guest & external_user }.enable :read_container_image + rule { can?(:create_pipeline_schedule) }.policy do + enable :read_ci_pipeline_schedules_plan_limit + end + private def user_is_user? diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index af136cad7b367a35bd37ffebae517bb785b1bab0..7881f5afc33ec5372cb1e5a5479e89d3ab131545 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -24470,6 +24470,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectprintingmergerequestlinkenabled"></a>`printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. | | <a id="projectproductanalyticsinstrumentationkey"></a>`productAnalyticsInstrumentationKey` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.0. **Status**: Experiment. Product Analytics instrumentation key assigned to the project. | | <a id="projectproductanalyticsstate"></a>`productAnalyticsState` **{warning-solid}** | [`ProductAnalyticsState`](#productanalyticsstate) | **Introduced** in 15.10. **Status**: Experiment. Current state of the product analytics stack for this project.Can only be called for one project in a single request. | +| <a id="projectprojectplanlimits"></a>`projectPlanLimits` **{warning-solid}** | [`ProjectPlanLimits`](#projectplanlimits) | **Introduced** in 16.9. **Status**: Experiment. Plan limits for the current project. | | <a id="projectprotectablebranches"></a>`protectableBranches` **{warning-solid}** | [`[String!]`](#string) | **Introduced** in 16.9. **Status**: Experiment. List of unprotected branches, ignoring any wildcard branch rules. | | <a id="projectpublicjobs"></a>`publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. | | <a id="projectpushrules"></a>`pushRules` | [`PushRules`](#pushrules) | Project's push rules settings. | @@ -26128,6 +26129,16 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction). | <a id="projectpermissionsupdatewiki"></a>`updateWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_wiki` on this resource. | | <a id="projectpermissionsuploadfile"></a>`uploadFile` | [`Boolean!`](#boolean) | If `true`, the user can perform `upload_file` on this resource. | +### `ProjectPlanLimits` + +Plan limits for the current project. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectplanlimitscipipelineschedules"></a>`ciPipelineSchedules` | [`Int`](#int) | Maximum number of pipeline schedules allowed per project. | + ### `ProjectRepositoryRegistry` Represents the Geo replication and verification state of a project repository. diff --git a/spec/graphql/resolvers/projects/plan_limits_resolver_spec.rb b/spec/graphql/resolvers/projects/plan_limits_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2476964b94c49b0347334c4301d947c89f4eda38 --- /dev/null +++ b/spec/graphql/resolvers/projects/plan_limits_resolver_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Projects::PlanLimitsResolver, feature_category: :api do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let(:project) { build(:project, :repository) } + + describe 'Pipeline schedule limits' do + before do + project.add_owner(user) + end + + it 'gets the current limits for pipeline schedules' do + limits = resolve_plan_limits + + expect(limits).to include({ ci_pipeline_schedules: project.actual_limits.ci_pipeline_schedules }) + end + end + + describe 'Pipeline schedule limits without authorization' do + it 'returns a ResourceNotAvailable error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolve_plan_limits + end + end + + it 'returns null when a user is not allowed to see the limit but allowed to see project' do + project.add_reporter(user) + + limits = resolve_plan_limits + + expect(limits).to include({ ci_pipeline_schedules: nil }) + end + end + + def resolve_plan_limits(args: {}) + resolve(described_class, obj: project, ctx: { current_user: user }, args: args) + end +end diff --git a/spec/graphql/types/project_plan_limits_type_spec.rb b/spec/graphql/types/project_plan_limits_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac9af421a518f568dcb2ed9d6d88ceb678537d6f --- /dev/null +++ b/spec/graphql/types/project_plan_limits_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::ProjectPlanLimitsType, feature_category: :api do + include GraphqlHelpers + + specify { expect(described_class.graphql_name).to eq('ProjectPlanLimits') } + + it 'exposes the expected fields' do + expected_fields = %i[ci_pipeline_schedules] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 9f4bf4f6b369c873f8eb21b58a119c081802fd2e..69060ee4e1ebaf1e6e122530c5cbf6c98b1ecebc 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -216,6 +216,7 @@ def set_access_level(access_level) expect_allowed(:update_pipeline) expect_allowed(:cancel_pipeline) expect_allowed(:create_pipeline_schedule) + expect_allowed(:read_ci_pipeline_schedules_plan_limit) end end @@ -228,6 +229,7 @@ def set_access_level(access_level) expect_disallowed(:cancel_pipeline) expect_disallowed(:destroy_pipeline) expect_disallowed(:create_pipeline_schedule) + expect_disallowed(:read_ci_pipeline_schedules_plan_limit) end end