From f0c5fa0dbecb12124fa2be9f1009944e0987aba4 Mon Sep 17 00:00:00 2001 From: Ashvin Sharma Date: Mon, 16 Jun 2025 20:23:28 +0530 Subject: [PATCH 1/4] Run Create::DesiredConfig::Main in shadow with OldDesiredConfigGenerator `DesiredConfigFetcher` is responsible for fetching desired_config. It tries to fetch the desired_config from the database but in case it cannot find it there, it calls Create::DesiredConfig::Main.main function. `ConfigToApplyBuilder` is responsible for injecting secrets and filter out redundant objects from desired_config. It uses DesiredConfigFetcher class to fetch the desired_config. `ResponsePayloadBuilder` calls `ConfigToApplyFetcher` that calls `ConfigToApplyBuilder` and `OldDesiredConfigGenerator` and performs `#diff` on both. It logs the diff in case it is not empty but returns the older config_to_apply in both the cases. All of this is done so that we can test out the new logic in Create::DesiredConfig::Main::main to create desired_config. --- .../workspace_agentk_state.rb | 8 + .../workspace_operations/desired_config.rb | 7 + .../output/config_to_apply_builder.rb | 144 +++++ .../output/config_to_apply_fetcher.rb | 48 ++ .../output/desired_config_fetcher.rb | 46 ++ .../output/response_payload_builder.rb | 2 +- .../example.config_to_apply.json | 566 ++++++++++++++++++ ...onfig_to_apply_partial_reconciliation.json | 467 +++++++++++++++ ...conciliation_desired_state_terminated.json | 44 ++ .../create/main_integration_spec.rb | 2 +- .../workspace_agentk_state_creator_spec.rb | 2 +- .../desired_config_spec.rb | 10 +- .../output/config_to_apply_builder_spec.rb | 140 +++++ .../output/config_to_apply_fetcher_spec.rb | 63 ++ .../output/desired_config_fetcher_spec.rb | 78 +++ .../output/response_payload_builder_spec.rb | 7 + .../workspace_agentk_state_spec.rb | 22 + 17 files changed, 1649 insertions(+), 7 deletions(-) create mode 100644 ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder.rb create mode 100644 ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb create mode 100644 ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher.rb create mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply.json create mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json create mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json create mode 100644 ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher_spec.rb diff --git a/ee/app/models/remote_development/workspace_agentk_state.rb b/ee/app/models/remote_development/workspace_agentk_state.rb index 5b053d7baa5c12..02f03ff88f08eb 100644 --- a/ee/app/models/remote_development/workspace_agentk_state.rb +++ b/ee/app/models/remote_development/workspace_agentk_state.rb @@ -8,5 +8,13 @@ class WorkspaceAgentkState < ApplicationRecord validates :workspace_id, presence: true validates :project_id, presence: true validates :desired_config, presence: true + validate :desired_config_must_be_array + + 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/desired_config.rb b/ee/lib/remote_development/workspace_operations/desired_config.rb index c161b24e6279f9..2ab03909bee431 100644 --- a/ee/lib/remote_development/workspace_operations/desired_config.rb +++ b/ee/lib/remote_development/workspace_operations/desired_config.rb @@ -37,8 +37,15 @@ 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 + + # @param [Hash, nil] options + # @return [Array, Hash] + def as_json(_options = nil) + desired_config_array + 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 00000000000000..dfbc2a74455154 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + # noinspection RubyLiteralArrayInspection + class ConfigToApplyBuilder + include WorkspaceOperationsConstants + + # @param [RemoteDevelopment::Workspace] workspace + # @param [Boolean] include_all_resources + # @param [RemoteDevelopment::Logger] logger + # @return [Array] + def self.build(workspace:, include_all_resources:, logger:) + env_secret_name = "#{workspace.name}-env-var" + file_secret_name = "#{workspace.name}-file" + + config_to_apply = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) + inject_secrets(config_to_apply, env_secret_name, file_secret_name, workspace) + + return config_to_apply if include_all_resources + + config_to_apply.reject { |object| should_exclude_object?(object, workspace) } + 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 + def self.inject_secrets(config_to_apply, env_secret_name, file_secret_name, workspace) + append_secret_data_from_variables( + desired_config: config_to_apply, + secret_name: env_secret_name, + variables: workspace.workspace_variables.with_variable_type_environment + ) + + append_secret_data_from_variables( + desired_config: config_to_apply, + secret_name: file_secret_name, + variables: workspace.workspace_variables.with_variable_type_file + ) + + append_secret_data( + desired_config: config_to_apply, + secret_name: file_secret_name, + data: { WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_NAME.to_sym => workspace.actual_state } + ) + end + + # @param [Object] kubernetes_object + # @param [RemoteDevelopment::Workspace] workspace + # @return [Boolean] + def self.should_exclude_object?(kubernetes_object, workspace) + object_identifier = "#{kubernetes_object.fetch(:kind)}/#{kubernetes_object.fetch( + :metadata, {}).fetch(:name)}" + + return excluded_for_terminated(workspace).include?(object_identifier) if workspace.desired_state_terminated? + + excluded_for_running_or_stopped(workspace).include?(object_identifier) + end + + # @param [RemoteDevelopment::Workspace] workspace + # @return [Set[String]] + def self.excluded_for_running_or_stopped(workspace) + Set.new([ + "ConfigMap/#{workspace.name}-secrets-inventory", + "ResourceQuota/#{workspace.name}", + "Secret/#{workspace.name}-env-var", + "Secret/#{workspace.name}-file" + ]) + end + + # @param [RemoteDevelopment::Workspace] workspace + # @return [Set[String]] + def self.excluded_for_terminated(workspace) + Set.new([ + "Deployment/#{workspace.name}", + "Service/#{workspace.name}", + "PersistentVolumeClaim/#{workspace.name}-gl-workspace-data", + "NetworkPolicy/#{workspace.name}", + "ConfigMap/#{workspace.name}-scripts-configmap", + "ResourceQuota/#{workspace.name}", + "ServiceAccount/#{workspace.name}", + "Secret/#{workspace.name}-env-var", + "Secret/#{workspace.name}-file" + ]) + end + + # @param [Array] desired_config + # @param [String] secret_name + # @param [ActiveRecord::Relation] variables + # @return [void] + def self.append_secret_data_from_variables(desired_config:, 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( + desired_config: desired_config, + secret_name: secret_name, + data: data + ) + + nil + end + + # @param [Array] desired_config + # @param [String] secret_name + # @param [Hash] data + # @return [void] + # noinspection RubyUnusedLocalVariable -- Rubymine doesn't recognize '^' to use a variable in pattern-matching + def self.append_secret_data(desired_config:, secret_name:, data:) + desired_config => [ + *_, + { + metadata: { + name: ^secret_name + }, + data: secret_data + }, + *_ + ] + + transformed_data = data.transform_values { |value| Base64.strict_encode64(value) } + + secret_data.merge!(transformed_data) + + nil + end + + private_class_method :inject_secrets, + :should_exclude_object?, + :excluded_for_running_or_stopped, + :excluded_for_terminated, + :append_secret_data_from_variables, + :append_secret_data + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb new file mode 100644 index 00000000000000..c61f65f64abdae --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb @@ -0,0 +1,48 @@ +# 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 ConfigToApplyFetcher + # @param [RemoteDevelopment::Workspace] workspace + # @param [RemoteDevelopment::Logger] logger + # @param [Boolean] include_all_resources + # @return [Array] + def self.generate_and_compare_config_to_apply(workspace:, logger:, include_all_resources:) + old_config_to_apply_array = OldDesiredConfigGenerator.generate_desired_config( + workspace: workspace, + include_all_resources: include_all_resources, + logger: logger + ) + new_config_to_apply_array = ConfigToApplyBuilder.build( + workspace: workspace, + include_all_resources: include_all_resources, + logger: logger + ) + + old_config_to_apply = DesiredConfig.new(desired_config_array: old_config_to_apply_array) + new_config_to_apply = DesiredConfig.new(desired_config_array: 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, using old version.", + error_type: "reconcile_desired_configs_differ", + workspace_id: workspace.id, + diff: diff + ) + end + + old_config_to_apply_array + end + 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 00000000000000..9b5b1880e66e14 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + class DesiredConfigFetcher + # @param workspace [RemoteDevelopment::Workspace] + # @param logger [Logger] + # @return [Array] + def self.fetch(workspace:, logger:) + agentk_state = workspace.agentk_state + if agentk_state + array = agentk_state.desired_config + return array.map(&:deep_symbolize_keys) + end + + # TODO: remove this after a succesful shadow run + # Issue- https://gitlab.com/gitlab-org/gitlab/-/issues/551935 + fetch_from_create_desired_config_main(workspace: workspace, logger: logger) + end + + # @param workspace [RemoteDevelopment::Workspace] + # @param logger [Logger] + def self.fetch_from_create_desired_config_main(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.desired_config_array + end + + private_class_method :fetch_from_create_desired_config_main + 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 4183d1fcc0e58b..f5dd093ca3b35c 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,7 +84,7 @@ 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( + workspace_resources = ConfigToApplyFetcher.generate_and_compare_config_to_apply( workspace: workspace, include_all_resources: include_all_resources, logger: logger diff --git a/ee/spec/fixtures/remote_development/example.config_to_apply.json b/ee/spec/fixtures/remote_development/example.config_to_apply.json new file mode 100644 index 00000000000000..8751f744e7f918 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.config_to_apply.json @@ -0,0 +1,566 @@ +[ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "environment": "production", + "team": "engineering", + "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" + }, + "name": "workspace-991-990-fedcba-workspace-inventory", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "containers": [ + { + "args": [ + "echo 'tooling container args'" + ], + "command": [ + "/bin/sh", + "-c" + ], + "env": [ + { + "name": "GL_ENV_NAME", + "value": "gl-env-value" + }, + { + "name": "PROJECTS_ROOT", + "value": "/projects" + }, + { + "name": "PROJECT_SOURCE", + "value": "/projects" + } + ], + "envFrom": [ + { + "secretRef": { + "name": "workspace-991-990-fedcba-env-var" + } + } + ], + "image": "quay.io/mloriedo/universal-developer-image:ubi8-dw-demo", + "imagePullPolicy": "Always", + "name": "tooling-container", + "ports": [ + { + "containerPort": 60001, + "name": "server", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi" + }, + "requests": { + "cpu": "0.5", + "memory": "512Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false, + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "volumeMounts": [ + { + "mountPath": "/projects", + "name": "gl-workspace-data" + }, + { + "mountPath": "/.workspace-data/variables/file", + "name": "gl-workspace-variables" + }, + { + "name": "gl-workspace-scripts", + "mountPath": "/workspace-scripts" + } + ], + "lifecycle": { + "postStart": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "#!/bin/sh\n\nmkdir -p \"${GL_WORKSPACE_LOGS_DIR}\"\nln -sf \"${GL_WORKSPACE_LOGS_DIR}\" /tmp\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running poststart commands for workspace...\"\n\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running internal blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-internal-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\"\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running non-blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-non-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\" &\n" + ] + } + } + } + } + ], + "initContainers": [ + { + "args": [ + "echo 'project cloner container args'" + ], + "command": [ + "/bin/sh", + "-c" + ], + "env": [ + { + "name": "PROJECTS_ROOT", + "value": "/projects" + }, + { + "name": "PROJECT_SOURCE", + "value": "/projects" + } + ], + "envFrom": [ + { + "secretRef": { + "name": "workspace-991-990-fedcba-env-var" + } + } + ], + "image": "alpine/git:2.45.2", + "imagePullPolicy": "Always", + "name": "gl-project-cloner-gl-project-cloner-command-1", + "resources": { + "limits": { + "cpu": "500m", + "memory": "1000Mi" + }, + "requests": { + "cpu": "100m", + "memory": "500Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false, + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "volumeMounts": [ + { + "mountPath": "/projects", + "name": "gl-workspace-data" + }, + { + "mountPath": "/.workspace-data/variables/file", + "name": "gl-workspace-variables" + } + ] + } + ], + "runtimeClassName": "standard", + "securityContext": { + "fsGroup": 0, + "fsGroupChangePolicy": "OnRootMismatch", + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "serviceAccountName": "workspace-991-990-fedcba", + "volumes": [ + { + "name": "gl-workspace-data", + "persistentVolumeClaim": { + "claimName": "workspace-991-990-fedcba-gl-workspace-data" + } + }, + { + "name": "gl-workspace-variables", + "projected": { + "defaultMode": 508, + "sources": [ + { + "secret": { + "name": "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + "name": "gl-workspace-scripts", + "projected": { + "defaultMode": 365, + "sources": [ + { + "configMap": { + "name": "workspace-991-990-fedcba-scripts-configmap" + } + } + ] + } + } + ] + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "ports": [ + { + "name": "server", + "port": 60001, + "targetPort": 60001 + } + ], + "selector": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-gl-workspace-data", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "50Gi" + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "automountServiceAccountToken": false, + "imagePullSecrets": [ + { + "name": "registry-secret" + } + ], + "kind": "ServiceAccount", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "egress": [ + { + "ports": [ + { + "port": 53, + "protocol": "TCP" + }, + { + "port": 53, + "protocol": "UDP" + } + ], + "to": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "kube-system" + } + } + } + ] + }, + { + "to": [ + { + "ipBlock": { + "cidr": "0.0.0.0/0", + "except": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + } + } + ] + } + ], + "ingress": [ + { + "from": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "gitlab-workspaces" + } + }, + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "gitlab-workspaces-proxy" + } + } + } + ] + } + ], + "podSelector": {}, + "policyTypes": [ + "Ingress", + "Egress" + ] + } + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-scripts-configmap", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "data": {} + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "environment": "production", + "team": "engineering", + "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory" + }, + "name": "workspace-991-990-fedcba-secrets-inventory", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "v1", + "kind": "ResourceQuota", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "hard": { + "limits.cpu": "2", + "limits.memory": "4Gi", + "requests.cpu": "1", + "requests.memory": "1Gi" + } + } + }, + { + "apiVersion": "v1", + "data": { + "ENV_VAR1": "ZW52LXZhci12YWx1ZTE=" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-env-var", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "v1", + "data": { + "FILE_VAR1": "ZmlsZS12YXItdmFsdWUx", + "gl_workspace_reconciled_actual_state.txt": "UnVubmluZw==" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-file", + "namespace": "gl-rd-ns-991-990-fedcba" + } + } +] diff --git a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json new file mode 100644 index 00000000000000..91c11412022667 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json @@ -0,0 +1,467 @@ +[ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "environment": "production", + "team": "engineering", + "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" + }, + "name": "workspace-991-990-fedcba-workspace-inventory", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "containers": [ + { + "args": [ + "echo 'tooling container args'" + ], + "command": [ + "/bin/sh", + "-c" + ], + "env": [ + { + "name": "GL_ENV_NAME", + "value": "gl-env-value" + }, + { + "name": "PROJECTS_ROOT", + "value": "/projects" + }, + { + "name": "PROJECT_SOURCE", + "value": "/projects" + } + ], + "envFrom": [ + { + "secretRef": { + "name": "workspace-991-990-fedcba-env-var" + } + } + ], + "image": "quay.io/mloriedo/universal-developer-image:ubi8-dw-demo", + "imagePullPolicy": "Always", + "name": "tooling-container", + "ports": [ + { + "containerPort": 60001, + "name": "server", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi" + }, + "requests": { + "cpu": "0.5", + "memory": "512Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false, + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "volumeMounts": [ + { + "mountPath": "/projects", + "name": "gl-workspace-data" + }, + { + "mountPath": "/.workspace-data/variables/file", + "name": "gl-workspace-variables" + }, + { + "name": "gl-workspace-scripts", + "mountPath": "/workspace-scripts" + } + ], + "lifecycle": { + "postStart": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "#!/bin/sh\n\nmkdir -p \"${GL_WORKSPACE_LOGS_DIR}\"\nln -sf \"${GL_WORKSPACE_LOGS_DIR}\" /tmp\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running poststart commands for workspace...\"\n\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running internal blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-internal-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\"\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running non-blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-non-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\" &\n" + ] + } + } + } + } + ], + "initContainers": [ + { + "args": [ + "echo 'project cloner container args'" + ], + "command": [ + "/bin/sh", + "-c" + ], + "env": [ + { + "name": "PROJECTS_ROOT", + "value": "/projects" + }, + { + "name": "PROJECT_SOURCE", + "value": "/projects" + } + ], + "envFrom": [ + { + "secretRef": { + "name": "workspace-991-990-fedcba-env-var" + } + } + ], + "image": "alpine/git:2.45.2", + "imagePullPolicy": "Always", + "name": "gl-project-cloner-gl-project-cloner-command-1", + "resources": { + "limits": { + "cpu": "500m", + "memory": "1000Mi" + }, + "requests": { + "cpu": "100m", + "memory": "500Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false, + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "volumeMounts": [ + { + "mountPath": "/projects", + "name": "gl-workspace-data" + }, + { + "mountPath": "/.workspace-data/variables/file", + "name": "gl-workspace-variables" + } + ] + } + ], + "runtimeClassName": "standard", + "securityContext": { + "fsGroup": 0, + "fsGroupChangePolicy": "OnRootMismatch", + "runAsNonRoot": true, + "runAsUser": 5001 + }, + "serviceAccountName": "workspace-991-990-fedcba", + "volumes": [ + { + "name": "gl-workspace-data", + "persistentVolumeClaim": { + "claimName": "workspace-991-990-fedcba-gl-workspace-data" + } + }, + { + "name": "gl-workspace-variables", + "projected": { + "defaultMode": 508, + "sources": [ + { + "secret": { + "name": "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + "name": "gl-workspace-scripts", + "projected": { + "defaultMode": 365, + "sources": [ + { + "configMap": { + "name": "workspace-991-990-fedcba-scripts-configmap" + } + } + ] + } + } + ] + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "ports": [ + { + "name": "server", + "port": 60001, + "targetPort": 60001 + } + ], + "selector": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "creationTimestamp": null, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-gl-workspace-data", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "50Gi" + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "automountServiceAccountToken": false, + "imagePullSecrets": [ + { + "name": "registry-secret" + } + ], + "kind": "ServiceAccount", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "spec": { + "egress": [ + { + "ports": [ + { + "port": 53, + "protocol": "TCP" + }, + { + "port": 53, + "protocol": "UDP" + } + ], + "to": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "kube-system" + } + } + } + ] + }, + { + "to": [ + { + "ipBlock": { + "cidr": "0.0.0.0/0", + "except": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + } + } + ] + } + ], + "ingress": [ + { + "from": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "gitlab-workspaces" + } + }, + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "gitlab-workspaces-proxy" + } + } + } + ] + } + ], + "podSelector": {}, + "policyTypes": [ + "Ingress", + "Egress" + ] + } + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "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/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991" + }, + "name": "workspace-991-990-fedcba-scripts-configmap", + "namespace": "gl-rd-ns-991-990-fedcba" + }, + "data": {} + } +] diff --git a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json new file mode 100644 index 00000000000000..559e71f1289f04 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json @@ -0,0 +1,44 @@ +[ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "environment": "production", + "team": "engineering", + "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" + }, + "name": "workspace-991-990-fedcba-workspace-inventory", + "namespace": "gl-rd-ns-991-990-fedcba" + } + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "annotations": { + "environment": "production", + "team": "engineering", + "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" + }, + "labels": { + "app": "workspace", + "tier": "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory" + }, + "name": "workspace-991-990-fedcba-secrets-inventory", + "namespace": "gl-rd-ns-991-990-fedcba" + } + } +] 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 69d172707cc1e1..94551edb987fbb 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 @@ -187,7 +187,7 @@ end expect(workspace.agentk_state).to be_present - expect(workspace.agentk_state.desired_config.fetch("desired_config_array")).to be_an(Array) + expect(workspace.agentk_state.desired_config).to be_an(Array) 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 26b20d9282c273..9c82b81dc33b1a 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 @@ -10,7 +10,7 @@ let(:workspace_name) { "workspace-991-990-fedcba" } let(:workspace) { create(:workspace, name: workspace_name) } - let(:expected_desired_config_json) { desired_config.as_json } + let(:expected_desired_config_json) { desired_config.as_json.map(&:deep_stringify_keys) } let(:logger) { instance_double(RemoteDevelopment::Logger) } let(:desired_config) do ::RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: create_desired_config_array) 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 ee6a91ba9b0293..411f34ee6982df 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,13 +61,15 @@ end end - describe '#to_json' do + describe "#as_json" do let(:desired_config_array) { create_desired_config_array } - subject(:result) { desired_config.to_json } + context "when called without options" do + subject(:result) { desired_config.as_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(result).to eq(desired_config_array) } + it { expect(result).to be_kind_of(Array) } + end end describe '#==(other)' do 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 00000000000000..fdb60fddfe4ad3 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb @@ -0,0 +1,140 @@ +# 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 remote development shared fixtures" + 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(:actual_state) { states_module::RUNNING } + + # 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 + + let(:logger) { instance_double("RemoteDevelopment::Logger") } + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:include_all_resources) { true } + let(:desired_config_array) { create_desired_config_array } + let(:config_extractor_result) do + { + env_secret_name: "workspace-991-990-fedcba-env-var", + file_secret_name: "workspace-991-990-fedcba-file" + } + end + + describe "#build" do + subject(:config_to_apply) do + # noinspection RubyMismatchedArgumentType -- logger is mocked but RubyMine does not like it + described_class.build( + workspace: workspace, + include_all_resources: include_all_resources, + logger: logger + ) + end + + before do + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) + .to(receive(:fetch)) + .with(workspace: workspace, logger: logger) + .and_return(desired_config_array) + + allow(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::ConfigValuesExtractor) + .to(receive(:extract)) + .with(workspace: workspace) + .and_return(config_extractor_result) + end + + context "when include_all_resources is true" do + let(:desired_state_running) { true } + let(:desired_state_terminated) { false } + let(:expected_config_to_apply) { create_config_to_apply_array } + + it "returns all the resources including secret data" do + expect(config_to_apply).to eq(expected_config_to_apply) + end + end + + describe "when include_all_resources is false" do + let(:include_all_resources) { false } + + 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 + + context "when workspace.desired_state is terminated" do + let(:desired_state_running) { false } + let(:desired_state_terminated) { true } + let(:expected_config_to_apply) do + create_config_to_apply_array_with_partial_reconciliation_when_desired_state_is_terminated + end + + it_behaves_like "returns expected config_to_apply" + end + + context "when workspace.desired_state is not terminated" do + let(:desired_state_running) { true } + let(:desired_state_terminated) { false } + let(:expected_config_to_apply) { create_config_to_apply_array_with_partial_reconciliation } + + it_behaves_like "returns expected config_to_apply" + end + end + + def create_config_to_apply_array + json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file("example.config_to_apply.json") + Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) + end + + def create_config_to_apply_array_with_partial_reconciliation_when_desired_state_is_terminated + json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file( + "example.config_to_apply_partial_reconciliation_desired_state_terminated.json") + Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) + end + + def create_config_to_apply_array_with_partial_reconciliation + json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file( + "example.config_to_apply_partial_reconciliation.json") + Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) + end + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb new file mode 100644 index 00000000000000..9e8ae0f25089cd --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyFetcher, feature_category: :workspaces do + describe "#generate_and_compare_config_to_apply" 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 + let(:expected_include_all_resources) { true } + let(:old_desired_config_array) { [{ kind: "Deployment" }] } + let(:new_desired_config_array) { [{ kind: "Deployment" }] } + + # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace and Logger + subject(:result) do + described_class.generate_and_compare_config_to_apply( + workspace: workspace, + logger: logger, + include_all_resources: true + ) + end + + before do + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) + .to(receive(:build)) + .with(workspace: workspace, include_all_resources: true, logger: logger) + .and_return(new_desired_config_array) + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator) + .to(receive(:generate_desired_config)) + .with(workspace: workspace, include_all_resources: true, logger: logger) + .and_return(old_desired_config_array) + end + + context "when config_to_apply are same" do + it "returns the value from OldDesiredConfigGenerator" do + expect(logger).not_to receive(:warn) + expect(result).to eq(old_desired_config_array) + end + end + + context "when config_to_apply are different" do + let(:old_desired_config_array) { [{ kind: "Deployment" }] } + let(:new_desired_config_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: "reconcile_desired_configs_differ", + message: "The generated config_to_apply from Create::DesiredConfig::Main and OldDesiredConfigGenerator " \ + "differ, using old version.", + workspace_id: 1 + }) + expect(result).to eq(old_desired_config_array) + end + 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 00000000000000..127fed5b83f0a3 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_fetcher_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher, feature_category: :workspaces do + # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version of these models, so we can use fast_spec_helper. + let(:logger) { instance_double("Logger") } + let(:agent) { instance_double("Clusters::Agent", id: 1) } + let(:workspace) { instance_double("RemoteDevelopment::Workspace", agent: agent) } + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:context) do + { + params: { + agent: agent + }, + workspace: workspace, + logger: logger + } + end + + let(:desired_config_array) { [{ kind: "Development" }] } + let(:expected_desired_config) do + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: desired_config_array) + end + + describe "#fetch" do + # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace + subject(:result) { described_class.fetch(workspace: workspace, logger: logger) } + + context "when agentk_state exists" do + let(:agentk_state) { instance_double("RemoteDevelopment::WorkspaceAgentkState") } # rubocop:disable RSpec/VerifiedDoubleReference -- we want to use fast_spec_helper which does not load this class + + before do + allow(workspace).to receive(:agentk_state).and_return(agentk_state) + allow(agentk_state).to receive(:desired_config).and_return(desired_config_array) + end + + it "returns desired_config_array from database" do + expect(result).to eq(desired_config_array) + end + + it "does not call Create::DesiredConfig::Main" do + expect(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main).not_to receive(:main) + result + end + end + + context "when agentk_state does not exist" do + before do + allow(workspace).to receive(:agentk_state).and_return(nil) + allow(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main) + .to(receive(:main)) + .with(context) + .and_return({ desired_config: expected_desired_config }) + end + + it "calls database" do + expect(workspace).to receive(:agentk_state) + result + end + + it "returns desired_config from Create::DesiredConfig::Main" do + # TODO: The class under test will not call DesiredConfig::Main by the end of this issue + # https://gitlab.com/gitlab-org/gitlab/-/issues/541907. This is done only to do the shadow run. + # noinspection RubyArgCount -- Rubymine thinks `with` is only supposed to be called as key-value pair + expect(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main) + .to(receive(:main)) + .with(context) + .and_return({ + desired_config: expected_desired_config + }) + + expect(result).to eq(desired_config_array) + end + end + end +end 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 787d855942f38c..cc31d7724fe522 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 @@ -121,10 +121,17 @@ ) end + # TODO Rename this context when the versioning of config generator is removed context "when workspace.desired_config_generator_version is current version" do let(:desired_config_generator_version) { current_desired_config_generator_version } before do + # NOTE: This is put here as a placeholder because we do not really care about the output of this function + # This temporarily allows to let shadow run of Create::DesiredConfig::Main pass. + # The actual test for the shadow run is written in config_to_apply_fetcher_spec.rb. + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) + .to(receive(:build)) + .and_return(generated_config_to_apply) 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 } 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 8220898274bfd8..0080354dab3249 100644 --- a/ee/spec/models/remote_development/workspace_agentk_state_spec.rb +++ b/ee/spec/models/remote_development/workspace_agentk_state_spec.rb @@ -32,5 +32,27 @@ 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 + context "when desired_config is not an array" do + shared_examples "invalid desired_config" do |value_description, test_value, expected_error| + context "when desired_config is #{value_description}" do + let(:workspace_agentk_state) do + build(:workspace_agentk_state, workspace: workspace, project: project, desired_config: test_value) + 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 + end + + include_examples "invalid desired_config", "a hash", { key: "value" }, "must be an array" + include_examples "invalid desired_config", "a string", "string", "must be an array" + include_examples "invalid desired_config", "a number", 1, "must be an array" + include_examples "invalid desired_config", "an empty array", [], "can't be blank" + end + end end end -- GitLab From ff1f8fbf29ab683007b0d649125d5754b6f0971c Mon Sep 17 00:00:00 2001 From: Ashvin Sharma Date: Mon, 30 Jun 2025 23:43:22 +0530 Subject: [PATCH 2/4] Address review comments Reconciliation logic will now filter the resources to be included in partial reconciliation using an annotation --- .../desired_config/config_values_extractor.rb | 5 +- .../desired_config/devfile_parser_getter.rb | 5 +- .../devfile_resource_appender.rb | 15 +- .../output/config_to_apply_builder.rb | 71 +-- .../output/config_to_apply_fetcher.rb | 32 +- .../workspace_operations_constants.rb | 5 + .../example.config_to_apply.json | 566 ------------------ ...onfig_to_apply_partial_reconciliation.json | 467 --------------- ...conciliation_desired_state_terminated.json | 44 -- .../devfile_parser_getter_spec.rb | 3 + .../devfile_resource_appender_spec.rb | 31 + .../desired_config/main_integeration_spec.rb | 165 +++-- .../output/config_to_apply_builder_spec.rb | 206 ++++++- 13 files changed, 400 insertions(+), 1215 deletions(-) delete mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply.json delete mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json delete mode 100644 ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json 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 157c7dd0350d37..eb64c90dd85584 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] @@ -60,8 +61,8 @@ def self.extract(context) # 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), 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 893076083c2473..63a8460d623ab8 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,6 +5,8 @@ module WorkspaceOperations module Create module DesiredConfig class DevfileParserGetter + include WorkspaceOperationsConstants + # @param [Hash] context # @return [Hash] def self.get(context) @@ -18,6 +20,7 @@ def self.get(context) workspace_namespace: workspace_namespace, replicas: replicas } + extra_annotations = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } begin context.merge( @@ -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(annotations.merge(extra_annotations).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 2d6256de6af622..42f2a93faab5a6 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,6 +5,8 @@ module WorkspaceOperations module Create module DesiredConfig class DevfileResourceAppender + include WorkspaceOperationsConstants + def self.append(context) context => { desired_config_array: desired_config_array, @@ -34,7 +36,8 @@ def self.append(context) namespace: workspace_namespace, labels: labels, annotations: common_annotations, - prepend: true + prepend: true, + include_in_partial_reconciliation: true ) append_image_pull_secrets_service_account( @@ -44,6 +47,7 @@ def self.append(context) image_pull_secrets: image_pull_secrets, labels: labels, annotations: workspace_inventory_annotations + .merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }) ) append_network_policy( @@ -55,6 +59,7 @@ def self.append(context) network_policy_egress: network_policy_egress, labels: labels, annotations: workspace_inventory_annotations + .merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }) ) append_scripts_resources( @@ -115,9 +120,13 @@ def self.append_inventory_config_map( namespace:, labels:, annotations:, - prepend: false + prepend: false, + include_in_partial_reconciliation: false ) extra_labels = { "cli-utils.sigs.k8s.io/inventory-id": name } + extra_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } + + annotations = annotations.merge(extra_annotation) if include_in_partial_reconciliation config_map = { kind: "ConfigMap", @@ -296,7 +305,7 @@ def self.append_scripts_resources( name: name, namespace: namespace, labels: labels, - annotations: annotations, + annotations: annotations.merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }), devfile_commands: devfile_commands, devfile_events: devfile_events ) 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 index dfbc2a74455154..db15fde214bf42 100644 --- 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 @@ -8,20 +8,43 @@ module Output 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::Logger] logger # @return [Array] def self.build(workspace:, include_all_resources:, logger:) - 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}" config_to_apply = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) inject_secrets(config_to_apply, env_secret_name, file_secret_name, workspace) return config_to_apply if include_all_resources - config_to_apply.reject { |object| should_exclude_object?(object, workspace) } + filter_partial_resources(config_to_apply, workspace.desired_state_terminated?) + end + + # @param [Array] config_to_apply + # @param [Boolean] is_desired_state_terminated + # @return [Array] + def self.filter_partial_resources(config_to_apply, is_desired_state_terminated) + if is_desired_state_terminated + return config_to_apply.select do |object| + if object.fetch(:kind) == "ConfigMap" + name = object.dig(:metadata, :name) + ALLOWLIST_WHEN_STATE_TERMINATED.any? { |regex| name =~ regex } + end + end + end + + config_to_apply.select do |object| + object.dig(:metadata, :annotations, ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION).present? + end end # Mutates `config_to_apply` and inject secret data in the `env_secret_name` @@ -50,45 +73,6 @@ def self.inject_secrets(config_to_apply, env_secret_name, file_secret_name, work ) end - # @param [Object] kubernetes_object - # @param [RemoteDevelopment::Workspace] workspace - # @return [Boolean] - def self.should_exclude_object?(kubernetes_object, workspace) - object_identifier = "#{kubernetes_object.fetch(:kind)}/#{kubernetes_object.fetch( - :metadata, {}).fetch(:name)}" - - return excluded_for_terminated(workspace).include?(object_identifier) if workspace.desired_state_terminated? - - excluded_for_running_or_stopped(workspace).include?(object_identifier) - end - - # @param [RemoteDevelopment::Workspace] workspace - # @return [Set[String]] - def self.excluded_for_running_or_stopped(workspace) - Set.new([ - "ConfigMap/#{workspace.name}-secrets-inventory", - "ResourceQuota/#{workspace.name}", - "Secret/#{workspace.name}-env-var", - "Secret/#{workspace.name}-file" - ]) - end - - # @param [RemoteDevelopment::Workspace] workspace - # @return [Set[String]] - def self.excluded_for_terminated(workspace) - Set.new([ - "Deployment/#{workspace.name}", - "Service/#{workspace.name}", - "PersistentVolumeClaim/#{workspace.name}-gl-workspace-data", - "NetworkPolicy/#{workspace.name}", - "ConfigMap/#{workspace.name}-scripts-configmap", - "ResourceQuota/#{workspace.name}", - "ServiceAccount/#{workspace.name}", - "Secret/#{workspace.name}-env-var", - "Secret/#{workspace.name}-file" - ]) - end - # @param [Array] desired_config # @param [String] secret_name # @param [ActiveRecord::Relation] variables @@ -132,9 +116,6 @@ def self.append_secret_data(desired_config:, secret_name:, data:) end private_class_method :inject_secrets, - :should_exclude_object?, - :excluded_for_running_or_stopped, - :excluded_for_terminated, :append_secret_data_from_variables, :append_secret_data end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb index c61f65f64abdae..bc4c4bf17c8515 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb @@ -10,6 +10,8 @@ module Output # than the old one. In this case we will log a warning and use the old desired # config generator instead. class ConfigToApplyFetcher + include WorkspaceOperationsConstants + # @param [RemoteDevelopment::Workspace] workspace # @param [RemoteDevelopment::Logger] logger # @param [Boolean] include_all_resources @@ -20,11 +22,7 @@ def self.generate_and_compare_config_to_apply(workspace:, logger:, include_all_r include_all_resources: include_all_resources, logger: logger ) - new_config_to_apply_array = ConfigToApplyBuilder.build( - workspace: workspace, - include_all_resources: include_all_resources, - logger: logger - ) + new_config_to_apply_array = generate_new_config(include_all_resources, logger, workspace) old_config_to_apply = DesiredConfig.new(desired_config_array: old_config_to_apply_array) new_config_to_apply = DesiredConfig.new(desired_config_array: new_config_to_apply_array) @@ -41,6 +39,30 @@ def self.generate_and_compare_config_to_apply(workspace:, logger:, include_all_r old_config_to_apply_array end + + def self.generate_new_config(include_all_resources, logger, workspace) + config = ConfigToApplyBuilder.build( + workspace: workspace, + include_all_resources: include_all_resources, + logger: logger + ) + + config.map do |resource| + 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_new_config end end 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 5232532367c4b2..0bb85d430754aa 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/fixtures/remote_development/example.config_to_apply.json b/ee/spec/fixtures/remote_development/example.config_to_apply.json deleted file mode 100644 index 8751f744e7f918..00000000000000 --- a/ee/spec/fixtures/remote_development/example.config_to_apply.json +++ /dev/null @@ -1,566 +0,0 @@ -[ - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "environment": "production", - "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", - "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991", - "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" - }, - "name": "workspace-991-990-fedcba-workspace-inventory", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "replicas": 1, - "selector": { - "matchLabels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - } - }, - "strategy": { - "type": "Recreate" - }, - "template": { - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "containers": [ - { - "args": [ - "echo 'tooling container args'" - ], - "command": [ - "/bin/sh", - "-c" - ], - "env": [ - { - "name": "GL_ENV_NAME", - "value": "gl-env-value" - }, - { - "name": "PROJECTS_ROOT", - "value": "/projects" - }, - { - "name": "PROJECT_SOURCE", - "value": "/projects" - } - ], - "envFrom": [ - { - "secretRef": { - "name": "workspace-991-990-fedcba-env-var" - } - } - ], - "image": "quay.io/mloriedo/universal-developer-image:ubi8-dw-demo", - "imagePullPolicy": "Always", - "name": "tooling-container", - "ports": [ - { - "containerPort": 60001, - "name": "server", - "protocol": "TCP" - } - ], - "resources": { - "limits": { - "cpu": "1", - "memory": "1Gi" - }, - "requests": { - "cpu": "0.5", - "memory": "512Mi" - } - }, - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false, - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "volumeMounts": [ - { - "mountPath": "/projects", - "name": "gl-workspace-data" - }, - { - "mountPath": "/.workspace-data/variables/file", - "name": "gl-workspace-variables" - }, - { - "name": "gl-workspace-scripts", - "mountPath": "/workspace-scripts" - } - ], - "lifecycle": { - "postStart": { - "exec": { - "command": [ - "/bin/sh", - "-c", - "#!/bin/sh\n\nmkdir -p \"${GL_WORKSPACE_LOGS_DIR}\"\nln -sf \"${GL_WORKSPACE_LOGS_DIR}\" /tmp\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running poststart commands for workspace...\"\n\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running internal blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-internal-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\"\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running non-blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-non-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\" &\n" - ] - } - } - } - } - ], - "initContainers": [ - { - "args": [ - "echo 'project cloner container args'" - ], - "command": [ - "/bin/sh", - "-c" - ], - "env": [ - { - "name": "PROJECTS_ROOT", - "value": "/projects" - }, - { - "name": "PROJECT_SOURCE", - "value": "/projects" - } - ], - "envFrom": [ - { - "secretRef": { - "name": "workspace-991-990-fedcba-env-var" - } - } - ], - "image": "alpine/git:2.45.2", - "imagePullPolicy": "Always", - "name": "gl-project-cloner-gl-project-cloner-command-1", - "resources": { - "limits": { - "cpu": "500m", - "memory": "1000Mi" - }, - "requests": { - "cpu": "100m", - "memory": "500Mi" - } - }, - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false, - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "volumeMounts": [ - { - "mountPath": "/projects", - "name": "gl-workspace-data" - }, - { - "mountPath": "/.workspace-data/variables/file", - "name": "gl-workspace-variables" - } - ] - } - ], - "runtimeClassName": "standard", - "securityContext": { - "fsGroup": 0, - "fsGroupChangePolicy": "OnRootMismatch", - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "serviceAccountName": "workspace-991-990-fedcba", - "volumes": [ - { - "name": "gl-workspace-data", - "persistentVolumeClaim": { - "claimName": "workspace-991-990-fedcba-gl-workspace-data" - } - }, - { - "name": "gl-workspace-variables", - "projected": { - "defaultMode": 508, - "sources": [ - { - "secret": { - "name": "workspace-991-990-fedcba-file" - } - } - ] - } - }, - { - "name": "gl-workspace-scripts", - "projected": { - "defaultMode": 365, - "sources": [ - { - "configMap": { - "name": "workspace-991-990-fedcba-scripts-configmap" - } - } - ] - } - } - ] - } - } - }, - "status": {} - }, - { - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "ports": [ - { - "name": "server", - "port": 60001, - "targetPort": 60001 - } - ], - "selector": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - } - }, - "status": { - "loadBalancer": {} - } - }, - { - "apiVersion": "v1", - "kind": "PersistentVolumeClaim", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-gl-workspace-data", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "accessModes": [ - "ReadWriteOnce" - ], - "resources": { - "requests": { - "storage": "50Gi" - } - } - }, - "status": {} - }, - { - "apiVersion": "v1", - "automountServiceAccountToken": false, - "imagePullSecrets": [ - { - "name": "registry-secret" - } - ], - "kind": "ServiceAccount", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "networking.k8s.io/v1", - "kind": "NetworkPolicy", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "egress": [ - { - "ports": [ - { - "port": 53, - "protocol": "TCP" - }, - { - "port": 53, - "protocol": "UDP" - } - ], - "to": [ - { - "namespaceSelector": { - "matchLabels": { - "kubernetes.io/metadata.name": "kube-system" - } - } - } - ] - }, - { - "to": [ - { - "ipBlock": { - "cidr": "0.0.0.0/0", - "except": [ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ] - } - } - ] - } - ], - "ingress": [ - { - "from": [ - { - "namespaceSelector": { - "matchLabels": { - "kubernetes.io/metadata.name": "gitlab-workspaces" - } - }, - "podSelector": { - "matchLabels": { - "app.kubernetes.io/name": "gitlab-workspaces-proxy" - } - } - } - ] - } - ], - "podSelector": {}, - "policyTypes": [ - "Ingress", - "Egress" - ] - } - }, - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-scripts-configmap", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "data": {} - }, - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "environment": "production", - "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", - "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991", - "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory" - }, - "name": "workspace-991-990-fedcba-secrets-inventory", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "v1", - "kind": "ResourceQuota", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "hard": { - "limits.cpu": "2", - "limits.memory": "4Gi", - "requests.cpu": "1", - "requests.memory": "1Gi" - } - } - }, - { - "apiVersion": "v1", - "data": { - "ENV_VAR1": "ZW52LXZhci12YWx1ZTE=" - }, - "kind": "Secret", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-env-var", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "v1", - "data": { - "FILE_VAR1": "ZmlsZS12YXItdmFsdWUx", - "gl_workspace_reconciled_actual_state.txt": "UnVubmluZw==" - }, - "kind": "Secret", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-file", - "namespace": "gl-rd-ns-991-990-fedcba" - } - } -] diff --git a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json deleted file mode 100644 index 91c11412022667..00000000000000 --- a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation.json +++ /dev/null @@ -1,467 +0,0 @@ -[ - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "environment": "production", - "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", - "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991", - "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" - }, - "name": "workspace-991-990-fedcba-workspace-inventory", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "replicas": 1, - "selector": { - "matchLabels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - } - }, - "strategy": { - "type": "Recreate" - }, - "template": { - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "containers": [ - { - "args": [ - "echo 'tooling container args'" - ], - "command": [ - "/bin/sh", - "-c" - ], - "env": [ - { - "name": "GL_ENV_NAME", - "value": "gl-env-value" - }, - { - "name": "PROJECTS_ROOT", - "value": "/projects" - }, - { - "name": "PROJECT_SOURCE", - "value": "/projects" - } - ], - "envFrom": [ - { - "secretRef": { - "name": "workspace-991-990-fedcba-env-var" - } - } - ], - "image": "quay.io/mloriedo/universal-developer-image:ubi8-dw-demo", - "imagePullPolicy": "Always", - "name": "tooling-container", - "ports": [ - { - "containerPort": 60001, - "name": "server", - "protocol": "TCP" - } - ], - "resources": { - "limits": { - "cpu": "1", - "memory": "1Gi" - }, - "requests": { - "cpu": "0.5", - "memory": "512Mi" - } - }, - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false, - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "volumeMounts": [ - { - "mountPath": "/projects", - "name": "gl-workspace-data" - }, - { - "mountPath": "/.workspace-data/variables/file", - "name": "gl-workspace-variables" - }, - { - "name": "gl-workspace-scripts", - "mountPath": "/workspace-scripts" - } - ], - "lifecycle": { - "postStart": { - "exec": { - "command": [ - "/bin/sh", - "-c", - "#!/bin/sh\n\nmkdir -p \"${GL_WORKSPACE_LOGS_DIR}\"\nln -sf \"${GL_WORKSPACE_LOGS_DIR}\" /tmp\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running poststart commands for workspace...\"\n\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running internal blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-internal-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\"\n\n{\n echo \"$(date -Iseconds): ----------------------------------------\"\n echo \"$(date -Iseconds): Running non-blocking poststart commands script...\"\n} >> \"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\"\n\n\"/workspace-scripts/gl-run-non-blocking-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\" &\n" - ] - } - } - } - } - ], - "initContainers": [ - { - "args": [ - "echo 'project cloner container args'" - ], - "command": [ - "/bin/sh", - "-c" - ], - "env": [ - { - "name": "PROJECTS_ROOT", - "value": "/projects" - }, - { - "name": "PROJECT_SOURCE", - "value": "/projects" - } - ], - "envFrom": [ - { - "secretRef": { - "name": "workspace-991-990-fedcba-env-var" - } - } - ], - "image": "alpine/git:2.45.2", - "imagePullPolicy": "Always", - "name": "gl-project-cloner-gl-project-cloner-command-1", - "resources": { - "limits": { - "cpu": "500m", - "memory": "1000Mi" - }, - "requests": { - "cpu": "100m", - "memory": "500Mi" - } - }, - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false, - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "volumeMounts": [ - { - "mountPath": "/projects", - "name": "gl-workspace-data" - }, - { - "mountPath": "/.workspace-data/variables/file", - "name": "gl-workspace-variables" - } - ] - } - ], - "runtimeClassName": "standard", - "securityContext": { - "fsGroup": 0, - "fsGroupChangePolicy": "OnRootMismatch", - "runAsNonRoot": true, - "runAsUser": 5001 - }, - "serviceAccountName": "workspace-991-990-fedcba", - "volumes": [ - { - "name": "gl-workspace-data", - "persistentVolumeClaim": { - "claimName": "workspace-991-990-fedcba-gl-workspace-data" - } - }, - { - "name": "gl-workspace-variables", - "projected": { - "defaultMode": 508, - "sources": [ - { - "secret": { - "name": "workspace-991-990-fedcba-file" - } - } - ] - } - }, - { - "name": "gl-workspace-scripts", - "projected": { - "defaultMode": 365, - "sources": [ - { - "configMap": { - "name": "workspace-991-990-fedcba-scripts-configmap" - } - } - ] - } - } - ] - } - } - }, - "status": {} - }, - { - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "ports": [ - { - "name": "server", - "port": 60001, - "targetPort": 60001 - } - ], - "selector": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - } - }, - "status": { - "loadBalancer": {} - } - }, - { - "apiVersion": "v1", - "kind": "PersistentVolumeClaim", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "creationTimestamp": null, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-gl-workspace-data", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "accessModes": [ - "ReadWriteOnce" - ], - "resources": { - "requests": { - "storage": "50Gi" - } - } - }, - "status": {} - }, - { - "apiVersion": "v1", - "automountServiceAccountToken": false, - "imagePullSecrets": [ - { - "name": "registry-secret" - } - ], - "kind": "ServiceAccount", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "networking.k8s.io/v1", - "kind": "NetworkPolicy", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "spec": { - "egress": [ - { - "ports": [ - { - "port": 53, - "protocol": "TCP" - }, - { - "port": 53, - "protocol": "UDP" - } - ], - "to": [ - { - "namespaceSelector": { - "matchLabels": { - "kubernetes.io/metadata.name": "kube-system" - } - } - } - ] - }, - { - "to": [ - { - "ipBlock": { - "cidr": "0.0.0.0/0", - "except": [ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ] - } - } - ] - } - ], - "ingress": [ - { - "from": [ - { - "namespaceSelector": { - "matchLabels": { - "kubernetes.io/metadata.name": "gitlab-workspaces" - } - }, - "podSelector": { - "matchLabels": { - "app.kubernetes.io/name": "gitlab-workspaces-proxy" - } - } - } - ] - } - ], - "podSelector": {}, - "policyTypes": [ - "Ingress", - "Egress" - ] - } - }, - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "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/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991" - }, - "name": "workspace-991-990-fedcba-scripts-configmap", - "namespace": "gl-rd-ns-991-990-fedcba" - }, - "data": {} - } -] diff --git a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json b/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json deleted file mode 100644 index 559e71f1289f04..00000000000000 --- a/ee/spec/fixtures/remote_development/example.config_to_apply_partial_reconciliation_desired_state_terminated.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "environment": "production", - "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", - "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991", - "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory" - }, - "name": "workspace-991-990-fedcba-workspace-inventory", - "namespace": "gl-rd-ns-991-990-fedcba" - } - }, - { - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": { - "annotations": { - "environment": "production", - "team": "engineering", - "workspaces.gitlab.com/host-template": "3000-workspace-991-990-fedcba.workspaces.localdev.me", - "workspaces.gitlab.com/id": "993", - "workspaces.gitlab.com/max-resources-per-workspace-sha256": "24aefc317e11db538ede450d1773e273966b9801b988d49e1219f2a9bf8e7f66" - }, - "labels": { - "app": "workspace", - "tier": "development", - "agent.gitlab.com/id": "991", - "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory" - }, - "name": "workspace-991-990-fedcba-secrets-inventory", - "namespace": "gl-rd-ns-991-990-fedcba" - } - } -] 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 8ed1b10b75e58e..dbea4dad9f0084 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 @@ -37,6 +37,7 @@ annotations: k1: v1 k2: v2 + workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value @@ -56,6 +57,7 @@ annotations: k1: v1 k2: v2 + workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value @@ -92,6 +94,7 @@ annotations: k1: v1 k2: v2 + workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value 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 9cd79bde0f123f..a46776e604cfe5 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 @@ -42,6 +43,7 @@ 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" } @@ -119,6 +121,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/main_integeration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb index 3991dca84562da..d307230addded3 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/reconcile/output/config_to_apply_builder_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_builder_spec.rb index fdb60fddfe4ad3..3f6a41696fbc24 100644 --- 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 @@ -3,7 +3,6 @@ require "fast_spec_helper" RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder, :unlimited_max_formatted_output_length, feature_category: :workspaces do - include_context "with remote development shared fixtures" 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. @@ -52,7 +51,100 @@ # rubocop:enable RSpec/VerifiedDoubleReference let(:include_all_resources) { true } - let(:desired_config_array) { create_desired_config_array } + let(:desired_config_array) do + [ + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-workspace-inventory", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "Deployment", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "Service", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "PersistentVolumeClaim", + metadata: { + name: "workspace-991-990-fedcba-gl-workspace-data", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ServiceAccount", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "NetworkPolicy", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-scripts-configmap", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-secrets-inventory" + } + }, + { + kind: "ResourceQuota", + metadata: { + name: "workspace-991-990-fedcba" + } + }, + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-env-var" + }, + data: {} + }, + { + kind: "Secret", + metadata: { + name: "workspace-991-990-fedcba-file" + }, + data: {} + } + ] + end + let(:config_extractor_result) do { env_secret_name: "workspace-991-990-fedcba-env-var", @@ -75,17 +167,12 @@ .to(receive(:fetch)) .with(workspace: workspace, logger: logger) .and_return(desired_config_array) - - allow(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::ConfigValuesExtractor) - .to(receive(:extract)) - .with(workspace: workspace) - .and_return(config_extractor_result) end context "when include_all_resources is true" do let(:desired_state_running) { true } let(:desired_state_terminated) { false } - let(:expected_config_to_apply) { create_config_to_apply_array } + let(:expected_config_to_apply) { desired_config_array } it "returns all the resources including secret data" do expect(config_to_apply).to eq(expected_config_to_apply) @@ -105,7 +192,23 @@ let(:desired_state_running) { false } let(:desired_state_terminated) { true } let(:expected_config_to_apply) do - create_config_to_apply_array_with_partial_reconciliation_when_desired_state_is_terminated + [ + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-workspace-inventory", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-secrets-inventory" + } + } + ] end it_behaves_like "returns expected config_to_apply" @@ -114,27 +217,76 @@ context "when workspace.desired_state is not terminated" do let(:desired_state_running) { true } let(:desired_state_terminated) { false } - let(:expected_config_to_apply) { create_config_to_apply_array_with_partial_reconciliation } + let(:expected_config_to_apply) do + [ + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-workspace-inventory", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "Deployment", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "Service", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "PersistentVolumeClaim", + metadata: { + name: "workspace-991-990-fedcba-gl-workspace-data", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ServiceAccount", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "NetworkPolicy", + metadata: { + name: "workspace-991-990-fedcba", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + }, + { + kind: "ConfigMap", + metadata: { + name: "workspace-991-990-fedcba-scripts-configmap", + annotations: { + "workspaces.gitlab.com/include-in-partial-reconciliation": "true" + } + } + } + ] + end it_behaves_like "returns expected config_to_apply" end end - - def create_config_to_apply_array - json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file("example.config_to_apply.json") - Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) - end - - def create_config_to_apply_array_with_partial_reconciliation_when_desired_state_is_terminated - json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file( - "example.config_to_apply_partial_reconciliation_desired_state_terminated.json") - Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) - end - - def create_config_to_apply_array_with_partial_reconciliation - json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file( - "example.config_to_apply_partial_reconciliation.json") - Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) - end end end -- GitLab From f6bd5d388e4bfe2881638974bb6c55ce49c89e9f Mon Sep 17 00:00:00 2001 From: Ashvin Sharma Date: Thu, 3 Jul 2025 11:56:20 +0530 Subject: [PATCH 3/4] Address review comments - Add missing or outdate YARD annotations - Replace literals with constants - DRY the newly added annotation for partial reconciliation - Remove dependency of DesiredConfigFetcher on ConfigToApplyBuilder --- .../workspace_agentk_state.rb | 2 + .../desired_config/config_values_extractor.rb | 4 +- .../devfile_resource_appender.rb | 12 +- .../workspace_operations/desired_config.rb | 2 +- .../output/config_to_apply_builder.rb | 23 ++- ... => config_to_apply_shadow_run_handler.rb} | 26 ++- .../output/desired_config_fetcher.rb | 6 +- .../output/response_payload_builder.rb | 4 +- .../output/config_to_apply_builder_spec.rb | 159 ++++-------------- ...onfig_to_apply_shadow_run_handler_spec.rb} | 10 +- .../output/response_payload_builder_spec.rb | 3 + 11 files changed, 94 insertions(+), 157 deletions(-) rename ee/lib/remote_development/workspace_operations/reconcile/output/{config_to_apply_fetcher.rb => config_to_apply_shadow_run_handler.rb} (71%) rename ee/spec/lib/remote_development/workspace_operations/reconcile/output/{config_to_apply_fetcher_spec.rb => config_to_apply_shadow_run_handler_spec.rb} (87%) diff --git a/ee/app/models/remote_development/workspace_agentk_state.rb b/ee/app/models/remote_development/workspace_agentk_state.rb index 02f03ff88f08eb..cfbc2cb9efb788 100644 --- a/ee/app/models/remote_development/workspace_agentk_state.rb +++ b/ee/app/models/remote_development/workspace_agentk_state.rb @@ -10,6 +10,8 @@ class WorkspaceAgentkState < ApplicationRecord 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) 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 eb64c90dd85584..1604adf129df4d 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 @@ -46,8 +46,8 @@ def self.extract(context) # 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" + workspace_inventory_name = "#{workspace_name}#{WORKSPACE_INVENTORY}" + secrets_inventory_name = "#{workspace_name}#{SECRETS_INVENTORY}" scripts_configmap_name = "#{workspace_name}-scripts-configmap" context.merge({ 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 42f2a93faab5a6..e22e4663b5a2d9 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 @@ -7,7 +7,11 @@ module DesiredConfig class DevfileResourceAppender include WorkspaceOperationsConstants + # @param [Hash] context + # @return [Hash] def self.append(context) + partial_reconcile_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } + context => { desired_config_array: desired_config_array, workspace_name: workspace_name, @@ -46,8 +50,7 @@ def self.append(context) namespace: workspace_namespace, image_pull_secrets: image_pull_secrets, labels: labels, - annotations: workspace_inventory_annotations - .merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }) + annotations: workspace_inventory_annotations.merge(partial_reconcile_annotation) ) append_network_policy( @@ -58,8 +61,7 @@ def self.append(context) network_policy_enabled: network_policy_enabled, network_policy_egress: network_policy_egress, labels: labels, - annotations: workspace_inventory_annotations - .merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }) + annotations: workspace_inventory_annotations.merge(partial_reconcile_annotation) ) append_scripts_resources( @@ -113,6 +115,8 @@ def self.append(context) # @param [String] namespace # @param [Hash] labels # @param [Hash] annotations + # @param [Boolean] prepend -- If true, prepend the config_map to the desired_config_array + # @param [Boolean] include_in_partial_reconciliation # @return [void] def self.append_inventory_config_map( desired_config_array:, diff --git a/ee/lib/remote_development/workspace_operations/desired_config.rb b/ee/lib/remote_development/workspace_operations/desired_config.rb index 2ab03909bee431..c3475d5c1cd429 100644 --- a/ee/lib/remote_development/workspace_operations/desired_config.rb +++ b/ee/lib/remote_development/workspace_operations/desired_config.rb @@ -41,7 +41,7 @@ def diff(other) Hashdiff.diff(desired_config_array, other.desired_config_array, use_lcs: false) end - # @param [Hash, nil] options + # @param [Hash, nil] _options # @return [Array, Hash] def as_json(_options = nil) desired_config_array 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 index db15fde214bf42..6ddad62beffb3a 100644 --- 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 @@ -15,24 +15,32 @@ class ConfigToApplyBuilder # @param [RemoteDevelopment::Workspace] workspace # @param [Boolean] include_all_resources - # @param [RemoteDevelopment::Logger] logger + # @param [Array] desired_config_array # @return [Array] - def self.build(workspace:, include_all_resources:, logger:) + def self.build(workspace:, include_all_resources:, desired_config_array:) env_secret_name = "#{workspace.name}#{ENV_VAR_SECRET_SUFFIX}" file_secret_name = "#{workspace.name}#{FILE_SECRET_SUFFIX}" + config_to_apply = desired_config_array - config_to_apply = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) - inject_secrets(config_to_apply, env_secret_name, file_secret_name, workspace) + inject_secrets( + config_to_apply: config_to_apply, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name, + workspace: workspace + ) return config_to_apply if include_all_resources - filter_partial_resources(config_to_apply, workspace.desired_state_terminated?) + filter_partial_resources( + config_to_apply: config_to_apply, + is_desired_state_terminated: workspace.desired_state_terminated? + ) end # @param [Array] config_to_apply # @param [Boolean] is_desired_state_terminated # @return [Array] - def self.filter_partial_resources(config_to_apply, is_desired_state_terminated) + def self.filter_partial_resources(config_to_apply:, is_desired_state_terminated:) if is_desired_state_terminated return config_to_apply.select do |object| if object.fetch(:kind) == "ConfigMap" @@ -53,7 +61,8 @@ def self.filter_partial_resources(config_to_apply, is_desired_state_terminated) # @param [String] env_secret_name # @param [String] file_secret_name # @param [RemoteDevelopment::Workspace] workspace - def self.inject_secrets(config_to_apply, env_secret_name, file_secret_name, workspace) + # @return [Void] + def self.inject_secrets(config_to_apply:, env_secret_name:, file_secret_name:, workspace:) append_secret_data_from_variables( desired_config: config_to_apply, secret_name: env_secret_name, diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb similarity index 71% rename from ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb rename to ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb index bc4c4bf17c8515..51ac01fd8f02bd 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler.rb @@ -9,20 +9,24 @@ module Output # 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 ConfigToApplyFetcher + class ConfigToApplyShadowRunHandler include WorkspaceOperationsConstants # @param [RemoteDevelopment::Workspace] workspace # @param [RemoteDevelopment::Logger] logger # @param [Boolean] include_all_resources # @return [Array] - def self.generate_and_compare_config_to_apply(workspace:, logger:, include_all_resources:) + def self.handle(workspace:, logger:, include_all_resources:) old_config_to_apply_array = OldDesiredConfigGenerator.generate_desired_config( workspace: workspace, include_all_resources: include_all_resources, logger: logger ) - new_config_to_apply_array = generate_new_config(include_all_resources, logger, workspace) + new_config_to_apply_array = generate_diffable_new_config_to_apply_array( + include_all_resources: include_all_resources, + logger: logger, + workspace: workspace + ) old_config_to_apply = DesiredConfig.new(desired_config_array: old_config_to_apply_array) new_config_to_apply = DesiredConfig.new(desired_config_array: new_config_to_apply_array) @@ -40,14 +44,20 @@ def self.generate_and_compare_config_to_apply(workspace:, logger:, include_all_r old_config_to_apply_array end - def self.generate_new_config(include_all_resources, logger, workspace) - config = ConfigToApplyBuilder.build( + # @param [Boolean] include_all_resources + # @param [Logger] logger + # @param [Workspace] workspace + # @return [Array] + def self.generate_diffable_new_config_to_apply_array(include_all_resources:, logger:, workspace:) + desired_config_array = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) + + config_to_apply = ConfigToApplyBuilder.build( workspace: workspace, include_all_resources: include_all_resources, - logger: logger + desired_config_array: desired_config_array ) - config.map do |resource| + config_to_apply.map do |resource| annotations = resource.dig(:metadata, :annotations) next resource unless annotations @@ -62,7 +72,7 @@ def self.generate_new_config(include_all_resources, logger, workspace) end end - private_class_method :generate_new_config + private_class_method :generate_diffable_new_config_to_apply_array 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 index 9b5b1880e66e14..f883c277a79ca6 100644 --- 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 @@ -10,9 +10,10 @@ class DesiredConfigFetcher # @return [Array] def self.fetch(workspace:, logger:) agentk_state = workspace.agentk_state + if agentk_state - array = agentk_state.desired_config - return array.map(&:deep_symbolize_keys) + desired_config_array = agentk_state.desired_config + return desired_config_array.map(&:deep_symbolize_keys) end # TODO: remove this after a succesful shadow run @@ -22,6 +23,7 @@ def self.fetch(workspace:, logger:) # @param workspace [RemoteDevelopment::Workspace] # @param logger [Logger] + # @return [Array] def self.fetch_from_create_desired_config_main(workspace:, logger:) result = Create::DesiredConfig::Main.main({ params: { 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 f5dd093ca3b35c..43b153f5d3ba88 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,13 @@ 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 = ConfigToApplyFetcher.generate_and_compare_config_to_apply( + config_to_apply_array = ConfigToApplyShadowRunHandler.handle( workspace: workspace, include_all_resources: include_all_resources, logger: logger ) - 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/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 index 3f6a41696fbc24..0916c27c00c7aa 100644 --- 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 @@ -41,17 +41,14 @@ 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 - - let(:logger) { instance_double("RemoteDevelopment::Logger") } # rubocop:enable RSpec/VerifiedDoubleReference let(:include_all_resources) { true } - let(:desired_config_array) do + let(:desired_config_array_with_partial_reconciliation_annotation) do [ { kind: "ConfigMap", @@ -63,71 +60,41 @@ } }, { - kind: "Deployment", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "Service", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "PersistentVolumeClaim", - metadata: { - name: "workspace-991-990-fedcba-gl-workspace-data", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "ServiceAccount", + kind: "object-with-annotation", metadata: { name: "workspace-991-990-fedcba", annotations: { "workspaces.gitlab.com/include-in-partial-reconciliation": "true" } } - }, - { - kind: "NetworkPolicy", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "ConfigMap", - metadata: { - name: "workspace-991-990-fedcba-scripts-configmap", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, + } + ] + end + + let(:secrets_inventory_config_map) do + [ { kind: "ConfigMap", metadata: { name: "workspace-991-990-fedcba-secrets-inventory" } - }, + } + ] + end + + let(:resources_without_partial_reconciliation_annotation) do + [ { - kind: "ResourceQuota", + kind: "object-without-annotation", metadata: { name: "workspace-991-990-fedcba" } - }, + } + ] + end + + let(:env_and_file_secrets) do + [ { kind: "Secret", metadata: { @@ -145,6 +112,13 @@ ] end + let(:desired_config_array) do + desired_config_array_with_partial_reconciliation_annotation + + resources_without_partial_reconciliation_annotation + + secrets_inventory_config_map + + env_and_file_secrets + end + let(:config_extractor_result) do { env_secret_name: "workspace-991-990-fedcba-env-var", @@ -154,21 +128,14 @@ describe "#build" do subject(:config_to_apply) do - # noinspection RubyMismatchedArgumentType -- logger is mocked but RubyMine does not like it + # noinspection RubyMismatchedArgumentType -- workspace is mocked but RubyMine does not like it described_class.build( workspace: workspace, include_all_resources: include_all_resources, - logger: logger + desired_config_array: desired_config_array ) end - before do - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) - .to(receive(:fetch)) - .with(workspace: workspace, logger: logger) - .and_return(desired_config_array) - end - context "when include_all_resources is true" do let(:desired_state_running) { true } let(:desired_state_terminated) { false } @@ -218,71 +185,7 @@ let(:desired_state_running) { true } let(:desired_state_terminated) { false } let(:expected_config_to_apply) do - [ - { - kind: "ConfigMap", - metadata: { - name: "workspace-991-990-fedcba-workspace-inventory", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "Deployment", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "Service", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "PersistentVolumeClaim", - metadata: { - name: "workspace-991-990-fedcba-gl-workspace-data", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "ServiceAccount", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "NetworkPolicy", - metadata: { - name: "workspace-991-990-fedcba", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "ConfigMap", - metadata: { - name: "workspace-991-990-fedcba-scripts-configmap", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - } - ] + desired_config_array_with_partial_reconciliation_annotation end it_behaves_like "returns expected config_to_apply" diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb similarity index 87% rename from ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb rename to ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb index 9e8ae0f25089cd..5457d74b66d64c 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_fetcher_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/config_to_apply_shadow_run_handler_spec.rb @@ -2,7 +2,7 @@ require "fast_spec_helper" -RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyFetcher, feature_category: :workspaces do +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyShadowRunHandler, feature_category: :workspaces do describe "#generate_and_compare_config_to_apply" 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 @@ -12,7 +12,7 @@ # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace and Logger subject(:result) do - described_class.generate_and_compare_config_to_apply( + described_class.handle( workspace: workspace, logger: logger, include_all_resources: true @@ -20,9 +20,13 @@ end before do + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) + .to(receive(:fetch)) + .with(workspace: workspace, logger: logger) + .and_return(new_desired_config_array) allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) .to(receive(:build)) - .with(workspace: workspace, include_all_resources: true, logger: logger) + .with(workspace: workspace, include_all_resources: true, desired_config_array: new_desired_config_array) .and_return(new_desired_config_array) allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator) .to(receive(:generate_desired_config)) 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 cc31d7724fe522..2600c3ca8d57c4 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 @@ -129,6 +129,9 @@ # NOTE: This is put here as a placeholder because we do not really care about the output of this function # This temporarily allows to let shadow run of Create::DesiredConfig::Main pass. # The actual test for the shadow run is written in config_to_apply_fetcher_spec.rb. + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) + .to(receive(:fetch)) + .and_return(generated_config_to_apply) allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) .to(receive(:build)) .and_return(generated_config_to_apply) -- GitLab From 9df120e7265c185eee4620fed52a76e745061687 Mon Sep 17 00:00:00 2001 From: Chad Woolley Date: Sat, 5 Jul 2025 20:36:25 -0700 Subject: [PATCH 4/4] Apply Chad's patch --- ee/app/models/remote_development/workspace.rb | 4 +- .../workspace_agentk_state.rb | 2 +- .../desired_config/config_values_extractor.rb | 37 ++- .../desired_config/devfile_parser_getter.rb | 6 +- .../devfile_resource_appender.rb | 69 +++-- .../devfile_resource_modifier.rb | 197 +++++++------- .../create/workspace_agentk_state_creator.rb | 2 +- .../workspace_operations/desired_config.rb | 7 +- .../output/config_to_apply_builder.rb | 99 ++++--- .../config_to_apply_shadow_run_handler.rb | 32 +-- .../output/desired_config_fetcher.rb | 51 ++-- .../output/response_payload_builder.rb | 17 +- .../workspace_agentk_states.rb | 21 +- .../remote_development/workspaces.rb | 26 ++ .../example.desired_config.json | 24 +- .../config_values_extractor_spec.rb | 23 ++ .../devfile_parser_getter_spec.rb | 5 +- .../devfile_resource_appender_spec.rb | 15 ++ .../devfile_resource_modifier_spec.rb | 4 - .../create/main_integration_spec.rb | 5 +- .../workspace_agentk_state_creator_spec.rb | 10 +- .../desired_config_spec.rb | 16 +- .../reconcile/main_integration_spec.rb | 2 +- .../main_reconcile_scenarios_spec.rb | 4 +- .../output/config_to_apply_builder_spec.rb | 224 +++++++++------- ...config_to_apply_shadow_run_handler_spec.rb | 95 +++---- .../output/desired_config_fetcher_spec.rb | 104 ++++---- .../old_desired_config_generator_spec.rb | 1 + .../output/response_payload_builder_spec.rb | 245 ++++++++++-------- .../workspace_agentk_state_spec.rb | 70 +++-- .../remote_development/workspace_spec.rb | 36 +-- 31 files changed, 839 insertions(+), 614 deletions(-) diff --git a/ee/app/models/remote_development/workspace.rb b/ee/app/models/remote_development/workspace.rb index 692fc04ec0710e..53ca8b034a54e6 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 cfbc2cb9efb788..e6c8469513f11e 100644 --- a/ee/app/models/remote_development/workspace_agentk_state.rb +++ b/ee/app/models/remote_development/workspace_agentk_state.rb @@ -2,7 +2,7 @@ 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 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 1604adf129df4d..e31a93107f30e3 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 @@ -32,30 +32,43 @@ 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. @@ -71,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 63a8460d623ab8..d93d1f464099f9 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 @@ -13,14 +13,14 @@ 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, workspace_namespace: workspace_namespace, replicas: replicas } - extra_annotations = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } begin context.merge( @@ -29,7 +29,7 @@ def self.get(context) workspace_name, workspace_namespace, YAML.dump(labels.deep_stringify_keys), - YAML.dump(annotations.merge(extra_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 e22e4663b5a2d9..6e4aa1e09b2008 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 @@ -10,38 +10,38 @@ class DevfileResourceAppender # @param [Hash] context # @return [Hash] def self.append(context) - partial_reconcile_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } - 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, - prepend: true, - include_in_partial_reconciliation: true + annotations: common_annotations_for_partial_reconciliation, + prepend: true ) append_image_pull_secrets_service_account( @@ -50,7 +50,7 @@ def self.append(context) namespace: workspace_namespace, image_pull_secrets: image_pull_secrets, labels: labels, - annotations: workspace_inventory_annotations.merge(partial_reconcile_annotation) + annotations: workspace_inventory_annotations_for_partial_reconciliation ) append_network_policy( @@ -61,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.merge(partial_reconcile_annotation) + annotations: workspace_inventory_annotations_for_partial_reconciliation ) append_scripts_resources( @@ -70,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, @@ -115,24 +115,19 @@ def self.append(context) # @param [String] namespace # @param [Hash] labels # @param [Hash] annotations - # @param [Boolean] prepend -- If true, prepend the config_map to the desired_config_array - # @param [Boolean] include_in_partial_reconciliation + # @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:, labels:, annotations:, - prepend: false, - include_in_partial_reconciliation: false + prepend: false ) extra_labels = { "cli-utils.sigs.k8s.io/inventory-id": name } - extra_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } - - annotations = annotations.merge(extra_annotation) if include_in_partial_reconciliation - config_map = { + configmap = { kind: "ConfigMap", apiVersion: "v1", metadata: { @@ -144,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 @@ -309,7 +304,7 @@ def self.append_scripts_resources( name: name, namespace: namespace, labels: labels, - annotations: annotations.merge({ ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" }), + annotations: annotations, devfile_commands: devfile_commands, devfile_events: devfile_events ) @@ -419,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 e11b491fd4714b..fbcf6bab305409 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 fcba5eafe9df23..a722532764f3bf 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 c3475d5c1cd429..db2ae408988409 100644 --- a/ee/lib/remote_development/workspace_operations/desired_config.rb +++ b/ee/lib/remote_development/workspace_operations/desired_config.rb @@ -41,10 +41,9 @@ def diff(other) Hashdiff.diff(desired_config_array, other.desired_config_array, use_lcs: false) end - # @param [Hash, nil] _options - # @return [Array, Hash] - def as_json(_options = nil) - desired_config_array + # @return [Array] + def symbolized_desired_config_array + as_json.fetch("desired_config_array").map(&:deep_symbolize_keys) 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 index 6ddad62beffb3a..8c918a1c85bcfe 100644 --- 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 @@ -15,12 +15,16 @@ class ConfigToApplyBuilder # @param [RemoteDevelopment::Workspace] workspace # @param [Boolean] include_all_resources - # @param [Array] desired_config_array + # @param [RemoteDevelopment::WorkspaceOperations::DesiredConfig] desired_config # @return [Array] - def self.build(workspace:, include_all_resources:, desired_config_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}" - config_to_apply = desired_config_array + 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, @@ -29,30 +33,26 @@ def self.build(workspace:, include_all_resources:, desired_config_array:) workspace: workspace ) - return config_to_apply if include_all_resources - - filter_partial_resources( + update_spec_replicas( config_to_apply: config_to_apply, - is_desired_state_terminated: workspace.desired_state_terminated? + workspace: workspace ) + + filter_partial_resources(config_to_apply: config_to_apply) unless include_all_resources + + config_to_apply end # @param [Array] config_to_apply - # @param [Boolean] is_desired_state_terminated # @return [Array] - def self.filter_partial_resources(config_to_apply:, is_desired_state_terminated:) - if is_desired_state_terminated - return config_to_apply.select do |object| - if object.fetch(:kind) == "ConfigMap" - name = object.dig(:metadata, :name) - ALLOWLIST_WHEN_STATE_TERMINATED.any? { |regex| name =~ regex } - end + 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.select do |object| - object.dig(:metadata, :annotations, ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION).present? - end + config_to_apply end # Mutates `config_to_apply` and inject secret data in the `env_secret_name` @@ -61,38 +61,42 @@ def self.filter_partial_resources(config_to_apply:, is_desired_state_terminated: # @param [String] env_secret_name # @param [String] file_secret_name # @param [RemoteDevelopment::Workspace] workspace - # @return [Void] + # @return [Array] def self.inject_secrets(config_to_apply:, env_secret_name:, file_secret_name:, workspace:) append_secret_data_from_variables( - desired_config: config_to_apply, + config_to_apply: config_to_apply, secret_name: env_secret_name, variables: workspace.workspace_variables.with_variable_type_environment ) append_secret_data_from_variables( - desired_config: config_to_apply, + config_to_apply: config_to_apply, secret_name: file_secret_name, variables: workspace.workspace_variables.with_variable_type_file ) append_secret_data( - desired_config: config_to_apply, + 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 - # @param [Array] desired_config + # 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(desired_config:, secret_name:, variables:) + 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( - desired_config: desired_config, + config_to_apply: config_to_apply, secret_name: secret_name, data: data ) @@ -100,13 +104,13 @@ def self.append_secret_data_from_variables(desired_config:, secret_name:, variab nil end - # @param [Array] desired_config + # @param [Array] config_to_apply # @param [String] secret_name # @param [Hash] data - # @return [void] + # @return [Array] # noinspection RubyUnusedLocalVariable -- Rubymine doesn't recognize '^' to use a variable in pattern-matching - def self.append_secret_data(desired_config:, secret_name:, data:) - desired_config => [ + def self.append_secret_data(config_to_apply:, secret_name:, data:) + config_to_apply => [ *_, { metadata: { @@ -121,12 +125,43 @@ def self.append_secret_data(desired_config:, secret_name:, data:) secret_data.merge!(transformed_data) - nil + 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 :inject_secrets, + private_class_method :select_only_configmaps, + :inject_secrets, :append_secret_data_from_variables, - :append_secret_data + :append_secret_data, + :update_spec_replicas, + :filter_partial_resources 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 index 51ac01fd8f02bd..cebe5800fb7b80 100644 --- 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 @@ -13,29 +13,28 @@ 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:, logger:, include_all_resources:) + 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 ) - new_config_to_apply_array = generate_diffable_new_config_to_apply_array( - include_all_resources: include_all_resources, - logger: logger, - workspace: workspace + 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: new_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, using old version.", - error_type: "reconcile_desired_configs_differ", + "OldDesiredConfigGenerator differ.", + error_type: "workspaces_reconcile_desired_configs_differ", workspace_id: workspace.id, diff: diff ) @@ -44,20 +43,13 @@ def self.handle(workspace:, logger:, include_all_resources:) old_config_to_apply_array end - # @param [Boolean] include_all_resources - # @param [Logger] logger - # @param [Workspace] workspace + # @param [Array] new_config_to_apply_array # @return [Array] - def self.generate_diffable_new_config_to_apply_array(include_all_resources:, logger:, workspace:) - desired_config_array = DesiredConfigFetcher.fetch(workspace: workspace, logger: logger) - - config_to_apply = ConfigToApplyBuilder.build( - workspace: workspace, - include_all_resources: include_all_resources, - desired_config_array: desired_config_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 - config_to_apply.map do |resource| annotations = resource.dig(:metadata, :annotations) next resource unless annotations 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 index f883c277a79ca6..bf9b19081f2b79 100644 --- 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 @@ -6,41 +6,48 @@ module Reconcile module Output class DesiredConfigFetcher # @param workspace [RemoteDevelopment::Workspace] - # @param logger [Logger] - # @return [Array] + # @param logger [RemoteDevelopment::Logger] + # @return [RemoteDevelopment::WorkspaceOperations::DesiredConfig] def self.fetch(workspace:, logger:) - agentk_state = workspace.agentk_state + workspace_agentk_state = workspace.workspace_agentk_state - if agentk_state - desired_config_array = agentk_state.desired_config - return desired_config_array.map(&:deep_symbolize_keys) + 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 after a succesful shadow run - # Issue- https://gitlab.com/gitlab-org/gitlab/-/issues/551935 - fetch_from_create_desired_config_main(workspace: workspace, logger: logger) + # 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 workspace [RemoteDevelopment::Workspace] - # @param logger [Logger] - # @return [Array] - def self.fetch_from_create_desired_config_main(workspace:, logger:) - result = Create::DesiredConfig::Main.main({ - params: { - agent: workspace.agent - }, - workspace: workspace, - logger: logger - }) + # @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.desired_config_array + desired_config end - private_class_method :fetch_from_create_desired_config_main + private_class_method :generate_new_desired_config 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 43b153f5d3ba88..c7c1182066d6cf 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,11 +84,26 @@ 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 - config_to_apply_array = ConfigToApplyShadowRunHandler.handle( + 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 = config_to_apply_array.map do |resource| Gitlab::Utils.deep_sort_hash(resource) diff --git a/ee/spec/factories/remote_development/workspace_agentk_states.rb b/ee/spec/factories/remote_development/workspace_agentk_states.rb index 5f81c1b5fba6ca..8e362192d98485 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 3b4fc574f872c0..cba08ed66e2115 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 05273df55d3d8c..f6f91165c69a01 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 36ddfbb25845f0..58b6ac5ac69f28 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 dbea4dad9f0084..3d6215bd243979 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, @@ -37,7 +37,6 @@ annotations: k1: v1 k2: v2 - workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value @@ -57,7 +56,6 @@ annotations: k1: v1 k2: v2 - workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value @@ -94,7 +92,6 @@ annotations: k1: v1 k2: v2 - workspaces.gitlab.com/include-in-partial-reconciliation: "true" creationTimestamp: null labels: other-label: other-value 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 a46776e604cfe5..2edd7793103217 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 @@ -38,10 +38,22 @@ 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" } @@ -93,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, 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 220aefe9f10c17..f2eb4bdc87b603 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/main_integration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/main_integration_spec.rb index 94551edb987fbb..76054104727244 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).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 9c82b81dc33b1a..9c7ca124e938f7 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.map(&:deep_stringify_keys) } + 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 411f34ee6982df..5e1f90997a7f1a 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,19 +61,15 @@ end end - describe "#as_json" do + describe "#symbolized_desired_config_array" do let(:desired_config_array) { create_desired_config_array } - context "when called without options" do - subject(:result) { desired_config.as_json } - - it { expect(result).to eq(desired_config_array) } - it { expect(result).to be_kind_of(Array) } - end + 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) } @@ -89,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 @@ -136,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 7b53fc37c87092..30b17f17339b2c 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 96246f4bff07d7..94de5928c336f6 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 index 0916c27c00c7aa..37a171ed7b344f 100644 --- 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 @@ -33,7 +33,11 @@ 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 @@ -41,14 +45,84 @@ 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(:include_all_resources) { true } - let(:desired_config_array_with_partial_reconciliation_annotation) do + 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", @@ -58,20 +132,40 @@ "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 - let(:secrets_inventory_config_map) do + # @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", @@ -82,7 +176,8 @@ ] end - let(:resources_without_partial_reconciliation_annotation) do + # @return [Array] + def resources_without_partial_reconciliation_annotation [ { kind: "object-without-annotation", @@ -93,7 +188,8 @@ ] end - let(:env_and_file_secrets) do + # @return [Array] + def env_and_file_secrets [ { kind: "Secret", @@ -112,84 +208,26 @@ ] end - let(:desired_config_array) do - desired_config_array_with_partial_reconciliation_annotation + - resources_without_partial_reconciliation_annotation + - secrets_inventory_config_map + - env_and_file_secrets - end - - let(:config_extractor_result) do - { - env_secret_name: "workspace-991-990-fedcba-env-var", - file_secret_name: "workspace-991-990-fedcba-file" - } - end - - describe "#build" do - 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_array: desired_config_array - ) - end - - context "when include_all_resources is true" do - let(:desired_state_running) { true } - let(:desired_state_terminated) { false } - let(:expected_config_to_apply) { desired_config_array } - - it "returns all the resources including secret data" do - expect(config_to_apply).to eq(expected_config_to_apply) - end - end - - describe "when include_all_resources is false" do - let(:include_all_resources) { false } - - 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 - - context "when workspace.desired_state is terminated" do - let(:desired_state_running) { false } - let(:desired_state_terminated) { true } - let(:expected_config_to_apply) do - [ - { - kind: "ConfigMap", - metadata: { - name: "workspace-991-990-fedcba-workspace-inventory", - annotations: { - "workspaces.gitlab.com/include-in-partial-reconciliation": "true" - } - } - }, - { - kind: "ConfigMap", - metadata: { - name: "workspace-991-990-fedcba-secrets-inventory" - } - } - ] - end - - it_behaves_like "returns expected config_to_apply" - end - - context "when workspace.desired_state is not terminated" do - let(:desired_state_running) { true } - let(:desired_state_terminated) { false } - let(:expected_config_to_apply) do - desired_config_array_with_partial_reconciliation_annotation - end - - it_behaves_like "returns expected config_to_apply" - end - 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 index 5457d74b66d64c..956c30ca4d7a6e 100644 --- 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 @@ -3,65 +3,56 @@ require "fast_spec_helper" RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyShadowRunHandler, feature_category: :workspaces do - describe "#generate_and_compare_config_to_apply" 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 - let(:expected_include_all_resources) { true } - let(:old_desired_config_array) { [{ kind: "Deployment" }] } - let(:new_desired_config_array) { [{ kind: "Deployment" }] } + 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 - # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace and Logger - subject(:result) do - described_class.handle( - workspace: workspace, - 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 - before do - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) - .to(receive(:fetch)) - .with(workspace: workspace, logger: logger) - .and_return(new_desired_config_array) - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) - .to(receive(:build)) - .with(workspace: workspace, include_all_resources: true, desired_config_array: new_desired_config_array) - .and_return(new_desired_config_array) - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator) - .to(receive(:generate_desired_config)) - .with(workspace: workspace, include_all_resources: true, logger: logger) - .and_return(old_desired_config_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" }] } - context "when config_to_apply are same" do - it "returns the value from OldDesiredConfigGenerator" do - expect(logger).not_to receive(:warn) - expect(result).to eq(old_desired_config_array) - end + 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_desired_config_array) { [{ kind: "Deployment" }] } - let(:new_desired_config_array) { [{ kind: "Service" }] } + 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 + 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: "reconcile_desired_configs_differ", - message: "The generated config_to_apply from Create::DesiredConfig::Main and OldDesiredConfigGenerator " \ - "differ, using old version.", - workspace_id: 1 - }) - expect(result).to eq(old_desired_config_array) - 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 index 127fed5b83f0a3..f1ea9f573c1a50 100644 --- 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 @@ -1,77 +1,85 @@ # frozen_string_literal: true -require "fast_spec_helper" +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 - # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version of these models, so we can use fast_spec_helper. - let(:logger) { instance_double("Logger") } - let(:agent) { instance_double("Clusters::Agent", id: 1) } - let(:workspace) { instance_double("RemoteDevelopment::Workspace", agent: agent) } - # rubocop:enable RSpec/VerifiedDoubleReference + let(:logger) { instance_double(RemoteDevelopment::Logger) } - let(:context) do - { - params: { - agent: agent - }, - workspace: workspace, - logger: 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 - let(:desired_config_array) { [{ kind: "Development" }] } - let(:expected_desired_config) do - RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: desired_config_array) + # 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 - # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace - subject(:result) { described_class.fetch(workspace: workspace, logger: logger) } + # 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 } - context "when agentk_state exists" do - let(:agentk_state) { instance_double("RemoteDevelopment::WorkspaceAgentkState") } # rubocop:disable RSpec/VerifiedDoubleReference -- we want to use fast_spec_helper which does not load this class + 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 - allow(workspace).to receive(:agentk_state).and_return(agentk_state) - allow(agentk_state).to receive(:desired_config).and_return(desired_config_array) + workspace.workspace_agentk_state.update_attribute(:desired_config, "invalid json") end - it "returns desired_config_array from database" do - expect(result).to eq(desired_config_array) + it "raises an error" do + expect { desired_config }.to raise_error(ActiveModel::ValidationError) end + end - it "does not call Create::DesiredConfig::Main" do - expect(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main).not_to receive(:main) - result - 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 - context "when agentk_state does not exist" do + # 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(:agentk_state).and_return(nil) - allow(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main) - .to(receive(:main)) - .with(context) - .and_return({ desired_config: expected_desired_config }) + allow(workspace).to receive(:workspace_agentk_state).and_return(nil) end - it "calls database" do - expect(workspace).to receive(:agentk_state) - result + 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 - # TODO: The class under test will not call DesiredConfig::Main by the end of this issue - # https://gitlab.com/gitlab-org/gitlab/-/issues/541907. This is done only to do the shadow run. - # noinspection RubyArgCount -- Rubymine thinks `with` is only supposed to be called as key-value pair expect(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main) - .to(receive(:main)) - .with(context) - .and_return({ - desired_config: expected_desired_config - }) - - expect(result).to eq(desired_config_array) + .to receive(:main).with(expected_context) { result } + expect(desired_config).to eq(expected_desired_config) 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 2e9a204e93abbb..369898ac72400e 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 2600c3ca8d57c4..0709bfa16de361 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,117 +128,145 @@ desired_state_terminated_and_actual_state_not_terminated?: desired_state_terminated_and_actual_state_not_terminated ) - end - # TODO Rename this context when the versioning of config generator is removed - context "when workspace.desired_config_generator_version is current version" do - let(:desired_config_generator_version) { current_desired_config_generator_version } - - before do - # NOTE: This is put here as a placeholder because we do not really care about the output of this function - # This temporarily allows to let shadow run of Create::DesiredConfig::Main pass. - # The actual test for the shadow run is written in config_to_apply_fetcher_spec.rb. - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) - .to(receive(:fetch)) - .and_return(generated_config_to_apply) - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::ConfigToApplyBuilder) - .to(receive(:build)) - .and_return(generated_config_to_apply) - 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 } - end + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigFetcher) + .to receive(:fetch) + .and_return(desired_config) - 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 } + 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) - it "includes config_to_apply with all resources included" do - expect(returned_value).to eq(expected_returned_value) + 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 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 + # 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 + + 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 } + + 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 0080354dab3249..a786de93a2f273 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 @@ -34,24 +48,30 @@ it { is_expected.to validate_presence_of(:desired_config) } describe "desired_config validations" do - context "when desired_config is not an array" do - shared_examples "invalid desired_config" do |value_description, test_value, expected_error| - context "when desired_config is #{value_description}" do - let(:workspace_agentk_state) do - build(:workspace_agentk_state, workspace: workspace, project: project, desired_config: test_value) - 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 + 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 - include_examples "invalid desired_config", "a hash", { key: "value" }, "must be an array" - include_examples "invalid desired_config", "a string", "string", "must be an array" - include_examples "invalid desired_config", "a number", 1, "must be an array" - include_examples "invalid desired_config", "an empty array", [], "can't be blank" + with_them do + it_behaves_like "invalid desired_config" end end end diff --git a/ee/spec/models/remote_development/workspace_spec.rb b/ee/spec/models/remote_development/workspace_spec.rb index 303baae104c7c7..fb198e9cd35dec 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 -- GitLab