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.
- checksums.yaml +4 -4
- data/Gemfile +6 -4
- data/lib/kitchen/cli.rb +33 -8
- data/lib/kitchen/command/list.rb +49 -9
- data/lib/kitchen/command/logs.rb +185 -0
- data/lib/kitchen/command/test.rb +2 -1
- data/lib/kitchen/command.rb +22 -9
- data/lib/kitchen/config.rb +8 -0
- data/lib/kitchen/data_munger.rb +47 -12
- data/lib/kitchen/driver/base.rb +19 -0
- data/lib/kitchen/driver/dummy.rb +27 -0
- data/lib/kitchen/errors.rb +1 -0
- data/lib/kitchen/instance.rb +251 -88
- data/lib/kitchen/lifecycle_hook/remote.rb +5 -2
- data/lib/kitchen/logger.rb +407 -74
- data/lib/kitchen/logging.rb +2 -2
- data/lib/kitchen/provisioner/base.rb +27 -17
- data/lib/kitchen/provisioner/external.rb +420 -0
- data/lib/kitchen/shell_out.rb +1 -1
- data/lib/kitchen/state_file.rb +12 -2
- data/lib/kitchen/transport/base.rb +1 -1
- data/lib/kitchen/transport/ssh.rb +5 -5
- data/lib/kitchen/verifier/base.rb +17 -5
- data/lib/kitchen/version.rb +1 -1
- data/lib/kitchen.rb +21 -2
- data/test-kitchen.gemspec +1 -1
- metadata +12 -4
data/lib/kitchen/instance.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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::
|
|
25
|
-
|
|
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
|