bolt 2.17.0 → 2.22.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/lib/bolt/applicator.rb +19 -14
  8. data/lib/bolt/apply_result.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +68 -13
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +232 -47
  12. data/lib/bolt/config.rb +34 -13
  13. data/lib/bolt/config/options.rb +16 -1
  14. data/lib/bolt/config/transport/options.rb +16 -10
  15. data/lib/bolt/config/transport/ssh.rb +24 -10
  16. data/lib/bolt/executor.rb +21 -0
  17. data/lib/bolt/inventory/group.rb +3 -2
  18. data/lib/bolt/inventory/inventory.rb +4 -3
  19. data/lib/bolt/logger.rb +21 -0
  20. data/lib/bolt/module.rb +2 -1
  21. data/lib/bolt/outputter/rainbow.rb +9 -2
  22. data/lib/bolt/pal.rb +8 -2
  23. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  24. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  25. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  26. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  27. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  28. data/lib/bolt/plugin/module.rb +2 -4
  29. data/lib/bolt/plugin/puppetdb.rb +3 -2
  30. data/lib/bolt/project.rb +25 -11
  31. data/lib/bolt/puppetdb/client.rb +2 -0
  32. data/lib/bolt/puppetdb/config.rb +16 -0
  33. data/lib/bolt/result.rb +7 -0
  34. data/lib/bolt/shell/bash.rb +24 -4
  35. data/lib/bolt/shell/powershell.rb +10 -4
  36. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  37. data/lib/bolt/transport/base.rb +24 -0
  38. data/lib/bolt/transport/docker.rb +8 -0
  39. data/lib/bolt/transport/docker/connection.rb +20 -2
  40. data/lib/bolt/transport/local/connection.rb +14 -1
  41. data/lib/bolt/transport/orch.rb +12 -0
  42. data/lib/bolt/transport/simple.rb +6 -0
  43. data/lib/bolt/transport/ssh.rb +7 -1
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  46. data/lib/bolt/transport/winrm/connection.rb +118 -8
  47. data/lib/bolt/util.rb +26 -11
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/transport_app.rb +3 -2
  50. data/lib/bolt_spec/bolt_context.rb +7 -2
  51. data/lib/bolt_spec/plans.rb +15 -2
  52. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  53. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  54. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  55. data/lib/bolt_spec/run.rb +22 -0
  56. data/libexec/bolt_catalog +3 -2
  57. data/modules/secure_env_vars/plans/init.pp +20 -0
  58. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d7b3fbf02e3b0162ead0c7ca8994eb6a96f5f8d49d9b27aa4620a54c111396d
4
- data.tar.gz: 3ec22c605ca39808ffdc1f9589bdc05921889f4cdf7ef7079682614bd1298a81
3
+ metadata.gz: 60e995704052fee2778474c532df844e51ca0888017e28c883866e6b61a9678f
4
+ data.tar.gz: 8133b33137d67ba8880270f4ed20aa8346e13b2aa7ae2b0c390e3fbbba660172
5
5
  SHA512:
6
- metadata.gz: 71c5644b0b1bcaee1aaf5d74b6e1ae895e82ffa56dc52df9b4fc2995f1fb7541c9da8c3a0a4d31b63b72e04ab2245d4fd377f3bf7eaf18ea4de7f8c7c2f46c6b
7
- data.tar.gz: 503755fc3f2196d53fc26d6bfbe662671689ed6208ccf9ab06a384eb254d878f2e00d62b182f3c2185d09fc558bed8a2dfa8d2b942024bd62977b0e50e8c0d33
6
+ metadata.gz: a057aebe5bd7ce01a8369f3c7a05371f0dab79143cd465f6bd2f0a0ad23fe897fcff6624d03f07ead07b86c888512166232182c294cd1188fcd8fb45b55f463d
7
+ data.tar.gz: 4c30348d86636398f25568dcd77af25e6eb8e54af5c9ce44125a110b8089c15415a4a051aa8aac066f7404f9ca7faf253dd69e350780ef8d24e34f385c1dd8de
data/Puppetfile CHANGED
@@ -5,7 +5,7 @@ forge "http://forge.puppetlabs.com"
5
5
  moduledir File.join(File.dirname(__FILE__), 'modules')
6
6
 
7
7
  # Core modules used by 'apply'
8
- mod 'puppetlabs-service', '1.2.0'
8
+ mod 'puppetlabs-service', '1.3.0'
9
9
  mod 'puppetlabs-puppet_agent', '3.2.0'
10
10
  mod 'puppetlabs-facts', '1.0.0'
11
11
 
@@ -28,6 +28,7 @@ mod 'puppetlabs-python_task_helper', '0.4.3'
28
28
  mod 'puppetlabs-reboot', '3.0.0'
29
29
  mod 'puppetlabs-ruby_task_helper', '0.5.1'
30
30
  mod 'puppetlabs-ruby_plugin_helper', '0.1.0'
31
+ mod 'puppetlabs-stdlib', '6.3.0'
31
32
 
32
33
  # Plugin modules
33
34
  mod 'puppetlabs-aws_inventory', '0.5.0'
@@ -42,3 +43,4 @@ mod 'puppetlabs-yaml', '0.2.0'
42
43
  mod 'canary', local: true
43
44
  mod 'aggregate', local: true
44
45
  mod 'puppetdb_fact', local: true
46
+ mod 'secure_env_vars', local: true
@@ -13,10 +13,13 @@ require 'bolt/task'
13
13
  # > **Note:** Not available in apply block
14
14
  Puppet::Functions.create_function(:apply_prep) do
15
15
  # @param targets A pattern or array of patterns identifying a set of targets.
16
+ # @param options Options hash.
17
+ # @option options [Array] _required_modules An array of modules to sync to the target.
16
18
  # @example Prepare targets by name.
17
19
  # apply_prep('target1,target2')
18
20
  dispatch :apply_prep do
19
21
  param 'Boltlib::TargetSpec', :targets
22
+ optional_param 'Hash[String, Data]', :options
20
23
  end
21
24
 
22
25
  def script_compiler
@@ -60,18 +63,34 @@ Puppet::Functions.create_function(:apply_prep) do
60
63
  @executor ||= Puppet.lookup(:bolt_executor)
61
64
  end
62
65
 
63
- def apply_prep(target_spec)
66
+ def apply_prep(target_spec, options = {})
64
67
  unless Puppet[:tasks]
65
68
  raise Puppet::ParseErrorWithIssue
66
69
  .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
67
70
  end
68
71
 
72
+ options = options.transform_keys { |k| k.sub(/^_/, '').to_sym }
73
+
69
74
  applicator = Puppet.lookup(:apply_executor)
70
75
 
71
76
  executor.report_function_call(self.class.name)
72
77
 
73
78
  targets = inventory.get_targets(target_spec)
74
79
 
80
+ required_modules = options[:required_modules].nil? ? nil : Array(options[:required_modules])
81
+ if required_modules&.any?
82
+ Puppet.debug("Syncing only required modules: #{required_modules.join(',')}.")
83
+ end
84
+
85
+ # Gather facts, including custom facts
86
+ plugins = applicator.build_plugin_tarball do |mod|
87
+ next unless required_modules.nil? || required_modules.include?(mod.name)
88
+ search_dirs = []
89
+ search_dirs << mod.plugins if mod.plugins?
90
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
91
+ search_dirs
92
+ end
93
+
75
94
  executor.log_action('install puppet and gather facts', targets) do
76
95
  executor.without_default_logging do
77
96
  # Skip targets that include the puppet-agent feature, as we know an agent will be available.
@@ -109,14 +128,6 @@ Puppet::Functions.create_function(:apply_prep) do
109
128
  need_install_targets.each { |target| set_agent_feature(target) }
110
129
  end
111
130
 
