cem_acpt 0.2.8-universal-java-17 → 0.2.11-universal-java-17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf596ee67704455f907b1d68959a733bf938051108534dad444942de893e3cf
4
- data.tar.gz: 9d47ed4ca9975d124d2550e51bcf00c945ad5f10b21c8fb8205f27365bd7f76b
3
+ metadata.gz: c97a0886b1604384a7e03f5edec960b594842f69502e5b1fcd4d2a6a18ecb3fc
4
+ data.tar.gz: 8ad5f879c25f70d0050ec0f7fd24891339b7a80271193d2549e8a6a7224d7727
5
5
  SHA512:
6
- metadata.gz: 16bdac45fb5384f85863967f64fa927a5bd610b483d6eab4f87cf45c55117cbe7fda818259f8b42fda621d9118bb46b8099fa7c7f5cd52a12d567691f729cc39
7
- data.tar.gz: a3957f255f0643f6102df4c32628d21ebfe6f51d7cba25a122be80b48e5288a2569830f1a0995298db082a4bb79e64570af9de1617c3e2ed4f812a83c33f8a02
6
+ metadata.gz: 2279fa0e8ab932d0f02982cf8cacf6aaf4bf8c0a5e709eeb7e3cd0d093a4712f501c175f2a57f64cd0c441e56fa0c51e4238ab1a196d89037f719194981536a1
7
+ data.tar.gz: 76ca3f9c58e4f1450b3e442b0edc825b68ea5b533796b34e90963630dc219731238128f44dbe1267979199d06efa1a3cef99913318bb3684af663e377ec8b97f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cem_acpt (0.2.8-universal-java-17)
4
+ cem_acpt (0.2.11-universal-java-17)
5
5
  concurrent-ruby (>= 1.1, < 2.0)
6
6
  deep_merge (>= 1.2, < 2.0)
7
7
  ed25519 (>= 1.2, < 2.0)
@@ -68,7 +68,7 @@ GEM
68
68
  rspec-its
69
69
  specinfra (~> 2.83.1)
70
70
  sfl (2.3)
71
- specinfra (2.83.2)
71
+ specinfra (2.83.3)
72
72
  net-scp
73
73
  net-ssh (>= 2.7)
74
74
  net-telnet (= 0.1.1)
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # CemAcpt
2
2
 
3
- CemAcpt is an acceptance testing library / command line application for running acceptance tests against the CEM modules. It is heavily inspired by Puppet Litmus. CemAcpt is fully compatible with Puppet Litmus acceptance tests.
3
+ CemAcpt is an acceptance testing library / command line application for running acceptance tests for the CEM modules. It is heavily inspired by Puppet Litmus. CemAcpt is fully compatible with Puppet Litmus acceptance tests.
4
4
 
5
- CemAcpt was written to facilitate running a single acceptance test file against one or more test nodes concurrently. This is useful when your module supports a lot of different operating systems and modifies low-level components of those operating systems. For CEM, we manage things such as firewall and bootloader configurations which requires us to test against multiple base images of a single operating system (i.e. RHEL 8 with `iptables` and RHEL 8 with `firewalld`). Additionally, we test our module using both Puppet 6 and Puppet 7. As you can see, we now need to test against four test nodes that are all RHEL 8 with slightly different baseline configs. CemAcpt allows us to do so quickly in parallel.
5
+ CemAcpt was written to facilitate running a single acceptance test file against one or more individual test nodes concurrently, and have each additional acceptance test file do the same. This is useful when your module supports a lot of different operating systems and modifies low-level components of those operating systems. For CEM, we manage things such as firewall and bootloader configurations which requires us to test against multiple base images of a single operating system (i.e. RHEL 8 with `iptables` and RHEL 8 with `firewalld`). Additionally, we test our module using both Puppet 6 and Puppet 7. As you can see, we now need to test against four test nodes that are all RHEL 8 with slightly different baseline configs. CemAcpt allows us to do so quickly in parallel.
6
6
 
7
7
  Major differences between Puppet Litmus and CemAcpt are:
8
8
 
@@ -10,8 +10,72 @@ Major differences between Puppet Litmus and CemAcpt are:
10
10
  - Test node image names can be dynamically defined by configurable parsing of the acceptance test name.
11
11
  - This allows your acceptance test names to define the node it will run against.
12
12
  - CemAcpt has it's own CLI for running provisioning of hosts and the test suite and does not use `rake`.
13
- - CemAcpt runs everything, including provisioning the test node, creating a temporary manifest on the test node, and running the acceptance test on the test node, in parallel. No matter how many test / host combinations there are, running the test suite will only take as long as the longest running single test / host combo.
13
+ - CemAcpt runs everything, including provisioning the test node(s), creating a temporary manifest on the test node(s), and running the acceptance test on the test node(s), in parallel. No matter how many test / host combinations there are, running the test suite will only take as long as the longest running single test / host combo.
14
14
  - CemAcpt is configurable via a YAML file.
