Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-23 06:09:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-23 06:09:49 +0300
commit163b6c3c80c2aad98d0eedb3ccd76a72c5e72771 (patch)
tree68f939d4ea170754d063979501548259560b0236
parent5d3bcd82b5d6a8567c3c0b1d1620fdd26a4513c5 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/graphql/resolvers/clusters/agent_tokens_resolver.rb25
-rw-r--r--app/graphql/resolvers/clusters/agents_resolver.rb35
-rw-r--r--app/graphql/resolvers/kas/agent_configurations_resolver.rb32
-rw-r--r--app/graphql/resolvers/kas/agent_connections_resolver.rb41
-rw-r--r--app/graphql/types/clusters/agent_token_type.rb52
-rw-r--r--app/graphql/types/clusters/agent_type.rb67
-rw-r--r--app/graphql/types/kas/agent_configuration_type.rb17
-rw-r--r--app/graphql/types/kas/agent_connection_type.rb32
-rw-r--r--app/graphql/types/kas/agent_metadata_type.rb33
-rw-r--r--app/graphql/types/project_type.rb19
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/instance_configuration.rb7
-rw-r--r--app/policies/clusters/agent_policy.rb9
-rw-r--r--app/policies/clusters/agent_token_policy.rb9
-rw-r--r--db/migrate/20210921032008_add_suggest_pipeline_enabled_to_application_settings.rb7
-rw-r--r--db/schema_migrations/202109210320081
-rw-r--r--db/structure.sql1
-rw-r--r--doc/integration/saml.md9
-rw-r--r--qa/qa/resource/base.rb27
-rw-r--r--spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb32
-rw-r--r--spec/graphql/resolvers/clusters/agents_resolver_spec.rb77
-rw-r--r--spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb48
-rw-r--r--spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb66
-rw-r--r--spec/graphql/types/clusters/agent_token_type_spec.rb13
-rw-r--r--spec/graphql/types/clusters/agent_type_spec.rb13
-rw-r--r--spec/graphql/types/kas/agent_configuration_type_spec.rb11
-rw-r--r--spec/graphql/types/kas/agent_connection_type_spec.rb22
-rw-r--r--spec/graphql/types/kas/agent_metadata_type_spec.rb13
-rw-r--r--spec/graphql/types/project_type_spec.rb134
-rw-r--r--spec/models/instance_configuration_spec.rb17
-rw-r--r--spec/policies/clusters/agent_policy_spec.rb28
-rw-r--r--spec/policies/clusters/agent_token_policy_spec.rb31
32 files changed, 908 insertions, 21 deletions
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
new file mode 100644
index 00000000000..5ae19700fd5
--- /dev/null
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Clusters
+ class AgentTokensResolver < BaseResolver
+ type Types::Clusters::AgentTokenType, null: true
+
+ alias_method :agent, :object
+
+ delegate :project, to: :agent
+
+ def resolve(**args)
+ return ::Clusters::AgentToken.none unless can_read_agent_tokens?
+
+ agent.last_used_agent_tokens
+ end
+
+ private
+
+ def can_read_agent_tokens?
+ current_user.can?(:admin_cluster, project)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb
new file mode 100644
index 00000000000..9b8cea52e3b
--- /dev/null
+++ b/app/graphql/resolvers/clusters/agents_resolver.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Clusters
+ class AgentsResolver < BaseResolver
+ include LooksAhead
+
+ type Types::Clusters::AgentType.connection_type, null: true
+
+ extras [:lookahead]
+
+ when_single do
+ argument :name, GraphQL::Types::String,
+ required: true,
+ 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)
+ .execute
+ )
+ end
+
+ private
+
+ def preloads
+ { tokens: :last_used_agent_tokens }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/kas/agent_configurations_resolver.rb b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
new file mode 100644
index 00000000000..238dae0bf12
--- /dev/null
+++ b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Kas
+ class AgentConfigurationsResolver < BaseResolver
+ type Types::Kas::AgentConfigurationType, null: true
+
+ # Calls Gitaly via KAS
+ calls_gitaly!
+
+ alias_method :project, :object
+
+ def resolve
+ return [] unless can_read_agent_configuration?
+
+ kas_client.list_agent_config_files(project: project)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
+ end
+
+ private
+
+ def can_read_agent_configuration?
+ current_user.can?(:admin_cluster, project)
+ end
+
+ def kas_client
+ @kas_client ||= Gitlab::Kas::Client.new
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/kas/agent_connections_resolver.rb b/app/graphql/resolvers/kas/agent_connections_resolver.rb
new file mode 100644
index 00000000000..8b7c4003598
--- /dev/null
+++ b/app/graphql/resolvers/kas/agent_connections_resolver.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Kas
+ class AgentConnectionsResolver < BaseResolver
+ type Types::Kas::AgentConnectionType, null: true
+
+ alias_method :agent, :object
+
+ delegate :project, to: :agent
+
+ def resolve
+ return [] unless can_read_connected_agents?
+
+ BatchLoader::GraphQL.for(agent.id).batch(key: project, default_value: []) do |agent_ids, loader|
+ agents = get_connected_agents.group_by(&:agent_id).slice(*agent_ids)
+
+ agents.each do |agent_id, connections|
+ loader.call(agent_id, connections)
+ end
+ end
+ end
+
+ private
+
+ def can_read_connected_agents?
+ current_user.can?(:admin_cluster, project)
+ end
+
+ def get_connected_agents
+ kas_client.get_connected_agents(project: project)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
+ end
+
+ def kas_client
+ @kas_client ||= Gitlab::Kas::Client.new
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/clusters/agent_token_type.rb b/app/graphql/types/clusters/agent_token_type.rb
new file mode 100644
index 00000000000..94c5fc46a5d
--- /dev/null
+++ b/app/graphql/types/clusters/agent_token_type.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Types
+ module Clusters
+ class AgentTokenType < BaseObject
+ graphql_name 'ClusterAgentToken'
+
+ authorize :admin_cluster
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :cluster_agent,
+ Types::Clusters::AgentType,
+ description: 'Cluster agent this token is associated with.',
+ null: true
+
+ field :created_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the token was created.'
+
+ field :created_by_user,
+ Types::UserType,
+ null: true,
+ description: 'User who created the token.'
+
+ field :description,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Description of the token.'
+
+ field :last_used_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the token was last used.'
+
+ field :id,
+ ::Types::GlobalIDType[::Clusters::AgentToken],
+ null: false,
+ description: 'Global ID of the token.'
+
+ field :name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name given to the token.'
+
+ def cluster_agent
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(::Clusters::Agent, object.agent_id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb
new file mode 100644
index 00000000000..ce748f6e8ae
--- /dev/null
+++ b/app/graphql/types/clusters/agent_type.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Types
+ module Clusters
+ class AgentType < BaseObject
+ graphql_name 'ClusterAgent'
+
+ authorize :admin_cluster
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :created_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the cluster agent was created.'
+
+ field :created_by_user,
+ Types::UserType,
+ null: true,
+ description: 'User object, containing information about the person who created the agent.'
+
+ field :id, GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the cluster agent.'
+
+ field :name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the cluster agent.'
+
+ field :project, Types::ProjectType,
+ description: 'Project this cluster agent is associated with.',
+ null: true,
+ authorize: :read_project
+
+ field :tokens, Types::Clusters::AgentTokenType.connection_type,
+ description: 'Tokens associated with the cluster agent.',
+ null: true,
+ resolver: ::Resolvers::Clusters::AgentTokensResolver
+
+ field :updated_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the cluster agent was updated.'
+
+ field :web_path,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Web path of the cluster agent.'
+
+ field :connections,
+ Types::Kas::AgentConnectionType.connection_type,
+ null: true,
+ description: 'Active connections for the cluster agent',
+ complexity: 5,
+ resolver: ::Resolvers::Kas::AgentConnectionsResolver
+
+ def project
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
+ end
+
+ def web_path
+ ::Gitlab::Routing.url_helpers.project_cluster_agent_path(object.project, object.name)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/kas/agent_configuration_type.rb b/app/graphql/types/kas/agent_configuration_type.rb
new file mode 100644
index 00000000000..397a5739671
--- /dev/null
+++ b/app/graphql/types/kas/agent_configuration_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentConfigurationType < BaseObject
+ graphql_name 'AgentConfiguration'
+ description 'Configuration details for an Agent'
+
+ field :agent_name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the agent.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/kas/agent_connection_type.rb b/app/graphql/types/kas/agent_connection_type.rb
new file mode 100644
index 00000000000..9c6321bece9
--- /dev/null
+++ b/app/graphql/types/kas/agent_connection_type.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentConnectionType < BaseObject
+ graphql_name 'ConnectedAgent'
+ description 'Connection details for an Agent'
+
+ field :connected_at,
+ Types::TimeType,
+ null: true,
+ description: 'When the connection was established.'
+
+ field :connection_id,
+ GraphQL::Types::BigInt,
+ null: true,
+ description: 'ID of the connection.'
+
+ field :metadata,
+ Types::Kas::AgentMetadataType,
+ method: :agent_meta,
+ null: true,
+ description: 'Information about the Agent.'
+
+ def connected_at
+ Time.at(object.connected_at.seconds)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/kas/agent_metadata_type.rb b/app/graphql/types/kas/agent_metadata_type.rb
new file mode 100644
index 00000000000..4a3bb09b9e1
--- /dev/null
+++ b/app/graphql/types/kas/agent_metadata_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentMetadataType < BaseObject
+ graphql_name 'AgentMetadata'
+ description 'Information about a connected Agent'
+
+ field :version,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Agent version tag.'
+
+ field :commit,
+ GraphQL::Types::String,
+ method: :commit_id,
+ null: true,
+ description: 'Agent version commit.'
+
+ field :pod_namespace,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace of the pod running the Agent.'
+
+ field :pod_name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the pod running the Agent.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index aef46a05a2f..72529d59413 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -361,6 +361,25 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
+ field :agent_configurations,
+ ::Types::Kas::AgentConfigurationType.connection_type,
+ null: true,
+ description: 'Agent configurations defined by the project',
+ resolver: ::Resolvers::Kas::AgentConfigurationsResolver
+
+ field :cluster_agent,
+ ::Types::Clusters::AgentType,
+ null: true,
+ description: 'Find a single cluster agent by name.',
+ resolver: ::Resolvers::Clusters::AgentsResolver.single
+
+ field :cluster_agents,
+ ::Types::Clusters::AgentType.connection_type,
+ extras: [:lookahead],
+ null: true,
+ description: 'Cluster agents associated with the project.',
+ resolver: ::Resolvers::Clusters::AgentsResolver
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f43bbb157bb..4363480cd18 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -9,7 +9,6 @@ class ApplicationSetting < ApplicationRecord
include Sanitizable
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
- ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 1019d845a8c..0bf9e805aa8 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -22,7 +22,12 @@ class InstanceConfiguration
private
def ssh_algorithms_hashes
- SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
+ SSH_ALGORITHMS.select { |algo| ssh_algorithm_enabled?(algo) }.map { |algo| ssh_algorithm_hashes(algo) }.compact
+ end
+
+ def ssh_algorithm_enabled?(algorithm)
+ algorithm_key_restriction = application_settings["#{algorithm.downcase}_key_restriction"]
+ algorithm_key_restriction.nil? || algorithm_key_restriction != ApplicationSetting::FORBIDDEN_KEY_VALUE
end
def host
diff --git a/app/policies/clusters/agent_policy.rb b/app/policies/clusters/agent_policy.rb
new file mode 100644
index 00000000000..25e78c84802
--- /dev/null
+++ b/app/policies/clusters/agent_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentPolicy < BasePolicy
+ alias_method :cluster_agent, :subject
+
+ delegate { cluster_agent.project }
+ end
+end
diff --git a/app/policies/clusters/agent_token_policy.rb b/app/policies/clusters/agent_token_policy.rb
new file mode 100644
index 00000000000..e876ecfac26
--- /dev/null
+++ b/app/policies/clusters/agent_token_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentTokenPolicy < BasePolicy
+ alias_method :token, :subject
+
+ delegate { token.agent }
+ end
+end
diff --git a/db/migrate/20210921032008_add_suggest_pipeline_enabled_to_application_settings.rb b/db/migrate/20210921032008_add_suggest_pipeline_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..5ac12eccc7d
--- /dev/null
+++ b/db/migrate/20210921032008_add_suggest_pipeline_enabled_to_application_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSuggestPipelineEnabledToApplicationSettings < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :application_settings, :suggest_pipeline_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/db/schema_migrations/20210921032008 b/db/schema_migrations/20210921032008
new file mode 100644
index 00000000000..058a4366e4a
--- /dev/null
+++ b/db/schema_migrations/20210921032008
@@ -0,0 +1 @@
+262127539fc16715a56e2cf7426f0f8d24922e26847a01a0a15552d71cd148f8 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 205c1febe9a..ed96c443510 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10347,6 +10347,7 @@ CREATE TABLE application_settings (
sidekiq_job_limiter_mode smallint DEFAULT 1 NOT NULL,
sidekiq_job_limiter_compression_threshold_bytes integer DEFAULT 100000 NOT NULL,
sidekiq_job_limiter_limit_bytes integer DEFAULT 0 NOT NULL,
+ suggest_pipeline_enabled boolean DEFAULT true NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 9f5a603eb12..d84fcf69a0c 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -476,11 +476,10 @@ args: {
#### Set a username
By default, the email in the SAML response is used to automatically generate the
-user's GitLab username. If you'd like to set another attribute as the username,
-assign it to the `nickname` OmniAuth `info` hash attribute.
+user's GitLab username.
-For example, if you want to set the `username` attribute in your SAML Response to the username
-in GitLab, use the following setting:
+If you'd like to set another attribute as the username, assign it to the `nickname` OmniAuth `info`
+hash attribute, and add the following setting to your configuration file:
```yaml
args: {
@@ -493,6 +492,8 @@ args: {
}
```
+This also sets the `username` attribute in your SAML Response to the username in GitLab.
+
### `allowed_clock_drift`
The clock of the Identity Provider may drift slightly ahead of your system clocks.
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 2848e3ba7d2..a7243b7ebc2 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -100,7 +100,9 @@ module QA
attr_writer(name)
define_method(name) do
- instance_variable_get("@#{name}") || instance_variable_set("@#{name}", populate_attribute(name, block))
+ return instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}")
+
+ instance_variable_set("@#{name}", attribute_value(name, block))
end
end
@@ -121,9 +123,7 @@ module QA
return self unless api_resource
all_attributes.each do |attribute_name|
- api_value = api_resource[attribute_name]
-
- instance_variable_set("@#{attribute_name}", api_value) if api_value
+ instance_variable_set("@#{attribute_name}", api_resource[attribute_name]) if api_resource.key?(attribute_name)
end
self
@@ -160,20 +160,17 @@ module QA
private
- def populate_attribute(name, block)
- value = attribute_value(name, block)
-
- raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
-
- value
- end
-
def attribute_value(name, block)
- api_value = api_resource&.dig(name)
+ no_api_value = !api_resource&.key?(name)
+ raise NoValueError, "No value was computed for #{name} of #{self.class.name}." if no_api_value && !block
- log_having_both_api_result_and_block(name, api_value) if api_value && block
+ unless no_api_value
+ api_value = api_resource[name]
+ log_having_both_api_result_and_block(name, api_value) if block
+ return api_value
+ end
- api_value || (block && instance_exec(&block))
+ instance_exec(&block)
end
# Get all defined attributes across all parents
diff --git a/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
new file mode 100644
index 00000000000..6b8b88928d8
--- /dev/null
+++ b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Clusters::AgentTokensResolver do
+ include GraphqlHelpers
+
+ it { expect(described_class.type).to eq(Types::Clusters::AgentTokenType) }
+ it { expect(described_class.null).to be_truthy }
+
+ describe '#resolve' do
+ let(:agent) { create(:cluster_agent) }
+ let(:user) { create(:user, maintainer_projects: [agent.project]) }
+ let(:ctx) { Hash(current_user: user) }
+
+ let!(:matching_token1) { create(:cluster_agent_token, agent: agent, last_used_at: 5.days.ago) }
+ let!(:matching_token2) { create(:cluster_agent_token, agent: agent, last_used_at: 2.days.ago) }
+ let!(:other_token) { create(:cluster_agent_token) }
+
+ subject { resolve(described_class, obj: agent, ctx: ctx) }
+
+ it 'returns tokens associated with the agent, ordered by last_used_at' do
+ expect(subject).to eq([matching_token2, matching_token1])
+ 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
diff --git a/spec/graphql/resolvers/clusters/agents_resolver_spec.rb b/spec/graphql/resolvers/clusters/agents_resolver_spec.rb
new file mode 100644
index 00000000000..70f40748e1d
--- /dev/null
+++ b/spec/graphql/resolvers/clusters/agents_resolver_spec.rb
@@ -0,0 +1,77 @@
+# 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, maintainer_projects: [project]) }
+ let_it_be(:developer) { create(:user, developer_projects: [project]) }
+ let_it_be(:agents) { create_list(:cluster_agent, 2, project: project) }
+
+ let(:ctx) { { current_user: current_user } }
+
+ subject { resolve_agents }
+
+ context 'the current user has access to clusters' do
+ let(:current_user) { maintainer }
+
+ it 'finds all agents' do
+ expect(subject).to match_array(agents)
+ end
+ end
+
+ context 'the current user does not have access to clusters' do
+ let(:current_user) { developer }
+
+ 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
+
+RSpec.describe Resolvers::Clusters::AgentsResolver.single do
+ it { expect(described_class).to be < Resolvers::Clusters::AgentsResolver }
+
+ describe '.field_options' do
+ subject { described_class.field_options }
+
+ specify do
+ expect(subject).to include(
+ type: ::Types::Clusters::AgentType,
+ null: true,
+ extras: [:lookahead]
+ )
+ end
+ end
+
+ 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 be_kind_of GraphQL::Schema::NonNull
+ expect(subject.type.unwrap).to eq GraphQL::Types::String
+ expect(subject.description).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb b/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb
new file mode 100644
index 00000000000..bdb1ced46ae
--- /dev/null
+++ b/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Kas::AgentConfigurationsResolver do
+ include GraphqlHelpers
+
+ it { expect(described_class.type).to eq(Types::Kas::AgentConfigurationType) }
+ it { expect(described_class.null).to be_truthy }
+ it { expect(described_class.field_options).to include(calls_gitaly: true) }
+
+ describe '#resolve' do
+ let_it_be(:project) { create(:project) }
+
+ let(:user) { create(:user, maintainer_projects: [project]) }
+ let(:ctx) { Hash(current_user: user) }
+
+ let(:agent1) { double }
+ let(:agent2) { double }
+ let(:kas_client) { instance_double(Gitlab::Kas::Client, list_agent_config_files: [agent1, agent2]) }
+
+ subject { resolve(described_class, obj: project, ctx: ctx) }
+
+ before do
+ allow(Gitlab::Kas::Client).to receive(:new).and_return(kas_client)
+ end
+
+ it 'returns agents configured for the project' do
+ expect(subject).to contain_exactly(agent1, agent2)
+ end
+
+ context 'an error is returned from the KAS client' do
+ before do
+ allow(kas_client).to receive(:list_agent_config_files).and_raise(GRPC::DeadlineExceeded)
+ end
+
+ it 'raises a graphql error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'GRPC::DeadlineExceeded')
+ end
+ end
+
+ context 'user does not have permission' do
+ let(:user) { create(:user) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb b/spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb
new file mode 100644
index 00000000000..fe6509bcb3c
--- /dev/null
+++ b/spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Kas::AgentConnectionsResolver do
+ include GraphqlHelpers
+
+ it { expect(described_class.type).to eq(Types::Kas::AgentConnectionType) }
+ it { expect(described_class.null).to be_truthy }
+
+ describe '#resolve' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:agent1) { create(:cluster_agent, project: project) }
+ let_it_be(:agent2) { create(:cluster_agent, project: project) }
+
+ let(:user) { create(:user, maintainer_projects: [project]) }
+ let(:ctx) { Hash(current_user: user) }
+
+ let(:connection1) { double(agent_id: agent1.id) }
+ let(:connection2) { double(agent_id: agent1.id) }
+ let(:connection3) { double(agent_id: agent2.id) }
+ let(:connected_agents) { [connection1, connection2, connection3] }
+ let(:kas_client) { instance_double(Gitlab::Kas::Client, get_connected_agents: connected_agents) }
+
+ subject do
+ batch_sync do
+ resolve(described_class, obj: agent1, ctx: ctx)
+ end
+ end
+
+ before do
+ allow(Gitlab::Kas::Client).to receive(:new).and_return(kas_client)
+ end
+
+ it 'returns active connections for the agent' do
+ expect(subject).to contain_exactly(connection1, connection2)
+ end
+
+ it 'queries KAS once when multiple agents are requested' do
+ expect(kas_client).to receive(:get_connected_agents).once
+
+ response = batch_sync do
+ resolve(described_class, obj: agent1, ctx: ctx)
+ resolve(described_class, obj: agent2, ctx: ctx)
+ end
+
+ expect(response).to contain_exactly(connection3)
+ end
+
+ context 'an error is returned from the KAS client' do
+ before do
+ allow(kas_client).to receive(:get_connected_agents).and_raise(GRPC::DeadlineExceeded)
+ end
+
+ it 'raises a graphql error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'GRPC::DeadlineExceeded')
+ end
+ end
+
+ context 'user does not have permission' do
+ let(:user) { create(:user) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/graphql/types/clusters/agent_token_type_spec.rb b/spec/graphql/types/clusters/agent_token_type_spec.rb
new file mode 100644
index 00000000000..c872d201fd9
--- /dev/null
+++ b/spec/graphql/types/clusters/agent_token_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ClusterAgentToken'] do
+ let(:fields) { %i[cluster_agent created_at created_by_user description id last_used_at name] }
+
+ it { expect(described_class.graphql_name).to eq('ClusterAgentToken') }
+
+ it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/clusters/agent_type_spec.rb b/spec/graphql/types/clusters/agent_type_spec.rb
new file mode 100644
index 00000000000..4b4b601b230
--- /dev/null
+++ b/spec/graphql/types/clusters/agent_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+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] }
+
+ it { expect(described_class.graphql_name).to eq('ClusterAgent') }
+
+ it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/kas/agent_configuration_type_spec.rb b/spec/graphql/types/kas/agent_configuration_type_spec.rb
new file mode 100644
index 00000000000..e6cccfa56d2
--- /dev/null
+++ b/spec/graphql/types/kas/agent_configuration_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['AgentConfiguration'] do
+ let(:fields) { %i[agent_name] }
+
+ it { expect(described_class.graphql_name).to eq('AgentConfiguration') }
+ it { expect(described_class.description).to eq('Configuration details for an Agent') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/kas/agent_connection_type_spec.rb b/spec/graphql/types/kas/agent_connection_type_spec.rb
new file mode 100644
index 00000000000..0990d02af11
--- /dev/null
+++ b/spec/graphql/types/kas/agent_connection_type_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Kas::AgentConnectionType do
+ include GraphqlHelpers
+
+ let(:fields) { %i[connected_at connection_id metadata] }
+
+ it { expect(described_class.graphql_name).to eq('ConnectedAgent') }
+ it { expect(described_class.description).to eq('Connection details for an Agent') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+
+ describe '#connected_at' do
+ let(:connected_at) { double(Google::Protobuf::Timestamp, seconds: 123456, nanos: 654321) }
+ let(:object) { double(Gitlab::Agent::AgentTracker::ConnectedAgentInfo, connected_at: connected_at) }
+
+ it 'converts the seconds value to a timestamp' do
+ expect(resolve_field(:connected_at, object)).to eq(Time.at(connected_at.seconds))
+ end
+ end
+end
diff --git a/spec/graphql/types/kas/agent_metadata_type_spec.rb b/spec/graphql/types/kas/agent_metadata_type_spec.rb
new file mode 100644
index 00000000000..ebc12ebb72a
--- /dev/null
+++ b/spec/graphql/types/kas/agent_metadata_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Kas::AgentMetadataType do
+ include GraphqlHelpers
+
+ let(:fields) { %i[version commit pod_namespace pod_name] }
+
+ it { expect(described_class.graphql_name).to eq('AgentMetadata') }
+ it { expect(described_class.description).to eq('Information about a connected Agent') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index d825bd7ebd4..0ade8cbd60f 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['Project'] do
issue_status_counts terraform_states alert_management_integrations
container_repositories container_repositories_count
pipeline_analytics squash_read_only sast_ci_configuration
+ cluster_agent cluster_agents agent_configurations
ci_template timelogs
]
@@ -458,4 +459,137 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Ci::JobTokenScopeType) }
it { is_expected.to have_graphql_resolver(Resolvers::Ci::JobTokenScopeResolver) }
end
+
+ describe 'agent_configurations' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ agentConfigurations {
+ nodes {
+ agentName
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:agent_name) { 'example-agent-name' }
+ let(:kas_client) { instance_double(Gitlab::Kas::Client, list_agent_config_files: [double(agent_name: agent_name)]) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ before do
+ project.add_maintainer(user)
+ allow(Gitlab::Kas::Client).to receive(:new).and_return(kas_client)
+ end
+
+ it 'returns configured agents' do
+ agents = subject.dig('data', 'project', 'agentConfigurations', 'nodes')
+
+ expect(agents.count).to eq(1)
+ expect(agents.first['agentName']).to eq(agent_name)
+ end
+ end
+
+ describe 'cluster_agents' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ 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 {
+ count
+ nodes {
+ id
+ name
+ createdAt
+ updatedAt
+
+ project {
+ id
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ before do
+ 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
+
+ it 'returns count of cluster agents' do
+ count = subject.dig('data', 'project', 'clusterAgents', 'count')
+
+ expect(count).to be(project.cluster_agents.size)
+ end
+ end
+
+ describe 'cluster_agent' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:cluster_agent) { create(:cluster_agent, project: project, name: 'agent-name') }
+ let_it_be(:agent_token) { create(:cluster_agent_token, agent: cluster_agent) }
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ clusterAgent(name: "#{cluster_agent.name}") {
+ id
+
+ tokens {
+ count
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns associated cluster agents' do
+ agent = subject.dig('data', 'project', 'clusterAgent')
+ tokens = agent.dig('tokens', 'nodes')
+
+ expect(agent['id']).to eq(cluster_agent.to_global_id.to_s)
+
+ expect(tokens.count).to be(1)
+ expect(tokens.first['id']).to eq(agent_token.to_global_id.to_s)
+ end
+
+ it 'returns count of agent tokens' do
+ agent = subject.dig('data', 'project', 'clusterAgent')
+ count = agent.dig('tokens', 'count')
+
+ expect(cluster_agent.agent_tokens.size).to be(count)
+ end
+ end
end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index 5150615e72a..cc0b69e3526 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -31,6 +31,23 @@ RSpec.describe InstanceConfiguration do
expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size)
end
+ it 'includes all algorithms' do
+ stub_pub_file(pub_file)
+
+ result = subject.settings[:ssh_algorithms_hashes]
+
+ expect(result.map { |a| a[:name] }).to match_array(%w(DSA ECDSA ED25519 RSA))
+ end
+
+ it 'does not include disabled algorithm' do
+ Gitlab::CurrentSettings.current_application_settings.update!(dsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
+ stub_pub_file(pub_file)
+
+ result = subject.settings[:ssh_algorithms_hashes]
+
+ expect(result.map { |a| a[:name] }).to match_array(%w(ECDSA ED25519 RSA))
+ end
+
def pub_file(exist: true)
path = exist ? 'spec/fixtures/ssh_host_example_key.pub' : 'spec/fixtures/ssh_host_example_key.pub.random'
diff --git a/spec/policies/clusters/agent_policy_spec.rb b/spec/policies/clusters/agent_policy_spec.rb
new file mode 100644
index 00000000000..307d751b78b
--- /dev/null
+++ b/spec/policies/clusters/agent_policy_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentPolicy do
+ let(:cluster_agent) { create(:cluster_agent, name: 'agent' )}
+ let(:user) { create(:admin) }
+ let(:policy) { described_class.new(user, cluster_agent) }
+ let(:project) { cluster_agent.project }
+
+ describe 'rules' do
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
+end
diff --git a/spec/policies/clusters/agent_token_policy_spec.rb b/spec/policies/clusters/agent_token_policy_spec.rb
new file mode 100644
index 00000000000..9ae99e66f59
--- /dev/null
+++ b/spec/policies/clusters/agent_token_policy_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentTokenPolicy do
+ let_it_be(:token) { create(:cluster_agent_token) }
+
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, token) }
+ let(:project) { token.agent.project }
+
+ describe 'rules' do
+ context 'when 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 'when 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