bolt 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41eb3934a2ddce5d1021b66f5aca6eccfdb91bdedd5dc08cfdc1cfb4664f91e8
4
- data.tar.gz: 8a04142fd9de53e8812cff0c540098a8548a8b18245e300835896b457a86d348
3
+ metadata.gz: aa3e5ea7d868b032557568e9635fcf159f00b81e4961dce4032246cc150a7c16
4
+ data.tar.gz: 0e9fada8a0518ab174412e21ec843091272cb0afe87980703ebd6d8ed91002dd
5
5
  SHA512:
6
- metadata.gz: 24bc37edf14e9c5d3ef1b8df5d5ed9dd0161ebb44da96e0b2777703c5b71182179ebed740e5d67c04bad41b9ecdb6e0a40d5fcb5ec08eb59171da36a3d5a3756
7
- data.tar.gz: 0e5795469709939091282a51df744076e87e40e5dad0f5bc6395d0d7927dcbf5f788bccf23bd5c455c29232faa958ddcbe5130626eb82d056404f8cb1e088cdd
6
+ metadata.gz: 66d8df12e0142b6d5872a40f7c0d97b0438c833e4681be6646e17b85496c3a9e1148ff0a2ff34572e0ce788861d928c406dddcb97feb5281ac0d7eb2d7d14ead
7
+ data.tar.gz: f6dafaa37a4af7633610e26cc084d69c865f0b8df18221d407f2ee077e58e0a582e88cccf49d3a590e7984d56d8ac467ee8704e6ac0e60cc7e37b1a846396d26
@@ -29,7 +29,7 @@ module Bolt
29
29
  yaml_plan_count: :cd13
30
30
  }.freeze
31
31
 
32
- def self.build_client
32
+ def self.build_client(enabled = true)
33
33
  logger = Bolt::Logger.logger(self)
34
34
  begin
35
35
  config_file = config_path
@@ -38,7 +38,7 @@ module Bolt
38
38
  config = { 'disabled' => true }
39
39
  end
40
40
 
41
- if config['disabled'] || ENV['BOLT_DISABLE_ANALYTICS']
41
+ if !enabled || config['disabled'] || ENV['BOLT_DISABLE_ANALYTICS']
42
42
  logger.debug "Analytics opt-out is set, analytics will be disabled"
43
43
  NoopClient.new
44
44
  else
@@ -80,12 +80,8 @@ module Bolt
80
80
  unless ENV['BOLT_DISABLE_ANALYTICS']
81
81
  msg = <<~ANALYTICS
82
82
  Bolt collects data about how you use it. You can opt out of providing this data.
83
-
84
- To disable analytics data collection, add this line to ~/.puppetlabs/etc/bolt/analytics.yaml :
85
- disabled: true
86
-
87
- Read more about what data Bolt collects and why here:
88
- https://puppet.com/docs/bolt/latest/bolt_installing.html#analytics-data-collection
83
+ To learn how to disable data collection, or see what data Bolt collects and why,
84
+ see http://pup.pt/bolt-analytics
89
85
  ANALYTICS
90
86
  Bolt::Logger.warn_once('analytics_opt_out', msg)
91
87
  end
data/lib/bolt/cli.rb CHANGED
@@ -541,7 +541,11 @@ module Bolt
541
541
  end
542
542
  code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
543
543
  else
544
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
544
+ executor = Bolt::Executor.new(config.concurrency,
545
+ analytics,
546
+ options[:noop],
547
+ config.modified_concurrency,
548
+ config.future)
545
549
  targets = options[:targets]
546
550
 
547
551
  results = nil
@@ -631,7 +635,18 @@ module Bolt
631
635
 
632
636
  def list_targets
633
637
  inventoryfile = config.inventoryfile || config.default_inventoryfile
638
+ outputter.print_targets(group_targets_by_source, inventoryfile)
639
+ end
634
640
 
641
+ def show_targets
642
+ inventoryfile = config.inventoryfile || config.default_inventoryfile
643
+ outputter.print_target_info(group_targets_by_source, inventoryfile)
644
+ end
645
+
646
+ # Returns a hash of targets sorted by those that are found in the
647
+ # inventory and those that are provided on the command line.
648
+ #
649
+ private def group_targets_by_source
635
650
  # Retrieve the known group and target names. This needs to be done before
636
651
  # updating targets, as that will add adhoc targets to the inventory.
637
652
  known_names = inventory.target_names
@@ -642,17 +657,7 @@ module Bolt
642
657
  known_names.include?(target.name)
643
658
  end
644
659
 
645
- target_list = {
646
- inventory: inventory_targets,
647
- adhoc: adhoc_targets
648
- }
649
-
650
- outputter.print_targets(target_list, inventoryfile)
651
- end
652
-
653
- def show_targets
654
- update_targets(options)
655
- outputter.print_target_info(options[:targets])
660
+ { inventory: inventory_targets, adhoc: adhoc_targets }
656
661
  end
657
662
 
658
663
  def list_groups
@@ -688,7 +693,11 @@ module Bolt
688
693
  plan_context = { plan_name: plan_name,
689
694
  params: plan_arguments }
690
695
 
691
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
696
+ executor = Bolt::Executor.new(config.concurrency,
697
+ analytics,
698
+ options[:noop],
699
+ config.modified_concurrency,
700
+ config.future)
692
701
  if %w[human rainbow].include?(options.fetch(:format, 'human'))
693
702
  executor.subscribe(outputter)
694
703
  else
@@ -724,7 +733,11 @@ module Bolt
724
733
  Bolt::Logger.warn("empty_manifest", message)
725
734
  end
726
735
 
727
- executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
736
+ executor = Bolt::Executor.new(config.concurrency,
737
+ analytics,
738
+ noop,
739
+ config.modified_concurrency,
740
+ config.future)
728
741
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
729
742
  executor.subscribe(log_outputter)
730
743
  # apply logging looks like plan logging, so tell the outputter we're in a
@@ -809,15 +822,10 @@ module Bolt
809
822
  #
810
823
  def assert_project_file(project)
811
824
  unless project.project_file?
812
- msg = if project.config_file.exist?
813
- command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
814
- "Detected Bolt configuration file #{project.config_file}, unable to install "\
815
- "modules. To update to a project configuration file, run '#{command}'."
816
- else
817
- command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
818
- "Could not find project configuration file #{project.project_file}, unable "\
819
- "to install modules. To create a Bolt project, run '#{command}'."
820
- end
825
+ command = Bolt::Util.powershell? ? 'New-BoltProject' : 'bolt project init'
826
+
827
+ msg = "Could not find project configuration file #{project.project_file}, unable "\
828
+ "to install modules. To create a Bolt project, run '#{command}'."
821
829
 
822
830
  raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
823
831
  end
@@ -935,7 +943,7 @@ module Bolt
935
943
 
936
944
  def analytics
937
945
  @analytics ||= begin
938
- client = Bolt::Analytics.build_client
946
+ client = Bolt::Analytics.build_client(config.analytics)
939
947
  client.bundled_content = bundled_content
940
948
  client
941
949
  end
data/lib/bolt/config.rb CHANGED
@@ -169,6 +169,7 @@ module Bolt
169
169
  @config_files = []
170
170
 
171
171
  default_data = {
172
+ 'analytics' => true,
172
173
  'apply-settings' => {},
173
174
  'color' => true,
174
175
  'compile-concurrency' => Etc.nprocessors,
@@ -263,6 +264,8 @@ module Bolt
263
264
  # Disabled warnings are concatenated
264
265
  when 'disable-warnings'
265
266
  val1.concat(val2)
267
+ when 'analytics'
268
+ val1 && val2
266
269
  # All other values are overwritten
267
270
  else
268
271
  val2
@@ -328,13 +331,6 @@ module Bolt
328
331
  end
329
332
 
330
333
  def validate
331
- if @data['future']
332
- Bolt::Logger.warn(
333
- "future_option",
334
- "Configuration option 'future' no longer exposes future behavior."
335
- )
336
- end
337
-
338
334
  if @data['modulepath']&.include?(@project.managed_moduledir.to_s)
339
335
  raise Bolt::ValidationError,
340
336
  "Found invalid path in modulepath: #{@project.managed_moduledir}. This path "\
@@ -395,6 +391,10 @@ module Bolt
395
391
  @data['format'] = value
396
392
  end
397
393
 
394
+ def future
395
+ @data['future']
396
+ end
397
+
398
398
  def trace
399
399
  @data['trace']
400
400
  end
@@ -459,6 +459,10 @@ module Bolt
459
459
  Set.new(@project.disable_warnings + @data['disable-warnings'])
460
460
  end
461
461
 
462
+ def analytics
463
+ @data['analytics']
464
+ end
465
+
462
466
  # Check if there is a case-insensitive match to the path
463
467
  def check_path_case(type, paths)
464
468
  return if paths.nil?
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/config/transport/ssh'
4
- require 'bolt/config/transport/winrm'
5
- require 'bolt/config/transport/orch'
3
+ require 'bolt/config/transport/docker'
6
4
  require 'bolt/config/transport/local'
7
5
  require 'bolt/config/transport/lxd'
8
- require 'bolt/config/transport/docker'
6
+ require 'bolt/config/transport/orch'
7
+ require 'bolt/config/transport/podman'
9
8
  require 'bolt/config/transport/remote'
9
+ require 'bolt/config/transport/ssh'
10
+ require 'bolt/config/transport/winrm'
10
11
 
11
12
  module Bolt
12
13
  class Config
@@ -14,13 +15,14 @@ module Bolt
14
15
  # Transport config classes. Used to load default transport config which
15
16
  # gets passed along to the inventory.
16
17
  TRANSPORT_CONFIG = {
17
- 'ssh' => Bolt::Config::Transport::SSH,
18
- 'winrm' => Bolt::Config::Transport::WinRM,
19
- 'pcp' => Bolt::Config::Transport::Orch,
18
+ 'docker' => Bolt::Config::Transport::Docker,
20
19
  'local' => Bolt::Config::Transport::Local,
21
20
  'lxd' => Bolt::Config::Transport::LXD,
22
- 'docker' => Bolt::Config::Transport::Docker,
23
- 'remote' => Bolt::Config::Transport::Remote
21
+ 'pcp' => Bolt::Config::Transport::Orch,
22
+ 'podman' => Bolt::Config::Transport::Podman,
23
+ 'remote' => Bolt::Config::Transport::Remote,
24
+ 'ssh' => Bolt::Config::Transport::SSH,
25
+ 'winrm' => Bolt::Config::Transport::WinRM
24
26
  }.freeze
25
27
 
26
28
  # Plugin definition. This is used by the JSON schemas to indicate that an option
@@ -55,6 +57,13 @@ module Bolt
55
57
  # Definitions used to validate config options.
56
58
  # https://github.com/puppetlabs/bolt/blob/main/schemas/README.md
57
59
  OPTIONS = {
60
+ "analytics" => {
61
+ description: "Whether to disable analytics. Setting this option to 'false' in the system-wide "\
62
+ "or user-level configuration will disable analytics for all projects, even if this "\
63
+ "option is set to 'true' at the project level.",
64
+ type: [TrueClass, FalseClass],
65
+ _example: false
66
+ },
58
67
  "apply-settings" => {
59
68
  description: "A map of Puppet settings to use when applying Puppet code using the `apply` "\
60
69
  "plan function or the `bolt apply` command.",
@@ -133,6 +142,20 @@ module Bolt
133
142
  _example: "json",
134
143
  _default: "human"
135
144
  },
145
+ "future" => {
146
+ description: "Enable new Bolt features that may include breaking changes.",
147
+ type: Hash,
148
+ properties: {
149
+ "file_paths" => {
150
+ description: "Load scripts from the `scripts/` directory of a module",
151
+ type: [TrueClass, FalseClass],
152
+ _example: true,
153
+ _default: false
154
+ }
155
+ },
156
+ _plugin: false,
157
+ _example: { 'file_paths' => true }
158
+ },
136
159
  "hiera-config" => {
137
160
  description: "The path to the Hiera configuration file.",
138
161
  type: String,
@@ -497,6 +520,12 @@ module Bolt
497
520
  _plugin: true,
498
521
  _example: { "job-poll-interval" => 15, "job-poll-timeout" => 30 }
499
522
  },
523
+ "podman" => {
524
+ description: "A map of configuration options for the podman transport.",
525
+ type: Hash,
526
+ _plugin: true,
527
+ _example: { "cleanup" => false, "tmpdir" => "/mount/tmp" }
528
+ },
500
529
  "remote" => {
501
530
  description: "A map of configuration options for the remote transport.",
502
531
  type: Hash,
@@ -532,6 +561,7 @@ module Bolt
532
561
 
533
562
  # Options that are available in a bolt-defaults.yaml file
534
563
  DEFAULTS_OPTIONS = %w[
564
+ analytics
535
565
  color
536
566
  compile-concurrency
537
567
  concurrency
@@ -551,12 +581,14 @@ module Bolt
551
581
 
552
582
  # Options that are available in a bolt-project.yaml file
553
583
  PROJECT_OPTIONS = %w[
584
+ analytics
554
585
  apply-settings
555
586
  color
556
587
  compile-concurrency
557
588
  concurrency
558
589
  disable-warnings
559
590
  format
591
+ future
560
592
  hiera-config
561
593
  log
562
594
  modulepath
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Podman < Base
10
+ OPTIONS = %w[
11
+ cleanup
12
+ host
13
+ interpreters
14
+ shell-command
15
+ tmpdir
16
+ tty
17
+ ].freeze
18
+
19
+ DEFAULTS = {
20
+ 'cleanup' => true
21
+ }.freeze
22
+
23
+ private def validate
24
+ super
25
+
26
+ if @config['interpreters']
27
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/bolt/executor.rb CHANGED
@@ -12,34 +12,37 @@ require 'bolt/config'
12
12
  require 'bolt/result_set'
13
13
  require 'bolt/puppetdb'
14
14
  # Load transports
15
- require 'bolt/transport/ssh'
16
- require 'bolt/transport/winrm'
17
- require 'bolt/transport/orch'
15
+ require 'bolt/transport/docker'
18
16
  require 'bolt/transport/local'
19
17
  require 'bolt/transport/lxd'
20
- require 'bolt/transport/docker'
18
+ require 'bolt/transport/orch'
19
+ require 'bolt/transport/podman'
21
20
  require 'bolt/transport/remote'
21
+ require 'bolt/transport/ssh'
22
+ require 'bolt/transport/winrm'
22
23
  require 'bolt/yarn'
23
24
 
24
25
  module Bolt
25
26
  TRANSPORTS = {
26
- ssh: Bolt::Transport::SSH,
27
- winrm: Bolt::Transport::WinRM,
28
- pcp: Bolt::Transport::Orch,
27
+ docker: Bolt::Transport::Docker,
29
28
  local: Bolt::Transport::Local,
30
29
  lxd: Bolt::Transport::LXD,
31
- docker: Bolt::Transport::Docker,
32
- remote: Bolt::Transport::Remote
30
+ pcp: Bolt::Transport::Orch,
31
+ podman: Bolt::Transport::Podman,
32
+ remote: Bolt::Transport::Remote,
33
+ ssh: Bolt::Transport::SSH,
34
+ winrm: Bolt::Transport::WinRM
33
35
  }.freeze
34
36
 
35
37
  class Executor
36
- attr_reader :noop, :transports, :in_parallel
38
+ attr_reader :noop, :transports, :in_parallel, :future
37
39
  attr_accessor :run_as
38
40
 
39
41
  def initialize(concurrency = 1,
40
42
  analytics = Bolt::Analytics::NoopClient.new,
41
43
  noop = false,
42
- modified_concurrency = false)
44
+ modified_concurrency = false,
45
+ future = {})
43
46
  # lazy-load expensive gem code
44
47
  require 'concurrent'
45
48
  @analytics = analytics
@@ -64,6 +67,7 @@ module Bolt
64
67
  @noop = noop
65
68
  @run_as = nil
66
69
  @in_parallel = false
70
+ @future = future
67
71
  @pool = if concurrency > 0
68
72
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
69
73
  else
@@ -6,6 +6,7 @@ module Bolt
6
6
  class Outputter
7
7
  class Human < Bolt::Outputter
8
8
  COLORS = {
9
+ dim: "2", # Dim, the other color of the rainbow
9
10
  red: "31",
10
11
  green: "32",
11
12
  yellow: "33",
@@ -313,78 +314,115 @@ module Bolt
313
314
 
314
315
  # @param [Hash] task A hash representing the task
315
316
  def print_task_info(task)
316
- # Building lots of strings...
317
- pretty_params = +""
318
- task_info = +""
319
- usage = if Bolt::Util.powershell?
320
- +"Invoke-BoltTask -Name #{task.name} -Targets <targets>"
317
+ params = (task.parameters || []).sort
318
+
319
+ info = +''
320
+
321
+ # Add task name and description
322
+ info << colorize(:cyan, "#{task.name}\n")
323
+ info << if task.description
324
+ indent(2, task.description.chomp)
321
325
  else
322
- +"bolt task run #{task.name} --targets <targets>"
326
+ indent(2, 'No description')
323
327
  end
328
+ info << "\n\n"
324
329
 
325
- task.parameters&.each do |k, v|
326
- pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
327
- pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
328
- pretty_params << " #{v['description']}\n" if v['description']
329
- usage << if v['type']&.start_with?("Optional")
330
- " [#{k}=<value>]"
330
+ # Build usage string
331
+ usage = +''
332
+ usage << if Bolt::Util.powershell?
333
+ "Invoke-BoltTask -Name #{task.name} -Targets <targets>"
334
+ else
335
+ "bolt task run #{task.name} --targets <targets>"
336
+ end
337
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]') if task.supports_noop
338
+ params.each do |name, data|
339
+ usage << if data['type']&.start_with?('Optional')
340
+ " [#{name}=<value>]"
331
341
  else
332
- " #{k}=<value>"
342
+ " #{name}=<value>"
333
343
  end
334
344
  end
335
345
 
336
- if task.supports_noop
337
- usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]')
346
+ # Add usage
347
+ info << colorize(:cyan, "Usage\n")
348
+ info << indent(2, wrap(usage))
349
+ info << "\n"
350
+
351
+ # Add parameters, if any
352
+ if params.any?
353
+ info << colorize(:cyan, "Parameters\n")
354
+ params.each do |name, data|
355
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'] || 'Any')}\n")
356
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
357
+ info << indent(4, "Default: #{data['default'].inspect}\n") if data.key?('default')
358
+ info << "\n"
359
+ end
338
360
  end
339
361
 
340
- task_info << "\n#{task.name}"
341
- task_info << " - #{task.description}" if task.description
342
- task_info << "\n\n"
343
- task_info << "USAGE:\n#{usage}\n\n"
344
- task_info << "PARAMETERS:\n#{pretty_params}\n" unless pretty_params.empty?
345
- task_info << "MODULE:\n"
346
-
362
+ # Add module location
347
363
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
348
- task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
349
- "built-in module"
350
- else
351
- path
352
- end
353
- @stream.puts(task_info)
364
+ info << colorize(:cyan, "Module\n")
365
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
366
+ indent(2, 'built-in module')
367
+ else
368
+ indent(2, path)
369
+ end
370
+
371
+ @stream.puts info
354
372
  end
355
373
 
356
374
  # @param [Hash] plan A hash representing the plan
357
375
  def print_plan_info(plan)
358
- # Building lots of strings...
359
- pretty_params = +""
360
- plan_info = +""
361
- usage = if Bolt::Util.powershell?
362
- +"Invoke-BoltPlan -Name #{plan['name']}"
376
+ params = plan['parameters'].sort
377
+
378
+ info = +''
379
+
380
+ # Add plan name and description
381
+ info << colorize(:cyan, "#{plan['name']}\n")
382
+ info << if plan['description']
383
+ indent(2, plan['description'].chomp)
363
384
  else
364
- +"bolt plan run #{plan['name']}"
385
+ indent(2, 'No description')
365
386
  end
387
+ info << "\n\n"
366
388
 
367
- plan['parameters'].each do |name, p|
368
- pretty_params << "- #{name}: #{p['type']}\n"
369
- pretty_params << " Default: #{p['default_value']}\n" unless p['default_value'].nil?
370
- pretty_params << " #{p['description']}\n" if p['description']
371
- usage << (p.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
389
+ # Build the usage string
390
+ usage = +''
391
+ usage << if Bolt::Util.powershell?
392
+ "Invoke-BoltPlan -Name #{plan['name']}"
393
+ else
394
+ "bolt plan run #{plan['name']}"
395
+ end
396
+ params.each do |name, data|
397
+ usage << (data.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
372
398
  end
373
399
 
374
- plan_info << "\n#{plan['name']}"
375
- plan_info << " - #{plan['description']}" if plan['description']
376
- plan_info << "\n\n"
377
- plan_info << "USAGE:\n#{usage}\n\n"
378
- plan_info << "PARAMETERS:\n#{pretty_params}\n" unless plan['parameters'].empty?
379
- plan_info << "MODULE:\n"
400
+ # Add usage
401
+ info << colorize(:cyan, "Usage\n")
402
+ info << indent(2, wrap(usage))
403
+ info << "\n"
380
404
 
381
- path = plan['module']
382
- plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
383
- "built-in module"
384
- else
385
- path
386
- end
387
- @stream.puts(plan_info)
405
+ # Add parameters, if any
406
+ if params.any?
407
+ info << colorize(:cyan, "Parameters\n")
408
+
409
+ params.each do |name, data|
410
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'])}\n")
411
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
412
+ info << indent(4, "Default: #{data['default_value']}\n") unless data['default_value'].nil?
413
+ info << "\n"
414
+ end
415
+ end
416
+
417
+ # Add module location
418
+ info << colorize(:cyan, "Module\n")
419
+ info << if plan['module'].start_with?(Bolt::Config::Modulepath::MODULES_PATH)
420
+ indent(2, 'built-in module')
421
+ else
422
+ indent(2, plan['module'])
423
+ end
424
+
425
+ @stream.puts info
388
426
  end
389
427
 
390
428
  def print_plans(plans, modulepath)
@@ -452,29 +490,77 @@ module Bolt
452
490
  targets += target_list[:inventory].map { |target| [target.name, nil] }
453
491
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
454
492
 
455
- if targets.any?
456
- @stream.puts format_table(targets, 0, 2)
457
- @stream.puts
458
- end
493
+ info = +''
494
+
495
+ # Add target list
496
+ info << colorize(:cyan, "Targets\n")
497
+ info << if targets.any?
498
+ format_table(targets, 2, 2).to_s
499
+ else
500
+ indent(2, 'No targets')
501
+ end
502
+ info << "\n\n"
503
+
504
+ @stream.puts info
505
+
506
+ print_inventory_summary(
507
+ target_list[:inventory].count,
508
+ target_list[:adhoc].count,
509
+ inventoryfile
510
+ )
511
+ end
512
+
513
+ def print_target_info(target_list, inventoryfile)
514
+ adhoc_targets = target_list[:adhoc].map(&:name).to_set
515
+ inventory_targets = target_list[:inventory].map(&:name).to_set
516
+ targets = target_list.values.flatten.sort_by(&:name)
517
+
518
+ info = +''
459
519
 
460
- @stream.puts "INVENTORY FILE:"
461
- if File.exist?(inventoryfile)
462
- @stream.puts inventoryfile
520
+ if targets.any?
521
+ adhoc = colorize(:yellow, " (Not found in inventory file)")
522
+
523
+ targets.each do |target|
524
+ info << colorize(:cyan, target.name)
525
+ info << adhoc if adhoc_targets.include?(target.name)
526
+ info << "\n"
527
+ info << indent(2, target.detail.to_yaml.lines.drop(1).join)
528
+ info << "\n"
529
+ end
463
530
  else
464
- @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
531
+ info << colorize(:cyan, "Targets\n")
532
+ info << indent(2, "No targets\n\n")
465
533
  end
466
534
 
467
- @stream.puts "\nTARGET COUNT:"
468
- @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
469
- "#{target_list[:adhoc].count} adhoc"
470
- end
535
+ @stream.puts info
471
536
 
472
- def print_target_info(targets)
473
- @stream.puts ::JSON.pretty_generate(
474
- targets: targets.map(&:detail)
537
+ print_inventory_summary(
538
+ inventory_targets.count,
539
+ adhoc_targets.count,
540
+ inventoryfile
475
541
  )
476
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
477
- @stream.puts colorize(:green, count)
542
+ end
543
+
544
+ private def print_inventory_summary(inventory_count, adhoc_count, inventoryfile)
545
+ info = +''
546
+
547
+ # Add inventory file source
548
+ info << colorize(:cyan, "Inventory file\n")
549
+ info << if File.exist?(inventoryfile)
550
+ indent(2, "#{inventoryfile}\n")
551
+ else
552
+ indent(2, wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist\n"))
553
+ end
554
+ info << "\n"
555
+
556
+ # Add target count summary
557
+ count = "#{inventory_count + adhoc_count} total, "\
558
+ "#{inventory_count} from inventory, "\
559
+ "#{adhoc_count} adhoc"
560
+ info << colorize(:cyan, "Target count\n")
561
+ info << indent(2, count)
562
+
563
+ @stream.puts info
478
564
  end
479
565
 
480
566
  def print_groups(groups)
@@ -116,7 +116,9 @@ module Bolt
116
116
  )
117
117
  end
118
118
 
119
- def print_target_info(targets)
119
+ def print_target_info(target_list, _inventoryfile)
120
+ targets = target_list.values.flatten
121
+
120
122
  @stream.puts ::JSON.pretty_generate(
121
123
  targets: targets.map(&:detail),
122
124
  count: targets.count
data/lib/bolt/plugin.rb CHANGED
@@ -178,8 +178,6 @@ module Bolt
178
178
  end
179
179
 
180
180
  def add_ruby_plugin(plugin_name)
181
- raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
182
-
183
181
  cls_name = Bolt::Util.snake_name_to_class_name(plugin_name)
184
182
  filename = "bolt/plugin/#{plugin_name}"
185
183
  require filename
@@ -203,8 +201,6 @@ module Bolt
203
201
  }
204
202
 
205
203
  mod = modules[plugin_name]
206
- raise PluginError::Unknown, plugin_name unless mod&.plugin?
207
- raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
208
204
 
209
205
  plugin = Bolt::Plugin::Module.load(mod, opts)
210
206
  add_plugin(plugin)
@@ -224,6 +220,12 @@ module Bolt
224
220
  end
225
221
  end
226
222
 
223
+ def known_plugin?(plugin_name)
224
+ @plugins.include?(plugin_name) ||
225
+ RUBY_PLUGINS.include?(plugin_name) ||
226
+ (modules.include?(plugin_name) && modules[plugin_name].plugin?)
227
+ end
228
+
227
229
  def get_hook(plugin_name, hook)
228
230
  plugin = by_name(plugin_name)
229
231
  raise PluginError::Unknown, plugin_name unless plugin
@@ -235,16 +237,16 @@ module Bolt
235
237
 
236
238
  # Calling by_name or get_hook will load any module based plugin automatically
237
239
  def by_name(plugin_name)
238
- return @plugins[plugin_name] if @plugins.include?(plugin_name)
239
- begin
240
- if RUBY_PLUGINS.include?(plugin_name)
240
+ if known_plugin?(plugin_name)
241
+ if @plugins.include?(plugin_name)
242
+ @plugins[plugin_name]
243
+ elsif !@load_plugins
244
+ raise PluginError::LoadingDisabled, plugin_name
245
+ elsif RUBY_PLUGINS.include?(plugin_name)
241
246
  add_ruby_plugin(plugin_name)
242
- elsif !@unknown.include?(plugin_name)
247
+ else
243
248
  add_module_plugin(plugin_name)
244
249
  end
245
- rescue PluginError::Unknown
246
- @unknown << plugin_name
247
- nil
248
250
  end
249
251
  end
250
252
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'shellwords'
5
- require 'bolt/transport/base'
5
+ require 'bolt/transport/simple'
6
6
 
7
7
  module Bolt
8
8
  module Transport
@@ -34,6 +34,10 @@ module Bolt
34
34
  @container_info["Id"]
35
35
  end
36
36
 
37
+ def run_cmd(cmd, env_vars)
38
+ Bolt::Util.exec_docker(cmd, env_vars)
39
+ end
40
+
37
41
  private def env_hash
38
42
  # Set the DOCKER_HOST if we are using a non-default service-url
39
43
  @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
@@ -90,7 +94,7 @@ module Bolt
90
94
 
91
95
  def upload_file(source, destination)
92
96
  @logger.trace { "Uploading #{source} to #{destination}" }
93
- _out, err, stat = Bolt::Util.exec_docker(['cp', source, "#{container_id}:#{destination}"], env_hash)
97
+ _out, err, stat = run_cmd(['cp', source, "#{container_id}:#{destination}"], env_hash)
94
98
  unless stat.exitstatus.zero?
95
99
  raise "Error writing to container #{container_id}: #{err}"
96
100
  end
@@ -104,7 +108,7 @@ module Bolt
104
108
  # copy the *contents* of the directory.
105
109
  # https://docs.docker.com/engine/reference/commandline/cp/
106
110
  FileUtils.mkdir_p(destination)
107
- _out, err, stat = Bolt::Util.exec_docker(['cp', "#{container_id}:#{source}", destination], env_hash)
111
+ _out, err, stat = run_cmd(['cp', "#{container_id}:#{source}", destination], env_hash)
108
112
  unless stat.exitstatus.zero?
109
113
  raise "Error downloading content from container #{container_id}: #{err}"
110
114
  end
@@ -119,9 +123,9 @@ module Bolt
119
123
  # @param arguments [Array] Arguments to pass to the docker command
120
124
  # e.g. 'src' and 'dest' for `docker cp <src> <dest>
121
125
  # @return [Object] Ruby object representation of the JSON string
122
- private def execute_local_json_command(subcommand, arguments = [])
126
+ def execute_local_json_command(subcommand, arguments = [])
123
127
  cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
124
- out, _err, _stat = Bolt::Util.exec_docker(cmd, env_hash)
128
+ out, _err, _stat = run_cmd(cmd, env_hash)
125
129
  extract_json(out)
126
130
  end
127
131
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'shellwords'
5
+ require 'bolt/transport/base'
6
+
7
+ module Bolt
8
+ module Transport
9
+ class Podman < Docker
10
+ def with_connection(target)
11
+ conn = Connection.new(target)
12
+ conn.connect
13
+ yield conn
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'bolt/transport/podman/connection'
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logging'
4
+ require 'bolt/node/errors'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class Podman < Docker
9
+ class Connection < Connection
10
+ attr_reader :user, :target
11
+
12
+ def initialize(target)
13
+ raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
14
+ @target = target
15
+ @user = ENV['USER'] || Etc.getlogin
16
+ @logger = Bolt::Logger.logger(target.safe_name)
17
+ @container_info = {}
18
+ @logger.trace("Initializing podman connection to #{target.safe_name}")
19
+ end
20
+
21
+ def run_cmd(cmd, env_vars)
22
+ Bolt::Util.exec_podman(cmd, env_vars)
23
+ end
24
+
25
+ def shell
26
+ @shell ||= if Bolt::Util.windows?
27
+ Bolt::Shell::Powershell.new(target, self)
28
+ else
29
+ Bolt::Shell::Bash.new(target, self)
30
+ end
31
+ end
32
+
33
+ def connect
34
+ # We don't actually have a connection, but we do need to
35
+ # check that the container exists and is running.
36
+ ps = execute_local_json_command('ps')
37
+ container = Array(ps).find { |item|
38
+ item["ID"].to_s.eql?(@target.host) ||
39
+ item["Id"].to_s.start_with?(@target.host) ||
40
+ Array(item["Names"]).include?(@target.host)
41
+ }
42
+ raise "Could not find a container with name or ID matching '#{@target.host}'" if container.nil?
43
+ # Now find the indepth container information
44
+ id = container["ID"] || container["Id"]
45
+ output = execute_local_json_command('inspect', [id])
46
+ # Store the container information for later
47
+ @container_info = output.first
48
+ @logger.trace { "Opened session" }
49
+ true
50
+ rescue StandardError => e
51
+ raise Bolt::Node::ConnectError.new(
52
+ "Failed to connect to #{target.safe_name}: #{e.message}",
53
+ 'CONNECT_ERROR'
54
+ )
55
+ end
56
+
57
+ # Executes a command inside the target container. This is called from the shell class.
58
+ #
59
+ # @param command [string] The command to run
60
+ def execute(command)
61
+ args = []
62
+ args += %w[--interactive]
63
+ args += %w[--tty] if target.options['tty']
64
+ args += @env_vars if @env_vars
65
+
66
+ if target.options['shell-command'] && !target.options['shell-command'].empty?
67
+ # escape any double quotes in command
68
+ command = command.gsub('"', '\"')
69
+ command = "#{target.options['shell-command']} \"#{command}\""
70
+ end
71
+
72
+ podman_command = %w[podman exec] + args + [container_id] + Shellwords.split(command)
73
+ @logger.trace { "Executing: #{podman_command.join(' ')}" }
74
+
75
+ Open3.popen3(*podman_command)
76
+ rescue StandardError
77
+ @logger.trace { "Command aborted" }
78
+ raise
79
+ end
80
+
81
+ # Converts the JSON encoded STDOUT string from the podman cli into ruby objects
82
+ #
83
+ # @param stdout [String] The string to convert
84
+ # @return [Object] Ruby object representation of the JSON string
85
+ private def extract_json(stdout)
86
+ # Podman renders the output in pretty JSON, which results in a newline
87
+ # appearing in the output before the closing bracket.
88
+ # should we only get a single line with no newline at all, we also
89
+ # assume it is a single minified JSON object
90
+ stdout.strip!
91
+ newline = stdout.index("\n") || -1
92
+ bracket = stdout.index('}') || -1
93
+ JSON.parse(stdout) if bracket > newline
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/bolt/util.rb CHANGED
@@ -343,6 +343,17 @@ module Bolt
343
343
  Open3.capture3(env, 'docker', *cmd, { binmode: true })
344
344
  end
345
345
 
346
+ # Executes a Podman CLI command. This is useful for running commands as
347
+ # part of this class without having to go through the `execute`
348
+ # function and manage pipes.
349
+ #
350
+ # @param cmd [String] The podman command and arguments to run
351
+ # e.g. 'cp <src> <dest>' for `podman cp <src> <dest>`
352
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
353
+ def exec_podman(cmd, env = {})
354
+ Open3.capture3(env, 'podman', *cmd, { binmode: true })
355
+ end
356
+
346
357
  # Formats a map of environment variables to be passed to a command that
347
358
  # accepts repeated `--env` flags
348
359
  #
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.5.0'
4
+ VERSION = '3.6.0'
5
5
  end
@@ -703,6 +703,9 @@ module BoltServer
703
703
  connect_plugin = BoltServer::Plugin::PuppetConnectData.new(body['puppet_connect_data'])
704
704
  plugins = Bolt::Plugin.setup(context[:config], context[:pal], load_plugins: false)
705
705
  plugins.add_plugin(connect_plugin)
706
+ %w[aws_inventory azure_inventory gcloud_inventory].each do |plugin_name|
707
+ plugins.add_module_plugin(plugin_name) if plugins.known_plugin?(plugin_name)
708
+ end
706
709
  inventory = Bolt::Inventory.from_config(context[:config], plugins)
707
710
  target_list = inventory.get_targets('all').map do |targ|
708
711
  targ.to_h.merge({ 'transport' => targ.transport, 'plugin_hooks' => targ.plugin_hooks })
@@ -17,13 +17,14 @@ module BoltSpec
17
17
 
18
18
  # Nothing on the executor is 'public'
19
19
  class MockExecutor
20
- attr_reader :noop, :error_message, :in_parallel, :transports
20
+ attr_reader :noop, :error_message, :in_parallel, :transports, :future
21
21
  attr_accessor :run_as, :transport_features, :execute_any_plan
22
22
 
23
23
  def initialize(modulepath)
24
24
  @noop = false
25
25
  @run_as = nil
26
26
  @in_parallel = false
27
+ @future = {}
27
28
  @error_message = nil
28
29
  @allow_apply = false
29
30
  @modulepath = [modulepath].flatten.map { |path| File.absolute_path(path) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-29 00:00:00.000000000 Z
11
+ date: 2021-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -473,6 +473,7 @@ files:
473
473
  - lib/bolt/config/transport/lxd.rb
474
474
  - lib/bolt/config/transport/options.rb
475
475
  - lib/bolt/config/transport/orch.rb
476
+ - lib/bolt/config/transport/podman.rb
476
477
  - lib/bolt/config/transport/remote.rb
477
478
  - lib/bolt/config/transport/ssh.rb
478
479
  - lib/bolt/config/transport/winrm.rb
@@ -564,6 +565,8 @@ files:
564
565
  - lib/bolt/transport/lxd/connection.rb
565
566
  - lib/bolt/transport/orch.rb
566
567
  - lib/bolt/transport/orch/connection.rb
568
+ - lib/bolt/transport/podman.rb
569
+ - lib/bolt/transport/podman/connection.rb
567
570
  - lib/bolt/transport/remote.rb
568
571
  - lib/bolt/transport/simple.rb
569
572
  - lib/bolt/transport/ssh.rb