Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5d3bcd82b5
commit
163b6c3c80
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ' \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
class AgentPolicy < BasePolicy
|
||||
alias_method :cluster_agent, :subject
|
||||
|
||||
delegate { cluster_agent.project }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
class AgentTokenPolicy < BasePolicy
|
||||
alias_method :token, :subject
|
||||
|
||||
delegate { token.agent }
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
262127539fc16715a56e2cf7426f0f8d24922e26847a01a0a15552d71cd148f8
|
||||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue