Commit 14639321 authored by Tiger's avatar Tiger

Add GraphQL endpoint for fetching a cluster agent by name

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40779
parent 506a5e74
......@@ -8,6 +8,8 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
scope :with_name, -> (name) { where(name: name) }
validates :name,
presence: true,
length: { maximum: 63 },
......
......@@ -11671,6 +11671,16 @@ type Project {
last: Int
): BoardConnection
"""
Find a single cluster agent by name
"""
clusterAgent(
"""
Name of the cluster agent
"""
name: String!
): ClusterAgent
"""
Cluster agents associated with the project
"""
......
......@@ -34762,6 +34762,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clusterAgent",
"description": "Find a single cluster agent by name",
"args": [
{
"name": "name",
"description": "Name of the cluster agent",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ClusterAgent",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clusterAgents",
"description": "Cluster agents associated with the project",
......@@ -1747,6 +1747,7 @@ Autogenerated return type of PipelineRetry
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `clusterAgent` | ClusterAgent | Find a single cluster agent by name |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation |
......
......@@ -11,12 +11,15 @@ module Clusters
def execute
return ::Clusters::Agent.none unless can_read_cluster_agents?
project.cluster_agents
agents = project.cluster_agents
agents = agents.with_name(params[:name]) if params[:name].present?
agents
end
private
attr_reader :project, :current_user
attr_reader :project, :current_user, :params
def can_read_cluster_agents?
project.feature_available?(:cluster_agents) && current_user.can?(:read_cluster, project)
......
......@@ -93,6 +93,12 @@ module EE
description: 'DAST Site Profiles associated with the project',
resolve: -> (obj, _args, _ctx) { obj.dast_site_profiles.with_dast_site }
field :cluster_agent,
::Types::Clusters::AgentType,
null: true,
description: 'Find a single cluster agent by name',
resolver: ::Resolvers::Clusters::AgentResolver.single
field :cluster_agents,
::Types::Clusters::AgentType.connection_type,
null: true,
......
# frozen_string_literal: true
module Resolvers
module Clusters
class AgentResolver < AgentsResolver
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the cluster agent'
end
end
end
......@@ -9,7 +9,7 @@ module Resolvers
def resolve(**args)
::Clusters::AgentsFinder
.new(project, context[:current_user])
.new(project, context[:current_user], params: args)
.execute
end
end
......
......@@ -30,5 +30,23 @@ RSpec.describe Clusters::AgentsFinder do
it { is_expected.to be_empty }
end
context 'filtering by name' do
let(:params) { Hash(name: name_param) }
subject { described_class.new(project, user, params: params).execute }
context 'name does not match' do
let(:name_param) { 'other-name' }
it { is_expected.to be_empty }
end
context 'name does match' do
let(:name_param) { matching_agent.name }
it { is_expected.to contain_exactly(matching_agent) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Clusters::AgentResolver do
it { expect(described_class).to be < Resolvers::Clusters::AgentsResolver }
describe 'arguments' do
subject { described_class.arguments[argument] }
describe 'name' do
let(:argument) { 'name' }
it do
expect(subject).to be_present
expect(subject.type.to_s).to eq('String!')
expect(subject.description).to be_present
end
end
end
end
......@@ -13,12 +13,14 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
let(:finder) { double(execute: :result) }
let(:project) { create(:project) }
let(:args) { Hash(key: 'value') }
let(:ctx) { Hash(current_user: user) }
subject { resolve(described_class, obj: project, ctx: { current_user: user }) }
subject { resolve(described_class, obj: project, args: args, ctx: ctx) }
it 'calls the agents finder' do
expect(::Clusters::AgentsFinder).to receive(:new)
.with(project, user).and_return(finder)
.with(project, user, params: args).and_return(finder)
expect(subject).to eq(:result)
end
......
......@@ -228,4 +228,44 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(agents.first['project']['id']).to eq(project.to_global_id.to_s)
end
end
describe 'cluster_agent' 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}") {
clusterAgent(name: "#{cluster_agent.name}") {
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
agent = subject.dig('data', 'project', 'clusterAgent')
expect(agent['id']).to eq(cluster_agent.to_global_id.to_s)
expect(agent['name']).to eq('agent-name')
expect(agent['createdAt']).to be_present
expect(agent['updatedAt']).to be_present
expect(agent['project']['id']).to eq(project.to_global_id.to_s)
end
end
end
......@@ -12,6 +12,17 @@ RSpec.describe Clusters::Agent do
it { is_expected.to validate_length_of(:name).is_at_most(63) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
describe 'scopes' do
describe '.with_name' do
let!(:matching_name) { create(:cluster_agent, name: 'matching-name') }
let!(:other_name) { create(:cluster_agent, name: 'other-name') }
subject { described_class.with_name(matching_name.name) }
it { is_expected.to contain_exactly(matching_name) }
end
end
describe 'validation' do
describe 'name validation' do
it 'rejects names that do not conform to RFC 1123', :aggregate_failures do
......
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