112
- # Gather facts, including custom facts
113
- plugins = applicator.build_plugin_tarball do |mod|
114
- search_dirs = []
115
- search_dirs << mod.plugins if mod.plugins?
116
- search_dirs << mod.pluginfacts if mod.pluginfacts?
117
- search_dirs
118
- end
119
-
120
131
  task = applicator.custom_facts_task
121
132
  arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) }
122
133
  results = executor.run_task(targets, task, arguments)
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'bolt/error'
5
+
6
+ # Downloads the given file or directory from the given set of targets and saves it to a directory
7
+ # matching the target's name under the given destination directory. Returns the result from each
8
+ # download. This does nothing if the list of targets is empty.
9
+ #
10
+ # > **Note:** Existing content in the destination directory is deleted before downloading from
11
+ # > the targets.
12
+ #
13
+ # > **Note:** Not available in apply block
14
+ Puppet::Functions.create_function(:download_file, Puppet::Functions::InternalFunction) do
15
+ # Download a file or directory.
16
+ # @param source The absolute path to the file or directory on the target(s).
17
+ # @param destination The relative path to the destination directory on the local system. Expands
18
+ # relative to `<project>/downloads/`.
19
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
20
+ # @param options A hash of additional options.
21
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
22
+ # @option options [String] _run_as User to run as using privilege escalation.
23
+ # @return A list of results, one entry per target, with the path to the downloaded file under the
24
+ # `path` key.
25
+ # @example Download a file from multiple Linux targets to a destination directory
26
+ # download_file('/etc/ssh/ssh_config', '~/Downloads', $targets)
27
+ # @example Download a directory from multiple Linux targets to a project downloads directory
28
+ # download_file('/etc/ssh', 'ssh', $targets)
29
+ # @example Download a file from multiple Linux targets and compare its contents to a local file
30
+ # $results = download_file($source, $destination, $targets)
31
+ #
32
+ # $local_content = file::read($source)
33
+ #
34
+ # $mismatched_files = $results.filter |$result| {
35
+ # $remote_content = file::read($result['path'])
36
+ # $remote_content == $local_content
37
+ # }
38
+ dispatch :download_file do
39
+ param 'String[1]', :source
40
+ param 'String[1]', :destination
41
+ param 'Boltlib::TargetSpec', :targets
42
+ optional_param 'Hash[String[1], Any]', :options
43
+ return_type 'ResultSet'
44
+ end
45
+
46
+ # Download a file or directory, logging the provided description.
47
+ # @param source The absolute path to the file or directory on the target(s).
48
+ # @param destination The relative path to the destination directory on the local system. Expands
49
+ # relative to `<project>/downloads/`.
50
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
51
+ # @param description A description to be output when calling this function.
52
+ # @param options A hash of additional options.
53
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
54
+ # @option options [String] _run_as User to run as using privilege escalation.
55
+ # @return A list of results, one entry per target, with the path to the downloaded file under the
56
+ # `path` key.
57
+ # @example Download a file from multiple Linux targets to a destination directory
58
+ # download_file('/etc/ssh/ssh_config', '~/Downloads', $targets, 'Downloading remote SSH config')
59
+ dispatch :download_file_with_description do
60
+ param 'String[1]', :source
61
+ param 'String[1]', :destination
62
+ param 'Boltlib::TargetSpec', :targets
63
+ param 'String', :description
64
+ optional_param 'Hash[String[1], Any]', :options
65
+ return_type 'ResultSet'
66
+ end
67
+
68
+ def download_file(source, destination, targets, options = {})
69
+ download_file_with_description(source, destination, targets, nil, options)
70
+ end
71
+
72
+ def download_file_with_description(source, destination, targets, description = nil, options = {})
73
+ unless Puppet[:tasks]
74
+ raise Puppet::ParseErrorWithIssue
75
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'download_file')
76
+ end
77
+
78
+ options = options.select { |opt| opt.start_with?('_') }.transform_keys { |k| k.sub(/^_/, '').to_sym }
79
+ options[:description] = description if description
80
+
81
+ executor = Puppet.lookup(:bolt_executor)
82
+ inventory = Puppet.lookup(:bolt_inventory)
83
+
84
+ if (destination = destination.strip).empty?
85
+ raise Bolt::ValidationError, "Destination cannot be an empty string"
86
+ end
87
+
88
+ if (destination = Pathname.new(destination)).absolute?
89
+ raise Bolt::ValidationError, "Destination must be a relative path, received absolute path #{destination}"
90
+ end
91
+
92
+ # Prevent path traversal so downloads can't be saved outside of the project downloads directory
93
+ if (destination.each_filename.to_a & %w[. ..]).any?
94
+ raise Bolt::ValidationError, "Destination must not include path traversal, received #{destination}"
95
+ end
96
+
97
+ # Paths expand relative to the default downloads directory for the project
98
+ # e.g. ~/.puppetlabs/bolt/downloads/
99
+ destination = Puppet.lookup(:bolt_project_data).downloads + destination
100
+
101
+ # If the destination directory already exists, delete any existing contents
102
+ if Dir.exist?(destination)
103
+ FileUtils.rm_r(Dir.glob(destination + '*'), secure: true)
104
+ end
105
+
106
+ # Send Analytics Report
107
+ executor.report_function_call(self.class.name)
108
+
109
+ # Ensure that that given targets are all Target instances
110
+ targets = inventory.get_targets(targets)
111
+ if targets.empty?
112
+ call_function('debug', "Simulating file download of '#{source}' - no targets given - no action taken")
113
+ r = Bolt::ResultSet.new([])
114
+ else
115
+ r = executor.download_file(targets, source, destination, options)
116
+ end
117
+
118
+ if !r.ok && !options[:catch_errors]
119
+ raise Bolt::RunFailure.new(r, 'download_file', source)
120
+ end
121
+ r
122
+ end
123
+ end
@@ -11,6 +11,9 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
11
11
  # @param args A hash of arguments to the plan. Can also include additional options.