15
+ - CemAcpt provides extensive logging, including the ability to save logs to a file and an option to format log output on `stdout` specifically for GitHub Actions.
16
+
17
+ ## Installation
18
+
19
+ ### Command-line usage
20
+
21
+ Assuming cem_acpt is already configured in your Puppet module, you can run cem_acpt from your command line.
22
+
23
+ `cem_acpt` require JRuby 9.3.3.0 or greater. To install and use JRuby with `rvm`:
24
+
25
+ ```
26
+ rvm install jruby-9.3.3.0
27
+ rvm use jruby-9.3.3.0
28
+ # If you have a Gemfile.lock already
29
+ rm Gemfile.lock
30
+ bundle install
31
+ jruby --server <jruby opts> -S bundle exec cem_acpt <opts>
32
+ ```
33
+
34
+ > If you do not delete Gemfile.lock before running `bundle install`, you will most likely encounter dependency version errors. Just delete Gemfile.lock and run `bundle install` again to get past these.
35
+
36
+ ### Installing into a Puppet module
37
+
38
+ Add this line to your module's Gemfile (usually under `group :development`):
39
+
40
+ ```ruby
41
+ gem 'cem_acpt', require: false
42
+ ```
43
+
44
+ Then, add the following to your `spec_helper_acceptance.rb` (replacing the Puppet Litmus configuration lines):
45
+
46
+ ```ruby
47
+ require 'cem_acpt'
48
+
49
+ CemAcpt.configure_spec_helper!
50
+ ```
51
+
52
+ To use CemAcpt in your acceptance tests, you must call the method `initialize_test_environment!` in your acceptance test's `describe` block before doing anything else:
53
+
54
+ ```ruby
55
+ describe 'cem_linux CIS Level 1' do
56
+ initialize_test_environment!
57
+ ...
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### RSpec methods
63
+
64
+ CemAcpt enables you to use all ServerSpec methods in your acceptance tests, and adds three new methods that can be used (these methods are used the same as in Litmus):
65
+
66
+ - `apply_manifest(manifest, opts = {})`: Applies a Puppet manifest given as a string.
67
+ - `idempotent_apply(manifest, opts = {})`: Applies a Puppet manifest given as a string twice and fails if the second apply reports changes.
68
+ - `run_shell(command, opts = {})`: Runs a shell command against the test node.
69
+
70
+ ### Config file
71
+
72
+ CemAcpt can use a config file (default path is `./cem_acpt_config.yaml`) to configure it's settings. Below are some of the options available in the config file:
73
+
74
+ - `tests`: An array of test names you want to run. These are assumed to exist in the path `spec/acceptance`. You should leave off the suffix `_spec.rb` from the test file names.
75
+ - `platform`: The backend platform that acceptance tests will run on. Currently, only `gcp` is supported.
76
+ - `test_data`: Configurations for test data. Allows assigning variables to test data either dynamically or statically.
77
+ - `node_data`: Configurations for nodes created by the platform. The available options are platform-dependent.
78
+ - `image_name_builder`: If this key is specified, platforms will be passed a dynamically generated image name based on the configurations under this key.
15
79
 
16
80
  ## Concepts
17
81
 
@@ -79,62 +143,6 @@ Rules that allow for processing variables after all other test data rules are ra
79
143
 
80
144
  Much like `name_pattern_vars`, specifying the `image_name_builder` top-level key in the config allows you to manipulate acceptance test names to create a special test data variable called `image_name`. This is helpful for when you have multiple platform base images and want to use the correct image with the correct test. See [sample_config.yaml](sample_config.yaml) for an example.
81
145
 
82
- ## Installation
83
-
84
- ### Installing from RubyGems
85
-
86
- To install the gem locally to use as a CLI tool:
87
-
88
- `gem install cem_acpt --prerelease`
89
-
90
- ### Installing into a Puppet module
91
-
92
- Add this line to your module's Gemfile (usually under `group :development`):
93
-
94
- ```ruby
95
- gem 'cem_acpt', require: false
96
- ```
97
-
98
- Then, add the following to your `spec_helper_acceptance.rb` (replacing the Puppet Litmus configuration lines):
99
-
100
- ```ruby
101
- require 'cem_acpt'
102
-
103
- CemAcpt.configure_spec_helper!
104
- ```
105
-
106
- To use CemAcpt in your acceptance tests, you must call the method `initialize_test_environment!` in your acceptance test's `describe` block before doing anything else:
107
-
108
- ```ruby
109
- describe 'cem_linux CIS Level 1' do
110
- initialize_test_environment!
111
- ...
112
- ```
113
-
114
- ## Usage
115
-
116
- ### RSpec methods
117
-
118
- CemAcpt enables you to use all ServerSpec methods in your acceptance tests, and adds three new methods that can be used (these methods are used the same as in Litmus):
119
-
120
- - `apply_manifest(manifest, opts = {})`: Applies a Puppet manifest given as a string.
121
- - `idempotent_apply(manifest, opts = {})`: Applies a Puppet manifest given as a string twice and fails if the second apply reports changes.
122
- - `run_shell(command, opts = {})`: Runs a shell command against the test node.
123
-
124
- ### Config file
125
-
126
- CemAcpt can use a config file (default path is `./cem_acpt_config.yaml`) to configure it's settings. Below are some of the options available in the config file:
127
-
128
- - `tests`: An array of test names you want to run. These are assumed to exist in the path `spec/acceptance`. You should leave off the suffix `_spec.rb` from the test file names.
129
- - `platform`: The backend platform that acceptance tests will run on. Currently, only `gcp` is supported.
130
- - `test_data`: Configurations for test data. Allows assigning variables to test data either dynamically or statically.
131
- - `node_data`: Configurations for nodes created by the platform. The available options are platform-dependent.
132
- - `image_name_builder`: If this key is specified, platforms will be passed a dynamically generated image name based on the configurations under this key.
133
-
134
- ### Semantic acceptance test names
135
-
136
- With CemAcpt, acceptance test names can have semantic meaning and influence how your acceptance test nodes are provisioned and how test data is created. The semantic meaning of the test names can be configured in the config file.
137
-
138
146
  ## Development
139
147
 
140
148
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -36,10 +36,8 @@ module CemAcpt
36
36
  # Creates a SSH key and a SSH known hosts file for the acceptance test suite
37
37
  def new_test_ssh_key
38
38
  log('Creating ephemeral SSH key and known hosts file for acceptance test suites...')
39
- @ssh_priv_key, @ssh_pub_key = CemAcpt::Utils::SSH.ephemeral_ssh_key
40
- @ssh_known_hosts = CemAcpt::Utils::SSH.acpt_known_hosts
41
- CemAcpt::Utils::SSH.set_ssh_file_permissions(@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts)
42
- log('Successfully created SSH files...')
39
+ @ssh_priv_key, @ssh_pub_key, @ssh_known_hosts = CemAcpt::Utils::SSH::Ephemeral.create
40
+ log('Successfully created SSH files.')
43
41
  log("SSH private key: #{@ssh_priv_key}", :debug)
44
42
  log("SSH public key: #{@ssh_pub_key}", :debug)
45
43
  log("SSH known hosts: #{@ssh_known_hosts}", :debug)
@@ -48,7 +46,9 @@ module CemAcpt
48
46
  # Deletes acceptance test suite SSH files
49
47
  def clean_test_ssh_key
50
48
  log('Deleting ephemeral ssh keys and acpt_known_hosts if they exist...')
51
- [@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts].map { |f| File.delete(f) if File.exist?(f) }
49
+ cleaned = CemAcpt::Utils::SSH::Ephemeral.clean
50
+ log('Successfully cleaned ephemeral ssh files.')
51
+ log("Deleted: #{cleaned}", :debug)
52
52
  end
53
53
 
54
54
  # Prints a period to the terminal every 5 seconds in a single line to keep the terminal
@@ -62,8 +62,8 @@ module CemAcpt
62
62
  end
63
63
 
64
64
  def clean_up_test_suite(opts)
65
- @ctx.node_inventory.clear!
66
- @ctx.node_inventory.clean_local_files
65
+ @ctx&.node_inventory&.clear!
66
+ @ctx&.node_inventory&.clean_local_files
67
67
  clean_test_ssh_key unless opts[:no_ephemeral_ssh_key]
68
68
  @run_handler&.destroy_test_nodes
69
69
  @keep_terminal_alive&.kill
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module Logging
5
+ module Formatter
6
+ class << self
7
+ def for(log_format)
8
+ all.find { |f| f.log_format == log_format.downcase.to_sym }.get
9
+ end
10
+
11
+ private
12
+
13
+ def all
14
+ @all ||= [FileFormatter.new, JSONFormatter.new, TextFormatter.new, GithubActionFormatter.new]
15
+ end
16
+ end
17
+
18
+ class FileFormatter
19
+ attr_reader :log_format
20
+
21
+ def initialize
22
+ @log_format = :file
23
+ end
24
+
25
+ def get
26
+ proc do |severity, datetime, progname, msg|
27
+ format(severity, datetime, progname, msg)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def format(severity, datetime, _progname, msg)
34
+ "[#{datetime}] #{severity}: #{msg}\n"
35
+ end
36
+ end
37
+
38
+ class JSONFormatter < FileFormatter
39
+ def initialize
40
+ super
41
+ @log_format = :json
42
+ end
43
+
44
+ private
45
+
46
+ def format(severity, datetime, progname, msg)
47
+ require 'json'
48
+ {
49
+ timestamp: datetime,
50
+ progname: progname,
51
+ severity: severity,
52
+ message: msg,
53
+ }.to_json + "\n"
54
+ end
55
+ end
56
+
57
+ class TextFormatter < FileFormatter
58
+ def initialize
59
+ super
60
+ @log_format = :text
61
+ end
62
+
63
+ private
64
+
65
+ def format(severity, _datetime, _progname, msg)
66
+ "#{severity} - #{msg}\n"
67
+ end
68
+ end
69
+
70
+ class GithubActionFormatter < FileFormatter
71
+ SEV_MAP = {
72
+ 'DEBUG' => '::debug',
73
+ 'INFO' => '::notice',
74
+ 'WARN' => '::warning',
75
+ 'ERROR' => '::error',
76
+ 'FATAL' => '::error',
77
+ }.freeze
78
+
79
+ def initialize
80
+ super
81
+ @log_format = :github_action
82
+ end
83
+
84
+ private
85
+
86
+ def format(severity, _datetime, _progname, msg)
87
+ if severity == 'DEBUG'
88
+ "#{SEV_MAP[severity]}::{#{msg}}\n"
89
+ else
90
+ "#{SEV_MAP[severity]} #{msg}\n"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
+ require 'cem_acpt/logging/formatter'
4
5
 
5
6
  module CemAcpt
6
7
  # Logging for CemAcpt
@@ -92,41 +93,7 @@ module CemAcpt
92
93
  # @param f [Symbol] the log format style to set
93
94
  # @return [Proc] the proc to be passed to Logger#formatter=
94
95
  def new_log_formatter(f)
95
- case f.downcase.to_sym
96
- when :json
97
- require 'json'
98
- @current_log_format = :json
99
- proc do |severity, datetime, progname, msg|
100
- {
101
- timestamp: datetime,
102
- progname: progname,
103
- severity: severity,
104
- message: msg,
105
- }.to_json + "\n"
106
- end
107
- when :text
108
- @current_log_format = :text
109
- proc do |severity, _datetime, _progname, msg|
110
- "#{severity} - #{msg}\n"
111
- end
112
- when :github_action
113
- @current_log_format = :github_action
114
- sev_map = {
115
- 'DEBUG' => '::debug::',
116
- 'INFO' => '::notice::',
117
- 'WARN' => '::warning::',
118
- 'ERROR' => '::error::',
119
- 'FATAL' => '::error::',
120
- }
121
- proc do |severity, _datetime, _progname, msg|
122
- "#{sev_map[severity]}{#{msg}}\n"
123
- end
124
- else
125
- @current_log_format = :file
126
- proc do |severity, datetime, _progname, msg|
127
- "[#{datetime}] #{severity}: #{msg}\n"
128
- end
129
- end
96
+ CemAcpt::Logging::Formatter.for(f)
130
97
  end
131
98
 
132
99
  # Returns the current log config if set, or the default if not.
@@ -12,13 +12,17 @@ module CemAcpt
12
12
  class BundlerNotFoundError < StandardError; end
13
13
  class RSpecNotFoundError < StandardError; end
14
14
 
15
- # Holds and formats a RSpec command
16
- class Command
17
- include CemAcpt::LoggingAsync
18
- attr_reader :debug, :format, :test_path, :use_bundler, :pty_pid
15
+ # Holds RSpec options used by Command
16
+ class Options
17
+ OPTIONS = %i[test_path bundle rspec use_bundler bundle_install format debug quiet env].freeze
19
18
 
19
+ attr_accessor(*OPTIONS)
20
+
21
+ # @param config [CemAcpt::Config] A config object
20
22
  # @param opts [Hash] options hash for the RSpec command
21
23
  # @option opts [String] :test_path The path (or glob path) to the test file(s) to run. If blank, runs all.
24
+ # @option opts [String] :bundle The path to the `bundle` binary.
25
+ # @option opts [String] :rspec The path to the `rspec` binary.
22
26
  # @option opts [Hash] :format Format options for rspec where the key is the format (documentation, json, etc)
23
27
  # and the value is the out-file path. If you do not want to save the results of a format to a file, the
24
28
  # value should be `nil`.
@@ -30,56 +34,97 @@ module CemAcpt
30
34
  # Default is `true`.
31
35
  # @option opts [Boolean] :bundle_install Whether or not to run `bundle install` before the RSpec command
32
36
  # if `use_bundler` is `true`.
33
- # @option opts [Boolean] :use_shell Whether or not to add `$SHELL` as a prefix to the command
34
37
  # @option opts [Hash] :env Environment variables to prepend to the command
35
- def initialize(opts = {})
36
- @test_path = opts[:test_path]&.shellescape
37
- @format = opts.fetch(:format, {})
38
- @debug = opts.fetch(:debug, false)
39
- @quiet = @debug ? false : opts.fetch(:quiet, false)
40
- @use_bundler = opts.fetch(:use_bundler, false)
41
- @bundle_install = opts.fetch(:bundle_install, false)
42
- @env = opts.fetch(:env, {})
43
- @pty_pid = nil
44
- validate_and_set_bin_paths(opts)
38
+ def initialize(config, **opts)
39
+ @config = config
40
+ @opts = opts
41
+ define_option_instance_vars
42
+ end
43
+
44
+ # Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
45
+ # be either passed in as options in the `opts` Hash or interrogated from the
46
+ # system.
47
+ # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
48
+ # `bundle` binary is not found.
49
+ # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
50
+ def resolve_bin_paths
51
+ %i[bundle rspec].each do |bin|
52
+ bin_path = instance_variable_get("@#{bin}") || `#{ENV['SHELL']} -c 'command -v #{bin}'`.strip
53
+ bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
54
+ instance_variable_set("@#{bin}", bin_path)
55
+ end
56
+ end
57
+
58
+ # Detects if the current Ruby context is JRuby
59
+ def jruby?
60
+ Object.const_defined?('JRUBY_VERSION')
45
61
  end
46
62
 
47
- # Sets debug mode to `true`
48
- def set_debug
49
- @debug = true
50
- if @quiet
51
- async_debug('Setting :quiet to false because :debug is now true.')
52
- @quiet = false
63
+ private
64
+
65
+ def defaults
66
+ @defaults ||= { use_bundler: false,
67
+ bundle_install: false,
68
+ format: { documentation: nil },
69
+ debug: false,
70
+ quiet: false,
71
+ env: {} }
72
+ end
73
+
74
+ def define_option_instance_vars
75
+ OPTIONS.each do |o|
76
+ val = @config.get("rspec.#{o}") || @opts[o] || defaults[o] || nil
77
+ instance_variable_set("@#{o}", val)
53
78
  end
54
79
  end
55
80
 
56
- # Sets debug mode to `false`
57
- def unset_debug
58
- @debug = false
81
+ # Handles binary paths which are not found
82
+ # @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
83
+ # @param bin_path [String] The path to the binary that was checked.
84
+ # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
85
+ # `bundle` binary is not found.
86
+ # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
87
+ # @raise [RuntimeError] if `bin` is not :bundle or :rspec.
88
+ def bin_not_found(bin, bin_path)
89
+ msg_base = "#{bin} not found."
90
+ msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
91
+ case bin
92
+ when :bundle
93
+ raise BundlerNotFoundError, msg if opts.use_bundler
94
+ when :rspec
95
+ raise RSpecNotFoundError, msg
96
+ else
97
+ raise "bin #{bin} not recognized!"
98
+ end
59
99
  end
100
+ end
101
+
102
+ # Holds and formats a RSpec command
103
+ class Command
104
+ include CemAcpt::LoggingAsync
105
+ attr_reader :opts, :pty_pid
106
+
107
+ def initialize(opts = Options.new)
108
+ raise 'opts must be instance of CemAcpt::RSpecUtils::Options' unless opts.is_a?(CemAcpt::RSpecUtils::Options)
60
109
 
61
- def quiet
62
- @quiet && !debug
110
+ @opts = opts
111
+ @opts.resolve_bin_paths
112
+ @opts.env = @opts.env.merge({ 'RSPEC_DEBUG' => 'true' }) if @opts.debug
113
+ @pty_pid = nil
63
114
  end
64
115
 
65
116
  # Adds a new format to the RSpec command
66
117
  # @param fmt [String] The name of the format (i.e. "documentation", "json", etc.)
67
118
  # @param out [String] If specified, saves the specified format to a file at this path
68
119
  def with_format(fmt, out: nil)
69
- @format[fmt.to_sym] = out
70
- end
71
-
72
- # Environment variables that will be used for the RSpec command
73
- # @return [Hash] A Hash of environment variables with each key pair being: <var name> => <var value>
74
- def env
75
- @debug ? @env.merge({ 'RSPEC_DEBUG' => 'true' }) : @env
120
+ opts.format[fmt.to_sym] = out
76
121
  end
