cem_acpt 0.2.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +30 -0
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +95 -43
  5. data/README.md +144 -83
  6. data/cem_acpt.gemspec +12 -7
  7. data/exe/cem_acpt +41 -7
  8. data/lib/cem_acpt/config.rb +340 -0
  9. data/lib/cem_acpt/core_extensions.rb +17 -61
  10. data/lib/cem_acpt/goss/api/action_response.rb +175 -0
  11. data/lib/cem_acpt/goss/api.rb +83 -0
  12. data/lib/cem_acpt/goss.rb +8 -0
  13. data/lib/cem_acpt/image_name_builder.rb +0 -9
  14. data/lib/cem_acpt/logging/formatter.rb +97 -0
  15. data/lib/cem_acpt/logging.rb +168 -142
  16. data/lib/cem_acpt/platform/base.rb +26 -37
  17. data/lib/cem_acpt/platform/gcp.rb +48 -62
  18. data/lib/cem_acpt/platform.rb +30 -28
  19. data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
  20. data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
  21. data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
  22. data/lib/cem_acpt/provision/terraform.rb +193 -0
  23. data/lib/cem_acpt/provision.rb +20 -0
  24. data/lib/cem_acpt/puppet_helpers.rb +0 -1
  25. data/lib/cem_acpt/test_data.rb +23 -13
  26. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
  27. data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
  28. data/lib/cem_acpt/test_runner.rb +170 -3
  29. data/lib/cem_acpt/utils/puppet.rb +29 -0
  30. data/lib/cem_acpt/utils/ssh.rb +197 -0
  31. data/lib/cem_acpt/utils/terminal.rb +27 -0
  32. data/lib/cem_acpt/utils.rb +4 -138
  33. data/lib/cem_acpt/version.rb +1 -1
  34. data/lib/cem_acpt.rb +73 -23
  35. data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
  36. data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
  37. data/lib/terraform/gcp/linux/main.tf +191 -0
  38. data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
  39. data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
  40. data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
  41. data/lib/terraform/gcp/windows/.keep +0 -0
  42. data/sample_config.yaml +22 -21
  43. metadata +151 -51
  44. data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
  45. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
  46. data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
  47. data/lib/cem_acpt/bootstrap.rb +0 -12
  48. data/lib/cem_acpt/context.rb +0 -153
  49. data/lib/cem_acpt/platform/base/cmd.rb +0 -71
  50. data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
  51. data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
  52. data/lib/cem_acpt/platform/vmpooler.rb +0 -24
  53. data/lib/cem_acpt/rspec_utils.rb +0 -242
  54. data/lib/cem_acpt/shared_objects.rb +0 -537
  55. data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
  56. data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
  57. data/lib/cem_acpt/test_runner/runner.rb +0 -210
  58. data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'ruby-terraform'
6
+ require_relative '../logging'
7
+ require_relative 'terraform/linux'
8
+ require_relative 'terraform/windows'
9
+
10
+ module CemAcpt
11
+ module Provision
12
+ class Terraform
13
+ DEFAULT_PLAN_NAME = 'testplan.tfplan'
14
+ include CemAcpt::Logging
15
+
16
+ attr_reader :environment, :working_dir
17
+
18
+ def initialize(config, provision_data)
19
+ @config = config
20
+ @provision_data = provision_data
21
+ @backend = new_backend(@provision_data[:test_data].first[:test_name])
22
+ @environment = new_environment(@config)
23
+ @working_dir = nil
24
+ end
25
+
26
+ def provision
27
+ logger.info('Terraform') { 'Provisioning nodes...' }
28
+ @working_dir = new_working_dir
29
+ validate_working_dir!
30
+
31
+ terraform_configure_logging
32
+ terraform_init
33
+ terraform_plan(formatted_vars, DEFAULT_PLAN_NAME)
34
+ terraform_apply(DEFAULT_PLAN_NAME)
35
+ JSON.parse(terraform_output('instance_name_ip', json: true))
36
+ end
37
+
38
+ def destroy
39
+ terraform_destroy(formatted_vars)
40
+ logger.verbose('Terraform') { "Deleting old working directory #{working_dir}" }
41
+ FileUtils.rm_rf(working_dir)
42
+ @working_dir = nil
43
+ end
44
+
45
+ private
46
+
47
+ def terraform
48
+ @terraform ||= RubyTerraform
49
+ end
50
+
51
+ def terraform_configure_logging
52
+ terraform.configure do |c|
53
+ c.logger = logger
54
+ c.stdout = c.logger
55
+ c.stderr = c.logger
56
+ end
57
+ end
58
+
59
+ def terraform_init
60
+ logger.debug('Terraform') { 'Initializing Terraform' }
61
+ terraform.init({ chdir: working_dir, input: false, no_color: true }, { environment: environment })
62
+ end
63
+
64
+ def terraform_plan(vars, plan_name = DEFAULT_PLAN_NAME)
65
+ logger.debug('Terraform') { "Creating Terraform plan '#{plan_name}'" }
66
+ logger.verbose('Terraform') { "Using vars:\n#{JSON.pretty_generate(vars)}" }
67
+ terraform.plan(
68
+ {
69
+ chdir: working_dir,
70
+ input: false,
71
+ no_color: true,
72
+ plan: plan_name,
73
+ vars: vars,
74
+ },
75
+ {
76
+ environment: environment,
77
+ },
78
+ )
79
+ end
80
+
81
+ def terraform_apply(plan_name = DEFAULT_PLAN_NAME)
82
+ logger.debug('Terraform') { "Running Terraform apply with the plan #{plan_name}" }
83
+ terraform.apply({ chdir: working_dir, input: false, no_color: true, plan: plan_name }, { environment: environment })
84
+ end
85
+
86
+ def terraform_output(name, json: true)
87
+ logger.debug('Terraform') { "Getting Terraform output #{name}" }
88
+ terraform.output({ chdir: working_dir, no_color: true, json: json, name: name }, { environment: environment })
89
+ end
90
+
91
+ def terraform_destroy(vars)
92
+ logger.debug('Terraform') { 'Destroying Terraform resources' }
93
+ terraform.destroy(
94
+ {
95
+ chdir: working_dir,
96
+ auto_approve: true,
97
+ input: false,
98
+ no_color: true,
99
+ vars: vars,
100
+ },
101
+ {
102
+ environment: environment,
103
+ },
104
+ )
105
+ end
106
+
107
+ def new_backend(test_name)
108
+ if CemAcpt::Provision::Linux.use_for?(test_name)
109
+ logger.info('Terraform') { 'Using Linux backend' }
110
+ logger.verbose('Terraform') { "Creating backend with provision_data:\n#{JSON.pretty_generate(@provision_data)}" }
111
+ CemAcpt::Provision::Linux.new(@config, @provision_data)
112
+ elsif CemAcpt::Provision::Windows.use_for?(test_name)
113
+ logger.info('Terraform') { 'Using Windows backend' }
114
+ logger.verbose('Terraform') { "Creating backend with provision_data:\n#{JSON.pretty_generate(@provision_data)}" }
115
+ CemAcpt::Provision::Windows.new(@config, @provision_data)
116
+ else
117
+ err_msg = [
118
+ "Test name #{test_name} does not match any known OS.",
119
+ "Known OSes are: #{CemAcpt::Provision::Linux.valid_names.join(', ')}",
120
+ "and #{CemAcpt::Provision::Windows.valid_names.join(', ')}.",
121
+ "Known versions are: #{CemAcpt::Provision::Linux.valid_versions.join(', ')}",
122
+ ", and #{CemAcpt::Provision::Windows.valid_versions.join(', ')}."
123
+ ].join(' ')
124
+ logger.error('Terraform') { err_msg }
125
+ raise ArgumentError, err_msg
126
+ end
127
+ end
128
+
129
+ def new_environment(config)
130
+ env = (config.get('terraform.environment') || {})
131
+ env['CLOUDSDK_PYTHON_SITEPACKAGES'] = '1' # This is needed for gcloud to use numpy
132
+ logger.verbose('Terraform') { "Using environment:\n#{JSON.pretty_generate(env)}" }
133
+ env
134
+ end
135
+
136
+ def new_working_dir
137
+ logger.debug('Terraform') { "Creating new working directory" }
138
+ base_dir = File.join(@config.get('terraform.dir'), @config.get('platform.name'))
139
+ logger.verbose('Terraform') { "Base directory defined as #{base_dir}" }
140
+ @backend.base_provision_directory = base_dir
141
+ logger.verbose('Terraform') { 'Base directory set in backend' }
142
+ work_dir = File.join(@config.get('terraform.dir'), "test_#{Time.now.to_i.to_s}")
143
+ logger.verbose('Terraform') { "Working directory defined as #{work_dir}" }
144
+ logger.verbose('Terraform') { "Copying backend provision directory #{@backend.provision_directory} to working directory" }
145
+ FileUtils.cp_r(@backend.provision_directory, work_dir)
146
+ logger.verbose('Terraform') { "Copied provision directory #{@backend.provision_directory} to #{work_dir}" }
147
+ FileUtils.cp(@provision_data[:module_package_path], work_dir)
148
+ logger.verbose('Terraform') { "Copied module package #{@provision_data[:module_package_path]} to #{work_dir}" }
149
+ work_dir
150
+ rescue StandardError => e
151
+ logger.error('Terraform') { 'Error creating working directory' }
152
+ raise e
153
+ end
154
+
155
+ def validate_working_dir!
156
+ logger.debug('Terraform') { "Validating working directory #{working_dir}" }
157
+ logger.verbose('Terraform') { "Content of #{working_dir}:\n#{Dir.glob(File.join(working_dir, '*')).join("\n")}" }
158
+ raise "Terraform working directory #{working_dir} does not exist" unless File.directory?(working_dir)
159
+ raise "Terraform working directory #{working_dir} does not contain a Terraform file" unless Dir.glob(File.join(working_dir, '*.tf')).any?
160
+ logger.info('Terraform') { "Using working directory: #{working_dir}" }
161
+ rescue StandardError => e
162
+ logger.error('Terraform') { 'Error validating working directory' }
163
+ raise e
164
+ end
165
+
166
+ def provision_node_data
167
+ node_data = @provision_data[:nodes].each_with_object({}) do |node, h|
168
+ h[node.node_name] = node.node_data.merge({
169
+ goss_file: node.test_data[:goss_file],
170
+ puppet_manifest: node.test_data[:puppet_manifest],
171
+ provision_dir_source: @backend.provision_directory,
172
+ provision_dir_dest: @backend.destination_provision_directory,
173
+ provision_commands: @backend.provision_commands,
174
+ })
175
+ end
176
+ node_data.to_json
177
+ rescue StandardError => e
178
+ logger.error('Terraform') { 'Error creating node data' }
179
+ raise e
180
+ end
181
+
182
+ def formatted_vars
183
+ @provision_data[:nodes].first.platform_data.merge({
184
+ puppet_module_package: @provision_data[:module_package_path],
185
+ node_data: provision_node_data,
186
+ })
187
+ rescue StandardError => e
188
+ logger.error('Terraform') { 'Error creating formatted vars' }
189
+ raise e
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logging'
4
+ require_relative 'provision/terraform'
5
+
6
+ module CemAcpt
7
+ module Provision
8
+ include CemAcpt::Logging
9
+
10
+ def self.new_provisioner(config, provision_data)
11
+ case config.get('provisioner')
12
+ when 'terraform'
13
+ logger.debug('Provision') { 'Using Terraform provisioner' }
14
+ CemAcpt::Provision::Terraform.new(config, provision_data)
15
+ else
16
+ raise ArgumentError, "Unknown provisioner #{config.get('provisioner')}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -28,7 +28,6 @@ module CemAcpt
28
28
 
29
29
  # Validates module metadata by raising exception if invalid
30
30
  _metadata = builder.metadata
31
- logger.debug("Metadata for module #{builder.release_name} is valid")
32
31
 
33
32
  # Builds the module package
34
33
  logger.info("Building module package for #{builder.release_name}")
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
3
4
  require_relative 'core_extensions'
4
5
  require_relative 'logging'
5
6
 
@@ -19,28 +20,38 @@ module CemAcpt
19
20
  include CemAcpt::Logging
20
21
  using CemAcpt::CoreExtensions::ExtendedHash
21
22
 
22
- attr_reader :module_dir, :acceptance_tests
23
+ attr_reader :acpt_test_dir, :acceptance_tests
23
24
 
24
25
  # Initializes a new Fetcher object.
25
26
  # @param config [CemAcpt::Config] the config object
26
27
  def initialize(config)
27
28
  @config = config
28
- @module_dir = config.get('module_dir')
29
- @acceptance_tests = find_acceptance_tests(config.get('module_dir'))
29
+ @acpt_test_dir = Pathname(File.join(@config.get('module_dir'), 'spec', 'acceptance'))
30
+ find_acceptance_tests!
30
31
  end
31
32
 
32
33
  # Extracts, formats, and returns a test data hash.
33
34
  # @return [Array<Hash>] an array of test data hashes
34
35
  def acceptance_test_data
35
36
  logger.info 'Gathering acceptance test data...'
36
- acceptance_tests.each_with_object([]) do |t, a|
37
- logger.debug("Processing #{t}...")
38
- test_name = File.basename(t, '_spec.rb')
37
+ raise "No 'tests' entry found in config" unless @config.has?('tests')
38
+
39
+ @config.get('tests').each_with_object([]) do |test_name, a|
40
+ test_dir = acceptance_tests.find { |f| File.basename(f) == test_name }
41
+ raise "Test directory not found for test #{test_name}" unless test_dir
42
+
43
+ goss_file = File.expand_path(File.join(test_dir, 'goss.yaml'))
44
+ puppet_manifest = File.expand_path(File.join(test_dir, 'manifest.pp'))
45
+ raise "Goss file not found for test #{test_name}" unless File.exist?(goss_file)
46
+ raise "Puppet manifest not found for test #{test_name}" unless File.exist?(puppet_manifest)
47
+
48
+ logger.debug("Complete test directory found for test #{test_name}: #{test_dir}")
39
49
  test_data = {
40
50
  test_name: test_name,
41
- test_file: File.expand_path(t),
51
+ test_dir: File.expand_path(test_dir),
52
+ goss_file: goss_file,
53
+ puppet_manifest: puppet_manifest,
42
54
  }
43
- next unless @config.has?('tests') && @config.get('tests').include?(test_name)
44
55
 
45
56
  process_for_each(test_data).each do |test_data_i|
46
57
  process_static_vars(test_data_i)
@@ -56,12 +67,11 @@ module CemAcpt
56
67
 
57
68
  # Locates acceptance tests in the module directory.
58
69
  # @return [Array<String>] the list of acceptance test paths
59
- def find_acceptance_tests(module_dir)
60
- tests = Dir.glob(File.join(module_dir, 'spec', 'acceptance', '*_spec.rb'))
61
- raise 'No acceptance tests found' if tests.empty?
70
+ def find_acceptance_tests!
71
+ @acceptance_tests = acpt_test_dir.children.select { |f| f.directory? && File.exist?(File.join(f, 'goss.yaml')) }.map(&:to_s)
72
+ raise 'No acceptance tests found' if @acceptance_tests.empty?
62
73
 
63
- logger.info "Found #{tests.size} acceptance tests"
64
- tests
74
+ logger.info "Found #{@acceptance_tests.size} acceptance tests"
65
75
  end
66
76
 
67
77
  # Processes a for_each statement in the test data config.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module TestRunner
5
+ module LogFormatter
6
+ # Formats the results of a Goss action
7
+ class GossActionResponse
8
+ INDENT = ' '
9
+
10
+ def initialize(config, instance_names_ips)
11
+ @config = config
12
+ @instance_names_ips = instance_names_ips
13
+ end
14
+
15
+ def inspect
16
+ to_s
17
+ end
18
+
19
+ def to_s
20
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
21
+ end
22
+
23
+ def summary(response)
24
+ new_summary_message(response)
25
+ end
26
+
27
+ def results(response)
28
+ new_results_message(response.results)
29
+ end
30
+
31
+ def host_name(response)
32
+ name_from_ip(response.host)
33
+ end
34
+
35
+ def test_name(response)
36
+ test_from_ip(response.host)
37
+ end
38
+
39
+ private
40
+
41
+ def normalize_whitespace(str)
42
+ raise ArgumentError, 'str must be a String' unless str.is_a?(String)
43
+
44
+ str.gsub(%r{(\n|\r|\t)}, ' ').gsub(%r{\s{2,}}, ' ').strip
45
+ end
46
+
47
+ def success_str(success)
48
+ success ? 'passed' : 'failed'
49
+ end
50
+
51
+ def name_from_ip(ip)
52
+ @instance_names_ips.each do |name, val|
53
+ return name if val['ip'] == ip
54
+ end
55
+ ip
56
+ end
57
+
58
+ def test_from_ip(ip)
59
+ @instance_names_ips.each do |_name, val|
60
+ return val['test_name'] if val['ip'] == ip
61
+ end
62
+ '<unknown>'
63
+ end
64
+
65
+ def new_summary_message(response)
66
+ msg = [
67
+ "SUMMARY: #{success_str(response.success?).capitalize}: Test #{test_from_ip(response.host)}:",
68
+ "#{normalize_whitespace(response.summary.summary_line)}:",
69
+ "Action '#{response.action}' on host #{name_from_ip(response.host)}",
70
+ ].join(' ')
71
+ return msg unless @config.debug?
72
+
73
+ [
74
+ msg,
75
+ "HTTP Status: #{response.http_status}",
76
+ "Failed percentage: #{response.summary.failed_percentage}",
77
+ ].join("\n#{INDENT}")
78
+ end
79
+
80
+ def new_results_message(results)
81
+ results.map { |r| new_result_message(r) }
82
+ end
83
+
84
+ def new_result_message(result)
85
+ return "Error: #{result.error}" unless result.err.nil?
86
+
87
+ status = result.skipped? ? 'Skipped' : success_str(result.success?).capitalize
88
+ msg = [
89
+ "#{status}:",
90
+ normalize_whitespace(result.summary_line),
91
+ ]
92
+ return msg.join(' ') unless @config.debug?
93
+
94
+ [
95
+ msg.join(' '),
96
+ "Duration: #{result.duration.to_s}",
97
+ "Expected: #{result.expected}",
98
+ "Found: #{result.found}",
99
+ ].join("\n#{INDENT}")
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'log_formatter/goss_action_response'
4
+
5
+ module CemAcpt
6
+ module TestRunner
7
+ # Holds classes for formatting test runner results
8
+ module LogFormatter; end
9
+ end
10
+ end
@@ -1,10 +1,177 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'core_extensions'
4
+ require_relative 'goss'
5
+ require_relative 'logging'
6
+ require_relative 'platform'
7
+ require_relative 'provision'
8
+ require_relative 'test_data'
9
+ require_relative 'utils'
10
+ require_relative 'version'
11
+ require_relative 'test_runner/log_formatter'
12
+
3
13
  module CemAcpt
