diff --git a/README.md b/README.md index bd33c8db..8265c828 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,32 @@ As you may have noticed there are several ways how to specify arguments. Here's * values specified on CLI * interactive mode arguments +## Answer File Schema + +The answer file schema can be described using Puppet types as such: + +``` +Hash[ + String $puppet_class => Hash[ + String $parameter => Enum[true, false, Hash[String, Variant[String, Boolean, Integer, Array, Hash]]] + ] +] +``` + +An example of each available option: + +``` +class_a: true +class_b: false +class_c: {} +class_d: + key: value + key2: 'value' + key3: false + key4: 1 + key5: ['a', 'b'] +``` + ## Requirements Kafo is supported with Puppet versions 4.9+, 5 and 6. Puppet may be installed diff --git a/lib/kafo/answer_file.rb b/lib/kafo/answer_file.rb new file mode 100644 index 00000000..c1b4aff7 --- /dev/null +++ b/lib/kafo/answer_file.rb @@ -0,0 +1,68 @@ +require 'yaml' + +module Kafo + class AnswerFile + + attr_reader :answers, :filename, :version + + def initialize(answer_filename, version: 1, exit_handler: KafoConfigure, logger: KafoConfigure.logger) + @filename = answer_filename + @version = version.nil? ? 1 : version + @exit_handler = exit_handler + @logger = logger + + begin + @answers = YAML.load_file(@filename) + rescue Errno::ENOENT + @exit_handler.exit(:no_answer_file) do + @logger.error "No answer file found at #{@filename}" + end + end + + validate + end + + def filename + @filename + end + + def puppet_classes + @answers.keys.sort + end + + def parameters_for_class(puppet_class) + if @version == 1 + params = @answers[puppet_class] + params.is_a?(Hash) ? params : {} + end + end + + def class_enabled?(puppet_class) + if @version == 1 + value = @answers[puppet_class.is_a?(String) ? puppet_class : puppet_class.identifier] + !!value || value.is_a?(Hash) + end + end + + private + + def validate + if @version == 1 + validate_version_1 + end + end + + def validate_version_1 + invalid = @answers.reject do |puppet_class, value| + value.is_a?(Hash) || [true, false].include?(value) + end + + unless invalid.empty? + @exit_handler.exit(:invalid_values) do + @logger.error("Answer file at #{@filename} has invalid values for #{invalid.keys.join(', ')}. Please ensure they are either a hash or true/false.") + end + end + end + + end +end diff --git a/lib/kafo/configuration.rb b/lib/kafo/configuration.rb index d873b7bb..18da2a2b 100644 --- a/lib/kafo/configuration.rb +++ b/lib/kafo/configuration.rb @@ -6,6 +6,7 @@ require 'kafo/data_type_parser' require 'kafo/execution_environment' require 'kafo/scenario_option' +require 'kafo/answer_file' module Kafo class Configuration @@ -55,13 +56,8 @@ def initialize(file, persist = true) configure_application @logger = KafoConfigure.logger - @answer_file = app[:answer_file] - begin - @data = load_yaml_file(@answer_file) - rescue Errno::ENOENT - puts "No answer file at #{@answer_file} found, can not continue" - KafoConfigure.exit(:no_answer_file) - end + @answer_file = AnswerFile.new(app[:answer_file], version: app[:answer_file_version]) + @answers = @answer_file.answers @config_dir = File.dirname(@config_file) @scenario_id = Configuration.get_scenario_id(@config_file) @@ -130,7 +126,7 @@ def has_custom_fact?(key) def modules @modules ||= begin register_data_types - @data.keys.map { |mod| PuppetModule.new(mod, configuration: self).parse }.sort + @answer_file.puppet_classes.map { |mod| PuppetModule.new(mod, configuration: self).parse }.sort end end @@ -231,14 +227,12 @@ class { '::kafo_configure::dump_values': # if a value is a true we return empty hash because we have no specific options for a # particular puppet module - def [](key) - value = @data[key] - value.is_a?(Hash) ? value : {} + def [](puppet_class) + @answer_file.parameters_for_class(puppet_class) end - def module_enabled?(mod) - value = @data[mod.is_a?(String) ? mod : mod.identifier] - !!value || value.is_a?(Hash) + def module_enabled?(puppet_class) + @answer_file.class_enabled?(puppet_class) end def config_header @@ -248,7 +242,7 @@ def config_header end def store(data, file = nil) - filename = file || answer_file + filename = file || @answer_file.filename FileUtils.touch filename File.chmod 0600, filename File.open(filename, 'w') { |f| f.write(config_header + format(YAML.dump(data))) } @@ -316,17 +310,13 @@ def log_exists? log_files.any? { |f| File.size(f) > 0 } end - def answers - @data - end - def run_migrations migrations = Kafo::Migrations.new(migrations_dir) - @app, @data = migrations.run(app, answers) + @app, @answers = migrations.run(app, @answers) if migrations.migrations.count > 0 @modules = nil # force the lazy loaded modules to reload next time they are used save_configuration(app) - store(answers) + store(@answers) migrations.store_applied @logger.notice("#{migrations.migrations.count} migration/s were applied. Updated configuration was saved.") end diff --git a/test/fixtures/answer_files/v1/basic-answers.yaml b/test/fixtures/answer_files/v1/basic-answers.yaml new file mode 100644 index 00000000..8f31128f --- /dev/null +++ b/test/fixtures/answer_files/v1/basic-answers.yaml @@ -0,0 +1,6 @@ +--- +class_a: true +class_b: + key: value +class_c: {} +class_d: false diff --git a/test/fixtures/answer_files/v1/invalid-answers.yaml b/test/fixtures/answer_files/v1/invalid-answers.yaml new file mode 100644 index 00000000..d0a41c42 --- /dev/null +++ b/test/fixtures/answer_files/v1/invalid-answers.yaml @@ -0,0 +1,4 @@ +--- +class_a: 'true' +class_b: +class_c: 1 diff --git a/test/kafo/answer_file_test.rb b/test/kafo/answer_file_test.rb new file mode 100644 index 00000000..634441f9 --- /dev/null +++ b/test/kafo/answer_file_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +describe 'Kafo::AnswerFile' do + let(:dummy_logger) { DummyLogger.new } + + describe 'answer file version 1' do + describe 'valid answer file' do + let(:answer_file_path) { 'test/fixtures/answer_files/v1/basic-answers.yaml' } + let(:answer_file) { Kafo::AnswerFile.new(answer_file_path) } + + it 'returns the sorted puppet classes' do + _(answer_file.puppet_classes).must_equal(['class_a', 'class_b', 'class_c', 'class_d']) + end + + it 'returns the parameters for a class' do + _(answer_file.parameters_for_class('class_b')).must_equal({'key' => 'value'}) + end + + it 'returns true for a class with a hash' do + _(answer_file.class_enabled?('class_c')).must_equal(true) + end + + it 'returns true for a class set to true' do + _(answer_file.class_enabled?('class_a')).must_equal(true) + end + + it 'returns false for a class set to false' do + _(answer_file.class_enabled?('class_d')).must_equal(false) + end + end + + describe 'invalid answer file' do + let(:answer_file_path) { 'test/fixtures/answer_files/v1/invalid-answers.yaml' } + + before do + Kafo::KafoConfigure.logger = dummy_logger + end + + it 'exits with invalid_answer_file' do + must_exit_with_code(21) { Kafo::AnswerFile.new(answer_file_path) } + + dummy_logger.rewind + _(dummy_logger.error.read).must_match(%r{Answer file at test/fixtures/answer_files/v1/invalid-answers.yaml has invalid values for class_a, class_b, class_c. Please ensure they are either a hash or true/false.\n}) + end + end + end +end