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