Skip to content

Commit

Permalink
Introduce a class to represent the answer file
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ehelms committed Jul 21, 2021
1 parent 35a7034 commit e4000f6
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 26 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions lib/kafo/answer_file.rb
Original file line number Diff line number Diff line change
@@ -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
39 changes: 13 additions & 26 deletions lib/kafo/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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))) }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?('---') }
Expand Down
3 changes: 3 additions & 0 deletions lib/kafo/scenario_option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/answer_files/v1/basic-answers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
class_a: true
class_b:
key: value
class_c: {}
class_d: false
4 changes: 4 additions & 0 deletions test/fixtures/answer_files/v1/invalid-answers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
class_a: 'true'
class_b:
class_c: 1
47 changes: 47 additions & 0 deletions test/kafo/answer_file_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e4000f6

Please sign in to comment.