diff --git a/ee/app/models/remote_development/workspace.rb b/ee/app/models/remote_development/workspace.rb index 692fc04ec0710e9182a59dbac5d013f87632f2e6..53ca8b034a54e646a3e9819152a9dece11c6e4a6 100644 --- a/ee/app/models/remote_development/workspace.rb +++ b/ee/app/models/remote_development/workspace.rb @@ -27,8 +27,8 @@ class Workspace < ApplicationRecord has_many :user_provided_workspace_variables, -> { user_provided.with_variable_type_environment.order_id_desc }, class_name: "RemoteDevelopment::WorkspaceVariable", inverse_of: :workspace - has_one :agentk_state, inverse_of: :workspace, class_name: 'RemoteDevelopment::WorkspaceAgentkState' - has_one :workspace_token, inverse_of: :workspace, class_name: 'RemoteDevelopment::WorkspaceToken' + has_one :workspace_agentk_state, inverse_of: :workspace, class_name: "RemoteDevelopment::WorkspaceAgentkState" + has_one :workspace_token, inverse_of: :workspace, class_name: "RemoteDevelopment::WorkspaceToken" validates :user, presence: true validates :agent, presence: true diff --git a/ee/app/models/remote_development/workspace_agentk_state.rb b/ee/app/models/remote_development/workspace_agentk_state.rb index 5b053d7baa5c124846e7961b31873474bc0ca9a7..e6c8469513f11e3ffcdc1df22d0b77ac118e8dd1 100644 --- a/ee/app/models/remote_development/workspace_agentk_state.rb +++ b/ee/app/models/remote_development/workspace_agentk_state.rb @@ -2,11 +2,21 @@ module RemoteDevelopment class WorkspaceAgentkState < ApplicationRecord - belongs_to :workspace, inverse_of: :agentk_state + belongs_to :workspace, inverse_of: :workspace_agentk_state belongs_to :project, inverse_of: :workspace_agentk_states validates :workspace_id, presence: true validates :project_id, presence: true validates :desired_config, presence: true + validate :desired_config_must_be_array + + # Validates that desired_config is an array when present + # @return [void] + def desired_config_must_be_array + return if desired_config.blank? + return if desired_config.is_a?(Array) + + errors.add(:desired_config, "must be an array") + end end end diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor.rb index 157c7dd0350d374c96a86a792ee54d46610f656e..e31a93107f30e3af19ee3ebee9b80b0ed9a209b4 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor.rb +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor.rb @@ -6,6 +6,7 @@ module Create module DesiredConfig class ConfigValuesExtractor include States + include WorkspaceOperationsConstants # @param [Hash] context # @return [Hash] @@ -31,37 +32,50 @@ def self.extract(context) # TODO: Fix this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/541902 shared_namespace = "" if shared_namespace.nil? + workspace_inventory_name = "#{workspace_name}#{WORKSPACE_INVENTORY}" + secrets_inventory_name = "#{workspace_name}#{SECRETS_INVENTORY}" + extra_annotations = { "workspaces.gitlab.com/host-template": domain_template.to_s, "workspaces.gitlab.com/id": workspace_id.to_s, # NOTE: This annotation is added to cause the workspace to restart whenever the max resources change "workspaces.gitlab.com/max-resources-per-workspace-sha256": max_resources_per_workspace_sha256 } + partial_reconcile_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } agent_annotations = workspaces_agent_config.annotations - common_annotations = agent_annotations.merge(extra_annotations) + common_annotations = deep_sort_and_symbolize_hashes(agent_annotations.merge(extra_annotations)) + common_annotations_for_partial_reconciliation = + deep_sort_and_symbolize_hashes(common_annotations.merge(partial_reconcile_annotation)) + secrets_inventory_annotations = deep_sort_and_symbolize_hashes( + common_annotations.merge("config.k8s.io/owning-inventory": secrets_inventory_name) + ) + workspace_inventory_annotations = deep_sort_and_symbolize_hashes( + common_annotations.merge("config.k8s.io/owning-inventory": workspace_inventory_name) + ) + workspace_inventory_annotations_for_partial_reconciliation = deep_sort_and_symbolize_hashes( + workspace_inventory_annotations.merge(partial_reconcile_annotation) + ) agent_labels = workspaces_agent_config.labels labels = agent_labels.merge({ "agent.gitlab.com/id": workspaces_agent_id.to_s }) # TODO: Unconditionally add this label in https://gitlab.com/gitlab-org/gitlab/-/issues/535197 labels["workspaces.gitlab.com/id"] = workspace_id.to_s if shared_namespace.present? - workspace_inventory_name = "#{workspace_name}-workspace-inventory" - secrets_inventory_name = "#{workspace_name}-secrets-inventory" scripts_configmap_name = "#{workspace_name}-scripts-configmap" context.merge({ # Please keep alphabetized allow_privilege_escalation: workspaces_agent_config.allow_privilege_escalation, - common_annotations: deep_sort_and_symbolize_hashes(common_annotations), - default_resources_per_workspace_container: - default_resources_per_workspace_container, + common_annotations: common_annotations, + common_annotations_for_partial_reconciliation: common_annotations_for_partial_reconciliation, + default_resources_per_workspace_container: default_resources_per_workspace_container, default_runtime_class: workspaces_agent_config.default_runtime_class, domain_template: domain_template, # NOTE: update env_secret_name to "#{workspace.name}-environment". This is to ensure naming consistency. # Changing it now would require migration from old config version to a new one. # Update this when a new desired config generator is created for some other reason. - env_secret_name: "#{workspace_name}-env-var", - file_secret_name: "#{workspace_name}-file", + env_secret_name: "#{workspace_name}#{ENV_VAR_SECRET_SUFFIX}", + file_secret_name: "#{workspace_name}#{FILE_SECRET_SUFFIX}", gitlab_workspaces_proxy_namespace: workspaces_agent_config.gitlab_workspaces_proxy_namespace, image_pull_secrets: deep_sort_and_symbolize_hashes(workspaces_agent_config.image_pull_secrets), labels: deep_sort_and_symbolize_hashes(labels), @@ -70,17 +84,13 @@ def self.extract(context) network_policy_enabled: workspaces_agent_config.network_policy_enabled, replicas: workspace_desired_state_is_running ? 1 : 0, scripts_configmap_name: scripts_configmap_name, - secrets_inventory_annotations: - deep_sort_and_symbolize_hashes( - common_annotations.merge("config.k8s.io/owning-inventory": secrets_inventory_name) - ), + secrets_inventory_annotations: secrets_inventory_annotations, secrets_inventory_name: secrets_inventory_name, shared_namespace: shared_namespace, use_kubernetes_user_namespaces: workspaces_agent_config.use_kubernetes_user_namespaces, - workspace_inventory_annotations: - deep_sort_and_symbolize_hashes( - common_annotations.merge("config.k8s.io/owning-inventory": workspace_inventory_name) - ), + workspace_inventory_annotations: workspace_inventory_annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, workspace_inventory_name: workspace_inventory_name }).sort.to_h end diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb index 893076083c24731e2bf25c6946a96cac95881f38..d93d1f464099f94018d5778d4adcfa367870d6e9 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb @@ -5,13 +5,16 @@ module WorkspaceOperations module Create module DesiredConfig class DevfileParserGetter + include WorkspaceOperationsConstants + # @param [Hash] context # @return [Hash] def self.get(context) context => { logger: logger, processed_devfile_yaml: processed_devfile_yaml, - workspace_inventory_annotations: annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, domain_template: domain_template, labels: labels, workspace_name: workspace_name, @@ -26,7 +29,7 @@ def self.get(context) workspace_name, workspace_namespace, YAML.dump(labels.deep_stringify_keys), - YAML.dump(annotations.deep_stringify_keys), + YAML.dump(workspace_inventory_annotations_for_partial_reconciliation.deep_stringify_keys), replicas, domain_template, 'none' diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb index 2d6256de6af6229c531eb260aeefa4b2676f39d4..6e4aa1e09b2008c316fecdd6d7ffaaecf065709d 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb @@ -5,35 +5,42 @@ module WorkspaceOperations module Create module DesiredConfig class DevfileResourceAppender + include WorkspaceOperationsConstants + + # @param [Hash] context + # @return [Hash] def self.append(context) context => { - desired_config_array: desired_config_array, - workspace_name: workspace_name, - workspace_namespace: workspace_namespace, - labels: labels, - workspace_inventory_annotations: workspace_inventory_annotations, common_annotations: common_annotations, - workspace_inventory_name: workspace_inventory_name, - secrets_inventory_name: secrets_inventory_name, - secrets_inventory_annotations: secrets_inventory_annotations, - scripts_configmap_name: scripts_configmap_name, - processed_devfile_yaml: processed_devfile_yaml, + common_annotations_for_partial_reconciliation: common_annotations_for_partial_reconciliation, + desired_config_array: desired_config_array, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name, gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace, - network_policy_enabled: network_policy_enabled, - network_policy_egress: network_policy_egress, image_pull_secrets: image_pull_secrets, + labels: labels, max_resources_per_workspace: max_resources_per_workspace, + network_policy_egress: network_policy_egress, + network_policy_enabled: network_policy_enabled, + processed_devfile_yaml: processed_devfile_yaml, + scripts_configmap_name: scripts_configmap_name, + secrets_inventory_annotations: secrets_inventory_annotations, + secrets_inventory_name: secrets_inventory_name, shared_namespace: shared_namespace, - env_secret_name: env_secret_name, - file_secret_name: file_secret_name + workspace_inventory_annotations: workspace_inventory_annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, + workspace_inventory_name: workspace_inventory_name, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace } - append_inventory_config_map( + append_inventory_configmap( desired_config_array: desired_config_array, name: workspace_inventory_name, namespace: workspace_namespace, labels: labels, - annotations: common_annotations, + annotations: common_annotations_for_partial_reconciliation, prepend: true ) @@ -43,7 +50,7 @@ def self.append(context) namespace: workspace_namespace, image_pull_secrets: image_pull_secrets, labels: labels, - annotations: workspace_inventory_annotations + annotations: workspace_inventory_annotations_for_partial_reconciliation ) append_network_policy( @@ -54,7 +61,7 @@ def self.append(context) network_policy_enabled: network_policy_enabled, network_policy_egress: network_policy_egress, labels: labels, - annotations: workspace_inventory_annotations + annotations: workspace_inventory_annotations_for_partial_reconciliation ) append_scripts_resources( @@ -63,10 +70,10 @@ def self.append(context) name: scripts_configmap_name, namespace: workspace_namespace, labels: labels, - annotations: workspace_inventory_annotations + annotations: workspace_inventory_annotations_for_partial_reconciliation ) - append_inventory_config_map( + append_inventory_configmap( desired_config_array: desired_config_array, name: secrets_inventory_name, namespace: workspace_namespace, @@ -108,8 +115,9 @@ def self.append(context) # @param [String] namespace # @param [Hash] labels # @param [Hash] annotations + # @param [Boolean] prepend -- If true, prepend the configmap to the desired_config_array # @return [void] - def self.append_inventory_config_map( + def self.append_inventory_configmap( desired_config_array:, name:, namespace:, @@ -119,7 +127,7 @@ def self.append_inventory_config_map( ) extra_labels = { "cli-utils.sigs.k8s.io/inventory-id": name } - config_map = { + configmap = { kind: "ConfigMap", apiVersion: "v1", metadata: { @@ -131,9 +139,9 @@ def self.append_inventory_config_map( } if prepend - desired_config_array.prepend(config_map) + desired_config_array.prepend(configmap) else - desired_config_array.append(config_map) + desired_config_array.append(configmap) end nil @@ -406,7 +414,7 @@ def self.append_image_pull_secrets_service_account( nil end - private_class_method :append_inventory_config_map, + private_class_method :append_inventory_configmap, :append_secret, :append_network_policy, :append_scripts_resources, diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb index e11b491fd4714bc325327518b3003e3085038680..fbcf6bab3054090e2e9ad5c6d20de62f018432f4 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb @@ -7,6 +7,8 @@ module DesiredConfig class DevfileResourceModifier include RemoteDevelopment::WorkspaceOperations::Create::CreateConstants + # @param [Hash] context + # @return [Hash] def self.modify(context) context => { workspace_name: String => workspace_name, @@ -19,24 +21,28 @@ def self.modify(context) file_secret_name: String => file_secret_name, } - desired_config_array = set_host_users( + set_host_users( desired_config_array: desired_config_array, use_kubernetes_user_namespaces: use_kubernetes_user_namespaces ) - desired_config_array = set_runtime_class( + + set_runtime_class( desired_config_array: desired_config_array, runtime_class_name: default_runtime_class ) - desired_config_array = set_security_context( + + set_security_context( desired_config_array: desired_config_array, allow_privilege_escalation: allow_privilege_escalation ) - desired_config_array = patch_default_resources( + + patch_default_resources( desired_config_array: desired_config_array, default_resources_per_workspace_container: default_resources_per_workspace_container ) - desired_config_array = inject_secrets( + + inject_secrets( desired_config_array: desired_config_array, env_secret_name: env_secret_name, file_secret_name: file_secret_name @@ -50,35 +56,32 @@ def self.modify(context) context.merge({ desired_config_array: desired_config_array }) end + # @param [Array] desired_config_array + # @param [Boolean] use_kubernetes_user_namespaces + # @return [void] def self.set_host_users(desired_config_array:, use_kubernetes_user_namespaces:) # NOTE: Not setting the use_kubernetes_user_namespaces always since setting it now would require migration # from old config version to a new one. Set this field always # when a new devfile parser is created for some other reason. return desired_config_array unless use_kubernetes_user_namespaces - desired_config_array.each do |workspace_resource| - next unless workspace_resource[:kind] == 'Deployment' + find_pod_spec(desired_config_array)[:hostUsers] = use_kubernetes_user_namespaces - workspace_resource[:spec][:template][:spec][:hostUsers] = use_kubernetes_user_namespaces - end - desired_config_array + nil end # @param [Array] desired_config_array # @param [String] runtime_class_name - # @return [Array] + # @return [void] def self.set_runtime_class(desired_config_array:, runtime_class_name:) # NOTE: Not setting the runtime_class_name always since changing it now would require migration # from old config version to a new one. Update this field to `runtime_class_name.presence` # when a new devfile parser is created for some other reason. return desired_config_array if runtime_class_name.empty? - desired_config_array.each do |workspace_resource| - next unless workspace_resource[:kind] == 'Deployment' + find_pod_spec(desired_config_array)[:runtimeClassName] = runtime_class_name - workspace_resource[:spec][:template][:spec][:runtimeClassName] = runtime_class_name - end - desired_config_array + nil end # Devfile library allows specifying the security context of pods/containers as mentioned in @@ -92,114 +95,121 @@ def self.set_runtime_class(desired_config_array:, runtime_class_name:) # @param [Array] desired_config_array # @param [Boolean] allow_privilege_escalation # @param [Boolean] use_kubernetes_user_namespaces - # @return [Array] + # @return [void] def self.set_security_context( desired_config_array:, allow_privilege_escalation: ) - desired_config_array.each do |workspace_resource| - next unless workspace_resource[:kind] == 'Deployment' - - pod_security_context = { - runAsNonRoot: true, - runAsUser: RUN_AS_USER, - fsGroup: 0, - fsGroupChangePolicy: 'OnRootMismatch' - } - container_security_context = { - allowPrivilegeEscalation: allow_privilege_escalation, - privileged: false, - runAsNonRoot: true, - runAsUser: RUN_AS_USER - } + pod_security_context = { + runAsNonRoot: true, + runAsUser: RUN_AS_USER, + fsGroup: 0, + fsGroupChangePolicy: 'OnRootMismatch' + } + container_security_context = { + allowPrivilegeEscalation: allow_privilege_escalation, + privileged: false, + runAsNonRoot: true, + runAsUser: RUN_AS_USER + } - pod_spec = workspace_resource[:spec][:template][:spec] - # Explicitly set security context for the pod - pod_spec[:securityContext] = pod_security_context - # Explicitly set security context for all containers - pod_spec[:containers].each do |container| - container[:securityContext] = container_security_context - end - # Explicitly set security context for all init containers - pod_spec[:initContainers].each do |init_container| - init_container[:securityContext] = container_security_context - end + pod_spec = find_pod_spec(desired_config_array) + # Explicitly set security context for the pod + pod_spec[:securityContext] = pod_security_context + # Explicitly set security context for all containers + pod_spec[:containers].each do |container| + container[:securityContext] = container_security_context end - desired_config_array + # Explicitly set security context for all init containers + pod_spec[:initContainers].each do |init_container| + init_container[:securityContext] = container_security_context + end + + nil end # @param [Array] desired_config_array # @param [Hash] default_resources_per_workspace_container - # @return [Array] + # @return [void] def self.patch_default_resources(desired_config_array:, default_resources_per_workspace_container:) - desired_config_array.each do |workspace_resource| - next unless workspace_resource.fetch(:kind) == 'Deployment' - - pod_spec = workspace_resource.fetch(:spec).fetch(:template).fetch(:spec) - - container_types = [:initContainers, :containers] - container_types.each do |container_type| - # the purpose of this deep_merge is to ensure - # the values from the devfile override any defaults defined at the agent - pod_spec.fetch(container_type).each do |container| - container - .fetch(:resources, {}) - .deep_merge!(default_resources_per_workspace_container) { |_, val, _| val } - end + pod_spec = find_pod_spec(desired_config_array) + + container_types = [:initContainers, :containers] + container_types.each do |container_type| + # the purpose of this deep_merge is to ensure + # the values from the devfile override any defaults defined at the agent + pod_spec.fetch(container_type).each do |container| + container + .fetch(:resources, {}) + .deep_merge!(default_resources_per_workspace_container) { |_, val, _| val } end end - desired_config_array + + nil end # @param [Array] desired_config_array # @param [String] env_secret_name # @param [String] file_secret_name - # @return [Array] + # @return [void] def self.inject_secrets(desired_config_array:, env_secret_name:, file_secret_name:) - desired_config_array.each do |workspace_resource| - next unless workspace_resource.fetch(:kind) == 'Deployment' - - volume = { - name: VARIABLES_VOLUME_NAME, - projected: { - defaultMode: VARIABLES_VOLUME_DEFAULT_MODE, - sources: [{ secret: { name: file_secret_name } }] - } + volume = { + name: VARIABLES_VOLUME_NAME, + projected: { + defaultMode: VARIABLES_VOLUME_DEFAULT_MODE, + sources: [{ secret: { name: file_secret_name } }] } + } - volume_mount = { - name: VARIABLES_VOLUME_NAME, - mountPath: VARIABLES_VOLUME_PATH - } + volume_mount = { + name: VARIABLES_VOLUME_NAME, + mountPath: VARIABLES_VOLUME_PATH + } - env_from = [{ secretRef: { name: env_secret_name } }] + env_from = [{ secretRef: { name: env_secret_name } }] - pod_spec = workspace_resource.fetch(:spec).fetch(:template).fetch(:spec) - pod_spec.fetch(:volumes) << volume unless file_secret_name.empty? + pod_spec = find_pod_spec(desired_config_array) + pod_spec.fetch(:volumes) << volume unless file_secret_name.empty? - pod_spec.fetch(:initContainers).each do |init_container| - init_container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? - init_container[:envFrom] = env_from unless env_secret_name.empty? - end + pod_spec.fetch(:initContainers).each do |init_container| + init_container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? + init_container[:envFrom] = env_from unless env_secret_name.empty? + end - pod_spec.fetch(:containers).each do |container| - container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? - container[:envFrom] = env_from unless env_secret_name.empty? - end + pod_spec.fetch(:containers).each do |container| + container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? + container[:envFrom] = env_from unless env_secret_name.empty? end - desired_config_array + + nil end # @param [Array] desired_config_array # @param [String] service_account_name - # @return [Array] + # @return [void] def self.set_service_account(desired_config_array:, service_account_name:) - desired_config_array.each do |workspace_resource| - next unless workspace_resource.fetch(:kind) == 'Deployment' + find_pod_spec(desired_config_array)[:serviceAccountName] = service_account_name - workspace_resource[:spec][:template][:spec][:serviceAccountName] = service_account_name - end - desired_config_array + nil + end + + # @param [Array] desired_config_array + # @return [Hash] + def self.find_pod_spec(desired_config_array) + desired_config_array => [ + *_, + { + kind: "Deployment", + spec: { + template: { + spec: pod_spec + } + } + }, + *_ + ] + + pod_spec end private_class_method :set_host_users, @@ -207,7 +217,8 @@ def self.set_service_account(desired_config_array:, service_account_name:) :set_security_context, :patch_default_resources, :inject_secrets, - :set_service_account + :set_service_account, + :find_pod_spec end end end diff --git a/ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb b/ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb index fcba5eafe9df235434585ede7ace537cc3f2026a..a722532764f3bfa03fc5f13d46996a9c770e7f36 100644 --- a/ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb +++ b/ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb @@ -30,7 +30,7 @@ def self.create(context) workspace_agentk_state = WorkspaceAgentkState.create!( workspace: workspace, project: workspace.project, - desired_config: desired_config.as_json + desired_config: desired_config.symbolized_desired_config_array ) if workspace_agentk_state.errors.present? diff --git a/ee/lib/remote_development/workspace_operations/desired_config.rb b/ee/lib/remote_development/workspace_operations/desired_config.rb index c161b24e6279f932c649821393f62dedebae28fb..db2ae4089884096f5665f36857e13608b5d56b90 100644 --- a/ee/lib/remote_development/workspace_operations/desired_config.rb +++ b/ee/lib/remote_development/workspace_operations/desired_config.rb @@ -37,8 +37,14 @@ def diff(other) # because we want to catch changes at the index of self rather than find # the common elements between the two arrays. This example should help explain # the difference https://github.com/liufengyun/hashdiff/issues/43#issuecomment-485497196 + # noinspection RubyMismatchedArgumentType -- hashdiff also supports arrays Hashdiff.diff(desired_config_array, other.desired_config_array, use_lcs: false) end + + # @return [Array] + def symbolized_desired_config_array + as_json.fetch("desired_config_array").map(&:deep_symbolize_keys) + end end end end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c918a1c85bcfec6459addc544b6ea36ccc89604 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + # noinspection RubyLiteralArrayInspection + class ConfigToApplyBuilder + include WorkspaceOperationsConstants + + ALLOWLIST_WHEN_STATE_TERMINATED = Set.new([ + /#{SECRETS_INVENTORY}$/o, + /#{WORKSPACE_INVENTORY}$/o + ]) + + # @param [RemoteDevelopment::Workspace] workspace + # @param [Boolean] include_all_resources + # @param [RemoteDevelopment::WorkspaceOperations::DesiredConfig] desired_config + # @return [Array] + def self.build(workspace:, include_all_resources:, desired_config:) + env_secret_name = "#{workspace.name}#{ENV_VAR_SECRET_SUFFIX}" + file_secret_name = "#{workspace.name}#{FILE_SECRET_SUFFIX}" + desired_config_array = desired_config.symbolized_desired_config_array + + config_to_apply = desired_config_array # Start with the persisted desired_config_array and then mutate it + + return select_only_configmaps(config_to_apply: config_to_apply) if workspace.desired_state_terminated? + + inject_secrets( + config_to_apply: config_to_apply, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name, + workspace: workspace + ) + + update_spec_replicas( + config_to_apply: config_to_apply, + workspace: workspace + ) + + filter_partial_resources(config_to_apply: config_to_apply) unless include_all_resources + + config_to_apply + end + + # @param [Array] config_to_apply + # @return [Array] + def self.select_only_configmaps(config_to_apply:) + config_to_apply.select! do |object| + object.fetch(:kind) == "ConfigMap" && ALLOWLIST_WHEN_STATE_TERMINATED.any? do |regex| + object.dig(:metadata, :name) =~ regex + end + end + + config_to_apply + end + + # Mutates `config_to_apply` and inject secret data in the `env_secret_name` + # + # @param [Array] config_to_apply + # @param [String] env_secret_name + # @param [String] file_secret_name + # @param [RemoteDevelopment::Workspace] workspace + # @return [Array] + def self.inject_secrets(config_to_apply:, env_secret_name:, file_secret_name:, workspace:) + append_secret_data_from_variables( + config_to_apply: config_to_apply, + secret_name: env_secret_name, + variables: workspace.workspace_variables.with_variable_type_environment + ) + + append_secret_data_from_variables( + config_to_apply: config_to_apply, + secret_name: file_secret_name, + variables: workspace.workspace_variables.with_variable_type_file + ) + + append_secret_data( + config_to_apply: config_to_apply, + secret_name: file_secret_name, + data: { WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_NAME.to_sym => workspace.actual_state } + ) + + config_to_apply + end + + # Mutates `config_to_apply` to remove resources not applicable for partial reconciliation + # + # @param [Array] config_to_apply + # @param [String] secret_name + # @param [ActiveRecord::Relation] variables + # @return [void] + def self.append_secret_data_from_variables(config_to_apply:, secret_name:, variables:) + data = variables.each_with_object({}) do |workspace_variable, hash| + hash[workspace_variable.key.to_sym] = workspace_variable.value + end + + append_secret_data( + config_to_apply: config_to_apply, + secret_name: secret_name, + data: data + ) + + nil + end + + # @param [Array] config_to_apply + # @param [String] secret_name + # @param [Hash] data + # @return [Array] + # noinspection RubyUnusedLocalVariable -- Rubymine doesn't recognize '^' to use a variable in pattern-matching + def self.append_secret_data(config_to_apply:, secret_name:, data:) + config_to_apply => [ + *_, + { + metadata: { + name: ^secret_name + }, + data: secret_data + }, + *_ + ] + + transformed_data = data.transform_values { |value| Base64.strict_encode64(value) } + + secret_data.merge!(transformed_data) + + config_to_apply + end + + # @param [Array] config_to_apply + # @param [RemoteDevelopment::Workspace] workspace + # @return [Array] + def self.update_spec_replicas(config_to_apply:, workspace:) + config_to_apply => [ + *_, + { + kind: "Deployment", + spec: deployment_spec + }, + *_ + ] + + deployment_spec[:replicas] = workspace.desired_state_running? ? 1 : 0 + + config_to_apply + end + + # @param [Array] config_to_apply + # @return [Array] + def self.filter_partial_resources(config_to_apply:) + config_to_apply.select! do |object| + object.dig(:metadata, :annotations, ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION).present? + end + + config_to_apply + end + + private_class_method :select_only_configmaps, + :inject_secrets, + :append_secret_data_from_variables, + :append_secret_data, + :update_spec_replicas, + :filter_partial_resources + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb new file mode 100644 index 0000000000000000000000000000000000000000..cebe5800fb7b805f2bd0f852793a21997fe6451a --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + # TODO This class will be removed after the succesfull shadow run of Create::DesiredConfig::Main + # Epic- https://gitlab.com/groups/gitlab-org/-/epics/17483 + # This class handles the scenarios where the new desired config generator returns a different result + # than the old one. In this case we will log a warning and use the old desired + # config generator instead. + class ConfigToApplyShadowRunHandler + include WorkspaceOperationsConstants + + # @param [RemoteDevelopment::Workspace] workspace + # @param [Array] new_config_to_apply_array + # @param [RemoteDevelopment::Logger] logger + # @param [Boolean] include_all_resources + # @return [Array] + def self.handle(workspace:, new_config_to_apply_array:, logger:, include_all_resources:) + old_config_to_apply_array = OldDesiredConfigGenerator.generate_desired_config( + workspace: workspace, + include_all_resources: include_all_resources, + logger: logger + ) + diffable_new_config_to_apply_array = generate_diffable_new_config_to_apply_array( + new_config_to_apply_array: new_config_to_apply_array + ) + + old_config_to_apply = DesiredConfig.new(desired_config_array: old_config_to_apply_array) + new_config_to_apply = DesiredConfig.new(desired_config_array: diffable_new_config_to_apply_array) + diff = new_config_to_apply.diff(old_config_to_apply) + unless diff.empty? + logger.warn( + message: "The generated config_to_apply from Create::DesiredConfig::Main and " \ + "OldDesiredConfigGenerator differ.", + error_type: "workspaces_reconcile_desired_configs_differ", + workspace_id: workspace.id, + diff: diff + ) + end + + old_config_to_apply_array + end + + # @param [Array] new_config_to_apply_array + # @return [Array] + def self.generate_diffable_new_config_to_apply_array(new_config_to_apply_array:) + new_config_to_apply_array.map do |original_resource| + # Ensure we don't mutate the original resource, to avoid testing confusion + resource = original_resource.deep_dup + + annotations = resource.dig(:metadata, :annotations) + next resource unless annotations + + resource[:metadata][:annotations].delete(ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION) + + if resource[:kind] == "Deployment" + resource[:spec][:template][:metadata][:annotations].delete( + ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION) + end + + resource + end + end + + private_class_method :generate_diffable_new_config_to_apply_array + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher.rb new file mode 100644 index 0000000000000000000000000000000000000000..bf9b19081f2b796216ae2100a93ea04ea018b774 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + class DesiredConfigFetcher + # @param workspace [RemoteDevelopment::Workspace] + # @param logger [RemoteDevelopment::Logger] + # @return [RemoteDevelopment::WorkspaceOperations::DesiredConfig] + def self.fetch(workspace:, logger:) + workspace_agentk_state = workspace.workspace_agentk_state + + if workspace_agentk_state + desired_config_array = workspace_agentk_state.desired_config + desired_config = RemoteDevelopment::WorkspaceOperations::DesiredConfig.new( + desired_config_array: desired_config_array + ) + # If the workspace_agentk_state.desired_config_array was somehow persisted to the database in an invalid + # state, this will raise an exception. We validate it before saving, so this should never happen normally + desired_config.validate! + return desired_config + end + + # TODO: remove this and the above 'if' after a succesful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + generate_new_desired_config(workspace: workspace, logger: logger) + end + + # @param [RemoteDevelopment::Workspace] workspace + # @param [RemoteDevelopment::Logger] logger + # @return [RemoteDevelopment::WorkspaceOperations::DesiredConfig] + def self.generate_new_desired_config(workspace:, logger:) + result = Create::DesiredConfig::Main.main( + { + params: { + agent: workspace.agent + }, + workspace: workspace, + logger: logger + } + ) + + result => { + desired_config: RemoteDevelopment::WorkspaceOperations::DesiredConfig => desired_config, + } + + desired_config + end + + private_class_method :generate_new_desired_config + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder.rb index 4183d1fcc0e58b8c24a07abb006fa73e72b648c2..c7c1182066d6cfbb3efc1ce5c87745b7dd4c214c 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder.rb @@ -84,13 +84,28 @@ def self.generate_config_to_apply(workspace:, update_type:, logger:) include_all_resources = should_include_all_resources?(update_type: update_type, workspace: workspace) resources_include_type = include_all_resources ? ALL_RESOURCES_INCLUDED : PARTIAL_RESOURCES_INCLUDED - workspace_resources = OldDesiredConfigGenerator.generate_desired_config( + desired_config = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) + config_to_apply_array = ConfigToApplyBuilder.build( workspace: workspace, include_all_resources: include_all_resources, + desired_config: desired_config + ) + + if workspace.workspace_agentk_state + # Leverage the DesiredConfig Value Object to ensure that config_to_apply is valid + DesiredConfig.new(desired_config_array: config_to_apply_array).validate! + end + + # TODO: remove this and the above 'if' after a succesful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + old_config_to_apply = ConfigToApplyShadowRunHandler.handle( + workspace: workspace, + new_config_to_apply_array: config_to_apply_array, + include_all_resources: include_all_resources, logger: logger ) + config_to_apply_array = old_config_to_apply - stable_sorted_workspace_resources = workspace_resources.map do |resource| + stable_sorted_workspace_resources = config_to_apply_array.map do |resource| Gitlab::Utils.deep_sort_hash(resource) end diff --git a/ee/lib/remote_development/workspace_operations/workspace_operations_constants.rb b/ee/lib/remote_development/workspace_operations/workspace_operations_constants.rb index 5232532367c4b273afc71bc6d36c1279b8dcf768..0bb85d430754aa1ff95ec0343f28b60ad406c6b3 100644 --- a/ee/lib/remote_development/workspace_operations/workspace_operations_constants.rb +++ b/ee/lib/remote_development/workspace_operations/workspace_operations_constants.rb @@ -17,12 +17,17 @@ module WorkspaceOperations # See documentation at ../README.md#constant-declarations for more information. module WorkspaceOperationsConstants # Please keep alphabetized + ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION = :"workspaces.gitlab.com/include-in-partial-reconciliation" + ENV_VAR_SECRET_SUFFIX = "-env-var" + FILE_SECRET_SUFFIX = "-file" INTERNAL_COMMAND_LABEL = "gl-internal" INTERNAL_BLOCKING_COMMAND_LABEL = "#{INTERNAL_COMMAND_LABEL}-blocking".freeze + SECRETS_INVENTORY = "-secrets-inventory" VARIABLES_VOLUME_DEFAULT_MODE = 0o774 VARIABLES_VOLUME_NAME = "gl-workspace-variables" VARIABLES_VOLUME_PATH = "/.workspace-data/variables/file" WORKSPACE_DATA_VOLUME_PATH = "/projects" + WORKSPACE_INVENTORY = "-workspace-inventory" WORKSPACE_LOGS_DIR = "#{WORKSPACE_DATA_VOLUME_PATH}/workspace-logs".freeze WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_NAME = "gl_workspace_reconciled_actual_state.txt" WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_PATH = diff --git a/ee/spec/factories/remote_development/workspace_agentk_states.rb b/ee/spec/factories/remote_development/workspace_agentk_states.rb index 5f81c1b5fba6cadfa56713bf1d2993aa11aeef19..8e362192d98485be1e720f7956ba722ebc884d2a 100644 --- a/ee/spec/factories/remote_development/workspace_agentk_states.rb +++ b/ee/spec/factories/remote_development/workspace_agentk_states.rb @@ -1,14 +1,25 @@ # frozen_string_literal: true FactoryBot.define do - factory :workspace_agentk_state, class: 'RemoteDevelopment::WorkspaceAgentkState' do + factory :workspace_agentk_state, class: "RemoteDevelopment::WorkspaceAgentkState" do # noinspection RailsParamDefResolve -- RubyMine flags this as requiring a hash, but a symbol is a valid option - association :project, :in_group - - workspace + association :workspace, :without_workspace_agentk_state desired_config do - RemoteDevelopment::FixtureFileHelpers.read_fixture_file('example.desired_config.json') + # NOTE: This desired_config fixture has hardcoded data, and the IDs/names/values will not match the + # workspace's actual data. If you want a realistic desired_config which matches a workspace, use the + # workspace factory to create a workspace and get its associated workspace_agentk_state.desired_config + Gitlab::Json.parse(RemoteDevelopment::FixtureFileHelpers.read_fixture_file("example.desired_config.json")) + end + + before(:create) do |workspace_agentk_state, _| + unless workspace_agentk_state.project + workspace_project = workspace_agentk_state.workspace.project + + raise unless workspace_project # ensure that we never set a nil project - it can happen in some cases + + workspace_agentk_state.project = workspace_project + end end end end diff --git a/ee/spec/factories/remote_development/workspaces.rb b/ee/spec/factories/remote_development/workspaces.rb index 3b4fc574f872c080311f3c06387d803070a9592d..cba08ed66e21155da2c1fd15a912be90cca105b0 100644 --- a/ee/spec/factories/remote_development/workspaces.rb +++ b/ee/spec/factories/remote_development/workspaces.rb @@ -34,12 +34,19 @@ transient do random_string { SecureRandom.alphanumeric(6).downcase } + without_workspace_agentk_state { false } without_workspace_variables { false } without_realistic_after_create_timestamp_updates { false } after_initial_reconciliation { false } unprovisioned { false } end + trait :without_workspace_agentk_state do + transient do + without_workspace_agentk_state { true } + end + end + trait :without_workspace_variables do transient do without_workspace_variables { true } @@ -95,6 +102,25 @@ responded_to_agent_at: 1.second.ago ) else + unless evaluator.without_workspace_agentk_state + # NOTE: We could attempt to manually build a desired_config_array which has all the correct IDs and values + # agent, namespace, workspace, etc, but this would be a lot of work. For now, we will just use the + # business logic to create a valid one based on the workspace's current state and associations. + result = RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main.main({ + params: { + agent: workspace.agent + }, + workspace: workspace, + logger: nil + }) + desired_config_array = result.fetch(:desired_config).symbolized_desired_config_array + + workspace.create_workspace_agentk_state!( + project: workspace.project, + desired_config: desired_config_array + ) + end + unless evaluator.without_workspace_variables workspace_variables = RemoteDevelopment::WorkspaceOperations::Create::WorkspaceVariablesBuilder.build( name: workspace.name, diff --git a/ee/spec/fixtures/remote_development/example.desired_config.json b/ee/spec/fixtures/remote_development/example.desired_config.json index 05273df55d3d8c0494d6e904d44f2264645824cf..f6f91165c69a016189aa36e14cdff66d3bef8f2e 100644 --- a/ee/spec/fixtures/remote_development/example.desired_config.json +++ b/ee/spec/fixtures/remote_development/example.desired_config.json @@ -6,7 +6,7 @@ "annotations": { "environment": "production", "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -28,7 +28,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -59,7 +59,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -268,7 +268,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -307,7 +307,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -346,7 +346,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -367,7 +367,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -450,7 +450,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -471,7 +471,7 @@ "annotations": { "environment": "production", "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -493,7 +493,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -523,7 +523,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, @@ -545,7 +545,7 @@ "environment": "production", "team": "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" }, diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor_spec.rb index 36ddfbb25845f07716ed101343aa6b0e1e7efe97..58b6ac5ac69f2881a04db4f19369d962142a994e 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/config_values_extractor_spec.rb @@ -94,6 +94,7 @@ %i[ allow_privilege_escalation common_annotations + common_annotations_for_partial_reconciliation default_resources_per_workspace_container default_runtime_class domain_template @@ -114,6 +115,7 @@ workspace_desired_state_is_running workspace_id workspace_inventory_annotations + workspace_inventory_annotations_for_partial_reconciliation workspace_inventory_name workspace_name workspaces_agent_config @@ -133,6 +135,16 @@ "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" } ) + expect(extracted_values[:common_annotations_for_partial_reconciliation]).to eq( + { + "some/annotation": "value", + "workspaces.gitlab.com/host-template": "{{.port}}-#{workspace_name}.#{dns_zone}", + "workspaces.gitlab.com/id": workspace_id.to_s, + "workspaces.gitlab.com/include-in-partial-reconciliation": "true", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": + "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" + } + ) expect(extracted_values[:default_resources_per_workspace_container]) .to eq(default_resources_per_workspace_container) expect(extracted_values[:env_secret_name]).to eq("#{workspace_name}-env-var") @@ -172,6 +184,17 @@ "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" } ) + expect(extracted_values[:workspace_inventory_annotations_for_partial_reconciliation]).to eq( + { + "config.k8s.io/owning-inventory": "#{workspace_name}-workspace-inventory", + "some/annotation": "value", + "workspaces.gitlab.com/host-template": "{{.port}}-#{workspace_name}.#{dns_zone}", + "workspaces.gitlab.com/id": workspace_id.to_s, + "workspaces.gitlab.com/include-in-partial-reconciliation": "true", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": + "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" + } + ) expect(extracted_values[:workspace_inventory_name]).to eq("#{workspace_name}-workspace-inventory") end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb index 8ed1b10b75e58edd3aecfec1e56121426a1abca0..3d6215bd243979630e4c689761fd1a73f4b77c2b 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb @@ -14,7 +14,7 @@ { processed_devfile_yaml: example_devfile_yaml, logger: logger, - workspace_inventory_annotations: { k1: "v1", k2: "v2" }, + workspace_inventory_annotations_for_partial_reconciliation: { k1: "v1", k2: "v2" }, domain_template: "domain_template", labels: labels, workspace_name: workspace_name, diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb index 9cd79bde0f123f5a79280409a42082699afbd16c..2edd7793103217f5a100c21d0dc42a23b2cb3cf8 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb @@ -4,6 +4,7 @@ RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileResourceAppender, :freeze_time, feature_category: :workspaces do include_context "with remote development shared fixtures" + include_context "with constant modules" # rubocop:disable RSpec/VerifiedDoubleReference -- fast_spec_helper does not load Rails models, so we must use a quoted class name here.let(:env_var) do let(:env_var) do @@ -37,11 +38,24 @@ let(:labels) { { "app" => "workspace", "tier" => "development", "agent.gitlab.com/id" => "991" } } let(:workspace_inventory_annotations) { { "environment" => "production", "team" => "engineering" } } + let(:workspace_inventory_annotations_for_partial_reconciliation) do + workspace_inventory_annotations.merge( + { workspace_operations_constants_module::ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } + ) + end + let(:common_annotations) do { "workspaces.gitlab.com/host-template" => "3000-#{workspace.name}.workspaces.localdev.me" } end + let(:common_annotations_for_partial_reconciliation) do + common_annotations.merge( + { workspace_operations_constants_module::ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } + ) + end + let(:workspace_inventory_name) { "#{workspace.name}-workspace-inventory" } + let(:workspace_scripts_configmap_name) { "#{workspace.name}-scripts" } let(:secrets_inventory_name) { "#{workspace.name}-secrets-inventory" } let(:secrets_inventory_annotations) { { "config.k8s.io/owning-inventory" => secrets_inventory_name } } let(:scripts_configmap_name) { "#{workspace.name}-scripts" } @@ -91,7 +105,10 @@ workspace_namespace: workspace.namespace, labels: labels, workspace_inventory_annotations: workspace_inventory_annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, common_annotations: common_annotations, + common_annotations_for_partial_reconciliation: common_annotations_for_partial_reconciliation, workspace_inventory_name: workspace_inventory_name, secrets_inventory_name: secrets_inventory_name, secrets_inventory_annotations: secrets_inventory_annotations, @@ -119,6 +136,35 @@ expect(kinds_and_names).to include(["Secret", env_secret_name]) expect(kinds_and_names).to include(["Secret", file_secret_name]) + resources_included_in_partial_reconciliation = [ + { kind: "ConfigMap", name: workspace_inventory_name }, + { kind: "ServiceAccount", name: workspace.name }, + { kind: "NetworkPolicy", name: workspace.name }, + { kind: "ConfigMap", name: workspace_scripts_configmap_name } + ] + + result.each do |resource| + kind = resource[:kind] + name = resource.dig(:metadata, :name) + key = workspace_operations_constants_module::ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION + + included_in_partial_reconciliation, excluded_from_partial_reconciliation = result.partition do |r| + resources_included_in_partial_reconciliation.any? do |spec| + spec[:kind] == r[:kind] && spec[:name] == r.dig(:metadata, :name) + end + end + + included_in_partial_reconciliation.each do |r| + expect(r.dig(:metadata, :annotations, key)).to eq("true"), + "Expected #{kind}/#{name} to have annotation #{key}=true, but got #{r.dig(:metadata, :annotations)}" + end + + excluded_from_partial_reconciliation.each do |r| + expect(r.dig(:metadata, :annotations, key)).to be_nil, + "Expected annotation #{key} to not be present in #{kind}/#{name}, but it is" + end + end + secret_resources = result.select { |r| r[:kind] == "Secret" } secret_resources.each do |secret| expect(secret[:data]).to eq({}) diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb index 220aefe9f10c1766a11f56a7309a8d5544f85987..f2eb4bdc87b603abb6f8502e221abde7fa6e996c 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb @@ -40,10 +40,6 @@ ] end - # let(:desired_config_array_yaml) do - # [base_deployment_resource.deep_stringify_keys, non_deployment_resource.deep_stringify_keys].map(&:to_yaml).join - # end - let(:context) do { workspace_name: workspace_name, diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb index 3991dca84562dac3a48644b94f5b5c1209f4c883..d307230addded3ff608509a69b8b988d16b3b828 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb @@ -427,7 +427,8 @@ def expected_desired_config_array_with_desired_state_running team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -449,7 +450,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -480,7 +482,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -694,7 +697,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -733,7 +737,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -772,7 +777,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -793,7 +799,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -876,7 +883,8 @@ def expected_desired_config_array_with_desired_state_running "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1003,7 +1011,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1025,7 +1034,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1056,7 +1066,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1271,7 +1282,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1310,7 +1322,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1349,7 +1362,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1370,7 +1384,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1453,7 +1468,8 @@ def expected_desired_config_array_with_desired_state_running_with_use_kubernetes "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1579,7 +1595,8 @@ def expected_desired_config_array_with_desired_state_stopped team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1601,7 +1618,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1632,7 +1650,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1846,7 +1865,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1885,7 +1905,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -1924,7 +1945,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -1945,7 +1967,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2028,7 +2051,8 @@ def expected_desired_config_array_with_desired_state_stopped "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2154,7 +2178,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2176,7 +2201,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2207,7 +2233,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2421,7 +2448,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2460,7 +2488,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2499,7 +2528,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2520,7 +2550,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2603,7 +2634,8 @@ def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_de "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2728,7 +2760,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -2750,7 +2783,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2781,7 +2815,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -2963,7 +2998,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3002,7 +3038,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3041,7 +3078,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3062,7 +3100,8 @@ def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3244,7 +3283,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3267,7 +3307,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3300,7 +3341,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3515,7 +3557,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3556,7 +3599,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3596,7 +3640,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3618,7 +3663,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3706,7 +3752,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_runnin "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3807,7 +3854,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe team: "engineering", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -3830,7 +3878,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -3863,7 +3912,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -4078,7 +4128,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -4119,7 +4170,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, creationTimestamp: nil, labels: { @@ -4159,7 +4211,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -4181,7 +4234,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", @@ -4269,7 +4323,8 @@ def expected_desired_config_array_for_shared_namespace_with_desired_state_stoppe "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-workspace-inventory", "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca", + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" }, labels: { app: "workspace", diff --git a/ee/spec/lib/remote_development/workspace_operations/create/main_integration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/main_integration_spec.rb index 69d172707cc1e1361556289eca57c1319a7d1b2a..76054104727244662fc52e85c49e1f493dc3a7fc 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/main_integration_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/main_integration_spec.rb @@ -186,8 +186,9 @@ ).to eq(variable[:value]) end - expect(workspace.agentk_state).to be_present - expect(workspace.agentk_state.desired_config.fetch("desired_config_array")).to be_an(Array) + expect(workspace.workspace_agentk_state).to be_present + expect(workspace.workspace_agentk_state.desired_config).to be_an(Array) + pp workspace.workspace_agentk_state.desired_config end it_behaves_like 'tracks successful workspace creation event' diff --git a/ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb index 26b20d9282c2734de1e7f69dda0e8166fbaa93a7..9c7ca124e938f764b001bb88acb1c35a3c847063 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" # noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot RSpec.describe ::RemoteDevelopment::WorkspaceOperations::Create::WorkspaceAgentkStateCreator, feature_category: :workspaces do include ResultMatchers - include_context 'with remote development shared fixtures' + include_context "with remote development shared fixtures" let(:workspace_name) { "workspace-991-990-fedcba" } - let(:workspace) { create(:workspace, name: workspace_name) } - let(:expected_desired_config_json) { desired_config.as_json } + let(:workspace) { create(:workspace, :without_workspace_agentk_state, name: workspace_name) } + let(:expected_desired_config_json) { desired_config.as_json.fetch("desired_config_array") } let(:logger) { instance_double(RemoteDevelopment::Logger) } let(:desired_config) do ::RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: create_desired_config_array) @@ -28,7 +28,7 @@ described_class.create(context) # rubocop:disable Rails/SaveBang -- This is not an ActiveRecord method end - it 'persists the record and returns nil' do + it "persists the record and returns nil" do expect { result }.to change { RemoteDevelopment::WorkspaceAgentkState.count } expect(RemoteDevelopment::WorkspaceAgentkState.last) diff --git a/ee/spec/lib/remote_development/workspace_operations/desired_config_spec.rb b/ee/spec/lib/remote_development/workspace_operations/desired_config_spec.rb index ee6a91ba9b02939c0a40d176a5b856ce65b3d24c..5e1f90997a7f1ad2848f4892f4cfa149c4d0a457 100644 --- a/ee/spec/lib/remote_development/workspace_operations/desired_config_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/desired_config_spec.rb @@ -61,17 +61,15 @@ end end - describe '#to_json' do + describe "#symbolized_desired_config_array" do let(:desired_config_array) { create_desired_config_array } - subject(:result) { desired_config.to_json } - - it { expect(result).to be_valid_json } - it { expect(Gitlab::Json.parse(result)).to eq({ "desired_config_array" => create_desired_config_array }.as_json) } + it { expect(desired_config.symbolized_desired_config_array).to eq(desired_config_array) } + it { expect(desired_config.symbolized_desired_config_array).to be_kind_of(Array) } end describe '#==(other)' do - let(:desired_config) { described_class.new(desired_config_array: create_desired_config_array) } + let(:desired_config_array) { create_desired_config_array } context 'when both DesiredConfig instances have the same desired_config_array' do let(:other_desired_config) { described_class.new(desired_config_array: create_desired_config_array) } @@ -87,7 +85,7 @@ end describe "#diff(other)" do - let(:desired_config) { described_class.new(desired_config_array: create_desired_config_array) } + let(:desired_config_array) { create_desired_config_array } context 'when other is not of DesiredConfig type' do using RSpec::Parameterized::TableSyntax @@ -134,7 +132,7 @@ environment: "production", team: "engineering", "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-inventory", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/host-template": "{{.port}}-workspace-991-990-fedcba.workspaces.dev.test", "workspaces.gitlab.com/id": "993", "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb index 7b53fc37c87092147136241d408eed111bfc92a9..30b17f17339b2c4fa9c293a004ad1e4ca13cb8e0 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb @@ -263,7 +263,7 @@ let(:update_type) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes::FULL } let(:workspace_agent_infos) { [] } - it 'returns expected keys within the response payload' do + it 'returns expected keys within the response payload', :unlimited_max_formatted_output_length do expect(response.fetch(:payload).keys).to contain_exactly(:settings, :workspace_rails_infos) end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_reconcile_scenarios_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_reconcile_scenarios_spec.rb index 96246f4bff07d792c087b6a08345b18df4c4d40f..94de5928c336f6038e2d7012e4ae3aabec110955 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_reconcile_scenarios_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_reconcile_scenarios_spec.rb @@ -22,7 +22,7 @@ # See following documentation for details on all scenarios: # - # https://gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/-/blob/main/doc/workspace-updates.md + # https://gitlab.com/gitlab-org/workspaces/gitlab-workspaces-docs/-/blob/main/doc/workspace-reconciliation-logic.md # # Columns: # @@ -147,7 +147,7 @@ # rubocop:enable Layout/LineLength, Style/TrailingCommaInArrayLiteral with_them do - it 'behaves as expected' do + it 'behaves as expected', :unlimited_max_formatted_output_length do expect(logger).not_to receive(:warn) expect(logger).not_to receive(:error) diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..37a171ed7b344fd75f91504b6b1567b5e64bb901 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb @@ -0,0 +1,233 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder, :unlimited_max_formatted_output_length, feature_category: :workspaces do + include_context "with constant modules" + + # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version of these models, so we can use fast_spec_helper. + let(:workspace_variable_environment) do + instance_double( + "RemoteDevelopment::WorkspaceVariable", + key: "ENV_VAR1", + value: "env-var-value1" + ) + end + + let(:workspace_variable_file) do + instance_double( + "RemoteDevelopment::WorkspaceVariable", + key: "FILE_VAR1", + value: "file-var-value1" + ) + end + # rubocop:enable RSpec/VerifiedDoubleReference + + # rubocop:disable RSpec/VerifiedDoubles -- This is a scope which is of type ActiveRecord::Associations::CollectionProxy, it can't be a verified double + let(:workspace_variables) do + double( + :workspace_variables, + with_variable_type_environment: [workspace_variable_environment], + with_variable_type_file: [workspace_variable_file] + ) + end + # rubocop:enable RSpec/VerifiedDoubles + + let(:include_all_resources) { true } + let(:actual_state) { states_module::RUNNING } + let(:desired_state_running) { false } + let(:desired_state_terminated) { false } + let(:expected_replicas) { desired_state_running ? 1 : 0 } + + # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version of these models, so we can use fast_spec_helper. + let(:workspace) do + instance_double("RemoteDevelopment::Workspace", + name: "workspace-991-990-fedcba", + namespace: "gl-rd-ns-991-990-fedcba", + workspace_variables: workspace_variables, + desired_state_running?: desired_state_running, + desired_state_terminated?: desired_state_terminated, + actual_state: actual_state + ) + end + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:input_desired_config) do + replicas = 99 # Replicas set to 99 in original input desired_config, to ensure it gets updated + desired_config_array = + desired_config_array_with_partial_reconciliation_annotation(replicas: replicas) + + resources_without_partial_reconciliation_annotation + + secrets_inventory_configmap_resource + + env_and_file_secrets + + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new( + desired_config_array: desired_config_array + ) + end + + subject(:config_to_apply) do + # noinspection RubyMismatchedArgumentType -- workspace is mocked but RubyMine does not like it + described_class.build( + workspace: workspace, + include_all_resources: include_all_resources, + desired_config: input_desired_config + ) + end + + shared_examples "returns expected config_to_apply" do + it "returns expected config_to_apply" do + expect(config_to_apply).to eq(expected_config_to_apply) + end + end + + shared_examples "has correct terminated behavior" do + let(:desired_state_terminated) { true } + let(:expected_config_to_apply) do + workspace_inventory_configmap_resource + + secrets_inventory_configmap_resource + end + + it_behaves_like "returns expected config_to_apply" + end + + context "when include_all_resources is true" do + let(:expected_config_to_apply) do + desired_config_array_with_partial_reconciliation_annotation(replicas: expected_replicas) + + resources_without_partial_reconciliation_annotation + + secrets_inventory_configmap_resource + + populated_env_and_file_secrets + end + + it_behaves_like "returns expected config_to_apply" + + it_behaves_like "has correct terminated behavior" + + it "populates secret data" do + all_secrets_populated = config_to_apply.map do |resource| + resource[:kind] == "Secret" ? resource[:data].present? : true + end.all? + expect(all_secrets_populated).to be true + end + end + + context "when include_all_resources is false" do + let(:include_all_resources) { false } + let(:expected_config_to_apply) do + desired_config_array_with_partial_reconciliation_annotation(replicas: expected_replicas) + end + + it_behaves_like "returns expected config_to_apply" + + it_behaves_like "has correct terminated behavior" + end + + # @return [Array] + def workspace_inventory_configmap_resource + [ + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-workspace-inventory", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + } + ] + end + + # @param [Integer] replicas + # @return [Array] + def desired_config_array_with_partial_reconciliation_annotation(replicas:) + workspace_inventory_configmap_resource + + [ + { + kind: "Deployment", + spec: { + replicas: replicas # A value which is not 0 or 1, to ensure it gets updated + }, + metadata: { + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "object-with-annotation", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + } + ] + end + + # @return [Array] + def secrets_inventory_configmap_resource + [ + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-secrets-inventory" + } + } + ] + end + + # @return [Array] + def resources_without_partial_reconciliation_annotation + [ + { + kind: "object-without-annotation", + metadata: { + name: "workspace-991-990-fedcba" + } + } + ] + end + + # @return [Array] + def env_and_file_secrets + [ + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-env-var" + }, + data: {} + }, + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-file" + }, + data: {} + } + ] + end + + # @return [Array] + def populated_env_and_file_secrets + [ + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-env-var" + }, + data: { ENV_VAR1: "ZW52LXZhci12YWx1ZTE=" } + }, + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-file" + }, + data: { + FILE_VAR1: "ZmlsZS12YXItdmFsdWUx", + "gl_workspace_reconciled_actual_state.txt": "UnVubmluZw==" + } + } + ] + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..956c30ca4d7a6e3389381e72896c0b1d32d4b62e --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyShadowRunHandler, feature_category: :workspaces do + let(:logger) { instance_double("RemoteDevelopment::Logger") } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + let(:workspace) { instance_double("RemoteDevelopment::Workspace", id: 1) } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + + # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace and Logger + subject(:result) do + described_class.handle( + workspace: workspace, + new_config_to_apply_array: new_config_to_apply_array, + logger: logger, + include_all_resources: true + ) + end + + before do + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator) + .to(receive(:generate_desired_config)) + .with(workspace: workspace, include_all_resources: true, logger: logger) + .and_return(old_config_to_apply_array) + end + + context "when config_to_apply are same" do + let(:old_config_to_apply_array) { [{ kind: "Deployment" }] } + let(:new_config_to_apply_array) { [{ kind: "Deployment" }] } + + it "returns the value from OldDesiredConfigGenerator" do + expect(logger).not_to receive(:warn) + expect(result).to eq(old_config_to_apply_array) + end + end + + context "when config_to_apply are different" do + let(:old_config_to_apply_array) { [{ kind: "Deployment" }] } + let(:new_config_to_apply_array) { [{ kind: "Service" }] } + + before do + allow(logger).to receive(:warn) + end + + it "logs a warning and returns the value from OldDesiredConfigGenerator" do + # noinspection RubyArgCount -- False positive: Ruby thinks `with` is not supposed to get any argument + expect(logger) + .to(receive(:warn)) + .with({ + diff: [%w[~ [0].kind Service Deployment]], + error_type: "workspaces_reconcile_desired_configs_differ", + message: "The generated config_to_apply from Create::DesiredConfig::Main and OldDesiredConfigGenerator " \ + "differ.", + workspace_id: 1 + }) + expect(result).to eq(old_config_to_apply_array) + end + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f1ea9f573c1a509a1fc9030253b3e622cf489a95 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "spec_helper" + +# noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are Minitest, not FactoryBot +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher, feature_category: :workspaces do + let(:logger) { instance_double(RemoteDevelopment::Logger) } + + # NOTE: We are intentionally using `let` instead of `let_it_be` here. There are some subtle + # test- and fixture-ordering gotchas with the creation of the associated + # workspace_agentk_state by FactoryBot. + let(:workspace) { create(:workspace) } + let(:expected_desired_config_array) { workspace.workspace_agentk_state.desired_config } + let(:expected_desired_config) do + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: expected_desired_config_array) + end + + # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for logger + subject(:desired_config) { described_class.fetch(workspace: workspace, logger: logger) } + + it "fixture sanity check" do + # There are some subtle gotchas with the creation of the associated workspace_agentk_state + # by FactoryBot. This test ensures that it was correctly created and associated + expect(workspace.workspace_agentk_state).not_to be_nil + expect(workspace.workspace_agentk_state).to be_valid + expect(workspace).to be_valid + end + + describe "#fetch" do + # TODO: remove this 'let' after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + let(:generate_new_desired_config_method) { :generate_new_desired_config } + + it "returns desired_config" do + expect(desired_config).to eq(expected_desired_config) + end + + describe "when persisted desired_config_array is invalid" do + before do + workspace.workspace_agentk_state.update_attribute(:desired_config, "invalid json") + end + + it "raises an error" do + expect { desired_config }.to raise_error(ActiveModel::ValidationError) + end + end + + # TODO: remove this expectation after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + it "does not call RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main.main" do + expect(described_class).not_to receive(generate_new_desired_config_method) + desired_config + end + + # TODO: remove this context after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + context "when workspace_agentk_state does not exist" do + let(:expected_desired_config) do + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: []) + end + + let(:result) { { desired_config: expected_desired_config } } + let(:expected_context) do + { + params: { + agent: workspace.agent + }, + workspace: workspace, + logger: logger + } + end + + before do + allow(workspace).to receive(:workspace_agentk_state).and_return(nil) + end + + it "calls RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main.main" do + expect(described_class).to receive(generate_new_desired_config_method) + desired_config + end + + it "returns desired_config from Create::DesiredConfig::Main" do + expect(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main) + .to receive(:main).with(expected_context) { result } + expect(desired_config).to eq(expected_desired_config) + end + end + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb index 2e9a204e93abbb6a15adbd285890dcb20556f917..369898ac72400e278e0955bc090e41818c58b293 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb @@ -140,6 +140,7 @@ end context "when desired_state terminated" do + let(:include_all_resources) { true } # Ensure that the terminated behavior overrides the include_all_resources let(:desired_state_is_terminated) { true } let(:desired_state) { states_module::TERMINATED } diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder_spec.rb index 787d855942f38ca6c66eb7ac491c3963c96f1186..0709bfa16de3615220eb1e79998c6552bd3eddec 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/response_payload_builder_spec.rb @@ -3,18 +3,14 @@ require "fast_spec_helper" RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ResponsePayloadBuilder, feature_category: :workspaces do - include_context 'with remote development shared fixtures' + include_context "with constant modules" let(:update_types) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes } let(:logger) { instance_double(Logger) } let(:desired_state) { states_module::RUNNING } let(:actual_state) { states_module::STOPPED } - let(:processed_devfile_yaml) { example_processed_devfile_yaml } let(:force_include_all_resources) { false } let(:image_pull_secrets) { [{ name: "secret-name", namespace: "secret-namespace" }] } - let(:current_desired_config_generator_version) do - RemoteDevelopment::WorkspaceOperations::DesiredConfigGeneratorVersion::VERSION_3 - end let(:agent_config) do instance_double( @@ -23,6 +19,9 @@ ) end + # TODO: remove this after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + let(:workspace_agentk_state) { nil } + let(:workspace) do instance_double( "RemoteDevelopment::Workspace", # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper @@ -32,10 +31,10 @@ deployment_resource_version: "1", desired_state: desired_state, actual_state: actual_state, - processed_devfile: processed_devfile_yaml, - desired_config_generator_version: desired_config_generator_version, force_include_all_resources: force_include_all_resources, - workspaces_agent_config: agent_config + workspaces_agent_config: agent_config, + # TODO: remove this after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + workspace_agentk_state: workspace_agentk_state ) end @@ -55,6 +54,16 @@ } end + let(:desired_config_array) { [{}] } + let(:desired_config_array_is_valid) { true } + + let(:desired_config) do + instance_double( + "RemoteDevelopment::WorkspaceOperations::DesiredConfig", # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + desired_config_array: desired_config_array + ) + end + # NOTE: We are setting `expected_include_all_resources` into our fake `generated_config_to_apply` which is mocked to # be returned from DesiredConfigGenerator. This allows us to perform assertions on the expected passed/returned # value of `include_all_resources` using simple `let` statements, and avoid having to write complex mocks. @@ -119,107 +128,145 @@ desired_state_terminated_and_actual_state_not_terminated?: desired_state_terminated_and_actual_state_not_terminated ) - end - context "when workspace.desired_config_generator_version is current version" do - let(:desired_config_generator_version) { current_desired_config_generator_version } + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) + .to receive(:fetch) + .and_return(desired_config) - before do - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator) - .to(receive(:generate_desired_config)) - .with(hash_including(include_all_resources: expected_include_all_resources)) { generated_config_to_apply } + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) + .to receive(:build) + .with(desired_config: desired_config, workspace: workspace, include_all_resources: expected_include_all_resources) + .and_return(generated_config_to_apply) + + allow_next_instance_of( + RemoteDevelopment::WorkspaceOperations::DesiredConfig, + desired_config_array: generated_config_to_apply + ) do |instance| + if desired_config_array_is_valid + allow(instance).to receive(:validate!) + else + allow(instance).to receive(:validate!).and_raise("Validation failed") + end end - context "when update_type is FULL" do - let(:desired_state_updated_more_recently_than_last_response_to_agent) { false } - let(:actual_state_updated_more_recently_than_last_response_to_agent) { false } - let(:desired_state_terminated_and_actual_state_not_terminated) { false } - let(:update_type) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes::FULL } - let(:expected_include_all_resources) { true } + # TODO: remove this after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyShadowRunHandler) + .to receive(:handle) + .with( + hash_including( + workspace: workspace, + new_config_to_apply_array: generated_config_to_apply, + include_all_resources: expected_include_all_resources, + logger: logger + ) + ) { generated_config_to_apply } + end - it "includes config_to_apply with all resources included" do - expect(returned_value).to eq(expected_returned_value) - end + context "when update_type is FULL" do + let(:desired_state_updated_more_recently_than_last_response_to_agent) { false } + let(:actual_state_updated_more_recently_than_last_response_to_agent) { false } + let(:desired_state_terminated_and_actual_state_not_terminated) { false } + let(:update_type) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes::FULL } + let(:expected_include_all_resources) { true } - context "when config_to_apply contains multiple resources" do - let(:generated_config_to_apply) do - [ - { - a: { - z: 1, - a: 1 - } - }, - { - b: 2 - } - ] - end - - let(:expected_generated_config_to_apply) do - [ - { - a: { - a: 1, - z: 1 - } - }, - { - b: 2 + it "includes config_to_apply with all resources included" do + expect(returned_value).to eq(expected_returned_value) + end + + context "when config_to_apply contains multiple resources" do + let(:generated_config_to_apply) do + [ + { + a: { + z: 1, + a: 1 } - ] - end - - it "includes all resources with hashes deep sorted" do - expect(returned_value).to eq(expected_returned_value) - returned_value[:response_payload][:workspace_rails_infos].first[:config_to_apply] - returned_value => { - response_payload: { - workspace_rails_infos: [ - { - config_to_apply: config_to_apply_yaml_stream - }, - ] + }, + { + b: 2 + } + ] + end + + let(:expected_generated_config_to_apply) do + [ + { + a: { + a: 1, + z: 1 } + }, + { + b: 2 + } + ] + end + + it "includes all resources with hashes deep sorted" do + expect(returned_value).to eq(expected_returned_value) + returned_value[:response_payload][:workspace_rails_infos].first[:config_to_apply] + returned_value => { + response_payload: { + workspace_rails_infos: [ + { + config_to_apply: config_to_apply_yaml_stream + }, + ] } - loaded_multiple_docs = YAML.load_stream(config_to_apply_yaml_stream) - expect(loaded_multiple_docs.size).to eq(expected_generated_config_to_apply.size) - end + } + loaded_multiple_docs = YAML.load_stream(config_to_apply_yaml_stream) + expect(loaded_multiple_docs.size).to eq(expected_generated_config_to_apply.size) end end - context "when update_type is PARTIAL" do - let(:update_type) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes::PARTIAL } - - using RSpec::Parameterized::TableSyntax - - where( - :force_include_all_resources, - :desired_state_updated_more_recently_than_last_response_to_agent, - :actual_state_updated_more_recently_than_last_response_to_agent, - :desired_state_terminated_and_actual_state_not_terminated, - :expected_include_all_resources, - :expected_workspace_resources_included_type, - :expect_config_to_apply_to_be_included - ) do - # @formatter:off - Turn off RubyMine autoformatting - true | true | false | false | true | described_class::ALL_RESOURCES_INCLUDED | true - true | false | false | false | true | described_class::ALL_RESOURCES_INCLUDED | true - false | true | false | false | false | described_class::PARTIAL_RESOURCES_INCLUDED | true - false | false | false | false | false | described_class::NO_RESOURCES_INCLUDED | false - false | false | false | true | false | described_class::PARTIAL_RESOURCES_INCLUDED | true - true | true | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true - true | false | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true - false | true | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true - false | false | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true - # @formatter:on + context "when generated config_to_apply is not valid" do + # TODO: remove this 'let' after a successful shadow run. Issue - https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + let(:workspace_agentk_state) do + instance_double( + "RemoteDevelopment::WorkspaceAgentkState", # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + desired_config: generated_config_to_apply + ) end - with_them do - let(:generated_config_to_apply) { nil } unless params[:expect_config_to_apply_to_be_included] + let(:desired_config_array_is_valid) { false } - it { expect(returned_value).to eq(expected_returned_value) } + it "raises an error" do + expect { returned_value }.to raise_error(/Validation failed/) end end end + + context "when update_type is PARTIAL" do + let(:update_type) { RemoteDevelopment::WorkspaceOperations::Reconcile::UpdateTypes::PARTIAL } + + using RSpec::Parameterized::TableSyntax + + where( + :force_include_all_resources, + :desired_state_updated_more_recently_than_last_response_to_agent, + :actual_state_updated_more_recently_than_last_response_to_agent, + :desired_state_terminated_and_actual_state_not_terminated, + :expected_include_all_resources, + :expected_workspace_resources_included_type, + :expect_config_to_apply_to_be_included + ) do + # @formatter:off - Turn off RubyMine autoformatting + true | true | false | false | true | described_class::ALL_RESOURCES_INCLUDED | true + true | false | false | false | true | described_class::ALL_RESOURCES_INCLUDED | true + false | true | false | false | false | described_class::PARTIAL_RESOURCES_INCLUDED | true + false | false | false | false | false | described_class::NO_RESOURCES_INCLUDED | false + false | false | false | true | false | described_class::PARTIAL_RESOURCES_INCLUDED | true + true | true | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true + true | false | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true + false | true | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true + false | false | true | false | true | described_class::ALL_RESOURCES_INCLUDED | true + # @formatter:on + end + + with_them do + let(:generated_config_to_apply) { nil } unless params[:expect_config_to_apply_to_be_included] + + it { expect(returned_value).to eq(expected_returned_value) } + end + end end diff --git a/ee/spec/models/remote_development/workspace_agentk_state_spec.rb b/ee/spec/models/remote_development/workspace_agentk_state_spec.rb index 8220898274bfd87834fddb5914bbc840a216da84..a786de93a2f2737f292cdc7c00c864040cbb06d2 100644 --- a/ee/spec/models/remote_development/workspace_agentk_state_spec.rb +++ b/ee/spec/models/remote_development/workspace_agentk_state_spec.rb @@ -3,12 +3,18 @@ require "spec_helper" RSpec.describe RemoteDevelopment::WorkspaceAgentkState, feature_category: :workspaces do - let(:workspace) { create(:workspace) } + # noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are Minitest, not FactoryBot + let_it_be(:workspace) { create(:workspace) } - # noinspection RubyArgCount -- RubyMine thinks this is a kernel create method instead of a factory - let_it_be(:project) { create(:project) } - - subject(:workspace_agentk_state) { build(:workspace_agentk_state, workspace: workspace, project: project) } + subject(:workspace_agentk_state) do + described_class.new( + workspace: workspace, + project: workspace.project, + desired_config: Gitlab::Json.parse( + RemoteDevelopment::FixtureFileHelpers.read_fixture_file("example.desired_config.json") + ) + ) + end describe "associations" do context "for belongs_to" do @@ -17,13 +23,21 @@ end context "when from factory" do - before do - workspace.save! + subject(:created_workspace_agentk_state) do + create(:workspace_agentk_state) end it "has correct associations from factory" do - expect(workspace_agentk_state.workspace).to eq(workspace) - expect(workspace_agentk_state.project).to eq(project) + expect(created_workspace_agentk_state.workspace).not_to be_nil + expect(created_workspace_agentk_state.project).not_to be_nil + expect(created_workspace_agentk_state.project).to eq(created_workspace_agentk_state.workspace.project) + expect(created_workspace_agentk_state).to be_valid + created_desired_config = created_workspace_agentk_state.desired_config + expect(created_desired_config).to be_a(Array) + expect(created_desired_config).not_to be_nil + expect( + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: created_desired_config) + ).to be_valid end end end @@ -32,5 +46,33 @@ it { is_expected.to validate_presence_of(:workspace_id) } it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_presence_of(:desired_config) } + + describe "desired_config validations" do + using RSpec::Parameterized::TableSyntax + + shared_examples "invalid desired_config" do + before do + workspace_agentk_state.desired_config = desired_config + end + + it "is invalid" do + expect(workspace_agentk_state).to be_invalid + expect(workspace_agentk_state.errors[:desired_config]).to include(expected_error) + end + end + + where(:desired_config, :expected_error) do + # @formatter:off - RubyMine does not format table well + { key: "value" } | "must be an array" + "string" | "must be an array" + 1 | "must be an array" + [] | "can't be blank" + # @formatter:on + end + + with_them do + it_behaves_like "invalid desired_config" + end + end end end diff --git a/ee/spec/models/remote_development/workspace_spec.rb b/ee/spec/models/remote_development/workspace_spec.rb index 303baae104c7c76b59a085c9d617324cfd9aa762..fb198e9cd35dec50ccea9e8d81159f9e75ecb07e 100644 --- a/ee/spec/models/remote_development/workspace_spec.rb +++ b/ee/spec/models/remote_development/workspace_spec.rb @@ -57,7 +57,7 @@ end context "for has_one" do - it { is_expected.to have_one(:agentk_state) } + it { is_expected.to have_one(:workspace_agentk_state) } it { is_expected.to have_one(:workspace_token) } end @@ -75,24 +75,26 @@ end context "when from factory" do - before do - # we need to save to save to allow some associations verified below to register the new workspace - workspace.save! - end + let_it_be(:created_workspace) { create(:workspace) } it "has correct associations from factory" do - expect(workspace.user).to eq(user) - expect(workspace.project).to eq(project) - expect(workspace.agent).to eq(agent) - expect(workspace.personal_access_token).to eq(personal_access_token) - expect(agent.unversioned_latest_workspaces_agent_config.workspaces.first).to eq(workspace) - expect(workspace.url_prefix).to eq("#{create_constants_module::WORKSPACE_EDITOR_PORT}-#{workspace.name}") - expect(workspace.url_query_string).to eq("folder=dir%2Ffile") - end - - it "has correct workspaces_agent_config associations from factory" do - expect(workspace.workspaces_agent_config_version).to eq(agent_config.versions.size) - expect(workspace.workspaces_agent_config).to eq(agent_config) + expect(created_workspace.user).to be_valid + expect(created_workspace.project).to be_valid + expect(created_workspace.agent).to be_valid + expect(created_workspace.personal_access_token).to be_valid + expect(created_workspace.agent.unversioned_latest_workspaces_agent_config.workspaces.first) + .to eq(created_workspace) + expect(created_workspace.url_prefix) + .to eq("#{create_constants_module::WORKSPACE_EDITOR_PORT}-#{created_workspace.name}") + expect(created_workspace.url_query_string).to eq("folder=dir%2Ffile") + expect(created_workspace.workspace_agentk_state).not_to be_nil + expect(created_workspace.workspace_agentk_state).to be_valid + expect(created_workspace.workspace_agentk_state.desired_config).not_to be_nil + expect( + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new( + desired_config_array: created_workspace.workspace_agentk_state.desired_config + ) + ).to be_valid end end end