From 89f6a49a738802f711aacdbbb66844e747d8626b Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Tue, 6 Aug 2024 15:58:24 +1000 Subject: [PATCH 1/8] Add agent config type, resolve, finder with permission check --- doc/api/graphql/reference/index.md | 29 ++++ .../agent_configs_finder.rb | 35 +++++ .../filter_argument_validator.rb | 21 +++ .../remote_development/workspaces_finder.rb | 36 ++--- .../graphql/ee/types/clusters/agent_type.rb | 7 + .../agent_config_for_agent_resolver.rb | 46 ++++++ .../remote_development_agent_config_type.rb | 51 ++++++ .../remote_development_agent_config.rb | 3 + .../remote_development_agent_config_policy.rb | 15 ++ ee/spec/factories/clusters/agents.rb | 8 +- .../agent_configs_finder_spec.rb | 145 ++++++++++++++++++ .../filter_argument_validator_spec.rb | 89 +++++++++++ .../workspaces_finder_spec.rb | 16 ++ .../ee/types/clusters/agent_type_spec.rb | 66 +++++++- ...mote_development_agent_config_type_spec.rb | 50 ++++++ .../remote_development/agent_policy_spec.rb | 8 +- .../remote_development/group_policy_spec.rb | 8 +- ...te_development_agent_config_policy_spec.rb | 43 ++++++ .../remote_development/integration_spec.rb | 79 ++++++++-- 19 files changed, 707 insertions(+), 48 deletions(-) create mode 100644 ee/app/finders/remote_development/agent_configs_finder.rb create mode 100644 ee/app/finders/remote_development/filter_argument_validator.rb create mode 100644 ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb create mode 100644 ee/app/graphql/types/remote_development/remote_development_agent_config_type.rb create mode 100644 ee/app/policies/remote_development/remote_development_agent_config_policy.rb create mode 100644 ee/spec/finders/remote_development/agent_configs_finder_spec.rb create mode 100644 ee/spec/finders/remote_development/filter_argument_validator_spec.rb create mode 100644 ee/spec/graphql/types/remote_development/remote_development_agent_config_type_spec.rb create mode 100644 ee/spec/policies/remote_development/remote_development_agent_config_policy_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 12be1235faa7cd..6f3cb9c6a26d24 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18936,6 +18936,7 @@ GitLab CI/CD configuration template. | `id` | [`ID!`](#id) | ID of the cluster agent. | | `name` | [`String`](#string) | Name of the cluster agent. | | `project` | [`Project`](#project) | Project this cluster agent is associated with. | +| `remoteDevelopmentAgentConfig` | [`RemoteDevelopmentAgentConfig`](#remotedevelopmentagentconfig) | Remote development agent config for the cluster agent. | | `tokens` | [`ClusterAgentTokenConnection`](#clusteragenttokenconnection) | Tokens associated with the cluster agent. (see [Connections](#connections)) | | `updatedAt` | [`Time`](#time) | Timestamp the cluster agent was updated. | | `userAccessAuthorizations` | [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess) | User access config for the cluster agent. | @@ -30962,6 +30963,28 @@ Represents the source code attached to a release in a particular format. | `format` | [`String`](#string) | Format of the source. | | `url` | [`String`](#string) | Download URL of the source. | +### `RemoteDevelopmentAgentConfig` + +Represents a remote development agent configuration. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clusterAgent` | [`ClusterAgent!`](#clusteragent) | Cluster agent that the remote development agent config belongs to. | +| `createdAt` | [`Time!`](#time) | Timestamp of when the remote development agent config was created. | +| `defaultMaxHoursBeforeTermination` | [`Int!`](#int) | Default max hours before worksapce termination of the remote development agent config. | +| `dnsZone` | [`String!`](#string) | DNS zone where workspaces are available. | +| `enabled` | [`Boolean!`](#boolean) | Indicates whether remote development is enabled for the GitLab agent. | +| `gitlabWorkspacesProxyNamespace` | [`String!`](#string) | Namespace where gitlab-workspaces-proxy is installed. | +| `id` | [`RemoteDevelopmentRemoteDevelopmentAgentConfigID!`](#remotedevelopmentremotedevelopmentagentconfigid) | Global ID of the remote development agent config. | +| `maxHoursBeforeTerminationLimit` | [`Int!`](#int) | Max hours before worksapce termination limit of the remote development agent config. | +| `networkPolicyEnabled` | [`Boolean!`](#boolean) | Whether the network policy of the remote development agent config is enabled. | +| `projectId` | [`ID`](#id) | ID of the project that the remote development agent config belongs to. | +| `updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable remote development agent config property. | +| `workspacesPerUserQuota` | [`Int!`](#int) | Maximum number of workspaces per user. | +| `workspacesQuota` | [`Int!`](#int) | Maximum number of workspaces for the GitLab agent. | + ### `Repository` #### Fields @@ -38434,6 +38457,12 @@ A `ReleasesLinkID` is a global ID. It is encoded as a string. An example `ReleasesLinkID` is: `"gid://gitlab/Releases::Link/1"`. +### `RemoteDevelopmentRemoteDevelopmentAgentConfigID` + +A `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is a global ID. It is encoded as a string. + +An example `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is: `"gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/1"`. + ### `RemoteDevelopmentWorkspaceID` A `RemoteDevelopmentWorkspaceID` is a global ID. It is encoded as a string. diff --git a/ee/app/finders/remote_development/agent_configs_finder.rb b/ee/app/finders/remote_development/agent_configs_finder.rb new file mode 100644 index 00000000000000..9f784626653f03 --- /dev/null +++ b/ee/app/finders/remote_development/agent_configs_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class AgentConfigsFinder + # Executes a query to find agent configurations based on the provided filter arguments. + # + # @param [User] current_user The user making the request. Must have permission to access workspaces. + # @param [Array] ids A list of specific RemoteDevelopmentAgentConfig IDs to filter by (optional). + # @param [Array] cluster_agent_ids A list of ClusterAgent IDs to filter by (optional). + # @return [ActiveRecord::Relation] + # A collection of filtered RemoteDevelopmentAgentConfig records ordered by ID descending. + def self.execute(current_user:, ids: [], cluster_agent_ids: []) + return RemoteDevelopmentAgentConfig.none unless current_user.can?(:access_workspaces_feature) + + filter_arguments = { + ids: ids, + cluster_agent_ids: cluster_agent_ids + } + + filter_argument_types = { + ids: Integer, + cluster_agent_ids: Integer + } + + FilterArgumentValidator.validate_filter_argument_types!(filter_argument_types, filter_arguments) + FilterArgumentValidator.validate_at_least_one_filter_argument_provided!(**filter_arguments) + + collection_proxy = RemoteDevelopmentAgentConfig.all + collection_proxy = collection_proxy.id_in(ids) if ids.present? + collection_proxy = collection_proxy.by_cluster_agent_ids(cluster_agent_ids) if cluster_agent_ids.present? + + collection_proxy.order_id_desc + end + end +end diff --git a/ee/app/finders/remote_development/filter_argument_validator.rb b/ee/app/finders/remote_development/filter_argument_validator.rb new file mode 100644 index 00000000000000..28665bacc2a105 --- /dev/null +++ b/ee/app/finders/remote_development/filter_argument_validator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class FilterArgumentValidator + def self.validate_filter_argument_types!(types, filter_arguments) + errors = [] + + filter_arguments.each do |argument_name, argument| + type = types[argument_name.to_sym] + errors << "'#{argument_name}' must be an Array of '#{type}'" unless argument.is_a?(Array) && argument.all?(type) + end + + raise errors.join(", ") if errors.present? + end + + def self.validate_at_least_one_filter_argument_provided!(**filter_arguments) + no_filter_arguments_provided = filter_arguments.values.flatten.empty? + raise ArgumentError, "At least one filter argument must be provided" if no_filter_arguments_provided + end + end +end diff --git a/ee/app/finders/remote_development/workspaces_finder.rb b/ee/app/finders/remote_development/workspaces_finder.rb index c7496408d03c59..9cbb6b1d02e9d5 100644 --- a/ee/app/finders/remote_development/workspaces_finder.rb +++ b/ee/app/finders/remote_development/workspaces_finder.rb @@ -20,8 +20,16 @@ def self.execute(current_user:, ids: [], user_ids: [], project_ids: [], agent_id actual_states: actual_states } - validate_filter_argument_types!(**filter_arguments) - validate_at_least_one_filter_argument_provided!(**filter_arguments) + filter_argument_types = { + ids: Integer, + user_ids: Integer, + project_ids: Integer, + agent_ids: Integer, + actual_states: String + }.freeze + + FilterArgumentValidator.validate_filter_argument_types!(filter_argument_types, filter_arguments) + FilterArgumentValidator.validate_at_least_one_filter_argument_provided!(**filter_arguments) validate_actual_state_values!(actual_states) collection_proxy = Workspace.all @@ -34,30 +42,6 @@ def self.execute(current_user:, ids: [], user_ids: [], project_ids: [], agent_id collection_proxy.order_id_desc end - def self.validate_filter_argument_types!(**filter_arguments) - types = { - ids: Integer, - user_ids: Integer, - project_ids: Integer, - agent_ids: Integer, - actual_states: String - } - - errors = [] - - filter_arguments.each do |argument_name, argument| - type = types[argument_name.to_sym] - errors << "'#{argument_name}' must be an Array of '#{type}'" unless argument.is_a?(Array) && argument.all?(type) - end - - raise errors.join(", ") if errors.present? - end - - def self.validate_at_least_one_filter_argument_provided!(**filter_arguments) - no_filter_arguments_provided = filter_arguments.values.flatten.empty? - raise ArgumentError, "At least one filter argument must be provided" if no_filter_arguments_provided - end - def self.validate_actual_state_values!(actual_states) invalid_actual_state = actual_states.find do |actual_state| Workspaces::States::VALID_ACTUAL_STATES.exclude?(actual_state) diff --git a/ee/app/graphql/ee/types/clusters/agent_type.rb b/ee/app/graphql/ee/types/clusters/agent_type.rb index 66f7655757c4f8..2401fe4a2ed07c 100644 --- a/ee/app/graphql/ee/types/clusters/agent_type.rb +++ b/ee/app/graphql/ee/types/clusters/agent_type.rb @@ -18,6 +18,13 @@ module AgentType null: true, resolver: ::Resolvers::RemoteDevelopment::WorkspacesForAgentResolver, description: 'Workspaces associated with the agent.' + + field :remote_development_agent_config, + ::Types::RemoteDevelopment::RemoteDevelopmentAgentConfigType, + extras: [:lookahead], + null: true, + description: 'Remote development agent config for the cluster agent.', + resolver: ::Resolvers::RemoteDevelopment::AgentConfigForAgentResolver end end end diff --git a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb new file mode 100644 index 00000000000000..a2c6600294e027 --- /dev/null +++ b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Resolvers + module RemoteDevelopment + class AgentConfigForAgentResolver < ::Resolvers::BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead + + type Types::RemoteDevelopment::RemoteDevelopmentAgentConfigType, null: true + + alias_method :agent, :object + + def resolve_with_lookahead(**_args) + unless License.feature_available?(:remote_development) + raise_resource_not_available_error! "'remote_development' licensed feature is not available" + end + + raise Gitlab::Access::AccessDeniedError unless can_read_remote_development_agent_config? + + BatchLoader::GraphQL.for(agent.id).batch do |agent_ids, loader| + agent_configs = ::RemoteDevelopment::AgentConfigsFinder.execute( + current_user: current_user, + cluster_agent_ids: agent_ids + ) + apply_lookahead(agent_configs).each do |agent_config| + loader.call(agent_config.cluster_agent_id, agent_config) + end + end + end + + def preloads + { + default_max_hours_before_termination: [:default_max_hours_before_termination], + id: [:id] + } + end + + private + + def can_read_remote_development_agent_config? + # noinspection RubyNilAnalysis - This is because the superclass #current_user uses #[], which can return nil + current_user.can?(:read_cluster_agent, agent) + end + end + end +end diff --git a/ee/app/graphql/types/remote_development/remote_development_agent_config_type.rb b/ee/app/graphql/types/remote_development/remote_development_agent_config_type.rb new file mode 100644 index 00000000000000..60b7b5462a9e5b --- /dev/null +++ b/ee/app/graphql/types/remote_development/remote_development_agent_config_type.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Types + module RemoteDevelopment + class RemoteDevelopmentAgentConfigType < ::Types::BaseObject + graphql_name 'RemoteDevelopmentAgentConfig' + description 'Represents a remote development agent configuration' + + authorize :read_remote_development_agent_config + + field :id, ::Types::GlobalIDType[::RemoteDevelopment::RemoteDevelopmentAgentConfig], + null: false, description: 'Global ID of the remote development agent config.' + + field :cluster_agent, ::Types::Clusters::AgentType, + null: false, description: 'Cluster agent that the remote development agent config belongs to.' + + field :project_id, GraphQL::Types::ID, + null: true, description: 'ID of the project that the remote development agent config belongs to.' + + field :enabled, GraphQL::Types::Boolean, + null: false, description: 'Indicates whether remote development is enabled for the GitLab agent.' + + field :dns_zone, GraphQL::Types::String, + null: false, description: 'DNS zone where workspaces are available.' + + field :network_policy_enabled, GraphQL::Types::Boolean, + null: false, description: 'Whether the network policy of the remote development agent config is enabled.' + + field :gitlab_workspaces_proxy_namespace, GraphQL::Types::String, + null: false, description: 'Namespace where gitlab-workspaces-proxy is installed.' + + field :workspaces_quota, GraphQL::Types::Int, + null: false, description: 'Maximum number of workspaces for the GitLab agent.' + + field :workspaces_per_user_quota, GraphQL::Types::Int, # rubocop:disable GraphQL/ExtractType -- We don't want to extract this to a type, it's just an integer field + null: false, description: 'Maximum number of workspaces per user.' + + field :default_max_hours_before_termination, GraphQL::Types::Int, null: false, + description: 'Default max hours before worksapce termination of the remote development agent config.' + + field :max_hours_before_termination_limit, GraphQL::Types::Int, null: false, + description: 'Max hours before worksapce termination limit of the remote development agent config.' + + field :created_at, Types::TimeType, + null: false, description: 'Timestamp of when the remote development agent config was created.' + + field :updated_at, Types::TimeType, null: false, + description: 'Timestamp of the last update to any mutable remote development agent config property.' + end + end +end diff --git a/ee/app/models/remote_development/remote_development_agent_config.rb b/ee/app/models/remote_development/remote_development_agent_config.rb index d533fce18043b7..1d20e755254096 100644 --- a/ee/app/models/remote_development/remote_development_agent_config.rb +++ b/ee/app/models/remote_development/remote_development_agent_config.rb @@ -5,6 +5,7 @@ class RemoteDevelopmentAgentConfig < ApplicationRecord # NOTE: See the following comment for the reasoning behind the `RemoteDevelopment` prefix of this table/model: # https://gitlab.com/gitlab-org/gitlab/-/issues/410045#note_1385602915 include IgnorableColumns + include Sortable UNLIMITED_QUOTA = -1 MINIMUM_HOURS_BEFORE_TERMINATION = 1 @@ -40,5 +41,7 @@ class RemoteDevelopmentAgentConfig < ApplicationRecord validates :default_max_hours_before_termination, numericality: { only_integer: true, greater_than_or_equal_to: MINIMUM_HOURS_BEFORE_TERMINATION, less_than_or_equal_to: :max_hours_before_termination_limit } + + scope :by_cluster_agent_ids, ->(ids) { where(cluster_agent_id: ids) } end end diff --git a/ee/app/policies/remote_development/remote_development_agent_config_policy.rb b/ee/app/policies/remote_development/remote_development_agent_config_policy.rb new file mode 100644 index 00000000000000..61e726be22f2e8 --- /dev/null +++ b/ee/app/policies/remote_development/remote_development_agent_config_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class RemoteDevelopmentAgentConfigPolicy < BasePolicy + condition(:can_read_cluster_agent) { can?(:read_cluster_agent, agent) } + + rule { can_read_cluster_agent }.enable :read_remote_development_agent_config + + private + + def agent + @subject.agent + end + end +end diff --git a/ee/spec/factories/clusters/agents.rb b/ee/spec/factories/clusters/agents.rb index ee57ddcef9d532..153d0b563916d0 100644 --- a/ee/spec/factories/clusters/agents.rb +++ b/ee/spec/factories/clusters/agents.rb @@ -3,7 +3,13 @@ FactoryBot.define do factory :ee_cluster_agent, class: 'Clusters::Agent', parent: :cluster_agent do trait :with_remote_development_agent_config do - remote_development_agent_config + transient do + project_id { nil } + end + + remote_development_agent_config do + association(:remote_development_agent_config, project_id: project_id) + end end end end diff --git a/ee/spec/finders/remote_development/agent_configs_finder_spec.rb b/ee/spec/finders/remote_development/agent_configs_finder_spec.rb new file mode 100644 index 00000000000000..3cd9f05b0bc84d --- /dev/null +++ b/ee/spec/finders/remote_development/agent_configs_finder_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RemoteDevelopment::AgentConfigsFinder, feature_category: :remote_development do + let_it_be(:current_user) { create(:user) } + + let_it_be(:cluster_admin_user) { create(:user) } + let_it_be(:agent_a) do + create(:ee_cluster_agent, created_by_user: cluster_admin_user) + end + + let_it_be(:agent_b) do + create(:ee_cluster_agent, created_by_user: cluster_admin_user) + end + + let_it_be(:agent_config_a) do + create(:remote_development_agent_config, agent: agent_a) + end + + let_it_be(:agent_config_b) do + create(:remote_development_agent_config, agent: agent_b) + end + + subject(:collection_proxy) do + described_class.execute(current_user: current_user, **filter_arguments) + end + + before do + stub_licensed_features(remote_development: true) + allow(::RemoteDevelopment::FilterArgumentValidator).to receive(:validate_filter_argument_types!).and_return(true) + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_at_least_one_filter_argument_provided! + ).and_return(true) + end + + context "with ids argument" do + let(:filter_arguments) { { ids: [agent_config_a.id] } } + + it "returns only agent configs matching the specified IDs" do + expect(collection_proxy).to contain_exactly(agent_config_a) + end + end + + context "with cluster_agent_ids argument" do + let(:filter_arguments) { { cluster_agent_ids: [agent_a.id] } } + + it "returns only agent configs matching the specified agent IDs" do + expect(collection_proxy).to contain_exactly(agent_config_a) + end + end + + context "with multiple arguments" do + let(:filter_arguments) do + { + ids: [agent_config_a.id], + cluster_agent_ids: [agent_a.id, agent_b.id] + } + end + + it "handles multiple arguments and still returns all agent configs which match all filter arguments" do + expect(collection_proxy).to contain_exactly(agent_config_a) + end + end + + context "with extra empty filter arguments" do + let(:filter_arguments) do + { + ids: [agent_config_a.id], + cluster_agent_ids: [] + } + end + + it "still uses existing filter arguments" do + expect(collection_proxy).to contain_exactly(agent_config_a) + end + end + + describe "validations" do + context "when no filter arguments are provided" do + before do + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_at_least_one_filter_argument_provided! + ).and_raise(ArgumentError.new("At least one filter argument must be provided")) + end + + let(:filter_arguments) { {} } + + it "raises an ArgumentError" do + expect { collection_proxy }.to raise_error(ArgumentError, "At least one filter argument must be provided") + end + end + + context "when an invalid filter argument type is provided" do + let(:expected_exception_message) do + "'ids' must be an Array of 'Integer', " \ + "'cluster_agent_ids' must be an Array of 'Integer'" + end + + before do + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_filter_argument_types! + ).and_raise(RuntimeError.new(expected_exception_message)) + end + + context "when argument is not an array" do + let(:filter_arguments) do + { + ids: 1, + cluster_agent_ids: 1 + } + end + + it "raises an RuntimeError", :unlimited_max_formatted_output_length do + expect { collection_proxy.to_a }.to raise_error(RuntimeError, expected_exception_message) + end + end + + context "when array content is wrong type" do + let(:filter_arguments) do + { + ids: %w[a b], + cluster_agent_ids: %w[a b] + } + end + + it "raises an RuntimeError", :unlimited_max_formatted_output_length do + expect { collection_proxy.to_a }.to raise_error(RuntimeError, expected_exception_message) + end + end + end + end + + describe "no workspaces feature" do + before do + stub_licensed_features(remote_development: false) + end + + let(:filter_arguments) { { ids: [agent_config_a.id] } } + + it "returns no agent config" do + expect(collection_proxy).to eq([]) + end + end +end diff --git a/ee/spec/finders/remote_development/filter_argument_validator_spec.rb b/ee/spec/finders/remote_development/filter_argument_validator_spec.rb new file mode 100644 index 00000000000000..b2d15b03750a96 --- /dev/null +++ b/ee/spec/finders/remote_development/filter_argument_validator_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RemoteDevelopment::FilterArgumentValidator, feature_category: :remote_development do + let(:filter_arguments) do + { + ids: [1, 2], + cluster_agent_ids: [99, 98] + } + end + + let(:filter_argument_types) do + { + ids: Integer, + cluster_agent_ids: Integer + } + end + + describe ".validate_filter_argument_types!" do + context "when all types are valid" do + it "does not raise an error" do + expect do + described_class.validate_filter_argument_types!(filter_argument_types, filter_arguments) + end.not_to raise_error + end + end + + context "when an invalid filter argument type is provided" do + let(:expected_exception_message) do + "'ids' must be an Array of 'Integer', " \ + "'cluster_agent_ids' must be an Array of 'Integer'" + end + + context "when argument is not an array" do + let(:filter_arguments) do + { + ids: 1, + cluster_agent_ids: 1 + } + end + + it "raises an RuntimeError", :unlimited_max_formatted_output_length do + expect do + described_class.validate_filter_argument_types!(filter_argument_types, + filter_arguments) + end.to raise_error(RuntimeError, expected_exception_message) + end + end + + context "when array content is wrong type" do + let(:filter_arguments) do + { + ids: %w[a b], + cluster_agent_ids: %w[a b] + } + end + + it "raises an RuntimeError", :unlimited_max_formatted_output_length do + expect do + described_class.validate_filter_argument_types!(filter_argument_types, + filter_arguments) + end.to raise_error(RuntimeError, expected_exception_message) + end + end + end + end + + describe ".validate_at_least_one_filter_argument_provided!" do + context "when at lease one filter argument is provided" do + it "does not raise an error" do + expect do + described_class.validate_at_least_one_filter_argument_provided!(**filter_arguments) + end.not_to raise_error + end + end + + context "when no filter argument is provided" do + let(:filter_arguments) { {} } + + it "raise ArgumentError" do + expect do + described_class.validate_at_least_one_filter_argument_provided!(**filter_arguments) + end.to raise_error(ArgumentError, + "At least one filter argument must be provided") + end + end + end +end diff --git a/ee/spec/finders/remote_development/workspaces_finder_spec.rb b/ee/spec/finders/remote_development/workspaces_finder_spec.rb index 5dd74c950d9dcc..556aff610d7ee0 100644 --- a/ee/spec/finders/remote_development/workspaces_finder_spec.rb +++ b/ee/spec/finders/remote_development/workspaces_finder_spec.rb @@ -45,6 +45,10 @@ before do stub_licensed_features(remote_development: true) + allow(::RemoteDevelopment::FilterArgumentValidator).to receive(:validate_filter_argument_types!).and_return(true) + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_at_least_one_filter_argument_provided! + ).and_return(true) end context "with ids argument" do @@ -148,6 +152,12 @@ context "when no filter arguments are provided" do let(:filter_arguments) { {} } + before do + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_at_least_one_filter_argument_provided! + ).and_raise(ArgumentError.new("At least one filter argument must be provided")) + end + it "raises an ArgumentError" do expect { collection_proxy }.to raise_error(ArgumentError, "At least one filter argument must be provided") end @@ -162,6 +172,12 @@ "'actual_states' must be an Array of 'String'" end + before do + allow(::RemoteDevelopment::FilterArgumentValidator).to receive( + :validate_filter_argument_types! + ).and_raise(RuntimeError.new(expected_exception_message)) + end + context "when argument is not an array" do let(:filter_arguments) do { diff --git a/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb b/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb index 1c750bc04ecf85..735f344fcdfb76 100644 --- a/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb +++ b/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb @@ -5,7 +5,9 @@ RSpec.describe GitlabSchema.types['ClusterAgent'], feature_category: :environment_management do it 'includes the ee specific fields' do expect(described_class).to have_graphql_fields( - :vulnerability_images + :vulnerability_images, + :workspaces, + :remote_development_agent_config ).at_least end @@ -61,4 +63,66 @@ end end end + + describe 'remote_development_agent_config' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:cluster_agent) { create(:cluster_agent, project: project) } + let_it_be(:remote_development_agent_config) do + create(:remote_development_agent_config, cluster_agent_id: cluster_agent.id, project_id: project.id) + end + + let_it_be(:remote_development_namespace_cluster_agent_mapping) do + create(:remote_development_namespace_cluster_agent_mapping, agent: cluster_agent, namespace: group) + end + + let_it_be(:query) do + %( + query { + namespace(fullPath: "#{group.full_path}") { + remoteDevelopmentClusterAgents(filter: AVAILABLE) { + nodes { + remoteDevelopmentAgentConfig { + defaultMaxHoursBeforeTermination + } + } + } + } + } + ) + end + + before_all do + project.add_owner(user) + end + + before do + stub_licensed_features(remote_development: true) + end + + subject(:remote_development_agent_config_result) do + result = GitlabSchema.execute(query, context: { current_user: current_user }).as_json + result.dig('data', 'namespace', 'remoteDevelopmentClusterAgents', 'nodes', 0, 'remoteDevelopmentAgentConfig') + end + + context 'when user is not logged in' do + let(:current_user) { nil } + + it { is_expected.to be_nil } + end + + context 'when user is logged in' do + let(:current_user) { user } + let(:expected_default_max_hours_before_termination) do + remote_development_agent_config.default_max_hours_before_termination + end + + it 'returns associated remote development agent config' do + expect(remote_development_agent_config_result).to eq( + 'defaultMaxHoursBeforeTermination' => expected_default_max_hours_before_termination + ) + end + end + end end diff --git a/ee/spec/graphql/types/remote_development/remote_development_agent_config_type_spec.rb b/ee/spec/graphql/types/remote_development/remote_development_agent_config_type_spec.rb new file mode 100644 index 00000000000000..d8d15b57288a81 --- /dev/null +++ b/ee/spec/graphql/types/remote_development/remote_development_agent_config_type_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['RemoteDevelopmentAgentConfig'], feature_category: :remote_development do + let(:fields) do + %i[ + id cluster_agent project_id enabled dns_zone network_policy_enabled gitlab_workspaces_proxy_namespace + workspaces_quota workspaces_per_user_quota default_max_hours_before_termination max_hours_before_termination_limit + created_at updated_at + ] + end + + specify { expect(described_class.graphql_name).to eq('RemoteDevelopmentAgentConfig') } + + specify { expect(described_class).to have_graphql_fields(fields) } + + specify { expect(described_class).to require_graphql_authorizations(:read_remote_development_agent_config) } + + describe 'remote_development_agent_config' do + let_it_be(:group) { create(:group) } + + let_it_be(:query) do + %( + query { + namespace(fullPath: "#{group.full_path}") { + remoteDevelopmentClusterAgents(filter: AVAILABLE) { + nodes { + remoteDevelopmentAgentConfig { + defaultMaxHoursBeforeTermination + } + } + } + } + } + ) + end + + subject(:remote_development_agent_config_result) do + result = GitlabSchema.execute(query, context: { current_user: current_user }).as_json + result.dig('data', 'namespace', 'remoteDevelopmentClusterAgents', 'nodes', 0, 'remoteDevelopmentAgentConfig') + end + + context 'when user is not logged in' do + let(:current_user) { nil } + + it { is_expected.to be_nil } + end + end +end diff --git a/ee/spec/policies/remote_development/agent_policy_spec.rb b/ee/spec/policies/remote_development/agent_policy_spec.rb index c5ea0d0ab73c5b..e8949769adfef2 100644 --- a/ee/spec/policies/remote_development/agent_policy_spec.rb +++ b/ee/spec/policies/remote_development/agent_policy_spec.rb @@ -30,7 +30,7 @@ end with_them do - subject(:policy) { Clusters::AgentPolicy.new(user, agent) } + subject(:policy_instance) { Clusters::AgentPolicy.new(user, agent) } before do enable_admin_mode!(admin_in_admin_mode) if user == admin_in_admin_mode @@ -39,7 +39,7 @@ debug_policies(user, agent, Clusters::AgentPolicy, ability) if debug end - it { expect(policy.allowed?(ability)).to eq(result) } + it { expect(policy_instance.allowed?(ability)).to eq(result) } end end @@ -57,7 +57,7 @@ end with_them do - subject(:policy) { Clusters::AgentPolicy.new(user, agent) } + subject(:policy_instance) { Clusters::AgentPolicy.new(user, agent) } before do enable_admin_mode!(admin_in_admin_mode) if user == admin_in_admin_mode @@ -66,7 +66,7 @@ debug_policies(user, agent, Clusters::AgentPolicy, ability) if debug end - it { expect(policy.allowed?(ability)).to eq(result) } + it { expect(policy_instance.allowed?(ability)).to eq(result) } end end diff --git a/ee/spec/policies/remote_development/group_policy_spec.rb b/ee/spec/policies/remote_development/group_policy_spec.rb index 03270777309bd6..be2230c37e835c 100644 --- a/ee/spec/policies/remote_development/group_policy_spec.rb +++ b/ee/spec/policies/remote_development/group_policy_spec.rb @@ -36,7 +36,7 @@ end with_them do - subject(:policy) { policy_class.new(user, group) } + subject(:policy_instance) { policy_class.new(user, group) } before do enable_admin_mode!(admin_in_admin_mode) if user == admin_in_admin_mode @@ -45,7 +45,7 @@ debug_policies(user, group, policy_class, ability) if debug end - it { expect(policy.allowed?(ability)).to eq(result) } + it { expect(policy_instance.allowed?(ability)).to eq(result) } end end @@ -70,7 +70,7 @@ end with_them do - subject(:policy) { policy_class.new(user, group) } + subject(:policy_instance) { policy_class.new(user, group) } before do enable_admin_mode!(admin_in_admin_mode) if user == admin_in_admin_mode @@ -79,7 +79,7 @@ debug_policies(user, group, policy_class, ability) if debug end - it { expect(policy.allowed?(ability)).to eq(result) } + it { expect(policy_instance.allowed?(ability)).to eq(result) } end end diff --git a/ee/spec/policies/remote_development/remote_development_agent_config_policy_spec.rb b/ee/spec/policies/remote_development/remote_development_agent_config_policy_spec.rb new file mode 100644 index 00000000000000..ea364205a3eb3b --- /dev/null +++ b/ee/spec/policies/remote_development/remote_development_agent_config_policy_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RemoteDevelopment::RemoteDevelopmentAgentConfigPolicy, feature_category: :remote_development do + let_it_be(:agent_project_creator, refind: true) { create(:user) } + let_it_be(:agent_project, refind: true) { create(:project, creator: agent_project_creator) } + let_it_be(:agent, refind: true) do + create(:ee_cluster_agent, :with_remote_development_agent_config, project: agent_project) + end + + let_it_be(:agent_config) { agent.remote_development_agent_config } + + subject(:policy_instance) { described_class.new(user, agent_config) } + + context 'when user can read a cluster agent' do + let(:user) { agent_project_creator } + + before do + allow_next_instance_of(described_class) do |policy| + allow(policy).to receive(:can?).with(:read_cluster_agent, agent).and_return(true) + end + end + + it 'allows reading the corrosponding agent config' do + expect(policy_instance).to be_allowed(:read_remote_development_agent_config) + end + end + + context 'when user can not read a cluster agent' do + let(:user) { create(:admin) } + + before do + allow_next_instance_of(described_class) do |policy| + allow(policy).to receive(:can?).with(:read_cluster_agent, agent).and_return(false) + end + end + + it 'disallows reading the corrosponding agent config' do + expect(policy_instance).to be_disallowed(:read_remote_development_agent_config) + end + end +end diff --git a/ee/spec/requests/remote_development/integration_spec.rb b/ee/spec/requests/remote_development/integration_spec.rb index be9b0239ccb042..0cafe1f0507a72 100644 --- a/ee/spec/requests/remote_development/integration_spec.rb +++ b/ee/spec/requests/remote_development/integration_spec.rb @@ -10,6 +10,50 @@ include_context "with remote development shared fixtures" let(:agent_admin_user) { create(:user, name: "Agent Admin User") } + # Agent setup + let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) } + let(:agent_token) { create(:cluster_agent_token, agent: agent) } + let(:query) do + <<~GRAPHQL + query { + namespace(fullPath: "#{common_parent_namespace.full_path}") { + remoteDevelopmentClusterAgents(filter: AVAILABLE) { + nodes { + id + remoteDevelopmentAgentConfig { + id + projectId + enabled + gitlabWorkspacesProxyNamespace + networkPolicyEnabled + dnsZone + workspacesPerUserQuota + workspacesQuota + defaultMaxHoursBeforeTermination + maxHoursBeforeTerminationLimit + } + } + } + } + } + GRAPHQL + end + + let(:expected_agent_config) do + { + "id" => "gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/#{remote_development_agent_config.id}", + "projectId" => remote_development_agent_config.project_id.to_s, + "enabled" => remote_development_agent_config.enabled, + "gitlabWorkspacesProxyNamespace" => remote_development_agent_config.gitlab_workspaces_proxy_namespace, + "networkPolicyEnabled" => remote_development_agent_config.network_policy_enabled, + "dnsZone" => remote_development_agent_config.dns_zone, + "workspacesPerUserQuota" => remote_development_agent_config.workspaces_per_user_quota, + "workspacesQuota" => remote_development_agent_config.workspaces_quota, + "defaultMaxHoursBeforeTermination" => remote_development_agent_config.default_max_hours_before_termination, + "maxHoursBeforeTerminationLimit" => remote_development_agent_config.max_hours_before_termination_limit + } + end + let(:user) { create(:user, name: "Workspaces User", email: "workspaces-user@example.org") } let(:current_user) { user } let(:common_parent_namespace_name) { "common-parent-group" } @@ -69,7 +113,7 @@ { key: "GL_EDITOR_EXTENSIONS_GALLERY_SERVICE_URL", type: :environment, value: "https://open-vsx.org/vscode/gallery" }, { key: "GL_GIT_CREDENTIAL_STORE_FILE_PATH", type: :environment, value: "/.workspace-data/variables/file/gl_git_credential_store.sh" }, { key: "GL_TOKEN_FILE_PATH", type: :environment, value: "/.workspace-data/variables/file/gl_token" }, - { key: "GL_WORKSPACE_DOMAIN_TEMPLATE", type: :environment, value: "${PORT}-workspace-#{agent.id}-#{user.id}-#{random_string}.#{agent.remote_development_agent_config.dns_zone}" }, + { key: "GL_WORKSPACE_DOMAIN_TEMPLATE", type: :environment, value: "${PORT}-workspace-#{agent.id}-#{user.id}-#{random_string}.#{remote_development_agent_config.dns_zone}" }, { key: "GITLAB_WORKFLOW_INSTANCE_URL", type: :environment, value: Gitlab::Routing.url_helpers.root_url }, { key: "GITLAB_WORKFLOW_TOKEN_FILE", type: :environment, value: "/.workspace-data/variables/file/gl_token" }, { key: "gl_git_credential_store.sh", type: :file, value: RemoteDevelopment::Workspaces::Create::WorkspaceVariables::GIT_CREDENTIAL_STORE_SCRIPT }, @@ -90,9 +134,11 @@ let(:agent) do create(:ee_cluster_agent, :with_remote_development_agent_config, project: agent_project, - created_by_user: agent_admin_user) + created_by_user: agent_admin_user, project_id: agent_project.id) end + let(:remote_development_agent_config) { agent.remote_development_agent_config } + let(:namespace_create_remote_development_cluster_agent_mapping_create_mutation_args) do { namespace_id: common_parent_namespace.to_global_id.to_s, @@ -100,12 +146,12 @@ } end - let(:workspace_create_mutation_args) do + def workspace_create_mutation_args(cluster_agent_id) { desired_state: RemoteDevelopment::Workspaces::States::RUNNING, editor: "webide", max_hours_before_termination: 24, - cluster_agent_id: agent.to_global_id.to_s, + cluster_agent_id: cluster_agent_id, project_id: workspace_project.to_global_id.to_s, devfile_ref: devfile_ref, devfile_path: devfile_path, @@ -115,10 +161,6 @@ } end - # Agent setup - let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) } - let(:agent_token) { create(:cluster_agent_token, agent: agent) } - before do stub_licensed_features(remote_development: true) allow(SecureRandom).to receive(:alphanumeric) { random_string } @@ -140,10 +182,20 @@ def do_create_mapping ) end - def do_create_workspace # rubocop:disable Metrics/AbcSize -- We want this to stay a single method + def fetch_agent_config + get_graphql(query, current_user: agent_admin_user) + + expect( + graphql_data_at(:namespace, :remoteDevelopmentClusterAgents, :nodes, 0, :remoteDevelopmentAgentConfig) + ).to eq(expected_agent_config) + + graphql_data_at(:namespace, :remoteDevelopmentClusterAgents, :nodes, 0, :id) + end + + def do_create_workspace(cluster_agent_id) # rubocop:disable Metrics/AbcSize -- We want this to stay a single method create_mutation_response = do_graphql_mutation_post( name: :workspace_create, - input: workspace_create_mutation_args, + input: workspace_create_mutation_args(cluster_agent_id), user: user ) @@ -162,7 +214,7 @@ def do_create_workspace # rubocop:disable Metrics/AbcSize -- We want this to sta expect(workspace.namespace).to eq("gl-rd-ns-#{agent.id}-#{user.id}-#{random_string}") expect(workspace.editor).to eq("webide") expect(workspace.url).to eq(URI::HTTPS.build({ - host: "60001-#{workspace.name}.#{workspace.agent.remote_development_agent_config.dns_zone}", + host: "60001-#{workspace.name}.#{remote_development_agent_config.dns_zone}", query: { folder: "#{workspace_root}/#{workspace_project.path}" }.to_query @@ -265,8 +317,11 @@ def simulate_agentk_reconcile_post(workspace_agent_infos:, update_type:, agent_t # CREATE THE MAPPING VIA GRAPHQL API, SO WE HAVE PROPER AUTHORIZATION do_create_mapping + # FETCH THE AGENT CONFIG VIA THE GRAPHQL API, SO WE CAN USE ITS VALUES WHEN CREATING WORKSPACE + cluster_agent_id = fetch_agent_config + # DO THE INITAL WORKSPACE CREATION VIA GRAPHQL API - workspace = do_create_workspace + workspace = do_create_workspace(cluster_agent_id) # SIMULATE FIRST POLL FROM AGENTK TO PICK UP NEW WORKSPACE simulate_first_poll( -- GitLab From 81cc73b7b44b9436c488114afc2f3aae76a11a7b Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Tue, 6 Aug 2024 16:39:37 +1000 Subject: [PATCH 2/8] address comments --- ee/spec/factories/clusters/agents.rb | 8 +- .../remote_development_agent_configs.rb | 2 + ...opment_namespace_cluster_agent_mappings.rb | 2 + .../remote_development/workspace_variables.rb | 2 + .../remote_development/workspaces.rb | 2 + .../remote_development/workspaces_spec.rb | 23 +++- .../filter_argument_validator_spec.rb | 3 +- .../remote_development/integration_spec.rb | 120 +++++++++++------- .../integration_spec_helpers.rb | 32 +++-- 9 files changed, 125 insertions(+), 69 deletions(-) diff --git a/ee/spec/factories/clusters/agents.rb b/ee/spec/factories/clusters/agents.rb index 153d0b563916d0..ee57ddcef9d532 100644 --- a/ee/spec/factories/clusters/agents.rb +++ b/ee/spec/factories/clusters/agents.rb @@ -3,13 +3,7 @@ FactoryBot.define do factory :ee_cluster_agent, class: 'Clusters::Agent', parent: :cluster_agent do trait :with_remote_development_agent_config do - transient do - project_id { nil } - end - - remote_development_agent_config do - association(:remote_development_agent_config, project_id: project_id) - end + remote_development_agent_config end end end diff --git a/ee/spec/factories/remote_development/remote_development_agent_configs.rb b/ee/spec/factories/remote_development/remote_development_agent_configs.rb index 411443e489e13c..b99b737e37dcc4 100644 --- a/ee/spec/factories/remote_development/remote_development_agent_configs.rb +++ b/ee/spec/factories/remote_development/remote_development_agent_configs.rb @@ -5,5 +5,7 @@ agent factory: :cluster_agent enabled { true } dns_zone { 'workspaces.localdev.me' } + + after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb b/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb index 9f0c781d73d0dc..80f774c399448b 100644 --- a/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb +++ b/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb @@ -6,5 +6,7 @@ user factory: [:user] agent factory: [:cluster_agent, :in_group] namespace { agent.project.namespace } + + after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/workspace_variables.rb b/ee/spec/factories/remote_development/workspace_variables.rb index 04b4ed3dcbffb1..69fcffbf40f00a 100644 --- a/ee/spec/factories/remote_development/workspace_variables.rb +++ b/ee/spec/factories/remote_development/workspace_variables.rb @@ -7,5 +7,7 @@ key { 'my_key' } value { 'my_value' } variable_type { RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:file] } + + after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/workspaces.rb b/ee/spec/factories/remote_development/workspaces.rb index 0beae5e16d91bf..673e96fa300069 100644 --- a/ee/spec/factories/remote_development/workspaces.rb +++ b/ee/spec/factories/remote_development/workspaces.rb @@ -115,6 +115,8 @@ responded_to_agent_at: responded_to_agent_at ) end + + workspace.reload end trait :unprovisioned do diff --git a/ee/spec/features/remote_development/workspaces_spec.rb b/ee/spec/features/remote_development/workspaces_spec.rb index 9a01a63adffa35..0fef0da7609eb9 100644 --- a/ee/spec/features/remote_development/workspaces_spec.rb +++ b/ee/spec/features/remote_development/workspaces_spec.rb @@ -85,8 +85,19 @@ # ASSERT TERMINATE BUTTON IS AVAILABLE expect(page).to have_button('Terminate') + additional_args_for_expected_config_to_apply = + build_additional_args_for_expected_config_to_apply( + network_policy_enabled: true, + dns_zone: agent.remote_development_agent_config.dns_zone, + namespace_path: group.path, + project_name: project.path + ) + # SIMULATE FIRST POLL FROM AGENTK TO PICK UP NEW WORKSPACE - simulate_first_poll(workspace: workspace.reload) do |workspace_agent_infos:, update_type:| + simulate_first_poll( + workspace: workspace.reload, + **additional_args_for_expected_config_to_apply + ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( agent_token: agent_token, workspace_agent_infos: workspace_agent_infos, @@ -115,7 +126,10 @@ click_button 'Stop' # SIMULATE THIRD POLL FROM AGENTK TO UPDATE WORKSPACE TO STOPPING STATE - simulate_third_poll(workspace: workspace.reload) do |workspace_agent_infos:, update_type:| + simulate_third_poll( + workspace: workspace.reload, + **additional_args_for_expected_config_to_apply + ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( agent_token: agent_token, workspace_agent_infos: workspace_agent_infos, @@ -156,7 +170,10 @@ end # SIMULATE SIXTH POLL FROM AGENTK FOR FULL RECONCILE TO SHOW ALL WORKSPACES ARE SENT IN RAILS_INFOS - simulate_sixth_poll(workspace: workspace.reload) do |workspace_agent_infos:, update_type:| + simulate_sixth_poll( + workspace: workspace.reload, + **additional_args_for_expected_config_to_apply + ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( agent_token: agent_token, workspace_agent_infos: workspace_agent_infos, diff --git a/ee/spec/finders/remote_development/filter_argument_validator_spec.rb b/ee/spec/finders/remote_development/filter_argument_validator_spec.rb index b2d15b03750a96..469ebc5aea19de 100644 --- a/ee/spec/finders/remote_development/filter_argument_validator_spec.rb +++ b/ee/spec/finders/remote_development/filter_argument_validator_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require "spec_helper" +require "fast_spec_helper" +require_relative "../../../app/finders/remote_development/filter_argument_validator" RSpec.describe RemoteDevelopment::FilterArgumentValidator, feature_category: :remote_development do let(:filter_arguments) do diff --git a/ee/spec/requests/remote_development/integration_spec.rb b/ee/spec/requests/remote_development/integration_spec.rb index 0cafe1f0507a72..1a78fcbbf4b0da 100644 --- a/ee/spec/requests/remote_development/integration_spec.rb +++ b/ee/spec/requests/remote_development/integration_spec.rb @@ -13,7 +13,7 @@ # Agent setup let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) } let(:agent_token) { create(:cluster_agent_token, agent: agent) } - let(:query) do + let(:cluster_agents_query) do <<~GRAPHQL query { namespace(fullPath: "#{common_parent_namespace.full_path}") { @@ -39,18 +39,26 @@ GRAPHQL end + let(:gitlab_workspaces_proxy_namespace) { "gitlab-workspaces" } + let(:dns_zone) { "integration-spec-workspaces.localdev.me" } + let(:network_policy_enabled) { true } + let(:workspaces_per_user_quota) { 20 } + let(:workspaces_quota) { 100 } + let(:default_max_hours_before_termination) { 48 } + let(:max_hours_before_termination_limit) { 240 } + let(:expected_agent_config) do { "id" => "gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/#{remote_development_agent_config.id}", - "projectId" => remote_development_agent_config.project_id.to_s, - "enabled" => remote_development_agent_config.enabled, - "gitlabWorkspacesProxyNamespace" => remote_development_agent_config.gitlab_workspaces_proxy_namespace, - "networkPolicyEnabled" => remote_development_agent_config.network_policy_enabled, - "dnsZone" => remote_development_agent_config.dns_zone, - "workspacesPerUserQuota" => remote_development_agent_config.workspaces_per_user_quota, - "workspacesQuota" => remote_development_agent_config.workspaces_quota, - "defaultMaxHoursBeforeTermination" => remote_development_agent_config.default_max_hours_before_termination, - "maxHoursBeforeTerminationLimit" => remote_development_agent_config.max_hours_before_termination_limit + "projectId" => agent_project.id.to_s, + "enabled" => true, + "gitlabWorkspacesProxyNamespace" => "gitlab-workspaces", + "networkPolicyEnabled" => network_policy_enabled, + "dnsZone" => dns_zone, + "workspacesPerUserQuota" => workspaces_per_user_quota, + "workspacesQuota" => workspaces_quota, + "defaultMaxHoursBeforeTermination" => default_max_hours_before_termination, + "maxHoursBeforeTerminationLimit" => max_hours_before_termination_limit } end @@ -113,7 +121,7 @@ { key: "GL_EDITOR_EXTENSIONS_GALLERY_SERVICE_URL", type: :environment, value: "https://open-vsx.org/vscode/gallery" }, { key: "GL_GIT_CREDENTIAL_STORE_FILE_PATH", type: :environment, value: "/.workspace-data/variables/file/gl_git_credential_store.sh" }, { key: "GL_TOKEN_FILE_PATH", type: :environment, value: "/.workspace-data/variables/file/gl_token" }, - { key: "GL_WORKSPACE_DOMAIN_TEMPLATE", type: :environment, value: "${PORT}-workspace-#{agent.id}-#{user.id}-#{random_string}.#{remote_development_agent_config.dns_zone}" }, + { key: "GL_WORKSPACE_DOMAIN_TEMPLATE", type: :environment, value: "${PORT}-workspace-#{agent.id}-#{user.id}-#{random_string}.#{dns_zone}" }, { key: "GITLAB_WORKFLOW_INSTANCE_URL", type: :environment, value: Gitlab::Routing.url_helpers.root_url }, { key: "GITLAB_WORKFLOW_TOKEN_FILE", type: :environment, value: "/.workspace-data/variables/file/gl_token" }, { key: "gl_git_credential_store.sh", type: :file, value: RemoteDevelopment::Workspaces::Create::WorkspaceVariables::GIT_CREDENTIAL_STORE_SCRIPT }, @@ -133,11 +141,26 @@ end let(:agent) do - create(:ee_cluster_agent, :with_remote_development_agent_config, project: agent_project, - created_by_user: agent_admin_user, project_id: agent_project.id) + create(:ee_cluster_agent, project: agent_project, created_by_user: agent_admin_user, project_id: agent_project.id) end - let(:remote_development_agent_config) { agent.remote_development_agent_config } + # TODO: We should create the remote_development_agent_config via an API call to update the agent config + # with the relevant fixture values in its config file to represent a remote_development enabled agent. + # And, as we migrate the settings from the agent config file to the settings UI, we should add + # simulated API calls for setting the values that way too. + let!(:remote_development_agent_config) do + create( + :remote_development_agent_config, + agent: agent, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace, + dns_zone: dns_zone, + network_policy_enabled: network_policy_enabled, + workspaces_per_user_quota: workspaces_per_user_quota, + workspaces_quota: workspaces_quota, + default_max_hours_before_termination: default_max_hours_before_termination, + max_hours_before_termination_limit: max_hours_before_termination_limit + ) + end let(:namespace_create_remote_development_cluster_agent_mapping_create_mutation_args) do { @@ -146,21 +169,6 @@ } end - def workspace_create_mutation_args(cluster_agent_id) - { - desired_state: RemoteDevelopment::Workspaces::States::RUNNING, - editor: "webide", - max_hours_before_termination: 24, - cluster_agent_id: cluster_agent_id, - project_id: workspace_project.to_global_id.to_s, - devfile_ref: devfile_ref, - devfile_path: devfile_path, - variables: user_provided_variables.each_with_object([]) do |variable, arr| - arr << variable.merge(type: variable[:type].to_s.upcase) - end - } - end - before do stub_licensed_features(remote_development: true) allow(SecureRandom).to receive(:alphanumeric) { random_string } @@ -168,10 +176,6 @@ def workspace_create_mutation_args(cluster_agent_id) allow(Gitlab::Kas).to receive(:secret).and_return(jwt_secret) allow(workspace_project.repository).to receive_message_chain(:blob_at_branch, :data) { devfile_yaml } - - # reload projects, so any local debugging performed in the tests has the correct state - workspace_project.reload - agent_project.reload end def do_create_mapping @@ -183,7 +187,7 @@ def do_create_mapping end def fetch_agent_config - get_graphql(query, current_user: agent_admin_user) + get_graphql(cluster_agents_query, current_user: agent_admin_user) expect( graphql_data_at(:namespace, :remoteDevelopmentClusterAgents, :nodes, 0, :remoteDevelopmentAgentConfig) @@ -192,7 +196,8 @@ def fetch_agent_config graphql_data_at(:namespace, :remoteDevelopmentClusterAgents, :nodes, 0, :id) end - def do_create_workspace(cluster_agent_id) # rubocop:disable Metrics/AbcSize -- We want this to stay a single method + # rubocop:disable Metrics/AbcSize -- We want this to stay a single method + def do_create_workspace(cluster_agent_id) create_mutation_response = do_graphql_mutation_post( name: :workspace_create, input: workspace_create_mutation_args(cluster_agent_id), @@ -214,7 +219,7 @@ def do_create_workspace(cluster_agent_id) # rubocop:disable Metrics/AbcSize -- W expect(workspace.namespace).to eq("gl-rd-ns-#{agent.id}-#{user.id}-#{random_string}") expect(workspace.editor).to eq("webide") expect(workspace.url).to eq(URI::HTTPS.build({ - host: "60001-#{workspace.name}.#{remote_development_agent_config.dns_zone}", + host: "60001-#{workspace.name}.#{dns_zone}", query: { folder: "#{workspace_root}/#{workspace_project.path}" }.to_query @@ -250,6 +255,23 @@ def do_create_workspace(cluster_agent_id) # rubocop:disable Metrics/AbcSize -- W workspace end + # rubocop:enable Metrics/AbcSize + + def workspace_create_mutation_args(cluster_agent_id) + { + desired_state: RemoteDevelopment::Workspaces::States::RUNNING, + editor: "webide", + max_hours_before_termination: 24, + cluster_agent_id: cluster_agent_id, + project_id: workspace_project.to_global_id.to_s, + devfile_ref: devfile_ref, + devfile_path: devfile_path, + variables: user_provided_variables.each_with_object([]) do |variable, arr| + arr << variable.merge(type: variable[:type].to_s.upcase) + end + } + end + def do_stop_workspace(workspace) workspace_update_mutation_args = { id: global_id_of(workspace), @@ -280,10 +302,11 @@ def do_graphql_mutation_post(name:, input:, user:) def simulate_agentk_reconcile_post(workspace_agent_infos:, update_type:, agent_token:) # Add `travel(...)` based on full or partial reconciliation interval in response body - partial_reconciliation_interval_seconds = RemoteDevelopment::Settings - .get([:full_reconciliation_interval_seconds, :partial_reconciliation_interval_seconds]) - .fetch(:partial_reconciliation_interval_seconds) - .to_i + partial_reconciliation_interval_seconds = + RemoteDevelopment::Settings + .get([:full_reconciliation_interval_seconds, :partial_reconciliation_interval_seconds]) + .fetch(:partial_reconciliation_interval_seconds) + .to_i travel(partial_reconciliation_interval_seconds) jwt_token = JWT.encode( @@ -323,11 +346,18 @@ def simulate_agentk_reconcile_post(workspace_agent_infos:, update_type:, agent_t # DO THE INITAL WORKSPACE CREATION VIA GRAPHQL API workspace = do_create_workspace(cluster_agent_id) + additional_args_for_expected_config_to_apply = + build_additional_args_for_expected_config_to_apply( + network_policy_enabled: network_policy_enabled, + dns_zone: dns_zone, + namespace_path: workspace_project_namespace.full_path, + project_name: workspace_project_name + ) + # SIMULATE FIRST POLL FROM AGENTK TO PICK UP NEW WORKSPACE simulate_first_poll( workspace: workspace.reload, - namespace_path: workspace_namespace_path, - project_name: workspace_project_name + **additional_args_for_expected_config_to_apply ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( workspace_agent_infos: workspace_agent_infos, @@ -354,8 +384,7 @@ def simulate_agentk_reconcile_post(workspace_agent_infos:, update_type:, agent_t # SIMULATE THIRD POLL FROM AGENTK TO UPDATE WORKSPACE TO STOPPING STATE simulate_third_poll( workspace: workspace.reload, - namespace_path: workspace_namespace_path, - project_name: workspace_project_name + **additional_args_for_expected_config_to_apply ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( agent_token: agent_token, @@ -385,8 +414,7 @@ def simulate_agentk_reconcile_post(workspace_agent_infos:, update_type:, agent_t # SIMULATE SIXTH POLL FROM AGENTK FOR FULL RECONCILE TO SHOW ALL WORKSPACES ARE SENT IN RAILS_INFOS simulate_sixth_poll( workspace: workspace.reload, - namespace_path: workspace_namespace_path, - project_name: workspace_project_name + **additional_args_for_expected_config_to_apply ) do |workspace_agent_infos:, update_type:| simulate_agentk_reconcile_post( agent_token: agent_token, diff --git a/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb b/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb index 922414e69cc89c..a9c24bc003db99 100644 --- a/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb +++ b/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb @@ -2,10 +2,23 @@ module RemoteDevelopment module IntegrationSpecHelpers + def build_additional_args_for_expected_config_to_apply( + network_policy_enabled:, + dns_zone:, + namespace_path: workspace_project_namespace.full_path, + project_name: workspace_project_name + ) + { + dns_zone: dns_zone, + namespace_path: namespace_path, + project_name: project_name, + include_network_policy: network_policy_enabled + } + end + def simulate_first_poll( workspace:, - namespace_path: 'test-group', - project_name: 'test-project', + **additional_args_for_create_config_to_apply, &simulate_agentk_reconcile_post_block ) # SIMULATE FIRST POLL REQUEST FROM AGENTK TO GET NEW WORKSPACE @@ -32,8 +45,7 @@ def simulate_first_poll( workspace: workspace, started: true, include_all_resources: true, - namespace_path: namespace_path, - project_name: project_name + **additional_args_for_create_config_to_apply ) config_to_apply = info.fetch(:config_to_apply) @@ -77,8 +89,7 @@ def simulate_second_poll( def simulate_third_poll( workspace:, - namespace_path: 'test-group', - project_name: 'test-project', + **additional_args_for_create_config_to_apply, &simulate_agentk_reconcile_post_block ) # SIMULATE THIRD POLL REQUEST FROM AGENTK TO UPDATE WORKSPACE TO STOPPING STATE @@ -113,8 +124,7 @@ def simulate_third_poll( expected_config_to_apply = create_config_to_apply( workspace: workspace, started: false, - namespace_path: namespace_path, - project_name: project_name + **additional_args_for_create_config_to_apply ) config_to_apply = info.fetch(:config_to_apply) @@ -173,8 +183,7 @@ def simulate_fifth_poll(&simulate_agentk_reconcile_post_block) def simulate_sixth_poll( workspace:, - namespace_path: 'test-group', - project_name: 'test-project', + **additional_args_for_create_config_to_apply, &simulate_agentk_reconcile_post_block ) # SIMULATE FIFTH POLL FROM AGENTK FOR FULL RECONCILE TO SHOW ALL WORKSPACES ARE SENT IN RAILS_INFOS @@ -207,8 +216,7 @@ def simulate_sixth_poll( workspace: workspace, started: false, include_all_resources: true, - namespace_path: namespace_path, - project_name: project_name + **additional_args_for_create_config_to_apply ) config_to_apply = info.fetch(:config_to_apply) -- GitLab From fbc5bb8c98e8556b925241b9ffb94998dda40b1d Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Tue, 6 Aug 2024 16:42:33 +1000 Subject: [PATCH 3/8] address comments --- .../filter_argument_validator.rb | 15 +++++++++++++++ .../agent_config_for_agent_resolver.rb | 7 +++++++ .../graphql/ee/types/clusters/agent_type_spec.rb | 6 ------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ee/app/finders/remote_development/filter_argument_validator.rb b/ee/app/finders/remote_development/filter_argument_validator.rb index 28665bacc2a105..dfcc8d45f6113e 100644 --- a/ee/app/finders/remote_development/filter_argument_validator.rb +++ b/ee/app/finders/remote_development/filter_argument_validator.rb @@ -2,6 +2,14 @@ module RemoteDevelopment class FilterArgumentValidator + # + # Validate filter arguments against the given types. + # + # @param [Hash] types Types of arguments passed in the filter + # @param [Hash] filter_arguments Filter arguments to be validated + # + # @return [Boolean] Whether the arguments are valid + # def self.validate_filter_argument_types!(types, filter_arguments) errors = [] @@ -13,6 +21,13 @@ def self.validate_filter_argument_types!(types, filter_arguments) raise errors.join(", ") if errors.present? end + # + # Validate that at least one filter argument is provided. + # + # @param [Hash] **filter_arguments Filter arguments to be validated + # + # @return [Boolean] Whether at least one filter argument is provided + # def self.validate_at_least_one_filter_argument_provided!(**filter_arguments) no_filter_arguments_provided = filter_arguments.values.flatten.empty? raise ArgumentError, "At least one filter argument must be provided" if no_filter_arguments_provided diff --git a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb index a2c6600294e027..5ca49a96b7124e 100644 --- a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb @@ -10,6 +10,13 @@ class AgentConfigForAgentResolver < ::Resolvers::BaseResolver alias_method :agent, :object + # + # Resolve the remote development agent config for the given agent. + # + # @param [Hash] **_args The arguments passed to the resolver, and do not in use here + # + # @return [RemoteDevelopmentAgentConfig] The remote development agent config for the given agent + # def resolve_with_lookahead(**_args) unless License.feature_available?(:remote_development) raise_resource_not_available_error! "'remote_development' licensed feature is not available" diff --git a/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb b/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb index 735f344fcdfb76..63fce64edd8708 100644 --- a/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb +++ b/ee/spec/graphql/ee/types/clusters/agent_type_spec.rb @@ -106,12 +106,6 @@ result.dig('data', 'namespace', 'remoteDevelopmentClusterAgents', 'nodes', 0, 'remoteDevelopmentAgentConfig') end - context 'when user is not logged in' do - let(:current_user) { nil } - - it { is_expected.to be_nil } - end - context 'when user is logged in' do let(:current_user) { user } let(:expected_default_max_hours_before_termination) do -- GitLab From fcf098859767e6e3dc1ce445a29ce6f1325c9d36 Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Wed, 7 Aug 2024 17:55:07 +1000 Subject: [PATCH 4/8] remove factory reload --- .../remote_development/remote_development_agent_configs.rb | 2 -- .../remote_development_namespace_cluster_agent_mappings.rb | 3 +-- ee/spec/factories/remote_development/workspace_variables.rb | 2 -- ee/spec/factories/remote_development/workspaces.rb | 2 -- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ee/spec/factories/remote_development/remote_development_agent_configs.rb b/ee/spec/factories/remote_development/remote_development_agent_configs.rb index b99b737e37dcc4..411443e489e13c 100644 --- a/ee/spec/factories/remote_development/remote_development_agent_configs.rb +++ b/ee/spec/factories/remote_development/remote_development_agent_configs.rb @@ -5,7 +5,5 @@ agent factory: :cluster_agent enabled { true } dns_zone { 'workspaces.localdev.me' } - - after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb b/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb index 80f774c399448b..5efa67700f28d0 100644 --- a/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb +++ b/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb @@ -6,7 +6,6 @@ user factory: [:user] agent factory: [:cluster_agent, :in_group] namespace { agent.project.namespace } - - after(:create, &:reload) + # after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/workspace_variables.rb b/ee/spec/factories/remote_development/workspace_variables.rb index 69fcffbf40f00a..04b4ed3dcbffb1 100644 --- a/ee/spec/factories/remote_development/workspace_variables.rb +++ b/ee/spec/factories/remote_development/workspace_variables.rb @@ -7,7 +7,5 @@ key { 'my_key' } value { 'my_value' } variable_type { RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:file] } - - after(:create, &:reload) end end diff --git a/ee/spec/factories/remote_development/workspaces.rb b/ee/spec/factories/remote_development/workspaces.rb index 673e96fa300069..0beae5e16d91bf 100644 --- a/ee/spec/factories/remote_development/workspaces.rb +++ b/ee/spec/factories/remote_development/workspaces.rb @@ -115,8 +115,6 @@ responded_to_agent_at: responded_to_agent_at ) end - - workspace.reload end trait :unprovisioned do -- GitLab From ac81152fee542b724f3d4dfbe62c215aff326b73 Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Wed, 7 Aug 2024 21:14:23 +1000 Subject: [PATCH 5/8] make preloads private --- .../remote_development/agent_config_for_agent_resolver.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb index 5ca49a96b7124e..7f9dd63f22620f 100644 --- a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb @@ -35,6 +35,8 @@ def resolve_with_lookahead(**_args) end end + private + def preloads { default_max_hours_before_termination: [:default_max_hours_before_termination], @@ -42,8 +44,6 @@ def preloads } end - private - def can_read_remote_development_agent_config? # noinspection RubyNilAnalysis - This is because the superclass #current_user uses #[], which can return nil current_user.can?(:read_cluster_agent, agent) -- GitLab From 8be6c254d4ff434dc3af56a49d5526087f560f57 Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Thu, 8 Aug 2024 17:28:37 +1000 Subject: [PATCH 6/8] remove preloads --- .../remote_development/agent_config_for_agent_resolver.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb index 7f9dd63f22620f..12050da00a8985 100644 --- a/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/agent_config_for_agent_resolver.rb @@ -37,13 +37,6 @@ def resolve_with_lookahead(**_args) private - def preloads - { - default_max_hours_before_termination: [:default_max_hours_before_termination], - id: [:id] - } - end - def can_read_remote_development_agent_config? # noinspection RubyNilAnalysis - This is because the superclass #current_user uses #[], which can return nil current_user.can?(:read_cluster_agent, agent) -- GitLab From 9fde933cf2d38874ae85d3fec761b7e86aadb099 Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Fri, 9 Aug 2024 12:00:43 +1000 Subject: [PATCH 7/8] add resolver spec --- .../remote_development_agent_config_spec.rb | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb diff --git a/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb b/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb new file mode 100644 index 00000000000000..9d1bef0547237c --- /dev/null +++ b/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative './shared' + +RSpec.describe 'Query.namespace.remote_development_cluster_agents(filter: AVAILABLE)', feature_category: :remote_development do + include GraphqlHelpers + include StubFeatureFlags + + let_it_be(:user) { create(:user) } + let(:agent_config_id) { subject['id'] } + let_it_be(:current_user) { user } + let_it_be(:available_agent) do + create(:ee_cluster_agent, :in_group, :with_remote_development_agent_config).tap do |agent| + agent.project.namespace.add_maintainer(user) + end + end + + let_it_be(:namespace) { available_agent.project.namespace } + let_it_be(:namespace_agent_mapping) do + create( + :remote_development_namespace_cluster_agent_mapping, + user: user, + agent: available_agent, + namespace: namespace + ) + end + + let_it_be(:query) do + %( + query { + namespace(fullPath: "#{namespace.full_path}") { + remoteDevelopmentClusterAgents(filter: AVAILABLE) { + nodes { + remoteDevelopmentAgentConfig { + id + defaultMaxHoursBeforeTermination + } + } + } + } + } + ) + end + + subject do + graphql_data.dig('namespace', 'remoteDevelopmentClusterAgents', 'nodes', 0, 'remoteDevelopmentAgentConfig') + end + + before do + stub_licensed_features(remote_development: true) + end + + context 'when the params are valid' do + let(:expected_agent_config_id) do + "gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/" \ + "#{available_agent.remote_development_agent_config.id}" + end + + it 'returns cluster agents that are available for remote development in the namespace' do + get_graphql(query, current_user: current_user) + + expect(agent_config_id).to eq(expected_agent_config_id) + end + end + + include_examples "checks for remote_development licensed feature" +end -- GitLab From ecb4973f10d2e31265eda26f48a50556f09d6f99 Mon Sep 17 00:00:00 2001 From: Zhaochen Li Date: Mon, 12 Aug 2024 09:54:05 +1000 Subject: [PATCH 8/8] address comments --- .../remote_development_agent_config_spec.rb | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb b/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb index 9d1bef0547237c..cfbb9cd11d9c94 100644 --- a/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb +++ b/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb @@ -16,6 +16,7 @@ end end + let_it_be(:agent_config) { available_agent.remote_development_agent_config } let_it_be(:namespace) { available_agent.project.namespace } let_it_be(:namespace_agent_mapping) do create( @@ -26,20 +27,25 @@ ) end - let_it_be(:query) do - %( - query { - namespace(fullPath: "#{namespace.full_path}") { - remoteDevelopmentClusterAgents(filter: AVAILABLE) { - nodes { - remoteDevelopmentAgentConfig { - id - defaultMaxHoursBeforeTermination - } - } - } + let(:fields) do + <<~QUERY + nodes { + remoteDevelopmentAgentConfig { + #{all_graphql_fields_for('remote_development_agent_config'.classify, max_depth: 1)} } } + QUERY + end + + let(:query) do + graphql_query_for( + :namespace, + { full_path: namespace.full_path }, + query_graphql_field( + :remote_development_cluster_agents, + { filter: :AVAILABLE }, + fields + ) ) end @@ -54,7 +60,24 @@ context 'when the params are valid' do let(:expected_agent_config_id) do "gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/" \ - "#{available_agent.remote_development_agent_config.id}" + "#{agent_config.id}" + end + + let(:expected_agent_config) do + { + 'id' => expected_agent_config_id, + 'projectId' => agent_config.project_id, + 'enabled' => agent_config.enabled, + 'dnsZone' => agent_config.dns_zone, + 'networkPolicyEnabled' => agent_config.network_policy_enabled, + 'gitlabWorkspacesProxyNamespace' => agent_config.gitlab_workspaces_proxy_namespace, + 'workspacesQuota' => agent_config.workspaces_quota, + 'workspacesPerUserQuota' => agent_config.workspaces_per_user_quota, + 'defaultMaxHoursBeforeTermination' => agent_config.default_maxHours_before_termination, + 'maxHoursBeforeTerminationLimit' => agent_config.max_hours_before_termination_limit, + 'createdAt' => agent_config.created_at, + 'updatedAt' => agent_config.updated_at + } end it 'returns cluster agents that are available for remote development in the namespace' do -- GitLab