chef-apply 0.1.2 → 0.1.15

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.
@@ -20,24 +20,15 @@ require "fileutils"
20
20
 
21
21
  module ChefApply::Action::InstallChef
22
22
  class Base < ChefApply::Action::Base
23
- MIN_CHEF_VERSION = Gem::Version.new("13.0.0")
23
+ MIN_14_VERSION = Gem::Version.new("14.1.1")
24
+ MIN_13_VERSION = Gem::Version.new("13.10.0")
24
25
 
25
26
  def perform_action
26
- if target_host.installed_chef_version >= MIN_CHEF_VERSION
27
+ if check_minimum_chef_version!(target_host) == :minimum_version_met
27
28
  notify(:already_installed)
28
- return
29
+ else
30
+ perform_local_install
29
31
  end
30
- raise ClientOutdated.new(target_host.installed_chef_version, MIN_CHEF_VERSION)
31
- # NOTE: 2018-05-10 below is an intentionally dead code path that
32
- # will get re-visited once we determine how we want automatic
33
- # upgrades to behave.
34
- # @upgrading = true
35
- # perform_local_install
36
- rescue ChefApply::TargetHost::ChefNotInstalled
37
- if config[:check_only]
38
- raise ClientNotInstalled.new()
39
- end
40
- perform_local_install
41
32
  end
42
33
 
43
34
  def name
@@ -116,6 +107,31 @@ module ChefApply::Action::InstallChef
116
107
  remote_path
117
108
  end
118
109
 
110
+ def check_minimum_chef_version!(target)
111
+ begin
112
+ installed_version = target.installed_chef_version
113
+ rescue ChefApply::TargetHost::ChefNotInstalled
114
+ if config[:check_only]
115
+ raise ClientNotInstalled.new()
116
+ end
117
+ return :client_not_installed
118
+ end
119
+
120
+ case
121
+ when installed_version >= Gem::Version.new("14.0.0") && installed_version < MIN_14_VERSION
122
+ raise Client14Outdated.new(installed_version, MIN_14_VERSION)
123
+ when installed_version >= Gem::Version.new("13.0.0") && installed_version < MIN_13_VERSION
124
+ raise Client13Outdated.new(installed_version, MIN_13_VERSION, MIN_14_VERSION)
125
+ when installed_version < Gem::Version.new("13.0.0")
126
+ # If they have Chef < 13.0.0 installed we want to show them the easiest upgrade path -
127
+ # Chef 13 first and then Chef 14 since most customers cannot make the leap directly
128
+ # to 14.
129
+ raise Client13Outdated.new(installed_version, MIN_13_VERSION, MIN_14_VERSION)
130
+ end
131
+
132
+ :minimum_version_met
133
+ end
134
+
119
135
  def setup_remote_temp_path
120
136
  raise NotImplementedError
121
137
  end
@@ -129,9 +145,15 @@ module ChefApply::Action::InstallChef
129
145
  def initialize(); super("CHEFINS002"); end
130
146
  end
131
147
 
132
- class ClientOutdated < ChefApply::ErrorNoLogs
148
+ class Client13Outdated < ChefApply::ErrorNoLogs
149
+ def initialize(current_version, min_13_version, min_14_version)
150
+ super("CHEFINS003", current_version, min_13_version, min_14_version)
151
+ end
152
+ end
153
+
154
+ class Client14Outdated < ChefApply::ErrorNoLogs
133
155
  def initialize(current_version, target_version)
134
- super("CHEFINS003", current_version, target_version)
156
+ super("CHEFINS004", current_version, target_version)
135
157
  end
136
158
  end
137
159
  end
@@ -1,4 +1,5 @@
1
1
  #
2
+ #p
2
3
  # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
4
  # License:: Apache License, Version 2.0
4
5
  #
@@ -15,30 +16,37 @@
15
16
  # limitations under the License.
16
17
  #
17
18
  require "mixlib/cli"
18
- require "chef/log"
19
+
20
+ require "chef_apply/config"
19
21
  require "chef-config/config"
20
22
  require "chef-config/logger"
21
23
 
22
- require "chef_apply/cli_options"
24
+ require "chef_apply/cli/validation"
25
+ require "chef_apply/cli/options"
26
+ require "chef_apply/cli/help"
23
27
  require "chef_apply/action/converge_target"
28
+ require "chef_apply/action/generate_local_policy"
29
+ require "chef_apply/action/generate_temp_cookbook"
24
30
  require "chef_apply/action/install_chef"
25
- require "chef_apply/config"
26
31
  require "chef_apply/error"
27
32
  require "chef_apply/log"
28
- require "chef_apply/recipe_lookup"
29
33
  require "chef_apply/target_host"
30
34
  require "chef_apply/target_resolver"
31
35
  require "chef_apply/telemeter"
32
- require "chef_apply/telemeter/sender"
33
- require "chef_apply/temp_cookbook"
34
- require "chef_apply/ui/terminal"
35
36
  require "chef_apply/ui/error_printer"
36
- require "chef_apply/version"
37
+ require "chef_apply/ui/terminal"
37
38
 
38
39
  module ChefApply
39
40
  class CLI
41
+ attr_reader :temp_cookbook, :archive_file_location, :target_hosts
42
+
40
43
  include Mixlib::CLI
41
- include ChefApply::CLIOptions
44
+ # Pulls in the CLI options and flags we have defined for this command.
45
+ include ChefApply::CLI::Options
46
+ # Argument validation and parsing behaviors
47
+ include ChefApply::CLI::Validation
48
+ # Help and version formatting
49
+ include ChefApply::CLI::Help
42
50
 
43
51
  RC_OK = 0
44
52
  RC_COMMAND_FAILED = 1
@@ -98,27 +106,11 @@ module ChefApply
98
106
  show_version
99
107
  else
100
108
  validate_params(cli_arguments)
101
- configure_chef
102
- target_hosts = TargetResolver.new(cli_arguments.shift,
103
- parsed_options.delete(:protocol),
104
- parsed_options).targets
105
- temp_cookbook, initial_status_msg = generate_temp_cookbook(cli_arguments)
106
- local_policy_path = nil
107
- UI::Terminal.render_job(TS.generate_policyfile.generating) do |reporter|
108
- local_policy_path = create_local_policy(temp_cookbook)
109
- reporter.success(TS.generate_policyfile.success)
110
- end
111
- if target_hosts.length == 1
112
- # Note: UX discussed determined that when running with a single target,
113
- # we'll use multiple lines to display status for the target.
114
- run_single_target(initial_status_msg, target_hosts[0], local_policy_path)
115
- else
116
- @multi_target = true
117
- # Multi-target will use one line per target.
118
- run_multi_target(initial_status_msg, target_hosts, local_policy_path)
119
- end
109
+ target_hosts = resolve_targets(cli_arguments.shift, parsed_options)
110
+ render_cookbook_setup(cli_arguments)
111
+ render_converge(target_hosts)
120
112
  end
121
- rescue OptionParser::InvalidOption => e
113
+ rescue OptionParser::InvalidOption => e # from parse_options
122
114
  # Using nil here is a bit gross but it prevents usage from printing.
