cem_acpt 0.2.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +30 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +95 -43
- data/README.md +144 -83
- data/cem_acpt.gemspec +12 -7
- data/exe/cem_acpt +41 -7
- data/lib/cem_acpt/config.rb +340 -0
- data/lib/cem_acpt/core_extensions.rb +17 -61
- data/lib/cem_acpt/goss/api/action_response.rb +175 -0
- data/lib/cem_acpt/goss/api.rb +83 -0
- data/lib/cem_acpt/goss.rb +8 -0
- data/lib/cem_acpt/image_name_builder.rb +0 -9
- data/lib/cem_acpt/logging/formatter.rb +97 -0
- data/lib/cem_acpt/logging.rb +168 -142
- data/lib/cem_acpt/platform/base.rb +26 -37
- data/lib/cem_acpt/platform/gcp.rb +48 -62
- data/lib/cem_acpt/platform.rb +30 -28
- data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
- data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
- data/lib/cem_acpt/provision/terraform.rb +193 -0
- data/lib/cem_acpt/provision.rb +20 -0
- data/lib/cem_acpt/puppet_helpers.rb +0 -1
- data/lib/cem_acpt/test_data.rb +23 -13
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
- data/lib/cem_acpt/test_runner.rb +170 -3
- data/lib/cem_acpt/utils/puppet.rb +29 -0
- data/lib/cem_acpt/utils/ssh.rb +197 -0
- data/lib/cem_acpt/utils/terminal.rb +27 -0
- data/lib/cem_acpt/utils.rb +4 -138
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +73 -23
- data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
- data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
- data/lib/terraform/gcp/linux/main.tf +191 -0
- data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
- data/lib/terraform/gcp/windows/.keep +0 -0
- data/sample_config.yaml +22 -21
- metadata +151 -51
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
- data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
- data/lib/cem_acpt/bootstrap.rb +0 -12
- data/lib/cem_acpt/context.rb +0 -153
- data/lib/cem_acpt/platform/base/cmd.rb +0 -71
- data/lib/cem_acpt/platform/gcp/cmd.rb +0 -353
- data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
- data/lib/cem_acpt/platform/vmpooler.rb +0 -24
- data/lib/cem_acpt/rspec_utils.rb +0 -242
- data/lib/cem_acpt/shared_objects.rb +0 -537
- data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
- data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
- data/lib/cem_acpt/test_runner/runner.rb +0 -210
- 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}")
|
data/lib/cem_acpt/test_data.rb
CHANGED
@@ -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 :
|
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
|
-
@
|
29
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
60
|
-
|
61
|
-
raise 'No acceptance tests found' if
|
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 #{
|
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
|
data/lib/cem_acpt/test_runner.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
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
|