cem_acpt 0.6.5 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98336b152de85be1f1d7bfd20cd901da25d728d9ea120fb713becff384986560
4
- data.tar.gz: ab6e2579bb7df9e58a0c4ed290e40a64a4b1a0c6a6f00894b924694de915e65e
3
+ metadata.gz: f2539a6c12df56b28e4ede4dce5da549496fd1a8ebe7076da780b410bf7d5c58
4
+ data.tar.gz: 5fe4e01b0c307583fbe204dd893f05dce4361ab60c8d84ea9c5b5f11e806621f
5
5
  SHA512:
6
- metadata.gz: 3c353057f6cda43e7b2e2a6eacfba36cdd41c2c78e459f7548b3e3ddd789a1c732e143b008b62f7c4e8fbb11e823a3e69f16a9a810c3c81db86be77e45b4b89e
7
- data.tar.gz: c2cb83e57613ec0bed0292411e83a99705af80f0d205b699b52dd07ae49814d4adc8d235a9233c65b658d9652686b3dd302f5a1f45d0c19551f8c2811ef62ade
6
+ metadata.gz: 71d1fe8bf2935bded271ddf40fbb274d7c9748598dff84ff2ca5dd356c8ceb79137093ebefc33c8ee9270400b3afe2c4736578962af180a273dd9127600f542a
7
+ data.tar.gz: 1d09872c69652ac64e262c06474cc3fb08d0b37b6c548127fea91cd81f3b8e7fa3a79211adac934ae087f40113e5388f328cef17809c04e47f82d0d5fda0918f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cem_acpt (0.6.5)
4
+ cem_acpt (0.7.0)
5
5
  async-http (>= 0.60, < 0.70)
6
6
  bcrypt_pbkdf (>= 1.0, < 2.0)
7
7
  deep_merge (>= 1.2, < 2.0)
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CemAcpt is an acceptance testing library / command line application for running acceptance tests for the CEM modules.
4
4
 
