From 275073165ec43d90b3434c826d8a36295bb18478 Mon Sep 17 00:00:00 2001 From: Shinya Maeda <shinya@gitlab.com> Date: Thu, 18 Aug 2022 18:29:47 +0900 Subject: [PATCH] Add GraphQL query for deployment details This commit adds GraphQL query for fetching deployment details. Changelog: added --- app/graphql/resolvers/deployment_resolver.rb | 20 ++++++++ app/graphql/types/deployment_details_type.rb | 9 ++++ app/graphql/types/deployment_type.rb | 6 +++ app/graphql/types/project_type.rb | 6 +++ app/models/deployment.rb | 1 + doc/api/graphql/reference/index.md | 30 +++++++++++ .../resolvers/deployment_resolver_spec.rb | 28 ++++++++++ .../types/deployment_details_type_spec.rb | 17 +++++++ spec/models/deployment_spec.rb | 21 ++++++++ .../api/graphql/project/deployment_spec.rb | 51 +++++++++++++++++++ 10 files changed, 189 insertions(+) create mode 100644 app/graphql/resolvers/deployment_resolver.rb create mode 100644 app/graphql/types/deployment_details_type.rb create mode 100644 spec/graphql/resolvers/deployment_resolver_spec.rb create mode 100644 spec/graphql/types/deployment_details_type_spec.rb create mode 100644 spec/requests/api/graphql/project/deployment_spec.rb diff --git a/app/graphql/resolvers/deployment_resolver.rb b/app/graphql/resolvers/deployment_resolver.rb new file mode 100644 index 0000000000000..7d9ce0f023c04 --- /dev/null +++ b/app/graphql/resolvers/deployment_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + class DeploymentResolver < BaseResolver + argument :iid, + GraphQL::Types::ID, + required: true, + description: 'Project-level internal ID of the Deployment.' + + type Types::DeploymentType, null: true + + alias_method :project, :object + + def resolve(iid:) + return unless project.present? && project.is_a?(::Project) + + Deployment.for_iid(project, iid) + end + end +end diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb new file mode 100644 index 0000000000000..c1ea436bf3d27 --- /dev/null +++ b/app/graphql/types/deployment_details_type.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Types + class DeploymentDetailsType < DeploymentType + graphql_name 'DeploymentDetails' + description 'The details of the deployment' + authorize :read_deployment + end +end diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb index 863699a9cc5d4..1b61001fc10a4 100644 --- a/app/graphql/types/deployment_type.rb +++ b/app/graphql/types/deployment_type.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true module Types + # If you're considering to add a new field in DeploymentType, please follow this guideline: + # - If the field is preloadable in batch, define it in DeploymentType. + # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that + # fetching the specific field for multiple deployments doesn't cause N+1 query problem. + # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType. + # This type can be only fetched for a single deployment, so you don't need to take care of the preloading. class DeploymentType < BaseObject graphql_name 'Deployment' description 'The deployment of an environment' diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index ecc6c9d78119b..436fec4b8efce 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -179,6 +179,12 @@ class ProjectType < BaseObject description: 'A single environment of the project.', resolver: Resolvers::EnvironmentsResolver.single + field :deployment, + Types::DeploymentDetailsType, + null: true, + description: 'Details of the deployment of the project.', + resolver: Resolvers::DeploymentResolver.single + field :issue, Types::IssueType, null: true, diff --git a/app/models/deployment.rb b/app/models/deployment.rb index dfc9f3f8fcad3..325754f001abc 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -36,6 +36,7 @@ class Deployment < ApplicationRecord delegate :name, to: :environment, prefix: true delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true + scope :for_iid, -> (project, iid) { where(project: project, iid: iid) } scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment_name, -> (project, name) do where('deployments.environment_id = (?)', diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1e5472f43311e..2a6607839bb82 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10991,6 +10991,24 @@ The deployment of an environment. | <a id="deploymenttag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. | | <a id="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. | +### `DeploymentDetails` + +The details of the deployment. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="deploymentdetailscreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. | +| <a id="deploymentdetailsfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. | +| <a id="deploymentdetailsid"></a>`id` | [`ID`](#id) | Global ID of the deployment. | +| <a id="deploymentdetailsiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. | +| <a id="deploymentdetailsref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. | +| <a id="deploymentdetailssha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. | +| <a id="deploymentdetailsstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. | +| <a id="deploymentdetailstag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. | +| <a id="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. | + ### `Design` A single design. @@ -15871,6 +15889,18 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectdastsitevalidationsnormalizedtargeturls"></a>`normalizedTargetUrls` | [`[String!]`](#string) | Normalized URL of the target to be scanned. | | <a id="projectdastsitevalidationsstatus"></a>`status` | [`DastSiteValidationStatusEnum`](#dastsitevalidationstatusenum) | Status of the site validation. | +##### `Project.deployment` + +Details of the deployment of the project. + +Returns [`DeploymentDetails`](#deploymentdetails). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectdeploymentiid"></a>`iid` | [`ID!`](#id) | Project-level internal ID of the Deployment. | + ##### `Project.environment` A single environment of the project. diff --git a/spec/graphql/resolvers/deployment_resolver_spec.rb b/spec/graphql/resolvers/deployment_resolver_spec.rb new file mode 100644 index 0000000000000..9231edefddccb --- /dev/null +++ b/spec/graphql/resolvers/deployment_resolver_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::DeploymentResolver do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :private) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + + let(:current_user) { developer } + + describe '#resolve' do + it 'finds the deployment' do + expect(resolve_deployments(iid: deployment.iid)).to contain_exactly(deployment) + end + + it 'does not find the deployment if the IID does not match' do + expect(resolve_deployments(iid: non_existing_record_id)).to be_empty + end + end + + def resolve_deployments(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, args: args, ctx: context) + end +end diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb new file mode 100644 index 0000000000000..58756798ffb63 --- /dev/null +++ b/spec/graphql/types/deployment_details_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DeploymentDetails'] do + specify { expect(described_class.graphql_name).to eq('DeploymentDetails') } + + it 'has the expected fields' do + expected_fields = %w[ + id iid ref tag sha created_at updated_at finished_at status + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + specify { expect(described_class).to require_graphql_authorizations(:read_deployment) } +end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 0a4ee73f3d32a..c5ce18739aa65 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -74,6 +74,27 @@ end end + describe '.for_iid' do + subject { described_class.for_iid(project, iid) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:deployment) { create(:deployment, project: project) } + + let(:iid) { deployment.iid } + + it 'finds the deployment' do + is_expected.to contain_exactly(deployment) + end + + context 'when iid does not match' do + let(:iid) { non_existing_record_id } + + it 'does not find the deployment' do + is_expected.to be_empty + end + end + end + describe '.for_environment_name' do subject { described_class.for_environment_name(project, environment_name) } diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb new file mode 100644 index 0000000000000..e5ef7bcafbfd9 --- /dev/null +++ b/spec/requests/api/graphql/project/deployment_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Deployment query' do + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, environment: environment, project: project) } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + deployment(iid: #{deployment.iid}) { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + ) + end + + it 'returns the deployment of the project' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data['iid']).to eq(deployment.iid.to_s) + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data).to be_nil + end + end +end -- GitLab