77
122
 
78
123
  # Returns an array representation of the RSpec command
79
124
  def to_a
80
125
  cmd = cmd_base.dup
81
- cmd << test_path if test_path
82
- format.each do |fmt, out|
126
+ cmd << opts.test_path if opts.test_path
127
+ opts.format.each do |fmt, out|
83
128
  cmd += ['--format', fmt.to_s.shellescape]
84
129
  cmd += ['--out', out.to_s.shellescape] if out
85
130
  end
@@ -112,12 +157,12 @@ module CemAcpt
112
157
  # @return [Integer] The exit code of the RSpec command
113
158
  def execute_pty(log_prefix: 'RSPEC')
114
159
  async_debug("Executing RSpec command '#{self}' in PTY...", log_prefix)
115
- PTY.spawn(env, ENV['SHELL']) do |r, w, pid|
160
+ PTY.spawn(opts.env, ENV['SHELL']) do |r, w, pid|
116
161
  @pty_pid = pid
117
162
  async_debug("Spawned RSpec PTY with PID #{@pty_pid}", log_prefix)
118
163
  export_envs(w)
119
164
  w.puts "#{self}; exit $?"
120
- quiet ? wait_io(r) : read_io(r, log_prefix: log_prefix)
165
+ handle_io(r, log_prefix: log_prefix)
121
166
  end
122
167
  $CHILD_STATUS
123
168
  end
@@ -130,9 +175,9 @@ module CemAcpt
130
175
  def execute_no_pty(log_prefix: 'RSPEC')
131
176
  async_info("Executing RSpec command '#{self}' with Open3.popen2e()...", log_prefix)
132
177
  exit_status = nil
133
- Open3.popen2e(env, to_s) do |stdin, std_out_err, wait_thr|
178
+ Open3.popen2e(opts.env, to_s) do |stdin, std_out_err, wait_thr|
134
179
  stdin.close
135
- quiet ? wait_io(std_out_err) : read_io(std_out_err, log_prefix: log_prefix)
180
+ handle_io(std_out_err, log_prefix: log_prefix)
136
181
  exit_status = wait_thr.value
137
182
  end
138
183
  exit_status
@@ -147,26 +192,21 @@ module CemAcpt
147
192
 
148
193
  private
149
194
 
150
- # Detects if the current Ruby context is JRuby
151
- def jruby?
152
- File.basename(RbConfig.ruby) == 'jruby'
153
- end
154
-
155
195
  # The base RSpec command
156
196
  def cmd_base
157
- use_bundler ? cmd_base_bundler : cmd_base_rspec
197
+ opts.use_bundler ? cmd_base_bundler : cmd_base_rspec
158
198
  end
159
199
 
160
200
  # The base RSpec command if `:use_bundler` is `true`.
161
201
  def cmd_base_bundler
162
- base = [@bundle, 'exec', 'rspec']
163
- base.unshift("#{@bundle} install;") if @bundle_install
202
+ base = [opts.bundle, 'exec', 'rspec']
203
+ base.unshift("#{opts.bundle} install;") if opts.bundle_install
164
204
  base
165
205
  end
166
206
 
167
207
  # The base RSpec command if `:use_bundler` is `false`
168
208
  def cmd_base_rspec
169
- [@rspec]
209
+ [opts.rspec]
170
210
  end
171
211
 
172
212
  # Puts export statements for each key-value pair in `env` to the given writer.
@@ -174,46 +214,14 @@ module CemAcpt
174
214
  # pass the statements to a shell.
175
215
  # @param writer [IO] An IO object that supprts `puts` and can send statements to a shell
176
216
  def export_envs(writer)
177
- env.each do |key, val|
217
+ @opts.env.each do |key, val|
178
218
  writer.puts "export #{key}=#{val}"
179
219
  end
180
220
  end
181
221
 
182
- # Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
183
- # be either passed in as options in the `opts` Hash or interrogated from the
184
- # system.
185
- # @param opts [Hash] The options hash
186
- # @option opts [String] :bundle An absolute path on the system to the `bundle` binary.
187
- # @option opts [String] :rspec An absolute path on the system to the `rspec` binary.
188
- # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
189
- # `bundle` binary is not found.
190
- # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
191
- def validate_and_set_bin_paths(opts = {})
192
- %i[bundle rspec].each do |bin|
193
- bin_path = opts[bin] || `command -v #{bin}`.strip
194
- bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
195
- instance_variable_set("@#{bin}", bin_path)
196
- end
197
- end
198
-
199
- # Handles binary paths which are not found
200
- # @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
201
- # @param bin_path [String] The path to the binary that was checked.
202
- # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
203
- # `bundle` binary is not found.
204
- # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
205
- # @raise [RuntimeError] if `bin` is not :bundle or :rspec.
206
- def bin_not_found(bin, bin_path)
207
- msg_base = "#{bin} not found."
208
- msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
209
- case bin
210
- when :bundle
211
- raise BundlerNotFoundError, msg if @use_bundler
212
- when :rspec
213
- raise RSpecNotFoundError, msg
214
- else
215
- raise "bin #{bin} not recognized!"
216
- end
222
+ # Handles IO output
223
+ def handle_io(io_stream, **kwargs)
224
+ opts.quiet ? wait_io(io_stream) : read_io(io_stream, log_prefix: kwargs[:log_prefix])
217
225
  end
218
226
 
219
227
  # Blocking wait on an IO stream. Wait stops once the IO stream has reached
@@ -105,10 +105,7 @@ module CemAcpt
105
105
  if result['examples'].empty? && !result['messages'].empty?
106
106
  logger.error(result['messages'].join("\n"))
107
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
108
+ handle_example_error_pending_skipped(runner.node.node_name, result['examples'])
112
109
  end
113
110
  else
114
111
  handle_runner_error_results(runner)
@@ -118,6 +115,19 @@ module CemAcpt
118
115
  end
119
116
  end
120
117
 
118
+ # Handles logging for any failed / skipped / pending examples.
119
+ def handle_example_error_pending_skipped(node_name, examples)
120
+ examples.each do |e|
121
+ next if e['status'] == 'passed'
122
+
123
+ if e['status'] == 'pending'
124
+ logger.info(test_pending_msg(node_name, e))
125
+ else
126
+ logger.error(test_error_msg(node_name, e))
127
+ end
128
+ end
129
+ end
130
+
121
131
  # Handles logging the results of the runners that errored.
122
132
  def handle_runner_error_results(runner)
123
133
  logger.error("SUMMARY: Encountered an error with test #{runner.node.test_data[:test_name]} on node #{runner.node.node_name}")
@@ -136,6 +146,19 @@ module CemAcpt
136
146
  "Error: #{err.message}",
137
147
  'Backtrace:',
138
148
  err.backtrace.join("\n"),
149
+ "\n",
150
+ ].join("\n")
151
+ end
152
+
153
+ # Formats a test result for tests that are pending or skipped. Is used for logging.
154
+ # @param node [String] the name of the node the test ran on
155
+ # @param result [Hash] the test result to format
156
+ # @return [String] the formatted test result
157
+ def test_pending_msg(_, result)
158
+ [
159
+ "TEST PENDING / SKIPPED: #{result['full_description']}",
160
+ "REASON: #{result['pending_message']}",
161
+ "\n",
139
162
  ].join("\n")
140
163
  end
141
164
 
@@ -145,12 +145,12 @@ module CemAcpt
145
145
  async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
146
146
  @node.run_tests do |cmd_env|
147
147
  cmd_opts = rspec_opts
148
- cmd_opts[:env].merge!(cmd_env) if cmd_env
148
+ cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
149
149
  # Documentation format gets logged in real time, JSON file is read after the fact
150
150
  begin
151
151
  @rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
152
- @rspec_cmd.execute(log_prefix: log_prefix('RSPEC'))
153
- @run_result.from_json_file(cmd_opts[:format][:json])
152
+ @rspec_cmd.execute(pty: false, log_prefix: log_prefix('RSPEC'))
153
+ @run_result.from_json_file(cmd_opts.format[:json])
154
154
  rescue Errno::EIO => e
155
155
  async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
156
156
  @run_result.from_error(e)
@@ -191,19 +191,21 @@ module CemAcpt
191
191
 
192
192
  # Options used with RSpec
193
193
  def rspec_opts
194
- opts = {
195
- test_path: @node.test_data[:test_file],
196
- use_bundler: @context.config.get('rspec.use_bundler') || false,
197
- bundle_install: @context.config.get('rspec.bundle_install') || false,
198
- format: @context.config.get('rspec.format') || { json: "results_#{@node.test_data[:test_name]}.json" },
199
- debug: (@debug_mode && @context.config.get('verbose')),
200
- quiet: @context.config.get('quiet'),
201
- env: {
202
- 'TARGET_HOST' => @node.node_name,
203
- }
204
- }
205
- opts[:format][:documentation] = nil unless @context.config.get('verbose') || @context.config.get('rspec.format')
206
- opts
194
+ opts_test_path = @node.test_data[:test_file]
195
+ opts_env = { 'TARGET_HOST' => @node.node_name }
196
+ opts_debug = (@debug_mode && @context.config.get('verbose'))
197
+ opts_quiet = @context.config.get('quiet')
198
+ opts_format = if @context.config.get('verbose')
199
+ { json: "results_#{@node.test_data[:test_name]}.json", documentation: nil }
200
+ else
201
+ { json: "results_#{@node.test_data[:test_name]}.json" }
202
+ end
203
+ CemAcpt::RSpecUtils::Options.new(@context.config,
204
+ test_path: opts_test_path,
205
+ env: opts_env,
206
+ debug: opts_debug,
207
+ quiet: opts_quiet,
208
+ format: opts_format)
207
209
  end
208
210
  end
209
211
  end
@@ -81,6 +81,40 @@ module CemAcpt
81
81
 
82
82
  # SSH-related utilities
83
83
  module SSH
84
+ module Ephemeral
85
+ PRIV_KEY = 'acpt_test_key'
86
+ CREATE_OPTS = {
87
+ type: 'rsa',
88
+ bits: '4096',
89
+ comment: 'Ephemeral for cem_acpt',
90
+ password: '',
91
+ known_hosts: 'acpt_known_hosts',
92
+ overwrite_known_hosts: true,
93
+ }.freeze
94
+
95
+ class << self
96
+ attr_accessor :ephemeral_keydir
97
+ end
98
+
99
+ def self.create(keydir: CemAcpt::Utils::SSH.default_keydir)
100
+ self.ephemeral_keydir = keydir
101
+ @priv_key, @pub_key, @known_hosts = CemAcpt::Utils::SSH.create(PRIV_KEY, keydir: ephemeral_keydir, **CREATE_OPTS)
102
+ [@priv_key, @pub_key, @known_hosts]
103
+ end
104
+
105
+ def self.clean
106
+ [@priv_key, @pub_key, @known_hosts].each_with_object([]) do |f, arr|
107
+ next unless f
108
+
109
+ path = CemAcpt::Utils::SSH.file_path(f, keydir: ephemeral_keydir)
110
+ if ::File.exist?(path)
111
+ ::File.delete(path)
112
+ arr << path
113
+ end
114
+ end
115
+ end
116
+ end
117
+
84
118
  def self.ssh_keygen
