Commit c3afb319 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'emilyring-agent-graphql-query' into 'master'

Cluster Agent query for GraphQl

See merge request gitlab-org/gitlab!38833
parents 2fadc941 c797a78f
...@@ -1715,6 +1715,26 @@ type ClusterAgent { ...@@ -1715,6 +1715,26 @@ type ClusterAgent {
updatedAt: Time updatedAt: Time
} }
"""
The connection type for ClusterAgent.
"""
type ClusterAgentConnection {
"""
A list of edges.
"""
edges: [ClusterAgentEdge]
"""
A list of nodes.
"""
nodes: [ClusterAgent]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
""" """
Autogenerated input type of ClusterAgentDelete Autogenerated input type of ClusterAgentDelete
""" """
...@@ -1745,6 +1765,21 @@ type ClusterAgentDeletePayload { ...@@ -1745,6 +1765,21 @@ type ClusterAgentDeletePayload {
errors: [String!]! errors: [String!]!
} }
"""
An edge in a connection.
"""
type ClusterAgentEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ClusterAgent
}
type ClusterAgentToken { type ClusterAgentToken {
""" """
Cluster agent this token is associated with Cluster agent this token is associated with
...@@ -11318,6 +11353,31 @@ type Project { ...@@ -11318,6 +11353,31 @@ type Project {
last: Int last: Int
): BoardConnection ): BoardConnection
"""
Cluster agents associated with the project
"""
clusterAgents(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ClusterAgentConnection
""" """
Compliance frameworks associated with the project Compliance frameworks associated with the project
""" """
......
...@@ -4681,6 +4681,73 @@ ...@@ -4681,6 +4681,73 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ClusterAgentConnection",
"description": "The connection type for ClusterAgent.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ClusterAgentEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ClusterAgent",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "ClusterAgentDeleteInput", "name": "ClusterAgentDeleteInput",
...@@ -4769,6 +4836,51 @@ ...@@ -4769,6 +4836,51 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ClusterAgentEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ClusterAgent",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ClusterAgentToken", "name": "ClusterAgentToken",
...@@ -33855,6 +33967,59 @@ ...@@ -33855,6 +33967,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "clusterAgents",
"description": "Cluster agents associated with the project",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ClusterAgentConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "complianceFrameworks", "name": "complianceFrameworks",
"description": "Compliance frameworks associated with the project", "description": "Compliance frameworks associated with the project",
# frozen_string_literal: true
module Clusters
class AgentsFinder
def initialize(project, current_user)
@project = project
@current_user = current_user
end
def execute
return ::Clusters::Agent.none unless can_read_cluster_agents?
project.cluster_agents
end
private
attr_reader :project, :current_user
def can_read_cluster_agents?
project.feature_available?(:cluster_agents) && current_user.can?(:read_cluster, project)
end
end
end
...@@ -93,6 +93,12 @@ module EE ...@@ -93,6 +93,12 @@ module EE
description: 'DAST Site Profiles associated with the project', description: 'DAST Site Profiles associated with the project',
resolve: -> (obj, _args, _ctx) { obj.dast_site_profiles.with_dast_site } resolve: -> (obj, _args, _ctx) { obj.dast_site_profiles.with_dast_site }
field :cluster_agents,
::Types::Clusters::AgentType.connection_type,
null: true,
description: 'Cluster agents associated with the project',
resolver: ::Resolvers::Clusters::AgentsResolver
def self.requirements_available?(project, user) def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project) ::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project)
end end
......
# frozen_string_literal: true
module Resolvers
module Clusters
class AgentsResolver < BaseResolver
type Types::Clusters::AgentType, null: true
alias_method :project, :object
def resolve(**args)
::Clusters::AgentsFinder
.new(project, context[:current_user])
.execute
end
end
end
end
---
title: Add GraphQL endpoint for retrieving cluster agents for a project
merge_request: 38833
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::AgentsFinder do
describe '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user, maintainer_projects: [project]) }
let(:feature_available) { true }
let!(:matching_agent) { create(:cluster_agent, project: project) }
let!(:wrong_project) { create(:cluster_agent) }
subject { described_class.new(project, user).execute }
before do
stub_licensed_features(cluster_agents: feature_available)
end
it { is_expected.to contain_exactly(matching_agent) }
context 'feature is not available' do
let(:feature_available) { false }
it { is_expected.to be_empty }
end
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [project]) }
it { is_expected.to be_empty }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Clusters::AgentsResolver do
include GraphqlHelpers
it { expect(described_class.type).to eq(Types::Clusters::AgentType) }
it { expect(described_class.null).to be_truthy }
describe '#resolve' do
let_it_be(:user) { create(:user) }
let(:finder) { double(execute: :result) }
let(:project) { create(:project) }
subject { resolve(described_class, obj: project, ctx: { current_user: user }) }
it 'calls the agents finder' do
expect(::Clusters::AgentsFinder).to receive(:new)
.with(project, user).and_return(finder)
expect(subject).to eq(:result)
end
end
end
...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Project'] do
expected_fields = %w[ expected_fields = %w[
vulnerabilities sast_ci_configuration vulnerability_scanners requirement_states_count vulnerabilities sast_ci_configuration vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count
security_dashboard_path iterations security_dashboard_path iterations cluster_agents
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
...@@ -185,4 +185,47 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -185,4 +185,47 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(vulnerabilities.first['severity']).to eq('CRITICAL') expect(vulnerabilities.first['severity']).to eq('CRITICAL')
end end
end end
describe 'cluster_agents' do
let_it_be(:cluster_agent) { create(:cluster_agent, project: project, name: 'agent-name') }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
clusterAgents {
nodes {
id
name
createdAt
updatedAt
project {
id
}
}
}
}
}
)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
before do
stub_licensed_features(cluster_agents: true)
project.add_maintainer(user)
end
it 'returns associated cluster agents' do
agents = subject.dig('data', 'project', 'clusterAgents', 'nodes')
expect(agents.count).to be(1)
expect(agents.first['id']).to eq(cluster_agent.to_global_id.to_s)
expect(agents.first['name']).to eq('agent-name')
expect(agents.first['createdAt']).to be_present
expect(agents.first['updatedAt']).to be_present
expect(agents.first['project']['id']).to eq(project.to_global_id.to_s)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment