cem_acpt 0.7.3 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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