4
14
  # Namespace for all Runner-related classes and modules
5
15
  module TestRunner
6
- require_relative 'test_runner/run_handler'
7
- require_relative 'test_runner/runner'
8
- require_relative 'test_runner/runner_result'
16
+ # Holds all the Runner related code
17
+ class Runner
18
+ include CemAcpt::Logging
19
+
20
+ attr_reader :duration, :exit_code
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ @run_data = {}
25
+ @duration = 0
26
+ @exit_code = 0
27
+ @results = nil
28
+ @http_statuses = []
29
+ end
30
+
31
+ def inspect
32
+ to_s
33
+ end
34
+
35
+ def to_s
36
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
37
+ end
38
+
39
+ def run
40
+ @run_data = {}
41
+ @start_time = Time.now
42
+ logger.info('CemAcpt') { "Starting CemAcpt v#{CemAcpt::VERSION}..." }
43
+ logger.info('CemAcpt') { "Test suite started at #{@start_time}..." }
44
+ logger.info('CemAcpt') { "Using module directory: #{config.get('module_dir')}..." }
45
+ Dir.chdir(config.get('module_dir')) do
46
+ keep_terminal_alive
47
+ @run_data[:priv_key], @run_data[:pub_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
48
+ logger.info('CemAcpt') { 'Created ephemeral SSH key pair...' }
49
+ @run_data[:module_package_path] = build_module_package
50
+ logger.info('CemAcpt') { "Created module package: #{@run_data[:module_package_path]}..." }
51
+ @run_data[:test_data] = new_test_data
52
+ logger.info('CemAcpt') { 'Created test data...' }
53
+ logger.verbose('CemAcpt') { "Test data: #{@run_data[:test_data]}" }
54
+ @run_data[:nodes] = new_node_data
55
+ logger.info('CemAcpt') { 'Created node data...' }
56
+ logger.verbose('CemAcpt') { "Node data: #{@run_data[:nodes]}" }
57
+ @instance_names_ips = provision_test_nodes
58
+ logger.info('CemAcpt') { 'Provisioned test nodes...' }
59
+ logger.debug('CemAcpt') { "Instance names and IPs: #{@instance_names_ips}" }
60
+ @results = run_tests(@instance_names_ips.map { |_, v| v['ip'] },
61
+ config.get('actions.only'),
62
+ config.get('actions.except'))
63
+ end
64
+ ensure
65
+ clean_up
66
+ process_test_results
67
+ end
68
+
69
+ def clean_up(trap_context = false)
70
+ unless trap_context
71
+ kill_keep_terminal_alive
72
+ end
73
+ clean_ephemeral_ssh_keys
74
+ destroy_test_nodes
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :config
80
+
81
+ # @return [Thread] The thread that keeps the terminal alive
82
+ def keep_terminal_alive
83
+ return unless config.ci?
84
+
85
+ @keep_terminal_alive ||= CemAcpt::Utils::Terminal.keep_terminal_alive
86
+ end
87
+
88
+ def kill_keep_terminal_alive
89
+ return if @trap_context
90
+
91
+ keep_terminal_alive&.kill
92
+ end
93
+
94
+ # @return [String] The path to the module package
95
+ def build_module_package
96
+ CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
97
+ end
98
+
99
+ # @return [Array<String>] The paths to the ssh private key, public key, and known hosts file
100
+ def new_ephemeral_ssh_keys
101
+ return [nil, nil, nil] if config.get('no_ephemeral_ssh_key')
102
+
103
+ CemAcpt::Utils::SSH::Ephemeral.create
104
+ end
105
+
106
+ def clean_ephemeral_ssh_keys
107
+ return if config.get('no_ephemeral_ssh_key')
108
+
109
+ CemAcpt::Utils::SSH::Ephemeral.clean
110
+ end
111
+
112
+ def new_test_data
113
+ CemAcpt::TestData.acceptance_test_data(config)
114
+ end
115
+
116
+ def new_node_data
117
+ CemAcpt::Platform.use(config.get('platform.name'), config, @run_data[:test_data])
118
+ end
119
+
120
+ def provision_test_nodes
121
+ @provisioner = CemAcpt::Provision.new_provisioner(config, @run_data)
122
+ @provisioner.provision
123
+ end
124
+
125
+ def destroy_test_nodes
126
+ @provisioner&.destroy
127
+ end
128
+
129
+ def run_tests(hosts, only_actions, except_actions)
130
+ only_actions = [] if only_actions.nil?
131
+ except_actions = [] if except_actions.nil?
132
+ CemAcpt::Goss::Api.run_actions_async(hosts, only: only_actions, except: except_actions)
133
+ end
134
+
135
+ def result_log_formatter
136
+ @result_log_formatter ||= LogFormatter::GossActionResponse.new(config, @instance_names_ips)
137
+ end
138
+
139
+ def process_test_results
140
+ if @results.nil?
141
+ logger.error('CemAcpt') { 'No test results to process' }
142
+ @exit_code = 1
143
+ else
144
+ until @results.empty?
145
+ result = @results.pop
146
+ @http_statuses << result.http_status
147
+ log_test_result(result)
148
+ end
149
+ if @http_statuses.empty?
150
+ logger.error('CemAcpt') { 'No test results to process' }
151
+ @exit_code = 1
152
+ else
153
+ @exit_code = @http_statuses.any? { |s| s.to_i != 200 } ? 1 : 0
154
+ end
155
+ end
156
+ @duration = Time.now - @start_time
157
+ logger.info('CemAcpt') { "Test suite finished after ~#{duration.round} seconds." }
158
+ end
159
+
160
+ def log_test_result(result)
161
+ logger.with_ci_group("Test results for #{result_log_formatter.test_name(result)}") do
162
+ logger.info(result_log_formatter.summary(result))
163
+ formatted_results = result_log_formatter.results(result)
164
+ formatted_results.each do |r|
165
+ if r.start_with?('Passed:')
166
+ logger.verbose { r }
167
+ elsif r.start_with?('Skipped:')
168
+ logger.info { r }
169
+ else
170
+ logger.error { r }
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
9
176
  end
10
177
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet/modulebuilder'
4
+ require 'fileutils'
5
+
6
+ module CemAcpt
7
+ module Utils
8
+ # Puppet-related utilities
9
+ module Puppet
10
+ # Builds a Puppet module package.
11
+ # @param module_dir [String] Path to the module directory. If target_dir
12
+ # is specified as a relative path, it will be relative to the module dir.
13
+ # @param target_dir [String] Path to the target directory where the package
14
+ # will be built. This defaults to the relative path 'pkg/'.
15
+ # @param should_log [Boolean] Whether or not to log the build process.
16
+ # @return [String] Path to the built package.
17
+ def self.build_module_package(module_dir, target_dir = nil, should_log: false)
18
+ builder_logger = should_log ? logger : nil
19
+ builder = ::Puppet::Modulebuilder::Builder.new(::File.expand_path(module_dir), target_dir, builder_logger)
20
+
21
+ # Validates module metadata by raising exception if invalid
22
+ _metadata = builder.metadata
23
+
24
+ # Builds the module package
25
+ builder.build
26
+ end
27
+ end
28
+ end
29
+ end