diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-23 06:09:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-23 06:09:49 +0300 |
commit | 163b6c3c80c2aad98d0eedb3ccd76a72c5e72771 (patch) | |
tree | 68f939d4ea170754d063979501548259560b0236 | |
parent | 5d3bcd82b5d6a8567c3c0b1d1620fdd26a4513c5 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |