bolt 2.8.0 → 2.12.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.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +61 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
  10. data/bolt-modules/boltlib/types/planresult.pp +12 -1
  11. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  14. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  16. data/lib/bolt/analytics.rb +21 -2
  17. data/lib/bolt/applicator.rb +3 -1
  18. data/lib/bolt/apply_result.rb +1 -1
  19. data/lib/bolt/apply_target.rb +3 -2
  20. data/lib/bolt/bolt_option_parser.rb +18 -8
  21. data/lib/bolt/cli.rb +35 -5
  22. data/lib/bolt/config.rb +45 -13
  23. data/lib/bolt/config/transport/docker.rb +2 -0
  24. data/lib/bolt/config/transport/local.rb +2 -0
  25. data/lib/bolt/config/transport/orch.rb +2 -0
  26. data/lib/bolt/config/transport/remote.rb +2 -0
  27. data/lib/bolt/config/transport/ssh.rb +50 -1
  28. data/lib/bolt/config/transport/winrm.rb +2 -0
  29. data/lib/bolt/inventory.rb +2 -1
  30. data/lib/bolt/inventory/group.rb +1 -0
  31. data/lib/bolt/inventory/inventory.rb +5 -0
  32. data/lib/bolt/inventory/target.rb +17 -1
  33. data/lib/bolt/node/output.rb +1 -1
  34. data/lib/bolt/outputter/human.rb +5 -4
  35. data/lib/bolt/outputter/json.rb +1 -1
  36. data/lib/bolt/pal.rb +4 -1
  37. data/lib/bolt/pal/yaml_plan.rb +1 -0
  38. data/lib/bolt/plugin.rb +13 -7
  39. data/lib/bolt/plugin/puppetdb.rb +5 -2
  40. data/lib/bolt/project.rb +25 -7
  41. data/lib/bolt/puppetdb/config.rb +14 -26
  42. data/lib/bolt/rerun.rb +1 -1
  43. data/lib/bolt/resource_instance.rb +126 -0
  44. data/lib/bolt/result.rb +46 -23
  45. data/lib/bolt/result_set.rb +2 -5
  46. data/lib/bolt/shell/bash.rb +1 -1
  47. data/lib/bolt/shell/powershell.rb +12 -4
  48. data/lib/bolt/target.rb +12 -1
  49. data/lib/bolt/transport/ssh.rb +6 -2
  50. data/lib/bolt/transport/ssh/connection.rb +4 -0
  51. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  52. data/lib/bolt/transport/winrm/connection.rb +6 -2
  53. data/lib/bolt/version.rb +1 -1
  54. data/lib/bolt_server/pe/pal.rb +1 -38
  55. data/lib/bolt_server/transport_app.rb +7 -7
  56. data/lib/bolt_spec/bolt_context.rb +1 -4
  57. data/lib/bolt_spec/plans/mock_executor.rb +1 -0
  58. data/lib/bolt_spec/run.rb +2 -5
  59. metadata +6 -2
@@ -3,7 +3,7 @@
3
3
  module Bolt
4
4
  class ApplyTarget
5
5
  ATTRIBUTES = %i[uri name target_alias config vars facts features
6
- plugin_hooks safe_name].freeze
6
+ plugin_hooks resources safe_name].freeze
7
7
  COMPUTED = %i[host password port protocol user].freeze
8
8
 
9
9
  attr_reader(*ATTRIBUTES)
@@ -24,7 +24,8 @@ module Bolt
24
24
  facts = nil,
25
25
  vars = nil,
26
26
  features = nil,
27
- plugin_hooks = nil)
27
+ plugin_hooks = nil,
28
+ resources = nil)
28
29
  raise Bolt::Error.new("Target objects cannot be instantiated inside apply blocks", 'bolt/apply-error')
29
30
  end
30
31
  # rubocop:enable Lint/UnusedMethodArgument
@@ -11,7 +11,7 @@ module Bolt
11
11
  escalation: %w[run-as sudo-password sudo-password-prompt sudo-executable],
12
12
  run_context: %w[concurrency inventoryfile save-rerun cleanup],
13
13
  global_config_setters: %w[modulepath boltdir configfile],
14
- transports: %w[transport connect-timeout tty],
14
+ transports: %w[transport connect-timeout tty ssh-command copy-command],
15
15
  display: %w[format color verbose trace],
16
16
  global: %w[help version debug] }.freeze
17
17
 
@@ -20,7 +20,7 @@ module Bolt
20
20
  def get_help_text(subcommand, action = nil)
21
21
  case subcommand
22
22
  when 'apply'
23
- { flags: ACTION_OPTS + %w[noop execute compile-concurrency],
23
+ { flags: ACTION_OPTS + %w[noop execute compile-concurrency hiera-config],
24
24
  banner: APPLY_HELP }
25
25
  when 'command'
26
26
  case action
@@ -172,13 +172,14 @@ module Bolt
172
172
  apply
173
173
 
174
174
  USAGE
175
- bolt apply <manifest.pp> [options]
175
+ bolt apply [manifest.pp] [options]
176
176
 
177
177
  DESCRIPTION
178
178
  Apply Puppet manifest code on the specified targets.
179
179
 
180
180
  EXAMPLES
181
- bolt apply manifest.pp --targets target1,target2
181
+ bolt apply manifest.pp -t target
182
+ bolt apply -e "file { '/etc/puppetlabs': ensure => present }" -t target
182
183
  HELP
183
184
 
184
185
  COMMAND_HELP = <<~HELP
@@ -594,6 +595,7 @@ module Bolt
594
595
  HELP
595
596
 
596
597
  attr_reader :warnings
598
+
597
599
  def initialize(options)
598
600
  super()
599
601
 
@@ -655,8 +657,8 @@ module Bolt
655
657
  @options[:password] = STDIN.noecho(&:gets).chomp
656
658
  STDERR.puts
657
659
  end
658
- define('--private-key KEY', 'Private ssh key to authenticate with') do |key|
659
- @options[:'private-key'] = key
660
+ define('--private-key KEY', 'Path to private ssh key to authenticate with') do |key|
661
+ @options[:'private-key'] = File.expand_path(key)
660
662
  end
661
663
  define('--[no-]host-key-check', 'Check host keys with SSH') do |host_key_check|
662
664
  @options[:'host-key-check'] = host_key_check
@@ -688,7 +690,7 @@ module Bolt
688
690
 
689
691
  separator "\nRUN CONTEXT OPTIONS"
690
692
  define('-c', '--concurrency CONCURRENCY', Integer,
691
- 'Maximum number of simultaneous connections (default: 100)') do |concurrency|
693
+ 'Maximum number of simultaneous connections') do |concurrency|
692
694
  @options[:concurrency] = concurrency
693
695
  end
694
696
  define('--compile-concurrency CONCURRENCY', Integer,
@@ -718,7 +720,7 @@ module Bolt
718
720
  end
719
721
  define('--hiera-config FILEPATH',
720
722
  'Specify where to load Hiera config from (default: ~/.puppetlabs/bolt/hiera.yaml)') do |path|
721
- @options[:'hiera-config'] = path
723
+ @options[:'hiera-config'] = File.expand_path(path)
722
724
  end
723
725
  define('-i', '--inventoryfile FILEPATH',
724
726
  'Specify where to load inventory from (default: ~/.puppetlabs/bolt/inventory.yaml)') do |path|
@@ -741,6 +743,14 @@ module Bolt
741
743
  "Specify a default transport: #{TRANSPORTS.keys.join(', ')}") do |t|
742
744
  @options[:transport] = t
743
745
  end
746
+ define('--ssh-command EXEC', "Executable to use instead of the net-ssh ruby library. ",
747
+ "This option is experimental.") do |exec|
748
+ @options[:'ssh-command'] = exec
749
+ end
750
+ define('--copy-command EXEC', "Command to copy files to remote hosts if using external SSH. ",
751
+ "This option is experimental.") do |exec|
752
+ @options[:'copy-command'] = exec
753
+ end
744
754
  define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout|
745
755
  @options[:'connect-timeout'] = timeout
746
756
  end
@@ -124,8 +124,9 @@ module Bolt
124
124
 
125
125
  Bolt::Logger.configure(config.log, config.color)
126
126
 
127
- # Logger must be configured before checking path case, otherwise warnings will not display
127
+ # Logger must be configured before checking path case and project file, otherwise warnings will not display
128
128
  @config.check_path_case('modulepath', @config.modulepath)
129
+ @config.project.check_deprecated_file
129
130
 
130
131
  # Log the file paths for loaded config files
131
132
  config_loaded
@@ -257,13 +258,11 @@ module Bolt
257
258
  end
258
259
 
259
260
  def puppetdb_client
260
- return @puppetdb_client if @puppetdb_client
261
- puppetdb_config = Bolt::PuppetDB::Config.load_config(nil, config.puppetdb, config.project.path)
262
- @puppetdb_client = Bolt::PuppetDB::Client.new(puppetdb_config)
261
+ plugins.puppetdb_client
263
262
  end
264
263
 
265
264
  def plugins
266
- @plugins ||= Bolt::Plugin.setup(config, pal, puppetdb_client, analytics)
265
+ @plugins ||= Bolt::Plugin.setup(config, pal, analytics)
267
266
  end
268
267
 
269
268
  def query_puppetdb_nodes(query)
@@ -538,6 +537,17 @@ module Bolt
538
537
  Puppet[:tasks] = false
539
538
  ast = pal.parse_manifest(code, filename)
540
539
 
540
+ if defined?(ast.body) &&
541
+ (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
542
+ ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
543
+ message = "Manifest only contains definitions and will result in no changes on the targets. "\
544
+ "Definitions must be declared for their resources to be applied. You can read more "\
545
+ "about defining and declaring classes and types in the Puppet documentation at "\
546
+ "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
547
+ "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
548
+ @logger.warn(message)
549
+ end
550
+
541
551
  executor = Bolt::Executor.new(config.concurrency, analytics, noop)
542
552
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
543
553
  executor.subscribe(log_outputter)
@@ -804,6 +814,20 @@ module Bolt
804
814
  end
805
815
 
806
816
  def bundled_content
817
+ # If the bundled content directory is empty, Bolt is likely installed as a gem.
818
+ if ENV['BOLT_GEM'].nil? && incomplete_install?
819
+ msg = <<~MSG.chomp
820
+ Bolt may be installed as a gem. To use Bolt reliably and with all of its
821
+ dependencies, uninstall the 'bolt' gem and install Bolt as a package:
822
+ https://puppet.com/docs/bolt/latest/bolt_installing.html
823
+
824
+ If you meant to install Bolt as a gem and want to disable this warning,
825
+ set the BOLT_GEM environment variable.
826
+ MSG
827
+
828
+ @logger.warn(msg)
829
+ end
830
+
807
831
  # We only need to enumerate bundled content when running a task or plan
808
832
  content = { 'Plan' => [],
809
833
  'Task' => [],
@@ -827,5 +851,11 @@ module Bolt
827
851
  MSG
828
852
  @logger.debug(msg)
829
853
  end
854
+
855
+ # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
856
+ # package installs include modules listed in the Bolt repo Puppetfile
857
+ def incomplete_install?
858
+ (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
859
+ end
830
860
  end
831
861
  end
@@ -34,6 +34,8 @@ module Bolt
34
34
  'remote' => Bolt::Config::Transport::Remote
35
35
  }.freeze
36
36
 
37
+ # NOTE: All configuration options should have a corresponding schema property
38
+ # in schemas/bolt-config.schema.json
37
39
  OPTIONS = {
38
40
  "apply_settings" => "A map of Puppet settings to use when applying Puppet code",
39
41
  "color" => "Whether to use colored output when printing messages to the console.",
@@ -64,8 +66,8 @@ module Bolt
64
66
 
65
67
  DEFAULT_OPTIONS = {
66
68
  "color" => true,
67
- "concurrency" => 100,
68
69
  "compile-concurrency" => "Number of cores",
70
+ "concurrency" => "100 or one-third of the ulimit, whichever is lower",
69
71
  "format" => "human",
70
72
  "hiera-config" => "Boltdir/hiera.yaml",
71
73
  "inventoryfile" => "Boltdir/inventory.yaml",
@@ -101,6 +103,8 @@ module Bolt
101
103
  "show_diff" => false
102
104
  }.freeze
103
105
 
106
+ DEFAULT_DEFAULT_CONCURRENCY = 100
107
+
104
108
  def self.default
105
109
  new(Bolt::Project.new('.'), {})
106
110
  end
@@ -111,7 +115,7 @@ module Bolt
111
115
  data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
112
116
  }
113
117
 
114
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
118
+ data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
115
119
 
116
120
  new(project, data, overrides)
117
121
  end
@@ -123,27 +127,29 @@ module Bolt
123
127
  filepath: project.config_file,
124
128
  data: Bolt::Util.read_yaml_hash(configfile, 'config')
125
129
  }
126
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
130
+ data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
127
131
 
128
132
  new(project, data, overrides)
129
133
  end
130
134
 
131
- def self.load_defaults
135
+ def self.load_defaults(project)
132
136
  # Lazy-load expensive gem code
133
137
  require 'win32/dir' if Bolt::Util.windows?
134
138
 
135
- system_path = if Bolt::Util.windows?
136
- Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc', 'bolt.yaml'))
137
- else
138
- Pathname.new(File.join('/etc', 'puppetlabs', 'bolt', 'bolt.yaml'))
139
- end
139
+ # Don't load /etc/puppetlabs/bolt/bolt.yaml twice
140
+ confs = if project.path == Bolt::Project.system_path
141
+ []
142
+ else
143
+ system_path = Pathname.new(File.join(Bolt::Project.system_path, 'bolt.yaml'))
144
+ [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
145
+ end
146
+
140
147
  user_path = begin
141
148
  Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'bolt.yaml')))
142
149
  rescue ArgumentError
143
150
  nil
144
151
  end
145
152
 
146
- confs = [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
147
153
  confs << { filepath: user_path, data: Bolt::Util.read_optional_yaml_hash(user_path, 'config') } if user_path
148
154
  confs
149
155
  end
@@ -163,7 +169,7 @@ module Bolt
163
169
  'apply_settings' => {},
164
170
  'color' => true,
165
171
  'compile-concurrency' => Etc.nprocessors,
166
- 'concurrency' => 100,
172
+ 'concurrency' => default_concurrency,
167
173
  'format' => 'human',
168
174
  'log' => { 'console' => {} },
169
175
  'plugin_hooks' => {},
@@ -181,6 +187,18 @@ module Bolt
181
187
 
182
188
  override_data = normalize_overrides(overrides)
183
189
 
190
+ # If we need to lower concurrency and concurrency is not configured
191
+ ld_concurrency = loaded_data.map(&:keys).flatten.include?('concurrency')
192
+ if default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
193
+ !ld_concurrency &&
194
+ !override_data.key?('concurrency')
195
+ concurrency_warning = { option: 'concurrency',
196
+ msg: "Concurrency will default to #{default_concurrency} because ulimit "\
197
+ "is low: #{Etc.sysconf(Etc::SC_OPEN_MAX)}. Set concurrency with "\
198
+ "'--concurrency', or set your ulimit with 'ulimit -n <limit>'" }
199
+ @warnings << concurrency_warning
200
+ end
201
+
184
202
  @data = merge_config_layers(default_data, *loaded_data, override_data)
185
203
 
186
204
  TRANSPORT_CONFIG.each do |transport, config|
@@ -310,10 +328,10 @@ module Bolt
310
328
  @warnings << { option: 'future', msg: msg }
311
329
  end
312
330
 
313
- keys = OPTIONS.keys - %w[plugins plugin_hooks]
331
+ keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
314
332
  keys.each do |key|
315
333
  next unless Bolt::Util.references?(@data[key])
316
- valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks]
334
+ valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks puppetdb]
317
335
  raise Bolt::ValidationError,
318
336
  "Found unsupported key _plugin in config setting #{key}. Plugins are only available in "\
319
337
  "#{valid_keys.join(', ')}."
@@ -456,5 +474,19 @@ module Bolt
456
474
  l =~ /[A-Za-z]/ ? "[#{l.upcase}#{l.downcase}]" : l
457
475
  end.join
458
476
  end
477
+
478
+ # Etc::SC_OPEN_MAX is meaningless on windows, not defined in PE Jruby and not available
479
+ # on some platforms. This method holds the logic to decide whether or not to even consider it.
480
+ def sc_open_max_available?
481
+ !Bolt::Util.windows? && defined?(Etc::SC_OPEN_MAX) && Etc.sysconf(Etc::SC_OPEN_MAX)
482
+ end
483
+
484
+ def default_concurrency
485
+ @default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
486
+ DEFAULT_DEFAULT_CONCURRENCY
487
+ else
488
+ Etc.sysconf(Etc::SC_OPEN_MAX) / 3
489
+ end
490
+ end
459
491
  end
460
492
  end
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Docker < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cleanup" => { type: TrueClass,
12
14
  desc: "Whether to clean up temporary files created on targets." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Local < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cleanup" => { type: TrueClass,
12
14
  desc: "Whether to clean up temporary files created on targets." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Orch < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cacert" => { type: String,
12
14
  desc: "The path to the CA certificate." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Remote < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "run-on" => { type: String,
12
14
  desc: "The proxy target that the task executes on." }
@@ -8,16 +8,25 @@ module Bolt
8
8
  module Transport
9
9
  class SSH < Base
10
10
  LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