12
12
  # @option args [Boolean] _catch_errors Whether to catch raised errors.
13
13
  # @option args [String] _run_as User to run as using privilege escalation.
14
+ # This option sets the [run-as user](privilege_escalation.md) for all
15
+ # targets whenever Bolt connects to a target. This is set for all functions
16
+ # in the called plan, including `run_plan()`.
14
17
  # @return [PlanResult] The result of running the plan. Undef if plan does not explicitly return results.
15
18
  # @example Run a plan
16
19
  # run_plan('canary', 'command' => 'false', 'targets' => $targets, '_catch_errors' => true)
@@ -31,6 +34,9 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
31
34
  # @param args A hash of arguments to the plan. Can also include additional options.
32
35
  # @option args [Boolean] _catch_errors Whether to catch raised errors.
33
36
  # @option args [String] _run_as User to run as using privilege escalation.
37
+ # This option sets the [run-as user](privilege_escalation.md) for all
38
+ # targets whenever Bolt connects to a target. This is set for all functions
39
+ # in the called plan, including `run_plan()`.
34
40
  # @return [PlanResult] The result of running the plan. Undef if plan does not explicitly return results.
35
41
  # @example Run a plan
36
42
  # run_plan('canary', $targets, 'command' => 'false')
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ # Returns an array containing all of the filenames except for "." and ".." in the given directory.
6
+ Puppet::Functions.create_function(:'dir::children', Puppet::Functions::InternalFunction) do
7
+ # @param dirname Absolute path or Puppet module name.
8
+ # @return Array of files in the given directory.
9
+ # @example List filenames from an absolute path.
10
+ # dir::children('/home/user/subdir/')
11
+ # @example List filenames from a Puppet file path.
12
+ # dir::children('puppet_agent')
13
+ dispatch :children do
14
+ scope_param
15
+ required_param 'String', :dirname
16
+ return_type 'Array'
17
+ end
18
+
19
+ def children(scope, dirname)
20
+ # Send Analytics Report
21
+ Puppet.lookup(:bolt_executor) {}&.report_function_call(self.class.name)
22
+ modname, subpath = dirname.split(File::SEPARATOR, 2)
23
+ mod_path = scope.compiler.environment.module(modname)&.path
24
+
25
+ full_mod_path = File.join(mod_path, subpath || '') if mod_path
26
+
27
+ # Expand relative to the project directory if path is relative
28
+ project = Puppet.lookup(:bolt_project_data)
29
+ pathname = Pathname.new(dirname)
30
+ full_dir = pathname.absolute? ? dirname : File.expand_path(File.join(project.path, dirname))
31
+
32
+ # Sort for testability
33
+ Dir.children(full_mod_path || full_dir).sort
34
+ end
35
+ end
@@ -18,7 +18,6 @@ module Bolt
18
18
  pdb_client, hiera_config, max_compiles, apply_settings)
