test-kitchen 3.9.1 → 4.1.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -5
  3. data/lib/kitchen/cli.rb +33 -8
  4. data/lib/kitchen/collection.rb +1 -1
  5. data/lib/kitchen/command/list.rb +49 -9
  6. data/lib/kitchen/command/logs.rb +185 -0
  7. data/lib/kitchen/command/test.rb +2 -1
  8. data/lib/kitchen/command.rb +22 -9
  9. data/lib/kitchen/config.rb +8 -0
  10. data/lib/kitchen/configurable.rb +4 -4
  11. data/lib/kitchen/data_munger.rb +54 -21
  12. data/lib/kitchen/driver/base.rb +19 -0
  13. data/lib/kitchen/driver/dummy.rb +27 -0
  14. data/lib/kitchen/driver/proxy.rb +11 -6
  15. data/lib/kitchen/errors.rb +1 -0
  16. data/lib/kitchen/generator/init.rb +1 -1
  17. data/lib/kitchen/instance.rb +248 -182
  18. data/lib/kitchen/lazy_hash.rb +1 -1
  19. data/lib/kitchen/lifecycle_hook/remote.rb +5 -2
  20. data/lib/kitchen/logger.rb +407 -74
  21. data/lib/kitchen/logging.rb +2 -2
  22. data/lib/kitchen/metadata_chopper.rb +1 -1
  23. data/lib/kitchen/provisioner/base.rb +27 -35
  24. data/lib/kitchen/provisioner/external.rb +420 -0
  25. data/lib/kitchen/provisioner.rb +1 -2
  26. data/lib/kitchen/shell_out.rb +1 -1
  27. data/lib/kitchen/state_file.rb +12 -2
  28. data/lib/kitchen/transport/base.rb +1 -1
  29. data/lib/kitchen/transport/ssh.rb +5 -5
  30. data/lib/kitchen/transport/winrm.rb +8 -8
  31. data/lib/kitchen/util.rb +1 -1
  32. data/lib/kitchen/verifier/base.rb +17 -5
  33. data/lib/kitchen/version.rb +1 -1
  34. data/lib/kitchen.rb +21 -4
  35. data/lib/vendor/hash_recursive_merge.rb +2 -2
  36. data/templates/driver/driver.rb.erb +1 -1
  37. data/test-kitchen.gemspec +5 -7
  38. metadata +36 -41
  39. data/lib/kitchen/driver/ssh_base.rb +0 -346
  40. data/lib/kitchen/provisioner/chef/berkshelf.rb +0 -116
  41. data/lib/kitchen/provisioner/chef/common_sandbox.rb +0 -352
  42. data/lib/kitchen/provisioner/chef/policyfile.rb +0 -173
  43. data/lib/kitchen/provisioner/chef_apply.rb +0 -121
  44. data/lib/kitchen/provisioner/chef_base.rb +0 -723
  45. data/lib/kitchen/provisioner/chef_infra.rb +0 -167
  46. data/lib/kitchen/provisioner/chef_solo.rb +0 -82
  47. data/lib/kitchen/provisioner/chef_target.rb +0 -130
  48. data/lib/kitchen/provisioner/chef_zero.rb +0 -12
  49. data/lib/kitchen/ssh.rb +0 -296
@@ -16,6 +16,7 @@ require_relative "../lazy_hash"
16
16
  require_relative "../logging"
17
17
  require_relative "../plugin_base"
18
18
  require_relative "../shell_out"
19
+ require "time" unless defined?(Time)
19
20
 
20
21
  module Kitchen
21
22
  module Driver
@@ -63,6 +64,24 @@ module Kitchen
63
64
  false
64
65
  end
65
66
 
67
+ # Reports whether the backing instance is known to be live.
68
+ #
69
+ # Drivers that can ask their provider should override this method.
70
+ # Existing drivers remain compatible by inheriting an unknown status.
71
+ #
72
+ # @param state [Hash] mutable instance and driver state
73
+ # @return [Hash] normalized status data
74
+ def status(state) # rubocop:disable Lint/UnusedMethodArgument
75
+ {
76
+ live: nil,
77
+ state: "unknown",
78
+ source: "driver",
79
+ resource_id: nil,
80
+ message: "#{self.class} does not support status checks",
81
+ checked_at: Time.now.utc.iso8601,
82
+ }
83
+ end
84
+
66
85
  # Sets the API version for this driver. If the driver does not set this
67
86
  # value, then `nil` will be used and reported.
68
87
  #
@@ -11,6 +11,7 @@
11
11
  # limitations under the License.
12
12
 
13
13
  require_relative "../../kitchen"
14
+ require "time" unless defined?(Time)
14
15
 
15
16
  module Kitchen
16
17
  module Driver
@@ -51,6 +52,32 @@ module Kitchen
51
52
  state.delete(:my_id)
52
53
  end
53
54
 
55
+ # Reports whether the dummy instance has a generated resource id.
56
+ #
57
+ # @param state [Hash] mutable instance and driver state
58
+ # @return [Hash] normalized status data
59
+ def status(state)
60
+ if state[:my_id]
61
+ {
62
+ live: true,
63
+ state: "running",
64
+ source: "driver",
65
+ resource_id: state[:my_id],
66
+ message: "Dummy instance exists",
67
+ checked_at: Time.now.utc.iso8601,
68
+ }
69
+ else
70
+ {
71
+ live: false,
72
+ state: "not_created",
73
+ source: "driver",
74
+ resource_id: nil,
75
+ message: "Dummy instance has no resource id",
76
+ checked_at: Time.now.utc.iso8601,
77
+ }
78
+ end
79
+ end
80
+
54
81
  private
55
82
 
56
83
  # Report what action is taking place, sleeping if so configured, and
@@ -11,7 +11,7 @@
11
11
  # limitations under the License.
12
12
  #
13
13
 
14
- require_relative "ssh_base"
14
+ require_relative "base"
15
15
  require_relative "../version"
16
16
 
17
17
  module Kitchen
@@ -23,7 +23,7 @@ module Kitchen
23
23
  # the driver was created.
24
24
  #
25
25
  # @author Seth Chisamore <schisamo@opscode.com>
26
- class Proxy < Kitchen::Driver::SSHBase
26
+ class Proxy < Kitchen::Driver::Base
27
27
  plugin_version Kitchen::VERSION
28
28
 
29
29
  required_config :host
@@ -33,8 +33,11 @@ module Kitchen
33
33
 
34
34
  # (see Base#create)
35
35
  def create(state)
36
- # TODO: Once this isn't using SSHBase, it should call `super` to support pre_create_command.
36
+ super
37
37
  state[:hostname] = config[:host]
38
+ state[:port] = config[:port] if config[:port]
39
+ state[:username] = config[:username] if config[:username]
40
+ state[:password] = config[:password] if config[:password]
38
41
  reset_instance(state)
39
42
  end
40
43
 
@@ -48,15 +51,17 @@ module Kitchen
48
51
 
49
52
  private
50
53
 
51
- # Resets the non-Kitchen managed instance using by issuing a command
52
- # over SSH.
54
+ # Resets the non-Kitchen managed instance by issuing a command
55
+ # over the transport.
53
56
  #
54
57
  # @param state [Hash] the state hash
55
58
  # @api private
56
59
  def reset_instance(state)
57
60
  if (cmd = config[:reset_command])
58
61
  info("Resetting instance state with command: #{cmd}")
59
- ssh(build_ssh_args(state), cmd)
62
+ instance.transport.connection(state) do |conn|
63
+ conn.execute(cmd)
64
+ end
60
65
  end
61
66
  end
62
67
  end
@@ -206,6 +206,7 @@ module Kitchen
206
206
  Kitchen.logger.debug(line)
207
207
  else
208
208
  Kitchen.logger.logdev && Kitchen.logger.logdev.public_send(level, line)
209
+ Kitchen.logger.structured_logdev && Kitchen.logger.structured_logdev.public_send(level, line)
209
210
  end
210
211
  end
211
212
  end
@@ -39,7 +39,7 @@ module Kitchen
39
39
  class_option :provisioner,
40
40
  type: :string,
41
41
  aliases: "-P",