85
119
  bin_path = `#{ENV['SHELL']} -c 'command -v ssh-keygen'`.chomp
86
120
  raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
@@ -97,26 +131,52 @@ module CemAcpt
97
131
  ssh_dir
98
132
  end
99
133
 
100
- def self.ephemeral_ssh_key(type: 'rsa', bits: '4096', comment: nil, keydir: default_keydir)
101
- raise ArgumentError, 'keydir does not exist' unless ::File.directory?(keydir)
134
+ def self.file_path(file_name, keydir: default_keydir)
135
+ ::File.join(keydir, file_name)
136
+ end
137
+
138
+ # Takes a file name (not path) and optional SSH key directory and returns the paths
139
+ # to the private key and public key based on the file name given.
140
+ # @param file_name [String] The base name for the keys
141
+ # @param keydir [String] An optional SSH key directory
142
+ def self.key_paths(file_name, keydir: default_keydir)
143
+ [file_path(file_name, keydir: keydir), file_path("#{file_name}.pub", keydir: keydir)]
144
+ end
102
145
 
103
- keyfile = ::File.join(keydir, 'acpt_test_key')
104
- keygen_cmd = [ssh_keygen, "-t #{type}", "-b #{bits}", "-f #{keyfile}", '-N ""']
146
+ def self.create(key_name, type: 'rsa', bits: '4096', comment: nil, password: '', known_hosts: nil, overwrite_known_hosts: true, keydir: default_keydir)
147
+ raise ArgumentError, "Key directory #{keydir} does not exist" unless ::File.directory?(keydir)
148
+
149
+ keys = key_paths(key_name, keydir: keydir)
150
+ # If we don't delete an existing key file, generation will fail
151
+ keys.each { |f| ::File.delete(f) if ::File.exist?(f) }
152
+ keygen_cmd = [ssh_keygen, "-t #{type}", "-b #{bits}", "-f #{keys[0]}", "-N '#{password}'"]
105
153
  keygen_cmd << "-C \"#{comment}\"" if comment
106
154
  _, stderr, status = Open3.capture3(keygen_cmd.join(' '))
107
- raise "Failed to generate ephemeral SSH key: #{stderr}" unless status.success?
155
+ raise "Failed to generate ephemeral SSH key: STDOUT: #{stdout}; STDERR: #{stderr}" unless status.success?
108
156
 
109
- [keyfile, "#{keyfile}.pub"]
157
+ keys << create_known_hosts(known_hosts, overwrite: overwrite_known_hosts, keydir: keydir)
158
+ set_ssh_file_permissions(*keys)
159
+ keys
110
160
  end
111
161
 
112
- def self.acpt_known_hosts(keydir: default_keydir, file_name: 'acpt_known_hosts', overwrite: true)
113
- kh_file = ::File.join(keydir, file_name)
162
+ def self.create_known_hosts(known_hosts, overwrite: true, keydir: default_keydir)
163
+ return nil unless known_hosts
164
+
165
+ kh_file = file_path(known_hosts, keydir: keydir)
114
166
  ::File.open(kh_file, 'w') { |f| f.write("\n") } unless ::File.exist?(kh_file) && !overwrite
115
167
  kh_file
116
168
  end
117
169
 
118
- def self.set_ssh_file_permissions(priv_key, pub_key, known_hosts)
119
- CemAcpt::Utils::File.set_permissions(0o600, priv_key, pub_key, known_hosts)
170
+ def self.set_ssh_file_permissions(*files)
171
+ CemAcpt::Utils::File.set_permissions(0o600, *files.uniq.compact)
172
+ end
173
+
174
+ def self.ephemeral_ssh_key(keydir: default_keydir)
175
+ CemAcpt::Utils::SSH::Ephemeral.create(keydir: keydir)
176
+ end
177
+
178
+ def self.clean_ephemeral_keys
179
+ CemAcpt::Utils::SSH::Ephemeral.clean
120
180
  end
121
181
  end
122
182
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CemAcpt
4
- VERSION = '0.2.8'
4
+ VERSION = '0.2.11'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cem_acpt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.2.11
5
5
  platform: universal-java-17
6
6
  authors:
7
7
  - puppetlabs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-28 00:00:00.000000000 Z
11
+ date: 2022-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -160,6 +160,7 @@ files:
160
160
  - lib/cem_acpt/core_extensions.rb
161
161
  - lib/cem_acpt/image_name_builder.rb
162
162
  - lib/cem_acpt/logging.rb
163
+ - lib/cem_acpt/logging/formatter.rb
163
164
  - lib/cem_acpt/platform.rb
164
165
  - lib/cem_acpt/platform/base.rb
165
166
  - lib/cem_acpt/platform/base/cmd.rb