From b3bc4ebc21037766c4239cbb8378246768a55cc5 Mon Sep 17 00:00:00 2001 From: Avielle Wolfe <awolfe@gitlab.com> Date: Tue, 14 Mar 2023 18:36:14 +0100 Subject: [PATCH] Add GraphQL fields for Catalog resources This commit adds `Ci::Catalog::ResourceType` and a `ciCatalogResources` field to `QueryType`. All fields are `alpha` because CI Catalog features are subject to rapid iteration. --- app/graphql/types/ci/catalog/resource_type.rb | 27 ++++++ app/models/ci/catalog/resource.rb | 2 + doc/api/graphql/reference/index.md | 55 +++++++++++++ ee/app/graphql/ee/types/query_type.rb | 7 ++ .../ci/catalog/resources_resolver.rb | 32 ++++++++ .../ci/catalog/resources_resolver_spec.rb | 47 +++++++++++ ee/spec/graphql/types/query_type_spec.rb | 1 + .../api/graphql/ci/catalog/resources_spec.rb | 82 +++++++++++++++++++ .../types/ci/catalog/resource_type_spec.rb | 18 ++++ spec/models/ci/catalog/resource_spec.rb | 6 ++ 10 files changed, 277 insertions(+) create mode 100644 app/graphql/types/ci/catalog/resource_type.rb create mode 100644 ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb create mode 100644 ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb create mode 100644 ee/spec/requests/api/graphql/ci/catalog/resources_spec.rb create mode 100644 spec/graphql/types/ci/catalog/resource_type_spec.rb diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb new file mode 100644 index 0000000000000..b5947826fa168 --- /dev/null +++ b/app/graphql/types/ci/catalog/resource_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Ci + module Catalog + # rubocop: disable Graphql/AuthorizeTypes + class ResourceType < BaseObject + graphql_name 'CiCatalogResource' + + connection_type_class(Types::CountableConnectionType) + + field :id, GraphQL::Types::ID, null: false, description: 'ID of the catalog resource.', + alpha: { milestone: '15.11' } + + field :name, GraphQL::Types::String, null: true, description: 'Name of the catalog resource.', + alpha: { milestone: '15.11' } + + field :description, GraphQL::Types::String, null: true, description: 'Description of the catalog resource.', + alpha: { milestone: '15.11' } + + field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.', + method: :avatar_path, alpha: { milestone: '15.11' } + end + # rubocop: enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index 40ec41de6feee..837f1352b4db7 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -13,6 +13,8 @@ class Resource < ::ApplicationRecord belongs_to :project scope :for_projects, ->(project_ids) { where(project_id: project_ids) } + + delegate :avatar_path, :description, :name, to: :project end end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 539f1c1d33f7f..58fe354093731 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -55,6 +55,26 @@ CI related settings that apply to the entire instance. Returns [`CiApplicationSettings`](#ciapplicationsettings). +### `Query.ciCatalogResources` + +CI Catalog resources visible to the current user. + +WARNING: +**Introduced** in 15.11. +This feature is in Alpha. It can be changed or removed at any time. + +Returns [`CiCatalogResourceConnection`](#cicatalogresourceconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="querycicatalogresourcesprojectpath"></a>`projectPath` | [`ID`](#id) | Project with the namespace catalog. | + ### `Query.ciConfig` Linted and processed contents of a CI config. @@ -7113,6 +7133,30 @@ The edge type for [`CiBuildNeed`](#cibuildneed). | <a id="cibuildneededgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="cibuildneededgenode"></a>`node` | [`CiBuildNeed`](#cibuildneed) | The item at the end of the edge. | +#### `CiCatalogResourceConnection` + +The connection type for [`CiCatalogResource`](#cicatalogresource). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cicatalogresourceconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. | +| <a id="cicatalogresourceconnectionedges"></a>`edges` | [`[CiCatalogResourceEdge]`](#cicatalogresourceedge) | A list of edges. | +| <a id="cicatalogresourceconnectionnodes"></a>`nodes` | [`[CiCatalogResource]`](#cicatalogresource) | A list of nodes. | +| <a id="cicatalogresourceconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `CiCatalogResourceEdge` + +The edge type for [`CiCatalogResource`](#cicatalogresource). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cicatalogresourceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="cicatalogresourceedgenode"></a>`node` | [`CiCatalogResource`](#cicatalogresource) | The item at the end of the edge. | + #### `CiConfigGroupConnection` The connection type for [`CiConfigGroup`](#ciconfiggroup). @@ -11677,6 +11721,17 @@ Represents the total number of issues and their weights for a particular day. | <a id="cibuildneedid"></a>`id` | [`ID!`](#id) | ID of the BuildNeed. | | <a id="cibuildneedname"></a>`name` | [`String`](#string) | Name of the job we need to complete. | +### `CiCatalogResource` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cicatalogresourcedescription"></a>`description` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is in Alpha. It can be changed or removed at any time. Description of the catalog resource. | +| <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is in Alpha. It can be changed or removed at any time. Icon for the catalog resource. | +| <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 15.11. This feature is in Alpha. It can be changed or removed at any time. ID of the catalog resource. | +| <a id="cicatalogresourcename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is in Alpha. It can be changed or removed at any time. Name of the catalog resource. | + ### `CiConfig` #### Fields diff --git a/ee/app/graphql/ee/types/query_type.rb b/ee/app/graphql/ee/types/query_type.rb index 7096067428350..ecf2f6caaaf54 100644 --- a/ee/app/graphql/ee/types/query_type.rb +++ b/ee/app/graphql/ee/types/query_type.rb @@ -68,6 +68,13 @@ module QueryType required: true, description: 'Global ID of the Vulnerability.' end + + field :ci_catalog_resources, + ::Types::Ci::Catalog::ResourceType.connection_type, + null: true, + alpha: { milestone: '15.11' }, + description: 'CI Catalog resources visible to the current user', + resolver: ::Resolvers::Ci::Catalog::ResourcesResolver end def vulnerability(id:) diff --git a/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb new file mode 100644 index 0000000000000..c233ab6e41244 --- /dev/null +++ b/ee/app/graphql/resolvers/ci/catalog/resources_resolver.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + module Catalog + class ResourcesResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesProject + + authorize :read_namespace_catalog + + type ::Types::Ci::Catalog::ResourceType.connection_type, null: true + + argument :project_path, GraphQL::Types::ID, + required: false, + description: 'Project with the namespace catalog.' + + def resolve(project_path:) + project = authorized_find!(project_path: project_path) + + ::Ci::Catalog::Listing.new(project.root_namespace, context[:current_user]).resources + end + + private + + def find_object(project_path:) + resolve_project(full_path: project_path) + end + end + end + end +end diff --git a/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb b/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb new file mode 100644 index 0000000000000..15888f9593fbd --- /dev/null +++ b/ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:namespace) { create(:group) } + let_it_be(:project_1) { create(:project, name: 'Component Repository 1', namespace: namespace) } + let_it_be(:project_2) { create(:project, name: 'Component Repository 2', namespace: namespace) } + let_it_be(:resource_1) { create(:catalog_resource, project: project_1) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:user) { create(:user) } + + describe '#resolve' do + it 'returns all CI Catalog resources visible to the current user in the namespace' do + stub_licensed_features(ci_namespace_catalog: true) + namespace.add_owner(user) + + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) + + expect(result.items.count).to be(2) + expect(result.items.pluck(:name)).to contain_exactly('Component Repository 1', 'Component Repository 2') + end + + context 'when the current user cannot read the namespace catalog' do + it 'raises ResourceNotAvailable' do + stub_licensed_features(ci_namespace_catalog: true) + namespace.add_guest(user) + + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) + + expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the namespace catalog feature is not available' do + it 'raises ResourceNotAvailable' do + namespace.add_owner(user) + + result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path }) + + expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end +end diff --git a/ee/spec/graphql/types/query_type_spec.rb b/ee/spec/graphql/types/query_type_spec.rb index 4ce45073515ba..4ec8b477e4463 100644 --- a/ee/spec/graphql/types/query_type_spec.rb +++ b/ee/spec/graphql/types/query_type_spec.rb @@ -7,6 +7,7 @@ specify do expected_ee_fields = [ + :ci_catalog_resources, :ci_minutes_usage, :current_license, :devops_adoption_enabled_namespaces, diff --git a/ee/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/ee/spec/requests/api/graphql/ci/catalog/resources_spec.rb new file mode 100644 index 0000000000000..95b2090be247c --- /dev/null +++ b/ee/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:namespace) { create(:group) } + let_it_be(:project_2) { create(:project, namespace: namespace) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:user) { create(:user) } + + let_it_be(:project_1) do + create( + :project, :with_avatar, + name: 'Component Repository', + description: 'A simple component', + namespace: namespace + ) + end + + let_it_be(:resource_1) { create(:catalog_resource, project: project_1) } + + let(:query) do + %( + query { + ciCatalogResources(projectPath: "#{project_2.full_path}") { + count + + nodes { + name + description + icon + } + } + } + ) + end + + context 'when the CI Namespace Catalog feature is available' do + before do + stub_licensed_features(ci_namespace_catalog: true) + end + + it 'returns all resources visible to the current user in the namespace' do + namespace.add_developer(user) + + post_graphql(query, current_user: user) + + resources_data = graphql_data['ciCatalogResources'] + expect(resources_data['count']).to be(2) + expect(resources_data['nodes'].count).to be(2) + + resource_1_data = resources_data['nodes'].first + expect(resource_1_data['name']).to eq('Component Repository') + expect(resource_1_data['description']).to eq('A simple component') + expect(resource_1_data['icon']).to eq(project_1.avatar_path) + end + + context 'when the current user does not have permission to read the namespace catalog' do + it 'returns an empty array' do + namespace.add_guest(user) + + post_graphql(query, current_user: user) + + resources_data = graphql_data['ciCatalogResources'] + expect(resources_data).to be_nil + end + end + end + + context 'when the CI Namespace Catalog feature is not available' do + it 'returns an empty array' do + namespace.add_developer(user) + + post_graphql(query, current_user: user) + + resources_data = graphql_data['ciCatalogResources'] + expect(resources_data).to be_nil + end + end +end diff --git a/spec/graphql/types/ci/catalog/resource_type_spec.rb b/spec/graphql/types/ci/catalog/resource_type_spec.rb new file mode 100644 index 0000000000000..d0bb45a4f1d43 --- /dev/null +++ b/spec/graphql/types/ci/catalog/resource_type_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::Catalog::ResourceType, feature_category: :pipeline_composition do + specify { expect(described_class.graphql_name).to eq('CiCatalogResource') } + + it 'exposes the expected fields' do + expected_fields = %i[ + id + name + description + icon + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb index 78656c743eddf..1a699fcd842ae 100644 --- a/spec/models/ci/catalog/resource_spec.rb +++ b/spec/models/ci/catalog/resource_spec.rb @@ -3,6 +3,12 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do + it { is_expected.to belong_to(:project) } + + it { is_expected.to delegate_method(:avatar_path).to(:project) } + it { is_expected.to delegate_method(:description).to(:project) } + it { is_expected.to delegate_method(:name).to(:project) } + describe '.for_projects' do it 'returns catalog resources for the given project IDs' do project = create(:project) -- GitLab