42
- default: "chef_infra",
42
+ default: "shell",
43
43
  desc: <<-D.gsub(/^\s+/, "").tr("\n", " ")
44
44
  The default Kitchen Provisioner to use
45
45
  D
@@ -17,6 +17,8 @@
17
17
 
18
18
  require "benchmark" unless defined?(Benchmark)
19
19
  require "fileutils" unless defined?(FileUtils)
20
+ require "securerandom" unless defined?(SecureRandom)
21
+ require "time" unless defined?(Time)
20
22
 
21
23
  module Kitchen
22
24
  # An instance of a suite running on a platform. A created instance may be a
@@ -210,11 +212,7 @@ module Kitchen
210
212
  raise UserError, "Instance #{to_str} has not yet been created"
211
213
  end
212
214
 
213
- lc = if legacy_ssh_base_driver?
214
- legacy_ssh_base_login(state)
215
- else
216
- transport.connection(state).login_command
217
- end
215
+ lc = transport.connection(state).login_command
218
216
 
219
217
  debug(%{Login command: #{lc.command} #{lc.arguments.join(" ")} } \
220
218
  "(Options: #{lc.options})")
@@ -291,6 +289,46 @@ module Kitchen
291
289
  state_file.read[:last_error]
292
290
  end
293
291
 
292
+ # Returns the current instance session identifier, if one has been
293
+ # established.
294
+ #
295
+ # @return [String,nil] the current instance session id
296
+ def current_session_id
297
+ state_file.read[:instance_session_id]
298
+ end
299
+
300
+ # Returns the path to the text log file for this instance.
301
+ #
302
+ # @return [String,nil] the instance text log path
303
+ def log_path
304
+ logger.logdev_path
305
+ end
306
+
307
+ # Returns the path to the structured log file for this instance.
308
+ #
309
+ # @return [String,nil] the instance structured log path
310
+ def structured_log_path
311
+ logger.structured_logdev_path
312
+ end
313
+
314
+ # Returns the path to the state file for this instance.
315
+ #
316
+ # @return [String] the instance state file path
317
+ def state_path
318
+ state_file.path
319
+ end
320
+
321
+ # Returns normalized liveness status for this instance.
322
+ #
323
+ # @param probe [Boolean] whether to probe the transport as well
324
+ # @return [Hash] normalized status data
325
+ def status(probe: false)
326
+ state = state_file.read
327
+ result = driver_status(state)
328
+ result[:transport_probe] = transport_probe(state) if probe
329
+ result
330
+ end
331
+
294
332
  # Clean up any per-instance resources before exiting.
295
333
  #
296
334
  # @return [void]
@@ -410,17 +448,14 @@ module Kitchen
410
448
  # @return [self] this instance, used to chain actions
411
449
  # @api private
412
450
  def converge_action
413
- banner "Converging #{to_str}..."
414
451
  elapsed = action(:converge) do |state|
415
- if legacy_ssh_base_driver?
416
- provisioner.check_license
417
- legacy_ssh_base_converge(state)
418
- else
419
- provisioner.check_license
420
- provisioner.call(state)
421
- end
452
+ banner "Converging #{to_str}..."
453
+ provisioner.check_license
454
+ provisioner.call(state)
455
+ end
456
+ with_current_action_metadata(:converge) do
457
+ info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
422
458
  end
423
- info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
424
459
  self
425
460
  end
426
461
 
@@ -430,47 +465,28 @@ module Kitchen
430
465
  # @return [self] this instance, used to chain actions
431
466
  # @api private
432
467
  def setup_action
433
- banner "Setting up #{to_str}..."
434
468
  elapsed = action(:setup) do |state|
435
- legacy_ssh_base_setup(state) if legacy_ssh_base_driver?
469
+ banner "Setting up #{to_str}..."
470
+ end
471
+ with_current_action_metadata(:setup) do
472
+ info("Finished setting up #{to_str} #{Util.duration(elapsed.real)}.")
436
473
  end
437
- info("Finished setting up #{to_str} #{Util.duration(elapsed.real)}.")
438
474
  self
439
475
  end
440
476
 
441
- # returns true, if the verifier is busser
442
- def verifier_busser?(verifier)
443
- !defined?(Kitchen::Verifier::Busser).nil? && verifier.is_a?(Kitchen::Verifier::Busser)
444
- end
445
-
446
- # returns true, if the verifier is dummy
447
- def verifier_dummy?(verifier)
448
- !defined?(Kitchen::Verifier::Dummy).nil? && verifier.is_a?(Kitchen::Verifier::Dummy)
449
- end
450
-
451
- def use_legacy_ssh_verifier?(verifier)
452
- verifier_busser?(verifier) || verifier_dummy?(verifier)
453
- end
454
-
455
477
  # Perform the verify action.
456
478
  #
457
479
  # @see Driver::Base#verify
458
480
  # @return [self] this instance, used to chain actions
459
481
  # @api private
460
482
  def verify_action
461
- banner "Verifying #{to_str}..."
462
483
  elapsed = action(:verify) do |state|
463
- # use special handling for legacy driver
464
- if legacy_ssh_base_driver? && use_legacy_ssh_verifier?(verifier)
465
- legacy_ssh_base_verify(state)
466
- elsif legacy_ssh_base_driver?
467
- # read ssh options from legacy driver
468
- verifier.call(driver.legacy_state(state))
469
- else
470
- verifier.call(state)
471
- end
484
+ banner "Verifying #{to_str}..."
485
+ verifier.call(state)
486
+ end
487
+ with_current_action_metadata(:verify) do
488
+ info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
472
489
  end
473
- info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
474
490
  self
475
491
  end
476
492
 
@@ -488,14 +504,18 @@ module Kitchen
488
504
  # @param verb [Symbol] the action to be performed
489
505
  # @param output_verb [String] a verb representing the action, suitable for
490
506
  # use in output logging
491
- # @yield perform optional work just after action has complted
507
+ # @yield perform optional work just after action has completed
492
508
  # @return [self] this instance, used to chain actions
493
509
  # @api private
494
510
  def perform_action(verb, output_verb)
495
- banner "#{output_verb} #{to_str}..."
496
- elapsed = action(verb) { |state| driver.public_send(verb, state) }
497
- info("Finished #{output_verb.downcase} #{to_str}" \
498
- " #{Util.duration(elapsed.real)}.")
511
+ elapsed = action(verb) do |state|
512
+ banner "#{output_verb} #{to_str}..."
513
+ driver.public_send(verb, state)
514
+ end
515
+ with_current_action_metadata(verb) do
516
+ info("Finished #{output_verb.downcase} #{to_str}" \
517
+ " #{Util.duration(elapsed.real)}.")
518
+ end
499
519
  yield if block_given?
500
520
  self
501
521
  end
@@ -520,64 +540,7 @@ module Kitchen
520
540
  # occurred, etc.
521
541
  # @api private
522
542
  def action(what, &block)
523
- state = state_file.read
524
- elapsed = Benchmark.measure do
525
- synchronize_or_call(what, state, &block)
526
- end
527
- state[:last_action] = what.to_s
528
- state[:last_error] = nil
529
- elapsed
530
- rescue ActionFailed => e
531
- log_failure(what, e)
532
- state[:last_error] = e.class.name
533
- raise(InstanceFailure, failure_message(what) +
534
- " Please see .kitchen/logs/#{name}.log for more details",
535
- e.backtrace)
536
- rescue Exception => e # rubocop:disable Lint/RescueException
537
- log_failure(what, e)
538
- state[:last_error] = e.class.name
539
- raise ActionFailed,
540
- "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
541
- ensure
542
- state_file.write(state)
543
- end
544
-
545
- # Runs a given action block through a common driver mutex if required or
546
- # runs it directly otherwise. If a driver class' `.serial_actions` array
547
- # includes the desired action, then the action must be run with a muxtex
548
- # lock. Otherwise, it is assumed that the action can happen concurrently,
549
- # or fully in parallel.
550
- #
551
- # @param what [Symbol] the action to be performed
552
- # @param state [Hash] a mutable state hash for this instance
553
- # @param block [Proc] a block to be called
554
- # @api private
555
- def synchronize_or_call(what, state)
556
- plugin_class = plugin_class_for_action(what)
557
- if Array(plugin_class.serial_actions).include?(what)
558
- debug("#{to_str} is synchronizing on #{plugin_class}##{what}")
559
- self.class.mutexes[plugin_class].synchronize do
560
- debug("#{to_str} is messaging #{plugin_class}##{what}")
561
- yield(state)
562
- end
563
- else
564
- yield(state)
565
- end
566
- end
567
-
568
- # Maps the given action to the plugin class associated with the action
569
- #
570
- # @param what[Symbol] action
571
- # @return [Class] Kitchen::Plugin::Base
572
- # @api private
573
- def plugin_class_for_action(what)
574
- {
575
- create: driver,
576
- setup: transport,
577
- converge: provisioner,
578
- verify: verifier,
579
- destroy: driver,
580
- }[what].class
543
+ action_runner.call(what, &block)
581
544
  end
582
545
 
583
546
  # Writes a high level message for logging and/or output.
@@ -589,97 +552,200 @@ module Kitchen
589
552
  # @api private
590
553
  def banner(*args)
591
554
  Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
555
+ if Kitchen.logger.structured_logdev
556
+ Kitchen.logger.with_metadata(logger.metadata) do
557
+ Kitchen.logger.structured_logdev.banner(*args)
558
+ end
559
+ end
592
560
  super
593
561
  end
594
562
 
595
- # Logs a failure (message and backtrace) to the instance's file logger
596
- # to help with debugging and diagnosing issues without overwhelming the
597
- # console output in the default case (i.e. running kitchen with :info
598
- # level debugging).
599
- #
600
- # @param what [String] an action
601
- # @param e [Exception] an exception
602
- # @api private
603
- def log_failure(what, e)
604
- return if logger.logdev.nil?
605
-
606
- logger.logdev.error(failure_message(what))
607
- Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
563
+ def action_runner
564
+ @action_runner ||= ActionRunner.new(self, state_file)
608
565
  end
609
566
 
610
- # Returns a string explaining what action failed, at a high level. Used
611
- # for displaying to end user.
612
- #
613
- # @param what [String] an action
614
- # @return [String] a failure message
615
- # @api private
616
- def failure_message(what)
617
- "#{what.capitalize} failed on instance #{to_str}."
567
+ def with_current_action_metadata(what)
568
+ state = state_file.read
569
+ logger.with_metadata(
570
+ action: what.to_s,
571
+ action_id: state[:last_action_id],
572
+ instance_session_id: state[:instance_session_id],
573
+ kitchen_run_id: state[:last_kitchen_run_id] || Kitchen.run_id
574
+ ) do
575
+ yield
576
+ end
618
577
  end
619
578
 
620
- # Invokes `Driver#converge` on a legacy Driver, which inherits from
621
- # `Kitchen::Driver::SSHBase`.
622
- #
623
- # @param state [Hash] mutable instance state
624
- # @deprecated When legacy Driver::SSHBase support is removed, the
625
- # `#converge` method will no longer be called on the Driver.
626
- # @api private
627
- def legacy_ssh_base_converge(state)
628
- warn("Running legacy converge for '#{driver.name}' Driver")
629
- # TODO: Document upgrade path and provide link
630
- # warn("Driver authors: please read http://example.com for more details.")
631
- driver.converge(state)
579
+ def driver_status(state)
580
+ if driver.method(:status).arity == 0
581
+ return unknown_driver_status(
582
+ "Driver status method does not accept Kitchen state"
583
+ )
584
+ end
585
+
586
+ normalize_driver_status(driver.status(state))
587
+ rescue Exception => e # rubocop:disable Lint/RescueException
588
+ unknown_driver_status(e.message, e.class.name)
632
589
  end
633
590
 
634
- # @return [TrueClass,FalseClass] whether or not the Driver inherits from
635
- # `Kitchen::Driver::SSHBase`
636
- # @deprecated When legacy Driver::SSHBase support is removed, the
637
- # `#converge` method will no longer be called on the Driver.
638
- # @api private
639
- def legacy_ssh_base_driver?
640
- driver.class < Kitchen::Driver::SSHBase
591
+ def normalize_driver_status(status)
592
+ status = Util.symbolized_hash(status || {})
593
+ {
594
+ live: status[:live],
595
+ state: status[:state] || "unknown",
596
+ source: status[:source] || "driver",
597
+ resource_id: status[:resource_id],
598
+ message: status[:message],
599
+ checked_at: status[:checked_at] || Time.now.utc.iso8601,
600
+ }.compact
641
601
  end
642
602
 
643
- # Invokes `Driver#login_command` on a legacy Driver, which inherits from
644
- # `Kitchen::Driver::SSHBase`.
645
- #
646
- # @param state [Hash] mutable instance state
647
- # @deprecated When legacy Driver::SSHBase support is removed, the
648
- # `#login_command` method will no longer be called on the Driver.
649
- # @api private
650
- def legacy_ssh_base_login(state)
651
- warn("Running legacy login for '#{driver.name}' Driver")
652
- # TODO: Document upgrade path and provide link
653
- # warn("Driver authors: please read http://example.com for more details.")
654
- driver.login_command(state)
603
+ def unknown_driver_status(message, error = nil)
604
+ {
605
+ live: nil,
606
+ state: "unknown",
607
+ source: "driver",
608
+ error:,
609
+ message:,
610
+ checked_at: Time.now.utc.iso8601,
611
+ }.compact
655
612
  end
656
613
 
657
- # Invokes `Driver#setup` on a legacy Driver, which inherits from
658
- # `Kitchen::Driver::SSHBase`.
659
- #
660
- # @param state [Hash] mutable instance state
661
- # @deprecated When legacy Driver::SSHBase support is removed, the
662
- # `#setup` method will no longer be called on the Driver.
663
- # @api private
664
- def legacy_ssh_base_setup(state)
665
- warn("Running legacy setup for '#{driver.name}' Driver")
666
- # TODO: Document upgrade path and provide link
667
- # warn("Driver authors: please read http://example.com for more details.")
668
- driver.setup(state)
614
+ def transport_probe(state)
615
+ return not_created_transport_probe if state[:last_action].nil?
616
+
617
+ transport.connection(state, &:wait_until_ready)
618
+ {
619
+ reachable: true,
620
+ state: "reachable",
621
+ source: "transport",
622
+ message: "Transport connection succeeded",
623
+ checked_at: Time.now.utc.iso8601,
624
+ }
625
+ rescue Exception => e # rubocop:disable Lint/RescueException
626
+ {
627
+ reachable: false,
628
+ state: "unreachable",
629
+ source: "transport",
630
+ error: e.class.name,
631
+ message: e.message,
632
+ checked_at: Time.now.utc.iso8601,
633
+ }
634
+ ensure
635
+ transport.cleanup! if transport
669
636
  end
670
637
 
671
- # Invokes `Driver#verify` on a legacy Driver, which inherits from
672
- # `Kitchen::Driver::SSHBase`.
673
- #
674
- # @param state [Hash] mutable instance state
675
- # @deprecated When legacy Driver::SSHBase support is removed, the
676
- # `#verify` method will no longer be called on the Driver.
677
- # @api private
678
- def legacy_ssh_base_verify(state)
679
- warn("Running legacy verify for '#{driver.name}' Driver")
680
- # TODO: Document upgrade path and provide link
681
- # warn("Driver authors: please read http://example.com for more details.")
682
- driver.verify(state)
638
+ def not_created_transport_probe
639
+ {
640
+ reachable: false,
641
+ state: "not_created",
642
+ source: "transport",
643
+ message: "Instance has not yet been created",
644
+ checked_at: Time.now.utc.iso8601,
645
+ }
646
+ end
647
+
648
+ # Coordinates one instance action, including state persistence, plugin
649
+ # serialization, and failure wrapping.
650
+ class ActionRunner
651
+ def initialize(instance, state_file)
652
+ @instance = instance
653
+ @state_file = state_file
654
+ end
655
+
656
+ def call(what, &block)
657
+ state = nil
658
+ begin
659
+ state = state_file.read
660
+ rescue ::StandardError => e
661
+ log_failure(what, e)
662
+ raise ActionFailed,
663
+ "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
664
+ end
665
+
666
+ action_id = new_id
667
+ session_id = state[:instance_session_id] || new_id
668
+ instance.logger.with_metadata(
669
+ action: what.to_s,
670
+ action_id:,
671
+ instance_session_id: session_id,
672
+ kitchen_run_id: Kitchen.run_id
673
+ ) do
674
+ elapsed = Benchmark.measure do
675
+ synchronize_or_call(what, state, &block)
676
+ end
677
+ record_attempt(state, what, action_id, session_id)
678
+ state[:last_action] = what.to_s
679
+ state[:last_error] = nil
680
+ elapsed
681
+ rescue ActionFailed => e
682
+ log_failure(what, e)
683
+ record_attempt(state, what, action_id, session_id)
684
+ state[:last_error] = e.class.name
685
+ raise(InstanceFailure, failure_message(what) +
686
+ " Please see .kitchen/logs/#{instance.name}.log for more details",
687
+ e.backtrace)
688
+ rescue ::StandardError => e
689
+ log_failure(what, e)
690
+ record_attempt(state, what, action_id, session_id)
691
+ state[:last_error] = e.class.name
692
+ raise ActionFailed,
693
+ "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
694
+ ensure
695
+ state_file.write(state)
696
+ end
697
+ end
698
+
699
+ private
700
+
701
+ attr_reader :instance, :state_file
702
+
703
+ def synchronize_or_call(what, state)
704
+ plugin_class = plugin_class_for_action(what)
705
+ if Array(plugin_class.serial_actions).include?(what)
706
+ instance.debug("#{instance.to_str} is synchronizing on #{plugin_class}##{what}")
707
+ instance.class.mutexes[plugin_class].synchronize do
708
+ instance.debug("#{instance.to_str} is messaging #{plugin_class}##{what}")
709
+ yield(state)
710
+ end
711
+ else
712
+ yield(state)
713
+ end
714
+ end
715
+
716
+ def plugin_class_for_action(what)
717
+ {
718
+ create: instance.driver,
719
+ setup: instance.transport,
720
+ converge: instance.provisioner,
721
+ verify: instance.verifier,
722
+ destroy: instance.driver,
723
+ }[what].class
724
+ end
725
+
726
+ def log_failure(what, e)
727
+ return if instance.logger.logdev.nil? && instance.logger.structured_logdev.nil?
728
+
729
+ [instance.logger.logdev, instance.logger.structured_logdev].compact.each do |logdev|
730
+ logdev.error(failure_message(what))
731
+ Error.formatted_trace(e).each { |line| logdev.error(line) }
732
+ end
733
+ end
734
+
735
+ def failure_message(what)
736
+ "#{what.capitalize} failed on instance #{instance.to_str}."
737
+ end
738
+
739
+ def record_attempt(state, what, action_id, session_id)
740
+ state[:instance_session_id] = session_id
741
+ state[:last_attempted_action] = what.to_s
742
+ state[:last_action_id] = action_id
743
+ state[:last_kitchen_run_id] = Kitchen.run_id
744
+ end
745
+
746
+ def new_id
747
+ SecureRandom.uuid
748
+ end
683
749
  end
684
750
 
685
751
  # The simplest finite state machine pseudo-implementation needed to manage
@@ -689,7 +755,7 @@ module Kitchen
689
755
  # @author Fletcher Nichol <fnichol@nichol.ca>
690
756
  class FSM
691
757
  # Returns an Array of all transitions to bring an Instance from its last
692
- # reported transistioned state into the desired transitioned state.
758
+ # reported transitioned state into the desired transitioned state.
693
759
  #
694
760
  # @param last [String,Symbol,nil] the last known transitioned state of
695
761
  # the Instance, defaulting to `nil` (for unknown or no history)
@@ -18,7 +18,7 @@
18
18
  require "delegate"
19
19
 
20
20
  module Kitchen
21
- # A modifed Hash object that may contain callables as a value which must be
21
+ # A modified Hash object that may contain callables as a value which must be
22
22
  # executed in the context of another object. This allows for delayed
23
23
  # evaluation of a hash value while still looking and largely feeling like a
24
24
  # normal Ruby Hash.