From e4000f627d87df89cb0cde86870b2876a40db163 Mon Sep 17 00:00:00 2001 From: "Eric D. Helms" Date: Mon, 19 Jul 2021 08:31:14 -0400 Subject: [PATCH] Introduce a class to represent the answer file Refactors code into an AnswerFile class for handling the loading of and details of the parts of the answer file. This abstraction will allow easier introduction of newer versions of the answer file. --- README.md | 26 +++++++ lib/kafo/answer_file.rb | 68 +++++++++++++++++++ lib/kafo/configuration.rb | 39 ++++------- lib/kafo/scenario_option.rb | 3 + .../answer_files/v1/basic-answers.yaml | 6 ++ .../answer_files/v1/invalid-answers.yaml | 4 ++ test/kafo/answer_file_test.rb | 47 +++++++++++++ 7 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 lib/kafo/answer_file.rb create mode 100644 test/fixtures/answer_files/v1/basic-answers.yaml create mode 100644 test/fixtures/answer_files/v1/invalid-answers.yaml create mode 100644 test/kafo/answer_file_test.rb 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..a6996763 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 @@ -43,6 +44,7 @@ class Configuration ScenarioOption::KAFO_MODULES_DIR => nil, ScenarioOption::CONFIG_HEADER_FILE => nil, ScenarioOption::DONT_SAVE_ANSWERS => nil, + ScenarioOption::ANSWER_FILE_VERSION => 1, } def self.get_scenario_id(filename) @@ -55,13 +57,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) @@ -95,7 +92,7 @@ def configure_application def app @app ||= begin begin - configuration = load_yaml_file(@config_file) + configuration = YAML.load_file(@config_file) rescue configuration = {} end @@ -130,7 +127,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 +228,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 +243,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 +311,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 @@ -376,10 +367,6 @@ def format(data) data.gsub('!ruby/sym ', ':') end - def load_yaml_file(filename) - YAML.load_file(filename) - end - # Loads YAML from mixed output, finding the "---" and "..." document start/end delimiters def load_yaml_from_output(lines) start = lines.find_index { |l| l.start_with?('---') } diff --git a/lib/kafo/scenario_option.rb b/lib/kafo/scenario_option.rb index b399d683..f34af176 100644 --- a/lib/kafo/scenario_option.rb +++ b/lib/kafo/scenario_option.rb @@ -12,6 +12,9 @@ class ScenarioOption # Path to answer file, if the file does not exist a $pwd/config/answers.yaml is used as a fallback ANSWER_FILE = :answer_file + # The version of the answer file schema being used + ANSWER_FILE_VERSION = :answer_file_version + # Enable colors? If you don't touch this, we'll autodetect terminal capabilities COLORS = :colors # Color scheme, we support :bright and :dark (first is better for white background, dark for black background) 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