cem_acpt 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,380 +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.cp_r(File.expand_path(File.join(__dir__, '..', 'terraform')), user_config_dir)
363
- @module_terraform_checksum = new_module_terraform_checksum
364
- File.write(module_terraform_checksum_file, module_terraform_checksum)
365
- end
366
-
367
- def new_module_terraform_checksum
368
- sha256 = Digest::SHA256.new
369
- files_and_dirs = Dir.glob(File.join(__dir__, '..', 'terraform', '**', '*'))
370
- files_and_dirs.each do |file|
371
- sha256 << if File.directory?(file)
372
- File.basename(file)
373
- else
374
- File.read(file)
375
- end
376
- end
377
- sha256.hexdigest
378
- end
5
+ module Config
6
+ require_relative 'config/cem_acpt'
7
+ require_relative 'config/cem_acpt_image'
379
8
  end
380
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