diff --git a/ee/lib/remote_development/workspaces/config_version.rb b/ee/lib/remote_development/workspaces/config_version.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad5c3641c11c7d5804355a75a957b7e93b2d5c17 --- /dev/null +++ b/ee/lib/remote_development/workspaces/config_version.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module Workspaces + module ConfigVersion + VERSION_1 = 1 + VERSION_2 = 2 + end + end +end diff --git a/ee/lib/remote_development/workspaces/create/project_cloner_component_injector.rb b/ee/lib/remote_development/workspaces/create/project_cloner_component_injector.rb index bb1f167f237d2145ecccfc173b75a015d43ac5fa..0554d563b3eda6314eaa6d7c6ec823a98c2c4198 100644 --- a/ee/lib/remote_development/workspaces/create/project_cloner_component_injector.rb +++ b/ee/lib/remote_development/workspaces/create/project_cloner_component_injector.rb @@ -42,9 +42,6 @@ def self.inject(value) if [ ! -d '#{clone_dir}' ]; then git clone --branch #{Shellwords.shellescape(project_ref)} #{Shellwords.shellescape(project_url)} #{Shellwords.shellescape(clone_dir)}; - cd #{Shellwords.shellescape(clone_dir)}; - git config user.name "${GIT_AUTHOR_NAME}"; - git config user.email "${GIT_AUTHOR_EMAIL}"; fi SH diff --git a/ee/lib/remote_development/workspaces/create/workspace_creator.rb b/ee/lib/remote_development/workspaces/create/workspace_creator.rb index 02f059b97e4cb04e854311c49a17320bcf3854bf..75558cd2ac5b35596844cfcbb082bbe33195ef7e 100644 --- a/ee/lib/remote_development/workspaces/create/workspace_creator.rb +++ b/ee/lib/remote_development/workspaces/create/workspace_creator.rb @@ -40,6 +40,7 @@ def self.create(value) workspace.processed_devfile = YAML.dump(processed_devfile.deep_stringify_keys) # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31542 workspace.actual_state = CREATION_REQUESTED + workspace.config_version = RemoteDevelopment::Workspaces::ConfigVersion::VERSION_2 workspace.url = URI::HTTPS.build({ host: workspace_host(workspace: workspace), query: { diff --git a/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator.rb b/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator.rb index 0d12d379e0f9fdb8d5bb77e13442b354281f3316..9adc15e6f1254ebd0fc41ee18eb08e3d18ea5f18 100644 --- a/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator.rb +++ b/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator.rb @@ -12,61 +12,124 @@ class DesiredConfigGenerator include States # @param [RemoteDevelopment::Workspaces::Workspace] workspace + # @param [Boolean] include_secrets # @param [RemoteDevelopment::Logger] logger # @return [Array] - def self.generate_desired_config(workspace:, logger:) - name = workspace.name - namespace = workspace.namespace - agent = workspace.agent - desired_state = workspace.desired_state - user = workspace.user - - domain_template = "{{.port}}-#{name}.#{workspace.dns_zone}" - - workspace_inventory_config_map, owning_inventory = - create_workspace_inventory_config_map(name: name, namespace: namespace, agent_id: agent.id) - replicas = get_workspace_replicas(desired_state: desired_state) + def self.generate_desired_config(workspace:, include_secrets:, logger:) + desired_config = [] + env_secret_name = "#{workspace.name}-env-var" + file_secret_name = "#{workspace.name}-file" + env_secret_names = [env_secret_name] + file_secret_names = [file_secret_name] + replicas = get_workspace_replicas(desired_state: workspace.desired_state) + domain_template = get_domain_template_annotation(name: workspace.name, dns_zone: workspace.dns_zone) + inventory_name = "#{workspace.name}-workspace-inventory" labels, annotations = get_labels_and_annotations( - agent_id: agent.id, - owning_inventory: owning_inventory, + agent_id: workspace.agent.id, domain_template: domain_template, + owning_inventory: inventory_name, workspace_id: workspace.id ) + k8s_inventory_for_workspace_core = get_inventory_config_map( + name: inventory_name, + namespace: workspace.namespace, + agent_id: workspace.agent.id + ) + # TODO: https://gitlab.com/groups/gitlab-org/-/epics/10461 - handle error - workspace_resources = DevfileParser.get_all( + k8s_resources_for_workspace_core = DevfileParser.get_all( processed_devfile: workspace.processed_devfile, - name: name, - namespace: namespace, + name: workspace.name, + namespace: workspace.namespace, replicas: replicas, domain_template: domain_template, labels: labels, annotations: annotations, - user: user, + env_secret_names: env_secret_names, + file_secret_names: file_secret_names, logger: logger ) # If we got no resources back from the devfile parser, this indicates some error was encountered in parsing # the processed_devfile. So we return an empty array which will result in no updates being applied by the # agent. We should not continue on and try to add anything else to the resources, as this would result # in an invalid configuration being applied to the cluster. - return [] if workspace_resources.empty? + return [] if k8s_resources_for_workspace_core.empty? - workspace_resources.insert(0, workspace_inventory_config_map) + desired_config.append(k8s_inventory_for_workspace_core, *k8s_resources_for_workspace_core) remote_development_agent_config = workspace.agent.remote_development_agent_config if remote_development_agent_config.network_policy_enabled gitlab_workspaces_proxy_namespace = remote_development_agent_config.gitlab_workspaces_proxy_namespace network_policy = get_network_policy( - name: name, - namespace: namespace, + name: workspace.name, + namespace: workspace.namespace, labels: labels, annotations: annotations, gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace ) - workspace_resources.append(network_policy) + desired_config.append(network_policy) + end + + if include_secrets + k8s_resources_for_secrets = get_k8s_resources_for_secrets( + workspace: workspace, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name + ) + desired_config.append(*k8s_resources_for_secrets) end - workspace_resources + desired_config + end + + # @param [RemoteDevelopment::Workspaces::Workspace] workspace + # @param [String] env_secret_name + # @param [String] file_secret_name + # @param [String] env_secret_name + # @param [String] file_secret_name + # @return [Array<(Hash)>] + def self.get_k8s_resources_for_secrets(workspace:, env_secret_name:, file_secret_name:) + inventory_name = "#{workspace.name}-secrets-inventory" + domain_template = get_domain_template_annotation(name: workspace.name, dns_zone: workspace.dns_zone) + labels, annotations = get_labels_and_annotations( + agent_id: workspace.agent.id, + domain_template: domain_template, + owning_inventory: inventory_name, + workspace_id: workspace.id + ) + + k8s_inventory = get_inventory_config_map( + name: inventory_name, + namespace: workspace.namespace, + agent_id: workspace.agent.id + ) + + data_for_env_var = workspace.workspace_variables.with_variable_type_env_var + data_for_env_var = data_for_env_var.each_with_object({}) do |workspace_variable, hash| + hash[workspace_variable.key] = workspace_variable.value + end + k8s_secret_for_env_var = get_secret( + name: env_secret_name, + namespace: workspace.namespace, + labels: labels, + annotations: annotations, + data: data_for_env_var + ) + + data_for_file = workspace.workspace_variables.with_variable_type_file + data_for_file = data_for_file.each_with_object({}) do |workspace_variable, hash| + hash[workspace_variable.key] = workspace_variable.value + end + k8s_secret_for_file = get_secret( + name: file_secret_name, + namespace: workspace.namespace, + labels: labels, + annotations: annotations, + data: data_for_file + ) + + [k8s_inventory, k8s_secret_for_env_var, k8s_secret_for_file] end # @param [String] desired_state @@ -83,30 +146,34 @@ def self.get_workspace_replicas(desired_state:) # @param [String] name # @param [String] namespace # @param [Integer] agent_id - # @return [Array(Hash, String (frozen))] - def self.create_workspace_inventory_config_map(name:, namespace:, agent_id:) - owning_inventory = "#{name}-workspace-inventory" - workspace_inventory_config_map = { + # @return [Hash] + def self.get_inventory_config_map(name:, namespace:, agent_id:) + { kind: 'ConfigMap', apiVersion: 'v1', metadata: { - name: owning_inventory, + name: name, namespace: namespace, labels: { - 'cli-utils.sigs.k8s.io/inventory-id': owning_inventory, + 'cli-utils.sigs.k8s.io/inventory-id': name, 'agent.gitlab.com/id': agent_id.to_s } } }.deep_stringify_keys.to_h - [workspace_inventory_config_map, owning_inventory] end # @param [Integer] agent_id - # @param [String] owning_inventory # @param [String] domain_template + # @param [String] owning_inventory + # @param [String] object_type # @param [Integer] workspace_id - # @return [Array<(Hash, Hash)>] - def self.get_labels_and_annotations(agent_id:, owning_inventory:, domain_template:, workspace_id:) + # @return [Array] + def self.get_labels_and_annotations( + agent_id:, + domain_template:, + owning_inventory:, + workspace_id: + ) labels = { 'agent.gitlab.com/id' => agent_id.to_s } @@ -118,6 +185,33 @@ def self.get_labels_and_annotations(agent_id:, owning_inventory:, domain_templat [labels, annotations] end + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @param [Hash] data + # @return [Hash] + def self.get_secret(name:, namespace:, labels:, annotations:, data:) + { + kind: 'Secret', + apiVersion: 'v1', + metadata: { + name: name, + namespace: namespace, + labels: labels, + annotations: annotations + }, + data: data.transform_values { |v| Base64.strict_encode64(v) } + }.deep_stringify_keys.to_h + end + + # @param [String] name + # @param [String] dns_zone + # @return [String] + def self.get_domain_template_annotation(name:, dns_zone:) + "{{.port}}-#{name}.#{dns_zone}" + end + # @param [String] name # @param [String] namespace # @param [Hash] labels diff --git a/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1.rb b/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1.rb new file mode 100644 index 0000000000000000000000000000000000000000..52bafe9aed8fe3ef56be750e68a597c016914e1d --- /dev/null +++ b/ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module Workspaces + module Reconcile + module Output + # rubocop:disable Layout/LineLength + # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31542 + # noinspection RubyClassMethodNamingConvention,RubyLocalVariableNamingConvention,RubyParameterNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ + # rubocop:enable Layout/LineLength + class DesiredConfigGeneratorPrev1 + include States + + # @param [RemoteDevelopment::Workspaces::Workspace] workspace + # @param [RemoteDevelopment::Logger] logger + # @return [Array] + def self.generate_desired_config(workspace:, logger:) + name = workspace.name + namespace = workspace.namespace + agent = workspace.agent + desired_state = workspace.desired_state + user = workspace.user + + domain_template = "{{.port}}-#{name}.#{workspace.dns_zone}" + + workspace_inventory_config_map, owning_inventory = + create_workspace_inventory_config_map(name: name, namespace: namespace, agent_id: agent.id) + replicas = get_workspace_replicas(desired_state: desired_state) + labels, annotations = get_labels_and_annotations( + agent_id: agent.id, + owning_inventory: owning_inventory, + domain_template: domain_template, + workspace_id: workspace.id + ) + + # TODO: https://gitlab.com/groups/gitlab-org/-/epics/10461 - handle error + workspace_resources = DevfileParserPrev1.get_all( + processed_devfile: workspace.processed_devfile, + name: name, + namespace: namespace, + replicas: replicas, + domain_template: domain_template, + labels: labels, + annotations: annotations, + user: user, + logger: logger + ) + # If we got no resources back from the devfile parser, this indicates some error was encountered in parsing + # the processed_devfile. So we return an empty array which will result in no updates being applied by the + # agent. We should not continue on and try to add anything else to the resources, as this would result + # in an invalid configuration being applied to the cluster. + return [] if workspace_resources.empty? + + workspace_resources.insert(0, workspace_inventory_config_map) + + remote_development_agent_config = workspace.agent.remote_development_agent_config + if remote_development_agent_config.network_policy_enabled + gitlab_workspaces_proxy_namespace = remote_development_agent_config.gitlab_workspaces_proxy_namespace + network_policy = get_network_policy( + name: name, + namespace: namespace, + labels: labels, + annotations: annotations, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace + ) + workspace_resources.append(network_policy) + end + + workspace_resources + end + + # @param [String] desired_state + # @return [Integer] + def self.get_workspace_replicas(desired_state:) + return 1 if [ + CREATION_REQUESTED, + RUNNING + ].include?(desired_state) + + 0 + end + + # @param [String] name + # @param [String] namespace + # @param [Integer] agent_id + # @return [Array(Hash, String (frozen))] + def self.create_workspace_inventory_config_map(name:, namespace:, agent_id:) + owning_inventory = "#{name}-workspace-inventory" + workspace_inventory_config_map = { + kind: 'ConfigMap', + apiVersion: 'v1', + metadata: { + name: owning_inventory, + namespace: namespace, + labels: { + 'cli-utils.sigs.k8s.io/inventory-id': owning_inventory, + 'agent.gitlab.com/id': agent_id.to_s + } + } + }.deep_stringify_keys.to_h + [workspace_inventory_config_map, owning_inventory] + end + + # @param [Integer] agent_id + # @param [String] owning_inventory + # @param [String] domain_template + # @param [Integer] workspace_id + # @return [Array<(Hash, Hash)>] + def self.get_labels_and_annotations(agent_id:, owning_inventory:, domain_template:, workspace_id:) + labels = { + 'agent.gitlab.com/id' => agent_id.to_s + } + annotations = { + 'config.k8s.io/owning-inventory' => owning_inventory.to_s, + 'workspaces.gitlab.com/host-template' => domain_template.to_s, + 'workspaces.gitlab.com/id' => workspace_id.to_s + } + [labels, annotations] + end + + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @param [string] gitlab_workspaces_proxy_namespace + # @return [Hash] + def self.get_network_policy(name:, namespace:, labels:, annotations:, gitlab_workspaces_proxy_namespace:) + policy_types = [ + - "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_except_cidr = [ + - "10.0.0.0/8", + - "172.16.0.0/12", + - "192.168.0.0/16" + ] + egress = [ + { to: [{ ipBlock: { cidr: "0.0.0.0/0", except: egress_except_cidr } }] }, + { + ports: [{ port: 53, protocol: "TCP" }, { port: 53, protocol: "UDP" }], + to: [{ namespaceSelector: kube_system_namespace_selector }] + } + ] + + { + apiVersion: "networking.k8s.io/v1", + kind: "NetworkPolicy", + metadata: { + annotations: annotations, + labels: labels, + name: name, + namespace: namespace + }, + spec: { + egress: egress, + ingress: ingress, + podSelector: {}, + policyTypes: policy_types + } + }.deep_stringify_keys.to_h + end + end + end + end + end +end diff --git a/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser.rb b/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser.rb index c239895a8e07407bbbf9729876358e8ec0840f02..a01caeb5af1356f459d4d277fe962e906a5344ef 100644 --- a/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser.rb +++ b/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser.rb @@ -8,6 +8,7 @@ module Reconcile module Output # noinspection RubyClassMethodNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ class DevfileParser + # rubocop:disable Metrics/ParameterLists # @param [String] processed_devfile # @param [String] name # @param [String] namespace @@ -15,10 +16,22 @@ class DevfileParser # @param [String] domain_template # @param [Hash] labels # @param [Hash] annotations - # @param [User] user + # @param [Array] env_secret_names + # @param [Array] file_secret_names # @param [RemoteDevelopment::Logger] logger # @return [Array] - def self.get_all(processed_devfile:, name:, namespace:, replicas:, domain_template:, labels:, annotations:, user:, logger:) # rubocop:disable Metrics/ParameterLists + def self.get_all( + processed_devfile:, + name:, + namespace:, + replicas:, + domain_template:, + labels:, + annotations:, + env_secret_names:, + file_secret_names:, + logger: + ) begin workspace_resources_yaml = Devfile::Parser.get_all( processed_devfile, @@ -43,12 +56,13 @@ def self.get_all(processed_devfile:, name:, namespace:, replicas:, domain_templa workspace_resources = YAML.load_stream(workspace_resources_yaml) workspace_resources = set_security_context(workspace_resources: workspace_resources) - workspace_resources = set_git_configuration(workspace_resources: workspace_resources, user: user) - set_workspace_environment_variables( + inject_secrets( workspace_resources: workspace_resources, - domain_template: domain_template + env_secret_names: env_secret_names, + file_secret_names: file_secret_names ) end + # rubocop:enable Metrics/ParameterLists # 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. @@ -65,84 +79,71 @@ def self.set_security_context(workspace_resources:) workspace_resources.each do |workspace_resource| next unless workspace_resource['kind'] == 'Deployment' - pod_spec = workspace_resource['spec']['template']['spec'] - # Explicitly set security context for the pod - pod_spec['securityContext'] = { + pod_security_context = { 'runAsNonRoot' => true, 'runAsUser' => RUN_AS_USER, 'fsGroup' => 0, 'fsGroupChangePolicy' => 'OnRootMismatch' } + container_security_context = { + 'allowPrivilegeEscalation' => false, + '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'] = { - 'allowPrivilegeEscalation' => false, - 'privileged' => false, - 'runAsNonRoot' => true, - 'runAsUser' => RUN_AS_USER - } + container['securityContext'] = container_security_context end # Explicitly set security context for all init containers pod_spec['initContainers'].each do |init_container| - init_container['securityContext'] = { - 'allowPrivilegeEscalation' => false, - 'privileged' => false, - 'runAsNonRoot' => true, - 'runAsUser' => RUN_AS_USER - } + init_container['securityContext'] = container_security_context end end workspace_resources end # @param [Array] workspace_resources - # @param [User] user + # @param [Array] env_secret_names + # @param [Array] file_secret_names # @return [Array] - def self.set_git_configuration(workspace_resources:, user:) + def self.inject_secrets(workspace_resources:, env_secret_names:, file_secret_names:) workspace_resources.each do |workspace_resource| next unless workspace_resource.fetch('kind') == 'Deployment' - # Set git configuration for the `gl-cloner-injector-*` - pod_spec = workspace_resource.fetch('spec').fetch('template').fetch('spec') - pod_spec.fetch('initContainers').each do |init_container| - next unless init_container.fetch('name').starts_with?('gl-cloner-injector-') - - init_container.fetch('env').concat([ - { - 'name' => 'GIT_AUTHOR_NAME', - 'value' => user.name - }, - { - 'name' => 'GIT_AUTHOR_EMAIL', - 'value' => user.email + volume_name = 'gl-workspace-variables' + volumes = [ + { + 'name' => volume_name, + 'projected' => { + 'defaultMode' => 0o774, + 'sources' => file_secret_names.map { |v| { 'secret' => { 'name' => v } } } } - ]) - end - end - workspace_resources - end - - # @param [Array] workspace_resources - # @param [String] domain_template - # @return [Array] - def self.set_workspace_environment_variables(workspace_resources:, domain_template:) - env_variables = [ - { - 'name' => 'GL_WORKSPACE_DOMAIN_TEMPLATE', - 'value' => domain_template.sub(/{{.port}}/, "${PORT}") - } - ] - workspace_resources.each do |workspace_resource| - next unless workspace_resource['kind'] == 'Deployment' + } + ] + volume_mounts = [ + { + 'name' => volume_name, + 'mountPath' => RemoteDevelopment::Workspaces::FileMounts::VARIABLES_FILE_DIR + } + ] + env_from = env_secret_names.map { |v| { 'secretRef' => { 'name' => v } } } - pod_spec = workspace_resource['spec']['template']['spec'] + pod_spec = workspace_resource.fetch('spec').fetch('template').fetch('spec') + pod_spec.fetch('volumes').concat(volumes) unless file_secret_names.empty? - pod_spec['initContainers'].each do |init_containers| - init_containers.fetch('env').concat(env_variables) + pod_spec.fetch('initContainers').each do |init_container| + init_container.fetch('volumeMounts').concat(volume_mounts) unless file_secret_names.empty? + init_container['envFrom'] = env_from unless env_secret_names.empty? end - pod_spec['containers'].each do |container| - container.fetch('env').concat(env_variables) + pod_spec.fetch('containers').each do |container| + container.fetch('volumeMounts').concat(volume_mounts) unless file_secret_names.empty? + container['envFrom'] = env_from unless env_secret_names.empty? end end workspace_resources diff --git a/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1.rb b/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1.rb new file mode 100644 index 0000000000000000000000000000000000000000..8bc29975c66629bef45198b415ea18c54821aadc --- /dev/null +++ b/ee/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'devfile' + +module RemoteDevelopment + module Workspaces + module Reconcile + module Output + # noinspection RubyClassMethodNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ + class DevfileParserPrev1 + # @param [String] processed_devfile + # @param [String] name + # @param [String] namespace + # @param [Integer] replicas + # @param [String] domain_template + # @param [Hash] labels + # @param [Hash] annotations + # @param [User] user + # @param [RemoteDevelopment::Logger] logger + # @return [Array] + def self.get_all(processed_devfile:, name:, namespace:, replicas:, domain_template:, labels:, annotations:, user:, logger:) # rubocop:disable Metrics/ParameterLists + begin + workspace_resources_yaml = Devfile::Parser.get_all( + processed_devfile, + name, + namespace, + YAML.dump(labels.deep_stringify_keys), + YAML.dump(annotations.deep_stringify_keys), + replicas, + domain_template, + 'none' + ) + rescue Devfile::CliError => e + logger.warn( + message: 'Error parsing devfile with Devfile::Parser.get_all', + error_type: 'reconcile_devfile_parser_error', + workspace_name: name, + workspace_namespace: namespace, + devfile_parser_error: e.message + ) + return [] + end + + workspace_resources = YAML.load_stream(workspace_resources_yaml) + workspace_resources = set_security_context(workspace_resources: workspace_resources) + workspace_resources = set_git_configuration(workspace_resources: workspace_resources, user: user) + set_workspace_environment_variables( + workspace_resources: workspace_resources, + domain_template: domain_template + ) + 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. + RUN_AS_USER = 5001 + + # @param [Array] workspace_resources + # @return [Array] + def self.set_security_context(workspace_resources:) + workspace_resources.each do |workspace_resource| + next unless workspace_resource['kind'] == 'Deployment' + + pod_spec = workspace_resource['spec']['template']['spec'] + # Explicitly set security context for the pod + pod_spec['securityContext'] = { + 'runAsNonRoot' => true, + 'runAsUser' => RUN_AS_USER, + 'fsGroup' => 0, + 'fsGroupChangePolicy' => 'OnRootMismatch' + } + # Explicitly set security context for all containers + pod_spec['containers'].each do |container| + container['securityContext'] = { + 'allowPrivilegeEscalation' => false, + 'privileged' => false, + 'runAsNonRoot' => true, + 'runAsUser' => RUN_AS_USER + } + end + # Explicitly set security context for all init containers + pod_spec['initContainers'].each do |init_container| + init_container['securityContext'] = { + 'allowPrivilegeEscalation' => false, + 'privileged' => false, + 'runAsNonRoot' => true, + 'runAsUser' => RUN_AS_USER + } + end + end + workspace_resources + end + + # @param [Array] workspace_resources + # @param [User] user + # @return [Array] + def self.set_git_configuration(workspace_resources:, user:) + workspace_resources.each do |workspace_resource| + next unless workspace_resource.fetch('kind') == 'Deployment' + + # Set git configuration for the `gl-cloner-injector-*` + pod_spec = workspace_resource.fetch('spec').fetch('template').fetch('spec') + pod_spec.fetch('initContainers').each do |init_container| + next unless init_container.fetch('name').starts_with?('gl-cloner-injector-') + + init_container.fetch('env').concat([ + { + 'name' => 'GIT_AUTHOR_NAME', + 'value' => user.name + }, + { + 'name' => 'GIT_AUTHOR_EMAIL', + 'value' => user.email + } + ]) + end + end + workspace_resources + end + + # @param [Array] workspace_resources + # @param [String] domain_template + # @return [Array] + def self.set_workspace_environment_variables(workspace_resources:, domain_template:) + env_variables = [ + { + 'name' => 'GL_WORKSPACE_DOMAIN_TEMPLATE', + 'value' => domain_template.sub(/{{.port}}/, "${PORT}") + } + ] + workspace_resources.each do |workspace_resource| + next unless workspace_resource['kind'] == 'Deployment' + + pod_spec = workspace_resource['spec']['template']['spec'] + + pod_spec['initContainers'].each do |init_containers| + init_containers.fetch('env').concat(env_variables) + end + + pod_spec['containers'].each do |container| + container.fetch('env').concat(env_variables) + end + end + workspace_resources + end + end + end + end + end +end diff --git a/ee/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter.rb b/ee/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter.rb index 8b6eb9e4f3346df8433df7053b182d0234197763..aaf73b4f9349fd150e92906e17858fe121eed056 100644 --- a/ee/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter.rb +++ b/ee/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter.rb @@ -44,7 +44,25 @@ def self.convert(value) def self.config_to_apply(workspace:, update_type:, logger:) return unless should_include_config_to_apply?(update_type: update_type, workspace: workspace) - workspace_resources = DesiredConfigGenerator.generate_desired_config(workspace: workspace, logger: logger) + include_secrets = false + + if update_type == FULL || (update_type == PARTIAL && workspace.actual_state == States::CREATION_REQUESTED) + include_secrets = true + end + + workspace_resources = case workspace.config_version + when ConfigVersion::VERSION_2 + DesiredConfigGenerator.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) + else + DesiredConfigGeneratorPrev1.generate_desired_config( + workspace: workspace, + logger: logger + ) + end desired_config_to_apply_array = workspace_resources.map do |resource| YAML.dump(resource.deep_stringify_keys) diff --git a/ee/spec/factories/remote_development/workspaces.rb b/ee/spec/factories/remote_development/workspaces.rb index fcd989858a0615d2138da5339b3b8145fb595aaf..16cb80b547f3cde4fdb103e2aa2e4304d27373bf 100644 --- a/ee/spec/factories/remote_development/workspaces.rb +++ b/ee/spec/factories/remote_development/workspaces.rb @@ -8,6 +8,7 @@ personal_access_token name { "workspace-#{agent.id}-#{user.id}-#{random_string}" } + config_version { RemoteDevelopment::Workspaces::ConfigVersion::VERSION_2 } add_attribute(:namespace) { "gl-rd-ns-#{agent.id}-#{user.id}-#{random_string}" } diff --git a/ee/spec/features/remote_development/workspaces_spec.rb b/ee/spec/features/remote_development/workspaces_spec.rb index 4495103bdc38e7036e7d915d0e1e5b400f9c1786..0e4a6136aba06f2707bba0ada7aae759a75c1bec 100644 --- a/ee/spec/features/remote_development/workspaces_spec.rb +++ b/ee/spec/features/remote_development/workspaces_spec.rb @@ -72,6 +72,12 @@ id = workspace.id name = workspace.name namespace = workspace.namespace + workspace_variables_env_var = get_workspace_variables_env_var( + workspace_variables: workspace.workspace_variables + ) + workspace_variables_file = get_workspace_variables_file( + workspace_variables: workspace.workspace_variables + ) # ASSERT ON NEW WORKSPACE IN LIST page.find('td', text: name) @@ -83,11 +89,22 @@ expect(page).to have_button('Terminate') # SIMULATE FIRST POLL FROM AGENTK TO PICK UP NEW WORKSPACE - simulate_first_poll(id: id, name: name, namespace: namespace) + simulate_first_poll( + id: id, + name: name, + namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file + ) # SIMULATE SECOND POLL FROM AGENTK TO UPDATE WORKSPACE TO RUNNING STATE - - simulate_second_poll(id: id, name: name, namespace: namespace) + simulate_second_poll( + id: id, + name: name, + namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file + ) # ASSERT WORKSPACE SHOWS RUNNING STATE IN UI AND UPDATES URL expect_workspace_state_indicator(RemoteDevelopment::Workspaces::States::RUNNING) @@ -101,8 +118,13 @@ click_button 'Stop' # SIMULATE THIRD POLL FROM AGENTK TO UPDATE WORKSPACE TO STOPPING STATE - - simulate_third_poll(id: id, name: name, namespace: namespace) + simulate_third_poll( + id: id, + name: name, + namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file + ) # ASSERT WORKSPACE SHOWS STOPPING STATE IN UI expect_workspace_state_indicator(RemoteDevelopment::Workspaces::States::STOPPING) @@ -112,8 +134,13 @@ expect(page).to have_button('Terminate') # SIMULATE FOURTH POLL FROM AGENTK TO UPDATE WORKSPACE TO STOPPED STATE - - simulate_fourth_poll(id: id, name: name, namespace: namespace) + simulate_fourth_poll( + id: id, + name: name, + namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file + ) # ASSERT WORKSPACE SHOWS STOPPED STATE IN UI expect_workspace_state_indicator(RemoteDevelopment::Workspaces::States::STOPPED) @@ -123,7 +150,13 @@ expect(page).to have_button('Terminate') end - def simulate_first_poll(id:, name:, namespace:) + def simulate_first_poll( + id:, + name:, + namespace:, + workspace_variables_env_var:, + workspace_variables_file: + ) # SIMULATE FIRST POLL REQUEST FROM AGENTK TO GET NEW WORKSPACE reconcile_post_response = simulate_agentk_reconcile_post(workspace_agent_infos: []) @@ -145,17 +178,24 @@ def simulate_first_poll(id:, name:, namespace:) workspace_id: id, workspace_name: name, workspace_namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent.id, started: true, - user_name: user.name, - user_email: user.email + include_secrets: true ) config_to_apply = info.fetch(:config_to_apply) expect(config_to_apply).to eq(expected_config_to_apply) end - def simulate_second_poll(id:, name:, namespace:) + def simulate_second_poll( + id:, + name:, + namespace:, + workspace_variables_env_var:, + workspace_variables_file: + ) # SIMULATE SECOND POLL REQUEST FROM AGENTK TO UPDATE WORKSPACE TO RUNNING STATE resource_version = '1' @@ -163,13 +203,13 @@ def simulate_second_poll(id:, name:, namespace:) workspace_id: id, workspace_name: name, workspace_namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent.id, resource_version: resource_version, previous_actual_state: RemoteDevelopment::Workspaces::States::STARTING, current_actual_state: RemoteDevelopment::Workspaces::States::RUNNING, - workspace_exists: false, - user_name: user.name, - user_email: user.email + workspace_exists: false ) reconcile_post_response = simulate_agentk_reconcile_post(workspace_agent_infos: [workspace_agent_info]) @@ -189,7 +229,13 @@ def simulate_second_poll(id:, name:, namespace:) expect(info.fetch(:config_to_apply)).to be_nil end - def simulate_third_poll(id:, name:, namespace:) + def simulate_third_poll( + id:, + name:, + namespace:, + workspace_variables_env_var:, + workspace_variables_file: + ) # SIMULATE THIRD POLL REQUEST FROM AGENTK TO UPDATE WORKSPACE TO STOPPING STATE resource_version = '1' @@ -197,13 +243,13 @@ def simulate_third_poll(id:, name:, namespace:) workspace_id: id, workspace_name: name, workspace_namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent.id, resource_version: resource_version, previous_actual_state: RemoteDevelopment::Workspaces::States::RUNNING, current_actual_state: RemoteDevelopment::Workspaces::States::STOPPING, - workspace_exists: true, - user_name: user.name, - user_email: user.email + workspace_exists: true ) reconcile_post_response = simulate_agentk_reconcile_post(workspace_agent_infos: [workspace_agent_info]) @@ -225,17 +271,23 @@ def simulate_third_poll(id:, name:, namespace:) workspace_id: id, workspace_name: name, workspace_namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent.id, - started: false, - user_name: user.name, - user_email: user.email + started: false ) config_to_apply = info.fetch(:config_to_apply) expect(config_to_apply).to eq(expected_config_to_apply) end - def simulate_fourth_poll(id:, name:, namespace:) + def simulate_fourth_poll( + id:, + name:, + namespace:, + workspace_variables_env_var:, + workspace_variables_file: + ) # SIMULATE FOURTH POLL REQUEST FROM AGENTK TO UPDATE WORKSPACE TO STOPPED STATE resource_version = '2' @@ -243,13 +295,13 @@ def simulate_fourth_poll(id:, name:, namespace:) workspace_id: id, workspace_name: name, workspace_namespace: namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent.id, resource_version: resource_version, previous_actual_state: RemoteDevelopment::Workspaces::States::STOPPING, current_actual_state: RemoteDevelopment::Workspaces::States::STOPPED, - workspace_exists: true, - user_name: user.name, - user_email: user.email + workspace_exists: true ) reconcile_post_response = simulate_agentk_reconcile_post(workspace_agent_infos: [workspace_agent_info]) diff --git a/ee/spec/fixtures/remote_development/example.processed-devfile-prev1.yaml b/ee/spec/fixtures/remote_development/example.processed-devfile-prev1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..14dcd8241d3e72417ad443c12635d02d5b6a0aa8 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.processed-devfile-prev1.yaml @@ -0,0 +1,86 @@ +--- +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 + command: + - /projects/.gl-editor/start_server.sh + volumeMounts: + - name: gl-workspace-data + path: /projects + env: + - name: EDITOR_VOLUME_DIR + value: "/projects/.gl-editor" + - name: EDITOR_PORT + value: "60001" + - name: SSH_PORT + value: "60022" + endpoints: + - name: editor-server + targetPort: 60001 + exposure: public + secure: true + protocol: https + - name: ssh-server + targetPort: 60022 + exposure: internal + secure: true + dedicatedPod: false + mountSources: true + - name: gl-workspace-data + volume: + size: 15Gi + - name: gl-editor-injector + container: + image: registry.gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork/web-ide-injector:2 + volumeMounts: + - name: gl-workspace-data + path: /projects + env: + - name: EDITOR_VOLUME_DIR + value: "/projects/.gl-editor" + - name: EDITOR_PORT + value: "60001" + - name: SSH_PORT + value: "60022" + memoryLimit: 256Mi + memoryRequest: 128Mi + cpuLimit: 500m + cpuRequest: 100m + - name: gl-cloner-injector + container: + image: alpine/git:2.36.3 + volumeMounts: + - name: gl-workspace-data + path: "/projects" + args: + - |- + if [ ! -d '/projects/test-project' ]; + then + git clone --branch master http://localhost/test-group/test-project.git /projects/test-project; + cd /projects/test-project; + git config user.name "${GIT_AUTHOR_NAME}"; + git config user.email "${GIT_AUTHOR_EMAIL}"; + fi + command: + - "/bin/sh" + - "-c" + memoryLimit: 256Mi + memoryRequest: 128Mi + cpuLimit: 500m + cpuRequest: 100m +events: + preStart: + - gl-editor-injector-command + - gl-cloner-injector-command +commands: + - id: gl-editor-injector-command + apply: + component: gl-editor-injector + - id: gl-cloner-injector-command + apply: + component: gl-cloner-injector diff --git a/ee/spec/fixtures/remote_development/example.processed-devfile.yaml b/ee/spec/fixtures/remote_development/example.processed-devfile.yaml index 14dcd8241d3e72417ad443c12635d02d5b6a0aa8..38a466e3cc25426abc5bda5db2494aaf08a9b83f 100644 --- a/ee/spec/fixtures/remote_development/example.processed-devfile.yaml +++ b/ee/spec/fixtures/remote_development/example.processed-devfile.yaml @@ -62,9 +62,6 @@ components: if [ ! -d '/projects/test-project' ]; then git clone --branch master http://localhost/test-group/test-project.git /projects/test-project; - cd /projects/test-project; - git config user.name "${GIT_AUTHOR_NAME}"; - git config user.email "${GIT_AUTHOR_EMAIL}"; fi command: - "/bin/sh" diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/input/actual_state_calculator_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/input/actual_state_calculator_spec.rb index 10fd17430d92db64f853bd05f35b14fce0c77c9b..ae6109edd70e6f267cd804e38acf37672cbf3e78 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/input/actual_state_calculator_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/input/actual_state_calculator_spec.rb @@ -47,13 +47,13 @@ workspace_id: 1, workspace_name: 'name', workspace_namespace: 'namespace', + workspace_variables_env_var: {}, + workspace_variables_file: {}, agent_id: 1, resource_version: 1, previous_actual_state: previous_actual_state, current_actual_state: current_actual_state, - workspace_exists: workspace_exists, - user_name: "does not matter", - user_email: "does@not.matter" + workspace_exists: workspace_exists ) workspace_agent_info_hash.fetch(:latest_k8s_deployment_info) end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb index ad32525694c83dd02a80a671f38c37cae1db8c3e..ab28ca237f120940edc9d3ff10ce5256ee84022c 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb @@ -1,27 +1,34 @@ # frozen_string_literal: true -require_relative '../../../fast_spec_helper' +require 'spec_helper' RSpec.describe RemoteDevelopment::Workspaces::Reconcile::Input::Factory, feature_category: :remote_development do include_context 'with remote development shared fixtures' - let(:namespace) { "namespace" } let(:agent) { instance_double("Clusters::Agent", id: 1) } let(:user) { instance_double("User", name: "name", email: "name@example.com") } - let(:workspace) { instance_double("RemoteDevelopment::Workspace", id: 1, name: "name", namespace: namespace) } + let(:workspace) { create(:workspace) } + let(:namespace) { workspace.namespace } + let(:workspace_variables_env_var) do + get_workspace_variables_env_var(workspace_variables: workspace.workspace_variables) + end + + let(:workspace_variables_file) do + get_workspace_variables_file(workspace_variables: workspace.workspace_variables) + end let(:workspace_agent_info_hash) do create_workspace_agent_info_hash( workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: namespace, - agent_id: agent.id, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, + agent_id: workspace.agent.id, resource_version: "1", previous_actual_state: previous_actual_state, current_actual_state: current_actual_state, - workspace_exists: false, - user_name: user.name, - user_email: user.email + workspace_exists: false ) end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb index bb578d0e1a6a65f270c5a2706344d7d5bd90f46f..2bcbf091a38ff1f2a2e699aafd98d8efe21cbbcd 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb @@ -17,6 +17,15 @@ let_it_be(:user) { create(:user) } let_it_be(:agent) { create(:ee_cluster_agent, :with_remote_development_agent_config) } + let(:workspace_variables) { workspace.workspace_variables } + let(:workspace_variables_env_var) do + get_workspace_variables_env_var(workspace_variables: workspace_variables) + end + + let(:workspace_variables_file) do + get_workspace_variables_file(workspace_variables: workspace_variables) + end + let(:logger) { instance_double(::Logger) } let(:expected_value_for_started) { true } @@ -75,13 +84,13 @@ workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace.agent.id, resource_version: deployment_resource_version_from_agent, previous_actual_state: previous_actual_state, current_actual_state: current_actual_state, workspace_exists: workspace_exists, - user_name: user.name, - user_email: user.email, error_details: error_from_agent ) end @@ -201,18 +210,20 @@ agent: agent, user: user) end + let(:workspace_variables) { workspace2.workspace_variables } + let(:workspace2_agent_info) do create_workspace_agent_info_hash( workspace_id: workspace2.id, workspace_name: workspace2.name, workspace_namespace: workspace2.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace2.agent.id, resource_version: deployment_resource_version_from_agent, previous_actual_state: previous_actual_state, current_actual_state: current_actual_state, - workspace_exists: workspace_exists, - user_name: user.name, - user_email: user.email + workspace_exists: workspace_exists ) end @@ -348,10 +359,10 @@ workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace.agent.id, - started: expected_value_for_started, - user_name: user.name, - user_email: user.email + started: expected_value_for_started ) end @@ -466,10 +477,10 @@ workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace.agent.id, - started: expected_value_for_started, - user_name: user.name, - user_email: user.email + started: expected_value_for_started ) end @@ -506,13 +517,13 @@ workspace_id: 1, workspace_name: workspace_name, workspace_namespace: workspace_namespace, + workspace_variables_env_var: {}, + workspace_variables_file: {}, agent_id: '1', resource_version: '42', previous_actual_state: RemoteDevelopment::Workspaces::States::STOPPING, current_actual_state: RemoteDevelopment::Workspaces::States::STOPPED, - workspace_exists: false, - user_name: user.name, - user_email: user.email + workspace_exists: false ) end @@ -538,6 +549,7 @@ end let(:workspace_agent_infos) { [] } + let(:workspace_variables) { unprovisioned_workspace.workspace_variables } # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31542 let(:expected_config_to_apply) do @@ -545,10 +557,11 @@ workspace_id: unprovisioned_workspace.id, workspace_name: unprovisioned_workspace.name, workspace_namespace: unprovisioned_workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: unprovisioned_workspace.agent.id, started: expected_value_for_started, - user_name: user.name, - user_email: user.email + include_secrets: true ) end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/main_reconcile_scenarios_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/main_reconcile_scenarios_spec.rb index 59da67a458a587c66f8a3ac24efd179a8f23b586..7d0e4f2689a36b57dd8155c920900acb135c8d55 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/main_reconcile_scenarios_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/main_reconcile_scenarios_spec.rb @@ -206,18 +206,24 @@ current_actual_state = actual_state_update_fixture_args[1].to_s.camelize workspace_exists = actual_state_update_fixture_args[2] deployment_resource_version_from_agent = (deployment_resource_version_from_agent.to_i + 1).to_s + workspace_variables_env_var = get_workspace_variables_env_var( + workspace_variables: workspace.workspace_variables + ) + workspace_variables_file = get_workspace_variables_file( + workspace_variables: workspace.workspace_variables + ) [ create_workspace_agent_info_hash( workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace.agent.id, resource_version: deployment_resource_version_from_agent, current_actual_state: current_actual_state, previous_actual_state: previous_actual_state, - workspace_exists: workspace_exists, - user_name: user.name, - user_email: user.email + workspace_exists: workspace_exists ) ] else diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..514e0431777519af47dccfb81e8d70aa544a1995 --- /dev/null +++ b/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require_relative '../../../fast_spec_helper' + +RSpec.describe RemoteDevelopment::Workspaces::Reconcile::Output::DesiredConfigGeneratorPrev1, :freeze_time, feature_category: :remote_development do + include_context 'with remote development shared fixtures' + + describe '#generate_desired_config' do + let(:logger) { instance_double(Logger) } + let(:user) { instance_double("User", name: "name", email: "name@example.com") } + let(:remote_development_agent_config) do + instance_double( + "RemoteDevelopment::RemoteDevelopmentAgentConfig", + network_policy_enabled: network_policy_enabled, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace + ) + end + + let(:agent) do + instance_double("Clusters::Agent", id: 1, remote_development_agent_config: remote_development_agent_config) + end + + let(:desired_state) { RemoteDevelopment::Workspaces::States::RUNNING } + let(:actual_state) { RemoteDevelopment::Workspaces::States::STOPPED } + let(:deployment_resource_version_from_agent) { workspace.deployment_resource_version } + let(:network_policy_enabled) { true } + let(:gitlab_workspaces_proxy_namespace) { 'gitlab-workspaces' } + + let(:workspace) do + instance_double( + "RemoteDevelopment::Workspace", + id: 1, + name: "name", + namespace: "namespace", + deployment_resource_version: "1", + desired_state: desired_state, + actual_state: actual_state, + dns_zone: "workspaces.localdev.me", + processed_devfile: read_devfile('example.processed-devfile-prev1.yaml'), + user: user, + agent: agent, + config_version: 1 + ) + end + + let(:expected_config) do + YAML.load_stream( + create_config_to_apply_prev1( + workspace_id: workspace.id, + workspace_name: workspace.name, + workspace_namespace: workspace.namespace, + agent_id: workspace.agent.id, + started: started, + user_name: user.name, + user_email: user.email, + include_network_policy: network_policy_enabled + ) + ) + end + + subject do + described_class + end + + context 'when desired_state results in started=true' do + let(:started) { true } + + it 'returns expected config' do + workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + + expect(workspace_resources).to eq(expected_config) + end + end + + context 'when desired_state results in started=false' do + let(:desired_state) { RemoteDevelopment::Workspaces::States::STOPPED } + let(:started) { false } + + it 'returns expected config' do + workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + + expect(workspace_resources).to eq(expected_config) + end + end + + context 'when network policy is disabled for agent' do + let(:started) { true } + let(:network_policy_enabled) { false } + + it 'returns expected config without network policy' do + workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + + expect(workspace_resources).to eq(expected_config) + end + end + + context 'when DevfileParser returns empty array' do + before do + allow(RemoteDevelopment::Workspaces::Reconcile::Output::DevfileParserPrev1).to receive(:get_all).and_return([]) + end + + it 'returns an empty array' do + workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + + expect(workspace_resources).to eq([]) + end + end + end +end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_spec.rb index 423721fe49b90279d6bf84a681a09d63e0d9c447..9442d70062ced2d8463188270221496b294690cc 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_spec.rb @@ -1,58 +1,52 @@ # frozen_string_literal: true -require_relative '../../../fast_spec_helper' +require 'spec_helper' RSpec.describe RemoteDevelopment::Workspaces::Reconcile::Output::DesiredConfigGenerator, :freeze_time, feature_category: :remote_development do include_context 'with remote development shared fixtures' describe '#generate_desired_config' do let(:logger) { instance_double(Logger) } - let(:user) { instance_double("User", name: "name", email: "name@example.com") } - let(:remote_development_agent_config) do - instance_double( - "RemoteDevelopment::RemoteDevelopmentAgentConfig", - network_policy_enabled: network_policy_enabled, - gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace - ) - end - - let(:agent) do - instance_double("Clusters::Agent", id: 1, remote_development_agent_config: remote_development_agent_config) - end - + let(:user) { create(:user) } + let(:agent) { create(:ee_cluster_agent, :with_remote_development_agent_config) } let(:desired_state) { RemoteDevelopment::Workspaces::States::RUNNING } let(:actual_state) { RemoteDevelopment::Workspaces::States::STOPPED } + let(:started) { true } + let(:include_secrets) { false } let(:deployment_resource_version_from_agent) { workspace.deployment_resource_version } let(:network_policy_enabled) { true } let(:gitlab_workspaces_proxy_namespace) { 'gitlab-workspaces' } let(:workspace) do - instance_double( - "RemoteDevelopment::Workspace", - id: 1, - name: "name", - namespace: "namespace", - deployment_resource_version: "1", - desired_state: desired_state, - actual_state: actual_state, - dns_zone: "workspaces.localdev.me", - processed_devfile: example_processed_devfile, + create( + :workspace, + agent: agent, user: user, - agent: agent + desired_state: desired_state, + actual_state: actual_state ) end + let(:workspace_variables_env_var) do + get_workspace_variables_env_var(workspace_variables: workspace.workspace_variables) + end + + let(:workspace_variables_file) do + get_workspace_variables_file(workspace_variables: workspace.workspace_variables) + end + let(:expected_config) do YAML.load_stream( create_config_to_apply( workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: workspace.agent.id, started: started, - user_name: user.name, - user_email: user.email, - include_network_policy: network_policy_enabled + include_network_policy: network_policy_enabled, + include_secrets: include_secrets ) ) end @@ -61,11 +55,18 @@ described_class end - context 'when desired_state results in started=true' do - let(:started) { true } + before do + allow(agent.remote_development_agent_config) + .to receive(:network_policy_enabled).and_return(network_policy_enabled) + end + context 'when desired_state results in started=true' do it 'returns expected config' do - workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + workspace_resources = subject.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) expect(workspace_resources).to eq(expected_config) end @@ -76,18 +77,39 @@ let(:started) { false } it 'returns expected config' do - workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + workspace_resources = subject.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) expect(workspace_resources).to eq(expected_config) end end context 'when network policy is disabled for agent' do - let(:started) { true } let(:network_policy_enabled) { false } it 'returns expected config without network policy' do - workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + workspace_resources = subject.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) + + expect(workspace_resources).to eq(expected_config) + end + end + + context 'when include_secrets=true results in secrets being generated' do + let(:include_secrets) { true } + + it 'returns expected config' do + workspace_resources = subject.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) expect(workspace_resources).to eq(expected_config) end @@ -99,7 +121,11 @@ end it 'returns an empty array' do - workspace_resources = subject.generate_desired_config(workspace: workspace, logger: logger) + workspace_resources = subject.generate_desired_config( + workspace: workspace, + include_secrets: include_secrets, + logger: logger + ) expect(workspace_resources).to eq([]) end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebc6eaa0b3a615c5af711e4fa058b5e06f423223 --- /dev/null +++ b/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require_relative '../../../fast_spec_helper' + +RSpec.describe RemoteDevelopment::Workspaces::Reconcile::Output::DevfileParserPrev1, feature_category: :remote_development do + include_context 'with remote development shared fixtures' + + let(:dns_zone) { "workspaces.localdev.me" } + let(:logger) { instance_double(Logger) } + let(:user) { instance_double("User", name: "name", email: "name@example.com") } + let(:agent) { instance_double("Clusters::Agent", id: 1) } + let(:processed_devfile) { read_devfile('example.processed-devfile-prev1.yaml') } + let(:workspace) do + instance_double( + "RemoteDevelopment::Workspace", + id: 1, + name: "name", + namespace: "namespace", + deployment_resource_version: "1", + desired_state: RemoteDevelopment::Workspaces::States::RUNNING, + actual_state: RemoteDevelopment::Workspaces::States::STOPPED, + dns_zone: dns_zone, + processed_devfile: processed_devfile, + user: user, + agent: agent + ) + end + + let(:owning_inventory) { "#{workspace.name}-workspace-inventory" } + + let(:domain_template) { "{{.port}}-#{workspace.name}.#{workspace.dns_zone}" } + + let(:expected_workspace_resources) do + YAML.load_stream( + create_config_to_apply_prev1( + workspace_id: workspace.id, + workspace_name: workspace.name, + workspace_namespace: workspace.namespace, + agent_id: workspace.agent.id, + started: true, + include_inventory: false, + include_network_policy: false, + dns_zone: dns_zone, + user_name: user.name, + user_email: user.email + ) + ) + end + + subject do + described_class + end + + it 'returns workspace_resources' do + workspace_resources = subject.get_all( + processed_devfile: processed_devfile, + name: workspace.name, + namespace: workspace.namespace, + replicas: 1, + domain_template: domain_template, + labels: { 'agent.gitlab.com/id' => workspace.agent.id }, + annotations: { + 'config.k8s.io/owning-inventory' => owning_inventory, + 'workspaces.gitlab.com/host-template' => domain_template, + 'workspaces.gitlab.com/id' => workspace.id + }, + user: user, + logger: logger + ) + + expect(workspace_resources).to eq(expected_workspace_resources) + end + + context "when Devfile::CliError is raised" do + before do + allow(Devfile::Parser).to receive(:get_all).and_raise(Devfile::CliError.new("some error")) + end + + it "logs the error" do + expect(logger).to receive(:warn).with( + message: 'Error parsing devfile with Devfile::Parser.get_all', + error_type: 'reconcile_devfile_parser_error', + workspace_name: workspace.name, + workspace_namespace: workspace.namespace, + devfile_parser_error: "some error" + ) + + workspace_resources = subject.get_all( + processed_devfile: "", + name: workspace.name, + namespace: workspace.namespace, + replicas: 1, + domain_template: "", + labels: {}, + annotations: {}, + user: user, + logger: logger + ) + + expect(workspace_resources).to eq([]) + end + end +end diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_spec.rb index 0f71e96e12f329a215a0a5e388635642c7eb0ab0..83c6dff9e0765b5e84a8c98acb99c64546b48e68 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_spec.rb @@ -25,9 +25,9 @@ ) end - let(:owning_inventory) { "#{workspace.name}-workspace-inventory" } - let(:domain_template) { "{{.port}}-#{workspace.name}.#{workspace.dns_zone}" } + let(:env_var_secret_name) { "#{workspace.name}-env-var" } + let(:file_secret_name) { "#{workspace.name}-file" } let(:expected_workspace_resources) do YAML.load_stream( @@ -35,13 +35,14 @@ workspace_id: workspace.id, workspace_name: workspace.name, workspace_namespace: workspace.namespace, + workspace_variables_env_var: {}, + workspace_variables_file: {}, agent_id: workspace.agent.id, started: true, include_inventory: false, include_network_policy: false, - dns_zone: dns_zone, - user_name: user.name, - user_email: user.email + include_secrets: false, + dns_zone: dns_zone ) ) end @@ -59,11 +60,12 @@ domain_template: domain_template, labels: { 'agent.gitlab.com/id' => workspace.agent.id }, annotations: { - 'config.k8s.io/owning-inventory' => owning_inventory, + 'config.k8s.io/owning-inventory' => "#{workspace.name}-workspace-inventory", 'workspaces.gitlab.com/host-template' => domain_template, 'workspaces.gitlab.com/id' => workspace.id }, - user: user, + env_secret_names: [env_var_secret_name], + file_secret_names: [file_secret_name], logger: logger ) @@ -92,7 +94,8 @@ domain_template: "", labels: {}, annotations: {}, - user: user, + env_secret_names: [env_var_secret_name], + file_secret_names: [file_secret_name], logger: logger ) diff --git a/ee/spec/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter_spec.rb b/ee/spec/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter_spec.rb index 8c09961812ab42c3c9265b8000fab4d3e7d4d279..356d10bba8ea1a44d7430d1f3ca33909106c78e9 100644 --- a/ee/spec/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter_spec.rb +++ b/ee/spec/lib/remote_development/workspaces/reconcile/output/workspaces_to_rails_infos_converter_spec.rb @@ -3,9 +3,12 @@ require_relative '../../../fast_spec_helper' RSpec.describe RemoteDevelopment::Workspaces::Reconcile::Output::WorkspacesToRailsInfosConverter, feature_category: :remote_development do + include_context 'with remote development shared fixtures' + let(:logger) { instance_double(Logger) } let(:desired_state) { RemoteDevelopment::Workspaces::States::RUNNING } let(:actual_state) { RemoteDevelopment::Workspaces::States::STOPPED } + let(:processed_devfile) { example_processed_devfile } let(:config_to_apply) { { foo: "bar" } } let(:config_to_apply_yaml) { "---\nfoo: bar\n" } let(:workspace1) do @@ -16,7 +19,9 @@ namespace: "namespace1", deployment_resource_version: "1", desired_state: desired_state, - actual_state: actual_state + actual_state: actual_state, + processed_devfile: processed_devfile, + config_version: RemoteDevelopment::Workspaces::ConfigVersion::VERSION_2 ) end @@ -28,11 +33,47 @@ namespace: "namespace2", deployment_resource_version: "2", desired_state: desired_state, - actual_state: actual_state + actual_state: actual_state, + processed_devfile: processed_devfile, + config_version: RemoteDevelopment::Workspaces::ConfigVersion::VERSION_2 + ) + end + + let(:prev1_workspace1) do + instance_double( + "RemoteDevelopment::Workspace", + id: 1, + name: "workspace_prev1", + namespace: "namespace1", + deployment_resource_version: "1", + desired_state: desired_state, + actual_state: actual_state, + processed_devfile: processed_devfile, + config_version: RemoteDevelopment::Workspaces::ConfigVersion::VERSION_1 ) end - let(:value) { { update_type: update_type, workspaces_to_be_returned: [workspace1, workspace2], logger: logger } } + let(:prev1_workspace2) do + instance_double( + "RemoteDevelopment::Workspace", + id: 1, + name: "workspace_prev2", + namespace: "namespace2", + deployment_resource_version: "2", + desired_state: desired_state, + actual_state: actual_state, + processed_devfile: processed_devfile, + config_version: RemoteDevelopment::Workspaces::ConfigVersion::VERSION_1 + ) + end + + let(:value) do + { + update_type: update_type, + workspaces_to_be_returned: [workspace1, workspace2, prev1_workspace1, prev1_workspace2], + logger: logger + } + end subject do described_class.convert(value) @@ -41,6 +82,8 @@ before do allow(RemoteDevelopment::Workspaces::Reconcile::Output::DesiredConfigGenerator) .to receive(:generate_desired_config) { [config_to_apply] } + allow(RemoteDevelopment::Workspaces::Reconcile::Output::DesiredConfigGeneratorPrev1) + .to receive(:generate_desired_config) { [config_to_apply] } end context "when update_type is FULL" do @@ -65,6 +108,22 @@ desired_state: desired_state, actual_state: actual_state, config_to_apply: config_to_apply_yaml + }, + { + name: "workspace_prev1", + namespace: "namespace1", + deployment_resource_version: "1", + desired_state: desired_state, + actual_state: actual_state, + config_to_apply: config_to_apply_yaml + }, + { + name: "workspace_prev2", + namespace: "namespace2", + deployment_resource_version: "2", + desired_state: desired_state, + actual_state: actual_state, + config_to_apply: config_to_apply_yaml } ] ) @@ -76,9 +135,18 @@ let(:update_type) { RemoteDevelopment::Workspaces::Reconcile::UpdateTypes::PARTIAL } before do - allow(workspace1).to receive(:desired_state_updated_more_recently_than_last_response_to_agent?).and_return(true) + allow(workspace1) + .to receive(:desired_state_updated_more_recently_than_last_response_to_agent?) + .and_return(true) allow(workspace2) - .to receive(:desired_state_updated_more_recently_than_last_response_to_agent?).and_return(false) + .to receive(:desired_state_updated_more_recently_than_last_response_to_agent?) + .and_return(false) + allow(prev1_workspace1) + .to receive(:desired_state_updated_more_recently_than_last_response_to_agent?) + .and_return(true) + allow(prev1_workspace2) + .to receive(:desired_state_updated_more_recently_than_last_response_to_agent?) + .and_return(false) end context "when workspace.desired_state_updated_more_recently_than_last_response_to_agent == true" do 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 4987363769e07d5e2280b65513ffc2cd5974f0c1..a506b85f561051d3a170cc773eebbd75c7bd059e 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 @@ -11,6 +11,8 @@ def create_workspace_agent_info_hash( workspace_id:, workspace_name:, workspace_namespace:, + workspace_variables_env_var:, + workspace_variables_file:, agent_id:, resource_version:, # NOTE: previous_actual_state is the actual state of the workspace IMMEDIATELY prior to the current state. We don't @@ -19,8 +21,6 @@ def create_workspace_agent_info_hash( current_actual_state:, # NOTE: workspace_exists is whether the workspace exists in the cluster at the time of the current_actual_state. workspace_exists:, - user_name:, - user_email:, dns_zone: 'workspaces.localdev.me', error_details: nil ) @@ -258,12 +258,13 @@ def create_workspace_agent_info_hash( workspace_id: workspace_id, workspace_name: workspace_name, workspace_namespace: workspace_namespace, + workspace_variables_env_var: workspace_variables_env_var, + workspace_variables_file: workspace_variables_file, agent_id: agent_id, started: started, - user_name: user_name, - user_email: user_email, include_inventory: false, include_network_policy: false, + include_secrets: false, dns_zone: dns_zone ) config_to_apply = YAML.load_stream(config_to_apply) @@ -302,6 +303,100 @@ def create_workspace_rails_info( # rubocop:disable Metrics/ParameterLists # noinspection RubyLocalVariableNamingConvention,RubyParameterNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ def create_config_to_apply( + workspace_id:, + workspace_name:, + workspace_namespace:, + workspace_variables_env_var:, + workspace_variables_file:, + agent_id:, + started:, + include_inventory: true, + include_network_policy: true, + include_secrets: false, + dns_zone: 'workspaces.localdev.me' + ) + spec_replicas = started == true ? 1 : 0 + host_template_annotation = get_workspace_host_template_annotation(workspace_name, dns_zone) + + workspace_inventory = workspace_inventory( + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id + ) + + workspace_deployment = workspace_deployment( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + spec_replicas: spec_replicas, + host_template_annotation: host_template_annotation + ) + + workspace_service = workspace_service( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + host_template_annotation: host_template_annotation + ) + + workspace_pvc = workspace_pvc( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + host_template_annotation: host_template_annotation + ) + + workspace_network_policy = workspace_network_policy( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + host_template_annotation: host_template_annotation + ) + + workspace_secrets_inventory = workspace_secrets_inventory( + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id + ) + + workspace_secret_env_var = workspace_secret_env_var( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + host_template_annotation: host_template_annotation, + workspace_variables_env_var: workspace_variables_env_var + ) + + workspace_secret_file = workspace_secret_file( + workspace_id: workspace_id, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + agent_id: agent_id, + host_template_annotation: host_template_annotation, + workspace_variables_file: workspace_variables_file + ) + + resources = [] + resources << workspace_inventory if include_inventory + resources << workspace_deployment + resources << workspace_service + resources << workspace_pvc + resources << workspace_network_policy if include_network_policy + resources << workspace_secrets_inventory if include_secrets && include_inventory + resources << workspace_secret_env_var if include_secrets + resources << workspace_secret_file if include_secrets + + resources.map do |resource| + YAML.dump(resource.deep_stringify_keys) + end.join + end + + def create_config_to_apply_prev1( workspace_id:, workspace_name:, workspace_namespace:, @@ -323,7 +418,7 @@ def create_config_to_apply( agent_id: agent_id ) - workspace_deployment = workspace_deployment( + workspace_deployment = workspace_deployment_prev1( workspace_id: workspace_id, workspace_name: workspace_name, workspace_namespace: workspace_namespace, @@ -391,9 +486,280 @@ def workspace_inventory( } end - # noinspection RubyParameterNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ - # rubocop:disable Metrics/ParameterLists def workspace_deployment( + workspace_id:, + workspace_name:, + workspace_namespace:, + agent_id:, + spec_replicas:, + host_template_annotation: + ) + variables_file_mount_path = RemoteDevelopment::Workspaces::FileMounts::VARIABLES_FILE_DIR + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + annotations: { + "config.k8s.io/owning-inventory": "#{workspace_name}-workspace-inventory", + "workspaces.gitlab.com/host-template": host_template_annotation.to_s, + "workspaces.gitlab.com/id": workspace_id.to_s + }, + creationTimestamp: nil, + labels: { + "agent.gitlab.com/id": agent_id.to_s + }, + name: workspace_name.to_s, + namespace: workspace_namespace.to_s + }, + spec: { + replicas: spec_replicas, + selector: { + matchLabels: { + "agent.gitlab.com/id": agent_id.to_s + } + }, + strategy: { + type: "Recreate" + }, + template: { + metadata: { + annotations: { + "config.k8s.io/owning-inventory": "#{workspace_name}-workspace-inventory", + "workspaces.gitlab.com/host-template": host_template_annotation.to_s, + "workspaces.gitlab.com/id": workspace_id.to_s + }, + creationTimestamp: nil, + labels: { + "agent.gitlab.com/id": agent_id.to_s + }, + name: workspace_name.to_s, + namespace: workspace_namespace.to_s + }, + spec: { + containers: [ + { + command: [ + "/projects/.gl-editor/start_server.sh" + ], + env: [ + { + name: "EDITOR_VOLUME_DIR", + value: "/projects/.gl-editor" + }, + { + name: "EDITOR_PORT", + value: "60001" + }, + { + name: "SSH_PORT", + value: "60022" + }, + { + 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", + ports: [ + { + containerPort: 60001, + name: "editor-server", + protocol: "TCP" + }, + { + containerPort: 60022, + name: "ssh-server", + protocol: "TCP" + } + ], + resources: {}, + volumeMounts: [ + { + mountPath: "/projects", + name: "gl-workspace-data" + }, + { + name: "gl-workspace-variables", + mountPath: variables_file_mount_path.to_s + } + ], + securityContext: { + allowPrivilegeEscalation: false, + privileged: false, + runAsNonRoot: true, + runAsUser: 5001 + }, + envFrom: [ + { + secretRef: { + name: "#{workspace_name}-env-var" + } + } + ] + } + ], + initContainers: [ + { + args: [ + <<~ARGS.chomp + if [ ! -d '/projects/test-project' ]; + then + git clone --branch master #{root_url}test-group/test-project.git /projects/test-project; + fi + ARGS + ], + command: %w[/bin/sh -c], + env: [ + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + image: "alpine/git:2.36.3", + imagePullPolicy: "Always", + name: "gl-cloner-injector-gl-cloner-injector-command-1", + resources: { + limits: { + cpu: "500m", + memory: "256Mi" + }, + requests: { + cpu: "100m", + memory: "128Mi" + } + }, + volumeMounts: [ + { + mountPath: "/projects", + name: "gl-workspace-data" + }, + { + name: "gl-workspace-variables", + mountPath: variables_file_mount_path.to_s + } + ], + securityContext: { + allowPrivilegeEscalation: false, + privileged: false, + runAsNonRoot: true, + runAsUser: 5001 + }, + envFrom: [ + { + secretRef: { + name: "#{workspace_name}-env-var" + } + } + ] + }, + { + env: [ + { + name: "EDITOR_VOLUME_DIR", + value: "/projects/.gl-editor" + }, + { + name: "EDITOR_PORT", + value: "60001" + }, + { + name: "SSH_PORT", + value: "60022" + }, + { + name: "PROJECTS_ROOT", + value: "/projects" + }, + { + name: "PROJECT_SOURCE", + value: "/projects" + } + ], + image: "registry.gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork/web-ide-injector:2", + imagePullPolicy: "Always", + name: "gl-editor-injector-gl-editor-injector-command-2", + resources: { + limits: { + cpu: "500m", + memory: "256Mi" + }, + requests: { + cpu: "100m", + memory: "128Mi" + } + }, + volumeMounts: [ + { + mountPath: "/projects", + name: "gl-workspace-data" + }, + { + name: "gl-workspace-variables", + mountPath: variables_file_mount_path.to_s + } + ], + securityContext: { + allowPrivilegeEscalation: false, + privileged: false, + runAsNonRoot: true, + runAsUser: 5001 + }, + envFrom: [ + { + secretRef: { + name: "#{workspace_name}-env-var" + } + } + ] + } + ], + volumes: [ + { + name: "gl-workspace-data", + persistentVolumeClaim: { + claimName: "#{workspace_name}-gl-workspace-data" + } + }, + { + name: "gl-workspace-variables", + projected: { + defaultMode: 508, + sources: [ + { + secret: { + name: "#{workspace_name}-file" + } + } + ] + } + } + ], + securityContext: { + runAsNonRoot: true, + runAsUser: 5001, + fsGroup: 0, + fsGroupChangePolicy: "OnRootMismatch" + } + } + } + }, + status: {} + } + end + + # noinspection RubyParameterNamingConvention + # rubocop:disable Metrics/ParameterLists + def workspace_deployment_prev1( workspace_id:, workspace_name:, workspace_namespace:, @@ -807,6 +1173,118 @@ def workspace_network_policy( } end + def workspace_secrets_inventory( + workspace_name:, + workspace_namespace:, + agent_id: + ) + { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: "#{workspace_name}-secrets-inventory", + namespace: workspace_namespace.to_s, + labels: { + "cli-utils.sigs.k8s.io/inventory-id": "#{workspace_name}-secrets-inventory", + "agent.gitlab.com/id": agent_id.to_s + } + } + } + end + + def workspace_secret_env_var( + workspace_id:, + workspace_name:, + workspace_namespace:, + agent_id:, + host_template_annotation:, + workspace_variables_env_var: + ) + git_config_count = workspace_variables_env_var.fetch('GIT_CONFIG_COUNT', '') + git_config_key_0 = workspace_variables_env_var.fetch('GIT_CONFIG_KEY_0', '') + git_config_value_0 = workspace_variables_env_var.fetch('GIT_CONFIG_VALUE_0', '') + git_config_key_1 = workspace_variables_env_var.fetch('GIT_CONFIG_KEY_1', '') + git_config_value_1 = workspace_variables_env_var.fetch('GIT_CONFIG_VALUE_1', '') + git_config_key_2 = workspace_variables_env_var.fetch('GIT_CONFIG_KEY_2', '') + git_config_value_2 = workspace_variables_env_var.fetch('GIT_CONFIG_VALUE_2', '') + gl_git_credential_store_file_path = workspace_variables_env_var.fetch('GL_GIT_CREDENTIAL_STORE_FILE_PATH', '') + gl_token_file_path = workspace_variables_env_var.fetch('GL_TOKEN_FILE_PATH', '') + gl_workspace_domain_template = workspace_variables_env_var.fetch('GL_WORKSPACE_DOMAIN_TEMPLATE', '') + # TODO: figure out why there is flakiness in the order of the environment variables? + { + kind: "Secret", + apiVersion: "v1", + metadata: { + name: "#{workspace_name}-env-var", + namespace: workspace_namespace.to_s, + labels: { + "agent.gitlab.com/id": agent_id.to_s + }, + annotations: { + "config.k8s.io/owning-inventory": "#{workspace_name}-secrets-inventory", + "workspaces.gitlab.com/host-template": host_template_annotation.to_s, + "workspaces.gitlab.com/id": workspace_id.to_s + } + }, + data: { + GIT_CONFIG_COUNT: Base64.strict_encode64(git_config_count).to_s, + GIT_CONFIG_KEY_0: Base64.strict_encode64(git_config_key_0).to_s, + GIT_CONFIG_VALUE_0: Base64.strict_encode64(git_config_value_0).to_s, + GIT_CONFIG_KEY_1: Base64.strict_encode64(git_config_key_1).to_s, + GIT_CONFIG_VALUE_1: Base64.strict_encode64(git_config_value_1).to_s, + GIT_CONFIG_KEY_2: Base64.strict_encode64(git_config_key_2).to_s, + GIT_CONFIG_VALUE_2: Base64.strict_encode64(git_config_value_2).to_s, + GL_GIT_CREDENTIAL_STORE_FILE_PATH: Base64.strict_encode64(gl_git_credential_store_file_path).to_s, + GL_TOKEN_FILE_PATH: Base64.strict_encode64(gl_token_file_path).to_s, + GL_WORKSPACE_DOMAIN_TEMPLATE: Base64.strict_encode64(gl_workspace_domain_template).to_s + } + } + end + + def workspace_secret_file( + workspace_id:, + workspace_name:, + workspace_namespace:, + agent_id:, + host_template_annotation:, + workspace_variables_file: + ) + gl_git_credential_store = workspace_variables_file.fetch('gl_git_credential_store.sh', '') + gl_token = workspace_variables_file.fetch('gl_token', '') + { + kind: "Secret", + apiVersion: "v1", + metadata: { + name: "#{workspace_name}-file", + namespace: workspace_namespace.to_s, + labels: { + "agent.gitlab.com/id": agent_id.to_s + }, + annotations: { + "config.k8s.io/owning-inventory": "#{workspace_name}-secrets-inventory", + "workspaces.gitlab.com/host-template": host_template_annotation.to_s, + "workspaces.gitlab.com/id": workspace_id.to_s + } + }, + data: { + gl_token: Base64.strict_encode64(gl_token).to_s, + "gl_git_credential_store.sh": Base64.strict_encode64(gl_git_credential_store).to_s + } + } + end + + def get_workspace_variables_env_var(workspace_variables:) + workspace_variables.with_variable_type_env_var.each_with_object({}) do |workspace_variable, hash| + hash[workspace_variable.key] = workspace_variable.value + end + end + + def get_workspace_variables_file(workspace_variables:) + workspace_variables.with_variable_type_file.each_with_object({}) do |workspace_variable, hash| + hash[workspace_variable.key] = workspace_variable.value + end + end + # noinspection RubyInstanceMethodNamingConvention - See https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/ def get_workspace_host_template_annotation(workspace_name, dns_zone) "{{.port}}-#{workspace_name}.#{dns_zone}" @@ -826,15 +1304,15 @@ def example_flattened_devfile end def example_processed_devfile - devfile_contents = read_devfile('example.processed-devfile.yaml') - devfile_contents.gsub!('http://localhost/', root_url) - devfile_contents + read_devfile('example.processed-devfile.yaml') end # TODO: Rename this method and all methods which use it to end in `_yaml`, to clearly distinguish between # a String YAML representation of a devfile, and a devfile which has been converted to a Hash. def read_devfile(filename) - File.read(Rails.root.join('ee/spec/fixtures/remote_development', filename).to_s) + devfile_contents = File.read(Rails.root.join('ee/spec/fixtures/remote_development', filename).to_s) + devfile_contents.gsub!('http://localhost/', root_url) + devfile_contents end def root_url