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