diff --git a/app/validators/kubernetes_container_resources_validator.rb b/app/validators/kubernetes_container_resources_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..f261f8de27d6ae3bda48af88415ddb7abf56a9be --- /dev/null +++ b/app/validators/kubernetes_container_resources_validator.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# KubernetesPodContainerResourcesValidator +# +# Validates that value is a Kubernetes resource specifying cpu and memory. +# +# Example: +# +# class Group < ActiveRecord::Base +# validates :resource, presence: true, kubernetes_pod_container_resources: true +# end + +class KubernetesContainerResourcesValidator < ActiveModel::EachValidator # rubocop:disable Gitlab/NamespacedClass -- This is a globally shareable validator, but it's unclear what namespace it should belong in + # https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/#cpu-units + # The CPU resource is measured in CPU units. Fractional values are allowed. You can use the suffix m to mean milli. + # (\d+m|\d+(\.\d*)?): Two alternatives separated by |: + # \d+m: Matches positive whole numbers followed by "m". + # \d+(\.\d*)?: Matches positive decimal numbers. + CPU_UNITS = /^(\d+m|\d+(\.\d*)?)$/ + + # https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/#memory-units + # The memory resource is measured in bytes. You can express memory as a plain integer or a fixed-point integer + # with one of these suffixes: E, P, T, G, M, K, Ei, Pi, Ti, Gi, Mi, Ki. + # \d+(\.\d*)?: Matches positive decimal numbers. + # ([EPTGMK]|[EPTGMK][i])?: Optional suffix part, where: + # [EPTGMK]: Matches a single character from the set E, P, T, G, M, K. + # [EPTGMK]i: Matches characters from the set followed by an "i". + MEMORY_UNITS = /^\d+(\.\d*)?([EPTGMK]|[EPTGMK]i)?$/ + + def validate_each(record, attribute, value) + unless value.is_a?(Hash) + record.errors.add(attribute, _("must be a hash")) + return + end + + if value == {} + record.errors.add( + attribute, + _("must be a hash containing 'cpu' and 'memory' attribute of type string") + ) + return + end + + cpu = value.deep_symbolize_keys.fetch(:cpu, nil) + unless cpu.is_a?(String) + record.errors.add( + attribute, + format(_("'cpu: %{cpu}' must be a string"), cpu: cpu) + ) + end + + if cpu.is_a?(String) && !CPU_UNITS.match?(cpu) + record.errors.add( + attribute, + format(_("'cpu: %{cpu}' must match the regex '%{cpu_regex}'"), cpu: cpu, cpu_regex: CPU_UNITS.source) + ) + end + + memory = value.deep_symbolize_keys.fetch(:memory, nil) + unless memory.is_a?(String) + record.errors.add( + attribute, + format(_("'memory: %{memory}' must be a string"), memory: memory) + ) + end + + if memory.is_a?(String) && !MEMORY_UNITS.match?(memory) # rubocop:disable Style/GuardClause -- Easier to read this way + record.errors.add( + attribute, + format(_("'memory: %{memory}' must match the regex '%{memory_regex}'"), + memory: memory, + memory_regex: MEMORY_UNITS.source + ) + ) + end + end +end diff --git a/ee/app/models/remote_development/remote_development_agent_config.rb b/ee/app/models/remote_development/remote_development_agent_config.rb index f962c3a71efed91de8b6da53eff22bc892c5ae2b..4225e892ddeee6965ed5f590783891b3f5951c9e 100644 --- a/ee/app/models/remote_development/remote_development_agent_config.rb +++ b/ee/app/models/remote_development/remote_development_agent_config.rb @@ -22,6 +22,12 @@ class RemoteDevelopmentAgentConfig < ApplicationRecord validates :network_policy_egress, json_schema: { filename: 'remote_development_agent_configs_network_policy_egress' } validates :network_policy_egress, 'remote_development/network_policy_egress': true + validates :default_resources_per_workspace_container, + json_schema: { filename: 'remote_development_agent_configs_workspace_container_resources' } + validates :default_resources_per_workspace_container, 'remote_development/workspace_container_resources': true + validates :max_resources_per_workspace, + json_schema: { filename: 'remote_development_agent_configs_workspace_container_resources' } + validates :max_resources_per_workspace, 'remote_development/workspace_container_resources': true # noinspection RubyResolve - likely due to https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31540 before_validation :prevent_dns_zone_update, if: ->(record) { record.persisted? && record.dns_zone_changed? } diff --git a/ee/app/validators/json_schemas/remote_development_agent_configs_workspace_container_resources.json b/ee/app/validators/json_schemas/remote_development_agent_configs_workspace_container_resources.json new file mode 100644 index 0000000000000000000000000000000000000000..3e4d5fd51c669d2b961f0e33d0e22a1c76c6d6a5 --- /dev/null +++ b/ee/app/validators/json_schemas/remote_development_agent_configs_workspace_container_resources.json @@ -0,0 +1,28 @@ +{ + "description": "Default/max resources of the workspace pod/container for Remote Development Agent Configs", + "type": "object", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + } + }, + "requests": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + } + } + } +} diff --git a/ee/app/validators/remote_development/workspace_container_resources_validator.rb b/ee/app/validators/remote_development/workspace_container_resources_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..622f6e535c82296a9b9c652eedd24f48c27101da --- /dev/null +++ b/ee/app/validators/remote_development/workspace_container_resources_validator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class WorkspaceContainerResourcesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return true if value == {} + + unless value.is_a?(Hash) + record.errors.add(attribute, _("must be a hash")) + return + end + + limits = value.deep_symbolize_keys.fetch(:limits, nil) + unless limits.is_a?(Hash) + record.errors.add(attribute, _("must be a hash containing 'limits' attribute of type hash")) + return + end + + requests = value.deep_symbolize_keys.fetch(:requests, nil) + unless requests.is_a?(Hash) + record.errors.add(attribute, _("must be a hash containing 'requests' attribute of type hash")) + return + end + + resources_validator = KubernetesContainerResourcesValidator.new(attributes: attribute) + resources_validator.validate_each(record, "#{attribute}_limits", limits) + resources_validator.validate_each(record, "#{attribute}_requests", requests) + end + end +end diff --git a/ee/lib/remote_development/agent_config/updater.rb b/ee/lib/remote_development/agent_config/updater.rb index 3c4645edcb196d7c87f47fcab3ed6931d73c044d..fd254c180eb99551c0c03603e464980e6aca5819 100644 --- a/ee/lib/remote_development/agent_config/updater.rb +++ b/ee/lib/remote_development/agent_config/updater.rb @@ -15,6 +15,8 @@ class Updater ] } ].freeze + DEFAULT_RESOURCES_PER_WORKSPACE_CONTAINER_DEFAULT = {}.freeze + MAX_RESOURCES_PER_WORKSPACE_DEFAULT = {}.freeze # @param [Hash] value # @return [Result] @@ -37,6 +39,10 @@ def self.update(value) config_from_agent_config_file.fetch(:network_policy, {}).fetch(:egress, NETWORK_POLICY_EGRESS_DEFAULT) model_instance.gitlab_workspaces_proxy_namespace = config_from_agent_config_file.fetch(:gitlab_workspaces_proxy, {}).fetch(:namespace, 'gitlab-workspaces') + model_instance.default_resources_per_workspace_container = + config_from_agent_config_file.fetch(:default_resources_per_workspace_container, {}) + model_instance.max_resources_per_workspace = + config_from_agent_config_file.fetch(:max_resources_per_workspace, {}) if model_instance.save model_instance.workspaces.without_terminated.update_all(force_include_all_resources: true) diff --git a/ee/spec/lib/remote_development/agent_config/updater_spec.rb b/ee/spec/lib/remote_development/agent_config/updater_spec.rb index 53ee4d798f0764e9066d3772b88cc4b92ee51aa3..7866a768a487f6bdbb4b64db05afcd74cb06373d 100644 --- a/ee/spec/lib/remote_development/agent_config/updater_spec.rb +++ b/ee/spec/lib/remote_development/agent_config/updater_spec.rb @@ -29,6 +29,17 @@ { namespace: gitlab_workspaces_proxy_namespace } end + let(:default_default_resources_per_workspace_container) do + RemoteDevelopment::AgentConfig::Updater::DEFAULT_RESOURCES_PER_WORKSPACE_CONTAINER_DEFAULT + end + + let(:default_resources_per_workspace_container) { default_default_resources_per_workspace_container } + let(:default_max_resources_per_workspace) do + RemoteDevelopment::AgentConfig::Updater::MAX_RESOURCES_PER_WORKSPACE_DEFAULT + end + + let(:max_resources_per_workspace) { default_max_resources_per_workspace } + let_it_be(:agent) { create(:cluster_agent) } let_it_be(:workspace1) { create(:workspace, force_include_all_resources: false) } let_it_be(:workspace2) { create(:workspace, force_include_all_resources: false) } @@ -40,6 +51,8 @@ } remote_development_config[:network_policy] = network_policy if network_policy_present remote_development_config[:gitlab_workspaces_proxy] = gitlab_workspaces_proxy if gitlab_workspaces_proxy_present + remote_development_config[:default_resources_per_workspace_container] = default_resources_per_workspace_container + remote_development_config[:max_resources_per_workspace] = max_resources_per_workspace { remote_development: remote_development_config } @@ -63,7 +76,7 @@ end context 'when config passed is not empty' do - context 'when a config file is valid' do + shared_examples 'successful update' do it 'creates a config record and returns an ok Result containing the agent config' do expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } @@ -73,14 +86,22 @@ expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) + expect(config_instance.default_resources_per_workspace_container.deep_symbolize_keys) + .to eq(default_resources_per_workspace_container) + expect(config_instance.max_resources_per_workspace.deep_symbolize_keys) + .to eq(max_resources_per_workspace) expect(result) .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( { remote_development_agent_config: config_instance } )) expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) + .to all(have_attributes(force_include_all_resources: true)) end + end + + context 'when a config file is valid' do + it_behaves_like 'successful update' context 'when enabled is not present in the config passed' do let(:config) { { remote_development: { dns_zone: dns_zone } } } @@ -98,45 +119,13 @@ context 'when network_policy key is empty hash in the config passed' do let(:network_policy) { {} } - it 'creates a config record with default value and returns an ok Result containing the agent config' do - expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } - - config_instance = agent.reload.remote_development_agent_config - expect(config_instance.enabled).to eq(enabled) - expect(config_instance.dns_zone).to eq(dns_zone) - expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) - expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) - expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) - - expect(result) - .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( - { remote_development_agent_config: config_instance } - )) - expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) - end + it_behaves_like 'successful update' end context 'when network_policy.enabled is explicitly specified in the config passed' do let(:network_policy_enabled) { false } - it 'creates a config record with specified value and returns an ok Result containing the agent config' do - expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } - - config_instance = agent.reload.remote_development_agent_config - expect(config_instance.enabled).to eq(enabled) - expect(config_instance.dns_zone).to eq(dns_zone) - expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) - expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) - expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) - - expect(result) - .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( - { remote_development_agent_config: config_instance } - )) - expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) - end + it_behaves_like 'successful update' end context 'when network_policy.egress is explicitly specified in the config passed' do @@ -153,23 +142,7 @@ let(:network_policy) { network_policy_with_egress } - it 'creates a config record with specified value and returns an ok Result containing the agent config' do - expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } - - config_instance = agent.reload.remote_development_agent_config - expect(config_instance.enabled).to eq(enabled) - expect(config_instance.dns_zone).to eq(dns_zone) - expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) - expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) - expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) - - expect(result) - .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( - { remote_development_agent_config: config_instance } - )) - expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) - end + it_behaves_like 'successful update' end end @@ -179,45 +152,45 @@ context 'when gitlab_workspaces_proxy is empty hash in the config passed' do let(:gitlab_workspaces_proxy) { {} } - it 'creates a config record with default value and returns an ok Result containing the agent config' do - expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } - - config_instance = agent.reload.remote_development_agent_config - expect(config_instance.enabled).to eq(enabled) - expect(config_instance.dns_zone).to eq(dns_zone) - expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) - expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) - expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) - - expect(result) - .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( - { remote_development_agent_config: config_instance } - )) - expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) - end + it_behaves_like 'successful update' end context 'when gitlab_workspaces_proxy.namespace is explicitly specified in the config passed' do let(:gitlab_workspaces_proxy_namespace) { 'gitlab-workspaces-specified' } - it 'creates a config record with specified value and returns an ok Result containing the agent config' do - expect { result }.to change { RemoteDevelopment::RemoteDevelopmentAgentConfig.count } - - config_instance = agent.reload.remote_development_agent_config - expect(config_instance.enabled).to eq(enabled) - expect(config_instance.dns_zone).to eq(dns_zone) - expect(config_instance.network_policy_enabled).to eq(network_policy_enabled) - expect(config_instance.network_policy_egress.map(&:deep_symbolize_keys)).to eq(network_policy_egress) - expect(config_instance.gitlab_workspaces_proxy_namespace).to eq(gitlab_workspaces_proxy_namespace) - - expect(result) - .to be_ok_result(RemoteDevelopment::Messages::AgentConfigUpdateSuccessful.new( - { remote_development_agent_config: config_instance } - )) - expect(config_instance.workspaces.without_terminated) - .to all(have_attributes(force_include_all_resources: true)) + it_behaves_like 'successful update' + end + end + + context 'when default_resources_per_workspace_container is present in the config passed' do + context 'when gitlab_workspaces_proxy is empty hash in the config passed' do + let(:default_resources_per_workspace_container) { {} } + + it_behaves_like 'successful update' + end + + context 'when default_resources_per_workspace_container is explicitly specified in the config passed' do + let(:default_resources_per_workspace_container) do + { limits: { cpu: "500m", memory: "1Gi" }, requests: { cpu: "200m", memory: "0.5Gi" } } end + + it_behaves_like 'successful update' + end + end + + context 'when max_resources_per_workspace is present in the config passed' do + context 'when gitlab_workspaces_proxy is empty hash in the config passed' do + let(:max_resources_per_workspace) { {} } + + it_behaves_like 'successful update' + end + + context 'when max_resources_per_workspace is explicitly specified in the config passed' do + let(:max_resources_per_workspace) do + { limits: { cpu: "500m", memory: "1Gi" }, requests: { cpu: "200m", memory: "0.5Gi" } } + end + + it_behaves_like 'successful update' end end end diff --git a/ee/spec/models/remote_development/remote_development_agent_config_spec.rb b/ee/spec/models/remote_development/remote_development_agent_config_spec.rb index 18fd3a860ccab4071dc93d14a2336cb4d9d73033..2badb4817662ba3546a4489194ec545edce7613b 100644 --- a/ee/spec/models/remote_development/remote_development_agent_config_spec.rb +++ b/ee/spec/models/remote_development/remote_development_agent_config_spec.rb @@ -6,6 +6,8 @@ RSpec.describe RemoteDevelopment::RemoteDevelopmentAgentConfig, feature_category: :remote_development do # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31543 let_it_be_with_reload(:agent) { create(:ee_cluster_agent, :with_remote_development_agent_config) } + let(:default_default_resources_per_workspace_container) { {} } + let(:default_max_resources_per_workspace) { {} } let(:default_network_policy_egress) do [ { @@ -74,5 +76,33 @@ 'must be an array' ) end + + it 'when default_resources_per_workspace_container is not specified explicitly' do + expect(config).to be_valid + expect(config.default_resources_per_workspace_container).to eq(default_default_resources_per_workspace_container) + end + + it 'when default_resources_per_workspace_container is nil' do + config.default_resources_per_workspace_container = nil + expect(config).not_to be_valid + expect(config.errors[:default_resources_per_workspace_container]).to include( + 'must be a valid json schema', + 'must be a hash' + ) + end + + it 'when max_resources_per_workspace is not specified explicitly' do + expect(config).to be_valid + expect(config.max_resources_per_workspace).to eq(default_max_resources_per_workspace) + end + + it 'when default_resources_per_workspace_container is nil' do + config.max_resources_per_workspace = nil + expect(config).not_to be_valid + expect(config.errors[:max_resources_per_workspace]).to include( + 'must be a valid json schema', + 'must be a hash' + ) + end end end diff --git a/ee/spec/validators/remote_development/workspace_container_resources_validator_spec.rb b/ee/spec/validators/remote_development/workspace_container_resources_validator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee7064777cd85ff868d019320adcec80a7f14457 --- /dev/null +++ b/ee/spec/validators/remote_development/workspace_container_resources_validator_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RemoteDevelopment::WorkspaceContainerResourcesValidator, feature_category: :remote_development do + let(:model) do + Class.new do + include ActiveModel::Model + include ActiveModel::Validations + + attr_accessor :resources + alias_method :resources_before_type_cast, :resources + + validates :resources, 'remote_development/workspace_container_resources': true + end.new + end + + using RSpec::Parameterized::TableSyntax + + # noinspection RubyMismatchedArgumentType - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-32041 + where(:resources, :validity, :errors) do + # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors + nil | false | { resources: ['must be a hash'] } + 'not-an-array' | false | { resources: ['must be a hash'] } + { limits: nil } | false | { resources: ["must be a hash containing 'limits' attribute of type hash"] } + { limits: { cpu: "500Invalid", memory: "1Gi" }, requests: nil } | false | { resources: ["must be a hash containing 'requests' attribute of type hash"] } + { limits: 1 } | false | { resources: ["must be a hash containing 'limits' attribute of type hash"] } + { limits: { cpu: "500Invalid", memory: "1Gi" }, requests: 1 } | false | { resources: ["must be a hash containing 'requests' attribute of type hash"] } + { limits: {}, requests: {} } | false | { resources_limits: ["must be a hash containing 'cpu' and 'memory' attribute of type string"], resources_requests: ["must be a hash containing 'cpu' and 'memory' attribute of type string"] } + { limits: { cpu: 1, memory: 5 }, requests: { cpu: 1, memory: 5 } } | false | { resources_limits: ["'cpu: 1' must be a string", "'memory: 5' must be a string"], resources_requests: ["'cpu: 1' must be a string", "'memory: 5' must be a string"] } + {} | true | {} + { limits: { cpu: "500m", memory: "1Gi" }, requests: { cpu: "200m", memory: "0.5Gi" } } | true | {} + # rubocop:enable Layout/LineLength + end + + with_them do + before do + model.resources = resources + model.validate + end + + it { expect(model.valid?).to eq(validity) } + it { expect(model.errors.messages).to eq(errors) } + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a616fc197dfade2e3bc0af5b607c2fb7dc8f91d0..60e524b1ca57ef5f7555965cea3d25788af7ed82 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1378,9 +1378,21 @@ msgstr "" msgid "'allow: %{allow}' must be a string" msgstr "" +msgid "'cpu: %{cpu}' must be a string" +msgstr "" + +msgid "'cpu: %{cpu}' must match the regex '%{cpu_regex}'" +msgstr "" + msgid "'except: %{except}' must be an array of string" msgstr "" +msgid "'memory: %{memory}' must be a string" +msgstr "" + +msgid "'memory: %{memory}' must match the regex '%{memory_regex}'" +msgstr "" + msgid "'projects' is not yet supported" msgstr "" @@ -58147,6 +58159,18 @@ msgstr "" msgid "must be a boolean value" msgstr "" +msgid "must be a hash" +msgstr "" + +msgid "must be a hash containing 'cpu' and 'memory' attribute of type string" +msgstr "" + +msgid "must be a hash containing 'limits' attribute of type hash" +msgstr "" + +msgid "must be a hash containing 'requests' attribute of type hash" +msgstr "" + msgid "must be a root group." msgstr "" diff --git a/spec/validators/kubernetes_container_resources_validator_spec.rb b/spec/validators/kubernetes_container_resources_validator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..aea561bafb9a74f5e6db20f5f8031cc0a17799bb --- /dev/null +++ b/spec/validators/kubernetes_container_resources_validator_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe KubernetesContainerResourcesValidator, feature_category: :shared do + let(:model) do + Class.new do + include ActiveModel::Model + include ActiveModel::Validations + + attr_accessor :resources + alias_method :resources_before_type_cast, :resources + + validates :resources, kubernetes_container_resources: true + end.new + end + + using RSpec::Parameterized::TableSyntax + + # noinspection RubyMismatchedArgumentType - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-32041 + where(:resources, :validity, :errors) do + # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors + nil | false | { resources: ["must be a hash"] } + '' | false | { resources: ["must be a hash"] } + {} | false | { resources: ["must be a hash containing 'cpu' and 'memory' attribute of type string"] } + { cpu: nil, memory: nil } | false | { resources: ["'cpu: ' must be a string", "'memory: ' must be a string"] } + { cpu: "123di", memory: "123oi" } | false | { resources: ["'cpu: 123di' must match the regex '^(\\d+m|\\d+(\\.\\d*)?)$'", "'memory: 123oi' must match the regex '^\\d+(\\.\\d*)?([EPTGMK]|[EPTGMK]i)?$'"] } + { cpu: "123di", memory: "123oi" } | false | { resources: ["'cpu: 123di' must match the regex '^(\\d+m|\\d+(\\.\\d*)?)$'", "'memory: 123oi' must match the regex '^\\d+(\\.\\d*)?([EPTGMK]|[EPTGMK]i)?$'"] } + { cpu: "100m", memory: "123Mi" } | true | {} + # rubocop:enable Layout/LineLength + end + + with_them do + before do + model.resources = resources + model.validate + end + + it { expect(model.valid?).to eq(validity) } + it { expect(model.errors.messages).to eq(errors) } + end +end