diff --git a/ee/lib/remote_development/devfile_operations/restrictions_enforcer.rb b/ee/lib/remote_development/devfile_operations/restrictions_enforcer.rb index 377735942395430bb0af3234fcbb89d90ffde511..143a1f7d78d2d65b83055fd0e19dd43194eb68cb 100644 --- a/ee/lib/remote_development/devfile_operations/restrictions_enforcer.rb +++ b/ee/lib/remote_development/devfile_operations/restrictions_enforcer.rb @@ -6,6 +6,8 @@ class RestrictionsEnforcer include RemoteDevelopmentConstants include Messages + MAX_DEVFILE_SIZE_BYTES = 3.megabytes + # Since this is called after flattening the devfile, we can safely assume that it has valid syntax # as per devfile standard. If you are validating something that is not available across all devfile versions, # add additional guard clauses. @@ -22,12 +24,19 @@ class RestrictionsEnforcer SUPPORTED_COMMAND_TYPES = %i[exec apply].freeze # Currently, we only support `preStart` events - SUPPORTED_EVENTS = %i[preStart].freeze + SUPPORTED_EVENTS = %i[preStart postStart].freeze + + # Currently, we only support the following options for exec commands + SUPPORTED_EXEC_COMMAND_OPTIONS = %i[commandLine component label hotReloadCapable].freeze + + # Currently, we only support the default value `false` for the `hotReloadCapable` option + SUPPORTED_HOT_RELOAD_VALUE = false # @param [Hash] context # @return [Gitlab::Fp::Result] def self.enforce(context) Gitlab::Fp::Result.ok(context) + .and_then(method(:validate_devfile_size)) .and_then(method(:validate_schema_version)) .and_then(method(:validate_parent)) .and_then(method(:validate_projects)) @@ -40,6 +49,8 @@ def self.enforce(context) .and_then(method(:validate_variables)) end + private + # @param [Hash] context # @return [Hash] the `processed_devfile` out of the `context` if it exists, otherwise the `devfile` def self.devfile_to_validate(context) @@ -48,6 +59,29 @@ def self.devfile_to_validate(context) context[:processed_devfile] || context[:devfile] end + # @param [Hash] context + # @return [Gitlab::Fp::Result] + def self.validate_devfile_size(context) + devfile = devfile_to_validate(context) + + # Calculate the size of the devfile by converting it to JSON + devfile_json = devfile.to_json + devfile_size_bytes = devfile_json.bytesize + + if devfile_size_bytes > MAX_DEVFILE_SIZE_BYTES + return err( + format( + _("Devfile size (%{current_size}) exceeds the maximum allowed size of %{max_size}"), + current_size: ActiveSupport::NumberHelper.number_to_human_size(devfile_size_bytes), + max_size: ActiveSupport::NumberHelper.number_to_human_size(MAX_DEVFILE_SIZE_BYTES) + ), + context + ) + end + + Gitlab::Fp::Result.ok(context) + end + # @param [Hash] context # @return [Gitlab::Fp::Result] def self.validate_schema_version(context) @@ -131,6 +165,7 @@ def self.validate_components(context) end if injected_main_components.length > 1 + # noinspection RailsParamDefResolve -- this pluck isn't from ActiveRecord, it's from ActiveSupport return err( format( _("Multiple components '%{name}' have '%{attribute}' attribute"), @@ -242,48 +277,82 @@ def self.validate_endpoints(context) def self.validate_commands(context) devfile = devfile_to_validate(context) - # Ensure no command name starts with restricted_prefix devfile.fetch(:commands, []).each do |command| command_id = command.fetch(:id) - if command_id.downcase.start_with?(RESTRICTED_PREFIX) + + # Check command_id for restricted prefix + error_result = validate_restricted_prefix(command_id, 'command_id', context) + return error_result if error_result + + supported_command_type = SUPPORTED_COMMAND_TYPES.find { |type| command[type].present? } + + unless supported_command_type return err( format( - _("Command id '%{command}' must not start with '%{prefix}'"), + _("Command '%{command}' must have one of the supported command types: %{supported_types}"), command: command_id, - prefix: RESTRICTED_PREFIX + supported_types: SUPPORTED_COMMAND_TYPES.join(", ") ), context ) end # Ensure no command is referring to a component with restricted_prefix - SUPPORTED_COMMAND_TYPES.each do |supported_command_type| - command_type = command[supported_command_type] - next unless command_type + command_type = command[supported_command_type] + + # Check if component is present (required for both exec and apply) + unless command_type[:component].present? + return err( + format( + _("'%{type}' command '%{command}' must specify a 'component'"), + type: supported_command_type, + command: command_id + ), + context + ) + end - component_name = command_type.fetch(:component) + # Check component name for restricted prefix + component_name = command_type.fetch(:component) - if component_name.downcase.start_with?(RESTRICTED_PREFIX) - return err( - format( - _("Component name '%{component}' for command id '%{command}' must not start with '%{prefix}'"), - component: component_name, - command: command_id, - prefix: RESTRICTED_PREFIX - ), - context - ) - end + error_result = validate_restricted_prefix(component_name, 'component_name', context, + { command: command_id }) + return error_result if error_result - command_label = command_type.fetch(:label, "") - next unless command_label.downcase.start_with?(RESTRICTED_PREFIX) + # Check label for restricted prefix + command_label = command_type.fetch(:label, "") + error_result = validate_restricted_prefix(command_label, 'label', context, + { command: command_id }) + return error_result if command_label.present? && error_result + + # Type-specicific validations for `exec` commands + # Since we only support the exec command type for user defined poststart events + # We don't need to have validation for other command types + next unless supported_command_type == :exec + + exec_command = command_type + + # Validate that only the supported options are used + unsupported_options = exec_command.keys - SUPPORTED_EXEC_COMMAND_OPTIONS + if unsupported_options.any? return err( format( - _("Label '%{command_label}' for command id '%{command}' must not start with '%{prefix}'"), - command_label: command_label, + _("Unsupported options '%{options}' for exec command '%{command}'. " \ + "Only '%{supported_options}' are supported."), + options: unsupported_options.join(", "), command: command_id, - prefix: RESTRICTED_PREFIX + supported_options: SUPPORTED_EXEC_COMMAND_OPTIONS.join(", ") + ), + context + ) + end + + if exec_command.key?(:hotReloadCapable) && exec_command[:hotReloadCapable] != SUPPORTED_HOT_RELOAD_VALUE + return err( + format( + _("Property 'hotReloadCapable' for exec command '%{command}' must be false when specified"), + command: command_id ), context ) @@ -293,10 +362,44 @@ def self.validate_commands(context) Gitlab::Fp::Result.ok(context) end + # @param [String] value + # @param [String] type + # @param [Hash] context + # @param [Hash] additional_params + # @return [Gitlab::Fp::Result.err] + def self.validate_restricted_prefix(value, type, context, additional_params = {}) + return unless value.downcase.start_with?(RESTRICTED_PREFIX) + + error_messages = { + 'command_id' => _("Command id '%{command}' must not start with '%{prefix}'"), + 'component_name' => _( + "Component name '%{component}' for command id '%{command}' must not start with '%{prefix}'" + ), + 'label' => _("Label '%{command_label}' for command id '%{command}' must not start with '%{prefix}'") + } + + message_template = error_messages[type] + return unless message_template + + params = { prefix: RESTRICTED_PREFIX }.merge(additional_params) + + case type + when 'command_id' + params[:command] = value + when 'component_name' + params[:component] = value + when 'label' + params[:command_label] = value + end + + err(format(message_template, params), context) + end + # @param [Hash] context # @return [Gitlab::Fp::Result] def self.validate_events(context) devfile = devfile_to_validate(context) + commands = devfile.fetch(:commands, []) devfile.fetch(:events, {}).each do |event_type, event_type_events| # Ensure no event type other than "preStart" are allowed @@ -308,18 +411,35 @@ def self.validate_events(context) end # Ensure no event starts with restricted_prefix - event_type_events.each do |event| - next unless event.downcase.start_with?(RESTRICTED_PREFIX) + event_type_events.each do |command_name| + if command_name.downcase.start_with?(RESTRICTED_PREFIX) + return err( + format( + _("Event '%{event}' of type '%{event_type}' must not start with '%{prefix}'"), + event: command_name, + event_type: event_type, + prefix: RESTRICTED_PREFIX + ), + context + ) + end - return err( - format( - _("Event '%{event}' of type '%{event_type}' must not start with '%{prefix}'"), - event: event, - event_type: event_type, - prefix: RESTRICTED_PREFIX - ), - context - ) + next unless event_type == :postStart + + # ===== postStart specific validations ===== + + # Check if the referenced command is an exec command + referenced_command = commands.find { |cmd| cmd[:id] == command_name } + unless referenced_command[:exec].present? + return err( + format( + _("PostStart event references command '%{command}' which is not an exec command. Only exec " \ + "commands are supported in postStart events"), + command: command_name + ), + context + ) + end end end @@ -358,9 +478,9 @@ def self.validate_variables(context) def self.err(details, context) Gitlab::Fp::Result.err(DevfileRestrictionsFailed.new({ details: details, context: context })) end - private_class_method :devfile_to_validate, :validate_schema_version, :validate_parent, + private_class_method :devfile_to_validate, :validate_devfile_size, :validate_schema_version, :validate_parent, :validate_projects, :validate_components, :validate_containers, - :validate_endpoints, :validate_commands, :validate_events, + :validate_endpoints, :validate_commands, :validate_restricted_prefix, :validate_events, :validate_variables, :err, :validate_root_attributes end end diff --git a/ee/lib/remote_development/workspace_operations/create/internal_poststart_commands_inserter.rb b/ee/lib/remote_development/workspace_operations/create/internal_poststart_commands_inserter.rb index 02290ae04cc8e89dc8e4b28490714e43e04e1515..5016c9924ca65aff535a71aed1909ec066d25f29 100644 --- a/ee/lib/remote_development/workspace_operations/create/internal_poststart_commands_inserter.rb +++ b/ee/lib/remote_development/workspace_operations/create/internal_poststart_commands_inserter.rb @@ -66,7 +66,6 @@ def self.insert(context) label: INTERNAL_BLOCKING_COMMAND_LABEL } } - poststart_events << clone_project_command_id # Add the start_sshd event start_sshd_command_id = "gl-start-sshd-command" @@ -78,7 +77,6 @@ def self.insert(context) label: INTERNAL_BLOCKING_COMMAND_LABEL } } - poststart_events << start_sshd_command_id # Add the start_vscode event start_vscode_command_id = "gl-init-tools-command" @@ -90,7 +88,6 @@ def self.insert(context) label: INTERNAL_BLOCKING_COMMAND_LABEL } } - poststart_events << start_vscode_command_id # Add the sleep_until_container_is_running event sleep_until_container_is_running_command_id = "gl-sleep-until-container-is-running-command" @@ -108,7 +105,14 @@ def self.insert(context) label: INTERNAL_COMMAND_LABEL } } - poststart_events << sleep_until_container_is_running_command_id + + # Prepend internal commands so they are executed before any user-defined poststart events. + poststart_events.prepend( + clone_project_command_id, + start_sshd_command_id, + start_vscode_command_id, + sleep_until_container_is_running_command_id + ) context end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender.rb index 95c0f49594e5d83941e940b53d11e39199a13733..e5f042689a5ffa0018f2e2d5de6e2a23d729a534 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender.rb @@ -99,7 +99,6 @@ def self.add_run_poststart_commands_script_to_configmap_data( # 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. - # TODO: User provided commands should be added to the non-blocking poststart script with https://gitlab.com/gitlab-org/gitlab/-/issues/505988 internal_blocking_poststart_command_ids, non_blocking_poststart_command_ids = poststart_command_ids.partition do |id| command = devfile_commands.find { |cmd| cmd[:id] == id } diff --git a/ee/spec/features/remote_development/workspaces_spec.rb b/ee/spec/features/remote_development/workspaces_spec.rb index fc9fd01def4cdb7ed2c821bca0a47eff130d66b5..84ae7ff39568c1d8ff4f4a68cac7cad5e04d53c2 100644 --- a/ee/spec/features/remote_development/workspaces_spec.rb +++ b/ee/spec/features/remote_development/workspaces_spec.rb @@ -35,6 +35,19 @@ let(:variable_value) { "value 1" } let(:workspaces_group_settings_path) { "/groups/#{group.name}/-/settings/workspaces" } + let(:user_defined_commands) do + [ + { + id: "user-defined-command", + exec: { + component: "tooling-container", + commandLine: "echo 'user-defined postStart command'", + hotReloadCapable: false + } + } + ] + end + # @param [String] state # @return [void] def expect_workspace_state_indicator(state) @@ -168,7 +181,8 @@ def do_reconcile_post(params:, agent_token:) dns_zone: workspaces_agent_config.dns_zone, namespace_path: group.path, project_name: project.path, - image_pull_secrets: image_pull_secrets + image_pull_secrets: image_pull_secrets, + user_defined_commands: user_defined_commands ) # SIMULATE RECONCILE RESPONSE TO AGENTK SENDING NEW WORKSPACE diff --git a/ee/spec/fixtures/remote_development/example.devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.devfile.yaml.erb index cae2fcd1d74330ef65db39dac570337b3be93ff3..b23ec15cc93a0f151d0c8722c721f4fd64aa1fb8 100644 --- a/ee/spec/fixtures/remote_development/example.devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.devfile.yaml.erb @@ -12,4 +12,13 @@ components: env: - name: MYSQL_ROOT_PASSWORD value: "my-secret-pw" - +commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false +events: + preStart: [] + postStart: + - user-defined-command diff --git a/ee/spec/fixtures/remote_development/example.flattened-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.flattened-devfile.yaml.erb index ddb034751444d00b70161fdbd4347cc7bd7fb8c3..a1ab14be9569e1575d7958f5abe39f85e65dfd36 100644 --- a/ee/spec/fixtures/remote_development/example.flattened-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.flattened-devfile.yaml.erb @@ -17,9 +17,14 @@ components: env: - name: MYSQL_ROOT_PASSWORD value: "my-secret-pw" -commands: [] -events: { - preStart: [], - postStart: [] -} +commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false +events: + postStart: + - user-defined-command + preStart: [] variables: {} diff --git a/ee/spec/fixtures/remote_development/example.internal-poststart-commands-inserted-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.internal-poststart-commands-inserted-devfile.yaml.erb index 1c73ef9b7214c348911aec49f3520372ec6299ca..93b06e24fff62a04ee6e5c7c2124a6bbfc7638df 100644 --- a/ee/spec/fixtures/remote_development/example.internal-poststart-commands-inserted-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.internal-poststart-commands-inserted-devfile.yaml.erb @@ -55,6 +55,11 @@ components: cpuLimit: 500m cpuRequest: 100m commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false - id: gl-tools-injector-command apply: component: gl-tools-injector @@ -99,4 +104,5 @@ events: - gl-start-sshd-command - gl-init-tools-command - gl-sleep-until-container-is-running-command + - user-defined-command variables: {} diff --git a/ee/spec/fixtures/remote_development/example.invalid-command-missing-component-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-command-missing-component-devfile.yaml.erb new file mode 100644 index 0000000000000000000000000000000000000000..2d2d69306788bdda8a18d165ec5abd982fa14f42 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.invalid-command-missing-component-devfile.yaml.erb @@ -0,0 +1,12 @@ +--- +schemaVersion: 2.2.0 +components: + - name: example + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo +commands: + - id: missing-component-command + exec: + commandLine: echo "Hello, World!" diff --git a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-component-name-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-component-name-devfile.yaml.erb index eb67d6ca6a35c3e634fd82bcdcd502cf83114500..73128597de04722355126980133da18c64c11e83 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-component-name-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-component-name-devfile.yaml.erb @@ -11,6 +11,5 @@ commands: exec: component: gl-example commandLine: mvn clean - workingDir: /projects/spring-petclinic events: {} variables: {} diff --git a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-label-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-label-devfile.yaml.erb index 599d5ed62eb43c27b2fb7ad663f73818fdd20e76..b4ed85beacf4294fdf4afc7cf3d6a24b2df9e415 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-label-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-exec-label-devfile.yaml.erb @@ -11,7 +11,6 @@ commands: exec: component: example commandLine: mvn clean - workingDir: /projects/spring-petclinic label: gl-example events: {} variables: {} diff --git a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-name-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-name-devfile.yaml.erb index 6db207f3f7bfe5b00a4157db940e855031611300..63acc386c83e3aa1b82c16cb0a91c997b3f90540 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-name-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-command-name-devfile.yaml.erb @@ -11,6 +11,5 @@ commands: exec: component: example commandLine: mvn clean - workingDir: /projects/spring-petclinic events: {} variables: {} diff --git a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-event-type-prestop-name-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-event-type-prestop-name-devfile.yaml.erb index beb62f964e72a9dd2d985d39f6562c65b41aff2c..1ca19380ddf32614a91c544c8b9040a71ea2cf32 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-event-type-prestop-name-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-restricted-prefix-event-type-prestop-name-devfile.yaml.erb @@ -11,7 +11,6 @@ commands: exec: component: example commandLine: mvn clean - workingDir: /projects/spring-petclinic events: preStop: - example diff --git a/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-hot-reload-capable-option-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-hot-reload-capable-option-devfile.yaml.erb new file mode 100644 index 0000000000000000000000000000000000000000..697529b3185693aca2041436bbbcc3489bc50fd8 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-hot-reload-capable-option-devfile.yaml.erb @@ -0,0 +1,14 @@ +--- +schemaVersion: 2.2.0 +components: + - name: example + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo +commands: + - id: unsupported-hot-reload-option + exec: + component: example + commandLine: echo "Hello, World!" + hotReloadCapable: true diff --git a/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-options-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-options-devfile.yaml.erb new file mode 100644 index 0000000000000000000000000000000000000000..2b981466a5b799e96167f13419c4f644b3c5db20 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-exec-options-devfile.yaml.erb @@ -0,0 +1,14 @@ +--- +schemaVersion: 2.2.0 +components: + - name: example + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo +commands: + - id: unsupported-options + exec: + component: example + commandLine: echo "Hello, World!" + unsupportedOption: true diff --git a/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-devfile.yaml.erb new file mode 100644 index 0000000000000000000000000000000000000000..9fff21edcfc5462530466f1cc14c03c1f487b2b9 --- /dev/null +++ b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-devfile.yaml.erb @@ -0,0 +1,12 @@ +--- +schemaVersion: 2.2.0 +components: + - name: example + attributes: + gl/inject-editor: true + container: + image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo +commands: + - id: composite-command + composite: + commands: [] diff --git a/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-poststart-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-poststart-event-devfile.yaml.erb similarity index 65% rename from ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-poststart-devfile.yaml.erb rename to ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-poststart-event-devfile.yaml.erb index 49133608fe5134ffa7375f2a88f9146864526054..4960e26035d0673edc2e933bcdde2107f43f2fc7 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-poststart-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-unsupported-command-type-poststart-event-devfile.yaml.erb @@ -7,12 +7,9 @@ components: container: image: quay.io/mloriedo/universal-developer-image:ubi8-dw-demo commands: - - id: example - exec: + - id: apply-command + apply: component: example - commandLine: mvn clean - workingDir: /projects/spring-petclinic events: postStart: - - example -variables: {} + - apply-command diff --git a/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-prestop-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-prestop-devfile.yaml.erb index 63039bf768db94ffb200d16b4464baa63a643b1f..bcfe5f1cb47ef639598a594be4c2f8bc5ce78749 100644 --- a/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-prestop-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.invalid-unsupported-event-type-prestop-devfile.yaml.erb @@ -11,7 +11,6 @@ commands: exec: component: example commandLine: mvn clean - workingDir: /projects/spring-petclinic events: preStop: - example diff --git a/ee/spec/fixtures/remote_development/example.main-container-updated-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.main-container-updated-devfile.yaml.erb index 2740b5f509fa6d2b8c5185f7b4f94a276ed198f5..65f71c915b09080adcaf3f648ebb2e3847303009 100644 --- a/ee/spec/fixtures/remote_development/example.main-container-updated-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.main-container-updated-devfile.yaml.erb @@ -55,11 +55,17 @@ components: cpuLimit: 500m cpuRequest: 100m commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false - id: gl-tools-injector-command apply: component: gl-tools-injector events: preStart: - gl-tools-injector-command - postStart: [] + postStart: + - user-defined-command variables: {} diff --git a/ee/spec/fixtures/remote_development/example.main-container-updated-marketplace-disabled-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.main-container-updated-marketplace-disabled-devfile.yaml.erb index 2740b5f509fa6d2b8c5185f7b4f94a276ed198f5..65f71c915b09080adcaf3f648ebb2e3847303009 100644 --- a/ee/spec/fixtures/remote_development/example.main-container-updated-marketplace-disabled-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.main-container-updated-marketplace-disabled-devfile.yaml.erb @@ -55,11 +55,17 @@ components: cpuLimit: 500m cpuRequest: 100m commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false - id: gl-tools-injector-command apply: component: gl-tools-injector events: preStart: - gl-tools-injector-command - postStart: [] + postStart: + - user-defined-command variables: {} diff --git a/ee/spec/fixtures/remote_development/example.multi-entry-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.multi-entry-devfile.yaml.erb index 8a37e943a53aee50f4436074931419e32de85f91..cdbdb84a7f38f1245d52d9bd93c1f3ed9cbe2acf 100644 --- a/ee/spec/fixtures/remote_development/example.multi-entry-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.multi-entry-devfile.yaml.erb @@ -20,15 +20,10 @@ components: protocol: https exposure: none commands: - - id: example-valid-non-apply-or-exec-command - composite: - commands: - - example-valid-command - - example-valid-second-command - parallel: false - id: example-valid-command - apply: + exec: component: example-valid-component + commandLine: echo "valid command" - id: example-valid-second-command apply: component: example-valid-second-component diff --git a/ee/spec/fixtures/remote_development/example.processed-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.processed-devfile.yaml.erb index 9e596cd810835de4c8c4df72ceac39f73719027f..6e824aaa12d67e5c0944a4781166972042ee5a6d 100644 --- a/ee/spec/fixtures/remote_development/example.processed-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.processed-devfile.yaml.erb @@ -67,6 +67,11 @@ components: volume: size: 50Gi commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false - id: gl-tools-injector-command apply: component: gl-tools-injector @@ -111,4 +116,5 @@ events: - gl-start-sshd-command - gl-init-tools-command - gl-sleep-until-container-is-running-command + - user-defined-command variables: {} diff --git a/ee/spec/fixtures/remote_development/example.tools-injector-inserted-devfile.yaml.erb b/ee/spec/fixtures/remote_development/example.tools-injector-inserted-devfile.yaml.erb index 52180a5d81234940ff5729ea7dd9afc7c8092787..57754eda21c8fce0f2f964fc70735fef630589da 100644 --- a/ee/spec/fixtures/remote_development/example.tools-injector-inserted-devfile.yaml.erb +++ b/ee/spec/fixtures/remote_development/example.tools-injector-inserted-devfile.yaml.erb @@ -30,8 +30,14 @@ components: events: preStart: - gl-tools-injector-command - postStart: [] + postStart: + - user-defined-command commands: + - id: user-defined-command + exec: + component: tooling-container + commandLine: echo 'user-defined postStart command' + hotReloadCapable: false - id: gl-tools-injector-command apply: component: gl-tools-injector diff --git a/ee/spec/lib/remote_development/devfile_operations/flattener_spec.rb b/ee/spec/lib/remote_development/devfile_operations/flattener_spec.rb index bcbdd8a6bb5ef5aee82f000626e4a14dd40da887..abba522ef442d2c658619d506fe855d4774ab0c2 100644 --- a/ee/spec/lib/remote_development/devfile_operations/flattener_spec.rb +++ b/ee/spec/lib/remote_development/devfile_operations/flattener_spec.rb @@ -25,6 +25,16 @@ ) end + it "removes unsupported exec command options" do + result_value = result.unwrap + exec_command = result_value[:processed_devfile][:commands].first[:exec] + + supported_options = RemoteDevelopment::DevfileOperations::RestrictionsEnforcer::SUPPORTED_EXEC_COMMAND_OPTIONS + + # Should keep only supported options + expect(exec_command.keys).to all(be_in(supported_options)) + end + context "when devfile has no elements" do let(:devfile_yaml) { read_devfile_yaml('example.invalid-no-elements-devfile.yaml.erb') } let(:expected_processed_devfile) do diff --git a/ee/spec/lib/remote_development/devfile_operations/restrictions_enforcer_spec.rb b/ee/spec/lib/remote_development/devfile_operations/restrictions_enforcer_spec.rb index 20bc9be47c8849a76345c1272152a1433424471b..6e665c33bad781d1d93c43a344500964322535ed 100644 --- a/ee/spec/lib/remote_development/devfile_operations/restrictions_enforcer_spec.rb +++ b/ee/spec/lib/remote_development/devfile_operations/restrictions_enforcer_spec.rb @@ -68,6 +68,7 @@ "example.invalid-attributes-tools-injector-absent-devfile.yaml.erb" | "No component has '#{main_component_indicator_attribute}' attribute" "example.invalid-attributes-tools-injector-multiple-devfile.yaml.erb" | "Multiple components '[\"tooling-container\", \"tooling-container-2\"]' have '#{main_component_indicator_attribute}' attribute" "example.invalid-component-missing-name.yaml.erb" | "Components must have a 'name'" + "example.invalid-command-missing-component-devfile.yaml.erb" | "'exec' command 'missing-component-command' must specify a 'component'" "example.invalid-components-attributes-container-overrides-devfile.yaml.erb" | "Attribute 'container-overrides' is not yet supported" "example.invalid-components-attributes-pod-overrides-devfile.yaml.erb" | "Attribute 'pod-overrides' is not yet supported" "example.invalid-components-entry-empty-devfile.yaml.erb" | "No components present in devfile" @@ -86,11 +87,14 @@ "example.invalid-restricted-prefix-command-exec-label-devfile.yaml.erb" | "Label 'gl-example' for command id 'example' must not start with 'gl-'" "example.invalid-restricted-prefix-variable-name-with-underscore-devfile.yaml.erb" | "Variable name 'gl_example' must not start with 'gl_'" "example.invalid-root-attributes-pod-overrides-devfile.yaml.erb" | "Attribute 'pod-overrides' is not yet supported" + "example.invalid-unsupported-command-exec-hot-reload-capable-option-devfile.yaml.erb" | "Property 'hotReloadCapable' for exec command 'unsupported-hot-reload-option' must be false when specified" + "example.invalid-unsupported-command-exec-options-devfile.yaml.erb" | "Unsupported options 'unsupportedOption' for exec command 'unsupported-options'. Only 'commandLine, component, label, hotReloadCapable' are supported." + "example.invalid-unsupported-command-type-devfile.yaml.erb" | "Command 'composite-command' must have one of the supported command types: exec, apply" + "example.invalid-unsupported-command-type-poststart-event-devfile.yaml.erb" | "PostStart event references command 'apply-command' which is not an exec command. Only exec commands are supported in postStart events" "example.invalid-unsupported-component-container-dedicated-pod-devfile.yaml.erb" | "Property 'dedicatedPod' of component 'example' is not yet supported" "example.invalid-unsupported-component-type-image-devfile.yaml.erb" | "Component type 'image' is not yet supported" "example.invalid-unsupported-component-type-kubernetes-devfile.yaml.erb" | "Component type 'kubernetes' is not yet supported" "example.invalid-unsupported-component-type-openshift-devfile.yaml.erb" | "Component type 'openshift' is not yet supported" - "example.invalid-unsupported-event-type-poststart-devfile.yaml.erb" | "Event type 'postStart' is not yet supported" "example.invalid-unsupported-event-type-poststop-devfile.yaml.erb" | "Event type 'postStop' is not yet supported" "example.invalid-unsupported-event-type-prestop-devfile.yaml.erb" | "Event type 'preStop' is not yet supported" "example.invalid-unsupported-parent-inheritance-devfile.yaml.erb" | "Inheriting from 'parent' is not yet supported" @@ -135,5 +139,26 @@ it_behaves_like "an err result" end end + + context "for devfile size validation" do + let(:input_devfile_name) { "example.devfile.yaml.erb" } + + context "when devfile exceeds maximum size" do + before do + json_string = input_devfile.to_json + allow(input_devfile).to receive(:to_json).and_return(json_string) + allow(json_string).to receive(:bytesize).and_return(3.megabytes + 1) + end + + it "returns an err Result with size exceeded message" do + is_expected.to be_err_result do |message| + expect(message).to be_a(RemoteDevelopment::Messages::DevfileRestrictionsFailed) + message.content => { details: String => error_details, context: Hash => actual_context } + expect(error_details).to match(/Devfile size .* exceeds the maximum allowed size/) + expect(actual_context).to eq(context) + end + end + end + end end end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb index 8094515d369a313712e3e88d162193223021637a..0402ccb54cf07fbd6471f55b883e0479096a07c8 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/main_integration_spec.rb @@ -71,6 +71,19 @@ let(:logger) { instance_double(::Logger) } + let(:user_defined_commands) do + [ + { + id: "user-defined-command", + exec: { + component: "tooling-container", + commandLine: "echo 'user-defined postStart command'", + hotReloadCapable: false + } + } + ] + end + let(:expected_config_to_apply_yaml_stream) do create_config_to_apply_yaml_stream( workspace: workspace, @@ -84,7 +97,8 @@ egress_ip_rules: egress_ip_rules, max_resources_per_workspace: max_resources_per_workspace, default_resources_per_workspace_container: default_resources_per_workspace_container, - image_pull_secrets: image_pull_secrets + image_pull_secrets: image_pull_secrets, + user_defined_commands: user_defined_commands ) end @@ -261,6 +275,33 @@ it_behaves_like 'unprovisioned workspace expectations' it_behaves_like 'versioned workspaces_agent_configs behavior' end + + context 'when workspace has user-defined postStart commands' do + it 'includes user-defined commands in the scripts configmap' do + workspace_rails_infos = response.fetch(:payload).fetch(:workspace_rails_infos) + actual_workspace_rails_info = workspace_rails_infos.detect { |info| info.fetch(:name) == workspace.name } + actual_config_to_apply = + yaml_safe_load_stream_symbolized(actual_workspace_rails_info.fetch(:config_to_apply)) + + scripts_configmap = actual_config_to_apply.find do |resource| + resource[:kind] == "ConfigMap" && resource[:metadata][:name].end_with?("-scripts-configmap") + end + + expect(scripts_configmap).to be_present + + expect(scripts_configmap[:data].keys).to include(user_defined_commands.first[:id].to_sym) + + expect(scripts_configmap[:data][user_defined_commands.first[:id].to_sym]).to eq( + user_defined_commands.first[:exec][:commandLine] + ) + + # Verify the poststart script includes the user-defined command + poststart_script = scripts_configmap[:data][ + reconcile_constants_module::RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym + ] + expect(poststart_script).to include("Running /workspace-scripts/user-defined-command") + end + end end context 'when update_type is partial' do diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb index 3b8b83f9568fd3bc41e86f9c25a99017b9dc4579..026c50e63826ba4dd9ee8a20a938d5e65a0c4aa7 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator_spec.rb @@ -79,6 +79,19 @@ ) end + let(:user_defined_commands) do + [ + { + id: "user-defined-command", + exec: { + component: "tooling-container", + commandLine: "echo 'user-defined postStart command'", + hotReloadCapable: false + } + } + ] + end + let(:expected_config) do create_config_to_apply( workspace: workspace, @@ -98,7 +111,8 @@ agent_labels: workspace.workspaces_agent_config.labels.deep_symbolize_keys, agent_annotations: workspace.workspaces_agent_config.annotations.deep_symbolize_keys, image_pull_secrets: image_pull_secrets.map(&:deep_symbolize_keys), - shared_namespace: shared_namespace + shared_namespace: shared_namespace, + user_defined_commands: user_defined_commands ) end @@ -293,6 +307,8 @@ read_devfile_yaml("example.legacy-poststart-in-container-command-processed-devfile.yaml.erb") end + let(:user_defined_commands) { [] } + it 'returns expected config without script resources' do expect(workspace_resources).to eq(expected_config) end diff --git a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender_spec.rb b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender_spec.rb index 23c9eca6be6a3cc184f48c107118150f9de1bde0..e6d1c62b1b098458f331adee4cd480f8387560b8 100644 --- a/ee/spec/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/reconcile/output/scripts_configmap_appender_spec.rb @@ -55,10 +55,11 @@ reconcile_constants_module::RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => internal_blocking_poststart_commands_script, reconcile_constants_module::RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => - non_blocking_poststart_commands_script, + non_blocking_poststart_commands_script(user_command_ids: ["user-defined-command"]), "gl-sleep-until-container-is-running-command": sleep_until_container_is_running_script, - "gl-start-sshd-command": files::INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT + "gl-start-sshd-command": files::INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT, + "user-defined-command": "echo 'user-defined postStart command'" ) end diff --git a/ee/spec/requests/remote_development/integration_spec.rb b/ee/spec/requests/remote_development/integration_spec.rb index 5c5186595da68ed2feae34159aa155c0f32783d4..f3a6d24c94a3765ce40bff259ac8ddf39dfb4afb 100644 --- a/ee/spec/requests/remote_development/integration_spec.rb +++ b/ee/spec/requests/remote_development/integration_spec.rb @@ -72,6 +72,19 @@ ] end + let(:user_defined_commands) do + [ + { + id: "user-defined-command", + exec: { + component: "tooling-container", + commandLine: "echo 'user-defined postStart command'", + hotReloadCapable: false + } + } + ] + end + let(:namespace_agents_config_query) do <<~GRAPHQL query { @@ -445,7 +458,8 @@ def do_reconcile_post(params:, agent_token:) dns_zone: dns_zone, namespace_path: workspace_project_namespace.full_path, project_name: workspace_project_name, - image_pull_secrets: image_pull_secrets + image_pull_secrets: image_pull_secrets, + user_defined_commands: user_defined_commands ) # SIMULATE RECONCILE RESPONSE TO AGENTK SENDING NEW WORKSPACE diff --git a/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb b/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb index 2552049afb335c906eef9544ec1cb1ad62d1c340..d3843924a1610050b80dfb71ab275849a68ec372 100644 --- a/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb +++ b/ee/spec/support/helpers/remote_development/integration_spec_helpers.rb @@ -9,20 +9,23 @@ module IntegrationSpecHelpers # @param [String] namespace_path # @param [String] project_name # @param [Array] image_pull_secrets + # @param [Array] user_defined_commands # @return [Hash] def build_additional_args_for_expected_config_to_apply_yaml_stream( network_policy_enabled:, dns_zone:, namespace_path:, project_name:, - image_pull_secrets: + image_pull_secrets:, + user_defined_commands: ) { dns_zone: dns_zone, namespace_path: namespace_path, project_name: project_name, include_network_policy: network_policy_enabled, - image_pull_secrets: image_pull_secrets + image_pull_secrets: image_pull_secrets, + user_defined_commands: user_defined_commands } end diff --git a/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb b/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb index 4c00056d1f59641ab73682e259951f435027cfb0..5d667ba17665aa6a8fc8e58345300a937b8ec5e6 100644 --- a/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb +++ b/ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb @@ -7,7 +7,7 @@ # rubocop:todo Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Cleanup as part of https://gitlab.com/gitlab-org/gitlab/-/issues/421687 - # @return [Array] + # @return [Array] def create_desired_config_array json_content = RemoteDevelopment::FixtureFileHelpers.read_fixture_file('example.desired_config.json') Gitlab::Json.parse(json_content).map(&:deep_symbolize_keys) @@ -361,6 +361,8 @@ def create_config_to_apply(workspace:, **args) # @param [Array] image_pull_secrets # @param [Boolean] include_scripts_resources # @param [Boolean] legacy_no_poststart_container_command + # @param [Boolean] legacy_poststart_container_command + # @param [Array] user_defined_commands # @param [String] shared_namespace # @param [Boolean] core_resources_only # @return [Array] @@ -392,6 +394,7 @@ def create_config_to_apply_v3( include_scripts_resources: true, legacy_no_poststart_container_command: false, legacy_poststart_container_command: false, + user_defined_commands: [], shared_namespace: "", core_resources_only: false ) @@ -482,7 +485,8 @@ def create_config_to_apply_v3( workspace_namespace: workspace.namespace, labels: labels, annotations: workspace_inventory_annotations, - legacy_poststart_container_command: legacy_poststart_container_command + legacy_poststart_container_command: legacy_poststart_container_command, + user_defined_commands: user_defined_commands ) secrets_inventory_config_map = secrets_inventory_config_map( @@ -1175,15 +1179,27 @@ def internal_blocking_poststart_commands_script SCRIPT end + # @param [Array] user_command_ids # @return [String] - def non_blocking_poststart_commands_script - <<~SCRIPT + def non_blocking_poststart_commands_script(user_command_ids: []) + script = <<~SCRIPT #!/bin/sh echo "$(date -Iseconds): Running #{reconcile_constants_module::WORKSPACE_SCRIPTS_VOLUME_PATH}/gl-sleep-until-container-is-running-command..." #{reconcile_constants_module::WORKSPACE_SCRIPTS_VOLUME_PATH}/gl-sleep-until-container-is-running-command || true SCRIPT + + # Add user-defined commands if any + user_command_ids.each do |command_id| + script += <<~SCRIPT + echo "$(date -Iseconds): Running #{reconcile_constants_module::WORKSPACE_SCRIPTS_VOLUME_PATH}/#{command_id}..." + #{reconcile_constants_module::WORKSPACE_SCRIPTS_VOLUME_PATH}/#{command_id} || true + SCRIPT + end + + script end + # @return [String] def legacy_poststart_commands_script <<~SCRIPT #!/bin/sh @@ -1228,21 +1244,25 @@ def sleep_until_container_is_running_script # @param [Hash] labels # @param [Hash] annotations # @param [Boolean] legacy_poststart_container_command + # @param [Array] user_defined_commands # @return [Hash] def scripts_configmap( workspace_name:, workspace_namespace:, labels:, annotations:, - legacy_poststart_container_command: + legacy_poststart_container_command:, + user_defined_commands: ) + user_command_ids = user_defined_commands.pluck(:id) + data = { "gl-clone-project-command": clone_project_script, "gl-init-tools-command": files_module::INTERNAL_POSTSTART_COMMAND_START_VSCODE_SCRIPT, reconcile_constants_module::RUN_INTERNAL_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => internal_blocking_poststart_commands_script, reconcile_constants_module::RUN_NON_BLOCKING_POSTSTART_COMMANDS_SCRIPT_NAME.to_sym => - non_blocking_poststart_commands_script, + non_blocking_poststart_commands_script(user_command_ids: user_command_ids), "gl-sleep-until-container-is-running-command": sleep_until_container_is_running_script, "gl-start-sshd-command": files_module::INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT } @@ -1254,6 +1274,11 @@ def scripts_configmap( legacy_poststart_commands_script end + # Add each user-defined command to the data hash + user_defined_commands.each do |cmd| + data[cmd[:id].to_sym] = cmd[:exec][:commandLine] + end + { apiVersion: "v1", kind: "ConfigMap", diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5491d5e0338d29bf97b3d007d22997990629943c..ec2a1fc33d7a8e65c7b25d45d009cfeb120a39df 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1710,6 +1710,9 @@ msgstr "" msgid "'%{template_name}' is unknown or invalid" msgstr "" +msgid "'%{type}' command '%{command}' must specify a 'component'" +msgstr "" + msgid "'%{value}' days of inactivity must be greater than or equal to 90" msgstr "" @@ -14990,6 +14993,9 @@ msgstr "" msgid "Command" msgstr "" +msgid "Command '%{command}' must have one of the supported command types: %{supported_types}" +msgstr "" + msgid "Command id '%{command}' must not start with '%{prefix}'" msgstr "" @@ -22409,6 +22415,9 @@ msgstr "" msgid "Development widget is not enabled for this work item type" msgstr "" +msgid "Devfile size (%{current_size}) exceeds the maximum allowed size of %{max_size}" +msgstr "" + msgid "Device name" msgstr "" @@ -46417,6 +46426,9 @@ msgstr "" msgid "Possible to override in each project." msgstr "" +msgid "PostStart event references command '%{command}' which is not an exec command. Only exec commands are supported in postStart events" +msgstr "" + msgid "Postman collection" msgstr "" @@ -49671,6 +49683,9 @@ msgstr "" msgid "Property 'dedicatedPod' of component '%{name}' is not yet supported" msgstr "" +msgid "Property 'hotReloadCapable' for exec command '%{command}' must be false when specified" +msgstr "" + msgid "Protect" msgstr "" @@ -65663,6 +65678,9 @@ msgstr "" msgid "Unsupported node type for alias: %{type}" msgstr "" +msgid "Unsupported options '%{options}' for exec command '%{command}'. Only '%{supported_options}' are supported." +msgstr "" + msgid "Unsupported signature" msgstr ""