From 5393332a98419971e1b0e3a3c8b96317bfba83be Mon Sep 17 00:00:00 2001 From: Ashvin Sharma Date: Wed, 16 Jul 2025 02:01:27 +0530 Subject: [PATCH] Add files from RemoteDevelopment module for migration These files will be required to do migration. --- .../bm_create_desired_config.rb | 83 ++++ .../remote_development/bm_files.rb | 133 ++++++ .../remote_development/models/bm_workspace.rb | 30 ++ .../models/bm_workspace_agent.rb | 22 + .../models/bm_workspace_agent_config.rb | 21 + .../models/bm_workspace_agentk_state.rb | 15 + .../settings/bm_default_devfile.yaml | 7 + .../workspace_operations/bm_desired_config.rb | 56 +++ .../workspace_operations/bm_states.rb | 55 +++ .../bm_workspace_operations_constants.rb | 44 ++ .../create/bm_create_constants.rb | 44 ++ ...nternal_poststart_command_clone_project.sh | 45 ++ ...ernal_poststart_command_clone_unshallow.sh | 38 ++ ...ommand_sleep_until_workspace_is_running.sh | 12 + ...m_internal_poststart_command_start_sshd.sh | 13 + ...internal_poststart_command_start_vscode.sh | 101 ++++ ...m_main_component_updater_container_args.sh | 1 + ...orkspace_variables_git_credential_store.sh | 18 + .../bm_config_values_extractor.rb | 117 +++++ .../bm_desired_config_yaml_parser.rb | 29 ++ .../bm_devfile_parser_getter.rb | 74 +++ .../bm_devfile_resource_appender.rb | 435 ++++++++++++++++++ .../bm_devfile_resource_modifier.rb | 235 ++++++++++ ...ubernetes_legacy_poststart_hook_command.sh | 5 + .../bm_kubernetes_poststart_hook_command.sh | 21 + .../bm_kubernetes_poststart_hook_inserter.rb | 78 ++++ .../create/desired_config/bm_main.rb | 61 +++ .../bm_scripts_configmap_appender.rb | 163 +++++++ .../bm_scripts_volume_inserter.rb | 50 ++ 29 files changed, 2006 insertions(+) create mode 100644 lib/gitlab/background_migration/remote_development/bm_create_desired_config.rb create mode 100644 lib/gitlab/background_migration/remote_development/bm_files.rb create mode 100644 lib/gitlab/background_migration/remote_development/models/bm_workspace.rb create mode 100644 lib/gitlab/background_migration/remote_development/models/bm_workspace_agent.rb create mode 100644 lib/gitlab/background_migration/remote_development/models/bm_workspace_agent_config.rb create mode 100644 lib/gitlab/background_migration/remote_development/models/bm_workspace_agentk_state.rb create mode 100644 lib/gitlab/background_migration/remote_development/settings/bm_default_devfile.yaml create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/bm_desired_config.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/bm_states.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/bm_workspace_operations_constants.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_create_constants.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_project.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_unshallow.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_sleep_until_workspace_is_running.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_sshd.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_vscode.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_main_component_updater_container_args.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_workspace_variables_git_credential_store.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_config_values_extractor.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_desired_config_yaml_parser.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_parser_getter.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_appender.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_modifier.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_legacy_poststart_hook_command.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_command.sh create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_inserter.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_main.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_configmap_appender.rb create mode 100644 lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_volume_inserter.rb diff --git a/lib/gitlab/background_migration/remote_development/bm_create_desired_config.rb b/lib/gitlab/background_migration/remote_development/bm_create_desired_config.rb new file mode 100644 index 00000000000000..e9001f87e952c1 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/bm_create_desired_config.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + class BmCreateDesiredConfig # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + # @param [Integer] workspace_id + # @param [Boolean] dry_run + # @return [Void] + def self.create_and_save(workspace_id:, dry_run: false) + workspace = RemoteDevelopment::Models::BmWorkspace.find(workspace_id) + + result = BackgroundMigration::RemoteDevelopment::WorkspaceOperations::Create::DesiredConfig::BmMain.main( + { + params: { + agent: workspace.agent + }, + workspace: workspace, + logger: logger + } + ) + + validate_and_create_workspace_agentk_state( + workspace: workspace, + desired_config: result[:desired_config], + logger: logger, + dry_run: dry_run + ) + end + + # @param [BackgroundMigration::RemoteDevelopment::Models::BMWorkspace] workspace + # @param [BackgroundMigration::RemoteDevelopment::WorkspaceOperations::BMDesiredConfig] desired_config + # @param [Gitlab::BackgroundMigration::Logger] logger + # @param [Boolean] dry_run + # @return [Void] + def self.validate_and_create_workspace_agentk_state(workspace:, desired_config:, logger:, dry_run:) # rubocop:disable Metrics/MethodLength -- need it big + if dry_run + puts "For workspace_id #{workspace.id}" + puts "Valid desired_config? #{desired_config.valid?}" + desired_config.errors.full_messages.each do |message| + puts message + end + end + + unless desired_config.valid? + logger.error( + message: "desired_config is invalid", + error_type: "workspace_agentk_state_migration_error", + workspace_id: workspace.id, + validation_error: desired_config.errors.full_messages + ) + + return + end + + if dry_run + workspace_agentk_state = RemoteDevelopment::Models::BmWorkspaceAgentkState.new( + workspace_id: workspace.id, + project_id: workspace.project_id, + desired_config: desired_config + ) + puts "Valid state model? #{workspace_agentk_state.valid?}" + + workspace_agentk_state.errors.full_messages.each do |message| + puts message + end + else + RemoteDevelopment::Models::BmWorkspaceAgentkState.create!( + workspace_id: workspace.id, + project_id: workspace.project_id, + desired_config: desired_config + ) + end + end + + # @return [Gitlab::BackgroundMigration::Logger] + def self.logger + @logger ||= Gitlab::BackgroundMigration::Logger.build + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/bm_files.rb b/lib/gitlab/background_migration/remote_development/bm_files.rb new file mode 100644 index 00000000000000..de32273d1b33b8 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/bm_files.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + # This module contains constants for all the files (default devfile, shell scripts, script fragments, commands, + # etc) + # that are used in the Remote Development domain. They are pulled out to separate files instead of being hardcoded + # via inline HEREDOC or other means, so that they can have full support for + # syntax highlighting, refactoring, linting, etc. + module BmFiles + # @param [String] path - file path relative to domain logic root (this directory, `ee/lib/remote_development`) + # @return [String] content of the file + def self.read_file(path) + File.read(File.join(__dir__, path)) + end + + # @return [String] content of the file + def self.default_devfile_yaml + # When updating DEFAULT_DEVFILE_YAML contents in `bm_default_devfile.yaml`, update the user facing doc as well + # https://docs.gitlab.com/ee/user/workspace/#gitlab-default-devfile + # + # The container image is pinned to linux/amd64 digest, instead of the tag digest. + # This is to prevent Rancher Desktop from pulling the linux/arm64 architecture of the image + # which will disrupt local development since vscode fork and workspace tools image does not support + # that architecture yet and thus the workspace won't start. + # This will be fixed in https://gitlab.com/gitlab-org/gitlab/-/issues/550128 + read_file("settings/bm_default_devfile.yaml") + end + + # @return [String] content of the file + def self.git_credential_store_script + read_file("workspace_operations/create/bm_workspace_variables_git_credential_store.sh") + end + + # @return [String] content of the file + def self.kubernetes_legacy_poststart_hook_command + read_file("workspace_operations/create/desired_config/bm_kubernetes_legacy_poststart_hook_command.sh") + end + + # @return [String] content of the file + def self.kubernetes_poststart_hook_command + read_file("workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_command.sh") + end + + # @return [String] content of the file + def self.main_component_updater_container_args + read_file("workspace_operations/create/bm_main_component_updater_container_args.sh") + end + + # @return [String] content of the file + def self.internal_poststart_command_start_vscode_script + read_file("workspace_operations/create/bm_internal_poststart_command_start_vscode.sh") + end + + # @return [String] content of the file + def self.internal_poststart_command_sleep_until_workspace_is_running_script + read_file("workspace_operations/create/bm_internal_poststart_command_sleep_until_workspace_is_running.sh") + end + + # @return [String] content of the file + def self.internal_poststart_command_start_sshd_script + read_file("workspace_operations/create/bm_internal_poststart_command_start_sshd.sh") + end + + # @return [String] content of the file + def self.internal_poststart_command_clone_project_script + read_file("workspace_operations/create/bm_internal_poststart_command_clone_project.sh") + end + + # @return [String] content of the file + def self.internal_poststart_command_clone_unshallow_script + read_file("workspace_operations/create/bm_internal_poststart_command_clone_unshallow.sh") + end + + #################################### + # Please keep this list alphabetized + #################################### + + # NOTE: We intentionally duplicate these explicit declaration of constants in addition to dynamically redefining + # them in `reload_constants!` method below. This is because we want them to be resolve-able in IDEs, and + # if + # we only define them dynamically, they will not be recognized by IDEs. + DEFAULT_DEVFILE_YAML = default_devfile_yaml + GIT_CREDENTIAL_STORE_SCRIPT = git_credential_store_script + INTERNAL_POSTSTART_COMMAND_CLONE_PROJECT_SCRIPT = internal_poststart_command_clone_project_script + INTERNAL_POSTSTART_COMMAND_CLONE_UNSHALLOW_SCRIPT = internal_poststart_command_clone_unshallow_script + INTERNAL_POSTSTART_COMMAND_START_VSCODE_SCRIPT = internal_poststart_command_start_vscode_script + INTERNAL_POSTSTART_COMMAND_SLEEP_UNTIL_WORKSPACE_IS_RUNNING_SCRIPT = + internal_poststart_command_sleep_until_workspace_is_running_script + INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT = internal_poststart_command_start_sshd_script + KUBERNETES_LEGACY_POSTSTART_HOOK_COMMAND = kubernetes_legacy_poststart_hook_command + KUBERNETES_POSTSTART_HOOK_COMMAND = kubernetes_poststart_hook_command + MAIN_COMPONENT_UPDATER_CONTAINER_ARGS = main_component_updater_container_args + + # @return [Array] + def self.all_expected_file_constants + # NOTE: We explicitly keep a duplicate list of the defined constants, to ensure that we keep both the explicit + # declarations above and the dynamically defined ones in reload_constants! in sync. + [ + :DEFAULT_DEVFILE_YAML, + :GIT_CREDENTIAL_STORE_SCRIPT, + :INTERNAL_POSTSTART_COMMAND_CLONE_PROJECT_SCRIPT, + :INTERNAL_POSTSTART_COMMAND_CLONE_UNSHALLOW_SCRIPT, + :INTERNAL_POSTSTART_COMMAND_START_VSCODE_SCRIPT, + :INTERNAL_POSTSTART_COMMAND_SLEEP_UNTIL_WORKSPACE_IS_RUNNING_SCRIPT, + :INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT, + :KUBERNETES_LEGACY_POSTSTART_HOOK_COMMAND, + :KUBERNETES_POSTSTART_HOOK_COMMAND, + :MAIN_COMPONENT_UPDATER_CONTAINER_ARGS + ] + end + + # @return [void] + def self.reload_constants! + expected_count = 10 # Update this count if you add/remove constants + raise "File constants count mismatch!" unless all_expected_file_constants.count == expected_count + + all_expected_file_constants.each do |const_name| + # If you get an exception on this line, update the `all_file_constants` method above + remove_const(const_name) + method_name = const_name.to_s.downcase + const_set(const_name, public_method(method_name).call) + end + end + + private_class_method :all_expected_file_constants, :read_file + + reload_constants! + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/models/bm_workspace.rb b/lib/gitlab/background_migration/remote_development/models/bm_workspace.rb new file mode 100644 index 00000000000000..35f8c33ddfb1a7 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/models/bm_workspace.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module Models + # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + class BmWorkspace < ::Gitlab::Database::Migration[2.3]::MigrationRecord + include WorkspaceOperations::BmStates + + self.table_name = 'workspaces' + + belongs_to :agent, class_name: "Clusters::Agent", foreign_key: "cluster_agent_id", inverse_of: :workspaces + + # @return [Boolean] + def desired_state_running? + desired_state == RUNNING + end + + # @return [BackgroundMigration::Models::BmWorkspaceAgentConfig] + def workspaces_agent_config + agent.unversioned_latest_workspaces_agent_config + end + end + + # rubocop:enable Migration/BatchedMigrationBaseClass + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent.rb b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent.rb new file mode 100644 index 00000000000000..c159af809b2447 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module Models + # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + class BmWorkspaceAgent < ::Gitlab::Database::Migration[2.3]::MigrationRecord + include WorkspaceOperations::BmStates + + self.table_name = 'cluster_agents' + + has_one :unversioned_latest_workspaces_agent_config, + class_name: 'RemoteDevelopment::WorkspacesAgentConfig', + inverse_of: :agent, + foreign_key: :cluster_agent_id + end + # rubocop:enable Migration/BatchedMigrationBaseClass + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent_config.rb b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent_config.rb new file mode 100644 index 00000000000000..2cdf5252bd5007 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agent_config.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module Models + # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + class BmWorkspaceAgentConfig < ::Gitlab::Database::Migration[2.3]::MigrationRecord + include WorkspaceOperations::BmStates + + self.table_name = 'workspace_agent_configs' + + belongs_to :agent, + class_name: 'Clusters::Agent', foreign_key: 'cluster_agent_id', + inverse_of: :unversioned_latest_workspaces_agent_config + end + # rubocop:enable Migration/BatchedMigrationBaseClass + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/models/bm_workspace_agentk_state.rb b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agentk_state.rb new file mode 100644 index 00000000000000..915ad8fb34a8b7 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/models/bm_workspace_agentk_state.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module Models + # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + class BmWorkspaceAgentkState < ::Gitlab::Database::Migration[2.3]::MigrationRecord + self.table_name = 'workspace_agentk_states' + end + # rubocop:enable Migration/BatchedMigrationBaseClass + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/settings/bm_default_devfile.yaml b/lib/gitlab/background_migration/remote_development/settings/bm_default_devfile.yaml new file mode 100644 index 00000000000000..744beabb5376ed --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/settings/bm_default_devfile.yaml @@ -0,0 +1,7 @@ +schemaVersion: "%s" +components: + - name: development-environment + attributes: + gl/inject-editor: true + container: + image: "registry.gitlab.com/gitlab-org/gitlab-build-images/workspaces/ubuntu-24.04:20250414234733-golang-1.23-node-23.9-yarn-1.22-ruby-3.4.2-rust-1.85-php-8.4.5-java-21.0.6-python-3.13-docker-27.5.1@sha256:3b3fb1374084a20349019b88302fcc8ace1a3de5ab09465668d09f95a0eaa34b" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/bm_desired_config.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_desired_config.rb new file mode 100644 index 00000000000000..262517fb77408b --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_desired_config.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "hashdiff" + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + class BmDesiredConfig # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + include ActiveModel::Serialization + include ActiveModel::Serializers::JSON + + # @!attribute [rw] desired_config_array + # @return [Array] + attribute :desired_config_array + + validates :desired_config_array, presence: true, json_schema: { + filename: 'workspaces_kubernetes', + detail_errors: true, + size_limit: 64.kilobytes + } + + # @param [BmDesiredConfig] other + # @return [Boolean] + def ==(other) + return false unless other.is_a?(self.class) + return true if equal?(other) + + desired_config_array == other.desired_config_array + end + + # @param [BmDesiredConfig] other + # @return [Array] + def diff(other) + raise ArgumentError, "Expected #{self.class}, got #{other.class}" unless other.is_a?(self.class) + + # we do not want to calculate diff using the longest common subsequence + # because we want to catch changes at the index of self rather than find + # the common elements between the two arrays. This example should help explain + # the difference https://github.com/liufengyun/hashdiff/issues/43#issuecomment-485497196 + # noinspection RubyMismatchedArgumentType -- hashdiff also supports arrays + Hashdiff.diff(desired_config_array, other.desired_config_array, use_lcs: false) + end + + # @return [Array] + def symbolized_desired_config_array + as_json.fetch("desired_config_array").map(&:deep_symbolize_keys) + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/bm_states.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_states.rb new file mode 100644 index 00000000000000..dfef404a7f2a85 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_states.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module BmStates + CREATION_REQUESTED = 'CreationRequested' + STARTING = 'Starting' + RESTART_REQUESTED = 'RestartRequested' + RUNNING = 'Running' + STOPPING = 'Stopping' + STOPPED = 'Stopped' + TERMINATING = 'Terminating' + TERMINATED = 'Terminated' + FAILED = 'Failed' + ERROR = 'Error' + UNKNOWN = 'Unknown' + + VALID_DESIRED_STATES = [ + RUNNING, + RESTART_REQUESTED, + STOPPED, + TERMINATED + ].freeze + + VALID_ACTUAL_STATES = [ + CREATION_REQUESTED, + STARTING, + RUNNING, + STOPPING, + STOPPED, + TERMINATING, + TERMINATED, + FAILED, + ERROR, + UNKNOWN + ].freeze + + # @param [String] state + # @return [TrueClass, FalseClass] + def valid_desired_state?(state) + VALID_DESIRED_STATES.include?(state) + end + + # @param [String] state + # @return [TrueClass, FalseClass] + def valid_actual_state?(state) + VALID_ACTUAL_STATES.include?(state) + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/bm_workspace_operations_constants.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_workspace_operations_constants.rb new file mode 100644 index 00000000000000..20b7311989e807 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/bm_workspace_operations_constants.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + # NOTE: Constants are scoped to the namespace in which they are used in production + # code (but they may still be referenced by specs or fixtures or factories). + # For example, this RemoteDevelopment::BMWorkspaceOperations::BmWorkspaceOperationsConstants + # file only contains constants which are used by multiple sub-namespaces + # of BMWorkspaceOperations, such as Create and Reconcile. + # Constants which are only used by a specific use-case sub-namespace + # like Create or Reconcile should be contained in the corresponding + # constants class such as BmCreateConstants or ReconcileConstants. + # + # Multiple related constants may be declared in their own dedicated + # namespace, such as RemoteDevelopment::BMWorkspaceOperations::BmStates. + # + # See documentation at ../README.md#constant-declarations for more information. + module BmWorkspaceOperationsConstants + # Please keep alphabetized + ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION = :"workspaces.gitlab.com/include-in-partial-reconciliation" + ENV_VAR_SECRET_SUFFIX = "-env-var" + FILE_SECRET_SUFFIX = "-file" + INTERNAL_COMMAND_LABEL = "gl-internal" + INTERNAL_BLOCKING_COMMAND_LABEL = "#{INTERNAL_COMMAND_LABEL}-blocking".freeze + SECRETS_INVENTORY = "-secrets-inventory" + VARIABLES_VOLUME_DEFAULT_MODE = 0o774 + VARIABLES_VOLUME_NAME = "gl-workspace-variables" + VARIABLES_VOLUME_PATH = "/.workspace-data/variables/file" + WORKSPACE_DATA_VOLUME_PATH = "/projects" + WORKSPACE_INVENTORY = "-workspace-inventory" + WORKSPACE_LOGS_DIR = "#{WORKSPACE_DATA_VOLUME_PATH}/workspace-logs".freeze + WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_NAME = "gl_workspace_reconciled_actual_state.txt" + WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_PATH = + "#{VARIABLES_VOLUME_PATH}/#{WORKSPACE_RECONCILED_ACTUAL_STATE_FILE_NAME}".freeze + # Image digest used to avoid arm64 compatibility issues in local development + # See https://gitlab.com/gitlab-org/gitlab/-/issues/550128 for tracking arm64 support + WORKSPACE_TOOLS_IMAGE = "registry.gitlab.com/gitlab-org/gitlab-build-images:20250627091546-workspaces-tools@sha256:9bf96edd6a7e64ee898d774f55e153f78b85e2a911e565158e374efdd2def2c5" # rubocop:disable Layout/LineLength, Lint/RedundantCopDisableDirective -- Docker image should not be in multi-lines + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_create_constants.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_create_constants.rb new file mode 100644 index 00000000000000..a7ad87e0690a43 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_create_constants.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + # NOTE: Constants are scoped to the use-case namespace in which they are used in production + # code (but they may still be referenced by specs or fixtures or factories). + # For example, this RemoteDevelopment::BMWorkspaceOperations::Create::BmCreateConstants + # file contains constants which are only used by classes within that namespace. + # + # See documentation at ../../README.md#constant-declarations for more information. + module BmCreateConstants + include BmWorkspaceOperationsConstants + + # Please keep alphabetized + GIT_CREDENTIAL_STORE_SCRIPT_FILE_NAME = "gl_git_credential_store.sh" + GIT_CREDENTIAL_STORE_SCRIPT_FILE_PATH = + "#{VARIABLES_VOLUME_PATH}/#{GIT_CREDENTIAL_STORE_SCRIPT_FILE_NAME}".freeze + LEGACY_RUN_POSTSTART_COMMANDS_SCRIPT_NAME = "gl-run-poststart-commands.sh" + NAMESPACE_PREFIX = "gl-rd-ns" + PROJECT_CLONING_SUCCESSFUL_FILE_NAME = ".gl_project_cloning_successful" + CLONE_DEPTH_OPTION = "--depth 10" + RUN_AS_USER = 5001 + RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME = "gl-run-internal-blocking-poststart-commands.sh" + RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME = "gl-run-non-blocking-poststart-commands.sh" + TOKEN_FILE_NAME = "gl_token" + TOKEN_FILE_PATH = "#{VARIABLES_VOLUME_PATH}/#{TOKEN_FILE_NAME}".freeze + TOOLS_DIR_NAME = ".gl-tools" + TOOLS_DIR_ENV_VAR = "GL_TOOLS_DIR" + TOOLS_INJECTOR_COMPONENT_NAME = "gl-tools-injector" + WORKSPACE_DATA_VOLUME_NAME = "gl-workspace-data" + WORKSPACE_EDITOR_PORT = 60001 + WORKSPACE_SCRIPTS_VOLUME_DEFAULT_MODE = 0o555 + WORKSPACE_SCRIPTS_VOLUME_NAME = "gl-workspace-scripts" + WORKSPACE_SCRIPTS_VOLUME_PATH = "/workspace-scripts" + WORKSPACE_SSH_PORT = 60022 + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_project.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_project.sh new file mode 100644 index 00000000000000..6480bc1ac63cea --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_project.sh @@ -0,0 +1,45 @@ +#!/bin/sh +echo "$(date -Iseconds): ----------------------------------------" +echo "$(date -Iseconds): Cloning project if necessary..." + +# The project should be cloned only if one is not cloned successfully already. +# This is required to avoid resetting user's modifications to the files. +# This is achieved by checking for the existence of a file before cloning. +# If the file does not exist, clone the project. +if [ -f "%s" ] +then + echo "$(date -Iseconds): Project cloning was already successful, because '%s' file already exists" + echo "$(date -Iseconds): ----------------------------------------" + exit 0 +fi + +# To accommodate for scenarios where the project cloning failed midway in the previous attempt, +# remove the directory before cloning. +if [ -d "%s" ] +then + echo "$(date -Iseconds): Removing unsuccessfully cloned project directory" + rm -rf "%s" +fi + +clone_depth_option="%s" +depth_msg="${clone_depth_option:+ with \"${clone_depth_option}\" option}" + +echo "$(date -Iseconds): Cloning project${depth_msg}" +git clone $clone_depth_option --branch "%s" "%s" "%s" +exit_code=$? + +# Once cloning is successful, create the file which is used in the check above. +# This will ensure the project is not cloned again on restarts. +if [ "${exit_code}" -eq 0 ] +then + echo "$(date -Iseconds): Project cloning successful" + touch "%s" + echo "$(date -Iseconds): Updated '%s' file to indicate successful project cloning" + echo "$(date -Iseconds): Successfully finished cloning project." +else + echo "$(date -Iseconds): Project cloning failed with exit code: ${exit_code}" >&2 + echo "$(date -Iseconds): Failed to clone project, exit code was ${exit_code}" +fi + +echo "$(date -Iseconds): ----------------------------------------" +exit "${exit_code}" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_unshallow.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_unshallow.sh new file mode 100644 index 00000000000000..4f8d7c1db61a0f --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_clone_unshallow.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +echo "$(date -Iseconds): ----------------------------------------" +echo "$(date -Iseconds): Spawning background process to unshallow repo if necessary..." + +if [ ! -f "%s" ]; then + echo "$(date -Iseconds): Project cloning previously failed. Unshallow skipped" + echo "$(date -Iseconds): ----------------------------------------" + exit 0 +fi + +# shellcheck disable=SC2164 # We assume that 'clone_dir' must exist if 'project_cloning_successful_file' exists +cd "%s" + +if [ "$(git rev-parse --is-shallow-repository)" != "true" ]; then + echo "$(date -Iseconds): Repository is not shallow, skipping unshallow" + echo "$(date -Iseconds): ----------------------------------------" + exit 0 +fi + +echo "$(date -Iseconds): Repository is shallow, proceeding with unshallow" +UNSHALLOW_LOG_FILE="${GL_WORKSPACE_LOGS_DIR}/clone-unshallow.log" + +echo "$(date -Iseconds): Starting unshallow in background, with output written to ${UNSHALLOW_LOG_FILE}" +{ + echo "$(date -Iseconds): ----------------------------------------" + echo "$(date -Iseconds): Starting unshallow in background" + if git fetch --unshallow --progress 2>&1; then + echo "$(date -Iseconds): Unshallow completed successfully" + else + echo "$(date -Iseconds): Unshallow failed with exit code $?" + fi + echo "$(date -Iseconds): ----------------------------------------" +} >> "${UNSHALLOW_LOG_FILE}" & + +echo "$(date -Iseconds): Finished spawning background process to unshallow repo." +echo "$(date -Iseconds): ----------------------------------------" +exit 0 diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_sleep_until_workspace_is_running.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_sleep_until_workspace_is_running.sh new file mode 100644 index 00000000000000..578ca14bc35e34 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_sleep_until_workspace_is_running.sh @@ -0,0 +1,12 @@ +#!/bin/sh +echo "$(date -Iseconds): ----------------------------------------" +echo "$(date -Iseconds): Sleeping until workspace is running..." +time_to_sleep=5 +status_file="%s" +while [ "$(cat ${status_file})" != "Running" ]; do + echo "$(date -Iseconds): Workspace state is '$(cat ${status_file})' from status file '${status_file}'. Blocking remaining postStart events execution for ${time_to_sleep} seconds until state is 'Running'..." + sleep ${time_to_sleep} +done +echo "$(date -Iseconds): Workspace state is now 'Running', continuing postStart hook execution." +echo "$(date -Iseconds): Finished sleeping until workspace is running." +echo "$(date -Iseconds): ----------------------------------------" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_sshd.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_sshd.sh new file mode 100644 index 00000000000000..ba3b2b30ff4b92 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_sshd.sh @@ -0,0 +1,13 @@ +#!/bin/sh +echo "$(date -Iseconds): ----------------------------------------" +echo "$(date -Iseconds): Starting sshd in background if it is found..." +sshd_path=$(which sshd) +if [ -x "${sshd_path}" ]; then + echo "$(date -Iseconds): Starting ${sshd_path} in background on port ${GL_SSH_PORT} with output written to ${GL_WORKSPACE_LOGS_DIR}/start-sshd.log" + "${sshd_path}" -D -p "${GL_SSH_PORT}" >> "${GL_WORKSPACE_LOGS_DIR}/start-sshd.log" 2>&1 & + echo "$(date -Iseconds): Finished starting sshd in background if it is found." +else + echo "$(date -Iseconds): 'sshd' not found in path. Not starting SSH server." >&2 + echo "$(date -Iseconds): Failed to start sshd, no sshd executable found" +fi +echo "$(date -Iseconds): ----------------------------------------" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_vscode.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_vscode.sh new file mode 100644 index 00000000000000..75d68ac2c18642 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_internal_poststart_command_start_vscode.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +echo "$(date -Iseconds): ----------------------------------------" +echo "$(date -Iseconds): Starting GitLab Fork of VS Code server in background with output written to ${GL_WORKSPACE_LOGS_DIR}/start-vscode.log..." + +# Define log file path +LOG_FILE="${GL_WORKSPACE_LOGS_DIR}/start-vscode.log" + +mkdir -p "$(dirname "${LOG_FILE}")" + +echo "$(date -Iseconds): VS Code initialization started" + +# Start logging +exec 1>>"${LOG_FILE}" 2>&1 + +# # This script initilizes the tools injected into the workspace on startup. +# # +# # It uses the following environment variables +# # $GL_TOOLS_DIR - directory where the tools are copied. +# # $GL_VSCODE_LOG_LEVEL - log level for the server. defaults to "info". +# # $GL_VSCODE_PORT - port on which the server is exposed. defaults to "60001". +# # $GL_VSCODE_IGNORE_VERSION_MISMATCH - if set to true, the server works even when server and WebIDE versions do not match. +# # $GL_VSCODE_ENABLE_MARKETPLACE - if set to true, set configuration to enable marketplace. +# # $GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL - service url for the extensions marketplace. +# # $GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL - item url for the extensions marketplace. +# # $GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE - resource url template for the extensions marketplace. +# # $GITLAB_WORKFLOW_TOKEN - a giltab personal access token to configure the gitlab vscode extension. +# # $GITLAB_WORKFLOW_TOKEN_FILE - the contents of this file populate GITLAB_WORKFLOW_TOKEN if it is not set. + +if [ -z "${GL_TOOLS_DIR}" ]; then + echo "$(date -Iseconds): \$GL_TOOLS_DIR is not set" + exit 1 +fi + +if [ -z "${GL_VSCODE_LOG_LEVEL}" ]; then + GL_VSCODE_LOG_LEVEL="info" + echo "$(date -Iseconds): Setting default GL_VSCODE_LOG_LEVEL=${GL_VSCODE_LOG_LEVEL}" +fi + +if [ -z "${GL_VSCODE_PORT}" ]; then + GL_VSCODE_PORT="60001" + echo "$(date -Iseconds): Setting default GL_VSCODE_PORT=${GL_VSCODE_PORT}" +fi + +if [ -z "${GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL}" ]; then + GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL="https://open-vsx.org/vscode/gallery" + echo "$(date -Iseconds): Setting default GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL=${GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL}" +fi + +if [ -z "${GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL}" ]; then + GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL="https://open-vsx.org/vscode/item" + echo "$(date -Iseconds): Setting default GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL=${GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL}" +fi + +if [ -z "${GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE}" ]; then + GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE="https://open-vsx.org/api/{publisher}/{name}/{version}/file/{path}" + echo "$(date -Iseconds): Setting default GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE=${GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE}" +fi + +PRODUCT_JSON_FILE="${GL_TOOLS_DIR}/vscode-reh-web/product.json" + +if [ "$GL_VSCODE_IGNORE_VERSION_MISMATCH" = true ]; then + # TODO: remove this section once issue is fixed - https://gitlab.com/gitlab-org/gitlab/-/issues/373669 + # remove "commit" key from product.json to avoid client-server mismatch + # TODO: remove this once we are not worried about version mismatch + # https://gitlab.com/gitlab-org/gitlab/-/issues/373669 + echo "$(date -Iseconds): Ignoring VS Code client-server version mismatch" + sed -i.bak '/"commit"/d' "${PRODUCT_JSON_FILE}" && rm "${PRODUCT_JSON_FILE}.bak" + echo "$(date -Iseconds): Removed 'commit' key from ${PRODUCT_JSON_FILE}" +fi + +if [ "$GL_VSCODE_ENABLE_MARKETPLACE" = true ]; then + EXTENSIONS_GALLERY_KEY="{\\n\\t\"extensionsGallery\": {\\n\\t\\t\"serviceUrl\": \"${GL_VSCODE_EXTENSION_MARKETPLACE_SERVICE_URL}\",\\n\\t\\t\"itemUrl\": \"${GL_VSCODE_EXTENSION_MARKETPLACE_ITEM_URL}\",\\n\\t\\t\"resourceUrlTemplate\": \"${GL_VSCODE_EXTENSION_MARKETPLACE_RESOURCE_URL_TEMPLATE}\"\\n\\t}," + echo "$(date -Iseconds): '${EXTENSIONS_GALLERY_KEY}' in '${PRODUCT_JSON_FILE}' at the beginning of the file" + sed -i.bak "1s|.*|$EXTENSIONS_GALLERY_KEY|" "${PRODUCT_JSON_FILE}" && rm "${PRODUCT_JSON_FILE}.bak" + echo "$(date -Iseconds): Extensions gallery configuration added" +fi + +echo "$(date -Iseconds): Contents of ${PRODUCT_JSON_FILE} are: " +cat "${PRODUCT_JSON_FILE}" +echo + +GL_VSCODE_HOST="0.0.0.0" + +echo "$(date -Iseconds): Starting server for the editor with:" +echo "$(date -Iseconds): - Host: ${GL_VSCODE_HOST}" +echo "$(date -Iseconds): - Port: ${GL_VSCODE_PORT}" +echo "$(date -Iseconds): - Log level: ${GL_VSCODE_LOG_LEVEL}" +echo "$(date -Iseconds): - Without connection token: yes" +echo "$(date -Iseconds): - Workspace trust disabled: yes" + +# The server execution is backgrounded to allow for the rest of the internal init scripts to execute. +"${GL_TOOLS_DIR}/vscode-reh-web/bin/gitlab-webide-server" \ + --host "${GL_VSCODE_HOST}" \ + --port "${GL_VSCODE_PORT}" \ + --log "${GL_VSCODE_LOG_LEVEL}" \ + --without-connection-token \ + --disable-workspace-trust & + +echo "$(date -Iseconds): Finished starting GitLab Fork of VS Code server in background" +echo "$(date -Iseconds): ----------------------------------------" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_main_component_updater_container_args.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_main_component_updater_container_args.sh new file mode 100644 index 00000000000000..cc66f385651346 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_main_component_updater_container_args.sh @@ -0,0 +1 @@ +tail -f /dev/null diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_workspace_variables_git_credential_store.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_workspace_variables_git_credential_store.sh new file mode 100644 index 00000000000000..517eeb6cee4ca1 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_workspace_variables_git_credential_store.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# This is a readonly store so we can exit cleanly when git attempts a store or erase action +if [ "$1" != "get" ]; +then + exit 0 +fi + +if [ -z "${GL_TOKEN_FILE_PATH}" ]; +then + echo "$(date -Iseconds): We could not find the GL_TOKEN_FILE_PATH variable" >&2 + exit 1 +fi +password=$(cat "${GL_TOKEN_FILE_PATH}") + +# The username is derived from the "user.email" configuration item. Ensure it is set. +echo "username=does-not-matter" +echo "password=${password}" +exit 0 diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_config_values_extractor.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_config_values_extractor.rb new file mode 100644 index 00000000000000..159d88ea9e2f28 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_config_values_extractor.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmConfigValuesExtractor # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmStates + include BmWorkspaceOperationsConstants + + # rubocop:disable Metrics/MethodLength -- this is a won't fix + # @param [Hash] context + # @return [Hash] + def self.extract(context) # rubocop:disable Metrics/AbcSize -- this is a won't fix + 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) + 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) + + 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? + + workspace_inventory_name = "#{workspace_name}#{WORKSPACE_INVENTORY}" + secrets_inventory_name = "#{workspace_name}#{SECRETS_INVENTORY}" + + extra_annotations = { + "workspaces.gitlab.com/host-template": domain_template.to_s, + "workspaces.gitlab.com/id": workspace_id.to_s, + # NOTE: This annotation is added to cause the workspace to restart whenever the max resources change + "workspaces.gitlab.com/max-resources-per-workspace-sha256": max_resources_per_workspace_sha256 + } + partial_reconcile_annotation = { ANNOTATION_KEY_INCLUDE_IN_PARTIAL_RECONCILIATION => "true" } + agent_annotations = workspaces_agent_config.annotations + common_annotations = deep_sort_and_symbolize_hashes(agent_annotations.merge(extra_annotations)) + common_annotations_for_partial_reconciliation = + deep_sort_and_symbolize_hashes(common_annotations.merge(partial_reconcile_annotation)) + secrets_inventory_annotations = deep_sort_and_symbolize_hashes( + common_annotations.merge("config.k8s.io/owning-inventory": secrets_inventory_name) + ) + workspace_inventory_annotations = deep_sort_and_symbolize_hashes( + common_annotations.merge("config.k8s.io/owning-inventory": workspace_inventory_name) + ) + workspace_inventory_annotations_for_partial_reconciliation = deep_sort_and_symbolize_hashes( + workspace_inventory_annotations.merge(partial_reconcile_annotation) + ) + + agent_labels = workspaces_agent_config.labels + labels = agent_labels.merge({ "agent.gitlab.com/id": workspaces_agent_id.to_s }) + # TODO: Unconditionally add this label in https://gitlab.com/gitlab-org/gitlab/-/issues/535197 + labels["workspaces.gitlab.com/id"] = workspace_id.to_s if shared_namespace.present? + + scripts_configmap_name = "#{workspace_name}-scripts-configmap" + + context.merge({ + # Please keep alphabetized + allow_privilege_escalation: workspaces_agent_config.allow_privilege_escalation, + common_annotations: common_annotations, + common_annotations_for_partial_reconciliation: common_annotations_for_partial_reconciliation, + default_resources_per_workspace_container: default_resources_per_workspace_container, + default_runtime_class: workspaces_agent_config.default_runtime_class, + domain_template: domain_template, + env_secret_name: "#{workspace_name}#{ENV_VAR_SECRET_SUFFIX}", + file_secret_name: "#{workspace_name}#{FILE_SECRET_SUFFIX}", + gitlab_workspaces_proxy_namespace: workspaces_agent_config.gitlab_workspaces_proxy_namespace, + image_pull_secrets: deep_sort_and_symbolize_hashes(workspaces_agent_config.image_pull_secrets), + labels: deep_sort_and_symbolize_hashes(labels), + max_resources_per_workspace: max_resources_per_workspace, + network_policy_egress: deep_sort_and_symbolize_hashes(workspaces_agent_config.network_policy_egress), + 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: secrets_inventory_annotations, + secrets_inventory_name: secrets_inventory_name, + shared_namespace: shared_namespace, + use_kubernetes_user_namespaces: workspaces_agent_config.use_kubernetes_user_namespaces, + workspace_inventory_annotations: workspace_inventory_annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, + workspace_inventory_name: workspace_inventory_name + }).sort.to_h + end + # rubocop:enable Metrics/MethodLength + + # @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 + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_desired_config_yaml_parser.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_desired_config_yaml_parser.rb new file mode 100644 index 00000000000000..9408fe06e6edd4 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_desired_config_yaml_parser.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmDesiredConfigYamlParser # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + # @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 + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_parser_getter.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_parser_getter.rb new file mode 100644 index 00000000000000..e9e7ee8770c949 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_parser_getter.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmDevfileParserGetter # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmWorkspaceOperationsConstants + + # @param [Hash] context + # @return [Hash] + def self.get(context) # rubocop:disable Metrics/MethodLength -- This method is complex but we don't need to break it up. + context => { + logger: logger, + processed_devfile_yaml: processed_devfile_yaml, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, + domain_template: domain_template, + labels: labels, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace, + replicas: replicas + } + + 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(workspace_inventory_annotations_for_partial_reconciliation.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 + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_appender.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_appender.rb new file mode 100644 index 00000000000000..5646dd339e15fb --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_appender.rb @@ -0,0 +1,435 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + # rubocop:disable Metrics/MethodLength -- The original method is copied from ee/lib/remotedevelopment + # rubocop:disable Metrics/ClassLength -- The original class is copied from ee/lib/remotedevelopment + class BmDevfileResourceAppender # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmWorkspaceOperationsConstants + + # @param [Hash] context + # @return [Hash] + def self.append(context) + context => { + common_annotations: common_annotations, + common_annotations_for_partial_reconciliation: common_annotations_for_partial_reconciliation, + desired_config_array: desired_config_array, + env_secret_name: env_secret_name, + file_secret_name: file_secret_name, + gitlab_workspaces_proxy_namespace: gitlab_workspaces_proxy_namespace, + image_pull_secrets: image_pull_secrets, + labels: labels, + max_resources_per_workspace: max_resources_per_workspace, + network_policy_egress: network_policy_egress, + network_policy_enabled: network_policy_enabled, + processed_devfile_yaml: processed_devfile_yaml, + scripts_configmap_name: scripts_configmap_name, + secrets_inventory_annotations: secrets_inventory_annotations, + secrets_inventory_name: secrets_inventory_name, + shared_namespace: shared_namespace, + workspace_inventory_annotations: workspace_inventory_annotations, + workspace_inventory_annotations_for_partial_reconciliation: + workspace_inventory_annotations_for_partial_reconciliation, + workspace_inventory_name: workspace_inventory_name, + workspace_name: workspace_name, + workspace_namespace: workspace_namespace + } + + append_inventory_configmap( + desired_config_array: desired_config_array, + name: workspace_inventory_name, + namespace: workspace_namespace, + labels: labels, + annotations: common_annotations_for_partial_reconciliation, + 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_for_partial_reconciliation + ) + + 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_for_partial_reconciliation + ) + + 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_for_partial_reconciliation + ) + + append_inventory_configmap( + 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 + # @param [Boolean] prepend -- If true, prepend the configmap to the desired_config_array + # @return [void] + def self.append_inventory_configmap( + desired_config_array:, + name:, + namespace:, + labels:, + annotations:, + prepend: false + ) + extra_labels = { "cli-utils.sigs.k8s.io/inventory-id": name } + + configmap = { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: name, + namespace: namespace, + labels: labels.merge(extra_labels), + annotations: annotations + } + } + + if prepend + desired_config_array.prepend(configmap) + else + desired_config_array.append(configmap) + 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? + + BmScriptsConfigmapAppender.append( + desired_config_array: desired_config_array, + name: name, + namespace: namespace, + labels: labels, + annotations: annotations, + devfile_commands: devfile_commands, + devfile_events: devfile_events + ) + + BmScriptsVolumeInserter.insert( + configmap_name: name, + containers: containers, + volumes: volumes + ) + + BmKubernetesPoststartHookInserter.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_configmap, + :append_secret, + :append_network_policy, + :append_scripts_resources, + :append_resource_quota, + :append_image_pull_secrets_service_account + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/ClassLength + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_modifier.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_modifier.rb new file mode 100644 index 00000000000000..9d74d92185fa12 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_devfile_resource_modifier.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + # rubocop:disable Metrics/MethodLength -- The original method is copied from ee/lib/remotedevelopment + # rubocop:disable Metrics/ClassLength -- The original class is copied from ee/lib/remotedevelopment + class BmDevfileResourceModifier # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include RemoteDevelopment::WorkspaceOperations::Create::BmCreateConstants + + # @param [Hash] context + # @return [Hash] + 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, + } + + set_host_users( + desired_config_array: desired_config_array, + use_kubernetes_user_namespaces: use_kubernetes_user_namespaces + ) + + set_runtime_class( + desired_config_array: desired_config_array, + runtime_class_name: default_runtime_class + ) + + set_security_context( + desired_config_array: desired_config_array, + allow_privilege_escalation: allow_privilege_escalation + ) + + patch_default_resources( + desired_config_array: desired_config_array, + default_resources_per_workspace_container: + default_resources_per_workspace_container + ) + + 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 + + # @param [Array] desired_config_array + # @param [Boolean] use_kubernetes_user_namespaces + # @return [void] + def self.set_host_users(desired_config_array:, use_kubernetes_user_namespaces:) + # NOTE: Not setting the use_kubernetes_user_namespaces always since setting it now would require + # migration + # from old config version to a new one. Set this field always + # when a new devfile parser is created for some other reason. + return desired_config_array unless use_kubernetes_user_namespaces + + find_pod_spec(desired_config_array)[:hostUsers] = use_kubernetes_user_namespaces + + nil + end + + # @param [Array] desired_config_array + # @param [String] runtime_class_name + # @return [void] + def self.set_runtime_class(desired_config_array:, runtime_class_name:) + # NOTE: Not setting the runtime_class_name always since changing it now would require migration + # from old config version to a new one. Update this field to `runtime_class_name.presence` + # when a new devfile parser is created for some other reason. + return desired_config_array if runtime_class_name.empty? + + find_pod_spec(desired_config_array)[:runtimeClassName] = runtime_class_name + + nil + 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 [void] + def self.set_security_context( + desired_config_array:, + allow_privilege_escalation: + ) + 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 = find_pod_spec(desired_config_array) + # Explicitly set security context for the pod + pod_spec[:securityContext] = pod_security_context + # Explicitly set security context for all containers + pod_spec[:containers].each do |container| + container[:securityContext] = container_security_context + end + # Explicitly set security context for all init containers + pod_spec[:initContainers].each do |init_container| + init_container[:securityContext] = container_security_context + end + + nil + end + + # @param [Array] desired_config_array + # @param [Hash] default_resources_per_workspace_container + # @return [void] + def self.patch_default_resources(desired_config_array:, default_resources_per_workspace_container:) + pod_spec = find_pod_spec(desired_config_array) + + container_types = [:initContainers, :containers] + container_types.each do |container_type| + # the purpose of this deep_merge is to ensure + # the values from the devfile override any defaults defined at the agent + pod_spec.fetch(container_type).each do |container| + container + .fetch(:resources, {}) + .deep_merge!(default_resources_per_workspace_container) { |_, val, _| val } + end + end + + nil + end + + # @param [Array] desired_config_array + # @param [String] env_secret_name + # @param [String] file_secret_name + # @return [void] + def self.inject_secrets(desired_config_array:, env_secret_name:, file_secret_name:) + volume = { + name: VARIABLES_VOLUME_NAME, + projected: { + defaultMode: VARIABLES_VOLUME_DEFAULT_MODE, + sources: [{ secret: { name: file_secret_name } }] + } + } + + volume_mount = { + name: VARIABLES_VOLUME_NAME, + mountPath: VARIABLES_VOLUME_PATH + } + + env_from = [{ secretRef: { name: env_secret_name } }] + + pod_spec = find_pod_spec(desired_config_array) + pod_spec.fetch(:volumes) << volume unless file_secret_name.empty? + + pod_spec.fetch(:initContainers).each do |init_container| + init_container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? + init_container[:envFrom] = env_from unless env_secret_name.empty? + end + + pod_spec.fetch(:containers).each do |container| + container.fetch(:volumeMounts) << volume_mount unless file_secret_name.empty? + container[:envFrom] = env_from unless env_secret_name.empty? + end + + nil + end + + # @param [Array] desired_config_array + # @param [String] service_account_name + # @return [void] + def self.set_service_account(desired_config_array:, service_account_name:) + find_pod_spec(desired_config_array)[:serviceAccountName] = service_account_name + + nil + end + + # @param [Array] desired_config_array + # @return [Hash] + def self.find_pod_spec(desired_config_array) + desired_config_array => [ + *_, + { + kind: "Deployment", + spec: { + template: { + spec: pod_spec + } + } + }, + *_ + ] + + pod_spec + end + + private_class_method :set_host_users, + :set_runtime_class, + :set_security_context, + :patch_default_resources, + :inject_secrets, + :set_service_account, + :find_pod_spec + end + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/ClassLength + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_legacy_poststart_hook_command.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_legacy_poststart_hook_command.sh new file mode 100644 index 00000000000000..c0963bf88df7ee --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_legacy_poststart_hook_command.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir -p "${GL_WORKSPACE_LOGS_DIR}" +ln -sf "${GL_WORKSPACE_LOGS_DIR}" /tmp +"%s" 1>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log" 2>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log" diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_command.sh b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_command.sh new file mode 100644 index 00000000000000..792e1cad5d3a53 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_command.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +mkdir -p "${GL_WORKSPACE_LOGS_DIR}" +ln -sf "${GL_WORKSPACE_LOGS_DIR}" /tmp + +{ + echo "$(date -Iseconds): ----------------------------------------" + echo "$(date -Iseconds): Running poststart commands for workspace..." + + echo "$(date -Iseconds): ----------------------------------------" + echo "$(date -Iseconds): Running internal blocking poststart commands script..." +} >> "${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log" + +"%s" 1>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log" 2>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log" + +{ + echo "$(date -Iseconds): ----------------------------------------" + echo "$(date -Iseconds): Running non-blocking poststart commands script..." +} >> "${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log" + +"%s" 1>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stdout.log" 2>>"${GL_WORKSPACE_LOGS_DIR}/poststart-stderr.log" & diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_inserter.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_inserter.rb new file mode 100644 index 00000000000000..a432ddc877354a --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_kubernetes_poststart_hook_inserter.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + # NOTE: This class has "Kubernetes" prepended to "Poststart" in the name to make it explicit that it + # deals with Kubernetes postStart hooks in the Kubernetes Deployment resource, and that + # it is NOT dealing with the postStart events which are found in devfiles. + class BmKubernetesPoststartHookInserter # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmFiles + include BmCreateConstants + + # @param [Array] containers + # @param [Array] devfile_commands + # @param [Hash] devfile_events + # @return [void] + def self.insert(containers:, devfile_commands:, devfile_events:) # rubocop:disable Metrics/MethodLength -- copied from ee/lib/remote_development + internal_blocking_command_label_present = devfile_commands.any? do |command| + command.dig(:exec, :label) == INTERNAL_BLOCKING_COMMAND_LABEL + end + + devfile_events => { postStart: Array => poststart_command_ids } + + containers_with_devfile_poststart_commands = + poststart_command_ids.each_with_object([]) do |poststart_command_id, accumulator| + command = devfile_commands.find { |command| command.fetch(:id) == poststart_command_id } + command => { + exec: { + component: String => container_name + } + } + accumulator << container_name + end.uniq + + containers.each do |container| # rubocop:disable Metrics/BlockLength -- need it big + container_name = container.fetch(:name) + + next unless containers_with_devfile_poststart_commands.include?(container_name) + + if internal_blocking_command_label_present + kubernetes_poststart_hook_script = + format( + KUBERNETES_POSTSTART_HOOK_COMMAND, + run_internal_blocking_poststart_commands_script_file_path: + "#{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME}", + run_non_blocking_poststart_commands_script_file_path: + "#{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME}" + ) + else + kubernetes_poststart_hook_script = + format( + KUBERNETES_LEGACY_POSTSTART_HOOK_COMMAND, + run_internal_blocking_poststart_commands_script_file_path: + "#{WORKSPACE_SCRIPTS_VOLUME_PATH}/#{LEGACY_RUN_POSTSTART_COMMANDS_SCRIPT_NAME}" + ) + end + + container[:lifecycle] = { + postStart: { + exec: { + command: ["/bin/sh", "-c", kubernetes_poststart_hook_script] + } + } + } + end + + nil + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_main.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_main.rb new file mode 100644 index 00000000000000..4047604d7be9c7 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_main.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmMain # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + # @param [Hash] parent_context + # @return [Hash] + def self.main(parent_context) # rubocop:disable Metrics/MethodLength -- this is a won't fix + 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(BmConfigValuesExtractor.method(:extract)) + .map(BmDevfileParserGetter.method(:get)) + .map(BmDesiredConfigYamlParser.method(:parse)) + .map(BmDevfileResourceModifier.method(:modify)) + .map(BmDevfileResourceAppender.method(:append)) + .map( + ->(context) do + context.merge( + desired_config: + RemoteDevelopment::WorkspaceOperations::BmDesiredConfig.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 + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_configmap_appender.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_configmap_appender.rb new file mode 100644 index 00000000000000..cba325cad961b5 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_configmap_appender.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmScriptsConfigmapAppender # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmCreateConstants + include BmWorkspaceOperationsConstants + + # @param [Array] desired_config_array + # @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_array:, + 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_array.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 + end +end diff --git a/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_volume_inserter.rb b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_volume_inserter.rb new file mode 100644 index 00000000000000..ed6ee1ddfec8fc --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/workspace_operations/create/desired_config/bm_scripts_volume_inserter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + module WorkspaceOperations + module Create + module DesiredConfig + class BmScriptsVolumeInserter # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + include BmCreateConstants + + # @param [String] configmap_name + # @param [Array] containers + # @param [Array] volumes + # @return [void] + def self.insert(configmap_name:, containers:, volumes:) + volume = + { + name: WORKSPACE_SCRIPTS_VOLUME_NAME, + projected: { + defaultMode: WORKSPACE_SCRIPTS_VOLUME_DEFAULT_MODE, + sources: [ + { + configMap: { + name: configmap_name + } + } + ] + } + } + volume_mount = + { + name: WORKSPACE_SCRIPTS_VOLUME_NAME, + mountPath: WORKSPACE_SCRIPTS_VOLUME_PATH + } + + volumes << volume + containers.each do |container| + container.fetch(:volumeMounts) << volume_mount + end + + nil + end + end + end + end + end + end + end +end -- GitLab