cem_acpt 0.7.3 → 0.8.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.
@@ -2,11 +2,11 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'json'
5
- require 'ruby-terraform'
6
5
  require_relative 'logging'
7
6
  require_relative 'platform'
8
7
  require_relative 'utils'
9
8
  require_relative 'version'
9
+ require_relative 'provision/terraform/terraform_cmd'
10
10
  require_relative 'image_builder/exec'
11
11
  require_relative 'image_builder/provision_commands'
12
12
 
@@ -18,11 +18,11 @@ module CemAcpt
18
18
 
19
19
  builder = TerraformBuilder.new(config)
20
20
  builder.run
21
- logger.info('ImageBuilder') { "Image builder finished after #{builder.duration} seconds" }
21
+ logger.info('CemAcpt::ImageBuilder') { "Image builder finished after #{builder.duration} seconds" }
22
22
  if builder.exit_code.zero?
23
- logger.info('ImageBuilder') { 'Image builder finished successfully' }
23
+ logger.info('CemAcpt::ImageBuilder') { 'Image builder finished successfully' }
24
24
  else
25
- logger.error('ImageBuilder') { "Image builder finished with exit code #{builder.exit_code}" }
25
+ logger.error('CemAcpt::ImageBuilder') { "Image builder finished with exit code #{builder.exit_code}" }
26
26
  end
27
27
  exit builder.exit_code
28
28
  end
@@ -55,42 +55,41 @@ module CemAcpt
55
55
 
56
56
  def run
57
57
  @start_time = Time.now
58
- logger.with_ci_group("CemAcptImage v#{CemAcpt::VERSION} run started at #{@start_time}") do
59
- @all_tfvars = new_tfvars(@config)
60
- @linux_tfvars, @windows_tfvars = divide_tfvars_by_os(@all_tfvars)
61
- image_types = []
62
- image_types << [@linux_tfvars, 'linux'] unless no_linux?
63
- image_types << [@windows_tfvars, 'windows'] unless no_windows?
64
- return dry_run(image_types) if @config.get('dry_run')
65
-
66
- @working_dir = new_working_dir
67
- logger.info('CemAcptImage') { "Using working directory: #{@working_dir}..." }
68
- keep_terminal_alive
69
- save_vars_to_file!('linux', @linux_tfvars)
70
- save_vars_to_file!('windows', @windows_tfvars)
71
- terraform_init
72
- image_types.each do |tfvars, os_str|
73
- terraform_plan(os_str, tfvars, DEFAULT_PLAN_NAME)
74
- begin
75
- terraform_apply(os_str, DEFAULT_PLAN_NAME)
76
- output = JSON.parse(terraform_output(os_str, 'node-data', json: true))
77
- output.each do |instance_name, data|
78
- logger.info('CemAcptImage') { "Stopping instance #{instance_name}..." }
79
- @exec.run('compute', 'instances', 'stop', instance_name)
80
- unless @config.get('no_build_images')
81
- deprecate_old_images_in_family(data['image_family'])
82
- create_image_from_disk(data['disk_link'], image_name_from_image_family(data['image_family']), data['image_family'])
83
- end
84
- @exit_code = 0
58
+ logger.start_ci_group("CemAcptImage v#{CemAcpt::VERSION} run started at #{@start_time}")
59
+ @all_tfvars = new_tfvars(@config)
60
+ @linux_tfvars, @windows_tfvars = divide_tfvars_by_os(@all_tfvars)
61
+ image_types = []
62
+ image_types << [@linux_tfvars, 'linux'] unless no_linux?
63
+ image_types << [@windows_tfvars, 'windows'] unless no_windows?
64
+ return dry_run(image_types) if @config.get('dry_run')
65
+
66
+ @working_dir = new_working_dir
67
+ logger.info('CemAcpt::ImageBuilder') { "Using working directory: #{@working_dir}..." }
68
+ keep_terminal_alive
69
+ save_vars_to_file!('linux', @linux_tfvars)
70
+ save_vars_to_file!('windows', @windows_tfvars)
71
+ terraform_init
72
+ image_types.each do |tfvars, os_str|
73
+ terraform_plan(os_str, tfvars, DEFAULT_PLAN_NAME)
74
+ begin
75
+ terraform_apply(os_str, DEFAULT_PLAN_NAME)
76
+ output = JSON.parse(terraform_output(os_str, 'node-data', json: true))
77
+ output.each do |instance_name, data|
78
+ logger.info('CemAcpt::ImageBuilder') { "Stopping instance #{instance_name}..." }
79
+ @exec.run('compute', 'instances', 'stop', instance_name)
80
+ unless @config.get('no_build_images')
81
+ deprecate_old_images_in_family(data['image_family'])
82
+ create_image_from_disk(data['disk_link'], image_name_from_image_family(data['image_family']), data['image_family'])
85
83
  end
86
- ensure
87
- terraform_destroy(os_str, tfvars) unless @config.get('no_destroy_nodes')
84
+ @exit_code = 0
88
85
  end
86
+ ensure
87
+ terraform_destroy(os_str, tfvars) unless @config.get('no_destroy_nodes')
89
88
  end
90
89
  end
91
90
  rescue StandardError => e
92
- logger.error('CemAcptImage') { "Image builder failed with error: #{e}" }
93
- logger.verbose('CemAcptImage') { e.backtrace.join("\n") }
91
+ logger.error('CemAcpt::ImageBuilder') { "Image builder failed with error: #{e}" }
92
+ logger.verbose('CemAcpt::ImageBuilder') { e.backtrace.join("\n") }
94
93
  @exit_code = 1
95
94
  ensure
96
95
  if @start_time
@@ -98,6 +97,7 @@ module CemAcpt
98
97
  else
99
98
  @duration = 0
100
99
  end
100
+ logger.end_ci_group
101
101
  end
102
102
 
103
103
  private
@@ -109,21 +109,21 @@ module CemAcpt
109
109
  end
110
110
 
111
111
  def deprecate_old_images_in_family(image_family)
112
- logger.info('CemAcptImage') { "Deprecating old images in family #{image_family}..." }
112
+ logger.info('CemAcpt::ImageBuilder') { "Deprecating old images in family #{image_family}..." }
113
113
  images = @exec.run('compute', 'images', 'list', '--project', @config.get('platform.project'), '--filter', "family:#{image_family}")
114
114
  images.each do |image|
115
115
  next unless image['status'] == 'READY'
116
116
 
117
- logger.info('CemAcptImage') { "Deprecating image #{image['name']}..." }
117
+ logger.info('CemAcpt::ImageBuilder') { "Deprecating image #{image['name']}..." }
118
118
  @exec.run('compute', 'images', 'deprecate', image['name'], '--state', 'DEPRECATED', '--delete-in', '1d', '--project', @config.get('platform.project'))
119
119
  end
120
120
  end
121
121
 
122
122
  def create_image_from_disk(disk_link, image_name, image_family)
123
- logger.info('CemAcptImage') { "Creating image from disk #{disk_link}..." }
123
+ logger.info('CemAcpt::ImageBuilder') { "Creating image from disk #{disk_link}..." }
124
124
  @exec.run('compute', 'images', 'create', image_name, '--family', image_family,
125
125
  '--source-disk', disk_link, '--project', @config.get('platform.project'))
126
- logger.info('CemAcptImage') { "Image #{image_name} created for family #{image_family}"}
126
+ logger.info('CemAcpt::ImageBuilder') { "Image #{image_name} created for family #{image_family}"}
127
127
  end
128
128
 
129
129
  def no_windows?
@@ -135,30 +135,22 @@ module CemAcpt
135
135
  end
136
136
 
137
137
  def dry_run(image_types)
138
- logger.info('CemAcptImage') { 'Dry run mode enabled. No images will be built.' }
138
+ logger.info('CemAcpt::ImageBuilder') { 'Dry run mode enabled. No images will be built.' }
139
139
  image_types.each do |tfvars, os_str|
140
- logger.info('CemAcptImage') { "Dry run for #{os_str}..." }
141
- logger.info('CemAcptImage') { "Terraform vars:\n#{JSON.pretty_generate(tfvars)}" }
140
+ logger.info('CemAcpt::ImageBuilder') { "Dry run for #{os_str}..." }
141
+ logger.info('CemAcpt::ImageBuilder') { "Terraform vars:\n#{JSON.pretty_generate(tfvars)}" }
142
142
  end
143
143
  @exit_code = 0
144
144
  end
145
145
 
146
146
  def terraform
147
- return @terraform if defined?(@terraform)
148
-
149
- RubyTerraform.configure do |c|
150
- c.logger = logger
151
- c.stdout = logger
152
- c.stderr = logger
153
- end
154
- @terraform = RubyTerraform
155
- @terraform
147
+ @terraform ||= CemAcpt::Provision::TerraformCmd.new(@image_terraform_dir, @environment)
156
148
  end
157
149
 
158
150
  def new_environment(config)
159
151
  env = (config.get('terraform.environment') || {})
160
152
  env['CLOUDSDK_PYTHON_SITEPACKAGES'] = '1' # This is needed for gcloud to use numpy
161
- logger.verbose('ImageBuilder') { "Using environment:\n#{JSON.pretty_generate(env)}" }
153
+ logger.verbose('CemAcpt::ImageBuilder') { "Using environment:\n#{JSON.pretty_generate(env)}" }
162
154
  env
163
155
  end
164
156
 
@@ -203,7 +195,7 @@ module CemAcpt
203
195
  return unless block_given?
204
196
 
205
197
  if no_os_str?(os_str)
206
- logger.debug('ImageBuilder') { "Skipping #{os_str} because no_#{os_str} is true" }
198
+ logger.debug('CemAcpt::ImageBuilder') { "Skipping #{os_str} because no_#{os_str} is true" }
207
199
  return
208
200
  end
209
201
 
@@ -216,7 +208,7 @@ module CemAcpt
216
208
  def terraform_init
217
209
  raise 'Cannot initialize Terraform, both no_linux and no_windows are true' if no_linux? && no_windows?
218
210
 
219
- logger.debug('ImageBuilder') { 'Initializing Terraform' }
211
+ logger.debug('CemAcpt::ImageBuilder') { 'Initializing Terraform' }
220
212
  in_os_dir('linux') do
221
213
  terraform.init({ input: false, no_color: true })
222
214
  end
@@ -227,37 +219,37 @@ module CemAcpt
227
219
 
228
220
  def terraform_plan(os_dir, tfvars, plan_name)
229
221
  in_os_dir(os_dir) do
230
- logger.debug('ImageBuilder') { "Creating Terraform plan #{plan_name} for #{os_dir}" }
231
- logger.verbose('ImageBuilder') { "Using vars:\n#{JSON.pretty_generate(tfvars)}" }
222
+ logger.debug('CemAcpt::ImageBuilder') { "Creating Terraform plan #{plan_name} for #{os_dir}" }
223
+ logger.verbose('CemAcpt::ImageBuilder') { "Using vars:\n#{JSON.pretty_generate(tfvars)}" }
232
224
  terraform.plan({ input: false, no_color: true, plan: plan_name, vars: tfvars }, { environment: environment })
233
225
  end
234
226
  end
235
227
 
236
228
  def terraform_apply(os_dir, plan_name)
237
229
  in_os_dir(os_dir) do
238
- logger.debug('ImageBuilder') { "Applying Terraform plan #{plan_name} for #{os_dir}" }
230
+ logger.debug('CemAcpt::ImageBuilder') { "Applying Terraform plan #{plan_name} for #{os_dir}" }
239
231
  terraform.apply({ input: false, no_color: true, plan: plan_name }, { environment: environment })
240
232
  end
241
233
  end
242
234
 
243
235
  def terraform_output(os_dir, output_name, json: true)
244
236
  in_os_dir(os_dir) do
245
- logger.debug('ImageBuilder') { "Getting Terraform output #{output_name} for #{os_dir}" }
237
+ logger.debug('CemAcpt::ImageBuilder') { "Getting Terraform output #{output_name} for #{os_dir}" }
246
238
  terraform.output({ no_color: true, json: json, name: output_name }, { environment: environment })
247
239
  end
248
240
  end
249
241
 
250
242
  def terraform_destroy(os_dir, tfvars)
251
243
  in_os_dir(os_dir) do
252
- logger.debug('ImageBuilder') { "Destroying Terraform resources for #{os_dir}" }
253
- logger.verbose('ImageBuilder') { "Using vars:\n#{JSON.pretty_generate(tfvars)}" }
244
+ logger.debug('CemAcpt::ImageBuilder') { "Destroying Terraform resources for #{os_dir}" }
245
+ logger.verbose('CemAcpt::ImageBuilder') { "Using vars:\n#{JSON.pretty_generate(tfvars)}" }
254
246
  terraform.destroy({ auto_approve: true, input: false, no_color: true, vars: tfvars }, { environment: environment })
255
247
  end
256
248
  end
257
249
 
258
250
  def terraform_show(os_dir)
259
251
  in_os_dir(os_dir) do
260
- logger.debug('ImageBuilder') { "Showing Terraform state for #{os_dir}" }
252
+ logger.debug('CemAcpt::ImageBuilder') { "Showing Terraform state for #{os_dir}" }
261
253
  terraform.show({ no_color: true }, { environment: environment })
262
254
  end
263
255
  end
@@ -288,7 +280,7 @@ module CemAcpt
288
280
  windows_image: windows_image?(image[:os]),
289
281
  )
290
282
  end
291
- logger.verbose('ImageBuilder') { "All Terraform variables:\n#{JSON.pretty_generate(tfvars)}" }
283
+ logger.verbose('CemAcpt::ImageBuilder') { "All Terraform variables:\n#{JSON.pretty_generate(tfvars)}" }
292
284
  tfvars
293
285
  end
294
286
 
@@ -299,8 +291,8 @@ module CemAcpt
299
291
  linux_tfvars[:node_data] = linux_node_data
300
292
  windows_tfvars = tfvars.dup
301
293
  windows_tfvars[:node_data] = windows_node_data
302
- logger.verbose('ImageBuilder') { "Linux Terraform variables:\n#{JSON.pretty_generate(linux_tfvars)}" }
303
- logger.verbose('ImageBuilder') { "Windows Terraform variables:\n#{JSON.pretty_generate(windows_tfvars)}" }
294
+ logger.verbose('CemAcpt::ImageBuilder') { "Linux Terraform variables:\n#{JSON.pretty_generate(linux_tfvars)}" }
295
+ logger.verbose('CemAcpt::ImageBuilder') { "Windows Terraform variables:\n#{JSON.pretty_generate(windows_tfvars)}" }
304
296
  [linux_tfvars, windows_tfvars]
305
297
  end
306
298
 
@@ -309,10 +301,10 @@ module CemAcpt
309
301
  end
310
302
 
311
303
  def save_vars_to_file!(prefix, vars)
312
- logger.debug('ImageBuilder') { "Saving vars to file #{File.join(@working_dir, "#{prefix}_#{DEFAULT_VARS_FILE}")}" }
304
+ logger.debug('CemAcpt::ImageBuilder') { "Saving vars to file #{File.join(@working_dir, "#{prefix}_#{DEFAULT_VARS_FILE}")}" }
313
305
  File.write(File.join(@working_dir, "#{prefix}_#{DEFAULT_VARS_FILE}"), vars.to_json)
314
306
  rescue StandardError => e
315
- logger.error('ImageBuilder') { 'Error saving vars to file' }
307
+ logger.error('CemAcpt::ImageBuilder') { 'Error saving vars to file' }
316
308
  raise e
317
309
  end
318
310
 
@@ -320,25 +312,25 @@ module CemAcpt
320
312
  unless Dir.exist?(@image_terraform_dir)
321
313
  raise "Image Terraform directory #{@image_terraform_dir} does not exist"
322
314
  end
323
- logger.debug('ImageBuilder') { "Creating new working directory in #{@image_terraform_dir}" }
315
+ logger.debug('CemAcpt::ImageBuilder') { "Creating new working directory in #{@image_terraform_dir}" }
324
316
  working_dir = File.join(@image_terraform_dir, "image_builder_#{Time.now.to_i}")
325
317
  FileUtils.mkdir_p(working_dir)
326
- logger.verbose('ImageBuilder') { "Created working directory #{working_dir}" }
318
+ logger.verbose('CemAcpt::ImageBuilder') { "Created working directory #{working_dir}" }
327
319
  FileUtils.cp_r(File.join(@image_terraform_dir, 'linux'), working_dir)
328
320
  FileUtils.cp_r(File.join(@image_terraform_dir, 'windows'), working_dir)
329
- logger.verbose('ImageBuilder') { "Copied Terraform files from #{@image_terraform_dir}/(linux|windows) to #{working_dir}" }
321
+ logger.verbose('CemAcpt::ImageBuilder') { "Copied Terraform files from #{@image_terraform_dir}/(linux|windows) to #{working_dir}" }
330
322
  if File.exist?(@all_tfvars[:private_key])
331
323
  FileUtils.cp(@all_tfvars[:private_key], working_dir)
332
324
  @private_key = File.join(working_dir, File.basename(@all_tfvars[:private_key]))
333
- logger.verbose('ImageBuilder') { "Copied private key #{@all_tfvars[:private_key]} to #{working_dir}" }
325
+ logger.verbose('CemAcpt::ImageBuilder') { "Copied private key #{@all_tfvars[:private_key]} to #{working_dir}" }
334
326
  end
335
327
  if File.exist?(@all_tfvars[:public_key])
336
328
  FileUtils.cp(@all_tfvars[:public_key], working_dir)
337
329
  @public_key = File.join(working_dir, File.basename(@all_tfvars[:public_key]))
338
- logger.verbose('ImageBuilder') { "Copied public key #{@all_tfvars[:public_key]} to #{working_dir}" }
330
+ logger.verbose('CemAcpt::ImageBuilder') { "Copied public key #{@all_tfvars[:public_key]} to #{working_dir}" }
339
331
  end
340
332
 
341
- logger.verbose('ImageBuilder') { "Content of #{working_dir}:\n#{Dir.glob(File.join(working_dir, '*')).join("\n")}" }
333
+ logger.verbose('CemAcpt::ImageBuilder') { "Content of #{working_dir}:\n#{Dir.glob(File.join(working_dir, '*')).join("\n")}" }
342
334
  working_dir
343
335
  end
344
336
  end
@@ -13,7 +13,7 @@ module CemAcpt
13
13
  'error' => ::Logger::ERROR,
14
14
  'fatal' => ::Logger::FATAL,
15
15
  'unknown' => ::Logger::UNKNOWN,
16
- }
16
+ }.freeze
17
17
 
18
18
  # Delegator class for when you want to log to multiple devices
19
19
  # at the same time, such as STDOUT and a file.
@@ -28,34 +28,39 @@ module CemAcpt
28
28
  def self.from_logger_configs(configs)
29
29
  return if configs.nil? || configs.empty?
30
30
 
31
- loggers = configs.map do |config|
31
+ logger_instances = configs.map do |config|
32
32
  CemAcpt::Logging::Logger.new(config[:logdev],
33
33
  config[:shift_age],
34
34
  config[:shift_size],
35
35
  **config.reject { |k, _| [:logdev, :shift_age, :shift_size].include?(k) })
36
36
  end
37
- new(*loggers)
37
+ new(*logger_instances)
38
38
  end
39
39
 
40
- def initialize(*loggers)
41
- @loggers = loggers
40
+ def initialize(*logger_instances)
41
+ @loggers = logger_instances
42
42
  end
43
43
 
44
44
  def logger_configs
45
- @loggers.map(&:config_hash)
45
+ loggers.map(&:config_hash)
46
46
  end
47
47
 
48
48
  def method_missing(m, *args, &block)
49
- if @loggers.all? { |l| l.respond_to?(m) }
50
- @loggers.map { |l| l.send(m, *args, &block) }
49
+ if loggers.all? { |l| l.respond_to?(m) }
50
+ loggers.map { |l| l.send(m, *args, &block) }
51
51
  else
52
52
  super
53
53
  end
54
54
  end
55
55
 
56
56
  def respond_to_missing?(m, include_private = false)
57
- @loggers.all? { |l| l.respond_to?(m) } || super
57
+ loggers.all? { |l| l.respond_to?(m) } || super
58
58
  end
59
+
60
+ private
61
+
62
+ # Basically just for making testing easier
63
+ attr_reader :loggers
59
64
  end
60
65
 
61
66
  # Delagator class for the standard Ruby Logger class.
@@ -89,55 +94,63 @@ module CemAcpt
89
94
  end
90
95
 
91
96
  def debug(progname = nil, &block)
92
- return if log_trap_context('debug', progname, &block)
93
- return if log_ci_mode('debug', progname, &block)
97
+ severity = 'debug'
98
+ return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
99
+ return log_ci_mode(severity, progname, &block) if log_ci_mode?(severity)
94
100
 
95
101
  super
96
102
  end
97
103
 
98
104
  def info(progname = nil, &block)
99
- return if log_trap_context('info', progname, &block)
100
- return if log_ci_mode('info', progname, &block)
105
+ severity = 'info'
106
+ return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
107
+ return log_ci_mode(severity, progname, &block) if log_ci_mode?(severity)
101
108
 
102
109
  super
103
110
  end
104
111
 
105
112
  def warn(progname = nil, &block)
106
- return if log_trap_context('warn', progname, &block)
107
- return if log_ci_mode('warn', progname, &block)
113
+ severity = 'warn'
114
+ return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
115
+ return log_ci_mode(severity, progname, &block) if log_ci_mode?(severity)
108
116
 
109
117
  super
110
118
  end
111
119
 
112
120
  def error(progname = nil, &block)
113
- return if log_trap_context('error', progname, &block)
114
- return if log_ci_mode('error', progname, &block)
121
+ severity = 'error'
122
+ return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
123
+ return log_ci_mode(severity, progname, &block) if log_ci_mode?(severity)
115
124
 
116
125
  super
117
126
  end
118
127
 
119
128
  def fatal(progname = nil, &block)
120
- return if log_trap_context('fatal', progname, &block)
121
- return if log_ci_mode('fatal', progname, &block)
129
+ severity = 'fatal'
130
+ return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
131
+ return log_ci_mode(severity, progname, &block) if log_ci_mode?(severity)
122
132
 
123
133
  super
124
134
  end
125
135
 
126
- # Wraps the given block in a Github Actions group if in CI mode
127
- # @param name [String] the name of the group
128
- def with_ci_group(name)
136
+ def start_ci_group(name)
129
137
  if @ci_mode
130
138
  self.<< "::group::#{name}\n"
131
139
  else
132
140
  info(name)
133
141
  end
134
- yield
135
- ensure
142
+ end
143
+
144
+ def end_ci_group
136
145
  self.<< "::endgroup::\n" if @ci_mode
137
146
  end
138
147
 
139
148
  private
140
149
 
150
+ def log_trap_context?(severity)
151
+ @trap_context && level <= LEVEL_MAP[severity]
152
+ end
153
+
141
154
  # Can't use the standard Logger methods when in trap context
142
155
  # because they don't work. So we have to write directly to the
143
156
  # raw logdev. Unlike the standard Logger methods, there is no
@@ -145,8 +158,6 @@ module CemAcpt
145
158
  # This is necessary in the trap context because using a Mutex
146
159
  # lock in a trap context will cause a deadlock.
147
160
  def log_trap_context(severity, progname = nil, &block)
148
- return false unless @trap_context && level <= LEVEL_MAP[severity]
149
-
150
161
  if @raw_logdev.respond_to?(:puts)
151
162
  @raw_logdev.puts(ci_format(severity, false, progname, &block))
152
163
  elsif @raw_logdev.respond_to?(:write)
@@ -155,7 +166,10 @@ module CemAcpt
155
166
  # Default to logging to STDERR
156
167
  $stderr.puts(ci_format(severity, false, progname, &block))
157
168
  end
158
- true
169
+ end
170
+
171
+ def log_ci_mode?(severity)
172
+ @ci_mode && level <= LEVEL_MAP[severity]
159
173
  end
160
174
 
161
175
  # CI mode uses << instead of the standard Logger methods
@@ -164,10 +178,7 @@ module CemAcpt
164
178
  # with a standard Logger, the CI mode messages are still
165
179
  # formatted correctly.
166
180
  def log_ci_mode(severity, progname = nil, &block)
