diff --git a/app/graphql/types/ci/config_variable_type.rb b/app/graphql/types/ci/config_variable_type.rb index 5b5890fd5a56c806618c3ec8a632aaf260d77011..020af5b2444625a2d6431dd8a595c17e61e02c45 100644 --- a/app/graphql/types/ci/config_variable_type.rb +++ b/app/graphql/types/ci/config_variable_type.rb @@ -19,6 +19,7 @@ class ConfigVariableType < BaseObject # rubocop:disable Graphql/AuthorizeTypes description: 'Value of the variable.' field :value_options, [GraphQL::Types::String], + hash_key: :options, null: true, description: 'Value options for the variable.' end diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb index 1e6dbb22209bd084285a1b5e16ee9f072da1ec5b..a3d57ab6ac6b12f07e15dbb7e6ab416062fb91a8 100644 --- a/lib/gitlab/ci/config/entry/root.rb +++ b/lib/gitlab/ci/config/entry/root.rb @@ -50,7 +50,7 @@ class Root < ::Gitlab::Config::Entry::Node entry :variables, Entry::Variables, description: 'Environment variables that will be used.', - metadata: { allowed_value_data: %i[value description expand], allow_array_value: true }, + metadata: { allowed_value_data: %i[value description expand options] }, reserved: true entry :stages, Entry::Stages, diff --git a/lib/gitlab/ci/config/entry/variable.rb b/lib/gitlab/ci/config/entry/variable.rb index 16091758916e26b121d10847cec69c6b9a17646f..decb568ffc920efc4b14eacb31d836f5fe7abd32 100644 --- a/lib/gitlab/ci/config/entry/variable.rb +++ b/lib/gitlab/ci/config/entry/variable.rb @@ -10,7 +10,6 @@ module Entry class Variable < ::Gitlab::Config::Entry::Simplifiable strategy :SimpleVariable, if: -> (config) { SimpleVariable.applies_to?(config) } strategy :ComplexVariable, if: -> (config) { ComplexVariable.applies_to?(config) } - strategy :ComplexArrayVariable, if: -> (config) { ComplexArrayVariable.applies_to?(config) } class SimpleVariable < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable @@ -41,20 +40,24 @@ def value_with_prefill_data class ComplexVariable < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable class << self def applies_to?(config) - config.is_a?(Hash) && !config[:value].is_a?(Array) + config.is_a?(Hash) end end + attributes :value, :description, :expand, :options, prefix: :config + validations do validates :key, alphanumeric: true - validates :config_value, alphanumeric: true, allow_nil: false, if: :config_value_defined? - validates :config_description, alphanumeric: true, allow_nil: false, if: :config_description_defined? - validates :config_expand, boolean: true, - allow_nil: false, - if: -> { ci_raw_variables_in_yaml_config_enabled? && config_expand_defined? } + validates :config_value, alphanumeric: true, allow_nil: true + validates :config_description, alphanumeric: true, allow_nil: true + validates :config_expand, boolean: true, allow_nil: true, if: -> { + ci_raw_variables_in_yaml_config_enabled? + } + validates :config_options, array_of_strings: true, allow_nil: true validate do allowed_value_data = Array(opt(:allowed_value_data)) @@ -66,91 +69,43 @@ def applies_to?(config) else errors.add(:config, "must be a string") end + + if config_options.present? && config_options.exclude?(config_value) + errors.add(:config, 'value must be present in options') + end end end def value + # Needed since the `Entry::Node` provides `value` (which is current hash) config_value.to_s end def value_with_data if ci_raw_variables_in_yaml_config_enabled? { - value: value, - raw: (!config_expand if config_expand_defined?) + value: config_value.to_s, + raw: (!config_expand if has_config_expand?) }.compact else { - value: value + value: config_value.to_s }.compact end end def value_with_prefill_data value_with_data.merge( - description: config_description + description: config_description, + options: config_options ).compact end - def config_value - @config[:value] - end - - def config_description - @config[:description] - end - - def config_expand - @config[:expand] - end - - def config_value_defined? - config.key?(:value) - end - - def config_description_defined? - config.key?(:description) - end - - def config_expand_defined? - config.key?(:expand) - end - def ci_raw_variables_in_yaml_config_enabled? YamlProcessor::FeatureFlags.enabled?(:ci_raw_variables_in_yaml_config) end end - class ComplexArrayVariable < ComplexVariable - include ::Gitlab::Config::Entry::Validatable - - class << self - def applies_to?(config) - config.is_a?(Hash) && config[:value].is_a?(Array) - end - end - - validations do - validates :config_value, array_of_strings: true, allow_nil: false, if: :config_value_defined? - - validate do - next if opt(:allow_array_value) - - errors.add(:config, 'value must be an alphanumeric string') - end - end - - def value - config_value.first - end - - def value_with_prefill_data - super.merge( - value_options: config_value - ).compact - end - end - class UnknownStrategy < ::Gitlab::Config::Entry::Node def errors ["variable definition must be either a string or a hash"] diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index ef4f74b9f56e0ff25a3e01e190728fe9a8627e96..e338bce3109a92d56f062ffa301627ba5b88677e 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -42,7 +42,7 @@ def composable_class(_name, _config) end def composable_metadata - { allowed_value_data: opt(:allowed_value_data), allow_array_value: opt(:allow_array_value) } + { allowed_value_data: opt(:allowed_value_data) } end end end diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb index d266d5218de06ce86286e2fead4c0555dc5f07e6..c8ad25215748c2f6f16bd31b95b8b0e65f9bac41 100644 --- a/lib/gitlab/config/entry/attributable.rb +++ b/lib/gitlab/config/entry/attributable.rb @@ -7,19 +7,21 @@ module Attributable extend ActiveSupport::Concern class_methods do - def attributes(*attributes) + def attributes(*attributes, prefix: nil) attributes.flatten.each do |attribute| - if method_defined?(attribute) - raise ArgumentError, "Method '#{attribute}' already defined in '#{name}'" + attribute_method = prefix ? "#{prefix}_#{attribute}" : attribute + + if method_defined?(attribute_method) + raise ArgumentError, "Method '#{attribute_method}' already defined in '#{name}'" end - define_method(attribute) do + define_method(attribute_method) do return unless config.is_a?(Hash) config[attribute] end - define_method("has_#{attribute}?") do + define_method("has_#{attribute_method}?") do config.is_a?(Hash) && config.key?(attribute) end end diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index 3e699b93fd3ba52ce0edadcb2142ee3d6ccdaab7..2360dd7d103c827a7cc6fd3b44a6bf449eec2665 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -295,11 +295,11 @@ describe('Pipeline New Form', () => { expect(dropdownItems.at(2).text()).toBe(valueOptions[2]); }); - it('variables with multiple predefined values sets the first option as the default', () => { + it('variable with multiple predefined values sets value as the default', () => { const dropdown = findValueDropdowns().at(0); const { valueOptions } = mockYamlVariables[2]; - expect(dropdown.props('text')).toBe(valueOptions[0]); + expect(dropdown.props('text')).toBe(valueOptions[1]); }); }); diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js index e95a65171fcf3001016e1d3683c67641db6c8422..2af0ef4d7c46aa2b15ceb0d157615c3f3522617f 100644 --- a/spec/frontend/pipeline_new/mock_data.js +++ b/spec/frontend/pipeline_new/mock_data.js @@ -83,7 +83,7 @@ export const mockYamlVariables = [ { description: 'This is a variable with predefined values.', key: 'VAR_WITH_OPTIONS', - value: 'development', + value: 'staging', valueOptions: ['development', 'staging', 'production'], }, ]; @@ -105,7 +105,7 @@ export const mockYamlVariablesWithoutDesc = [ { description: null, key: 'VAR_WITH_OPTIONS', - value: 'development', + value: 'staging', valueOptions: ['development', 'staging', 'production'], }, ]; diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index b9a705a4e1f5e54edf69075e41bdbc7286cd9c8f..c40589104cd0aaf2d100637f4d35710576710392 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -38,7 +38,7 @@ variables: { VAR: 'root', VAR2: { value: 'val 2', description: 'this is var 2' }, - VAR3: { value: %w[val3 val3b], description: 'this is var 3' } + VAR3: { value: 'val3', options: %w[val3 val4 val5], description: 'this is var 3 and some options' } }, after_script: ['make clean'], stages: %w(build pages release), @@ -326,6 +326,42 @@ end end + context 'when variables have `options` data' do + before do + root.compose! + end + + context 'and the value is in the `options` array' do + let(:hash) do + { + variables: { 'VAR' => { value: 'val1', options: %w[val1 val2] } }, + rspec: { script: 'bin/rspec' } + } + end + + it 'returns correct value' do + expect(root.variables_entry.value_with_data).to eq( + 'VAR' => { value: 'val1' } + ) + + expect(root.variables_value).to eq('VAR' => 'val1') + end + end + + context 'and the value is not in the `options` array' do + let(:hash) do + { + variables: { 'VAR' => { value: 'val', options: %w[val1 val2] } }, + rspec: { script: 'bin/rspec' } + } + end + + it 'returns an error' do + expect(root.errors).to contain_exactly('variables:var config value must be present in options') + end + end + end + context 'when variables have "expand" data' do let(:hash) do { diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb index d7023072312fc69813d993df8469ed03418db6be..97b06c8b1a55653be1cbec964753afdf310f2ad5 100644 --- a/spec/lib/gitlab/ci/config/entry/variable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb @@ -306,48 +306,48 @@ end end end - end - describe 'ComplexArrayVariable' do - context 'when allow_array_value metadata is false' do - let(:config) { { value: %w[value value2], description: 'description' } } - let(:metadata) { { allow_array_value: false } } + context 'when config is a hash with options' do + context 'when there is no metadata' do + let(:config) { { value: 'value', options: %w[value value2] } } + let(:metadata) { {} } - describe '#valid?' do - it { is_expected.not_to be_valid } - end + describe '#valid?' do + it { is_expected.not_to be_valid } + end - describe '#errors' do - subject(:errors) { entry.errors } + describe '#errors' do + subject(:errors) { entry.errors } - it { is_expected.to include 'var1 config value must be an alphanumeric string' } + it { is_expected.to include 'var1 config must be a string' } + end end - end - context 'when allow_array_value metadata is true' do - let(:config) { { value: %w[value value2], description: 'description' } } - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when options are allowed' do + let(:config) { { value: 'value', options: %w[value value2] } } + let(:metadata) { { allowed_value_data: %i[value options] } } - describe '#valid?' do - it { is_expected.to be_valid } - end + describe '#valid?' do + it { is_expected.to be_valid } + end - describe '#value' do - subject(:value) { entry.value } + describe '#value' do + subject(:value) { entry.value } - it { is_expected.to eq('value') } - end + it { is_expected.to eq('value') } + end - describe '#value_with_data' do - subject(:value_with_data) { entry.value_with_data } + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } - it { is_expected.to eq(value: 'value') } - end + it { is_expected.to eq(value: 'value') } + end - describe '#value_with_prefill_data' do - subject(:value_with_prefill_data) { entry.value_with_prefill_data } + describe '#value_with_prefill_data' do + subject(:value_with_prefill_data) { entry.value_with_prefill_data } - it { is_expected.to eq(value: 'value', description: 'description', value_options: %w[value value2]) } + it { is_expected.to eq(value: 'value', options: %w[value value2]) } + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 609e4422d5c18298e37a1c1dd944259d31109240..e7dbc78729db2078174c980ee83f54e0f7834d06 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -116,8 +116,8 @@ it_behaves_like 'invalid config', /variable_1 config must be a string/ end - context 'when metadata has allow_array_value and allowed_value_data' do - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when metadata has the allowed_value_data key' do + let(:metadata) { { allowed_value_data: %i[value description options] } } let(:result) do { 'VARIABLE_1' => 'value' } @@ -143,17 +143,15 @@ end end - context 'when entry config value has key-value pair and value is an array' do + context 'when entry config value has options' do let(:config) do - { 'VARIABLE_1' => { value: %w[value1 value2], description: 'variable 1' } } + { 'VARIABLE_1' => { + value: 'value1', options: %w[value1 value2], description: 'variable 1' + } } end - context 'when there is no allowed_value_data metadata' do - it_behaves_like 'invalid config', /variable_1 config value must be an alphanumeric string/ - end - - context 'when metadata has allow_array_value and allowed_value_data' do - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when metadata has allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description options] } } let(:result) do { 'VARIABLE_1' => 'value1' } @@ -172,7 +170,7 @@ describe '#value_with_prefill_data' do it 'returns variable with prefill data' do expect(entry.value_with_prefill_data).to eq( - 'VARIABLE_1' => { value: 'value1', value_options: %w[value1 value2], description: 'variable 1' } + 'VARIABLE_1' => { value: 'value1', options: %w[value1 value2], description: 'variable 1' } ) end end @@ -234,14 +232,6 @@ it_behaves_like 'invalid config', /variable_1 config uses invalid data keys: hello/ end - context 'when entry config value has hash with nil description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', description: nil } } - end - - it_behaves_like 'invalid config', /variable_1 config description must be an alphanumeric string/ - end - context 'when entry config value has hash without description' do let(:config) do { 'VARIABLE_1' => { value: 'value 1' } } diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index 8a207bddaae43491ade2834eab41895aa889c583..0a2f8ac2c3a669bf1b7b16000482e566484c5aa2 100644 --- a/spec/lib/gitlab/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -10,10 +10,11 @@ end let(:instance) { node.new } + let(:prefix) { nil } before do - node.class_eval do - attributes :name, :test + node.class_exec(prefix) do |pre| + attributes :name, :test, prefix: pre end end @@ -24,6 +25,17 @@ .and_return({ name: 'some name', test: 'some test' }) end + context 'and is provided a prefix' do + let(:prefix) { :pre } + + it 'returns the value of config' do + expect(instance).to have_pre_name + expect(instance.pre_name).to eq 'some name' + expect(instance).to have_pre_test + expect(instance.pre_test).to eq 'some test' + end + end + it 'returns the value of config' do expect(instance).to have_name expect(instance.name).to eq 'some name' diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index 945235917654b3d1276e743318e78db054eef723..dbbfe044e35dda65743ed4e2a41e114fd6ea25ca 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -12,7 +12,8 @@ variables: description: 'value of KEY_VALUE_VAR' DB_NAME: postgres ENVIRONMENT_VAR: - value: ['env var value', 'env var value2'] + value: 'env var value' + options: ['env var value', 'env var value2'] description: 'env var description' stages: