cem_acpt 0.6.5 → 0.7.1

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.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module CemAcpt
6
+ module Config
7
+ # Holds the configuration for cem_acpt
8
+ class CemAcpt < Base
9
+ VALID_KEYS = %i[
10
+ actions
11
+ ci_mode
12
+ config_file
13
+ image_name_builder
14
+ log_level
15
+ log_file
16
+ log_format
17
+ module_dir
18
+ node_data
19
+ no_destroy_nodes
20
+ no_ephemeral_ssh_key
21
+ platform
22
+ provisioner
23
+ quiet
24
+ terraform
25
+ test_data
26
+ tests
27
+ user_config
28
+ verbose
29
+ ].freeze
30
+
31
+ def valid_keys
32
+ VALID_KEYS
33
+ end
34
+
35
+ # The default configuration
36
+ def defaults
37
+ {
38
+ actions: {},
39
+ ci_mode: false,
40
+ config_file: nil,
41
+ image_name_builder: {
42
+ character_substitutions: ['_', '-'],
43
+ parts: ['cem-acpt', '$image_fam', '$collection', '$firewall'],
44
+ join_with: '-',
45
+ },
46
+ log_level: 'info',
47
+ log_file: nil,
48
+ log_format: 'text',
49
+ module_dir: Dir.pwd,
50
+ node_data: {},
51
+ no_ephemeral_ssh_key: false,
52
+ platform: {
53
+ name: 'gcp',
54
+ },
55
+ quiet: false,
56
+ test_data: {
57
+ for_each: {
58
+ collection: %w[puppet7],
59
+ },
60
+ vars: {},
61
+ name_pattern_vars: %r{^(?<framework>[a-z]+)_(?<image_fam>[a-z0-9-]+)_(?<firewall>[a-z]+)_(?<framework_vars>[-_a-z0-9]+)$},
62
+ vars_post_processing: {
63
+ new_vars: [
64
+ {
65
+ name: 'profile',
66
+ string_split: {
67
+ from: 'framework_vars',
68
+ using: '_',
69
+ part: 0,
70
+ },
71
+ },
72
+ {
73
+ name: 'level',
74
+ string_split: {
75
+ from: 'framework_vars',
76
+ using: '_',
77
+ part: 1,
78
+ },
79
+ },
80
+ ],
81
+ delete_vars: %w[framework_vars],
82
+ },
83
+ },
84
+ tests: [],
85
+ verbose: false,
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module CemAcpt
6
+ module Config
7
+ # Holds the configuration for cem_acpt_image
8
+ class CemAcptImage < Base
9
+ VALID_KEYS = %i[
10
+ ci_mode
11
+ config_file
12
+ images
13
+ log_level
14
+ log_file
15
+ log_format
16
+ no_destroy_nodes
17
+ no_ephemeral_ssh_key
18
+ platform
19
+ provisioner
20
+ quiet
21
+ terraform
22
+ user_config
23
+ verbose
24
+ ].freeze
25
+
26
+ def valid_keys
27
+ VALID_KEYS
28
+ end
29
+
30
+ def env_var_prefix
31
+ 'CEM_ACPT_IMAGE'
32
+ end
33
+
34
+ # The default configuration
35
+ def defaults
36
+ {
37
+ ci_mode: false,
38
+ config_file: nil,
39
+ images: {},
40
+ log_level: 'info',
41
+ log_file: nil,
42
+ log_format: 'text',
43
+ no_ephemeral_ssh_key: false,
44
+ platform: {
45
+ name: 'gcp',
46
+ },
47
+ quiet: false,
48
+ verbose: false,
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,381 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
4
- require 'json'
5
- require 'yaml'
6
- require_relative 'core_extensions'
7
-
3
+ # Module contains the CemAcptConfig::Base class which serves as a base class for different configs.
8
4
  module CemAcpt
9
- using CemAcpt::CoreExtensions::ExtendedHash
10
-
11
- # Holds the configuration for cem_acpt
12
- class Config
13
- KEYS = %i[
14
- actions
15
- ci_mode
16
- config_file
17
- image_name_builder
18
- log_level
19
- log_file
20
- log_format
21
- module_dir
22
- node_data
23
- no_destroy_nodes
24
- no_ephemeral_ssh_key
25
- platform
26
- provisioner
27
- quiet
28
- terraform
29
- test_data
30
- tests
31
- user_config
32
- verbose
33
- ].freeze
34
-
35
- attr_reader :config, :env_vars
36
-
37
- def initialize(opts: {}, config_file: nil, load_user_config: true)
38
- @load_user_config = load_user_config
39
- load(opts: opts, config_file: config_file)
40
- end
41
-
42
- # The default configuration
43
- def defaults
44
- {
45
- actions: {},
46
- ci_mode: false,
47
- config_file: nil,
48
- image_name_builder: {
49
- character_substitutions: ['_', '-'],
50
- parts: ['cem-acpt', '$image_fam', '$collection', '$firewall'],
51
- join_with: '-',
52
- },
53
- log_level: 'info',
54
- log_file: nil,
55
- log_format: 'text',
56
- module_dir: Dir.pwd,
57
- node_data: {},
58
- no_ephemeral_ssh_key: false,
59
- platform: {
60
- name: 'gcp',
61
- },
62
- quiet: false,
63
- test_data: {
64
- for_each: {
65
- collection: %w[puppet7],
66
- },
67
- vars: {},
68
- name_pattern_vars: %r{^(?<framework>[a-z]+)_(?<image_fam>[a-z0-9-]+)_(?<firewall>[a-z]+)_(?<framework_vars>[-_a-z0-9]+)$},
69
- vars_post_processing: {
70
- new_vars: [
71
- {
72
- name: 'profile',
73
- string_split: {
74
- from: 'framework_vars',
75
- using: '_',
76
- part: 0,
77
- },
78
- },
79
- {
80
- name: 'level',
81
- string_split: {
82
- from: 'framework_vars',
83
- using: '_',
84
- part: 1,
85
- },
86
- },
87
- ],
88
- delete_vars: %w[framework_vars],
89
- },
90
- },
91
- tests: [],
92
- verbose: false,
93
- }
94
- end
95
-
96
- # Load the configuration from the environment variables, config file, and opts
97
- # The order of precedence is:
98
- # 1. environment variables
99
- # 2. user config file (config.yaml in user_config_dir)
100
- # 3. specified config file (if it exists)
101
- # 4. opts
102
- # 5. static options (set in this class)
103
- # @param opts [Hash] The options to load
104
- # @param config_file [String] The config file to load
105
- # @return [self] This object with the config loaded
106
- def load(opts: {}, config_file: nil)
107
- create_config_dirs!
108
- init_config!(opts: opts, config_file: config_file)
109
- add_env_vars!(@config)
110
- @config.merge!(user_config) if user_config && @load_user_config
111
- @config.merge!(config_from_file) if config_from_file
112
- @config.merge!(@options) if @options
113
- add_static_options!(@config)
114
- @config.format! # Symbolize keys of all hashes
115
- validate_config!
116
- # Freeze the config so it can't be modified
117
- # This helps with thread safety and deterministic behavior
118
- @config.freeze
119
- self
120
- end
121
- alias to_h config
122
-
123
- def explain
124
- explanation = {}
125
- %i[defaults env_vars user_config config_from_file options].each do |source|
126
- source_vals = send(source).dup
127
- next if source_vals.nil? || source_vals.empty?
128
-
129
- # The loop below will overwrite the value of explanation[key] if the same key is found in multiple sources
130
- # This is intentional, as the last source to set the value is the one that should be used
131
- source_vals.each do |key, value|
132
- explanation[key] = source if @config.dget(key.to_s) == value
133
- end
134
- end
135
- explained = explanation.each_with_object([]) do |(key, value), ary|
136
- ary << "Key '#{key}' from source '#{value}'"
137
- end
138
- explained.join("\n")
139
- end
140
-
141
- def [](key)
142
- if key.is_a?(Symbol)
143
- @config[key].dup
144
- elsif key.is_a?(String)
145
- @config.dget(key).dup
146
- else
147
- raise ArgumentError, "Invalid key type '#{key.class}'"
148
- end
149
- end
150
-
151
- def get(dot_key)
152
- @config.dget(dot_key).dup
153
- end
154
- alias dget get
155
-
156
- def has?(dot_key)
157
- !!get(dot_key)
158
- end
159
-
160
- def empty?
161
- @config.empty?
162
- end
163
-
164
- def ci_mode?
165
- !!get('ci_mode') || !!(ENV['GITHUB_ACTIONS'] || ENV['CI'])
166
- end
167
- alias ci? ci_mode?
168
-
169
- def debug_mode?
170
- get('log_level') == 'debug'
171
- end
172
- alias debug? debug_mode?
173
-
174
- def verbose_mode?
175
- !!get('verbose')
176
- end
177
- alias verbose? verbose_mode?
178
-
179
- def quiet_mode?
180
- !!get('quiet')
181
- end
182
- alias quiet? quiet_mode?
183
-
184
- def to_yaml
185
- @config.to_yaml
186
- end
187
-
188
- def to_json(*args)
189
- @config.to_json(*args)
190
- end
191
-
192
- private
193
-
194
- attr_reader :options
195
-
196
- def user_config_dir
197
- @user_config_dir ||= File.join(Dir.home, '.cem_acpt')
198
- end
199
-
200
- def user_config_file
201
- @user_config_file ||= File.join(user_config_dir, 'config.yaml')
202
- end
203
-
204
- def terraform_dir
205
- @terraform_dir ||= File.join(user_config_dir, 'terraform')
206
- end
207
-
208
- def module_terraform_checksum_file
209
- @module_terraform_checksum_file ||= File.join(user_config_dir, 'terraform_checksum.txt')
210
- end
211
-
212
- def module_terraform_checksum
213
- @module_terraform_checksum ||= new_module_terraform_checksum
214
- end
215
-
216
- def valid_env_var?(env_var)
217
- env_var.start_with?('CEM_ACPT_') && ENV[env_var]
218
- end
219
-
220
- def env_var_to_dot_key(env_var)
221
- env_var.sub('CEM_ACPT_', '').gsub(%r{__}, '.').downcase
222
- end
223
-
224
- def add_static_options!(config)
225
- config.dset('user_config.dir', user_config_dir)
226
- config.dset('user_config.file', user_config_file)
227
- config.dset('provisioner', 'terraform')
228
- config.dset('terraform.dir', terraform_dir)
229
- set_third_party_env_vars!(config)
230
- end
231
-
232
- # Certain environment variables are used by other tools like GitHub Actions
233
- # This method sets the relative config values for those environment variables.
234
- # This is the last step in composing the config, so it will override any
235
- # values set by the user.
236
- def set_third_party_env_vars!(config)
237
- if ENV['RUNNER_DEBUG'] == '1'
238
- config.dset('log_level', 'debug')
239
- config.dset('verbose', true)
240
- end
241
- if ENV['GITHUB_ACTIONS'] == 'true' || ENV['CI'] == 'true'
242
- config.dset('ci_mode', true)
243
- end
244
- end
245
-
246
- # Used to source the config during loading of config files.
247
- # Because config may not be fully loaded yet, this method
248
- # checks the options hash first, then the current config,
249
- # then the environment variables for the given key
250
- # @param key [String] The key to find in dot notation
251
- # @return [Any] The value of the key
252
- def find_option(key)
253
- return @options.dget(key) if @options.dget(key)
254
- return @config.dget(key) if @config.dget(key)
255
- ENV.each do |k, v|
256
- next unless valid_env_var?(k)
257
-
258
- return v if env_var_to_dot_key(k) == key
259
- end
260
- nil
261
- end
262
-
263
- def init_config!(opts: {}, config_file: nil)
264
- # Blank out the config
265
- @user_config = {}
266
- @config_from_file = {}
267
- @options = {}
268
- @config = defaults.dup
269
- # Set the parameterized defaults
270
- config_file = ENV['CEM_ACPT_CONFIG_FILE'] if config_file.nil?
271
- @config.dset('config_file', config_file) if config_file
272
- @options = opts || {}
273
- end
274
-
275
- def add_env_vars!(config)
276
- @env_vars = {}
277
- # First load known environment variables into their respective config keys
278
- # Then load any environment variables that start with CEM_ACPT_<known key> into their respective config keys
279
- ENV.each do |env_var, value|
280
- next unless valid_env_var?(env_var)
281
-
282
- key = env_var_to_dot_key(env_var) # Convert CEM_ACPT_<key> to <dotkey>
283
- next unless KEYS.include?(key.split('.').first.to_sym) # Skip if the key is not a known config key
284
- @env_vars[key] = value
285
- config.dset(key, value)
286
- end
287
- end
288
-
289
- def user_config
290
- return @user_config unless @user_config.nil? || @user_config.empty?
291
-
292
- @user_config = if user_config_file && File.exist?(user_config_file)
293
- load_config_file(user_config_file)
294
- else
295
- {}
296
- end
297
-
298
- @user_config
299
- end
300
-
301
- def config_from_file
302
- return @config_from_file unless @config_from_file.nil? || @config_from_file.empty?
303
-
304
- conf_file = find_option('config_file')
305
- return {} if conf_file.nil? || conf_file.empty?
306
-
307
- unless conf_file
308
- warn "Invalid config_file type '#{conf_file.class}'. Must be a String."
309
- return {}
310
- end
311
-
312
- mod_dir = find_option('module_dir')
313
- if mod_dir && File.exist?(File.join(mod_dir, conf_file))
314
- @config_from_file = load_config_file(File.join(mod_dir, conf_file))
315
- elsif File.exist?(File.expand_path(conf_file))
316
- @config_from_file = load_config_file(File.expand_path(conf_file))
317
- else
318
- err_msg = [
319
- "Config file '#{File.expand_path(conf_file)}' does not exist.",
320
- ]
321
- err_msg << "Config file '#{File.join(mod_dir, conf_file)}' does not exist." if mod_dir
322
- raise err_msg.join("\n")
323
- end
324
-
325
- @config_from_file
326
- end
327
-
328
- def load_config_file(config_file)
329
- return {} if config_file.nil? || config_file.empty? || !File.exist?(File.expand_path(config_file))
330
-
331
- loaded = load_yaml(config_file)
332
- loaded.format!
333
- loaded
334
- end
335
-
336
- def load_yaml(config_file)
337
- if YAML.respond_to?(:safe_load_file) # Ruby 3.0+
338
- YAML.safe_load_file(File.expand_path(config_file), permitted_classes: [Regexp])
339
- else
340
- YAML.safe_load(File.read(File.expand_path(config_file)), permitted_classes: [Regexp])
341
- end
342
- end
343
-
344
- def validate_config!
345
- @config.each do |key, _value|
346
- warn "Unknown config key: #{key}" unless KEYS.include?(key)
347
- end
348
- end
349
-
350
- def create_config_dirs!
351
- FileUtils.mkdir_p(user_config_dir)
352
- create_terraform_dir!
353
- end
354
-
355
- def create_terraform_dir!
356
- raise 'Cannot create terraform dir without a user config dir' unless Dir.exist? user_config_dir
357
-
358
- if File.exist?(module_terraform_checksum_file)
359
- checksum = File.read(module_terraform_checksum_file).strip
360
- return if checksum == module_terraform_checksum
361
- end
362
- FileUtils.rm_rf(File.join(user_config_dir, 'terraform'))
363
- FileUtils.cp_r(File.expand_path(File.join(__dir__, '..', 'terraform')), user_config_dir)
364
- @module_terraform_checksum = new_module_terraform_checksum
365
- File.write(module_terraform_checksum_file, module_terraform_checksum)
366
- end
367
-
368
- def new_module_terraform_checksum
369
- sha256 = Digest::SHA256.new
370
- files_and_dirs = Dir.glob(File.join(__dir__, '..', 'terraform', '**', '*'))
371
- files_and_dirs.each do |file|
372
- sha256 << if File.directory?(file)
373
- File.basename(file)
374
- else
375
- File.read(file)
376
- end
377
- end
378
- sha256.hexdigest
379
- end
5
+ module Config
6
+ require_relative 'config/cem_acpt'
7
+ require_relative 'config/cem_acpt_image'
380
8
  end
381
9
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # This module holds extensions and refinements to Ruby and the Ruby stdlib
4
- module CemAcpt::CoreExtensions
4
+ module CemAcpt::CoreExt
5
5
  # Refines the Hash class with some convenience methods.
6
6
  # Must call `using CemAcpt::CoreExtensions::HashExtensions`
7
7
  # before these methods will be available.
@@ -15,6 +15,8 @@ module CemAcpt::CoreExtensions
15
15
  transform_values! do |value|
16
16
  if value.is_a?(Hash)
17
17
  value.format!
18
+ elsif value.is_a?(Array)
19
+ value.map { |v| v.is_a?(Hash) ? v.format! : v }
18
20
  else
19
21
  value
20
22
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require_relative '../logging'
6
+
7
+ module CemAcpt
8
+ module ImageBuilder
9
+ module Exec
10
+ def self.new_exec(config)
11
+ executor = config.get('exec') || 'gcloud'
12
+ case executor
13
+ when 'gcloud'
14
+ CemAcpt::ImageBuilder::Exec::Gcloud.new(config)
15
+ else
16
+ raise ArgumentError, "Unknown exec #{executor}"
17
+ end
18
+ end
19
+
20
+ class Gcloud
21
+ include CemAcpt::Logging
22
+
23
+ def initialize(config)
24
+ @config = config
25
+ verify_gcloud!
26
+ end
27
+
28
+ # Run a gcloud command
29
+ # @raise [RuntimeError] if the command fails
30
+ # @return [Hash] JSON output of the command
31
+ def run(*command)
32
+ formatted = format_command(*command)
33
+ logger.debug('Exec') { "Running command: #{formatted}" }
34
+ gcloud(formatted)
35
+ end
36
+
37
+ private
38
+
39
+ def verify_gcloud!
40
+ return if system('gcloud --version')
41
+
42
+ raise 'gcloud not found in PATH'
43
+ end
44
+
45
+ def format_command(*command)
46
+ command.unshift('gcloud')
47
+ command.push('--format=json')
48
+ command.join(' ')
49
+ end
50
+
51
+ def gcloud(formatted_command)
52
+ stdout, stderr, status = Open3.capture3(formatted_command)
53
+ raise "gcloud command failed: #{stderr}" unless status.success?
54
+
55
+ JSON.parse(stdout)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module ImageBuilder
5
+ # Holds methods for determining the provision commands for each supported OS / Puppet version
6
+ module ProvisionCommands
7
+ class EnterpriseLinuxFamily
8
+ def initialize(config, image_name, base_image, os_major_version, puppet_version)
9
+ @config = config
10
+ @image_name = image_name
11
+ @base_image = base_image
12
+ @os_major_version = os_major_version
13
+ @puppet_version = puppet_version
14
+ end
15
+
16
+ def default_provision_commands
17
+ [enable_puppet_repository_command, install_puppet_agent_command]
18
+ end
19
+
20
+ def provision_commands
21
+ commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
22
+ (default_provision_commands + commands_from_config).compact
23
+ end
24
+ alias to_a provision_commands
25
+
26
+ private
27
+
28
+ def package_manager
29
+ @package_manager ||= @os_major_version.to_i >= 8 ? 'dnf' : 'yum'
30
+ end
31
+
32
+ def package_manager_utils
33
+ @package_manager_utils ||= @os_major_version.to_i >= 8 ? 'dnf-utils' : 'yum-utils'
34
+ end
35
+
36
+ def upgrade_packages_command
37
+ "sudo #{package_manager} upgrade -y"
38
+ end
39
+
40
+ def install_package_manager_utils_command
41
+ "sudo #{package_manager} install -y #{package_manager_utils}"
42
+ end
43
+
44
+ def puppet_platform_repository_url
45
+ "https://yum.puppet.com/puppet#{@puppet_version}-release-el-#{@os_major_version}.noarch.rpm"
46
+ end
47
+
48
+ def enable_puppet_repository_command
49
+ "sudo rpm -U #{puppet_platform_repository_url}"
50
+ end
51
+
52
+ def install_puppet_agent_command
53
+ "sudo #{package_manager} install -y puppet-agent"
54
+ end
55
+ end
56
+
57
+ class WindowsFamily
58
+ def initialize(config, image_name, base_image, os_major_version, puppet_version)
59
+ @config = config
60
+ @image_name = image_name
61
+ @base_image = base_image
62
+ @os_major_version = os_major_version
63
+ @puppet_version = puppet_version
64
+ end
65
+
66
+ def default_provision_commands
67
+ []
68
+ end
69
+
70
+ def provision_commands
71
+ commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
72
+ (default_provision_commands + commands_from_config).compact
73
+ end
74
+ alias to_a provision_commands
75
+ end
76
+
77
+ class << self
78
+ # Map of OS to class that holds the provision commands for that OS
79
+ OS_CLASS_MAP = {
80
+ 'centos' => 'EnterpriseLinuxFamily',
81
+ 'rhel' => 'EnterpriseLinuxFamily',
82
+ 'alma' => 'EnterpriseLinuxFamily',
83
+ 'windows' => 'WindowsFamily',
84
+ }.freeze
85
+
86
+ def provision_commands(config, image_name:, base_image:, os:, os_major_version:, puppet_version:)
87
+ os_major_version = os_major_version.to_s
88
+ puppet_version = puppet_version.to_s
89
+ cmd_klass = OS_CLASS_MAP[os.to_s]
90
+ raise "Unsupported OS: #{os}" unless cmd_klass
91
+
92
+ const_get(cmd_klass).new(config, image_name, base_image, os_major_version, puppet_version).provision_commands
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end