123
115
  ove = OptionValidationError.new("CHEFVAL010", nil,
124
116
  e.message.split(":")[1].strip, # only want the flag
@@ -131,181 +123,47 @@ module ChefApply
131
123
  temp_cookbook.delete unless temp_cookbook.nil?
132
124
  end
133
125
 
134
- # Accepts a target_host and establishes the connection to that host
135
- # while providing visual feedback via the Terminal API.
136
- def connect_target(target_host, reporter = nil)
137
- connect_message = T.status.connecting(target_host.user)
138
- if reporter.nil?
139
- UI::Terminal.render_job(connect_message, prefix: "[#{target_host.config[:host]}]") do |rep|
140
- do_connect(target_host, rep, :success)
141
- end
142
- else
143
- reporter.update(connect_message)
144
- do_connect(target_host, reporter, :update)
145
- end
146
- target_host
126
+ def resolve_targets(host_spec, opts)
127
+ @target_hosts = TargetResolver.new(host_spec,
128
+ opts.delete(:protocol),
129
+ opts).targets
147
130
  end
148
131
 
149
- def run_single_target(initial_status_msg, target_host, local_policy_path)
150
- connect_target(target_host)
151
- prefix = "[#{target_host.hostname}]"
152
- UI::Terminal.render_job(TS.install_chef.verifying, prefix: prefix) do |reporter|
153
- install(target_host, reporter)
132
+ def render_cookbook_setup(arguments)
133
+ UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
134
+ @temp_cookbook = generate_temp_cookbook(arguments, reporter)
154
135
  end
155
- UI::Terminal.render_job(initial_status_msg, prefix: "[#{target_host.hostname}]") do |reporter|
156
- converge(reporter, local_policy_path, target_host)
136
+ UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
137
+ @archive_file_location = generate_local_policy(reporter)
157
138
  end
158
139
  end
159
140
 
160
- def run_multi_target(initial_status_msg, target_hosts, local_policy_path)
161
- # Our multi-host UX does not show a line item per action,
162
- # but rather a line-item per connection.
141
+ def render_converge(target_hosts)
163
142
  jobs = target_hosts.map do |target_host|
164
- # This block will run in its own thread during render.
143
+ # Each block will run in its own thread during render.
165
144
  UI::Terminal::Job.new("[#{target_host.hostname}]", target_host) do |reporter|
166
145
  connect_target(target_host, reporter)
167
- reporter.update(TS.install_chef.verifying)
168
146
  install(target_host, reporter)
169
- reporter.update(initial_status_msg)
170
- converge(reporter, local_policy_path, target_host)
147
+ converge(reporter, archive_file_location, target_host)
171
148
  end
172
149
  end
173
- UI::Terminal.render_parallel_jobs(TS.converge.multi_header, jobs)
150
+ header = TS.converge.header(target_hosts.length, temp_cookbook.descriptor, temp_cookbook.from)
151
+ UI::Terminal.render_parallel_jobs(header, jobs)
174
152
  handle_job_failures(jobs)
175
153
  end
176
154
 
177
- # The first param is always hostname. Then we either have
178
- # 1. A recipe designation
179
- # 2. A resource type and resource name followed by any properties
180
- PROPERTY_MATCHER = /^([a-zA-Z0-9_]+)=(.+)$/
181
- CB_MATCHER = '[\w\-]+'
182
- def validate_params(params)
183
- if params.size < 2
184
- raise OptionValidationError.new("CHEFVAL002", self)
185
- end
186
- if params.size == 2
187
- # Trying to specify a recipe to run remotely, no properties
188
- cb = params[1]
189
- if File.exist?(cb)
190
- # This is a path specification, and we know it is valid
191
- elsif cb =~ /^#{CB_MATCHER}$/ || cb =~ /^#{CB_MATCHER}::#{CB_MATCHER}$/
192
- # They are specifying a cookbook as 'cb_name' or 'cb_name::recipe'
193
- else
194
- raise OptionValidationError.new("CHEFVAL004", self, cb)
195
- end
196
- elsif params.size >= 3
197
- properties = params[3..-1]
198
- properties.each do |property|
199
- unless property =~ PROPERTY_MATCHER
200
- raise OptionValidationError.new("CHEFVAL003", self, property)
201
- end
202
- end
203
- end
204
- end
205
-
206
- # Now that we are leveraging Chef locally we want to perform some initial setup of it
207
- def configure_chef
208
- ChefConfig.logger = ChefApply::Log
209
- # Setting the config isn't enough, we need to ensure the logger is initialized
210
- # or automatic initialization will still go to stdout
211
- Chef::Log.init(ChefApply::Log)
212
- Chef::Log.level = ChefApply::Log.level
213
- end
214
-
215
- def format_properties(string_props)
216
- properties = {}
217
- string_props.each do |a|
218
- key, value = PROPERTY_MATCHER.match(a)[1..-1]
219
- value = transform_property_value(value)
220
- properties[key] = value
221
- end
222
- properties
223
- end
224
-
225
- # Incoming properties are always read as a string from the command line.
226
- # Depending on their type we should transform them so we do not try and pass
227
- # a string to a resource property that expects an integer or boolean.
228
- def transform_property_value(value)
229
- case value
230
- when /^0/
231
- # when it is a zero leading value like "0777" don't turn
232
- # it into a number (this is a mode flag)
233
- value
234
- when /^\d+$/
235
- value.to_i
236
- when /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/
237
- value.to_f
238
- when /true/i
239
- true
240
- when /false/i
241
- false
242
- else
243
- value
244
- end
245
- end
246
-
247
- # The user will either specify a single resource on the command line, or a recipe.
248
- # We need to parse out those two different situations
249
- def generate_temp_cookbook(cli_arguments)
250
- temp_cookbook = TempCookbook.new
251
- if recipe_strategy?(cli_arguments)
252
- recipe_specifier = cli_arguments.shift
253
- ChefApply::Log.debug("Beginning to look for recipe specified as #{recipe_specifier}")
254
- if File.file?(recipe_specifier)
255
- ChefApply::Log.debug("#{recipe_specifier} is a valid path to a recipe")
256
- recipe_path = recipe_specifier
257
- else
258
- rl = RecipeLookup.new(parsed_options[:cookbook_repo_paths])
259
- cookbook_path_or_name, optional_recipe_name = rl.split(recipe_specifier)
260
- cookbook = rl.load_cookbook(cookbook_path_or_name)
261
- recipe_path = rl.find_recipe(cookbook, optional_recipe_name)
262
- end
263
- temp_cookbook.from_existing_recipe(recipe_path)
264
- initial_status_msg = TS.converge.converging_recipe(recipe_specifier)
265
- else
266
- resource_type = cli_arguments.shift
267
- resource_name = cli_arguments.shift
268
- temp_cookbook.from_resource(resource_type, resource_name, format_properties(cli_arguments))
269
- full_rs_name = "#{resource_type}[#{resource_name}]"
270
- ChefApply::Log.debug("Converging resource #{full_rs_name} on target")
271
- initial_status_msg = TS.converge.converging_resource(full_rs_name)
272
- end
273
-
274
- [temp_cookbook, initial_status_msg]
275
- end
276
-
277
- def recipe_strategy?(cli_arguments)
278
- cli_arguments.size == 1
279
- end
280
-
281
- def create_local_policy(local_cookbook)
282
- require "chef-dk/ui"
283
- require "chef-dk/policyfile_services/export_repo"
284
- require "chef-dk/policyfile_services/install"
285
- policyfile_installer = ChefDK::PolicyfileServices::Install.new(
286
- ui: ChefDK::UI.null(),
287
- root_dir: local_cookbook.path
288
- )
289
- begin
290
- policyfile_installer.run
291
- rescue ChefDK::PolicyfileInstallError => e
292
- raise PolicyfileInstallError.new(e)
293
- end
294
- lock_path = File.join(local_cookbook.path, "Policyfile.lock.json")
295
- es = ChefDK::PolicyfileServices::ExportRepo.new(policyfile: lock_path,
296
- root_dir: local_cookbook.path,
297
- export_dir: File.join(local_cookbook.path, "export"),
298
- archive: true,
299
- force: true)
300
- es.run
301
- es.archive_file_location
155
+ # Accepts a target_host and establishes the connection to that host
156
+ # while providing visual feedback via the Terminal API.
157
+ def connect_target(target_host, reporter)
158
+ connect_message = T.status.connecting(target_host.user)
159
+ reporter.update(connect_message)
160
+ do_connect(target_host, reporter)
302
161
  end
303
162
 
304
- # Runs the InstallChef action and renders UI updates as
305
- # the action reports back
306
163
  def install(target_host, reporter)
307
- installer = Action::InstallChef.instance_for_target(target_host, check_only: !parsed_options[:install])
308
164
  context = TS.install_chef
165
+ reporter.update(context.verifying)
166
+ installer = Action::InstallChef.instance_for_target(target_host, check_only: !parsed_options[:install])
309
167
  installer.run do |event, data|
310
168
  case event
311
169
  when :installing
@@ -320,39 +178,82 @@ module ChefApply
320
178
  when :downloading
321
179
  reporter.update(context.downloading)
322
180
  when :already_installed
323
- meth = @multi_target ? :update : :success
324
- reporter.send(meth, context.already_present(target_host.installed_chef_version))
181
+ reporter.update(context.already_present(target_host.installed_chef_version))
325
182
  when :install_complete
326
- meth = @multi_target ? :update : :success
327
183
  if installer.upgrading?
328
184
  message = context.upgrade_success(target_host.installed_chef_version, installer.version_to_install)
329
185
  else
330
186
  message = context.install_success(installer.version_to_install)
331
187
  end
332
- reporter.send(meth, message)
188
+ reporter.update(message)
189
+ else
190
+ handle_message(event, data, reporter)
191
+ end
192
+ end
193
+ end
194
+
195
+ # Runs a GenerateCookbook action based on recipe/resource infoprovided
196
+ # and renders UI updates as the action reports back
197
+ def generate_temp_cookbook(arguments, reporter)
198
+ opts = if arguments.length == 1
199
+ { recipe_spec: arguments.shift,
200
+ cookbook_repo_paths: parsed_options[:cookbook_repo_paths] }
201
+ else
202
+ { resource_type: arguments.shift,
203
+ resource_name: arguments.shift,
204
+ resource_properties: properties_from_string(arguments) }
205
+ end
206
+ action = ChefApply::Action::GenerateTempCookbook.from_options(opts)
207
+ action.run do |event, data|
208
+ case event
209
+ when :generating
210
+ reporter.update(TS.generate_temp_cookbook.generating)
211
+ when :success
212
+ reporter.success(TS.generate_temp_cookbook.success)
213
+ else
214
+ handle_message(event, data, reporter)
215
+ end
216
+ end
217
+ action.generated_cookbook
218
+ end
219
+
220
+ # Runs the GenerateLocalPolicy action and renders UI updates
221
+ # as the action reports back
222
+ def generate_local_policy(reporter)
223
+ action = Action::GenerateLocalPolicy.new(cookbook: temp_cookbook)
224
+ action.run do |event, data|
225
+ case event
226
+ when :generating
227
+ reporter.update(TS.generate_local_policy.generating)
228
+ when :exporting
229
+ reporter.update(TS.generate_local_policy.exporting)
230
+ when :success
231
+ reporter.success(TS.generate_local_policy.success)
333
232
  else
334
233
  handle_message(event, data, reporter)
335
234
  end
336
235
  end
236
+ action.archive_file_location
337
237
  end
338
238
 
339
239
  # Runs the Converge action and renders UI updates as
340
240
  # the action reports back
341
241
  def converge(reporter, local_policy_path, target_host)
242
+ reporter.update(TS.converge.converging(temp_cookbook.descriptor))
342
243
  converge_args = { local_policy_path: local_policy_path, target_host: target_host }
343
244
  converger = Action::ConvergeTarget.new(converge_args)
344
245
  converger.run do |event, data|
345
246
  case event
346
247
  when :success
347
- reporter.success(TS.converge.success)
248
+ reporter.success(TS.converge.success(temp_cookbook.descriptor))
348
249
  when :converge_error
349
- reporter.error(TS.converge.failure)
250
+ reporter.error(TS.converge.failure(temp_cookbook.descriptor))
350
251
  when :creating_remote_policy
351
252
  reporter.update(TS.converge.creating_remote_policy)
352
253
  when :uploading_trusted_certs
353
254
  reporter.update(TS.converge.uploading_trusted_certs)
354
255
  when :running_chef
355
- reporter.update(TS.converge.running_chef)
256
+ reporter.update(TS.converge.converging(temp_cookbook.descriptor))
356
257
  when :reboot
357
258
  reporter.success(TS.converge.reboot)
358
259
  else
@@ -362,13 +263,14 @@ module ChefApply
362
263
  end
363
264
 
364
265
  def handle_perform_error(e)
266
+ require "chef_apply/errors/standard_error_resolver"
365
267
  id = e.respond_to?(:id) ? e.id : e.class.to_s
366
268
  # TODO: This is currently sending host information for certain ssh errors
367
269
  # post release we need to scrub this data. For now I'm redacting the
368
270
  # whole message.
369
271
  # message = e.respond_to?(:message) ? e.message : e.to_s
370
272
  Telemeter.capture(:error, exception: { id: id, message: "redacted" })
371
- wrapper = ChefApply::StandardErrorResolver.wrap_exception(e)
273
+ wrapper = ChefApply::Errors::StandardErrorResolver.wrap_exception(e)
372
274
  capture_exception_backtrace(wrapper)
373
275
  # Now that our housekeeping is done, allow user-facing handling/formatting
374
276
  # in `run` to execute by re-raising
@@ -377,13 +279,16 @@ module ChefApply
377
279
 
378
280
  # When running multiple jobs, exceptions are captured to the
379
281
  # job to avoid interrupting other jobs in process. This function
380
- # collects them and raises a MultiJobFailure if failure has occurred;
381
- # we do *not* differentiate between one failed jobs and multiple failed jobs
382
- # - if you're in the 'multi-job' path (eg, multiple targets) we handle
383
- # all errors the same to provide a consistent UX when running with mulitiple targets.
282
+ # collects them and raises directly (in the case of just one job in the list)
283
+ # or raises a MultiJobFailure (when more than one job was being run)
384
284
  def handle_job_failures(jobs)
385
285
  failed_jobs = jobs.select { |j| !j.exception.nil? }
386
286
  return if failed_jobs.empty?
287
+ if jobs.length == 1
288
+ # Don't provide a bad UX by showing a 'one or more jobs has failed'
289
+ # message when there was only one job.
290
+ raise jobs.first.exception
291
+ end
387
292
  raise ChefApply::MultiJobFailure.new(failed_jobs)
388
293
  end
389
294
 
@@ -399,72 +304,14 @@ module ChefApply
399
304
  UI::ErrorPrinter.write_backtrace(e, @argv)
400
305
  end
401
306
 
402
- def show_help
403
- UI::Terminal.output format_help
404
- end
405
-
406
- def do_connect(target_host, reporter, update_method)
307
+ def do_connect(target_host, reporter)
407
308
  target_host.connect!
408
- reporter.send(update_method, T.status.connected)
309
+ reporter.update(T.status.connected)
409
310
  rescue StandardError => e
410
311
  message = ChefApply::UI::ErrorPrinter.error_summary(e)
411
312
  reporter.error(message)
412
313
  raise
413
314
  end
414
315
 
415
- def format_help
416
- help_text = banner.clone # This prevents us appending to the banner text
417
- help_text << "\n"
418
- help_text << format_flags
419
- end
420
-
421
- def format_flags
422
- flag_text = "FLAGS:\n"
423
- justify_length = 0
424
- options.each_value do |spec|
425
- justify_length = [justify_length, spec[:long].length + 4].max
426
- end
427
- options.sort.to_h.each_value do |flag_spec|
428
- short = flag_spec[:short] || " "
429
- short = short[0, 2] # We only want the flag portion, not the capture portion (if present)
430
- if short == " "
431
- short = " "
432
- else
433
- short = "#{short}, "
434
- end
435
- flags = "#{short}#{flag_spec[:long]}"
436
- flag_text << " #{flags.ljust(justify_length)} "
437
- ml_padding = " " * (justify_length + 8)
438
- first = true
439
- flag_spec[:description].split("\n").each do |d|
440
- flag_text << ml_padding unless first
441
- first = false
442
- flag_text << "#{d}\n"
443
- end
444
- end
445
- flag_text
446
- end
447
-
448
- def usage
449
- T.usage
450
- end
451
-
452
- def show_version
453
- UI::Terminal.output T.version.show(ChefApply::VERSION)
454
- end
455
-
456
- class OptionValidationError < ChefApply::ErrorNoLogs
457
- attr_reader :command
458
- def initialize(id, calling_command, *args)
459
- super(id, *args)
460
- # TODO - this is getting cumbersome - move them to constructor options hash in base
461
- @decorate = false
462
- @command = calling_command
463
- end
464
- end
465
-
466
- class PolicyfileInstallError < ChefApply::Error
467
- def initialize(cause_err); super("CHEFPOLICY001", cause_err.message); end
468
- end
469
316
  end
470
317
  end