11
+
12
+ # NOTE: All transport configuration options should have a corresponding schema definition
13
+ # in schemas/bolt-transport-definitions.json
11
14
  OPTIONS = {
12
15
  "cleanup" => { type: TrueClass,
16
+ external: true,
13
17
  desc: "Whether to clean up temporary files created on targets." },
14
18
  "connect-timeout" => { type: Integer,
15
19
  desc: "How long to wait when establishing connections." },
20
+ "copy-command" => { external: true,
21
+ desc: "Command to use when copying files using ssh-command. "\
22
+ "Bolt runs `<copy-command> <src> <dest>`. **This option is experimental.**" },
16
23
  "disconnect-timeout" => { type: Integer,
17
24
  desc: "How long to wait before force-closing a connection." },
18
25
  "host" => { type: String,
26
+ external: true,
19
27
  desc: "Host name." },
20
28
  "host-key-check" => { type: TrueClass,
29
+ external: true,
21
30
  desc: "Whether to perform host key validation when connecting." },
22
31
  "extensions" => { type: Array,
23
32
  desc: "List of file extensions that are accepted for scripts or tasks on Windows. "\
@@ -26,6 +35,7 @@ module Bolt
26
35
  "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
27
36
  "`.pp` are always allowed and run via hard-coded executables." },
28
37
  "interpreters" => { type: Hash,
38
+ external: true,
29
39
  desc: "A map of an extension name to the absolute path of an executable, "\
30
40
  "enabling you to override the shebang defined in a task executable. The "\
31
41
  "extension can optionally be specified with the `.` character (`.py` and "\
@@ -41,26 +51,36 @@ module Bolt
41
51
  "password" => { type: String,
42
52
  desc: "Login password." },
43
53
  "port" => { type: Integer,
54
+ external: true,
44
55
  desc: "Connection port." },
45
- "private-key" => { desc: "Either the path to the private key file to use for authentication, or a "\
56
+ "private-key" => { external: true,
57
+ desc: "Either the path to the private key file to use for authentication, or a "\
46
58
  "hash with the key `key-data` and the contents of the private key." },
47
59
  "proxyjump" => { type: String,
48
60
  desc: "A jump host to proxy connections through, and an optional user to "\
49
61
  "connect with." },
50
62
  "run-as" => { type: String,
63
+ external: true,
51
64
  desc: "A different user to run commands as after login." },
52
65
  "run-as-command" => { type: Array,
66
+ external: true,
53
67
  desc: "The command to elevate permissions. Bolt appends the user and command "\
54
68
  "strings to the configured `run-as-command` before running it on the "\
55
69
  "target. This command must not require an interactive password prompt, "\
56
70
  "and the `sudo-password` option is ignored when `run-as-command` is "\
57
71
  "specified. The `run-as-command` must be specified as an array." },
58
72
  "script-dir" => { type: String,
73
+ external: true,
59
74
  desc: "The subdirectory of the tmpdir to use in place of a randomized "\
60
75
  "subdirectory for uploading and executing temporary files on the "\
61
76
  "target. It's expected that this directory already exists as a subdir "\
62
77
  "of tmpdir, which is either configured or defaults to `/tmp`." },
78
+ "ssh-command" => { external: true,
79
+ desc: "Command and flags to use when SSHing. This enables the external "\
80
+ "SSH transport which shells out to the specified command. "\
81
+ "**This option is experimental.**" },
63
82
  "sudo-executable" => { type: String,
83
+ external: true,
64
84
  desc: "The executable to use when escalating to the configured `run-as` "\
65
85
  "user. This is useful when you want to escalate using the configured "\
66
86
  "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
@@ -68,14 +88,17 @@ module Bolt
68
88
  "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
69
89
  "**This option is experimental.**" },
70
90
  "sudo-password" => { type: String,
91
+ external: true,
71
92
  desc: "Password to use when changing users via `run-as`." },
72
93
  "tmpdir" => { type: String,
94
+ external: true,
73
95
  desc: "The directory to upload and execute temporary files on the target." },
74
96
  "tty" => { type: TrueClass,
75
97
  desc: "Request a pseudo tty for the session. This option is generally "\
76
98
  "only used in conjunction with the `run-as` option when the sudoers "\
77
99
  "policy requires a `tty`." },
78
100
  "user" => { type: String,
101
+ external: true,
79
102
  desc: "Login user." }
80
103
  }.freeze
81
104
 
@@ -100,6 +123,13 @@ module Bolt
100
123
 
101
124
  if key_opt.instance_of?(String)
102
125
  @config['private-key'] = File.expand_path(key_opt, @project)
126
+
127
+ # We have an explicit test for this to only warn if using net-ssh transport
128
+ Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['ssh-command']
129
+ end
130
+
131
+ if key_opt.instance_of?(Hash) && @config['ssh-command']
132
+ raise Bolt::ValidationError, 'private-key must be a filepath when using ssh-command'
103
133
  end
104
134
  end
105
135
 
@@ -127,6 +157,25 @@ module Bolt
127
157
  end
128
158
  end
129
159
  end
160
+
161
+ if @config['ssh-command'] && !@config['load-config']
162
+ msg = 'Cannot use external SSH transport with load-config set to false'
163
+ raise Bolt::ValidationError, msg
164
+ end
165
+
166
+ if (ssh_cmd = @config['ssh-command'])
167
+ unless ssh_cmd.is_a?(String) || ssh_cmd.is_a?(Array)
168
+ raise Bolt::ValidationError,
169
+ "ssh-command must be a String or Array, received #{ssh_cmd.class} #{ssh_cmd.inspect}"
170
+ end
171
+ end
172
+
173
+ if (copy_cmd = @config['copy-command'])
174
+ unless copy_cmd.is_a?(String) || copy_cmd.is_a?(Array)
175
+ raise Bolt::ValidationError,
176
+ "copy-command must be a String or Array, received #{copy_cmd.class} #{copy_cmd.inspect}"
177
+ end
178
+ end
130
179
  end
131
180
  end
132
181
  end