19
19
  # lazy-load expensive gem code
20
20
  require 'concurrent'
21
-
22
21
  @inventory = inventory
23
22
  @executor = executor
24
23
  @modulepath = modulepath || []
@@ -30,17 +29,6 @@ module Bolt
30
29
 
31
30
  @pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
32
31
  @logger = Logging.logger[self]
33
- @plugin_tarball = Concurrent::Delay.new do
34
- build_plugin_tarball do |mod|
35
- search_dirs = []
36
- search_dirs << mod.plugins if mod.plugins?
37
- search_dirs << mod.pluginfacts if mod.pluginfacts?
38
- search_dirs << mod.files if mod.files?
39
- type_files = "#{mod.path}/types"
40
- search_dirs << type_files if File.exist?(type_files)
41
- search_dirs
42
- end
43
- end
44
32
  end
45
33
 
46
34
  private def libexec
@@ -188,7 +176,6 @@ module Bolt
188
176
 
189
177
  def apply_ast(raw_ast, targets, options, plan_vars = {})
190
178
  ast = Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
191
-
192
179
  # Serialize as pcore for *Result* objects
193
180
  plan_vars = Puppet::Pops::Serialization::ToDataConverter.convert(plan_vars,
194
181
  rich_data: true,
@@ -207,9 +194,26 @@ module Bolt
207
194
  # This data isn't available on the target config hash
208
195
  config: @inventory.transport_data_get
209
196
  }
210
-
211
197
  description = options[:description] || 'apply catalog'
212
198
 
199
+ required_modules = options[:required_modules].nil? ? nil : Array(options[:required_modules])
200
+ if required_modules&.any?
201
+ @logger.debug("Syncing only required modules: #{required_modules.join(',')}.")
202
+ end
203
+
204
+ @plugin_tarball = Concurrent::Delay.new do
205
+ build_plugin_tarball do |mod|
206
+ next unless required_modules.nil? || required_modules.include?(mod.name)
207
+ search_dirs = []
208
+ search_dirs << mod.plugins if mod.plugins?
209
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
210
+ search_dirs << mod.files if mod.files?
211
+ type_files = "#{mod.path}/types"
212
+ search_dirs << type_files if File.exist?(type_files)
213
+ search_dirs
214
+ end
215
+ end
216
+
213
217
  r = @executor.log_action(description, targets) do
214
218
  futures = targets.map do |target|
215
219
  Concurrent::Future.execute(executor: @pool) do
@@ -236,6 +240,7 @@ module Bolt
236
240
  result
237
241
  end
238
242
  else
243
+
239
244
  arguments = {
240
245
  'catalog' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(catalog),
241
246
  'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins),
@@ -20,7 +20,7 @@ module Bolt
20
20
  error_hash['msg'] =~ /The term 'ruby.exe' is not recognized as the name of a cmdlet/)
21
21
  # Windows does not have Ruby present
22
22
  {
23
- 'msg' => "Puppet is not installed on the target in $env:ProgramFiles, please install it to enable 'apply'",
23
+ 'msg' => "Puppet was not found on the target or in $env:ProgramFiles, please install it to enable 'apply'",
24
24
  'kind' => 'bolt/apply-error'
25
25
  }
26
26
  elsif exit_code == 1 && error_hash['msg'] =~ /cannot load such file -- puppet \(LoadError\)/
@@ -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 project configfile],
14
- transports: %w[transport connect-timeout tty ssh-command copy-command],
14
+ transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command],
15
15
  display: %w[format color verbose trace],
16
16
  global: %w[help version debug log-level] }.freeze
17
17
 
@@ -25,7 +25,7 @@ module Bolt
25
25
  when 'command'
26
26
  case action
27
27
  when 'run'
28
- { flags: ACTION_OPTS,
28
+ { flags: ACTION_OPTS + %w[env-var],
29
29
  banner: COMMAND_RUN_HELP }
30
30
  else
31
31
  { flags: OPTIONS[:global],
@@ -36,6 +36,9 @@ module Bolt
36
36
  when 'upload'
37
37
  { flags: ACTION_OPTS + %w[tmpdir],
38
38
  banner: FILE_UPLOAD_HELP }
39
+ when 'download'
40
+ { flags: ACTION_OPTS,
41
+ banner: FILE_DOWNLOAD_HELP }
39
42
  else
40
43
  { flags: OPTIONS[:global],
41
44
  banner: FILE_HELP }
@@ -63,6 +66,9 @@ module Bolt
63
66
  when 'convert'
64
67
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
65
68
  banner: PLAN_CONVERT_HELP }
69
+ when 'new'
70
+ { flags: OPTIONS[:global] + %w[configfile project],
71
+ banner: PLAN_NEW_HELP }
66
72
  when 'run'
67
73
  { flags: ACTION_OPTS + %w[params compile-concurrency tmpdir hiera-config],
68
74
  banner: PLAN_RUN_HELP }
@@ -103,7 +109,7 @@ module Bolt
103
109
  when 'script'
104
110
  case action
105
111
  when 'run'
106
- { flags: ACTION_OPTS + %w[tmpdir],
112
+ { flags: ACTION_OPTS + %w[tmpdir env-var],
107
113
  banner: SCRIPT_RUN_HELP }
108
114
  else
109
115
  { flags: OPTIONS[:global],
@@ -156,10 +162,10 @@ module Bolt
156
162
  SUBCOMMANDS
157
163
  apply Apply Puppet manifest code
158
164
  command Run a command remotely
159
- file Upload a local file or directory
165
+ file Copy files between the controller and targets
160
166
  group Show the list of groups in the inventory
161
167
  inventory Show the list of targets an action would run on
162
- plan Convert, show, and run Bolt plans
168
+ plan Convert, create, show, and run Bolt plans
163
169
  project Create and migrate Bolt projects
164
170
  puppetfile Install and list modules and generate type references
165
171
  script Upload a local script and run it remotely
@@ -218,10 +224,30 @@ module Bolt
218
224
  bolt file <action> [options]
219
225
 
220
226
  DESCRIPTION
221
- Upload a local file or directory
227
+ Copy files and directories between the controller and targets
222
228
 
223
229
  ACTIONS
224
- upload Upload a local file or directory
230
+ download Download a file or directory to the controller
231
+ upload Upload a local file or directory from the controller
232
+ HELP
233
+
234
+ FILE_DOWNLOAD_HELP = <<~HELP
235
+ NAME
236
+ download
237
+
238
+ USAGE
239
+ bolt file download <src> <dest> [options]
240
+
241
+ DESCRIPTION
242
+ Download a file or directory from one or more targets.
243
+
244
+ Downloaded files and directories are saved to the a subdirectory
245
+ matching the target's name under the destination directory. The
246
+ destination directory is expanded relative to the downloads
247
+ subdirectory of the project directory.
248
+
249
+ EXAMPLES
250
+ bolt file download /etc/ssh_config ssh_config -t all
225
251
  HELP
226
252
 
227
253
  FILE_UPLOAD_HELP = <<~HELP
@@ -296,10 +322,11 @@ module Bolt
296
322
  bolt plan <action> [parameters] [options]
297
323
 
298
324
  DESCRIPTION
299
- Convert, show, and run Bolt plans.
325
+ Convert, create, show, and run Bolt plans.
300
326
 
301
327
  ACTIONS
302
328
  convert Convert a YAML plan to a Bolt plan
329
+ new Create a new plan in the current project
303
330
  run Run a plan on the specified targets
304
331
  show Show available plans and plan documentation
305
332
  HELP
@@ -322,6 +349,20 @@ module Bolt
322
349
  bolt plan convert path/to/plan/myplan.yaml
323
350
  HELP
324
351
 
352
+ PLAN_NEW_HELP = <<~HELP
353
+ NAME
354
+ new
355
+
356
+ USAGE
357
+ bolt plan new <plan> [options]
358
+
359
+ DESCRIPTION
360
+ Create a new plan in the current project.
361
+
362
+ EXAMPLES
363
+ bolt plan new myproject::myplan
364
+ HELP
365
+
325
366
  PLAN_RUN_HELP = <<~HELP
326
367
  NAME
327
368
  run
@@ -594,13 +635,13 @@ module Bolt
594
635
  bolt task show canary
595
636
  HELP
596
637
 
597
- attr_reader :warnings
638
+ attr_reader :deprecations
598
639
 
599
640
  def initialize(options)
600
641
  super()
601
642
 
602
643
  @options = options
603
- @warnings = []
644
+ @deprecations = []
604
645
 
605
646
  separator "\nINVENTORY OPTIONS"
606
647
  define('-t', '--targets TARGETS',
@@ -738,16 +779,29 @@ module Bolt
738
779
  @options[:'save-rerun'] = save
739
780
  end
740
781
 
782
+ separator "\nREMOTE ENVIRONMENT OPTIONS"
783
+ define('--env-var ENVIRONMENT_VARIABLES', 'Environment variables to set on the target') do |envvar|
784
+ unless envvar.include?('=')
785
+ raise Bolt::CLIError, "Environment variables must be specified using 'myenvvar=key' format"
786
+ end
787
+ @options[:env_vars] ||= {}
788
+ @options[:env_vars].store(*envvar.split('=', 2))
789
+ end
790
+
741
791
  separator "\nTRANSPORT OPTIONS"
742
792
  define('--transport TRANSPORT', TRANSPORTS.keys.map(&:to_s),
743
793
  "Specify a default transport: #{TRANSPORTS.keys.join(', ')}") do |t|
744
794
  @options[:transport] = t
745
795
  end
746
- define('--ssh-command EXEC', "Executable to use instead of the net-ssh ruby library. ",
796
+ define('--[no-]native-ssh', 'Whether to shell out to native SSH or use the net-ssh Ruby library.',
797
+ 'This option is experimental') do |bool|
798
+ @options[:'native-ssh'] = bool
799
+ end
800
+ define('--ssh-command EXEC', "Executable to use instead of the net-ssh Ruby library. ",
747
801
  "This option is experimental.") do |exec|
748
802
  @options[:'ssh-command'] = exec
749
803
  end
750
- define('--copy-command EXEC', "Command to copy files to remote hosts if using external SSH. ",
804
+ define('--copy-command EXEC', "Command to copy files to remote hosts if using native SSH. ",
751
805
  "This option is experimental.") do |exec|
752
806
  @options[:'copy-command'] = exec
753
807
  end
@@ -805,7 +859,8 @@ module Bolt
805
859
  @options[:debug] = true
806
860
  # We don't actually set '--log-level debug' here, but once the options are evaluated by
807
861
  # the config class the end result is the same.
808
- @warnings << { msg: "Command line option '--debug' is deprecated, set '--log-level debug' instead." }
862
+ msg = "Command line option '--debug' is deprecated, set '--log-level debug' instead."
863
+ @deprecations << { type: 'Using --debug instead of --log-level debug', msg: msg }
809
864
  end
810
865
  define('--log-level LEVEL',
811
866
  "Set the log level for the console. Available options are",