diff --git a/app/finders/clusters/agents_finder.rb b/app/finders/clusters/agents_finder.rb index d0b1240157c19de6a1c8f8941c762ae6c9baf6b8..8bc78300a44d04a245edefbfaccd99539db4c72b 100644 --- a/app/finders/clusters/agents_finder.rb +++ b/app/finders/clusters/agents_finder.rb @@ -4,8 +4,8 @@ module Clusters class AgentsFinder include FinderMethods - def initialize(project, current_user, params: {}) - @project = project + def initialize(object, current_user, params: {}) + @object = object @current_user = current_user @params = params end @@ -13,7 +13,7 @@ def initialize(project, current_user, params: {}) def execute return ::Clusters::Agent.none unless can_read_cluster_agents? - agents = project.cluster_agents + agents = object.cluster_agents agents = agents.with_name(params[:name]) if params[:name].present? agents.ordered_by_name @@ -21,10 +21,10 @@ def execute private - attr_reader :project, :current_user, :params + attr_reader :object, :current_user, :params def can_read_cluster_agents? - current_user.can?(:read_cluster, project) + current_user&.can?(:read_cluster, object) end end end diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb index 28618bef80751631d7d44fa657dce3f520509c32..81cea24194453d0cf3793533ac2753a3d4b96f2d 100644 --- a/app/graphql/resolvers/clusters/agents_resolver.rb +++ b/app/graphql/resolvers/clusters/agents_resolver.rb @@ -15,12 +15,10 @@ class AgentsResolver < BaseResolver description: 'Name of the cluster agent.' end - alias_method :project, :object - def resolve_with_lookahead(**args) apply_lookahead( ::Clusters::AgentsFinder - .new(project, current_user, params: args) + .new(object, current_user, params: args) .execute ) end diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index fb12ce7d29286ef9f212e7eeca26421abef47f69..3478bb697075c3ef96e50cebea5410538910e9c2 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -53,3 +53,5 @@ def to_ability_name end end end + +Clusters::Agent.prepend_mod_with('Clusters::Agent') diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cc24847c3f0bb56484b7475d24c413267d02c1f2..3fc104f873f4b034b238ad3fbaa4c43f19f0b8ca 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11694,6 +11694,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. | | <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. | | <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. | +| <a id="groupclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with projects in the group and its subgroups. (see [Connections](#connections)) | | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. | | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. | | <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. | @@ -12573,6 +12574,7 @@ A block of time for which a participant is on-call. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="instancesecuritydashboardclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with projects selected in the Instance Security Dashboard. (see [Connections](#connections)) | | <a id="instancesecuritydashboardvulnerabilitygrades"></a>`vulnerabilityGrades` | [`[VulnerableProjectsByGrade!]!`](#vulnerableprojectsbygrade) | Represents vulnerable project counts for each grade. | | <a id="instancesecuritydashboardvulnerabilityscanners"></a>`vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the vulnerabilities from projects selected in Instance Security Dashboard. (see [Connections](#connections)) | diff --git a/ee/app/graphql/ee/types/group_type.rb b/ee/app/graphql/ee/types/group_type.rb index 3445369121aa326765a30811404899b53aa059f0..d03ca4b72996b95e26ff0d47aff428a6dc940cb3 100644 --- a/ee/app/graphql/ee/types/group_type.rb +++ b/ee/app/graphql/ee/types/group_type.rb @@ -115,6 +115,13 @@ module GroupType description: 'Indicates whether to regularly prune stale group runners. Defaults to false.', method: :allow_stale_runner_pruning? + field :cluster_agents, + ::Types::Clusters::AgentType.connection_type, + extras: [:lookahead], + null: true, + description: 'Cluster agents associated with projects in the group and its subgroups.', + resolver: ::Resolvers::Clusters::AgentsResolver + def billable_members_count(requested_hosted_plan: nil) object.billable_members_count(requested_hosted_plan) end diff --git a/ee/app/graphql/types/instance_security_dashboard_type.rb b/ee/app/graphql/types/instance_security_dashboard_type.rb index bcec5f4f7efb968bcb178360ccab5217bc910f1b..8f81ce587dca560c8b2ddcf7184f188405250046 100644 --- a/ee/app/graphql/types/instance_security_dashboard_type.rb +++ b/ee/app/graphql/types/instance_security_dashboard_type.rb @@ -27,6 +27,13 @@ class InstanceSecurityDashboardType < BaseObject null: false, description: 'Represents vulnerable project counts for each grade.' + field :cluster_agents, + ::Types::Clusters::AgentType.connection_type, + extras: [:lookahead], + null: true, + description: 'Cluster agents associated with projects selected in the Instance Security Dashboard.', + resolver: ::Resolvers::Clusters::AgentsResolver + def vulnerability_grades ::Gitlab::Graphql::Aggregations::VulnerabilityStatistics::LazyAggregate.new( context, diff --git a/ee/app/models/ee/clusters/agent.rb b/ee/app/models/ee/clusters/agent.rb new file mode 100644 index 0000000000000000000000000000000000000000..aec39fb38f7ea01914a07947271834844dccf3ce --- /dev/null +++ b/ee/app/models/ee/clusters/agent.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module EE + module Clusters + module Agent + extend ActiveSupport::Concern + + prepended do + scope :for_projects, -> (projects) { where(project: projects) } + end + end + end +end diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 42d8c448f855e0aade0c7c58b5347f596d893dfc..9d72dda8d51cbc49ba80d42fb6d3771a6da6319c 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -630,6 +630,10 @@ def shared_externally? end end + def cluster_agents + ::Clusters::Agent.for_projects(all_projects) + end + private override :post_create_hook diff --git a/ee/app/models/instance_security_dashboard.rb b/ee/app/models/instance_security_dashboard.rb index 05c3f05f9cd7e60a19164e6d5193f3d9d4c925a7..e4cbb2fc52752e5b4a8463ebdc57e9b23f624f05 100644 --- a/ee/app/models/instance_security_dashboard.rb +++ b/ee/app/models/instance_security_dashboard.rb @@ -51,6 +51,12 @@ def has_projects? projects.count > 0 end + def cluster_agents + return Clusters::Agent.none if projects.empty? + + Clusters::Agent.for_projects(projects) + end + private attr_reader :project_ids, :user diff --git a/ee/spec/factories/vulnerabilities/findings.rb b/ee/spec/factories/vulnerabilities/findings.rb index 63296040587d5de9557a04d5cdbffa18c2238157..67c2c754968bd60fa108fb2088d237428a35b6a1 100644 --- a/ee/spec/factories/vulnerabilities/findings.rb +++ b/ee/spec/factories/vulnerabilities/findings.rb @@ -577,6 +577,7 @@ trait :with_cluster_image_scanning_scanning_metadata do transient do location_image { "alpine:3.7" } + agent_id { '46357' } end after(:build) do |finding, evaluator| @@ -594,7 +595,7 @@ "image": evaluator.location_image, "kubernetes_resource": { "cluster_id": "1", - "agent_id": "46357" + "agent_id": evaluator.agent_id } } finding.raw_metadata = { diff --git a/ee/spec/graphql/ee/types/group_type_spec.rb b/ee/spec/graphql/ee/types/group_type_spec.rb index 8420f541765dbd4f8da37fd273fac6259837a30c..8aa3f45292a884fa165c9b01a2bb5d6279620c1b 100644 --- a/ee/spec/graphql/ee/types/group_type_spec.rb +++ b/ee/spec/graphql/ee/types/group_type_spec.rb @@ -23,6 +23,7 @@ it { expect(described_class).to have_graphql_field(:external_audit_event_destinations) } it { expect(described_class).to have_graphql_field(:merge_request_violations) } it { expect(described_class).to have_graphql_field(:allow_stale_runner_pruning) } + it { expect(described_class).to have_graphql_field(:cluster_agents) } describe 'vulnerabilities' do let_it_be(:group) { create(:group) } diff --git a/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb b/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b621122c5e9e2e3c4b1534b8fc473749beb34b6d --- /dev/null +++ b/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Clusters::AgentsResolver do + include GraphqlHelpers + + specify do + expect(described_class).to have_nullable_graphql_type(Types::Clusters::AgentType.connection_type) + end + + specify do + expect(described_class.field_options).to include(extras: include(:lookahead)) + end + + describe '#resolve' do + let_it_be(:project) { create(:project) } + let_it_be(:maintainer) { create(:user, developer_projects: [project]) } + let_it_be(:reporter) { create(:user) } + + let_it_be(:agent_1) { create(:cluster_agent, project: project) } + let_it_be(:agent_2) { create(:cluster_agent, project: project) } + + before do + project.add_reporter(reporter) + end + + let(:ctx) { { current_user: current_user } } + let(:params) { {} } + + subject { resolve_agents(params) } + + context 'the current user has access to clusters' do + let(:current_user) { maintainer } + + it 'finds all agents' do + expect(subject).to contain_exactly(agent_1, agent_2) + end + end + + context 'the current user does not have access to clusters' do + let(:current_user) { reporter } + + it 'returns an empty result' do + expect(subject).to be_empty + end + end + end + + def resolve_agents(args = {}) + resolve(described_class, obj: project, ctx: ctx, lookahead: positive_lookahead, args: args) + end +end diff --git a/ee/spec/graphql/types/instance_security_dashboard_type_spec.rb b/ee/spec/graphql/types/instance_security_dashboard_type_spec.rb index d1c42d7a2c6344ee155dc712aaf249174b6f4731..d332c4442da8e442d2d264543d28839934b84639 100644 --- a/ee/spec/graphql/types/instance_security_dashboard_type_spec.rb +++ b/ee/spec/graphql/types/instance_security_dashboard_type_spec.rb @@ -8,7 +8,7 @@ let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } let(:fields) do - %i[projects vulnerability_scanners vulnerability_severities_count vulnerability_grades] + %i[projects vulnerability_scanners vulnerability_severities_count vulnerability_grades cluster_agents] end before do diff --git a/ee/spec/models/ee/clusters/agent_spec.rb b/ee/spec/models/ee/clusters/agent_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a41591bf56fa8755705da38060f077e7563a5754 --- /dev/null +++ b/ee/spec/models/ee/clusters/agent_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Agent do + it { is_expected.to include_module(EE::Clusters::Agent) } + + describe '.for_projects' do + let_it_be(:agent_1) { create(:cluster_agent) } + let_it_be(:agent_2) { create(:cluster_agent) } + let_it_be(:agent_3) { create(:cluster_agent) } + + it 'return agents for selected projects' do + expect(described_class.for_projects([agent_1.project, agent_3.project])).to contain_exactly(agent_1, agent_3) + end + end +end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 0d56b324a098092bce16a50c9b221b552a768f6e..52a0c42ec17e6083b0c4be47783aa02c5728646f 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -2524,4 +2524,22 @@ def webhook_headers .to contain_exactly(project_user.id, owner1.id, owner2.id, project2_user.id, bot_project_user.id, requesting_user.id, group_user.id) end end + + describe '#cluster_agents' do + let_it_be(:other_group) { create(:group) } + let_it_be(:other_project) { create(:project, namespace: other_group) } + + let_it_be(:root_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: root_group) } + let_it_be(:project_in_group) { create(:project, namespace: root_group) } + let_it_be(:project_in_subgroup) { create(:project, namespace: subgroup) } + + let_it_be(:cluster_agent_for_other_project) { create(:cluster_agent, project: other_project) } + let_it_be(:cluster_agent_for_project) { create(:cluster_agent, project: project_in_group) } + let_it_be(:cluster_agent_for_project_in_subgroup) { create(:cluster_agent, project: project_in_subgroup) } + + subject { root_group.cluster_agents } + + it { is_expected.to contain_exactly(cluster_agent_for_project, cluster_agent_for_project_in_subgroup) } + end end diff --git a/ee/spec/models/instance_security_dashboard_spec.rb b/ee/spec/models/instance_security_dashboard_spec.rb index 3664bf57ee27dbe9db99d708b766289e1eba26d8..72256141c600a857f2cc3d4c88036db886a5c16d 100644 --- a/ee/spec/models/instance_security_dashboard_spec.rb +++ b/ee/spec/models/instance_security_dashboard_spec.rb @@ -217,6 +217,23 @@ end end + describe '#cluster_agents' do + let_it_be(:cluster_agent_for_project_1) { create(:cluster_agent, project: project1) } + let_it_be(:cluster_agent_for_project_3) { create(:cluster_agent, project: project3) } + + context 'when instance security dashboard has projects added' do + it { expect(instance_dashboard.cluster_agents).to contain_exactly(cluster_agent_for_project_1) } + end + + context 'when instance security dashboard does not have any projects added' do + let_it_be(:other_user) { create(:user) } + + subject(:instance_dashboard) { described_class.new(other_user, project_ids: []) } + + it { expect(instance_dashboard.cluster_agents).to be_empty } + end + end + describe '#full_path' do let(:user) { create(:user) }