cem_acpt 0.2.6-universal-java-17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/CODEOWNERS +1 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +93 -0
  7. data/README.md +150 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/cem_acpt.gemspec +39 -0
  12. data/exe/cem_acpt +84 -0
  13. data/lib/cem_acpt/bootstrap/bootstrapper.rb +206 -0
  14. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +129 -0
  15. data/lib/cem_acpt/bootstrap/operating_system.rb +17 -0
  16. data/lib/cem_acpt/bootstrap.rb +12 -0
  17. data/lib/cem_acpt/context.rb +153 -0
  18. data/lib/cem_acpt/core_extensions.rb +108 -0
  19. data/lib/cem_acpt/image_name_builder.rb +104 -0
  20. data/lib/cem_acpt/logging.rb +351 -0
  21. data/lib/cem_acpt/platform/base/cmd.rb +71 -0
  22. data/lib/cem_acpt/platform/base.rb +78 -0
  23. data/lib/cem_acpt/platform/gcp/cmd.rb +345 -0
  24. data/lib/cem_acpt/platform/gcp/compute.rb +332 -0
  25. data/lib/cem_acpt/platform/gcp.rb +85 -0
  26. data/lib/cem_acpt/platform/vmpooler.rb +24 -0
  27. data/lib/cem_acpt/platform.rb +103 -0
  28. data/lib/cem_acpt/puppet_helpers.rb +39 -0
  29. data/lib/cem_acpt/rspec_utils.rb +242 -0
  30. data/lib/cem_acpt/shared_objects.rb +537 -0
  31. data/lib/cem_acpt/spec_helper_acceptance.rb +184 -0
  32. data/lib/cem_acpt/test_data.rb +146 -0
  33. data/lib/cem_acpt/test_runner/run_handler.rb +187 -0
  34. data/lib/cem_acpt/test_runner/runner.rb +210 -0
  35. data/lib/cem_acpt/test_runner/runner_result.rb +103 -0
  36. data/lib/cem_acpt/test_runner.rb +10 -0
  37. data/lib/cem_acpt/utils.rb +144 -0
  38. data/lib/cem_acpt/version.rb +5 -0
  39. data/lib/cem_acpt.rb +34 -0
  40. data/sample_config.yaml +58 -0
  41. metadata +218 -0
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ require_relative 'shared_objects'
5
+
6
+ # This methods is used in spec_helper_acceptance.rb to set up
7
+ # baseline configurations used in acceptance tests.
8
+ def self.configure_spec_helper!
9
+ require 'serverspec'
10
+ require 'yaml'
11
+
12
+ RSpec.configure do |config|
13
+ config.include CemAcpt::SpecHelperAcceptance
14
+ config.extend CemAcpt::SpecHelperAcceptance
15
+ config.add_setting :acpt_test_data, default: {}
16
+ config.add_setting :acpt_node_inventory
17
+ config.threadsafe = true
18
+ config.color_mode = :off
19
+ config.fail_fast = false
20
+ end
21
+
22
+ node_inventory = NodeInventory.new
23
+ node_inventory.load
24
+ RSpec.configuration.acpt_node_inventory = node_inventory
25
+ end
26
+
27
+ # This module provides methods used in accpetance tests.
28
+ module SpecHelperAcceptance
29
+ # This method must be used inside of a `describe` block as the first
30
+ # statement. This prepares the ServerSpec configuration for the test node
31
+ # and sets up the test data.
32
+ def initialize_test_environment!
33
+ test_file = caller_locations.first.path
34
+
35
+ node_name, node_data = RSpec.configuration.acpt_node_inventory.get_by_property('test_data.test_file', test_file)
36
+ raise "Failed to get node data for node #{node_name}" unless node_data
37
+ raise "Node data format is incorrect: #{node_data}" unless node_data.is_a?(Hash)
38
+
39
+ backend = nil
40
+ host = nil
41
+ ssh_options = nil
42
+ puppet_path = nil
43
+
44
+ # Set remote communication variables based on transport type
45
+ case node_data[:transport]
46
+ when /ssh/
47
+ backend = :ssh
48
+ host = node_data[:node_name]
49
+ ssh_options = node_data[:ssh_opts]
50
+ sudo_options = '-n -u root -i'
51
+ puppet_path = '/opt/puppetlabs/bin/puppet'
52
+ when /winrm/
53
+ backend = :winrm
54
+ puppet_path = 'C:\Program Files\Puppet Labs\Puppet\bin\puppet.bat'
55
+ else
56
+ raise "Unknown transport: #{node[:transport]}"
57
+ end
58
+
59
+ # Set serverspec transport options and host for remote communication.
60
+ set :backend, backend
61
+ set :host, host
62
+ set(:ssh_options, ssh_options) if ssh_options
63
+ set(:sudo_options, sudo_options) if sudo_options
64
+ set(:os, family: 'windows') if backend == :winrm
65
+
66
+ # Get the command provider from the node's platform
67
+ # We add this as a RSpec config option so that we can use it in
68
+ # other functions.
69
+ require 'cem_acpt/platform'
70
+
71
+ acpt_test_data = {
72
+ test_file: test_file,
73
+ node_name: node_name,
74
+ node_data: node_data,
75
+ backend: backend,
76
+ platform: CemAcpt::Platform.get(node_data[:platform]),
77
+ puppet_path: puppet_path,
78
+ }
79
+ acpt_test_data[:host] = host if host
80
+ RSpec.configuration.acpt_test_data = acpt_test_data
81
+ end
82
+
83
+ # This method formats Puppet Apply options
84
+ def puppet_apply_options(opts = {})
85
+ if [opts[:catch_changes], opts[:expect_changes], opts[:catch_failures], opts[:expect_failures]].compact.length > 1
86
+ raise ArgumentError,
87
+ 'Please specify only one of "catch_changes", "expect_changes", "catch_failures", or "expect_failures"'
88
+ end
89
+
90
+ apply_opts = {}.merge(opts)
91
+
92
+ if opts[:catch_changes]
93
+ apply_opts[:detailed_exit_codes] = true
94
+ apply_opts[:acceptable_exit_codes] = [0]
95
+ elsif opts[:catch_failures]
96
+ apply_opts[:detailed_exit_codes] = true
97
+ apply_opts[:acceptable_exit_codes] = [0, 2]
98
+ elsif opts[:expect_failures]
99
+ apply_opts[:detailed_exit_codes] = true
100
+ apply_opts[:acceptable_exit_codes] = [1, 4, 6]
101
+ elsif opts[:expect_changes]
102
+ apply_opts[:detailed_exit_codes] = true
103
+ apply_opts[:acceptable_exit_codes] = [2]
104
+ else
105
+ apply_opts[:detailed_exit_codes] = false
106
+ apply_opts[:acceptable_exit_codes] = [0]
107
+ end
108
+ apply_opts
109
+ end
110
+
111
+ # This methods handles formatting the output from Puppet apply.
112
+ def handle_puppet_apply_output(result, apply_opts)
113
+ exit_code = result.exitstatus
114
+ output = result.to_s
115
+ if apply_opts[:catch_changes] && !apply_opts[:acceptable_exit_codes].include?(exit_code)
116
+ failure = <<~ERROR
117
+ Apply manifest expected no changes. Puppet Apply returned exit code #{exit_code}
118
+ ====== Start output of Puppet apply with unexpected changes ======
119
+ #{output}
120
+ ====== End output of Puppet apply with unexpected changes ======
121
+ ERROR
122
+ raise failure
123
+ elsif !apply_opts[:acceptable_exit_codes].include?(exit_code)
124
+ failure = <<~ERROR
125
+ Apply manifest failed with exit code #{exit_code} (expected: #{apply_opts[:acceptable_exit_codes]})
126
+ ====== Start output of failed Puppet apply ======
127
+ #{output}
128
+ ====== End output of failed Puppet apply ======
129
+ ERROR
130
+ raise failure
131
+ end
132
+
133
+ yield result if block_given?
134
+
135
+ if ENV['RSPEC_DEBUG']
136
+ run_result = <<~RUNRES
137
+ apply manifest succeded with status #{exit_code}
138
+ ===== Start output of successful Puppet apply ======
139
+ #{output}
140
+ ===== End output of successful Puppet apply ======
141
+ RUNRES
142
+ puts run_result
143
+ end
144
+ result
145
+ end
146
+
147
+ # This method runs a shell command on the test node.
148
+ def run_shell(cmd, opts = {})
149
+ cmd = cmd.join(' ') if cmd.is_a?(Array)
150
+
151
+ host = RSpec.configuration.acpt_test_data[:node_name]
152
+ opts[:ssh_opts] = RSpec.configuration.acpt_test_data[:node_data][:ssh_opts]
153
+ RSpec.configuration.acpt_test_data[:platform].run_shell(host, cmd, opts)
154
+ end
155
+
156
+ # This method runs puppet apply on the test node using the provided manifest.
157
+ def apply_manifest(manifest, opts = {})
158
+ host = RSpec.configuration.acpt_test_data[:node_name]
159
+ opts = {
160
+ apply: puppet_apply_options(opts),
161
+ ssh_opts: RSpec.configuration.acpt_test_data[:node_data][:ssh_opts],
162
+ puppet_path: RSpec.configuration.acpt_test_data[:puppet_path],
163
+ }
164
+ result = RSpec.configuration.acpt_test_data[:platform].apply_manifest(host, manifest, opts)
165
+ handle_puppet_apply_output(result, opts[:apply])
166
+ end
167
+
168
+ # This method runs puppet apply on the test node using the provided manifest twice and
169
+ # asserts that the second run has no changes.
170
+ def idempotent_apply(manifest, opts = {})
171
+ opts.reject! { |k, _| %i[catch_changes expect_changes catch_failures expect_failures].include?(k) }
172
+ begin
173
+ apply_manifest(manifest, opts.merge({ catch_failures: true }))
174
+ rescue StandardError => e
175
+ raise "Idempotent apply failed during first apply: #{e.message}\n#{e.backtrace.join("\n")}"
176
+ end
177
+ begin
178
+ apply_manifest(manifest, opts.merge({ catch_changes: true }))
179
+ rescue StandardError => e
180
+ raise "Idempotent apply failed during second apply: #{e.message}\n#{e.backtrace.join("\n")}"
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core_extensions'
4
+ require_relative 'logging'
5
+
6
+ module CemAcpt
7
+ # This module provides a method and class for extracting and formatting
8
+ # test data.
9
+ module TestData
10
+ # Returns a hash of test data.
11
+ # @param config [CemAcpt::Config] the config object
12
+ # @return [Array<Hash>] an array of test data hashes
13
+ def self.acceptance_test_data(config)
14
+ Fetcher.new(config).acceptance_test_data
15
+ end
16
+
17
+ # Fetcher provides the methods for extracting and formatting test data.
18
+ class Fetcher
19
+ include CemAcpt::Logging
20
+ using CemAcpt::CoreExtensions::ExtendedHash
21
+
22
+ attr_reader :module_dir, :acceptance_tests
23
+
24
+ # Initializes a new Fetcher object.
25
+ # @param config [CemAcpt::Config] the config object
26
+ def initialize(config)
27
+ @config = config
28
+ @module_dir = config.get('module_dir')
29
+ @acceptance_tests = find_acceptance_tests(config.get('module_dir'))
30
+ end
31
+
32
+ # Extracts, formats, and returns a test data hash.
33
+ # @return [Array<Hash>] an array of test data hashes
34
+ def acceptance_test_data
35
+ 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')
39
+ test_data = {
40
+ test_name: test_name,
41
+ test_file: File.expand_path(t),
42
+ }
43
+ next unless @config.has?('tests') && @config.get('tests').include?(test_name)
44
+
45
+ process_for_each(test_data).each do |test_data_i|
46
+ process_static_vars(test_data_i)
47
+ process_name_pattern_vars(test_name, test_data_i)
48
+ vars_post_processing!(test_data_i)
49
+ test_data_i.format!
50
+ a << test_data_i
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # Locates acceptance tests in the module directory.
58
+ # @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?
62
+
63
+ logger.info "Found #{tests.size} acceptance tests"
64
+ tests
65
+ end
66
+
67
+ # Processes a for_each statement in the test data config.
68
+ # @param test_data [Hash] the test data hash
69
+ # @return [Array<Hash>] the list of test data hashes
70
+ def process_for_each(test_data)
71
+ return [test_data] unless @config.has?('test_data.for_each')
72
+
73
+ @config.get('test_data.for_each').each_with_object([]) do |(k, v), a|
74
+ v.each do |v_i|
75
+ test_data_i = test_data.dup
76
+ test_data_i[k] = v_i
77
+ a << test_data_i
78
+ end
79
+ end
80
+ end
81
+
82
+ def process_static_vars(test_data)
83
+ return unless @config.has?('test_data.vars')
84
+
85
+ vars = @config.get('test_data.vars').each_with_object({}) do |(k, v), h|
86
+ logger.debug("Static variable: #{k}")
87
+ h[k] = v
88
+ end
89
+ test_data.merge!(vars)
90
+ test_data.format!
91
+ end
92
+
93
+ def process_name_pattern_vars(test_name, test_data)
94
+ name_pattern_vars = name_pattern_matches(test_name) || {}
95
+ test_data.merge!(name_pattern_vars)
96
+ test_data.format!
97
+ end
98
+
99
+ def name_pattern_matches(test_name)
100
+ return test_name unless @config.has?('test_data.name_pattern_vars')
101
+
102
+ pattern = @config.get('test_data.name_pattern_vars')
103
+ pattern_matches = test_name.match(pattern)&.named_captures
104
+ unless pattern_matches
105
+ logger.error("Test name #{test_name} does not match pattern #{pattern.inspect}")
106
+ return
107
+ end
108
+ pattern_matches
109
+ end
110
+
111
+ def vars_post_processing!(test_data)
112
+ return unless @config.has?('test_data.vars_post_processing')
113
+
114
+ post_processing_new_vars!(test_data)
115
+ post_processing_delete_vars!(test_data)
116
+ end
117
+
118
+ def post_processing_new_vars!(test_data)
119
+ new_vars = @config.get('test_data.vars_post_processing.new_vars')
120
+ return unless new_vars
121
+
122
+ new_vars.each do |var|
123
+ if var.has?('string_split')
124
+ test_data[var.dot_dig('name')] = op_string_split(test_data, var)
125
+ else
126
+ logger.error("Unknown post processing operation for new var #{var}")
127
+ end
128
+ end
129
+ end
130
+
131
+ def op_string_split(test_data, var)
132
+ from_var = var.dot_dig('string_split.from')
133
+ from = test_data.dot_dig(from_var)
134
+ parts = from.split(var.dot_dig('string_split.using'))
135
+ var.has?('string_split.part') ? parts[var.dot_dig('string_split.part')] : parts
136
+ end
137
+
138
+ def post_processing_delete_vars!(test_data)
139
+ delete_vars = @config.get('test_data.vars_post_processing.delete_vars')
140
+ return unless delete_vars
141
+
142
+ test_data.reject! { |k, _| delete_vars.include?(k.to_s) }
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+ require 'open3'
5
+ require_relative '../context'
6
+ require_relative '../logging'
7
+ require_relative '../puppet_helpers'
8
+ require_relative 'runner'
9
+
10
+ module CemAcpt
11
+ module TestRunner
12
+ # Raised when the RunHandler needs to be operating within the module directory and isn't
13
+ class RunHandlerNotInModuleDirError < StandardError; end
14
+
15
+ # Raised when the RunHandler executes a system command that fails
16
+ class RunHandlerSystemCommandError < StandardError; end
17
+
18
+ # RunHandler orchestrates the acceptance test suites, including
19
+ # creating Runner objects, handling input and output, and exception
20
+ # handling.
21
+ class RunHandler
22
+ include CemAcpt::Logging
23
+
24
+ attr_accessor :run_context
25
+
26
+ # @param run_context [CemAcpt::Context::Ctx] The context object for this run
27
+ def initialize(run_context)
28
+ @run_context = run_context
29
+ @module_pkg_path = Concurrent::IVar.new
30
+ @runners = Concurrent::Array.new
31
+ @thread_pool = Concurrent::SimpleExecutorService.new
32
+ end
33
+
34
+ # Gets the overall exit code for all runners
35
+ def exit_code
36
+ @runners&.map { |r| r.run_result.exit_status }&.all?(&:zero?) ? 0 : 1
37
+ end
38
+
39
+ # Runs the acceptance test suites.
40
+ def run
41
+ logger.info('RUN HANDLER: Creating and starting test runners...')
42
+ create_runners
43
+ logger.info("RUN HANDLER: Created #{@runners.length} runners...")
44
+ @runners.map { |r| @thread_pool.post { r.start } }
45
+ @thread_pool.shutdown
46
+ @thread_pool.wait_for_termination
47
+ @thread_pool = Concurrent::SimpleExecutorService.new
48
+ logger.info('Test runners have all exited...')
49
+ rescue StandardError => e
50
+ handle_fatal_error(e)
51
+ ensure
52
+ handle_test_results
53
+ end
54
+
55
+ def destroy_test_nodes
56
+ return if @runners.empty? || @runners.none?(&:node_exists)
57
+
58
+ @runners.each do |r|
59
+ r.send(:destroy) if r.node_exists
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Creates and starts Runner objects for each node in the acceptance test suites.
66
+ def create_runners
67
+ @run_context.nodes.each do |platform, nodes|
68
+ @runners += nodes.map { |node| create_runner(node, platform) }.compact
69
+ end
70
+ end
71
+
72
+ def create_runner(node, platform)
73
+ logger.info("RUN HANDLER: Creating runner for #{node.test_data[:test_name]} on node #{node.node_name}...")
74
+ runner = CemAcpt::TestRunner::Runner.new(node, @run_context, platform)
75
+ runner || runner_creation_error(node)
76
+ end
77
+
78
+ def runner_creation_error(node)
79
+ msg = [
80
+ "Failed to create runner object for node #{node.node_name}.",
81
+ "Cannot run test #{node.test_data[:test_name]}",
82
+ ].join(' ')
83
+ logger.error(msg)
84
+ nil
85
+ end
86
+
87
+ # Handles how test results are logged.
88
+ def handle_test_results
89
+ @runners.each do |runner|
90
+ result = runner.run_result.to_h
91
+ logger << "::group::{Results for #{runner.node.test_data[:test_name]}}\n"
92
+ if result.key?('summary_line')
93
+ logger.info("SUMMARY: #{result['summary_line']} for test #{runner.node.test_data[:test_name]}")
94
+ else
95
+ handle_runner_error_results(runner)
96
+ logger << "::endgroup::\n"
97
+ next
98
+ end
99
+ unless runner.test_failures?
100
+ logger << "::endgroup::\n"
101
+ next
102
+ end
103
+
104
+ if result.key?('examples') # Log errors outside of examples
105
+ if result['examples'].empty? && !result['messages'].empty?
106
+ logger.error(result['messages'].join("\n"))
107
+ else
108
+ failed = result['examples'].reject { |e| e['status'] == 'passed' }
109
+ failed.each do |e|
110
+ logger.error(test_error_msg(runner.node.node_name, e))
111
+ end
112
+ end
113
+ else
114
+ handle_runner_error_results(runner)
115
+ end
116
+ #debug_test_results(runner) if logger.debug?
117
+ logger << "::endgroup::\n"
118
+ end
119
+ end
120
+
121
+ # Handles logging the results of the runners that errored.
122
+ def handle_runner_error_results(runner)
123
+ logger.error("SUMMARY: Encountered an error with test #{runner.node.test_data[:test_name]} on node #{runner.node.node_name}")
124
+ runner.run_result.to_h.each do |k, v|
125
+ logger.error("#{k.upcase}: #{v}")
126
+ end
127
+ end
128
+
129
+ # Formats the error message for a runner that failed.
130
+ # @param node [Object] the node that failed
131
+ # @param err [Object] the error that occured
132
+ # @return [String] the formatted error message
133
+ def runner_error_msg(node, err)
134
+ [
135
+ "Error while running acceptance tests on node #{node.node_name}",
136
+ "Error: #{err.message}",
137
+ 'Backtrace:',
138
+ err.backtrace.join("\n"),
139
+ ].join("\n")
140
+ end
141
+
142
+ # Formats a test result for tests that have failed. Is used for logging.
143
+ # @param node [String] the name of the node the test ran on
144
+ # @param result [Hash] the test result to format
145
+ # @return [String] the formatted test result
146
+ def test_error_msg(node, result)
147
+ [
148
+ "TEST FAILED: #{result['id']}",
149
+ "DESCRIPTION: #{result['full_description']}",
150
+ "STATUS: #{result['status']}",
151
+ "LOCATION: #{result['file_path']}:#{result['line_number']}",
152
+ "NODE: #{node}",
153
+ result['exception']['message'],
154
+ "\n",
155
+ ].join("\n")
156
+ end
157
+
158
+ # Logs performance data for the acceptance test suites.
159
+ # This is only logged if the log level is set to debug.
160
+ # def debug_test_results(runner)
161
+ # examples_by_time = []
162
+ # runner.run_result.to_h.each do |node, result|
163
+ # next unless result['examples']
164
+
165
+ # result['examples'].each do |e|
166
+ # examples_by_time << [e['run_time'], e['id'], e['status'], e['line_number'], node]
167
+ # end
168
+ # end
169
+ # if examples_by_time
170
+ # logger.debug('Showing test results in order of execution time...')
171
+ # examples_by_time.sort_by(&:first).reverse.each do |e|
172
+ # logger.debug("RUNTIME: #{e[0]}; ID: #{e[1]}; STATUS: #{e[2]}; LINE: #{e[3]}; HOST: #{e[4]};")
173
+ # end
174
+ # else
175
+ # logger.debug('No debug results to show')
176
+ # end
177
+ # end
178
+
179
+ # Gracefully handles a fatal error and exits the program.
180
+ # @param err [StandardError, Exception] the error that caused the fatal error
181
+ def handle_fatal_error(err)
182
+ logger.fatal("RUN HANDLER: Fatal error: #{err.message}")
183
+ logger.debug(err.backtrace.join("\n"))
184
+ end
185
+ end
186
+ end
187
+ end