Commit 6aedb926 authored by Matthias Käppler's avatar Matthias Käppler

Merge branch '340364-agent-activity-events-graphql' into 'master'

Add GraphQL type and resolver for agent activity events

See merge request gitlab-org/gitlab!74651
parents ad6cfb71 a61b8130
# frozen_string_literal: true
module Resolvers
module Clusters
class AgentActivityEventsResolver < BaseResolver
type Types::Clusters::AgentActivityEventType, null: true
alias_method :agent, :object
delegate :project, to: :agent
def resolve(**args)
return ::Clusters::Agents::ActivityEvent.none unless can_view_activity_events?
agent.activity_events
end
private
def can_view_activity_events?
current_user.can?(:admin_cluster, project)
end
end
end
end
......@@ -28,7 +28,10 @@ module Resolvers
private
def preloads
{ tokens: :last_used_agent_tokens }
{
activity_events: { activity_events: [:user, agent_token: :agent] },
tokens: :last_used_agent_tokens
}
end
end
end
......
# frozen_string_literal: true
module Types
module Clusters
class AgentActivityEventType < BaseObject
graphql_name 'ClusterAgentActivityEvent'
authorize :admin_cluster
connection_type_class(Types::CountableConnectionType)
field :recorded_at,
Types::TimeType,
null: true,
description: 'Timestamp the event was recorded.'
field :kind,
GraphQL::Types::String,
null: true,
description: 'Type of event.'
field :level,
GraphQL::Types::String,
null: true,
description: 'Severity of the event.'
field :user,
Types::UserType,
null: true,
description: 'User associated with the event.'
field :agent_token,
Types::Clusters::AgentTokenType,
null: true,
description: 'Agent token associated with the event.'
end
end
end
......@@ -55,6 +55,12 @@ module Types
complexity: 5,
resolver: ::Resolvers::Kas::AgentConnectionsResolver
field :activity_events,
Types::Clusters::AgentActivityEventType.connection_type,
null: true,
description: 'Recent activity for the cluster agent.',
resolver: Resolvers::Clusters::AgentActivityEventsResolver
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
......
# frozen_string_literal: true
module Clusters
module Agents
class ActivityEventPolicy < BasePolicy
alias_method :event, :subject
delegate { event.agent }
end
end
end
......@@ -5457,6 +5457,30 @@ The edge type for [`CiStage`](#cistage).
| <a id="cistageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cistageedgenode"></a>`node` | [`CiStage`](#cistage) | The item at the end of the edge. |
#### `ClusterAgentActivityEventConnection`
The connection type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="clusteragentactivityeventconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="clusteragentactivityeventconnectionedges"></a>`edges` | [`[ClusterAgentActivityEventEdge]`](#clusteragentactivityeventedge) | A list of edges. |
| <a id="clusteragentactivityeventconnectionnodes"></a>`nodes` | [`[ClusterAgentActivityEvent]`](#clusteragentactivityevent) | A list of nodes. |
| <a id="clusteragentactivityeventconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ClusterAgentActivityEventEdge`
The edge type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="clusteragentactivityeventedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="clusteragentactivityeventedgenode"></a>`node` | [`ClusterAgentActivityEvent`](#clusteragentactivityevent) | The item at the end of the edge. |
#### `ClusterAgentConnection`
The connection type for [`ClusterAgent`](#clusteragent).
......@@ -8776,6 +8800,7 @@ GitLab CI/CD configuration template.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="clusteragentactivityevents"></a>`activityEvents` | [`ClusterAgentActivityEventConnection`](#clusteragentactivityeventconnection) | Recent activity for the cluster agent. (see [Connections](#connections)) |
| <a id="clusteragentconnections"></a>`connections` | [`ConnectedAgentConnection`](#connectedagentconnection) | Active connections for the cluster agent. (see [Connections](#connections)) |
| <a id="clusteragentcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp the cluster agent was created. |
| <a id="clusteragentcreatedbyuser"></a>`createdByUser` | [`UserCore`](#usercore) | User object, containing information about the person who created the agent. |
......@@ -8786,6 +8811,18 @@ GitLab CI/CD configuration template.
| <a id="clusteragentupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp the cluster agent was updated. |
| <a id="clusteragentwebpath"></a>`webPath` | [`String`](#string) | Web path of the cluster agent. |
### `ClusterAgentActivityEvent`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="clusteragentactivityeventagenttoken"></a>`agentToken` | [`ClusterAgentToken`](#clusteragenttoken) | Agent token associated with the event. |
| <a id="clusteragentactivityeventkind"></a>`kind` | [`String`](#string) | Type of event. |
| <a id="clusteragentactivityeventlevel"></a>`level` | [`String`](#string) | Severity of the event. |
| <a id="clusteragentactivityeventrecordedat"></a>`recordedAt` | [`Time`](#time) | Timestamp the event was recorded. |
| <a id="clusteragentactivityeventuser"></a>`user` | [`UserCore`](#usercore) | User associated with the event. |
### `ClusterAgentToken`
#### Fields
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Clusters::AgentActivityEventsResolver do
include GraphqlHelpers
it { expect(described_class.type).to eq(Types::Clusters::AgentActivityEventType) }
it { expect(described_class.null).to be_truthy }
describe '#resolve' do
let_it_be(:agent) { create(:cluster_agent) }
let(:user) { create(:user, maintainer_projects: [agent.project]) }
let(:ctx) { { current_user: user } }
let(:events) { double }
before do
allow(agent).to receive(:activity_events).and_return(events)
end
subject { resolve(described_class, obj: agent, ctx: ctx) }
it 'returns events associated with the agent' do
expect(subject).to eq(events)
end
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [agent.project]) }
it { is_expected.to be_empty }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ClusterAgentActivityEvent'] do
let(:fields) { %i[recorded_at kind level user agent_token] }
it { expect(described_class.graphql_name).to eq('ClusterAgentActivityEvent') }
it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ClusterAgent'] do
let(:fields) { %i[created_at created_by_user id name project updated_at tokens web_path connections] }
let(:fields) { %i[created_at created_by_user id name project updated_at tokens web_path connections activity_events] }
it { expect(described_class.graphql_name).to eq('ClusterAgent') }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Agents::ActivityEventPolicy do
let_it_be(:event) { create(:agent_activity_event) }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, event) }
let(:project) { event.agent.project }
describe 'rules' do
context 'developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
it { expect(policy).to be_disallowed :read_cluster }
end
context 'maintainer' do
before do
project.add_maintainer(user)
end
it { expect(policy).to be_allowed :admin_cluster }
it { expect(policy).to be_allowed :read_cluster }
end
end
end
......@@ -7,7 +7,7 @@ RSpec.describe 'Project.cluster_agents' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:agents) { create_list(:cluster_agent, 5, project: project) }
let_it_be(:agents) { create_list(:cluster_agent, 3, project: project) }
let(:first) { var('Int') }
let(:cluster_agents_fields) { nil }
......@@ -105,4 +105,37 @@ RSpec.describe 'Project.cluster_agents' do
})
end
end
context 'selecting activity events' do
let_it_be(:token) { create(:cluster_agent_token, agent: agents.first) }
let_it_be(:event) { create(:agent_activity_event, agent: agents.first, agent_token: token, user: current_user) }
let(:cluster_agents_fields) { [:id, query_nodes(:activity_events, of: 'ClusterAgentActivityEvent', max_depth: 2)] }
it 'retrieves activity event details' do
post_graphql(query, current_user: current_user)
response = graphql_data_at(:project, :cluster_agents, :nodes, :activity_events, :nodes).first
expect(response).to include({
'kind' => event.kind,
'level' => event.level,
'recordedAt' => event.recorded_at.iso8601,
'agentToken' => hash_including('name' => token.name),
'user' => hash_including('name' => current_user.name)
})
end
it 'preloads associations to prevent N+1 queries' do
user = create(:user)
token = create(:cluster_agent_token, agent: agents.second)
create(:agent_activity_event, agent: agents.second, agent_token: token, user: user)
post_graphql(query, current_user: current_user)
expect do
post_graphql(query, current_user: current_user)
end.to issue_same_number_of_queries_as { post_graphql(query, current_user: current_user, variables: [first.with(1)]) }
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