167
- return false unless @ci_mode && level <= LEVEL_MAP[severity]
168
-
169
181
  self.<<(ci_format(severity, true, progname, &block))
170
- true
171
182
  end
172
183
 
173
184
  # Formats the log message for CI mode, translating the severity
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent-ruby'
4
3
  require_relative 'logging'
5
4
 
6
5
  # CemAcpt::Platform manages creating and configring platform specific objects
@@ -25,7 +24,7 @@ module CemAcpt::Platform
25
24
  raise Error, 'run_data must include a :test_data key' unless run_data.key?(:test_data)
26
25
  raise Error, 'run_data[:test_data] must be an Array' unless run_data[:test_data].is_a?(Array)
27
26
 
28
- logger.info "Using #{platform} for #{run_data[:test_data].length} tests..."
27
+ logger.info('CemAcpt::Platform') { "Using #{platform} for #{run_data[:test_data].length} tests..." }
29
28
  run_data[:test_data].dup.each_with_object([]) do |single_test_data, ary|
30
29
  ary << new_test_platform_object(platform, config, single_test_data, **run_data.reject { |k, _| k == :test_data })
31
30
  end
@@ -53,7 +52,7 @@ module CemAcpt::Platform
53
52
  File.basename(file, '.rb') unless file.end_with?('base.rb')
54
53
  end
55
54
  @platforms.compact!
56
- logger.debug "Discovered platform(s): #{@platforms}"
55
+ logger.debug('CemAcpt::Platform') { "Discovered platform(s): #{@platforms}" }
57
56
  @platforms
58
57
  end
59
58
 
@@ -83,7 +82,7 @@ module CemAcpt::Platform
83
82
  require_relative 'platform/base'
84
83
  # If the class has already been defined, we can just use it.
85
84
  if Object.const_defined?(class_name)
86
- logger.debug "Using existing platform class #{class_name}"
85
+ logger.debug('CemAcpt::Platform') { "Using existing platform class #{class_name}" }
87
86
  klass = Object.const_get(class_name)
88
87
  else
89
88
  # Otherwise, we need to create the class. We do this by setting
@@ -110,7 +109,7 @@ module CemAcpt::Platform
110
109
  end,
111
110
  )
112
111
  end
113
- logger.debug "Using platform class: #{klass.inspect}"
112
+ logger.debug('CemAcpt::Platform') { "Using platform class: #{klass.inspect}" }
114
113
  klass
115
114
  end
116
115
  end
@@ -53,8 +53,8 @@ module CemAcpt
53
53
  '--logdest',
54
54
  'console,/opt/cem_acpt/provision_apply.log',
55
55
  ]
56
- cmd << '--debug' if @config.debug?
57
- cmd << '--verbose' if @config.verbose?
56
+ cmd << '--debug' if @config.debug? && !@config.get('puppet.no_debug')
57
+ cmd << '--verbose' if @config.verbose? && !@config.get('puppet.no_verbose')
58
58
  cmd << "#{destination_provision_directory}/#{puppet_manifest_file}"
59
59
  cmd.join(' ')
