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)