From 2aa4f9bc1ca5be26cc2394e5784b53325217fa9c Mon Sep 17 00:00:00 2001 From: Ashvin Sharma Date: Fri, 13 Jun 2025 15:20:19 +0530 Subject: [PATCH] Introduce create desired_config chain in the create workspace step This commit will introduce an ROP chain to create desired_config in the create step. It is similar to the logic in reconcile step except for following changes devfile_parser.rb is split to three different files adhering to SRP. These files are devfile_parser_getter.rb - Call the devfile-gem gets the Kubernetes object against a devfile. devfile_resource_modifier.rb - Modifies the objects from the previous step. devfile_resource_appender.rb - Appends more Kubernetes resources to the desired_config like NetworkPolicy, ConfigMaps, Secrets etc. --- ee/lib/remote_development/messages.rb | 1 + .../workspace_operations/create/creator.rb | 9 +- .../desired_config/config_values_extractor.rb | 43 +- .../desired_config_yaml_parser.rb | 25 + .../desired_config/devfile_parser_getter.rb | 67 + .../devfile_resource_appender.rb | 419 ++ .../devfile_resource_modifier.rb | 215 + .../create/desired_config/main.rb | 57 + .../scripts_configmap_appender.rb | 14 +- .../create/workspace_agentk_state_creator.rb | 48 + .../workspace_operations/desired_config.rb | 1 + .../output/old_config_values_extractor.rb | 111 + ...tor.rb => old_desired_config_generator.rb} | 8 +- .../output/old_devfile_parser.rb} | 8 +- .../output/old_scripts_configmap_appender.rb | 147 + .../output/response_payload_builder.rb | 2 +- .../create/creator_spec.rb | 22 +- .../config_values_extractor_spec.rb | 168 +- .../desired_config_yaml_parser_spec.rb | 33 + .../devfile_parser_getter_spec.rb | 164 + .../devfile_resource_appender_spec.rb | 169 + .../devfile_resource_modifier_spec.rb | 186 + .../desired_config/main_integeration_spec.rb | 4362 +++++++++++++++++ .../create/desired_config/main_spec.rb | 78 + .../scripts_configmap_appender_spec.rb | 10 +- .../create/main_integration_spec.rb | 7 +- .../workspace_agentk_state_creator_spec.rb | 82 + .../desired_config_spec.rb | 5 +- .../old_config_values_extractor_spec.rb | 269 + ...ed_config_generator_golden_master_spec.rb} | 4 +- ...b => old_desired_config_generator_spec.rb} | 4 +- .../output/old_devfile_parser_spec.rb} | 2 +- .../old_scripts_configmap_appender_spec.rb | 101 + .../output/response_payload_builder_spec.rb | 2 +- .../remote_development_shared_contexts.rb | 8 +- 35 files changed, 6691 insertions(+), 160 deletions(-) create mode 100644 ee/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/desired_config/main.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb create mode 100644 ee/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor.rb rename ee/lib/remote_development/workspace_operations/reconcile/output/{desired_config_generator.rb => old_desired_config_generator.rb} (98%) rename ee/lib/remote_development/workspace_operations/{create/desired_config/devfile_parser.rb => reconcile/output/old_devfile_parser.rb} (99%) create mode 100644 ee/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb create mode 100644 ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor_spec.rb rename ee/spec/lib/remote_development/workspace_operations/reconcile/output/{desired_config_generator_golden_master_spec.rb => old_desired_config_generator_golden_master_spec.rb} (99%) rename ee/spec/lib/remote_development/workspace_operations/reconcile/output/{desired_config_generator_spec.rb => old_desired_config_generator_spec.rb} (98%) rename ee/spec/lib/remote_development/workspace_operations/{create/desired_config/devfile_parser_spec.rb => reconcile/output/old_devfile_parser_spec.rb} (98%) create mode 100644 ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender_spec.rb diff --git a/ee/lib/remote_development/messages.rb b/ee/lib/remote_development/messages.rb index 783dbba05c8435..e5040511365156 100644 --- a/ee/lib/remote_development/messages.rb +++ b/ee/lib/remote_development/messages.rb @@ -19,6 +19,7 @@ module Messages WorkspaceModelCreateFailed = Class.new(Gitlab::Fp::Message) WorkspaceVariablesModelCreateFailed = Class.new(Gitlab::Fp::Message) WorkspaceCreateFailed = Class.new(Gitlab::Fp::Message) + WorkspaceAgentkStateCreateFailed = Class.new(Gitlab::Fp::Message) # Workspace update errors WorkspaceUpdateFailed = Class.new(Gitlab::Fp::Message) diff --git a/ee/lib/remote_development/workspace_operations/create/creator.rb b/ee/lib/remote_development/workspace_operations/create/creator.rb index 8e30c2d43c10a5..862503e065e9eb 100644 --- a/ee/lib/remote_development/workspace_operations/create/creator.rb +++ b/ee/lib/remote_development/workspace_operations/create/creator.rb @@ -23,11 +23,18 @@ def self.create(context) .and_then(PersonalAccessTokenCreator.method(:create)) .and_then(WorkspaceCreator.method(:create)) .and_then(WorkspaceVariablesCreator.method(:create)) + # NOTE: Even though DesiredConfig::Main is a nested ROP chain, it is namespaced as a peer to + # Creator namespace, to avoid excessive filesystem/namespace nesting. + # We need to call it here after the Workspace record is created, because the desired_config field + # JSON has some attributes which contain the Workspace record ID. + .map(DesiredConfig::Main.method(:main)) + .and_then(WorkspaceAgentkStateCreator.method(:create)) case result in { err: PersonalAccessTokenModelCreateFailed | WorkspaceModelCreateFailed | - WorkspaceVariablesModelCreateFailed => message + WorkspaceVariablesModelCreateFailed | + WorkspaceAgentkStateCreateFailed => message } model_errors = message.content[:errors] raise ActiveRecord::Rollback 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 b08534fe7b06f2..157c7dd0350d37 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 @@ -7,27 +7,22 @@ module DesiredConfig class ConfigValuesExtractor include States - # @param [RemoteDevelopment::Workspace] workspace + # @param [Hash] context # @return [Hash] - def self.extract(workspace:) - workspace_name = workspace.name - workspaces_agent_config = workspace.workspaces_agent_config + def self.extract(context) + context => { + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_desired_state_is_running: workspace_desired_state_is_running, + workspaces_agent_id: workspaces_agent_id, + workspaces_agent_config: workspaces_agent_config + } domain_template = "{{.port}}-#{workspace_name}.#{workspaces_agent_config.dns_zone}" max_resources_per_workspace = deep_sort_and_symbolize_hashes(workspaces_agent_config.max_resources_per_workspace) - - # NOTE: In order to prevent unwanted restarts of the workspace, we need to ensure that the hexdigest - # of the max_resources_per_workspace is backward compatible, and uses the same sorting as the - # legacy logic of existing running workspaces. This means that only the top level keys are sorted, - # not the nested hashes. But everywhere else we will use the deeply sorted version. This workaround - # can be removed if we move all of this logic from workspace reconcile-time to create-time. - # Also note that the value has always been deep_symbolized before #to_s, so we preserve that as well. - max_resources_per_workspace_sha256_with_legacy_sorting = - OpenSSL::Digest::SHA256.hexdigest( - workspaces_agent_config.max_resources_per_workspace.deep_symbolize_keys.sort.to_h.to_s - ) + max_resources_per_workspace_sha256 = OpenSSL::Digest::SHA256.hexdigest(max_resources_per_workspace.to_s) default_resources_per_workspace_container = deep_sort_and_symbolize_hashes(workspaces_agent_config.default_resources_per_workspace_container) @@ -38,24 +33,23 @@ def self.extract(workspace:) extra_annotations = { "workspaces.gitlab.com/host-template": domain_template.to_s, - "workspaces.gitlab.com/id": workspace.id.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_with_legacy_sorting + "workspaces.gitlab.com/max-resources-per-workspace-sha256": max_resources_per_workspace_sha256 } agent_annotations = workspaces_agent_config.annotations common_annotations = agent_annotations.merge(extra_annotations) agent_labels = workspaces_agent_config.labels - labels = agent_labels.merge({ "agent.gitlab.com/id": workspace.agent.id.to_s }) + 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? + 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), @@ -72,10 +66,9 @@ def self.extract(workspace:) image_pull_secrets: deep_sort_and_symbolize_hashes(workspaces_agent_config.image_pull_secrets), labels: deep_sort_and_symbolize_hashes(labels), max_resources_per_workspace: max_resources_per_workspace, - network_policy_enabled: workspaces_agent_config.network_policy_enabled, network_policy_egress: deep_sort_and_symbolize_hashes(workspaces_agent_config.network_policy_egress), - processed_devfile_yaml: workspace.processed_devfile, - replicas: workspace.desired_state_running? ? 1 : 0, + 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( @@ -89,7 +82,7 @@ def self.extract(workspace:) common_annotations.merge("config.k8s.io/owning-inventory": workspace_inventory_name) ), workspace_inventory_name: workspace_inventory_name - } + }).sort.to_h end # @param [Array, Hash] collection diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser.rb new file mode 100644 index 00000000000000..e2996d279aea94 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class DesiredConfigYamlParser + # @param [Hash] context + # @return [Hash] + def self.parse(context) + context => { + desired_config_yaml: desired_config_yaml + } + + desired_config_array = YAML.load_stream(desired_config_yaml).map(&:deep_symbolize_keys) + + context.merge({ + desired_config_array: desired_config_array + }) + end + end + end + end + end +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 new file mode 100644 index 00000000000000..893076083c2473 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class DevfileParserGetter + # @param [Hash] context + # @return [Hash] + def self.get(context) + context => { + logger: logger, + processed_devfile_yaml: processed_devfile_yaml, + workspace_inventory_annotations: annotations, + domain_template: domain_template, + labels: labels, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + replicas: replicas + } + + begin + context.merge( + desired_config_yaml: Devfile::Parser.get_all( + processed_devfile_yaml, + workspace_name, + workspace_namespace, + YAML.dump(labels.deep_stringify_keys), + YAML.dump(annotations.deep_stringify_keys), + replicas, + domain_template, + 'none' + ) + ) + rescue Devfile::CliError => e + error_message = <<~MSG.squish + #{e.class}: A non zero return code was observed when invoking the devfile CLI + executable from the devfile gem. + MSG + logger.warn( + message: error_message, + error_type: 'create_devfile_parser_error', + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + devfile_parser_error: e.message + ) + raise e + rescue StandardError => e + error_message = <<~MSG.squish + #{e.class}: An unrecoverable error occurred when invoking the devfile gem, + this may hint that a gem with a wrong architecture is being used. + MSG + logger.warn( + message: error_message, + error_type: 'create_devfile_parser_error', + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + devfile_parser_error: e.message + ) + raise e + end + end + end + end + end + end +end 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 new file mode 100644 index 00000000000000..2d6256de6af622 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender.rb @@ -0,0 +1,419 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class DevfileResourceAppender + def self.append(context) + context => { + desired_config_array: desired_config_array, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + labels: labels, + workspace_inventory_annotations: workspace_inventory_annotations, + common_annotations: common_annotations, + workspace_inventory_name: workspace_inventory_name, + secrets_inventory_name: secrets_inventory_name, + secrets_inventory_annotations: secrets_inventory_annotations, + scripts_configmap_name: scripts_configmap_name, + processed_devfile_yaml: processed_devfile_yaml, + 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, + max_resources_per_workspace: max_resources_per_workspace, + shared_namespace: shared_namespace, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name + } + + append_inventory_config_map( + desired_config_array: desired_config_array, + name: workspace_inventory_name, + namespace: workspace_namespace, + labels: labels, + annotations: common_annotations, + prepend: true + ) + + append_image_pull_secrets_service_account( + desired_config_array: desired_config_array, + name: workspace_name, + namespace: workspace_namespace, + image_pull_secrets: image_pull_secrets, + labels: labels, + annotations: workspace_inventory_annotations + ) + + append_network_policy( + desired_config_array: desired_config_array, + name: workspace_name, + namespace: workspace_namespace, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace, + network_policy_enabled: network_policy_enabled, + network_policy_egress: network_policy_egress, + labels: labels, + annotations: workspace_inventory_annotations + ) + + append_scripts_resources( + desired_config_array: desired_config_array, + processed_devfile_yaml: processed_devfile_yaml, + name: scripts_configmap_name, + namespace: workspace_namespace, + labels: labels, + annotations: workspace_inventory_annotations + ) + + append_inventory_config_map( + desired_config_array: desired_config_array, + name: secrets_inventory_name, + namespace: workspace_namespace, + labels: labels, + annotations: common_annotations + ) + + append_resource_quota( + desired_config_array: desired_config_array, + name: workspace_name, + namespace: workspace_namespace, + labels: labels, + annotations: workspace_inventory_annotations, + max_resources_per_workspace: max_resources_per_workspace, + shared_namespace: shared_namespace + ) + + append_secret( + desired_config_array: desired_config_array, + name: env_secret_name, + namespace: workspace_namespace, + labels: labels, + annotations: secrets_inventory_annotations + ) + + append_secret( + desired_config_array: desired_config_array, + name: file_secret_name, + namespace: workspace_namespace, + labels: labels, + annotations: secrets_inventory_annotations + ) + + context.merge({ desired_config_array: desired_config_array }) + end + + # @param [Array] desired_config_array + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @return [void] + def self.append_inventory_config_map( + desired_config_array:, + name:, + namespace:, + labels:, + annotations:, + prepend: false + ) + extra_labels = { "cli-utils.sigs.k8s.io/inventory-id": name } + + config_map = { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: name, + namespace: namespace, + labels: labels.merge(extra_labels), + annotations: annotations + } + } + + if prepend + desired_config_array.prepend(config_map) + else + desired_config_array.append(config_map) + end + + nil + end + + # @param [Array] desired_config_array + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @return [void] + def self.append_secret(desired_config_array:, name:, namespace:, labels:, annotations:) + secret = { + kind: "Secret", + apiVersion: "v1", + metadata: { + name: name, + namespace: namespace, + labels: labels, + annotations: annotations + }, + data: {} + } + + desired_config_array.append(secret) + + nil + end + + # @param [Array] desired_config_array + # @param [String] gitlab_workspaces_proxy_namespace + # @param [String] name + # @param [String] namespace + # @param [Boolean] network_policy_enabled + # @param [Array] network_policy_egress + # @param [Hash] labels + # @param [Hash] annotations + # @return [void] + def self.append_network_policy( + desired_config_array:, + name:, + namespace:, + gitlab_workspaces_proxy_namespace:, + network_policy_enabled:, + network_policy_egress:, + labels:, + annotations: + ) + return unless network_policy_enabled + + egress_ip_rules = network_policy_egress + + policy_types = %w[Ingress Egress] + + proxy_namespace_selector = { + matchLabels: { + "kubernetes.io/metadata.name": gitlab_workspaces_proxy_namespace + } + } + proxy_pod_selector = { + matchLabels: { + "app.kubernetes.io/name": "gitlab-workspaces-proxy" + } + } + ingress = [{ from: [{ namespaceSelector: proxy_namespace_selector, podSelector: proxy_pod_selector }] }] + + kube_system_namespace_selector = { + matchLabels: { + "kubernetes.io/metadata.name": "kube-system" + } + } + egress = [ + { + ports: [{ port: 53, protocol: "TCP" }, { port: 53, protocol: "UDP" }], + to: [{ namespaceSelector: kube_system_namespace_selector }] + } + ] + egress_ip_rules.each do |egress_rule| + egress.append( + { to: [{ ipBlock: { cidr: egress_rule[:allow], except: egress_rule[:except] } }] } + ) + end + + # Use the workspace_id as a pod selector if it is present + workspace_id = labels.fetch(:"workspaces.gitlab.com/id", nil) + pod_selector = {} + # TODO: Unconditionally add this pod selector in https://gitlab.com/gitlab-org/gitlab/-/issues/535197 + if workspace_id.present? + pod_selector[:matchLabels] = { + "workspaces.gitlab.com/id": workspace_id + } + end + + network_policy = { + apiVersion: "networking.k8s.io/v1", + kind: "NetworkPolicy", + metadata: { + annotations: annotations, + labels: labels, + name: name, + namespace: namespace + }, + spec: { + egress: egress, + ingress: ingress, + podSelector: pod_selector, + policyTypes: policy_types + } + } + + desired_config_array.append(network_policy) + + nil + end + + # @param [Array] desired_config_array + # @param [String] processed_devfile_yaml + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @return [void] + def self.append_scripts_resources( + desired_config_array:, + processed_devfile_yaml:, + name:, + namespace:, + labels:, + annotations: + ) + desired_config_array => [ + *_, + { + kind: "Deployment", + spec: { + template: { + spec: { + containers: Array => containers, + volumes: Array => volumes + } + } + } + }, + *_ + ] + + processed_devfile = YAML.safe_load(processed_devfile_yaml).deep_symbolize_keys.to_h + + devfile_commands = processed_devfile.fetch(:commands) + devfile_events = processed_devfile.fetch(:events) + + # NOTE: This guard clause ensures we still support older running workspaces which were started before we + # added support for devfile postStart events. In that case, we don't want to add any resources + # related to the postStart script handling, because that would cause those existing workspaces + # to restart because the deployment would be updated. + return unless devfile_events[:postStart].present? + + ScriptsConfigmapAppender.append( + desired_config_array: desired_config_array, + name: name, + namespace: namespace, + labels: labels, + annotations: annotations, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + ScriptsVolumeInserter.insert( + configmap_name: name, + containers: containers, + volumes: volumes + ) + + KubernetesPoststartHookInserter.insert( + containers: containers, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + nil + end + + # @param [Array] desired_config_array + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @param [Hash] max_resources_per_workspace + # @param [String] shared_namespace + # @return [void] + def self.append_resource_quota( + desired_config_array:, + name:, + namespace:, + labels:, + annotations:, + max_resources_per_workspace:, + shared_namespace: + ) + return unless max_resources_per_workspace.present? + return if shared_namespace.present? + + max_resources_per_workspace => { + limits: { + cpu: limits_cpu, + memory: limits_memory + }, + requests: { + cpu: requests_cpu, + memory: requests_memory + } + } + + resource_quota = { + apiVersion: "v1", + kind: "ResourceQuota", + metadata: { + annotations: annotations, + labels: labels, + name: name, + namespace: namespace + }, + spec: { + hard: { + "limits.cpu": limits_cpu, + "limits.memory": limits_memory, + "requests.cpu": requests_cpu, + "requests.memory": requests_memory + } + } + } + + desired_config_array.append(resource_quota) + + nil + end + + # @param [Array] desired_config_array + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @param [Array] image_pull_secrets + # @return [void] + def self.append_image_pull_secrets_service_account( + desired_config_array:, + name:, + namespace:, + labels:, + annotations:, + image_pull_secrets: + ) + image_pull_secrets_names = image_pull_secrets.map { |secret| { name: secret.fetch(:name) } } + + workspace_service_account_definition = { + apiVersion: "v1", + kind: "ServiceAccount", + metadata: { + name: name, + namespace: namespace, + annotations: annotations, + labels: labels + }, + automountServiceAccountToken: false, + imagePullSecrets: image_pull_secrets_names + } + + desired_config_array.append(workspace_service_account_definition) + + nil + end + + private_class_method :append_inventory_config_map, + :append_secret, + :append_network_policy, + :append_scripts_resources, + :append_resource_quota, + :append_image_pull_secrets_service_account + end + end + end + end +end 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 new file mode 100644 index 00000000000000..e11b491fd4714b --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class DevfileResourceModifier + include RemoteDevelopment::WorkspaceOperations::Create::CreateConstants + + def self.modify(context) + context => { + workspace_name: String => workspace_name, + desired_config_array: Array => desired_config_array, + use_kubernetes_user_namespaces: TrueClass | FalseClass => use_kubernetes_user_namespaces, + default_runtime_class: String => default_runtime_class, + allow_privilege_escalation: TrueClass | FalseClass => allow_privilege_escalation, + default_resources_per_workspace_container: Hash => default_resources_per_workspace_container, + env_secret_name: String => env_secret_name, + file_secret_name: String => file_secret_name, + } + + desired_config_array = set_host_users( + desired_config_array: desired_config_array, + use_kubernetes_user_namespaces: use_kubernetes_user_namespaces + ) + desired_config_array = set_runtime_class( + desired_config_array: desired_config_array, + runtime_class_name: default_runtime_class + ) + desired_config_array = set_security_context( + desired_config_array: desired_config_array, + allow_privilege_escalation: allow_privilege_escalation + ) + desired_config_array = patch_default_resources( + desired_config_array: desired_config_array, + default_resources_per_workspace_container: + default_resources_per_workspace_container + ) + desired_config_array = inject_secrets( + desired_config_array: desired_config_array, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name + ) + + set_service_account( + desired_config_array: desired_config_array, + service_account_name: workspace_name + ) + + context.merge({ desired_config_array: desired_config_array }) + end + + 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' + + workspace_resource[:spec][:template][:spec][:hostUsers] = use_kubernetes_user_namespaces + end + desired_config_array + end + + # @param [Array] desired_config_array + # @param [String] runtime_class_name + # @return [Array] + 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' + + workspace_resource[:spec][:template][:spec][:runtimeClassName] = runtime_class_name + end + desired_config_array + end + + # Devfile library allows specifying the security context of pods/containers as mentioned in + # https://github.com/devfile/api/issues/920 through `pod-overrides` and `container-overrides` attributes. + # However, https://github.com/devfile/library/pull/158 which is implementing this feature, + # is not part of v2.2.0 which is the latest release of the devfile which is being used in the devfile-gem. + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/409189 + # Once devfile library releases a new version, update the devfile-gem and move + # the logic of setting the security context as part of workspace creation. + + # @param [Array] desired_config_array + # @param [Boolean] allow_privilege_escalation + # @param [Boolean] use_kubernetes_user_namespaces + # @return [Array] + 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_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 + end + desired_config_array + end + + # @param [Array] desired_config_array + # @param [Hash] default_resources_per_workspace_container + # @return [Array] + 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 + end + end + desired_config_array + end + + # @param [Array] desired_config_array + # @param [String] env_secret_name + # @param [String] file_secret_name + # @return [Array] + 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_mount = { + name: VARIABLES_VOLUME_NAME, + mountPath: VARIABLES_VOLUME_PATH + } + + 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.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 + end + desired_config_array + end + + # @param [Array] desired_config_array + # @param [String] service_account_name + # @return [Array] + 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' + + workspace_resource[:spec][:template][:spec][:serviceAccountName] = service_account_name + end + desired_config_array + end + + private_class_method :set_host_users, + :set_runtime_class, + :set_security_context, + :patch_default_resources, + :inject_secrets, + :set_service_account + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/main.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/main.rb new file mode 100644 index 00000000000000..f7ffac7d6ad5b6 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/main.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class Main + # @param [Hash] parent_context + # @return [Hash] + def self.main(parent_context) + parent_context => { + params: params, + workspace: workspace, + logger: logger + } + + context = { + workspace_id: workspace.id, + workspace_name: workspace.name, + workspace_namespace: workspace.namespace, + workspace_desired_state_is_running: workspace.desired_state_running?, + workspaces_agent_id: params[:agent].id, + workspaces_agent_config: workspace.workspaces_agent_config, + processed_devfile_yaml: workspace.processed_devfile, + logger: logger, + desired_config_array: [] + } + + initial_result = Gitlab::Fp::Result.ok(context) + + result = + initial_result + .map(ConfigValuesExtractor.method(:extract)) + .map(DevfileParserGetter.method(:get)) + .map(DesiredConfigYamlParser.method(:parse)) + .map(DevfileResourceModifier.method(:modify)) + .map(DevfileResourceAppender.method(:append)) + .map( + ->(context) do + context.merge( + desired_config: + RemoteDevelopment::WorkspaceOperations::DesiredConfig.new( + desired_config_array: context.fetch(:desired_config_array) + ) + ) + end + ) + + parent_context[:desired_config] = result.unwrap.fetch(:desired_config) + + parent_context + end + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender.rb b/ee/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender.rb index b9889b637de4fe..83aea6dfaecab9 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender.rb +++ b/ee/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender.rb @@ -8,7 +8,7 @@ class ScriptsConfigmapAppender include CreateConstants include WorkspaceOperationsConstants - # @param [Array] desired_config + # @param [Array] desired_config_array # @param [String] name # @param [String] namespace # @param [Hash] labels @@ -16,7 +16,15 @@ class ScriptsConfigmapAppender # @param [Array] devfile_commands # @param [Hash] devfile_events # @return [void] - def self.append(desired_config:, name:, namespace:, labels:, annotations:, devfile_commands:, devfile_events:) + def self.append( + desired_config_array:, + name:, + namespace:, + labels:, + annotations:, + devfile_commands:, + devfile_events: + ) configmap_data = {} configmap = @@ -47,7 +55,7 @@ def self.append(desired_config:, name:, namespace:, labels:, annotations:, devfi # noinspection RubyMismatchedArgumentType - RubyMine is misinterpreting types for Hash values configmap[:data] = Gitlab::Utils.deep_sort_hashes(configmap_data).to_h - desired_config.append(configmap) + desired_config_array.append(configmap) nil 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 new file mode 100644 index 00000000000000..fcba5eafe9df23 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + class WorkspaceAgentkStateCreator + include Messages + + # @param [Hash] context + # @return [void] + def self.create(context) + context => { + workspace: Workspace => workspace, + # NOTE: This has to be fully qualified or the class will not be found + desired_config: ::RemoteDevelopment::WorkspaceOperations::DesiredConfig => desired_config, + logger: logger + } + + unless desired_config.valid? + logger.error( + message: "desired_config is invalid", + error_type: "workspace_agentk_state_error", + workspace_id: workspace.id, + validation_error: desired_config.errors.full_messages + ) + end + + # TODO: Enable it on production by the end of the epic https://gitlab.com/groups/gitlab-org/-/epics/17483 + if Rails.env.test? + workspace_agentk_state = WorkspaceAgentkState.create!( + workspace: workspace, + project: workspace.project, + desired_config: desired_config.as_json + ) + + if workspace_agentk_state.errors.present? + return Gitlab::Fp::Result.err( + WorkspaceAgentkStateCreateFailed.new({ errors: workspace_agentk_state.errors, context: context }) + ) + end + end + + Gitlab::Fp::Result.ok(context) + end + end + 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 ef9c349b4095a2..c161b24e6279f9 100644 --- a/ee/lib/remote_development/workspace_operations/desired_config.rb +++ b/ee/lib/remote_development/workspace_operations/desired_config.rb @@ -7,6 +7,7 @@ class DesiredConfig include ActiveModel::Attributes include ActiveModel::Validations include ActiveModel::Serialization + include ActiveModel::Serializers::JSON # @!attribute [rw] desired_config_array # @return [Array] diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor.rb new file mode 100644 index 00000000000000..de5ec9babd80e1 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + class OldConfigValuesExtractor + include States + + # @param [RemoteDevelopment::Workspace] workspace + # @return [Hash] + def self.extract(workspace:) + workspace_name = workspace.name + workspaces_agent_config = workspace.workspaces_agent_config + + domain_template = "{{.port}}-#{workspace_name}.#{workspaces_agent_config.dns_zone}" + + max_resources_per_workspace = + deep_sort_and_symbolize_hashes(workspaces_agent_config.max_resources_per_workspace) + + # NOTE: In order to prevent unwanted restarts of the workspace, we need to ensure that the hexdigest + # of the max_resources_per_workspace is backward compatible, and uses the same sorting as the + # legacy logic of existing running workspaces. This means that only the top level keys are sorted, + # not the nested hashes. But everywhere else we will use the deeply sorted version. This workaround + # can be removed if we move all of this logic from workspace reconcile-time to create-time. + # Also note that the value has always been deep_symbolized before #to_s, so we preserve that as well. + max_resources_per_workspace_sha256_with_legacy_sorting = + OpenSSL::Digest::SHA256.hexdigest( + workspaces_agent_config.max_resources_per_workspace.deep_symbolize_keys.sort.to_h.to_s + ) + + default_resources_per_workspace_container = + deep_sort_and_symbolize_hashes(workspaces_agent_config.default_resources_per_workspace_container) + + shared_namespace = workspaces_agent_config.shared_namespace + # TODO: Fix this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/541902 + shared_namespace = "" if shared_namespace.nil? + + 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_with_legacy_sorting + } + agent_annotations = workspaces_agent_config.annotations + common_annotations = agent_annotations.merge(extra_annotations) + + agent_labels = workspaces_agent_config.labels + labels = agent_labels.merge({ "agent.gitlab.com/id": workspace.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" + + { + # 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, + default_runtime_class: workspaces_agent_config.default_runtime_class, + domain_template: domain_template, + # NOTE: update env_secret_name to "#{workspace.name}-environment". This is to ensure naming consistency. + # Changing it now would require migration from old config version to a new one. + # Update this when a new desired config generator is created for some other reason. + env_secret_name: "#{workspace_name}-env-var", + file_secret_name: "#{workspace_name}-file", + 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), + max_resources_per_workspace: max_resources_per_workspace, + network_policy_enabled: workspaces_agent_config.network_policy_enabled, + network_policy_egress: deep_sort_and_symbolize_hashes(workspaces_agent_config.network_policy_egress), + processed_devfile_yaml: workspace.processed_devfile, + replicas: workspace.desired_state_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_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_name: workspace_inventory_name + } + end + + # @param [Array, Hash] collection + # @return [Array, Hash] + def self.deep_sort_and_symbolize_hashes(collection) + collection_to_return = Gitlab::Utils.deep_sort_hashes(collection) + + # NOTE: deep_symbolize_keys! is not available on Array, so we wrap the collection in a + # Hash in case it is an Array. + { to_symbolize: collection_to_return }.deep_symbolize_keys! + collection_to_return + end + + private_class_method :deep_sort_and_symbolize_hashes + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator.rb similarity index 98% rename from ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb rename to ee/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator.rb index d9c2ba84ec9857..80ccb4761278c2 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator.rb @@ -5,7 +5,7 @@ module WorkspaceOperations module Reconcile module Output # TODO this file is marked for deletion by the end of this epic https://gitlab.com/groups/gitlab-org/-/epics/17483 - class DesiredConfigGenerator + class OldDesiredConfigGenerator include Create::CreateConstants # @param [RemoteDevelopment::Workspace] workspace @@ -13,7 +13,7 @@ class DesiredConfigGenerator # @param [RemoteDevelopment::Logger] logger # @return [Array] def self.generate_desired_config(workspace:, include_all_resources:, logger:) - config_values_extractor_result = Create::DesiredConfig::ConfigValuesExtractor.extract(workspace: workspace) + config_values_extractor_result = OldConfigValuesExtractor.extract(workspace: workspace) config_values_extractor_result => { allow_privilege_escalation: TrueClass | FalseClass => allow_privilege_escalation, common_annotations: Hash => common_annotations, @@ -77,7 +77,7 @@ def self.generate_desired_config(workspace:, include_all_resources:, logger:) use_kubernetes_user_namespaces: use_kubernetes_user_namespaces } - resources_from_devfile_parser = Create::DesiredConfig::DevfileParser.get_all( + resources_from_devfile_parser = OldDevfileParser.get_all( processed_devfile_yaml: processed_devfile_yaml, params: devfile_parser_params, logger: logger @@ -402,7 +402,7 @@ def self.append_scripts_resources( # to restart because the deployment would be updated. return unless devfile_events[:postStart].present? - Create::DesiredConfig::ScriptsConfigmapAppender.append( + OldScriptsConfigmapAppender.append( desired_config: desired_config, name: name, namespace: namespace, diff --git a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser.rb similarity index 99% rename from ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser.rb rename to ee/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser.rb index 4996c95f6ce758..86a3eafa1670e0 100644 --- a/ee/lib/remote_development/workspace_operations/create/desired_config/devfile_parser.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser.rb @@ -4,11 +4,11 @@ module RemoteDevelopment module WorkspaceOperations - module Create - module DesiredConfig - class DevfileParser + module Reconcile + module Output + class OldDevfileParser include WorkspaceOperationsConstants - include CreateConstants + include Create::CreateConstants # @param [String] processed_devfile_yaml # @param [Hash] params diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender.rb new file mode 100644 index 00000000000000..e2613d1f941909 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Reconcile + module Output + class OldScriptsConfigmapAppender + include Create::CreateConstants + include WorkspaceOperationsConstants + + # @param [Array] desired_config + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @param [Array] devfile_commands + # @param [Hash] devfile_events + # @return [void] + def self.append(desired_config:, name:, namespace:, labels:, annotations:, devfile_commands:, devfile_events:) + configmap_data = {} + + configmap = + { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: name, + namespace: namespace, + labels: labels, + annotations: annotations + }, + data: configmap_data + } + + add_devfile_command_scripts_to_configmap_data( + configmap_data: configmap_data, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + add_run_poststart_commands_script_to_configmap_data( + configmap_data: configmap_data, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + # noinspection RubyMismatchedArgumentType - RubyMine is misinterpreting types for Hash values + configmap[:data] = Gitlab::Utils.deep_sort_hashes(configmap_data).to_h + + desired_config.append(configmap) + + nil + end + + # @param [Hash] configmap_data + # @param [Array] devfile_commands + # @param [Hash] devfile_events + # @return [void] + def self.add_devfile_command_scripts_to_configmap_data(configmap_data:, devfile_commands:, devfile_events:) + devfile_events => { postStart: Array => poststart_command_ids } + + poststart_command_ids.each do |poststart_command_id| + command = devfile_commands.find { |command| command.fetch(:id) == poststart_command_id } + command => { + exec: { + commandLine: String => command_line + } + } + + configmap_data[poststart_command_id.to_sym] = command_line + end + + nil + end + + # @param [Hash] configmap_data + # @param [Array] devfile_commands + # @param [Hash] devfile_events + # @return [void] + def self.add_run_poststart_commands_script_to_configmap_data( + configmap_data:, + devfile_commands:, + devfile_events: + ) + devfile_events => { postStart: Array => poststart_command_ids } + + internal_blocking_command_label_present = devfile_commands.find do |command| + command.dig(:exec, :label) == INTERNAL_BLOCKING_COMMAND_LABEL + end + + unless internal_blocking_command_label_present + configmap_data[LEGACY_RUN_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym] = + <<~SH.chomp + #!/bin/sh + #{get_poststart_command_script_content(poststart_command_ids: poststart_command_ids)} + SH + return + end + + # Segregate internal commands and user provided commands. + # Before any non-blocking post start command is executed, we wait for the workspace to be marked ready. + internal_blocking_poststart_command_ids, non_blocking_poststart_command_ids = + poststart_command_ids.partition do |id| + command = devfile_commands.find { |cmd| cmd[:id] == id } + command && command.dig(:exec, :label) == INTERNAL_BLOCKING_COMMAND_LABEL + end + + configmap_data[RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym] = + <<~SH.chomp + #!/bin/sh + #{get_poststart_command_script_content(poststart_command_ids: internal_blocking_poststart_command_ids)} + SH + + configmap_data[RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym] = + <<~SH.chomp + #!/bin/sh + #{get_poststart_command_script_content(poststart_command_ids: non_blocking_poststart_command_ids)} + SH + + nil + end + + # @param [Array] poststart_command_ids + # @return [String] + def self.get_poststart_command_script_content(poststart_command_ids:) + poststart_command_ids.map do |poststart_command_id| + # NOTE: We force all the poststart scripts to exit successfully with `|| true`, to + # prevent the Kubernetes poststart hook from failing, and thus prevent the + # container from exiting. Then users can view logs to debug failures. + # See https://github.com/eclipse-che/che/issues/23404#issuecomment-2787779571 + # for more context. + <<~SH + echo "$(date -Iseconds): ----------------------------------------" + echo "$(date -Iseconds): Running #{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{poststart_command_id}..." + #{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{poststart_command_id} || true + echo "$(date -Iseconds): Finished running #{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{poststart_command_id}." + SH + end.join + end + + private_class_method :add_devfile_command_scripts_to_configmap_data, + :add_run_poststart_commands_script_to_configmap_data, :get_poststart_command_script_content + 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 522b1da136a332..4183d1fcc0e58b 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 = DesiredConfigGenerator.generate_desired_config( + workspace_resources = OldDesiredConfigGenerator.generate_desired_config( workspace: workspace, include_all_resources: include_all_resources, logger: logger diff --git a/ee/spec/lib/remote_development/workspace_operations/create/creator_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/creator_spec.rb index 10013abd19819e..9a9425802784c2 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/creator_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/creator_spec.rb @@ -12,7 +12,9 @@ [RemoteDevelopment::WorkspaceOperations::Create::CreatorBootstrapper, :map], [RemoteDevelopment::WorkspaceOperations::Create::PersonalAccessTokenCreator, :and_then], [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceCreator, :and_then], - [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceVariablesCreator, :and_then] + [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceVariablesCreator, :and_then], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main, :map], + [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceAgentkStateCreator, :and_then] ] end @@ -36,6 +38,16 @@ describe "error cases" do let(:error_details) { "some error details" } let(:err_message_content) { { errors: error_details, context: context_passed_along_steps } } + let(:rop_steps) do + [ + [RemoteDevelopment::WorkspaceOperations::Create::CreatorBootstrapper, :map], + [RemoteDevelopment::WorkspaceOperations::Create::PersonalAccessTokenCreator, :and_then], + [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceCreator, :and_then], + [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceVariablesCreator, :and_then], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main, :map], + [RemoteDevelopment::WorkspaceOperations::Create::WorkspaceAgentkStateCreator, :and_then] + ] + end shared_examples "rop invocation with error response" do it "returns expected response" do @@ -78,6 +90,14 @@ }, lazy { Gitlab::Fp::Result.err(Messages::WorkspaceCreateFailed.new(err_message_content)) } ], + [ + "when WorkspaceAgentkState returns WorkspaceAgentkStateCreateFailed", + { + step_class: RemoteDevelopment::WorkspaceOperations::Create::WorkspaceAgentkStateCreator, + returned_message: lazy { Messages::WorkspaceAgentkStateCreateFailed.new(err_message_content) } + }, + lazy { Gitlab::Fp::Result.err(Messages::WorkspaceCreateFailed.new(err_message_content)) } + ], ] end # rubocop:enable Style/TrailingCommaInArrayLiteral 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 7f78039b0c643b..36ddfbb25845f0 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 @@ -1,26 +1,23 @@ # frozen_string_literal: true -require "spec_helper" +require "fast_spec_helper" # noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::ConfigValuesExtractor, feature_category: :workspaces do include_context "with constant modules" - let_it_be(:user) { create(:user) } - let_it_be(:agent, reload: true) { create(:ee_cluster_agent) } - let_it_be(:workspace_name) { "workspace-name" } - let(:desired_state) { states_module::STOPPED } - let_it_be(:actual_state) { states_module::STOPPED } - let_it_be(:dns_zone) { "my.dns-zone.me" } - let_it_be(:labels) { { "some-label": "value", "other-label": "other-value" } } - let_it_be(:started) { true } - let_it_be(:include_all_resources) { false } - let_it_be(:network_policy_enabled) { true } - let_it_be(:gitlab_workspaces_proxy_namespace) { "gitlab-workspaces" } - let_it_be(:image_pull_secrets) { [{ namespace: "default", name: "secret-name" }] } - let_it_be(:agent_annotations) { { "some/annotation": "value" } } - let_it_be(:shared_namespace) { "" } - let_it_be(:network_policy_egress) do + let(:workspace_name) { "workspace-name" } + let(:dns_zone) { "my.dns-zone.me" } + let(:labels) { { "some-label": "value", "other-label": "other-value" } } + let(:network_policy_enabled) { true } + let(:gitlab_workspaces_proxy_namespace) { "gitlab-workspaces" } + let(:image_pull_secrets) { [{ namespace: "default", name: "secret-name" }] } + let(:agent_annotations) { { "some/annotation": "value" } } + let(:shared_namespace) { "" } + let(:allow_privilege_escalation) { true } + let(:default_runtime_class) { "example-default-runtime-class" } + let(:use_kubernetes_user_namespaces) { true } + let(:network_policy_egress) do [ { except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16], @@ -29,7 +26,7 @@ ] end - let_it_be(:max_resources_per_workspace) do + let(:max_resources_per_workspace) do { requests: { memory: "512Mi", @@ -42,7 +39,7 @@ } end - let_it_be(:default_resources_per_workspace_container) do + let(:default_resources_per_workspace_container) do { requests: { memory: "600Mi", @@ -55,10 +52,8 @@ } end - let_it_be(:workspaces_agent_config) do - config = create( - :workspaces_agent_config, - agent: agent, + let(:workspaces_agent_config) do + instance_double("RemoteDevelopment::WorkspacesAgentConfig", # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper dns_zone: dns_zone, image_pull_secrets: image_pull_secrets, network_policy_enabled: network_policy_enabled, @@ -68,33 +63,31 @@ labels: labels.deep_stringify_keys, annotations: agent_annotations.deep_stringify_keys, network_policy_egress: network_policy_egress.map(&:deep_stringify_keys), - shared_namespace: shared_namespace + shared_namespace: shared_namespace, + allow_privilege_escalation: allow_privilege_escalation, + default_runtime_class: default_runtime_class, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace, + use_kubernetes_user_namespaces: use_kubernetes_user_namespaces ) - agent.reload - config end - let_it_be(:workspace) do - workspaces_agent_config - create( - :workspace, - name: workspace_name, - agent: agent, - user: user, - actual_state: actual_state - ) + let(:workspace_id) { 1 } + let(:workspace_desired_state_is_running) { true } + let(:workspaces_agent_id) { 11 } + let(:context) do + { + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_desired_state_is_running: workspace_desired_state_is_running, + workspaces_agent_id: workspaces_agent_id, + workspaces_agent_config: workspaces_agent_config + } end - let_it_be(:deployment_resource_version_from_agent) { workspace.deployment_resource_version } - subject(:extractor) { described_class } - before do - workspace.update!(desired_state: desired_state) - end - it "extracts the config values" do - extracted_values = extractor.extract(workspace: workspace) + extracted_values = extractor.extract(context) expect(extracted_values).to be_a(Hash) expect(extracted_values.keys) .to eq( @@ -110,17 +103,21 @@ image_pull_secrets labels max_resources_per_workspace - network_policy_enabled network_policy_egress - processed_devfile_yaml + network_policy_enabled replicas scripts_configmap_name secrets_inventory_annotations secrets_inventory_name shared_namespace use_kubernetes_user_namespaces + workspace_desired_state_is_running + workspace_id workspace_inventory_annotations workspace_inventory_name + workspace_name + workspaces_agent_config + workspaces_agent_id ] ) @@ -130,82 +127,65 @@ expect(extracted_values[:common_annotations]).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/host-template": "{{.port}}-#{workspace_name}.#{dns_zone}", + "workspaces.gitlab.com/id": workspace_id.to_s, "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") - - expect(extracted_values[:file_secret_name]).to eq("#{workspace.name}-file") - + expect(extracted_values[:env_secret_name]).to eq("#{workspace_name}-env-var") + expect(extracted_values[:file_secret_name]).to eq("#{workspace_name}-file") expect(extracted_values[:image_pull_secrets]).to eq([{ name: "secret-name", namespace: "default" }]) - expect(extracted_values[:gitlab_workspaces_proxy_namespace]).to eq("gitlab-workspaces") - expect(extracted_values[:labels]).to eq( { - "agent.gitlab.com/id": agent.id.to_s, + "agent.gitlab.com/id": workspaces_agent_id.to_s, "other-label": "other-value", "some-label": "value" } ) - expect(extracted_values[:network_policy_enabled]).to be(true) - expect(extracted_values[:network_policy_egress]) .to eq([{ allow: "0.0.0.0/0", except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16] }]) - expect(extracted_values[:max_resources_per_workspace]).to eq(max_resources_per_workspace) - - expect(extracted_values[:processed_devfile_yaml]).to eq(workspace.processed_devfile) - - expect(extracted_values[:scripts_configmap_name]).to eq("#{workspace.name}-scripts-configmap") - + expect(extracted_values[:scripts_configmap_name]).to eq("#{workspace_name}-scripts-configmap") expect(extracted_values[:secrets_inventory_annotations]).to eq( { - "config.k8s.io/owning-inventory": "#{workspace.name}-secrets-inventory", + "config.k8s.io/owning-inventory": "#{workspace_name}-secrets-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/host-template": "{{.port}}-#{workspace_name}.#{dns_zone}", + "workspaces.gitlab.com/id": workspace_id.to_s, "workspaces.gitlab.com/max-resources-per-workspace-sha256": "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" } ) - - expect(extracted_values[:secrets_inventory_name]).to eq("#{workspace.name}-secrets-inventory") - + expect(extracted_values[:secrets_inventory_name]).to eq("#{workspace_name}-secrets-inventory") expect(extracted_values[:workspace_inventory_annotations]).to eq( { - "config.k8s.io/owning-inventory": "#{workspace.name}-workspace-inventory", + "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/host-template": "{{.port}}-#{workspace_name}.#{dns_zone}", + "workspaces.gitlab.com/id": workspace_id.to_s, "workspaces.gitlab.com/max-resources-per-workspace-sha256": "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" } ) - - expect(extracted_values[:workspace_inventory_name]).to eq("#{workspace.name}-workspace-inventory") + expect(extracted_values[:workspace_inventory_name]).to eq("#{workspace_name}-workspace-inventory") end describe "devfile_parser_params[:replicas]" do - subject(:replicas) { extractor.extract(workspace: workspace).fetch(:replicas) } + subject(:replicas) { extractor.extract(context).fetch(:replicas) } context "when desired_state is Running" do - let(:desired_state) { states_module::RUNNING } let(:expected_replicas) { 1 } it { is_expected.to eq(expected_replicas) } end context "when desired_state is not CreationRequested nor Running" do - let(:desired_state) { states_module::STOPPED } + let(:workspace_desired_state_is_running) { false } let(:expected_replicas) { 0 } it { is_expected.to eq(expected_replicas) } @@ -213,12 +193,12 @@ end describe "devfile_parser_params[:labels]" do - subject(:actual_labels) { extractor.extract(workspace: workspace).fetch(:labels) } + subject(:actual_labels) { extractor.extract(context).fetch(:labels) } context "when shared_namespace is not set" do let(:expected_labels) do { - "agent.gitlab.com/id": agent.id.to_s, + "agent.gitlab.com/id": workspaces_agent_id.to_s, "other-label": "other-value", "some-label": "value" } @@ -228,38 +208,14 @@ end context "when shared_namespace is set" do - let_it_be(:shared_namespace) { "default" } - let_it_be(:workspace_name) { "workspace-name-shared-namespace" } - let_it_be(:agent, reload: true) { create(:ee_cluster_agent) } - let_it_be(:workspaces_agent_config) do - config = create( - :workspaces_agent_config, - agent: agent, - dns_zone: dns_zone, - labels: labels.deep_stringify_keys, - shared_namespace: shared_namespace - ) - agent.reload - config - end - - let_it_be(:workspace) do - workspaces_agent_config - create( - :workspace, - name: workspace_name, - agent: agent, - user: user, - actual_state: actual_state - ) - end - + let(:shared_namespace) { "default" } + let(:workspace_name) { "workspace-name-shared-namespace" } let(:expected_labels) do { - "agent.gitlab.com/id": agent.id.to_s, + "agent.gitlab.com/id": workspaces_agent_id.to_s, "other-label": "other-value", "some-label": "value", - "workspaces.gitlab.com/id": workspace.id.to_s + "workspaces.gitlab.com/id": workspace_id.to_s } end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser_spec.rb new file mode 100644 index 00000000000000..32437b7c7d7627 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/desired_config_yaml_parser_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DesiredConfigYamlParser, feature_category: :workspaces do + describe "#parse" do + # noinspection KubernetesNonEditableKeys + let(:desired_config_yaml) do + <<~YAML + key1: + k1: v1 + --- + key2: + k2: v2 + YAML + end + + let(:expected_array) do + [ + { key1: { k1: "v1" } }, + { key2: { k2: "v2" } } + ] + end + + let(:context) { { desired_config_yaml: desired_config_yaml } } + + subject(:result) { described_class.parse(context) } + + it "transforms the YAML to an array of hashes and adds it to the context" do + expect(result[:desired_config_array]).to eq(expected_array) + end + end +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 new file mode 100644 index 00000000000000..8ed1b10b75e58e --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_getter_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileParserGetter, feature_category: :workspaces do + include_context 'with remote development shared fixtures' + + let(:logger) { instance_double("RemoteDevelopment::Logger") } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + let(:labels) { { "some-label": "value", "other-label": "other-value" } } + let(:agent_annotations) { { "some/annotation": "value" } } + let(:workspace_name) { "workspace-name" } + let(:workspace_namespace) { "workspace-namespace" } + let(:context) do + { + processed_devfile_yaml: example_devfile_yaml, + logger: logger, + workspace_inventory_annotations: { k1: "v1", k2: "v2" }, + domain_template: "domain_template", + labels: labels, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + replicas: 1 + } + end + + describe "#get" do + subject(:result) { described_class.get(context) } + + context "when happy path" do + # noinspection KubernetesNonEditableKeys -- This is the resource as returned by the devfile executable, we want + # to assert on exactly this YAML + let(:expected_desired_config_yaml) do + <<~YAML + apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + k1: v1 + k2: v2 + creationTimestamp: null + labels: + other-label: other-value + some-label: value + name: workspace-name + namespace: workspace-namespace + spec: + replicas: 1 + selector: + matchLabels: + other-label: other-value + some-label: value + strategy: + type: Recreate + template: + metadata: + annotations: + k1: v1 + k2: v2 + creationTimestamp: null + labels: + other-label: other-value + some-label: value + name: workspace-name + namespace: workspace-namespace + spec: + containers: + - env: + - name: PROJECTS_ROOT + value: /projects + - name: PROJECT_SOURCE + value: /projects + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo + imagePullPolicy: Always + name: tooling-container + resources: {} + - env: + - name: MYSQL_ROOT_PASSWORD + value: my-secret-pw + - name: PROJECTS_ROOT + value: /projects + - name: PROJECT_SOURCE + value: /projects + image: mysql + imagePullPolicy: Always + name: database-container + resources: {} + status: {} + --- + apiVersion: v1 + kind: Service + metadata: + annotations: + k1: v1 + k2: v2 + creationTimestamp: null + labels: + other-label: other-value + some-label: value + name: workspace-name + namespace: workspace-namespace + spec: + selector: + other-label: other-value + some-label: value + status: + loadBalancer: {} + YAML + end + + it "returns devfile contents" do + expect(result).to include(:desired_config_yaml) + expect(result[:desired_config_yaml]).not_to be_empty + expect(result[:desired_config_yaml]).to eq(expected_desired_config_yaml) + end + end + + shared_examples "fails" do + it "logs the error and raises the exception" do + expect { result }.to raise_error(StandardError, exception_message) + expect(logger).to have_received(:warn).with( + message: error_message, + error_type: "create_devfile_parser_error", + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + devfile_parser_error: exception_message + ) + end + end + + context "when Devfile::Parser#get_all raises Devfile::CliError" do + let(:exception_message) { "exception message" } + let(:error_message) do + <<~MSG.squish + Devfile::CliError: A non zero return code was observed when invoking the devfile CLI + executable from the devfile gem. + MSG + end + + before do + allow(Devfile::Parser).to receive(:get_all).and_raise(Devfile::CliError.new(exception_message)) + allow(logger).to receive(:warn) + end + + it_behaves_like "fails" + end + + context "when Devfile::Parser#get_all raises StandardError" do + let(:exception_message) { "exception message" } + let(:error_message) do + <<~MSG.squish + StandardError: An unrecoverable error occurred when invoking the devfile gem, + this may hint that a gem with a wrong architecture is being used. + MSG + end + + before do + allow(Devfile::Parser).to receive(:get_all).and_raise(StandardError.new(exception_message)) + allow(logger).to receive(:warn) + end + + it_behaves_like "fails" + end + end +end 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 new file mode 100644 index 00000000000000..9cd79bde0f123f --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_appender_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileResourceAppender, :freeze_time, feature_category: :workspaces do + include_context "with remote development shared fixtures" + + # 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 + instance_double( + "RemoteDevelopment::WorkspaceVariable", + key: "ENV_KEY", + value: "ENV_VALUE" + ) + end + + let(:file_var) do + instance_double( + "RemoteDevelopment::WorkspaceVariable", + key: "FILE_KEY", + value: "FILE_VALUE" + ) + end + + let(:workspace) do + instance_double( + "RemoteDevelopment::Workspace", + name: "workspace-991-990-fedcba", + namespace: "gl-rd-ns-991-990-fedcba", + workspace_variables: [env_var, file_var], + id: "991-990-fedcba", + agent: instance_double("Clusters::Agent", id: "991"), + actual_state: "RUNNING" + ) + end + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:labels) { { "app" => "workspace", "tier" => "development", "agent.gitlab.com/id" => "991" } } + let(:workspace_inventory_annotations) { { "environment" => "production", "team" => "engineering" } } + let(:common_annotations) do + { "workspaces.gitlab.com/host-template" => "3000-#{workspace.name}.workspaces.localdev.me" } + end + + let(:workspace_inventory_name) { "#{workspace.name}-workspace-inventory" } + 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" } + let(:processed_devfile_yaml) { example_processed_devfile_yaml } + let(:gitlab_workspaces_proxy_namespace) { "gitlab-workspaces" } + let(:network_policy_enabled) { true } + let(:network_policy_egress) { [{ allow: "0.0.0.0/0", except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16] }] } + let(:image_pull_secrets) { [] } + let(:max_resources_per_workspace) { {} } + let(:shared_namespace) { "" } + let(:env_secret_name) { "#{workspace.name}-env-var" } + let(:file_secret_name) { "#{workspace.name}-file" } + let(:base_deployment_resource) do + { + kind: "Deployment", + spec: { + template: { + spec: { + containers: [ + { + name: "c1", + resources: { limits: { cpu: "1", memory: "1Gi" }, requests: { cpu: "250m", memory: "256Mi" } }, + volumeMounts: [] + }, + { name: "c2", resources: {}, volumeMounts: [] } + ], + initContainers: [ + { name: "ic1", resources: {}, volumeMounts: [] } + ], + volumes: [] + } + } + } + } + end + + let(:desired_config_array) do + [ + base_deployment_resource + ] + end + + let(:context) do + { + 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, + 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, + max_resources_per_workspace: max_resources_per_workspace, + shared_namespace: shared_namespace, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name + } + end + + subject(:appended_context) { described_class.append(context) } + + it "appends all expected resource kinds and names to the config array" do + result = appended_context[:desired_config_array] + kinds_and_names = result.map { |r| [r[:kind], r.dig(:metadata, :name)] } + expect(kinds_and_names).to include(["ConfigMap", workspace_inventory_name]) + expect(kinds_and_names).to include(["ServiceAccount", workspace.name]) + expect(kinds_and_names).to include(["NetworkPolicy", workspace.name]) + expect(kinds_and_names).to include(["Secret", env_secret_name]) + expect(kinds_and_names).to include(["Secret", file_secret_name]) + + secret_resources = result.select { |r| r[:kind] == "Secret" } + secret_resources.each do |secret| + expect(secret[:data]).to eq({}) + end + end + + context "when network policy is disabled" do + let(:network_policy_enabled) { false } + + it "does not include a NetworkPolicy resource" do + result = appended_context[:desired_config_array] + kinds_and_names = result.map { |r| [r[:kind], r.dig(:metadata, :name)] } + expect(kinds_and_names).not_to include(["NetworkPolicy", workspace.name]) + end + end + + context "when max_resources_per_workspace is set" do + let(:max_resources_per_workspace) do + { limits: { cpu: "1.5", memory: "786Mi" }, requests: { cpu: "0.6", memory: "512Mi" } } + end + + it "includes a ResourceQuota resource" do + result = appended_context[:desired_config_array] + kinds = result.map { |r| r[:kind] } # rubocop:disable Rails/Pluck -- Not an ActiveRecord object + expect(kinds).to include("ResourceQuota") + end + end + + context "when shared_namespace is set" do + let(:shared_namespace) { "shared-ns" } + + it "does not include a ResourceQuota resource" do + result = appended_context[:desired_config_array] + kinds = result.map { |r| r[:kind] } # rubocop:disable Rails/Pluck -- Not an ActiveRecord object + expect(kinds).not_to include("ResourceQuota") + end + end + + context "when image_pull_secrets are provided" do + let(:image_pull_secrets) { [{ name: "secret-name" }] } + + it "includes a ServiceAccount with imagePullSecrets" do + result = appended_context[:desired_config_array] + sa = result.find { |r| r[:kind] == "ServiceAccount" } + expect(sa[:imagePullSecrets]).to eq([{ name: "secret-name" }]) + end + end +end 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 new file mode 100644 index 00000000000000..220aefe9f10c17 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_resource_modifier_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileResourceModifier, feature_category: :workspaces do + include_context "with constant modules" + + let(:base_deployment_resource) do + { + kind: "Deployment", + spec: { + template: { + spec: { + containers: [ + { + name: "c1", + resources: { limits: { cpu: "1", memory: "1Gi" }, requests: { cpu: "250m", memory: "256Mi" } }, + volumeMounts: [] + }, + { name: "c2", resources: {}, volumeMounts: [] } + ], + initContainers: [ + { name: "ic1", resources: {}, volumeMounts: [] } + ], + volumes: [] + } + } + } + } + end + + let(:non_deployment_resource) do + { kind: "Service", spec: { foo: "bar" } } + end + + let(:desired_config_array) do + [ + base_deployment_resource, + non_deployment_resource + ] + 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, + desired_config_array: desired_config_array, + use_kubernetes_user_namespaces: use_kubernetes_user_namespaces, + default_runtime_class: default_runtime_class, + allow_privilege_escalation: allow_privilege_escalation, + default_resources_per_workspace_container: default_resources_per_workspace_container, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name + } + end + + let(:use_kubernetes_user_namespaces) { true } + let(:default_runtime_class) { "my-runtime-class" } + let(:allow_privilege_escalation) { false } + let(:default_resources_per_workspace_container) do + { limits: { memory: "2Gi" }, requests: { cpu: "500m", memory: "512Mi" } } + end + + let(:workspace_name) { "myworkspacename" } + let(:env_secret_name) { "#{workspace_name}-env-var" } + let(:file_secret_name) { "#{workspace_name}-file" } + + let(:expected_pod_security_context) do + { + runAsNonRoot: true, + runAsUser: create_constants_module::RUN_AS_USER, + fsGroup: 0, + fsGroupChangePolicy: "OnRootMismatch" + } + end + + let(:expected_container_security_context) do + { + allowPrivilegeEscalation: false, + privileged: false, + runAsNonRoot: true, + runAsUser: create_constants_module::RUN_AS_USER + } + end + + let(:expected_volume) do + { + name: workspace_operations_constants_module::VARIABLES_VOLUME_NAME, + projected: { + defaultMode: workspace_operations_constants_module::VARIABLES_VOLUME_DEFAULT_MODE, + sources: [{ secret: { name: file_secret_name } }] + } + } + end + + let(:expected_volume_mount) do + { + name: workspace_operations_constants_module::VARIABLES_VOLUME_NAME, + mountPath: workspace_operations_constants_module::VARIABLES_VOLUME_PATH + } + end + + let(:expected_env_from) do + [{ secretRef: { name: env_secret_name } }] + end + + subject(:result) { described_class.modify(context) } + + it 'adds :desired_config_array to context' do + expect(result).to include(:desired_config_array) + end + + it 'does not modify non-Deployment resources' do + service = result[:desired_config_array].find { |r| r[:kind] == "Service" } + expect(service).to eq(non_deployment_resource.deep_symbolize_keys) + end + + describe 'modifies Deployment resources' do + let(:deployment) { result[:desired_config_array].find { |r| r[:kind] == "Deployment" } } + let(:pod_spec) { deployment[:spec][:template][:spec] } + + it 'sets hostUsers if use_kubernetes_user_namespaces is true' do + expect(pod_spec[:hostUsers]).to be(true) + end + + it 'sets runtimeClassName if default_runtime_class is present' do + expect(pod_spec[:runtimeClassName]).to eq(default_runtime_class) + end + + it 'sets pod and container security contexts' do + expect(pod_spec[:securityContext]).to eq(expected_pod_security_context) + pod_spec[:containers].each do |container| + expect(container[:securityContext]).to eq(expected_container_security_context) + end + pod_spec[:initContainers].each do |container| + expect(container[:securityContext]).to eq(expected_container_security_context) + end + end + + it 'deep merges default_resources_per_workspace_container into all containers' do + expect(pod_spec[:containers][0][:resources][:limits]).to eq(cpu: "1", memory: "1Gi") + expect(pod_spec[:containers][0][:resources][:requests]).to eq(cpu: "250m", memory: "256Mi") + expect(pod_spec[:containers][1][:resources][:limits]).to eq(memory: "2Gi") + expect(pod_spec[:containers][1][:resources][:requests]).to eq(cpu: "500m", memory: "512Mi") + expect(pod_spec[:initContainers][0][:resources][:limits]).to eq(memory: "2Gi") + expect(pod_spec[:initContainers][0][:resources][:requests]).to eq(cpu: "500m", memory: "512Mi") + end + + it 'injects secrets as volumes, mounts, and envFrom' do + expect(pod_spec[:volumes]).to include(expected_volume) + pod_spec[:containers].each do |container| + expect(container[:volumeMounts]).to include(expected_volume_mount) + expect(container[:envFrom]).to eq(expected_env_from) + end + pod_spec[:initContainers].each do |container| + expect(container[:volumeMounts]).to include(expected_volume_mount) + expect(container[:envFrom]).to eq(expected_env_from) + end + end + + it 'sets serviceAccountName' do + expect(pod_spec[:serviceAccountName]).to eq(workspace_name) + end + end + + context 'when use_kubernetes_user_namespaces is false' do + let(:use_kubernetes_user_namespaces) { false } + + it 'does not set hostUsers' do + deployment = result[:desired_config_array].find { |r| r[:kind] == "Deployment" } + expect(deployment[:spec][:template][:spec]).not_to have_key(:hostUsers) + end + end + + context 'when default_runtime_class is empty' do + let(:default_runtime_class) { '' } + + it 'does not set runtimeClassName' do + deployment = result[:desired_config_array].find { |r| r[:kind] == "Deployment" } + expect(deployment[:spec][:template][:spec]).not_to have_key(:runtimeClassName) + end + end +end 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 new file mode 100644 index 00000000000000..3991dca84562da --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_integeration_spec.rb @@ -0,0 +1,4362 @@ +# frozen_string_literal: true + +require "fast_spec_helper" +# NOTE This explicit "hashdiff" require exists so we can run this spec against historical SHAs, before the require +# existed in ee/spec/fast_spec_helper.rb. It can be removed once there is no longer a need to run it against +# historical SHAs. +require "hashdiff" + +# noinspection RubyLiteralArrayInspection -- Keep original formatting for readability +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main, "Integration test for main", feature_category: :workspaces do + include_context "with constant modules" + + # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version of these models, so we can use fast_spec_helper. + let(:logger) { instance_double("Logger", debug: nil) } + let(:agent) { instance_double("Clusters::Agent", id: 991) } + + 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: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(:image_pull_secret_stringified) { { "name" => "registry-secret", "namespace" => "default" } } + let(:image_pull_secret_symbolized) { { name: "registry-secret", namespace: "default" } } + let(:image_pull_secret) { image_pull_secret_stringified } + let(:shared_namespace) { "" } + let(:use_kubernetes_user_namespaces) { false } + let(:workspace_namespace) { "gl-rd-ns-991-990-fedcba" } + + let(:workspaces_agent_config) do + instance_double( + "RemoteDevelopment::WorkspacesAgentConfig", + allow_privilege_escalation: false, + use_kubernetes_user_namespaces: use_kubernetes_user_namespaces, + default_runtime_class: "standard", + default_resources_per_workspace_container: + { requests: { cpu: "0.5", memory: "512Mi" }, limits: { cpu: "1", memory: "1Gi" } }, + # NOTE: This input version of max_resources_per_workspace is deeply UNSORTED, to verify the legacy behavior + # that the "workspaces.gitlab.com/max-resources-per-workspace-sha256" annotation should be calculated + # from the OpenSSL::Digest::SHA256.hexdigest of the #to_s of the SHALLOW sorted version of the hash. In other + # words, the hash is calculated with limits and requests in alphabetical order, but not memory and cpu. + max_resources_per_workspace: { requests: { memory: "1Gi", cpu: "1" }, limits: { memory: "4Gi", cpu: "2" } }, + annotations: { environment: "production", team: "engineering" }, + labels: { app: "workspace", tier: "development" }, + image_pull_secrets: [image_pull_secret], + network_policy_enabled: true, + network_policy_egress: [ + { + allow: "0.0.0.0/0", + except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16] + } + ], + gitlab_workspaces_proxy_namespace: "gitlab-workspaces", + dns_zone: "workspaces.localdev.me", + shared_namespace: shared_namespace + ) + end + + let(:input_processed_devfile_yaml) { input_processed_devfile_yaml_with_poststart_event } + + let(:workspace) do + instance_double( + "RemoteDevelopment::Workspace", + id: 993, + agent: agent, + workspaces_agent_config: workspaces_agent_config, + name: "workspace-991-990-fedcba", + namespace: workspace_namespace, + desired_state_running?: desired_state_running, + actual_state: states_module::RUNNING, + workspace_variables: workspace_variables, + processed_devfile: input_processed_devfile_yaml + ) + end + # rubocop:enable RSpec/VerifiedDoubleReference + + subject(:context) do + # noinspection RubyMismatchedArgumentType -- We are intentionally passing a double for Workspace + described_class.main( + params: { + agent: agent + }, + workspace: workspace, + logger: logger + ) + end + + shared_examples "generated desired_config checks" do + it "exactly matches the generated desired_config", :unlimited_max_formatted_output_length do + actual_desired_config = context[:desired_config] + expect(actual_desired_config).to be_a(::RemoteDevelopment::WorkspaceOperations::DesiredConfig) + expect(actual_desired_config).to be_valid + + actual_desired_config_array = + actual_desired_config + .attributes + .fetch("desired_config_array") + .map(&:deep_symbolize_keys) + + expected_desired_config_array_sorted = expected_desired_config_array.map(&:deep_symbolize_keys) + + # compare the names and kinds of the resources + expect(actual_desired_config_array.map { |c| [c.fetch(:kind), c.fetch(:metadata).fetch(:name)] }) + .to eq(expected_desired_config_array_sorted.map { |c| [c.fetch(:kind), c.fetch(:metadata).fetch(:name)] }) + + differences = {} + + # Use Hashdiff on each element to give a more concise and readable diff + actual_desired_config_array.each_with_index do |resource, index| + # NOTE: The order of the diff is expected value first, actual value second. This matches the + # "expected ..., got ..." order which RSpec uses by default. + resource_differences = Hashdiff.diff(expected_desired_config_array_sorted[index], resource, use_lcs: false) + + next unless resource_differences.present? + + key = "index=#{index}, kind='#{resource.fetch(:kind)}', name='#{resource.fetch(:metadata).fetch(:name)}'" + value = resource_differences.map(&:inspect).join("\n").to_s + differences[key] = value + end + + expect(differences) + .to be_empty, differences.map { |k, v| "Differences found in resource at #{k} \n#{v}" }.join("\n\n").to_s + end + end + + context "when desired_state is stopped" do + let(:desired_state_running) { false } + let(:expected_desired_config_array) { expected_desired_config_array_with_desired_state_stopped } + + it_behaves_like "generated desired_config checks" + + context "with shared namespace set" do + let(:shared_namespace) { "default" } + let(:workspace_namespace) { shared_namespace } + let(:expected_desired_config_array) do + expected_desired_config_array_for_shared_namespace_with_desired_state_stopped + end + + it_behaves_like "generated desired_config checks" + end + end + + context "when desired_state is running" do + let(:desired_state_running) { true } + let(:expected_desired_config_array) { expected_desired_config_array_with_desired_state_running } + + it_behaves_like "generated desired_config checks" + + context "with legacy devfile that includes postStart event" do + let(:input_processed_devfile_yaml) { input_processed_devfile_yaml_with_legacy_poststart_event } + let(:expected_desired_config_array) do + expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_desired_state_running + end + + it_behaves_like "generated desired_config checks" + end + + context "with legacy devfile that does not include postStart event" do + let(:input_processed_devfile_yaml) { input_processed_devfile_yaml_without_poststart_event } + let(:expected_desired_config_array) do + expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with_desired_state_running + end + + it_behaves_like "generated desired_config checks" + end + + context "with shared namespace set" do + let(:shared_namespace) { "default" } + let(:workspace_namespace) { shared_namespace } + let(:expected_desired_config_array) do + expected_desired_config_array_for_shared_namespace_with_desired_state_running + end + + it_behaves_like "generated desired_config checks" + end + + context "with use_kubernetes_user_namespaces set" do + let(:use_kubernetes_user_namespaces) { true } + let(:expected_desired_config_array) do + expected_desired_config_array_with_desired_state_running_with_use_kubernetes_user_namespaces_set + end + + it_behaves_like "generated desired_config checks" + end + end + + # @return [String] + def input_processed_devfile_yaml_with_poststart_event + <<~YAML + --- + schemaVersion: 2.2.0 + metadata: {} + components: + - name: tooling-container + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo + args: + - "echo 'tooling container args'" + command: + - "/bin/sh" + - "-c" + volumeMounts: + - name: gl-workspace-data + path: /projects + env: + - name: GL_ENV_NAME + value: "gl-env-value" + endpoints: + - name: server + targetPort: 60001 + exposure: public + secure: true + protocol: https + dedicatedPod: false + mountSources: true + - name: sidecar-container + container: + image: "sidecar-container:latest" + volumeMounts: + - name: gl-workspace-data + path: "/projects" + env: + - name: GL_ENV2_NAME + value: "gl-env2-value" + args: + - "echo 'sidecar container args'" + command: + - "/bin/sh" + - "-c" + memoryLimit: 1000Mi + memoryRequest: 500Mi + cpuLimit: 500m + cpuRequest: 100m + - name: gl-workspace-data + volume: + size: 50Gi + commands: + - id: gl-internal-example-command-1 + exec: + commandLine: "echo 'gl-internal-example-command-1'" + component: tooling-container + label: gl-internal-blocking + - id: gl-internal-example-command-2 + exec: + commandLine: "echo 'gl-internal-example-command-2'" + component: tooling-container + - id: example-prestart-apply-command + apply: + component: sidecar-container + events: + preStart: + - example-prestart-apply-command + postStart: + - gl-internal-example-command-1 + - gl-internal-example-command-2 + variables: {} + YAML + end + + # @return [String] + def input_processed_devfile_yaml_with_legacy_poststart_event + <<~YAML + --- + schemaVersion: 2.2.0 + metadata: {} + components: + - name: tooling-container + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo + args: + - "echo 'tooling container args'" + command: + - "/bin/sh" + - "-c" + volumeMounts: + - name: gl-workspace-data + path: /projects + env: + - name: GL_ENV_NAME + value: "gl-env-value" + endpoints: + - name: server + targetPort: 60001 + exposure: public + secure: true + protocol: https + dedicatedPod: false + mountSources: true + - name: sidecar-container + container: + image: "sidecar-container:latest" + volumeMounts: + - name: gl-workspace-data + path: "/projects" + env: + - name: GL_ENV2_NAME + value: "gl-env2-value" + args: + - "echo 'sidecar container args'" + command: + - "/bin/sh" + - "-c" + memoryLimit: 1000Mi + memoryRequest: 500Mi + cpuLimit: 500m + cpuRequest: 100m + - name: gl-workspace-data + volume: + size: 50Gi + commands: + - id: gl-internal-example-command-1 + exec: + commandLine: "echo 'gl-internal-example-command-1'" + component: tooling-container + - id: gl-internal-example-command-2 + exec: + commandLine: "echo 'gl-internal-example-command-2'" + component: tooling-container + - id: example-prestart-apply-command + apply: + component: sidecar-container + events: + preStart: + - example-prestart-apply-command + postStart: + - gl-internal-example-command-1 + - gl-internal-example-command-2 + variables: {} + YAML + end + + # @return [String] + def input_processed_devfile_yaml_without_poststart_event + <<~YAML + --- + schemaVersion: 2.2.0 + metadata: {} + components: + - name: tooling-container + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo + args: + - "echo 'tooling container args'" + command: + - "/bin/sh" + - "-c" + volumeMounts: + - name: gl-workspace-data + path: /projects + env: + - name: GL_ENV_NAME + value: "gl-env-value" + endpoints: + - name: server + targetPort: 60001 + exposure: public + secure: true + protocol: https + dedicatedPod: false + mountSources: true + - name: gl-project-cloner + container: + image: alpine/git:2.45.2 + volumeMounts: + - name: gl-workspace-data + path: "/projects" + args: + - "echo 'project cloner container args'" + command: + - "/bin/sh" + - "-c" + memoryLimit: 1000Mi + memoryRequest: 500Mi + cpuLimit: 500m + cpuRequest: 100m + - name: gl-workspace-data + volume: + size: 50Gi + commands: + - id: gl-project-cloner-command + apply: + component: gl-project-cloner + events: + preStart: + - gl-project-cloner-command + variables: {} + YAML + end + + # rubocop:disable Layout/LineLength, Style/WordArray -- Keep original formatting for readability + # noinspection RubyLiteralArrayInspection + # @return [Array] + def expected_desired_config_array_with_desired_state_running + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: { + "gl-run-internal-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\n", + "gl-run-non-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba-file", + namespace: "gl-rd-ns-991-990-fedcba" + } + } + ] + end + + # noinspection RubyLiteralArrayInspection + # @return [Array] + def expected_desired_config_array_with_desired_state_running_with_use_kubernetes_user_namespaces_set + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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", + hostUsers: true, + volumes: [ + { + name: "gl-workspace-data", + persistentVolumeClaim: { + claimName: "workspace-991-990-fedcba-gl-workspace-data" + } + }, + { + name: "gl-workspace-variables", + projected: { + defaultMode: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: { + "gl-run-internal-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\n", + "gl-run-non-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba-file", + namespace: "gl-rd-ns-991-990-fedcba" + } + } + ] + end + + # @return [Array] + def expected_desired_config_array_with_desired_state_stopped + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba", + namespace: "gl-rd-ns-991-990-fedcba" + }, + spec: { + replicas: 0, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: { + "gl-run-internal-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\n", + "gl-run-non-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba-file", + namespace: "gl-rd-ns-991-990-fedcba" + } + } + ] + end + + # @return [Array] + def expected_desired_config_array_from_legacy_devfile_with_poststart_and_with_desired_state_running + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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\"/workspace-scripts/gl-run-poststart-commands.sh\" 1>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log\" 2>>\"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log\"\n" + ] + } + } + } + } + ], + initContainers: [ + { + args: [ + "echo 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: { + "gl-run-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba-file", + namespace: "gl-rd-ns-991-990-fedcba" + } + } + ] + end + + # @return [Array] + def expected_desired_config_array_from_legacy_devfile_with_no_poststart_and_with_desired_state_running + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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" + } + ] + } + ], + 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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + } + ] + } + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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", + "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" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + 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: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991" + }, + name: "workspace-991-990-fedcba-file", + namespace: "gl-rd-ns-991-990-fedcba" + } + } + ] + end + + # @return [Array] + def expected_desired_config_array_for_shared_namespace_with_desired_state_running + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-workspace-inventory", + namespace: "default" + } + }, + { + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + 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 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + spec: { + ports: [ + { + name: "server", + port: 60001, + targetPort: 60001 + } + ], + selector: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-gl-workspace-data", + namespace: "default" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + } + }, + { + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + 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: { + matchLabels: { + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-scripts-configmap", + namespace: "default" + }, + data: { + "gl-run-internal-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\n", + "gl-run-non-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-secrets-inventory", + namespace: "default" + } + }, + { + apiVersion: "v1", + data: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-env-var", + namespace: "default" + } + }, + { + apiVersion: "v1", + data: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-file", + namespace: "default" + } + } + ] + end + + # @return [Array] + def expected_desired_config_array_for_shared_namespace_with_desired_state_stopped + [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-workspace-inventory", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-workspace-inventory", + namespace: "default" + } + }, + { + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + spec: { + replicas: 0, + selector: { + matchLabels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + 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 'sidecar container args'" + ], + command: [ + "/bin/sh", + "-c" + ], + env: [ + { + name: "GL_ENV2_NAME", + value: "gl-env2-value" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + envFrom: [ + { + secretRef: { + name: "workspace-991-990-fedcba-env-var" + } + } + ], + image: "sidecar-container:latest", + imagePullPolicy: "Always", + name: "sidecar-container-example-prestart-apply-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: 0o774, + sources: [ + { + secret: { + name: "workspace-991-990-fedcba-file" + } + } + ] + } + }, + { + name: "gl-workspace-scripts", + projected: { + defaultMode: 0o555, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + spec: { + ports: [ + { + name: "server", + port: 60001, + targetPort: 60001 + } + ], + selector: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + creationTimestamp: nil, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-gl-workspace-data", + namespace: "default" + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + } + }, + { + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba", + namespace: "default" + }, + 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: { + matchLabels: { + "workspaces.gitlab.com/id": "993" + } + }, + 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": "{{.port}}-workspace-991-990-fedcba.workspaces.localdev.me", + "workspaces.gitlab.com/id": "993", + "workspaces.gitlab.com/max-resources-per-workspace-sha256": "06879e20c353a4d871fb360635f6a87483987d44953ac6384af0451e8faa47ca" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-scripts-configmap", + namespace: "default" + }, + data: { + "gl-run-internal-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-1...\"\n/workspace-scripts/gl-internal-example-command-1 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-1.\"\n", + "gl-run-non-blocking-poststart-commands.sh": "#!/bin/sh\necho \"$(date -Iseconds): ----------------------------------------\"\necho \"$(date -Iseconds): Running /workspace-scripts/gl-internal-example-command-2...\"\n/workspace-scripts/gl-internal-example-command-2 || true\necho \"$(date -Iseconds): Finished running /workspace-scripts/gl-internal-example-command-2.\"\n", + "gl-internal-example-command-1": "echo 'gl-internal-example-command-1'", + "gl-internal-example-command-2": "echo 'gl-internal-example-command-2'" + } + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + annotations: { + environment: "production", + 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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "cli-utils.sigs.k8s.io/inventory-id": "workspace-991-990-fedcba-secrets-inventory", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-secrets-inventory", + namespace: "default" + } + }, + { + apiVersion: "v1", + data: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-env-var", + namespace: "default" + } + }, + { + apiVersion: "v1", + data: {}, + kind: "Secret", + metadata: { + annotations: { + environment: "production", + team: "engineering", + "config.k8s.io/owning-inventory": "workspace-991-990-fedcba-secrets-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" + }, + labels: { + app: "workspace", + tier: "development", + "agent.gitlab.com/id": "991", + "workspaces.gitlab.com/id": "993" + }, + name: "workspace-991-990-fedcba-file", + namespace: "default" + } + } + ] + end + + # rubocop:enable Layout/LineLength, Style/WordArray +end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_spec.rb new file mode 100644 index 00000000000000..e73f7321de5438 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/main_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +# noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot +RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::Main, :freeze_time, feature_category: :workspaces do + let(:rop_steps) do + [ + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::ConfigValuesExtractor, :map], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileParserGetter, :map], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DesiredConfigYamlParser, :map], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileResourceModifier, :map], + [RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileResourceAppender, :map] + ] + end + + let(:workspace_agent) { instance_double("Clusters::Agent", id: 1) } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + let(:workspaces_agent_config) { instance_double("RemoteDevelopment::WorkspacesAgentConfig") } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + let(:workspace) do + instance_double( + "RemoteDevelopment::Workspace", # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + id: 1, + name: "workspace-name", + namespace: "workspace-namespace", + workspaces_agent_config: workspaces_agent_config, + desired_state_running?: true, + processed_devfile: "---") + end + + let(:logger) { instance_double("RemoteDevelopment::Logger") } # rubocop:disable RSpec/VerifiedDoubleReference -- We're using the quoted version so we can use fast_spec_helper + + let(:parent_context) do + { + workspace: workspace, + logger: logger, + params: { + agent: workspace_agent + } + } + end + + let(:context_passed_along_steps) do + { + workspace_id: parent_context[:workspace].id, + workspace_name: parent_context[:workspace].name, + workspace_namespace: parent_context[:workspace].namespace, + workspace_desired_state_is_running: true, + workspaces_agent_id: 1, + workspaces_agent_config: workspaces_agent_config, + processed_devfile_yaml: parent_context[:workspace].processed_devfile, + logger: logger, + desired_config_array: [] + } + end + + let(:desired_config_array) { [] } + + describe "happy path" do + let(:expected_value) do + parent_context.merge( + desired_config: RemoteDevelopment::WorkspaceOperations::DesiredConfig.new( + desired_config_array: desired_config_array + ) + ) + end + + it "returns expected response" do + # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31542 + expect do + described_class.main(parent_context) + end + .to invoke_rop_steps(rop_steps) + .from_main_class(described_class) + .with_context_passed_along_steps(context_passed_along_steps) + .and_return_expected_value(expected_value) + end + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender_spec.rb index a9dfe811fe2779..de00abd60df229 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/desired_config/scripts_configmap_appender_spec.rb @@ -16,12 +16,12 @@ subject(:updated_desired_config) do # Make a fake desired config with one existing fake element, to prove we are appending - desired_config = [ + desired_config_array = [ {} ] described_class.append( - desired_config: desired_config, + desired_config_array: desired_config_array, name: name, namespace: namespace, labels: labels, @@ -30,10 +30,10 @@ devfile_events: devfile_events ) - desired_config + desired_config_array end - it "appends ConfigMap to desired_config" do + it "appends ConfigMap to desired_config_array" do expect(updated_desired_config.length).to eq(2) updated_desired_config => [ @@ -71,7 +71,7 @@ ) end - it "appends ConfigMap to desired_config" do + it "appends ConfigMap to desired_config_array" do expect(updated_desired_config.length).to eq(2) updated_desired_config => [ 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 3fdc9cebe61a9a..23dbaef0332175 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 @@ -50,6 +50,7 @@ let(:devfile_fixture_name) { 'example.devfile.yaml.erb' } let(:devfile_yaml) { read_devfile_yaml(devfile_fixture_name) } let(:expected_processed_devfile) { example_processed_devfile } + let(:logger) { instance_double(Logger) } let(:variables) do [ { key: 'VAR1', value: 'value 1', type: 'ENVIRONMENT' }, @@ -117,7 +118,8 @@ internal_events_class: Gitlab::InternalEvents, settings: settings, vscode_extension_marketplace: vscode_extension_marketplace, - vscode_extension_marketplace_metadata: { enabled: vscode_extension_marketplace_metadata_enabled } + vscode_extension_marketplace_metadata: { enabled: vscode_extension_marketplace_metadata_enabled }, + logger: logger } end @@ -183,6 +185,9 @@ ).first&.value ).to eq(variable[:value]) end + + expect(workspace.agentk_state).to be_present + expect(workspace.agentk_state.desired_config.fetch("desired_config_array")).to be_an(Array) 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 new file mode 100644 index 00000000000000..26b20d9282c273 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/create/workspace_agentk_state_creator_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +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' + + let(:workspace_name) { "workspace-991-990-fedcba" } + let(:workspace) { create(:workspace, name: workspace_name) } + let(:expected_desired_config_json) { desired_config.as_json } + let(:logger) { instance_double(RemoteDevelopment::Logger) } + let(:desired_config) do + ::RemoteDevelopment::WorkspaceOperations::DesiredConfig.new(desired_config_array: create_desired_config_array) + end + + let(:context) do + { + desired_config: desired_config, + workspace: workspace, + logger: logger + } + end + + subject(:result) do + described_class.create(context) # rubocop:disable Rails/SaveBang -- This is not an ActiveRecord method + end + + it 'persists the record and returns nil' do + expect { result }.to change { RemoteDevelopment::WorkspaceAgentkState.count } + + expect(RemoteDevelopment::WorkspaceAgentkState.last) + .to have_attributes( + desired_config: expected_desired_config_json, + workspace_id: workspace.id, + project_id: workspace.project.id + ) + + expect(result).to eq(Gitlab::Fp::Result.ok(context)) + end + + context "when there are errors persisting the record" do + it "returns wraps the database error in the Fp::Result error" do + fake_errors = instance_double(ActiveModel::Errors, present?: true) + fake_state = instance_double(RemoteDevelopment::WorkspaceAgentkState, errors: fake_errors) + allow(RemoteDevelopment::WorkspaceAgentkState).to receive(:create!).and_return(fake_state) + + expect(result).to eq( + Gitlab::Fp::Result.err( + RemoteDevelopment::Messages::WorkspaceAgentkStateCreateFailed.new({ errors: fake_errors, context: context }) + ) + ) + end + end + + context "when desired_config has errors" do + before do + allow(logger).to receive(:error) + allow(desired_config).to receive(:valid?).and_return(false) + allow(desired_config).to( + receive_message_chain(:errors, :full_messages) + .and_return(["cannot be nil", "has some issue"]) + ) + end + + it "logs an error message" do + result + + expect(desired_config).to have_received(:valid?) + expect(logger).to have_received(:error).with( + hash_including( + message: "desired_config is invalid", + error_type: "workspace_agentk_state_error", + workspace_id: workspace.id, + validation_error: ["cannot be nil", "has some issue"] + ) + ) + end + end +end 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 2dcc8d42d86706..ee6a91ba9b0293 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 @@ -64,7 +64,10 @@ describe '#to_json' do let(:desired_config_array) { create_desired_config_array } - it { expect(desired_config.to_json).to be_valid_json } + subject(:result) { desired_config.to_json } + + it { expect(result).to be_valid_json } + it { expect(Gitlab::Json.parse(result)).to eq({ "desired_config_array" => create_desired_config_array }.as_json) } end describe '#==(other)' do diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor_spec.rb new file mode 100644 index 00000000000000..0ee77aee7ec278 --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_config_values_extractor_spec.rb @@ -0,0 +1,269 @@ +# frozen_string_literal: true + +require "spec_helper" + +# noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldConfigValuesExtractor, feature_category: :workspaces do + include_context "with constant modules" + + let_it_be(:user) { create(:user) } + let_it_be(:agent, reload: true) { create(:ee_cluster_agent) } + let_it_be(:workspace_name) { "workspace-name" } + let(:desired_state) { states_module::STOPPED } + let_it_be(:actual_state) { states_module::STOPPED } + let_it_be(:dns_zone) { "my.dns-zone.me" } + let_it_be(:labels) { { "some-label": "value", "other-label": "other-value" } } + let_it_be(:started) { true } + let_it_be(:include_all_resources) { false } + let_it_be(:network_policy_enabled) { true } + let_it_be(:gitlab_workspaces_proxy_namespace) { "gitlab-workspaces" } + let_it_be(:image_pull_secrets) { [{ namespace: "default", name: "secret-name" }] } + let_it_be(:agent_annotations) { { "some/annotation": "value" } } + let_it_be(:shared_namespace) { "" } + let_it_be(:network_policy_egress) do + [ + { + except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16], + allow: "0.0.0.0/0" + } + ] + end + + let_it_be(:max_resources_per_workspace) do + { + requests: { + memory: "512Mi", + cpu: "0.6" + }, + limits: { + memory: "786Mi", + cpu: "1.5" + } + } + end + + let_it_be(:default_resources_per_workspace_container) do + { + requests: { + memory: "600Mi", + cpu: "0.5" + }, + limits: { + memory: "700Mi", + cpu: "1.0" + } + } + end + + let_it_be(:workspaces_agent_config) do + config = create( + :workspaces_agent_config, + agent: agent, + dns_zone: dns_zone, + image_pull_secrets: image_pull_secrets, + network_policy_enabled: network_policy_enabled, + # NOTE: We are stringifying all hashes we set here to ensure that the extracted values are converted to symbols + default_resources_per_workspace_container: default_resources_per_workspace_container.deep_stringify_keys, + max_resources_per_workspace: max_resources_per_workspace.deep_stringify_keys, + labels: labels.deep_stringify_keys, + annotations: agent_annotations.deep_stringify_keys, + network_policy_egress: network_policy_egress.map(&:deep_stringify_keys), + shared_namespace: shared_namespace + ) + agent.reload + config + end + + let_it_be(:workspace) do + workspaces_agent_config + create( + :workspace, + name: workspace_name, + agent: agent, + user: user, + actual_state: actual_state + ) + end + + let_it_be(:deployment_resource_version_from_agent) { workspace.deployment_resource_version } + + subject(:extractor) { described_class } + + before do + workspace.update!(desired_state: desired_state) + end + + it "extracts the config values" do + extracted_values = extractor.extract(workspace: workspace) + expect(extracted_values).to be_a(Hash) + expect(extracted_values.keys) + .to eq( + %i[ + allow_privilege_escalation + common_annotations + default_resources_per_workspace_container + default_runtime_class + domain_template + env_secret_name + file_secret_name + gitlab_workspaces_proxy_namespace + image_pull_secrets + labels + max_resources_per_workspace + network_policy_enabled + network_policy_egress + processed_devfile_yaml + replicas + scripts_configmap_name + secrets_inventory_annotations + secrets_inventory_name + shared_namespace + use_kubernetes_user_namespaces + workspace_inventory_annotations + workspace_inventory_name + ] + ) + + # NOTE: We don't explicitly test the values that are returned directly from the agent config without processing. + # Those are covered by the desired_config_generator tests. + + expect(extracted_values[:common_annotations]).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/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") + + expect(extracted_values[:file_secret_name]).to eq("#{workspace.name}-file") + + expect(extracted_values[:image_pull_secrets]).to eq([{ name: "secret-name", namespace: "default" }]) + + expect(extracted_values[:gitlab_workspaces_proxy_namespace]).to eq("gitlab-workspaces") + + expect(extracted_values[:labels]).to eq( + { + "agent.gitlab.com/id": agent.id.to_s, + "other-label": "other-value", + "some-label": "value" + } + ) + + expect(extracted_values[:network_policy_enabled]).to be(true) + + expect(extracted_values[:network_policy_egress]) + .to eq([{ allow: "0.0.0.0/0", except: %w[10.0.0.0/8 172.16.0.0/12 192.168.0.0/16] }]) + + expect(extracted_values[:max_resources_per_workspace]).to eq(max_resources_per_workspace) + + expect(extracted_values[:processed_devfile_yaml]).to eq(workspace.processed_devfile) + + expect(extracted_values[:scripts_configmap_name]).to eq("#{workspace.name}-scripts-configmap") + + expect(extracted_values[:secrets_inventory_annotations]).to eq( + { + "config.k8s.io/owning-inventory": "#{workspace.name}-secrets-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/max-resources-per-workspace-sha256": + "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" + } + ) + + expect(extracted_values[:secrets_inventory_name]).to eq("#{workspace.name}-secrets-inventory") + + expect(extracted_values[:workspace_inventory_annotations]).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/max-resources-per-workspace-sha256": + "e3dd9c9741b2b3f07cfd341f80ea3a9d4b5a09b29e748cf09b546e93ff98241c" + } + ) + + expect(extracted_values[:workspace_inventory_name]).to eq("#{workspace.name}-workspace-inventory") + end + + describe "devfile_parser_params[:replicas]" do + subject(:replicas) { extractor.extract(workspace: workspace).fetch(:replicas) } + + context "when desired_state is Running" do + let(:desired_state) { states_module::RUNNING } + let(:expected_replicas) { 1 } + + it { is_expected.to eq(expected_replicas) } + end + + context "when desired_state is not CreationRequested nor Running" do + let(:desired_state) { states_module::STOPPED } + let(:expected_replicas) { 0 } + + it { is_expected.to eq(expected_replicas) } + end + end + + describe "devfile_parser_params[:labels]" do + subject(:actual_labels) { extractor.extract(workspace: workspace).fetch(:labels) } + + context "when shared_namespace is not set" do + let(:expected_labels) do + { + "agent.gitlab.com/id": agent.id.to_s, + "other-label": "other-value", + "some-label": "value" + } + end + + it { is_expected.to eq(expected_labels) } + end + + context "when shared_namespace is set" do + let_it_be(:shared_namespace) { "default" } + let_it_be(:workspace_name) { "workspace-name-shared-namespace" } + let_it_be(:agent, reload: true) { create(:ee_cluster_agent) } + let_it_be(:workspaces_agent_config) do + config = create( + :workspaces_agent_config, + agent: agent, + dns_zone: dns_zone, + labels: labels.deep_stringify_keys, + shared_namespace: shared_namespace + ) + agent.reload + config + end + + let_it_be(:workspace) do + workspaces_agent_config + create( + :workspace, + name: workspace_name, + agent: agent, + user: user, + actual_state: actual_state + ) + end + + let(:expected_labels) do + { + "agent.gitlab.com/id": agent.id.to_s, + "other-label": "other-value", + "some-label": "value", + "workspaces.gitlab.com/id": workspace.id.to_s + } + end + + it { is_expected.to eq(expected_labels) } + end + end +end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_golden_master_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_golden_master_spec.rb similarity index 99% rename from ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_golden_master_spec.rb rename to ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_golden_master_spec.rb index d976bf2564938e..310580cedd628e 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_golden_master_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_golden_master_spec.rb @@ -7,7 +7,7 @@ require "hashdiff" # noinspection RubyLiteralArrayInspection -- Keep original formatting for readability -RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigGenerator, "Golden Master for desired_config", feature_category: :workspaces do +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator, "Golden Master for desired_config", feature_category: :workspaces do # !!!! IMPORTANT NOTE !!!! # DO NOT ADD # include_context "with constant modules" @@ -426,9 +426,9 @@ def input_processed_devfile_yaml_without_poststart_event YAML end - # @return [Array] # rubocop:disable Layout/LineLength, Style/WordArray -- Keep original formatting for readability # noinspection RubyLiteralArrayInspection + # @return [Array] def golden_master_desired_config_with_desired_state_terminated [ { diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb similarity index 98% rename from ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb rename to ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb index 0a3fef9771c8da..2e9a204e93abbb 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_desired_config_generator_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" # noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot -RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigGenerator, :freeze_time, feature_category: :workspaces do +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDesiredConfigGenerator, :freeze_time, feature_category: :workspaces do include_context "with remote development shared fixtures" RSpec.shared_examples "includes env and file secrets if the secrets-inventory configmap is present" do @@ -361,7 +361,7 @@ context "when DevfileParser returns empty array" do before do # rubocop:todo Layout/LineLength -- this line will not be too long once we rename RemoteDevelopment namespace to Workspaces - allow(RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileParser).to receive(:get_all).and_return([]) + allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDevfileParser).to receive(:get_all).and_return([]) # rubocop:enable Layout/LineLength end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser_spec.rb similarity index 98% rename from ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_spec.rb rename to ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser_spec.rb index c64427eb31165a..27dfe74861cceb 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/desired_config/devfile_parser_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_devfile_parser_spec.rb @@ -2,7 +2,7 @@ require "fast_spec_helper" -RSpec.describe RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::DevfileParser, feature_category: :workspaces do +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldDevfileParser, feature_category: :workspaces do include_context 'with remote development shared fixtures' let(:dns_zone) { "workspaces.localdev.me" } diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender_spec.rb new file mode 100644 index 00000000000000..775331d34f8d2b --- /dev/null +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/old_scripts_configmap_appender_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "fast_spec_helper" + +RSpec.describe RemoteDevelopment::WorkspaceOperations::Reconcile::Output::OldScriptsConfigmapAppender, feature_category: :workspaces do + include_context 'with remote development shared fixtures' + + let(:files) { RemoteDevelopment::Files } + let(:annotations) { { a: "1" } } + let(:labels) { { b: "2" } } + let(:name) { "workspacename-scripts-configmap" } + let(:namespace) { "namespace" } + let(:processed_devfile) { example_processed_devfile } + let(:devfile_commands) { processed_devfile.fetch(:commands) } + let(:devfile_events) { processed_devfile.fetch(:events) } + + subject(:updated_desired_config) do + # Make a fake desired config with one existing fake element, to prove we are appending + desired_config = [ + {} + ] + + described_class.append( + desired_config: desired_config, + name: name, + namespace: namespace, + labels: labels, + annotations: annotations, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + desired_config + end + + it "appends ConfigMap to desired_config" do + expect(updated_desired_config.length).to eq(2) + + updated_desired_config => [ + {}, # existing fake element + { + apiVersion: api_version, + metadata: { + name: configmap_name + }, + data: data + }, + ] + + expect(api_version).to eq("v1") + expect(configmap_name).to eq(name) + expect(data).to eq( + "gl-clone-project-command": clone_project_script, + "gl-clone-unshallow-command": clone_unshallow_script, + "gl-init-tools-command": files::INTERNAL_POSTSTART_COMMAND_START_VSCODE_SCRIPT, + create_constants_module::RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => + internal_blocking_poststart_commands_script, + create_constants_module::RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => + non_blocking_poststart_commands_script(user_command_ids: ["user-defined-command"]), + "gl-sleep-until-container-is-running-command": + sleep_until_container_is_running_script, + "gl-start-sshd-command": files::INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT, + "user-defined-command": "echo 'user-defined postStart command'" + ) + end + + context "when legacy poststart scripts are used" do + let(:processed_devfile) do + yaml_safe_load_symbolized( + read_devfile_yaml("example.legacy-poststart-in-container-command-processed-devfile.yaml.erb") + ) + end + + it "appends ConfigMap to desired_config" do + expect(updated_desired_config.length).to eq(2) + + updated_desired_config => [ + {}, # existing fake element + { + apiVersion: api_version, + metadata: { + name: configmap_name + }, + data: data + }, + ] + + expect(api_version).to eq("v1") + expect(configmap_name).to eq(name) + expect(data).to eq( + "gl-clone-project-command": clone_project_script, + "gl-init-tools-command": files::INTERNAL_POSTSTART_COMMAND_START_VSCODE_SCRIPT, + create_constants_module::LEGACY_RUN_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => + legacy_poststart_commands_script, + "gl-sleep-until-container-is-running-command": + sleep_until_container_is_running_script, + "gl-start-sshd-command": files::INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT + ) + 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 edb91d14748c1d..787d855942f38c 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 @@ -125,7 +125,7 @@ let(:desired_config_generator_version) { current_desired_config_generator_version } before do - allow(RemoteDevelopment::WorkspaceOperations::Reconcile::Output::DesiredConfigGenerator) + 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 diff --git a/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb b/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb index d575399d3867dd..04c2c232335005 100644 --- a/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb +++ b/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb @@ -7,10 +7,14 @@ # rubocop:todo Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Cleanup as part of https://gitlab.com/gitlab-org/gitlab/-/issues/421687 + # @return [String] + def create_desired_config_json + RemoteDevelopment::FixtureFileHelpers.read_fixture_file('example.desired_config.json') + end + # @return [Array] def create_desired_config_array - json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file('example.desired_config.json') - Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) + Gitlab::Json.parse(create_desired_config_json).map(&:deep_symbolize_keys) end # @param [RemoteDevelopment::Workspace] workspace -- GitLab