5
- CemAcpt uses [Terraform](#terraform) for provisioning test nodes, and [Goss](#goss) to execute acceptance tests.
5
+ CemAcpt uses [Terraform](#terraform) for provisioning test nodes, and [Goss](#goss) to execute acceptance tests. For provisioning nodes in GCP, CemAcpt also uses the [gcloud CLI](https://cloud.google.com/cli).
6
6
 
7
7
  ## Installation
8
8
 
@@ -16,7 +16,7 @@ gem install cem_acpt
16
16
 
17
17
  ### Quickstart
18
18
 
19
- Make sure Terraform is installed and in your PATH. Instructions for installing Terraform can be found [here](https://learn.hashicorp.com/tutorials/terraform/install-cli).Then, navigate to the root of the `cem_linux` module and run the following command:
19
+ Make sure Terraform and the gcloud CLI are installed and in your PATH. Instructions for installing Terraform can be found [here](https://learn.hashicorp.com/tutorials/terraform/install-cli) and instructions for installing the gcloud CLI can be found [here](https://cloud.google.com/sdk/docs/install).Then, navigate to the root of the `cem_linux` module and run the following command:
20
20
 
21
21
  ```bash
22
22
  cem_acpt --config ./cem_acpt_config.yaml
@@ -26,6 +26,7 @@ cem_acpt --config ./cem_acpt_config.yaml
26
26
 
27
27
  ```bash
28
28
  cem_acpt -h | --help
29
+ cem_acpt_image -h | --help
29
30
  ```
30
31
 
31
32
  > If you do not delete Gemfile.lock before running `bundle install`, you may encounter dependency version errors. Just delete Gemfile.lock and run `bundle install` again to get past these.
@@ -209,3 +210,41 @@ Much like `name_pattern_vars`, specifying the `image_name_builder` top-level key
209
210
  - Once node is provisioned, make HTTP get requests to the Goss server endpoints to run the tests
210
211
  - Destroy the test nodes
211
212
  - Report the results of the tests
213
+
214
+ ## Generating acceptance test node images
215
+
216
+ CemAcpt provides the command `cem_acpt_image` that allows you to generate new acceptance test node images that are then used with the `cem_acpt` command. This is useful for when you want to test a new version of Puppet or a new OS.
217
+
218
+ ### Configuring the image builder
219
+
220
+ Images are built according to the entries in the `images` config key. Each entry in the `images` hash should follow the following format:
221
+
222
+ ```yaml
223
+ images:
224
+ <image family name>:
225
+ os: <os key string (rhel, alma, etc.)>
226
+ os_major_version: <os major version integer>
227
+ puppet_version: <puppet major version integer>
228
+ base_image: <base image name / family string>
229
+ provision_commands: <array of commands to run on the image>
230
+ ```
231
+
232
+ For example, the following config would build an image for Puppet 7 on RHEL 8:
233
+
234
+ ```yaml
235
+ images:
236
+ cem-acpt-rhel-8-puppet7-firewalld:
237
+ os: rhel
238
+ os_major_version: 8
239
+ puppet_version: 7
240
+ base_image: 'rhel-cloud/rhel-8'
241
+ provision_commands:
242
+ - 'systemctl enable firewalld'
243
+ - 'systemctl start firewalld'
244
+ - 'firewall-cmd --permanent --add-service=ssh'
245
+ - 'firewall-cmd --reload'
246
+ - 'useradd testuser1'
247
+ - "echo 'testuser1:P@s5W-rd$' | chpasswd"
248
+ ```
249
+
250
+ See [sample_config.yaml](sample_config.yaml) for a more complete example.
data/exe/cem_acpt CHANGED
@@ -1,118 +1,15 @@
1
- #!/usr/bin/env jruby
1
+ #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'deep_merge'
5
- require 'json'
6
- require 'optparse'
7
- require 'yaml'
8
4
  require 'cem_acpt'
5
+ require 'cem_acpt/cli'
9
6
 
10
- options = {}
11
- parser = OptionParser.new do |opts|
12
- opts.banner = 'Usage: cem_acpt [options]'
13
-
14
- opts.on('-h', '--help', 'Show this help message') do
15
- puts opts
16
- exit 0
17
- end
18
-
19
- opts.on('-a', '--only-actions ACTIONS', 'Set actions. Example: -a "acpt,noop"') do |a|
20
- options[:actions] ||= {}
21
- options[:actions][:only] = a.split(',')
22
- end
23
-
24
- opts.on('-A', '--except-actions ACTIONS', 'Set excluded actions. Example: -A "noop,idempotent"') do |a|
25
- options[:actions] ||= {}
26
- options[:actions][:except] = a.split(',')
27
- end
28
-
29
- opts.on('-t', '--tests TESTS', 'Set tests. Example: -t "test1,test2"') do |t|
30
- options[:tests] = t.split(',')
31
- end
32
-
33
- opts.on('-D', '--debug', 'Enable debug logging') do
34
- options[:log_level] = 'debug'
35
- end
36
-
37
- opts.on('-L', '--log-file FILE', 'Log to FILE') do |file|
38
- options[:log_file] = file
39
- end
40
-
41
- opts.on('-O', '--options OPTS', 'Set options. Example: -P "param1=value1,param2=value2"') do |o|
42
- params = o.split(',').map { |s| s.split('=') }.to_h
43
- params.transform_keys(&:to_sym).each do |k, v|
44
- options[k] = v
45
- end
46
- end
47
-
48
- opts.on('-p', '--platform PLATFORM', 'Set platform. Example: -p "gcp"') do |p|
49
- options[:platform] = p
50
- end
51
-
52
- opts.on('-m', '--module-dir DIR', 'Set module directory. Example: -m "/tmp/module"') do |m|
53
- options[:module_dir] = m
54
- end
55
-
56
- opts.on('-c', '--config-file FILE', 'Set config file. Example: -c "/tmp/config.yaml"') do |c|
57
- options[:config_file] = c
58
- end
59
-
60
- opts.on('-I', '--CI', 'Run in CI mode') do
61
- options[:ci_mode] = true
62
- options[:log_format] = 'github_action'
63
- end
64
-
65
- opts.on('-E', '--no-destroy-nodes', 'Do not destroy nodes') do
66
- options[:no_destroy_nodes] = true
67
- end
68
-
69
- opts.on('-q', '--quiet', 'Do not log to stdout') do
70
- options[:quiet] = true
71
- end
72
-
73
- opts.on('-v', '--verbose', 'Enables verbose logging mode') do
74
- options[:verbose] = true
75
- end
76
-
77
- opts.on('-S', '--no-epehemeral-ssh-key', 'Do not generate an ephemeral SSH key for test suites') do
78
- options[:no_ephemeral_ssh_key] = true
79
- end
80
-
81
- opts.on('-V', '--version', 'Show the cem_acpt version') do
82
- puts CemAcpt.version(as_str: true)
83
- exit 0
84
- end
85
-
86
- opts.on('-Y', '--print-yaml-config', 'Loads and prints the config as YAML. Other specified options will be added to the config.') do
87
- options[:print_yaml_config] = true
88
- end
89
-
90
- opts.on('-X', '--explain-config', 'Loads and prints the config explanation. Other specified options will be added to the config.') do
91
- options[:explain_config] = true
92
- end
93
-
94
- # NOT IMPLEMENTED
95
- # opts.on('-R', '--reuse-nodes', 'Reuses test nodes if compatible node inventory exists') do
96
- # options[:reuse_nodes] = true
97
- # end
98
- end
99
-
100
- parser.parse!
101
- if options[:print_yaml_config]
102
- options.delete(:print_yaml_config)
103
- puts CemAcpt.print_config(options, format: :yaml)
104
- exit 0
105
- end
106
- if options[:explain_config]
107
- options.delete(:explain_config)
108
- puts CemAcpt.print_config(options, format: :explain)
109
- exit 0
110
- end
7
+ command, options = CemAcpt::CLI.parse_opts_for(:cem_acpt)
111
8
  # Set CLI defaults
112
9
  options[:module_dir] = Dir.pwd unless options[:module_dir]
113
10
  if (options[:log_level] == 'debug' || options[:verbose]) && !options[:quiet]
114
- puts '#################### RUNNING ACCEPTANCE TEST SUITE ####################'
115
- puts "Using options from command line: #{options}"
11
+ puts '############################### RUNNING ###############################'
12
+ puts "Command #{File.basename(__FILE__)}:#{command} using options from command line: #{options}"
116
13
  puts '#######################################################################'
117
14
  end
118
- CemAcpt.run(options)
15
+ CemAcpt.run(command, File.basename(__FILE__).to_sym, options)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'cem_acpt'
5
+ require 'cem_acpt/cli'
6
+
7
+ command, options = CemAcpt::Cli.parse_opts_for(:cem_acpt_image)
8
+ # Set CLI defaults
9
+ options[:module_dir] = Dir.pwd unless options[:module_dir]
10
+ if (options[:log_level] == 'debug' || options[:verbose]) && !options[:quiet]
11
+ puts '############################### RUNNING ###############################'
12
+ puts "Command #{File.basename(__FILE__)}:#{command} using options from command line: #{options}"
13
+ puts '#######################################################################'
14
+ end
15
+ CemAcpt.run(command, File.basename(__FILE__).to_sym, options)
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module CemAcpt
6
+ module Cli
7
+ def self.parse_opts_for(command)
8
+ cmd = command
9
+ options = {}
10
+ parser = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{command} [options]"
12
+
13
+ opts.on('-h', '--help', 'Show this help message') do
14
+ puts opts
15
+ exit 0
16
+ end
17
+
18
+ # Command specific options
19
+ case command
20
+ when :cem_acpt
21
+ opts.on('-m', '--module-dir DIR', 'Set module directory. Example: -m "/tmp/module"') do |m|
22
+ options[:module_dir] = m
23
+ end
24
+
25
+ opts.on('-a', '--only-actions ACTIONS', 'Set actions. Example: -a "acpt,noop"') do |a|
26
+ options[:actions] ||= {}
27
+ options[:actions][:only] = a.split(',')
28
+ end
29
+
30
+ opts.on('-A', '--except-actions ACTIONS', 'Set excluded actions. Example: -A "noop,idempotent"') do |a|
31
+ options[:actions] ||= {}
32
+ options[:actions][:except] = a.split(',')
33
+ end
34
+
35
+ opts.on('-t', '--tests TESTS', 'Set tests. Example: -t "test1,test2"') do |t|
36
+ options[:tests] = t.split(',')
37
+ end
38
+ when :cem_acpt_image
39
+ opts.on('-U', '--no-linux', 'Do not build Linux images') do
40
+ options[:cem_acpt_image] ||= {}
41
+ options[:cem_acpt_image][:no_linux] = true
42
+ end
43
+
44
+ opts.on('-W', '--no-windows', 'Do not build Windows images') do
45
+ options[:cem_acpt_image] ||= {}
46
+ options[:cem_acpt_image][:no_windows] = true
47
+ end
48
+ end
49
+
50
+ opts.on('-D', '--debug', 'Enable debug logging') do
51
+ options[:log_level] = 'debug'
52
+ end
53
+
54
+ opts.on('-L', '--log-file FILE', 'Log to FILE') do |file|
55
+ options[:log_file] = file
56
+ end
57
+
58
+ opts.on('-O', '--options OPTS', 'Set options. Example: -P "param1=value1,param2=value2"') do |o|
59
+ params = o.split(',').map { |s| s.split('=') }.to_h
60
+ params.transform_keys(&:to_sym).each do |k, v|
61
+ options[k] = v
62
+ end
63
+ end
64
+
65
+ opts.on('-p', '--platform PLATFORM', 'Set platform. Example: -p "gcp"') do |p|
66
+ options[:platform] = p
67
+ end
68
+
69
+ opts.on('-c', '--config-file FILE', 'Set config file. Example: -c "/tmp/config.yaml"') do |c|
70
+ options[:config_file] = c
71
+ end
72
+
73
+ opts.on('-I', '--CI', 'Run in CI mode (modifies logging for Github Actions output)') do
74
+ options[:ci_mode] = true
75
+ options[:log_format] = 'github_action'
76
+ end
77
+
78
+ opts.on('-E', '--no-destroy-nodes', 'Do not destroy nodes') do
79
+ options[:no_destroy_nodes] = true
80
+ end
81
+
82
+ opts.on('-q', '--quiet', 'Do not log to stdout') do
83
+ options[:quiet] = true
84
+ end
85
+
86
+ opts.on('-v', '--verbose', 'Enables verbose logging mode') do
87
+ options[:verbose] = true
88
+ end
89
+
90
+ opts.on('-S', '--no-epehemeral-ssh-key', 'Do not generate an ephemeral SSH key for test suites') do
91
+ options[:no_ephemeral_ssh_key] = true
92
+ end
93
+
94
+ opts.on('-V', '--version', 'Show the cem_acpt version') do
95
+ cmd = :version
96
+ end
97
+
98
+ opts.on('-Y', '--print-yaml-config', 'Loads and prints the config as YAML. Other specified options will be added to the config.') do
99
+ cmd = :print_yaml_config
100
+ end
101
+
102
+ opts.on('-X', '--explain-config', 'Loads and prints the config explanation. Other specified options will be added to the config.') do
103
+ cmd = :print_explain_config
104
+ end
105
+
106
+ # NOT IMPLEMENTED
107
+ # opts.on('-R', '--reuse-nodes', 'Reuses test nodes if compatible node inventory exists') do
108
+ # options[:reuse_nodes] = true
109
+ # end
110
+ end
111
+ parser.parse!
112
+ [cmd, options]
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'yaml'
7
+ require_relative '../core_ext'
8
+ require_relative '../version'
9
+
10
+ # Module contains the CemAcptConfig::Base class which serves as a base class for different configs.
11
+ module CemAcpt
12
+ module Config
13
+ using CemAcpt::CoreExt::ExtendedHash
14
+
15
+ # Base class for other config classes
16
+ # Child classes should implement the following methods:
17
+ # - valid_keys - provide an array of valid top-level keys for the config as symbols
18
+ # - defaults - provide a hash of default values for the config
19
+ # - env_var_prefix - provide a string to prefix environment variables with (defaults to 'CEM_ACPT').
20
+ # This will be converted to uppercase, have all non-alphanumeric characters replaced with
21
+ # underscores, and be joined with the key name with an underscore to form the environment
22
+ # variable name.
23
+ # However, they can override any of the methods in this class.
24
+ class Base
25
+ attr_reader :config, :env_vars
26
+
27
+ def initialize(opts: {}, config_file: nil, load_user_config: true)
28
+ @load_user_config = load_user_config
29
+ load(opts: opts, config_file: config_file)
30
+ end
31
+
32
+ # @return [String] The prefix for environment variables
33
+ def env_var_prefix
34
+ 'CEM_ACPT'
35
+ end
36
+
37
+ # @return [Array<Symbol>] Valid top-level keys for the config
38
+ def valid_keys
39
+ []
40
+ end
41
+
42
+ # @return [Hash] The default configuration
43
+ def defaults
44
+ {}
45
+ end
46
+
47
+ # Load the configuration from the environment variables, config file, and opts
48
+ # The order of precedence is:
49
+ # 1. environment variables
50
+ # 2. user config file (config.yaml in user_config_dir)
51
+ # 3. specified config file (if it exists)
52
+ # 4. opts
53
+ # 5. static options (set in this class)
54
+ # @param opts [Hash] The options to load
55
+ # @param config_file [String] The config file to load
56
+ # @return [self] This object with the config loaded
57
+ def load(opts: {}, config_file: nil)
58
+ create_config_dirs!
59
+ init_config!(opts: opts, config_file: config_file)
60
+ add_env_vars!(@config)
61
+ @config.merge!(user_config) if user_config && @load_user_config
62
+ @config.merge!(config_from_file) if config_from_file
63
+ @config.merge!(@options) if @options
64
+ add_static_options!(@config)
65
+ @config.format! # Symbolize keys of all hashes
66
+ validate_config!
67
+ # Freeze the config so it can't be modified
68
+ # This helps with thread safety and deterministic behavior
69
+ @config.freeze
70
+ self
71
+ end
72
+ alias to_h config
73
+
74
+ # Returns a string representation of how the config was loaded
75
+ def explain
76
+ explanation = {}
77
+ %i[defaults env_vars user_config config_from_file options].each do |source|
78
+ source_vals = send(source).dup
79
+ next if source_vals.nil? || source_vals.empty?
80
+
81
+ # The loop below will overwrite the value of explanation[key] if the same key is found in multiple sources
82
+ # This is intentional, as the last source to set the value is the one that should be used
83
+ source_vals.each do |key, value|
84
+ explanation[key] = source if @config.dget(key.to_s) == value
85
+ end
86
+ end
87
+ explained = explanation.each_with_object([]) do |(key, value), ary|
88
+ ary << "Key '#{key}' from source '#{value}'"
89
+ end
90
+ explained.join("\n")
91
+ end
92
+
93
+ def [](key)
94
+ if key.is_a?(Symbol)
95
+ @config[key].dup
96
+ elsif key.is_a?(String)
97
+ @config.dget(key).dup
98
+ else
99
+ raise ArgumentError, "Invalid key type '#{key.class}'"
100
+ end
101
+ end
102
+
103
+ def get(dot_key)
104
+ @config.dget(dot_key).dup
105
+ end
106
+ alias dget get
107
+
108
+ def has?(dot_key)
109
+ !!get(dot_key)
110
+ end
111
+
112
+ def empty?
113
+ @config.empty?
114
+ end
115
+
116
+ def ci_mode?
117
+ !!get('ci_mode') || !!(ENV['GITHUB_ACTIONS'] || ENV['CI'])
118
+ end
119
+ alias ci? ci_mode?
120
+
121
+ def debug_mode?
122
+ get('log_level') == 'debug'
123
+ end
124
+ alias debug? debug_mode?
125
+
126
+ def verbose_mode?
127
+ !!get('verbose')
128
+ end
129
+ alias verbose? verbose_mode?
130
+
131
+ def quiet_mode?
132
+ !!get('quiet')
133
+ end
134
+ alias quiet? quiet_mode?
135
+
136
+ def to_yaml
137
+ @config.to_yaml
138
+ end
139
+
140
+ def to_json(*args)
141
+ @config.to_json(*args)
142
+ end
143
+
144
+ private
145
+
146
+ attr_reader :options
147
+
148
+ def user_config_dir
149
+ @user_config_dir ||= File.join(Dir.home, '.cem_acpt')
150
+ end
151
+
152
+ def user_config_file
153
+ @user_config_file ||= File.join(user_config_dir, 'config.yaml')
154
+ end
155
+
156
+ def terraform_dir
157
+ @terraform_dir ||= File.join(user_config_dir, 'terraform')
158
+ end
159
+
160
+ def module_terraform_checksum_file
161
+ @module_terraform_checksum_file ||= File.join(user_config_dir, 'terraform_checksum.txt')
162
+ end
163
+
164
+ def module_terraform_checksum
165
+ @module_terraform_checksum ||= new_module_terraform_checksum
166
+ end
167
+
168
+ def valid_env_var?(env_var)
169
+ env_var.start_with?('CEM_ACPT_') && ENV[env_var]
170
+ end
171
+
172
+ def env_var_to_dot_key(env_var)
173
+ env_var.sub('CEM_ACPT_', '').gsub(%r{__}, '.').downcase
174
+ end
175
+
176
+ def add_static_options!(config)
177
+ config.dset('user_config.dir', user_config_dir)
178
+ config.dset('user_config.file', user_config_file)
179
+ config.dset('provisioner', 'terraform')
180
+ config.dset('terraform.dir', terraform_dir)
181
+ set_third_party_env_vars!(config)
182
+ end
183
+
184
+ # Certain environment variables are used by other tools like GitHub Actions
185
+ # This method sets the relative config values for those environment variables.
186
+ # This is the last step in composing the config, so it will override any
187
+ # values set by the user.
188
+ def set_third_party_env_vars!(config)
189
+ if ENV['RUNNER_DEBUG'] == '1'
190
+ config.dset('log_level', 'debug')
191
+ config.dset('verbose', true)
192
+ end
193
+ if ENV['GITHUB_ACTIONS'] == 'true' || ENV['CI'] == 'true'
194
+ config.dset('ci_mode', true)
195
+ end
196
+ end
197
+
198
+ # Used to source the config during loading of config files.
199
+ # Because config may not be fully loaded yet, this method
200
+ # checks the options hash first, then the current config,
201
+ # then the environment variables for the given key
202
+ # @param key [String] The key to find in dot notation
203
+ # @return [Any] The value of the key
204
+ def find_option(key)
205
+ return @options.dget(key) if @options.dget(key)
206
+ return @config.dget(key) if @config.dget(key)
207
+ ENV.each do |k, v|
208
+ next unless valid_env_var?(k)
209
+
210
+ return v if env_var_to_dot_key(k) == key
211
+ end
212
+ nil
213
+ end
214
+
215
+ def init_config!(opts: {}, config_file: nil)
216
+ # Blank out the config
217
+ @user_config = {}
218
+ @config_from_file = {}
219
+ @options = {}
220
+ @config = defaults.dup
221
+ # Set the parameterized defaults
222
+ config_file = ENV["#{env_var_prefix}_CONFIG_FILE"] if config_file.nil?
223
+ @config.dset('config_file', config_file) if config_file
224
+ @options = opts || {}
225
+ end
226
+
227
+ def add_env_vars!(config)
228
+ @env_vars = {}
229
+ # First load known environment variables into their respective config keys
230
+ # Then load any environment variables that start with CEM_ACPT_<known key> into their respective config keys
231
+ ENV.each do |env_var, value|
232
+ next unless valid_env_var?(env_var)
233
+
234
+ key = env_var_to_dot_key(env_var) # Convert CEM_ACPT_<key> to <dotkey>
235
+ next unless valid_keys.include?(key.split('.').first.to_sym) # Skip if the key is not a known config key
236
+ @env_vars[key] = value
237
+ config.dset(key, value)
238
+ end
239
+ end
240
+
241
+ def user_config
242
+ return @user_config unless @user_config.nil? || @user_config.empty?
243
+
244
+ @user_config = if user_config_file && File.exist?(user_config_file)
245
+ load_config_file(user_config_file)
246
+ else
247
+ {}
248
+ end
249
+
250
+ @user_config
251
+ end
252
+
253
+ # def config_from_file
254
+ # {}
255
+ # end
256
+
257
+ def load_config_file(config_file)
258
+ return {} if config_file.nil? || config_file.empty? || !File.exist?(File.expand_path(config_file))
259
+
260
+ loaded = load_yaml(config_file)
261
+ loaded.format!
262
+ loaded
263
+ end
264
+
265
+ def config_from_file
266
+ return @config_from_file unless @config_from_file.nil? || @config_from_file.empty?
267
+
268
+ conf_file = find_option('config_file')
269
+ return {} if conf_file.nil? || conf_file.empty?
270
+
271
+ unless conf_file
272
+ warn "Invalid config_file type '#{conf_file.class}'. Must be a String."
273
+ return {}
274
+ end
275
+
276
+ mod_dir = find_option('module_dir')
277
+ if mod_dir && File.exist?(File.join(mod_dir, conf_file))
278
+ @config_from_file = load_config_file(File.join(mod_dir, conf_file))
279
+ elsif File.exist?(File.expand_path(conf_file))
280
+ @config_from_file = load_config_file(File.expand_path(conf_file))
281
+ else
282
+ err_msg = [
283
+ "Config file '#{File.expand_path(conf_file)}' does not exist.",
284
+ ]
285
+ err_msg << "Config file '#{File.join(mod_dir, conf_file)}' does not exist." if mod_dir
286
+ raise err_msg.join("\n")
287
+ end
288
+
289
+ @config_from_file
290
+ end
291
+
292
+ def load_yaml(config_file)
293
+ if YAML.respond_to?(:safe_load_file) # Ruby 3.0+
294
+ YAML.safe_load_file(File.expand_path(config_file), permitted_classes: [Regexp])
295
+ else
296
+ YAML.safe_load(File.read(File.expand_path(config_file)), permitted_classes: [Regexp])
297
+ end
298
+ end
299
+
300
+ def validate_config!
301
+ return unless @config && !valid_keys.empty?
302
+
303
+ @config.each do |key, _value|
304
+ warn "Config key '#{key}' is not usable with this command" unless valid_keys.include?(key.to_sym)
305
+ end
306
+ end
307
+
308
+ def create_config_dirs!
309
+ FileUtils.mkdir_p(user_config_dir)
310
+ create_terraform_dir!
311
+ end
312
+
313
+ def create_terraform_dir!
314
+ raise 'Cannot create terraform dir without a user config dir' unless Dir.exist? user_config_dir
315
+
316
+ if File.exist?(module_terraform_checksum_file)
317
+ checksum = File.read(module_terraform_checksum_file).strip
318
+ return if checksum == module_terraform_checksum
319
+ end
320
+ module_terraform_dir = File.expand_path(File.join(__dir__, '..', '..', 'terraform'))
321
+ FileUtils.rm_rf(File.join(user_config_dir, 'terraform'))
322
+ FileUtils.cp_r(module_terraform_dir, user_config_dir)
323
+ raise 'Failed to copy terraform dir to local config dir' unless Dir.exist?(File.join(user_config_dir, 'terraform'))
324
+
325
+ @module_terraform_checksum = new_module_terraform_checksum
326
+ File.write(module_terraform_checksum_file, module_terraform_checksum)
327
+ end
328
+
329
+ def new_module_terraform_checksum
330
+ sha256 = Digest::SHA256.new
331
+ sha256 << ::CemAcpt::VERSION # This ensures the checksum changes when the gem version changes
332
+ path = File.join(File.expand_path(File.join(__dir__, '..', '..', 'terraform')), '**', '*')
333
+ files_and_dirs = Dir.glob(path)
334
+ raise "Failed to find terraform files at path #{path}" if files_and_dirs.empty?
335
+
336
+ files_and_dirs.each do |file|
337
+ sha256 << if File.directory?(file)
338
+ File.basename(file)
339
+ else
340
+ File.read(file)
341
+ end
342
+ end
343
+ sha256.hexdigest
344
+ end
345
+ end
346
+ end
347
+ end