cem_acpt 0.2.6-universal-java-17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/CODEOWNERS +1 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +93 -0
  7. data/README.md +150 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/cem_acpt.gemspec +39 -0
  12. data/exe/cem_acpt +84 -0
  13. data/lib/cem_acpt/bootstrap/bootstrapper.rb +206 -0
  14. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +129 -0
  15. data/lib/cem_acpt/bootstrap/operating_system.rb +17 -0
  16. data/lib/cem_acpt/bootstrap.rb +12 -0
  17. data/lib/cem_acpt/context.rb +153 -0
  18. data/lib/cem_acpt/core_extensions.rb +108 -0
  19. data/lib/cem_acpt/image_name_builder.rb +104 -0
  20. data/lib/cem_acpt/logging.rb +351 -0
  21. data/lib/cem_acpt/platform/base/cmd.rb +71 -0
  22. data/lib/cem_acpt/platform/base.rb +78 -0
  23. data/lib/cem_acpt/platform/gcp/cmd.rb +345 -0
  24. data/lib/cem_acpt/platform/gcp/compute.rb +332 -0
  25. data/lib/cem_acpt/platform/gcp.rb +85 -0
  26. data/lib/cem_acpt/platform/vmpooler.rb +24 -0
  27. data/lib/cem_acpt/platform.rb +103 -0
  28. data/lib/cem_acpt/puppet_helpers.rb +39 -0
  29. data/lib/cem_acpt/rspec_utils.rb +242 -0
  30. data/lib/cem_acpt/shared_objects.rb +537 -0
  31. data/lib/cem_acpt/spec_helper_acceptance.rb +184 -0
  32. data/lib/cem_acpt/test_data.rb +146 -0
  33. data/lib/cem_acpt/test_runner/run_handler.rb +187 -0
  34. data/lib/cem_acpt/test_runner/runner.rb +210 -0
  35. data/lib/cem_acpt/test_runner/runner_result.rb +103 -0
  36. data/lib/cem_acpt/test_runner.rb +10 -0
  37. data/lib/cem_acpt/utils.rb +144 -0
  38. data/lib/cem_acpt/version.rb +5 -0
  39. data/lib/cem_acpt.rb +34 -0
  40. data/sample_config.yaml +58 -0
  41. metadata +218 -0
@@ -0,0 +1,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