diff --git a/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator.rb b/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..982601b75c8ff8110224849e6b6cf565c0c4e938 --- /dev/null +++ b/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module RemoteDevelopment + # rubocop:disable Migration/BatchedMigrationBaseClass -- This is not a migration file class so we do not need to inherit from BatchedMigrationJob + class BmDesiredConfigArrayValidator < ActiveModel::EachValidator + EXPECTED_SEQUENCE = [ + { kind: "ConfigMap", name_pattern: /workspace-inventory$/ }, + { kind: "Deployment", name_pattern: /.*/ }, + { kind: "Service", name_pattern: /.*/ }, + { kind: "PersistentVolumeClaim", name_pattern: /.*/ }, + { kind: "ServiceAccount", name_pattern: /.*/ }, + { kind: "NetworkPolicy", name_pattern: /.*/ }, + { kind: "ConfigMap", name_pattern: /scripts-configmap$/ }, + { kind: "ConfigMap", name_pattern: /secrets-inventory$/ }, + { kind: "ResourceQuota", name_pattern: /.*/ }, + { kind: "Secret", name_pattern: /env-var$/ }, + { kind: "Secret", name_pattern: /file$/ } + ].freeze + + # @param [RemoteDevelopment::DesiredConfig] record + # @param [Symbol] attribute + # @param [Array] value + # @return [void] The result is stored in the errors param + def validate_each(record, attribute, value) + unless value.is_a?(Array) + record.errors.add(attribute, "must be an array") + return + end + + if value.empty? + record.errors.add(attribute, "must not be empty") + return + end + + value = value.map(&:deep_symbolize_keys) + normalized_expected_array = normalize_expected_order(value) + normalized_config_array = value.map do |item| + item => { + kind: String => item_kind, + metadata: { + name: String => item_name + }, + ** + } + + "#{item_kind}/#{item_name}" + end + + validate_order(normalized_config_array, normalized_expected_array, record.errors, attribute) + end + + private + + # @param [Array] normalized_config_array + # @param [Array] normalized_expected_array + # @param [ActiveModel::Errors] errors - stores the validation results + # @param [Symbol] attribute_symbol - symbol of the attribute associated with the error + # @return [Void] + def validate_order(normalized_config_array, normalized_expected_array, errors, attribute_symbol) + normalized_config_array.each_with_index do |item, index| + expected_index = normalized_expected_array.index(item) + + if expected_index.nil? + errors.add(attribute_symbol, "item #{item} at index #{index} is unexpected") + next + end + + next if expected_index == index + + errors.add(attribute_symbol, "item #{item} at index #{index} must be at #{expected_index}") + end + end + + # This method normalizes the expected order of items based on the config array + # It creates a mapping of expected positions for each kind/name combination + # + # @param [Array] config_array The array of configuration items to normalize + # @return [Array] An array with expected order of items + def normalize_expected_order(config_array) + expected_positions = [] + + EXPECTED_SEQUENCE.each_with_index do |expected, _| + config_array.each do |item| + item => { + kind: String => item_kind, + metadata: { + name: String => item_name + } + } + + expected_positions << "#{item_kind}/#{item_name}" if matches?(item_kind, item_name, expected) + end + end + + expected_positions + end + + # Validates if the given configuration matches the expected value + # + # @param [String] kind The type of configuration to validate + # @param [String] name The name of the configuration item + # @param [Object] expected The expected value to match against. See {#EXPECTED_SEQUENCE} above. + # @return [Boolean] Returns true if the configuration matches the expected value, false otherwise + def matches?(kind, name, expected) + kind == expected[:kind] && expected[:name_pattern].match?(name) + end + end + + # rubocop:enable Migration/BatchedMigrationBaseClass + 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 index de32273d1b33b8ec6d5a9ca46a17ff37e4256e74..f48142319f4d1bf3d8688b6da7b0b21306885fdf 100644 --- a/lib/gitlab/background_migration/remote_development/bm_files.rb +++ b/lib/gitlab/background_migration/remote_development/bm_files.rb @@ -44,8 +44,8 @@ def self.kubernetes_poststart_hook_command 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") + def self.container_keepalive_command_args + read_file("workspace_operations/create/bm_container_keepalive_command_args.sh") end # @return [String] content of the file @@ -91,7 +91,7 @@ def self.internal_poststart_command_clone_unshallow_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 + CONTAINER_KEEPALIVE_COMMAND_ARGS = container_keepalive_command_args # @return [Array] def self.all_expected_file_constants @@ -107,7 +107,7 @@ def self.all_expected_file_constants :INTERNAL_POSTSTART_COMMAND_START_SSHD_SCRIPT, :KUBERNETES_LEGACY_POSTSTART_HOOK_COMMAND, :KUBERNETES_POSTSTART_HOOK_COMMAND, - :MAIN_COMPONENT_UPDATER_CONTAINER_ARGS + :CONTAINER_KEEPALIVE_COMMAND_ARGS ] end 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 index 262517fb77408b275b95a721bb1953dbf21e6854..0286b04fd1e94cbec09bae46d8c0a07576fee465 100644 --- 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 @@ -22,6 +22,7 @@ class BmDesiredConfig # rubocop:disable Migration/BatchedMigrationBaseClass -- T detail_errors: true, size_limit: 64.kilobytes } + validate :desired_config_validator # @param [BmDesiredConfig] other # @return [Boolean] @@ -45,6 +46,12 @@ def diff(other) Hashdiff.diff(desired_config_array, other.desired_config_array, use_lcs: false) end + # @return [Object] + def desired_config_validator + validator = BmDesiredConfigArrayValidator.new(attributes: [:desired_config_array]) + validator.validate_each(self, :desired_config_array, desired_config_array) + end + # @return [Array] def symbolized_desired_config_array as_json.fetch("desired_config_array").map(&:deep_symbolize_keys) 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_container_keepalive_command_args.sh similarity index 100% rename from lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_main_component_updater_container_args.sh rename to lib/gitlab/background_migration/remote_development/workspace_operations/create/bm_container_keepalive_command_args.sh diff --git a/spec/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator_spec.rb b/spec/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..58fa461ef84fd87f4059df93aa284bc07fb31a04 --- /dev/null +++ b/spec/lib/gitlab/background_migration/remote_development/bm_desired_config_array_validator_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoteDevelopment::BmDesiredConfigArrayValidator, feature_category: :workspaces do + include_context "with remote development shared fixtures" + let(:model) do + Class.new do + # @return [String] + def self.name + "DesiredConfigArrayValidatorTest" + end + + include ActiveModel::Model + include ActiveModel::Validations + + attr_accessor :desired_config_array + alias_method :desired_config_before_type_cast, :desired_config_array + + validates :desired_config_array, 'remote_development/desired_config_array': true + end.new + end + + let(:desired_config_array_with_jumbled_items) do + items = create_desired_config_array + items[0], items[-1] = items[-1], items[0] + items[6], items[7] = items[7], items[6] + items + end + + let(:desired_config_array_with_unexpected_items) do + items = create_desired_config_array + items << { kind: "UnexpectedKind", metadata: { name: "unexpected-item" } } + items + end + + let(:desired_config_array_with_missing_items) { create_desired_config_array.tap { |items| items.delete_at(3) } } + + using RSpec::Parameterized::TableSyntax + + where(:desired_config_array, :validity, :errors) do + # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors + # @formatter:off - Turn off RubyMine autoformatting + create_desired_config_array | true | {} + ref(:desired_config_array_with_missing_items) | true | {} + ref(:desired_config_array_with_unexpected_items) | false | { desired_config_array: ["item UnexpectedKind/unexpected-item at index 12 is unexpected"] } + ref(:desired_config_array_with_jumbled_items) | false | { desired_config_array: ["item Secret/workspace-991-990-fedcba-file at index 0 must be at 11", "item ConfigMap/workspace-991-990-fedcba-scripts-configmap at index 6 must be at 7", "item NetworkPolicy/workspace-991-990-fedcba at index 7 must be at 6", "item ConfigMap/workspace-991-990-fedcba-workspace-inventory at index 11 must be at 0"] } + nil | false | { desired_config_array: ['must be an array'] } + {} | false | { desired_config_array: ['must be an array'] } + [] | false | { desired_config_array: ['must not be empty'] } + # @formatter:on + # rubocop:enable Layout/LineLength + end + + with_them do + before do + model.desired_config_array = desired_config_array + model.validate + end + + it { expect(model.valid?).to eq(validity) } + it { expect(model.errors.messages).to eq(errors) } + end +end