60
60
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'stringio'
6
+ require_relative '../../logging'
7
+ require_relative '../../utils/shell'
8
+
9
+ module CemAcpt
10
+ module Provision
11
+ # Stand-in for ruby-terraform because ruby-terraform doesn't work with Ruby 3
12
+ class TerraformCmd
13
+ include CemAcpt::Logging
14
+
15
+ attr_accessor :working_dir
16
+ attr_reader :bin_path
17
+
18
+ def initialize(working_dir = nil, environment = {})
19
+ @working_dir = working_dir
20
+ @environment = environment
21
+ @bin_path = which_terraform
22
+ end
23
+
24
+ def inspect
25
+ to_s
26
+ end
27
+
28
+ def to_s
29
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
30
+ end
31
+
32
+ def environment=(env)
33
+ raise ArgumentError, 'environment must be a Hash' unless env.is_a?(Hash)
34
+
35
+ @environment = env
36
+ end
37
+
38
+ def environment(env = {})
39
+ raise ArgumentError, 'additional environment must be a Hash' unless env.is_a?(Hash)
40
+
41
+ if env.key?(:environment) && env[:environment].is_a?(Hash)
42
+ env = env[:environment]
43
+ end
44
+ @environment.merge(env)
45
+ end
46
+
47
+ def init(opts = {}, env = {})
48
+ run_cmd('init', opts, env)
49
+ end
50
+
51
+ def plan(opts = {}, env = {})
52
+ plan = extract_arg!(opts, :plan, required: true)
53
+ opts[:out] = plan
54
+ run_cmd('plan', opts, env)
55
+ end
56
+
57
+ def apply(opts = {}, env = {})
58
+ plan = extract_arg!(opts, :plan, required: true)
59
+ run_cmd('apply', opts, env, suffix: plan)
60
+ end
61
+
62
+ def destroy(opts = {}, env = {})
63
+ run_cmd('destroy', opts, env)
64
+ end
65
+
66
+ def show(opts = {}, env = {})
67
+ run_cmd('show', opts, env)
68
+ end
69
+
70
+ def output(opts = {}, env = {})
71
+ name = extract_arg!(opts, :name, required: true)
72
+ run_cmd('output', opts, env, suffix: name)
73
+ end
74
+
75
+ private
76
+
77
+ def extract_arg!(opts, key, required: false)
78
+ key = key.to_sym
79
+ raise ArgumentError, "option #{key} is required" if required && !opts.key?(key) && (opts[key].nil? || opts[key].empty?)
80
+
81
+ opts.delete(key)
82
+ end
83
+
84
+ def run_cmd(cmd, opts = {}, env = {}, suffix: '')
85
+ cmd = format_cmd(cmd, opts, suffix)
86
+ env = environment(env)
87
+ logger.debug('CemAcpt::Provision::TerraformCmd') { "Running command \"#{cmd}\" with environment \"#{env}\"" }
88
+ CemAcpt::Utils::Shell.run_cmd(cmd, env, output: logger)
89
+ end
90
+
91
+ def chdir(opts = {})
92
+ [extract_arg!(opts, :chdir), working_dir, Dir.pwd].each do |d|
93
+ next if d.nil? || d.empty?
94
+
95
+ d = File.expand_path(d)
96
+ return "-chdir=#{d}" if File.directory?(d)
97
+
98
+ logger.warn('CemAcpt::Provision::TerraformCmd') { "Directory #{d} does not exist, using next..." }
99
+ end
100
+ end
101
+
102
+ def which_terraform
103
+ path = CemAcpt::Utils::Shell.which('terraform')
104
+ raise 'terraform not found in PATH, make sure it is installed' if path.nil?
105
+
106
+ path
107
+ end
108
+
109
+ def format_cmd(cmd, opts = {}, suffix = '')
110
+ formatted = [bin_path, chdir(opts), cmd]
111
+ opts_s = opts_string(opts)
112
+ formatted << opts_s unless opts_s.empty?
113
+ formatted << suffix unless suffix.empty?
114
+ formatted.join(' ')
115
+ end
116
+
117
+ def opts_string(opts = {})
118
+ formatted = opts.map do |k, v|
119
+ next if k.nil? || k.empty?
120
+
121
+ k = k.to_s
122
+ k.tr!('_', '-')
123
+ if k == 'vars'
124
+ v.map { |vk, vv| "-var '#{vk}=#{vv}'" }.join(' ')
125
+ elsif %w[input lock refresh].include?(k) # These are boolean flags with values
126
+ "-#{k}=#{v}"
127
+ elsif v.nil? || (v.respond_to?(:empty) && v.empty?) || v.is_a?(TrueClass)
128
+ "-#{k}"
129
+ else
130
+ case v
131
+ when Array, Hash
132
+ "-#{k}='#{v.to_json}'"
133
+ else
134
+ "-#{k}=#{v}"
135
+ end
136
+ end
137
+ end
138
+ formatted.join(' ')
139
+ end
140
+ end
141
+ end
142
+
143
+ # IO monkey patch for non-blocking readline
144
+ class ::IO
145
+ def readline_nonblock
146
+ rlnb = []
147
+ while (ch = read_nonblock(1))
148
+ rlnb << ch
149
+ break if ch == "\n"
150
+ end
151
+ rlnb.join
152
+ end
153
+ end
154
+ end