test-kitchen 4.0.0 → 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.
@@ -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
@@ -287,6 +289,46 @@ module Kitchen
287
289
  state_file.read[:last_error]
288
290
  end
289
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
+
290
332
  # Clean up any per-instance resources before exiting.
291
333
  #
292
334
  # @return [void]
@@ -406,12 +448,14 @@ module Kitchen
406
448
  # @return [self] this instance, used to chain actions
407
449
  # @api private
408
450
  def converge_action
409
- banner "Converging #{to_str}..."
410
451
  elapsed = action(:converge) do |state|
452
+ banner "Converging #{to_str}..."
411
453
  provisioner.check_license
412
454
  provisioner.call(state)
413
455
  end
414
- info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
456
+ with_current_action_metadata(:converge) do
457
+ info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
458
+ end
415
459
  self
416
460
  end
417
461
 
@@ -421,10 +465,12 @@ module Kitchen
421
465
  # @return [self] this instance, used to chain actions
422
466
  # @api private
423
467
  def setup_action
424
- banner "Setting up #{to_str}..."
425
468
  elapsed = action(:setup) do |state|
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)}.")
426
473
  end
427
- info("Finished setting up #{to_str} #{Util.duration(elapsed.real)}.")
428
474
  self
429
475
  end
430
476
 
@@ -434,11 +480,13 @@ module Kitchen
434
480
  # @return [self] this instance, used to chain actions
435
481
  # @api private
436
482
  def verify_action
437
- banner "Verifying #{to_str}..."
438
483
  elapsed = action(:verify) do |state|
484
+ banner "Verifying #{to_str}..."
439
485
  verifier.call(state)
440
486
  end
441
- info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
487
+ with_current_action_metadata(:verify) do
488
+ info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
489
+ end
442
490
  self
443
491
  end
444
492
 
@@ -460,10 +508,14 @@ module Kitchen
460
508
  # @return [self] this instance, used to chain actions
461
509
  # @api private
462
510
  def perform_action(verb, output_verb)
463
- banner "#{output_verb} #{to_str}..."
464
- elapsed = action(verb) { |state| driver.public_send(verb, state) }
465
- info("Finished #{output_verb.downcase} #{to_str}" \
466
- " #{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
467
519
  yield if block_given?
468
520
  self
469
521
  end
@@ -488,64 +540,7 @@ module Kitchen
488
540
  # occurred, etc.
489
541
  # @api private
490
542
  def action(what, &block)
491
- state = state_file.read
492
- elapsed = Benchmark.measure do
493
- synchronize_or_call(what, state, &block)
494
- end
495
- state[:last_action] = what.to_s
496
- state[:last_error] = nil
497
- elapsed
498
- rescue ActionFailed => e
499
- log_failure(what, e)
500
- state[:last_error] = e.class.name
501
- raise(InstanceFailure, failure_message(what) +
502
- " Please see .kitchen/logs/#{name}.log for more details",
503
- e.backtrace)
504
- rescue Exception => e # rubocop:disable Lint/RescueException
505
- log_failure(what, e)
506
- state[:last_error] = e.class.name
507
- raise ActionFailed,
508
- "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
509
- ensure
510
- state_file.write(state)
511
- end
512
-
513
- # Runs a given action block through a common driver mutex if required or
514
- # runs it directly otherwise. If a driver class' `.serial_actions` array
515
- # includes the desired action, then the action must be run with a muxtex
516
- # lock. Otherwise, it is assumed that the action can happen concurrently,
517
- # or fully in parallel.
518
- #
519
- # @param what [Symbol] the action to be performed
520
- # @param state [Hash] a mutable state hash for this instance
521
- # @param block [Proc] a block to be called
522
- # @api private
523
- def synchronize_or_call(what, state)
524
- plugin_class = plugin_class_for_action(what)
525
- if Array(plugin_class.serial_actions).include?(what)
526
- debug("#{to_str} is synchronizing on #{plugin_class}##{what}")
527
- self.class.mutexes[plugin_class].synchronize do
528
- debug("#{to_str} is messaging #{plugin_class}##{what}")
529
- yield(state)
530
- end
531
- else
532
- yield(state)
533
- end
534
- end
535
-
536
- # Maps the given action to the plugin class associated with the action
537
- #
538
- # @param what[Symbol] action
539
- # @return [Class] Kitchen::Plugin::Base
540
- # @api private
541
- def plugin_class_for_action(what)
542
- {
543
- create: driver,
544
- setup: transport,
545
- converge: provisioner,
546
- verify: verifier,
547
- destroy: driver,
548
- }[what].class
543
+ action_runner.call(what, &block)
549
544
  end
550
545
 
551
546
  # Writes a high level message for logging and/or output.
@@ -557,32 +552,200 @@ module Kitchen
557
552
  # @api private
558
553
  def banner(*args)
559
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
560
560
  super
561
561
  end
562
562
 
563
- # Logs a failure (message and backtrace) to the instance's file logger
564
- # to help with debugging and diagnosing issues without overwhelming the
565
- # console output in the default case (i.e. running kitchen with :info
566
- # level debugging).
567
- #
568
- # @param what [String] an action
569
- # @param e [Exception] an exception
570
- # @api private
571
- def log_failure(what, e)
572
- return if logger.logdev.nil?
563
+ def action_runner
564
+ @action_runner ||= ActionRunner.new(self, state_file)
565
+ end
566
+
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
577
+ end
578
+
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)
589
+ end
573
590
 
574
- logger.logdev.error(failure_message(what))
575
- Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
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
576
601
  end
577
602
 
578
- # Returns a string explaining what action failed, at a high level. Used
579
- # for displaying to end user.
580
- #
581
- # @param what [String] an action
582
- # @return [String] a failure message
583
- # @api private
584
- def failure_message(what)
585
- "#{what.capitalize} failed on instance #{to_str}."
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
612
+ end
613
+
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
636
+ end
637
+
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
586
749
  end
587
750
 
588
751
  # The simplest finite state machine pseudo-implementation needed to manage
@@ -21,8 +21,11 @@ module Kitchen
21
21
  begin
22
22
  conn = instance.transport.connection(state_file.read)
23
23
  conn.execute(command)
24
- rescue Kitchen::Transport::SshFailed => e
25
- return if hook[:skippable] && e.message.match(/^SSH exited \(\d{1,3}\) for command: \[.+\]$/)
24
+ rescue Kitchen::Transport::TransportFailed => e
25
+ if hook[:skippable] && e.message.match(/^SSH exited \(\d{1,3}\) for command: \[.+\]$/)
26
+ logger.debug("Skipping remote lifecycle hook #{command.inspect}: #{e.message}")
27
+ return
28
+ end
26
29
 
27
30
  raise
28
31
  end