diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 12be1235faa7cd7e73b908e9d673d387588153e1..6f3cb9c6a26d241a26d25453e6f8194f6df91453 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 0000000000000000000000000000000000000000..9f784626653f032da64a8741274e11c79c32817b --- /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 0000000000000000000000000000000000000000..dfcc8d45f6113e1c00e2f79d9ef1d700f9d4da84 --- /dev/null +++ b/ee/app/finders/remote_development/filter_argument_validator.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +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 = [] + + 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 + + # + # 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 + end + end +end diff --git a/ee/app/finders/remote_development/workspaces_finder.rb b/ee/app/finders/remote_development/workspaces_finder.rb index c7496408d03c59e09735b8ff43b2fe3d230fe4d3..9cbb6b1d02e9d5250fd1d6f6f6f03c2e5bda2ee3 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 66f7655757c4f81af0dfd93d25b271c6cccb7e69..2401fe4a2ed07c931f85d863c4d3518acbb6a8aa 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 0000000000000000000000000000000000000000..12050da00a8985abc896c58423eade4d86b3aba4 --- /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 + + # + # 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" + 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 + + 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 0000000000000000000000000000000000000000..60b7b5462a9e5baaf0f99c0d4fc623f75411776f --- /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 d533fce18043b70e6ac97b6c1c2261c37f3fdb71..1d20e755254096b4886d08d4aac00d6d51031381 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 0000000000000000000000000000000000000000..61e726be22f2e853dec6443a23b127a2a8afda3e --- /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/remote_development/remote_development_namespace_cluster_agent_mappings.rb b/ee/spec/factories/remote_development/remote_development_namespace_cluster_agent_mappings.rb index 9f0c781d73d0dcfe007d9b62a044e5b098159a0c..5efa67700f28d0bff3c825df54fd24a1795dccb7 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,6 @@ user factory: [:user] agent factory: [:cluster_agent, :in_group] namespace { agent.project.namespace } + # after(:create, &:reload) end end diff --git a/ee/spec/features/remote_development/workspaces_spec.rb b/ee/spec/features/remote_development/workspaces_spec.rb index 9a01a63adffa35c21d0a930274c490f1e69d40e5..0fef0da7609eb97e3c06f0a78c7bd1682cd4dde5 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/agent_configs_finder_spec.rb b/ee/spec/finders/remote_development/agent_configs_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3cd9f05b0bc84dd5fd9b609487f4dcaaf2f6b752 --- /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 0000000000000000000000000000000000000000..469ebc5aea19de38ddce0a2dbc012d8cbd2b0008 --- /dev/null +++ b/ee/spec/finders/remote_development/filter_argument_validator_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +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 + { + 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 5dd74c950d9dccdbb1bec248936436022aa2ae4a..556aff610d7ee03b64bc8ecd1eabcb1d240235b3 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 1c750bc04ecf858e8821cd402e7ed28ce61eea5e..63fce64edd870844ded93629786cb05b7e5a589c 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,60 @@ 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 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 0000000000000000000000000000000000000000..d8d15b57288a811b9e3eafc4f177e8e3ac57b417 --- /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 c5ea0d0ab73c5b01867a9bf53c9c2873224c72fb..e8949769adfef2719bf892d9d362e633d88bb152 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 03270777309bd6ffa2335e39731a6ea218805976..be2230c37e835c21469a546fe2670fe1e50c2002 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 0000000000000000000000000000000000000000..ea364205a3eb3b356cc9332cfbf2db6d6923c7b1 --- /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/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 0000000000000000000000000000000000000000..cfbb9cd11d9c948edcac0a7fd64b820455fa2abf --- /dev/null +++ b/ee/spec/requests/api/graphql/remote_development/namespace/remote_development_cluster_agents/remote_development_agent_config_spec.rb @@ -0,0 +1,91 @@ +# 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(:agent_config) { available_agent.remote_development_agent_config } + 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(: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 + + 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/" \ + "#{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 + 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 diff --git a/ee/spec/requests/remote_development/integration_spec.rb b/ee/spec/requests/remote_development/integration_spec.rb index be9b0239ccb042e15d9ffa8650c06f0553682e37..1a78fcbbf4b0da1dd47bf624a2c260a0f4d48070 100644 --- a/ee/spec/requests/remote_development/integration_spec.rb +++ b/ee/spec/requests/remote_development/integration_spec.rb @@ -10,6 +10,58 @@ 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(:cluster_agents_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(: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" => 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 + 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 +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}.#{agent.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 }, @@ -89,8 +141,25 @@ end let(:agent) do - create(:ee_cluster_agent, :with_remote_development_agent_config, project: agent_project, - created_by_user: agent_admin_user) + create(:ee_cluster_agent, project: agent_project, created_by_user: agent_admin_user, project_id: agent_project.id) + end + + # 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 @@ -100,25 +169,6 @@ } end - let(:workspace_create_mutation_args) do - { - desired_state: RemoteDevelopment::Workspaces::States::RUNNING, - editor: "webide", - max_hours_before_termination: 24, - cluster_agent_id: agent.to_global_id.to_s, - 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 - - # 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 } @@ -126,10 +176,6 @@ 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 @@ -140,10 +186,21 @@ 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(cluster_agents_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 + + # 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, + input: workspace_create_mutation_args(cluster_agent_id), user: user ) @@ -162,7 +219,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}.#{dns_zone}", query: { folder: "#{workspace_root}/#{workspace_project.path}" }.to_query @@ -198,6 +255,23 @@ def do_create_workspace # rubocop:disable Metrics/AbcSize -- We want this to sta 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), @@ -228,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( @@ -265,14 +340,24 @@ 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) + + 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, @@ -299,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, @@ -330,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 922414e69cc89cefefe9583d92b83ecc76205ec7..a9c24bc003db99beafccd0c8699a33223ee6a492 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)