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.
- 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 -345
- 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
|