cem_acpt 0.2.6-universal-java-17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Currently unused in this implementation.
4
+ module CemAcpt::Bootstrap::OperatingSystem
5
+ # This module holds methods used by Bootstrap for RHEL-family operating systems
6
+ module RhelFamily
7
+ def puppet_agent_repo
8
+ "https://yum.puppet.com/#{@collection}-release-el-#{@major_version}.noarch.rpm"
9
+ end
10
+
11
+ def package_manager
12
+ return 'dnf' if @major_version.to_i >= 8
13
+
14
+ 'yum'
15
+ end
16
+
17
+ def rvm_deps
18
+ [
19
+ 'kernel-devel',
20
+ 'gcc',
21
+ 'gcc-c++',
22
+ 'make',
23
+ 'augeas',
24
+ 'augeas-devel',
25
+ 'patch',
26
+ 'readline',
27
+ 'readline-devel',
28
+ 'zlib',
29
+ 'zlib-devel',
30
+ 'libffi-devel',
31
+ 'openssl-devel',
32
+ 'bzip2',
33
+ 'autoconf',
34
+ 'automake',
35
+ 'libtool',
36
+ 'bison',
37
+ 'sqlite-devel',
38
+ ]
39
+ end
40
+
41
+ def repo_installs(repos, sudo: true)
42
+ return if repos.nil? || repos.empty?
43
+
44
+ cmd = [
45
+ repo_install_cmd(repos, sudo: sudo),
46
+ repo_install_verify_cmd(repos, sudo: sudo),
47
+ ].join(' && ')
48
+ "#{cmd} | sudo tee -ai #{@log_file}"
49
+ end
50
+
51
+ def package_installs(packages, sudo: true)
52
+ return if packages.nil? || packages.empty?
53
+
54
+ packages += rvm_deps
55
+ cmd = [
56
+ package_install_cmd(packages, sudo: sudo),
57
+ package_install_verify_cmd(packages, sudo: sudo),
58
+ ].join(' && ')
59
+ "#{cmd} | sudo tee -ai #{@log_file}"
60
+ end
61
+
62
+ def service_starts(services, sudo: true)
63
+ return if services.nil? || services.empty?
64
+
65
+ cmd = [
66
+ #service_start_verify_existance_cmd(services, sudo: sudo),
67
+ service_start_cmd(services, sudo: sudo),
68
+ ].join(' && ')
69
+ "#{cmd} | sudo tee -ai #{@log_file}"
70
+ end
71
+
72
+ def repo_install_cmd(repos, sudo: true)
73
+ cmd = []
74
+ cmd << 'sudo' if sudo
75
+ cmd << 'rpm -Uv'
76
+ repos.each { |r| cmd << r }
77
+ cmd.join(' ')
78
+ end
79
+
80
+ def repo_install_verify_cmd(repos, sudo: true)
81
+ cmd = []
82
+ cmd << 'sudo' if sudo
83
+ cmd << 'rpm -q'
84
+ repos.each { |r| cmd << r }
85
+ cmd.join(' ')
86
+ end
87
+
88
+ def package_install_cmd(packages, sudo: true)
89
+ cmd = []
90
+ cmd << 'sudo' if sudo
91
+ cmd << "#{package_manager} install #{package_install_cmd_opts}"
92
+ packages.each { |p| cmd << p }
93
+ cmd << package_install_cmd_output_format
94
+ cmd.join(' ')
95
+ end
96
+
97
+ def package_install_verify_cmd(packages, sudo: true)
98
+ cmd = []
99
+ cmd << 'sudo' if sudo
100
+ cmd << "#{package_manager} list installed"
101
+ packages.each { |p| cmd << p }
102
+ cmd.join(' ')
103
+ end
104
+
105
+ def package_install_cmd_opts
106
+ '-y'
107
+ end
108
+
109
+ def package_install_cmd_output_format
110
+ '| tr "\n" "#" | sed -e \'s/# / /g\' | tr "#" "\n"'
111
+ end
112
+
113
+ def service_start_cmd(services, sudo: true)
114
+ cmd = []
115
+ cmd << 'sudo' if sudo
116
+ cmd << 'systemctl start'
117
+ services.each { |s| cmd << s }
118
+ cmd.join(' ')
119
+ end
120
+
121
+ def service_start_verify_existance_cmd(services, sudo: true)
122
+ cmd = []
123
+ cmd << 'sudo' if sudo
124
+ cmd << 'systemctl list-units --type=service'
125
+ services.each { |s| cmd << s }
126
+ cmd.join(' ')
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Currently unused in this implementation.
4
+ module CemAcpt::Bootstrap::OperatingSystem
5
+ class Error < StandardError; end
6
+
7
+ # Currently unused in this implementation.
8
+ def use_os(os)
9
+ case os
10
+ when %r{^(centos|rhel)$}
11
+ require_relative 'operating_system/rhel_family'
12
+ self.class.include CemAcpt::Bootstrap::OperatingSystem::RhelFamily
13
+ else
14
+ raise Error, "Operating system #{os} is not supported"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bootstrap provides a method for bootstrapping test nodes.
4
+ # Currently unused in this implementation.
5
+ module CemAcpt::Bootstrap
6
+ require_relative 'bootstrap/bootstrapper'
7
+
8
+ def self.run(instance_name, instance_image, cmd_provider, collection: 'puppet7', repos: [], packages: [], services: [], commands: [], cmd_provider_args: {})
9
+ bootstrapper = CemAcpt::Bootstrap::Bootstrapper.new(instance_name, instance_image, cmd_provider, collection: 'puppet7', repos: [], packages: [], services: [], commands: [], cmd_provider_args: {})
10
+ bootstrapper.run
11
+ end
12
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'platform'
4
+ require_relative 'utils'
5
+ require_relative 'shared_objects'
6
+ require_relative 'test_data'
7
+ require_relative 'test_runner/run_handler'
8
+ require_relative 'logging'
9
+
10
+ module CemAcpt
11
+ # Context provides the context in which the RunHandler creates and starts Runners.
12
+ module Context
13
+ class ContextError < StandardError; end
14
+
15
+ class << self
16
+ include CemAcpt::Logging
17
+
18
+ KEY_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_key']).freeze
19
+ KH_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_known_hosts']).freeze
20
+
21
+ def log(msg, level = :info)
22
+ real_msg = "CONTEXT: #{msg}"
23
+ logger.send(level, real_msg)
24
+ end
25
+
26
+ # Builds the Puppet module package
27
+ # @param opts [Hash] config opts
28
+ # @return [String] The path to the Puppet module package
29
+ def build_module_package(opts = {})
30
+ module_dir = opts[:module_dir] || __dir__
31
+ pkg_path = CemAcpt::Utils::Puppet.build_module_package(module_dir)
32
+ log("Module package built at #{pkg_path}")
33
+ pkg_path
34
+ end
35
+
36
+ # Creates a SSH key and a SSH known hosts file for the acceptance test suite
37
+ def new_test_ssh_key
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...')
43
+ log("SSH private key: #{@ssh_priv_key}", :debug)
44
+ log("SSH public key: #{@ssh_pub_key}", :debug)
45
+ log("SSH known hosts: #{@ssh_known_hosts}", :debug)
46
+ end
47
+
48
+ # Deletes acceptance test suite SSH files
49
+ def clean_test_ssh_key
50
+ 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) }
52
+ end
53
+
54
+ # Prints a period to the terminal every 5 seconds in a single line to keep the terminal
55
+ # alive. This is used when running in CI mode. Does nothing unless the option `:CI` is
56
+ # `true`, or the environment variables CI or GITHUB_ACTION are set to a truthy value.
57
+ # @param opts [Hash] config opts
58
+ def keep_terminal_alive(opts = {})
59
+ @keep_terminal_alive = if opts[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']
60
+ CemAcpt::Utils::Terminal.keep_terminal_alive
61
+ end
62
+ end
63
+
64
+ def clean_up_test_suite(opts)
65
+ @ctx.node_inventory.clear!
66
+ @ctx.node_inventory.clean_local_files
67
+ clean_test_ssh_key unless opts[:no_ephemeral_ssh_key]
68
+ @run_handler&.destroy_test_nodes
69
+ @keep_terminal_alive&.kill
70
+ end
71
+ end
72
+
73
+ # Creates a context (CemAcpt::Context::Ctx) object for the RunHandler to create and start Runners.
74
+ # Provides the following objects for the Runners: a config object,
75
+ # the test data hash, the node inventory, and the local port allocator.
76
+ # Additionally, it creates the platform-specific node objects for each
77
+ # test suite in the test data. It then calls the provided block with
78
+ # the context objects nodes, config, test_data, and node_inventory.
79
+ # @param config_opts [Hash] the config options
80
+ def self.with(**opts)
81
+ @opts = opts
82
+ @start_time = Time.now
83
+ raise CemAcpt::Context::ContextError, 'CemAcpt::Context.with requires a block' unless block_given?
84
+
85
+ config_file = @opts[:config_file] || File.expand_path('./cem_acpt_config.yaml')
86
+ logger.info("Running acceptance test suite at #{@start_time}")
87
+ logger.debug("Config opts: #{@opts}")
88
+ logger.debug("Config file: #{config_file}")
89
+ logger.info("Using module directory: #{@opts[:module_dir]}")
90
+ keep_terminal_alive(@opts)
91
+ Dir.chdir(opts[:module_dir]) do
92
+ new_test_ssh_key unless @opts[:no_ephemeral_ssh_key]
93
+ pkg_path = build_module_package(@opts)
94
+ @ctx = CemAcpt::Context::Ctx.new(opts: @opts, config_file: config_file, module_package_path: pkg_path)
95
+ logger.debug("Created Ctx object #{@ctx}")
96
+ @run_handler = CemAcpt::TestRunner::RunHandler.new(@ctx)
97
+ logger.debug("Created RunHandler object #{@run_handler}")
98
+ yield @run_handler
99
+ end
100
+ @exit_code = @run_handler.exit_code
101
+ rescue StandardError => e
102
+ logger.fatal("Acceptance test suite encountered an error: #{e.message}")
103
+ logger.fatal(e.backtrace.join("\n"))
104
+ @exit_code = 1
105
+ ensure
106
+ clean_up_test_suite(@opts)
107
+ total_minutes = ((Time.now - @start_time) / 60).round
108
+ logger.info("Test suite finished in ~#{total_minutes} minutes")
109
+ @exit_code || 1
110
+ end
111
+
112
+ # Ctx holds the context objects for the RunHandler to create and start Runners.
113
+ class Ctx
114
+ attr_reader :config, :test_data, :module_package_path, :node_inventory, :local_port_allocator
115
+
116
+ def initialize(opts: nil, config_file: nil, module_package_path: nil)
117
+ @config = CemAcpt::Config.new.load(opts: opts, config_file: config_file)
118
+ @test_data = CemAcpt::TestData.acceptance_test_data(@config)
119
+ @module_package_path = module_package_path
120
+ @node_inventory = CemAcpt::NodeInventory.new
121
+ @local_port_allocator = CemAcpt::LocalPortAllocator.new
122
+ prep_environment
123
+ end
124
+
125
+ def nodes
126
+ return @nodes if defined?(@nodes)
127
+
128
+ raise CemAcpt::Error, 'No platform(s) specified' unless @config.has?('platform') || @config.has?('platforms')
129
+
130
+ @nodes = nodes_from_platforms
131
+ @nodes
132
+ end
133
+
134
+ private
135
+
136
+ def prep_environment
137
+ @node_inventory.clean_local_files
138
+ end
139
+
140
+ def nodes_from_platforms
141
+ nodes = {}
142
+ if @config.has?('platform')
143
+ nodes[@config.get('platform')] = CemAcpt::Platform.use(@config.get('platform'), @config, @test_data, @local_port_allocator)
144
+ elsif @config.has?('platforms')
145
+ config.get('platforms').each do |platform|
146
+ nodes[platform] = CemAcpt::Platform.use(platform, @config, @test_data, @local_port_allocator)
147
+ end
148
+ end
149
+ nodes
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module holds extensions to Ruby and the Ruby stdlib
4
+ # Extensions related to deep_freeze were pulled from: https://gist.github.com/steakknife/1a37057b3b8539f4aca3
5
+ module CemAcpt::CoreExtensions
6
+ # DeepFreeze recursively freezes all keys and values in a hash
7
+ # Currently unused, but was used at one point and may be useful again
8
+ module DeepFreeze
9
+ # Holds deep_freeze extensions to Kernel
10
+ module Kernel
11
+ alias deep_freeze freeze
12
+ alias deep_frozen? frozen?
13
+ end
14
+
15
+ # Holds deep_freeze extensions to Enumerable
16
+ module Enumerable
17
+ def deep_freeze
18
+ unless @deep_frozen
19
+ each(&:deep_freeze)
20
+ @deep_frozen = true
21
+ end
22
+ freeze
23
+ end
24
+
25
+ def deep_frozen?
26
+ !!@deep_frozen
27
+ end
28
+ end
29
+
30
+ # Holds deep_freeze extensions to Hash
31
+ module Hash
32
+ def deep_freeze
33
+ transform_values! do |value|
34
+ value.respond_to?(:deep_freeze) ? value.deep_freeze : value.freeze
35
+ end
36
+ freeze
37
+ @deep_frozen = true
38
+ end
39
+
40
+ def deep_frozen?
41
+ !!@deep_frozen
42
+ end
43
+ end
44
+
45
+ # Holds deep_freeze extensions to OpenStruct
46
+ module OpenStruct
47
+ def deep_freeze
48
+ unless deep_frozen?
49
+ @table.reduce({}) do |h, (key, value)|
50
+ fkey = key.respond_to?(:deep_freeze) ? key.deep_freeze : key
51
+ fval = value.respond_to?(:deep_freeze) ? value.deep_freeze : value
52
+ h.merge(fkey => fval)
53
+ end.freeze
54
+ @deep_frozen = true
55
+ end
56
+ end
57
+
58
+ def deep_frozen?
59
+ !!@deep_frozen
60
+ end
61
+ end
62
+ end
63
+
64
+ # Refines the Hash class with some convenience methods.
65
+ # Must call `using CemAcpt::CoreExtensions::HashExtensions`
66
+ # before these methods will be available.
67
+ module ExtendedHash
68
+ refine Hash do
69
+ # Formats a hash by converting all keys to symbols.
70
+ # If any value is a hash, it will be recursively
71
+ # extend and formatted.
72
+ def format!
73
+ transform_keys!(&:to_sym)
74
+ transform_values! do |value|
75
+ if value.is_a?(Hash)
76
+ value.format!
77
+ else
78
+ value
79
+ end
80
+ end
81
+ end
82
+
83
+ def has?(path)
84
+ !!dot_dig(path)
85
+ end
86
+
87
+ # Digs into a Hash using a dot-separated path.
88
+ # If the path is not found, returns nil.
89
+ # Example:
90
+ # hash = {a: {b: {c: 1}}}
91
+ # hash.dot_dig('a.b.c') # => 1
92
+ def dot_dig(path)
93
+ dig(*path.split('.').map(&:to_sym)) || dig(*path.split('.'))
94
+ end
95
+
96
+ # Stores a value in a nested Hash using a dot-separated path
97
+ # to dig through keys.
98
+ # Example:
99
+ # hash = {a: {b: {c: 1}}}
100
+ # hash.dot_store('a.b.c', 2)
101
+ # hash #=> {a: {b: {c: 2}}}
102
+ def dot_store(path, value)
103
+ *key, last = path.split('.').map(&:to_sym)
104
+ key.inject(self, :fetch)[last] = value
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ require_relative 'core_extensions'
5
+ require_relative 'logging'
6
+
7
+ # Dynamically builds an image name based on parameters specified in the
8
+ # config. The config is expected to have a key 'image_name_builder' with
9
+ # the following options:
10
+ # - 'parts' - (Required) An array of strings to be joined together to form the image name.
11
+ # If the strings begin with a '$', they will be replaced with the corresponding
12
+ # value from current test data. To specify nested keys, use '.' to separate
13
+ # the keys. Example: '$name.pattern.framework' will be replaced with the
14
+ # value of the name pattern key framework in the current test data.
15
+ # - 'join_with' - (Optional) The string to join the parts with. Defaults to ''.
16
+ # - 'character_substitutions' - (Optional) An array of of 2-item arrays. The first item
17
+ # is the character to replace, the second is the replacement.
18
+ # Example: [[' ', '-'], ['_', '-']] will replace all spaces
19
+ # and underscores with dashes.
20
+ # - 'validation_pattern' - (Optional) A regex pattern to validate the image name against.
21
+ class ImageNameBuilder
22
+ include CemAcpt::Logging
23
+ using CemAcpt::CoreExtensions::ExtendedHash
24
+
25
+ # Initializes the ImageNameBuilder.
26
+ # @param config [CemAcpt::Config] The config to use.
27
+ def initialize(config)
28
+ unless config.has?('image_name_builder')
29
+ raise ArgumentError, 'Configuration does not have an image_name_builder key'
30
+ end
31
+
32
+ @config = config.get('image_name_builder')
33
+ end
34
+
35
+ # Builds an image name based on the config. It does so in three steps:
36
+ # 1. Resolve variables in the parts array.
37
+ # 2. Join the parts together with the join_with string if specified
38
+ # and validate the image name against the validation_pattern if
39
+ # specified.
40
+ # 3. Perform any specified character substitutions on the image name.
41
+ # @param test_data [Hash] The test data to use to build the image name.
42
+ # @return [String] The image name.
43
+ def build(test_data)
44
+ logger.debug 'Building image name...'
45
+ logger.debug "Using config: #{@config.to_h}"
46
+ logger.debug "Test data: #{test_data}"
47
+ parts = resolve_parts(test_data)
48
+ logger.debug "Resolved parts: #{parts}"
49
+ image_name = create_image_name(parts)
50
+ logger.debug "Created image name: #{image_name}"
51
+ final_image_name = character_substitutions(image_name)
52
+ logger.debug "Final image name: #{final_image_name}"
53
+ final_image_name
54
+ end
55
+
56
+ private
57
+
58
+ # Resolves variables in the parts array by replacing them with the
59
+ # corresponding value from the test data.
60
+ # @param test_data [Hash] The test data to use to build the image name.
61
+ # @return [Array] The parts array with variables resolved.
62
+ def resolve_parts(test_data)
63
+ @config[:parts].each_with_object([]) do |part, ary|
64
+ logger.debug "Resolving part: #{part}"
65
+ if part.start_with?('$')
66
+ var_path = part[1..-1]
67
+ logger.debug "Resolving variable path: #{var_path}"
68
+ ary << test_data.dot_dig(var_path)
69
+ else
70
+ ary << part
71
+ end
72
+ end
73
+ end
74
+
75
+ # Creates an image name based on the parts array.
76
+ # @param parts [Array] The parts array to use to build the image name.
77
+ # @return [String] The image name.
78
+ def create_image_name(parts)
79
+ image_name = @config[:join_with] ? parts.join(@config[:join_with]) : parts.join
80
+ logger.debug("Final image name: #{image_name}")
81
+
82
+ if @config[:validation_pattern]
83
+ logger.debug "Validating image name: #{image_name}..."
84
+ return image_name if image_name.match?(@config[:validation_pattern])
85
+
86
+ raise "Image name #{image_name} does not match validation pattern #{@config[:validation_pattern]}"
87
+ end
88
+ image_name
89
+ end
90
+
91
+ # Performs character substitutions on the image name.
92
+ # @param name [String] The image name to perform substitutions on.
93
+ # @return [String] The image name with substitutions performed.
94
+ def character_substitutions(name)
95
+ return name unless @config[:character_substitutions]
96
+
97
+ subbed_name = name
98
+ @config[:character_substitutions].each do |char_sub|
99
+ subbed_name.gsub!(char_sub[0], char_sub[1])
100
+ end
101
+ subbed_name
102
+ end
103
+ end
104
+ end