bolt 3.3.0 → 3.4.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: 7e32573bc97fea7eca64b7c915cb4b462e1325fb5d4dcbe95c48c72aae8c0e14
4
- data.tar.gz: abb279c5a363092ca37ddf4eac9de665de9f79aa74c9e0af9697910b336b7b93
3
+ metadata.gz: 2e66e28078af9324bb7d5668ee5079535d829913a2dcb4bce65daa4d471d4524
4
+ data.tar.gz: a821d82c490030be200c0d4ab5dc4a56a1abf1aa862d69d1e4c6a4595e47952d
5
5
  SHA512:
6
- metadata.gz: 23bf7042750c180eb618eaafd8af34e014deb5c7ab0dadcce316e92c3aa66e62ae85e89e96f471cc54f17c81eba463f73b776e9aa4581219567ed15876722967
7
- data.tar.gz: bcd1eb6192f5ac92959088edfbe7b333dcb9ed63f6da7a36a6274d72cbce32e77f4f01a8427edec78be3dd9caeab2472ec3691d846108367df407187606ba2ae
6
+ metadata.gz: 120dd18c9478c105387f2ad3634276cbaae05fe3638cc01d378aa9cbb1c423bf737991b7278ab4f96eb0a7cca39cd3d259cbba3d1734cea6ac071ba0695da901
7
+ data.tar.gz: 61171ff383d06883e6f6ffa0cafc423a80d41ea0f2d45c3c0e4b76c7037a4626ad7a4b8b625e3a013e077b9e96cdafaab06cad6d5a490dae392af3fbb410d0fe
data/Puppetfile CHANGED
@@ -6,7 +6,7 @@ moduledir File.join(File.dirname(__FILE__), 'modules')
6
6
 
7
7
  # Core modules used by 'apply'
8
8
  mod 'puppetlabs-service', '2.0.0'
9
- mod 'puppetlabs-puppet_agent', '4.4.0'
9
+ mod 'puppetlabs-puppet_agent', '4.5.0'
10
10
  mod 'puppetlabs-facts', '1.4.0'
11
11
 
12
12
  # Core types and providers for Puppet 6
@@ -24,9 +24,9 @@ mod 'puppetlabs-zone_core', '1.0.3'
24
24
  # Useful additional modules
25
25
  mod 'puppetlabs-package', '2.0.0'
26
26
  mod 'puppetlabs-powershell_task_helper', '0.1.0'
27
- mod 'puppetlabs-puppet_conf', '1.0.0'
27
+ mod 'puppetlabs-puppet_conf', '1.1.0'
28
28
  mod 'puppetlabs-python_task_helper', '0.5.0'
29
- mod 'puppetlabs-reboot', '4.0.0'
29
+ mod 'puppetlabs-reboot', '4.0.2'
30
30
  mod 'puppetlabs-ruby_task_helper', '0.6.0'
31
31
  mod 'puppetlabs-ruby_plugin_helper', '0.2.0'
32
32
  mod 'puppetlabs-stdlib', '7.0.0'
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/error'
4
+ require 'json'
4
5
 
5
6
  # Runs a command on the given set of targets and returns the result from each command execution.
6
7
  # This function does nothing if the list of targets is empty.
@@ -13,7 +14,7 @@ Puppet::Functions.create_function(:run_command) do
13
14
  # @param options A hash of additional options.
14
15
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
15
16
  # @option options [String] _run_as User to run as using privilege escalation.
16
- # @option options [Hash] _env_vars Map of environment variables to set
17
+ # @option options [Hash[String, Any]] _env_vars Map of environment variables to set
17
18
  # @return A list of results, one entry per target.
18
19
  # @example Run a command on targets
19
20
  # run_command('hostname', $targets, '_catch_errors' => true)
@@ -31,7 +32,7 @@ Puppet::Functions.create_function(:run_command) do
31
32
  # @param options A hash of additional options.
32
33
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
33
34
  # @option options [String] _run_as User to run as using privilege escalation.
34
- # @option options [Hash] _env_vars Map of environment variables to set
35
+ # @option options [Hash[String, Any]] _env_vars Map of environment variables to set
35
36
  # @return A list of results, one entry per target.
36
37
  # @example Run a command on targets
37
38
  # run_command('hostname', $targets, 'Get hostname')
@@ -56,6 +57,23 @@ Puppet::Functions.create_function(:run_command) do
56
57
  options = options.transform_keys { |k| k.sub(/^_/, '').to_sym }
57
58
  options[:description] = description if description
58
59
 
60
+ # Ensure env_vars is a hash and that each hash value is transformed to JSON
61
+ # so we don't accidentally pass Ruby-style data to the target.
62
+ if options[:env_vars]
63
+ unless options[:env_vars].is_a?(Hash)
64
+ raise Bolt::ValidationError, "Option 'env_vars' must be a hash"
65
+ end
66
+
67
+ if (bad_keys = options[:env_vars].keys.reject { |k| k.is_a?(String) }).any?
68
+ raise Bolt::ValidationError,
69
+ "Keys for option 'env_vars' must be strings: #{bad_keys.map(&:inspect).join(', ')}"
70
+ end
71
+
72
+ options[:env_vars] = options[:env_vars].transform_values do |val|
73
+ [Array, Hash].include?(val.class) ? val.to_json : val
74
+ end
75
+ end
76
+
59
77
  executor = Puppet.lookup(:bolt_executor)
60
78
  inventory = Puppet.lookup(:bolt_inventory)
61
79
 
@@ -16,7 +16,7 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
16
16
  # Cannot be used with `arguments`.
17
17
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
18
18
  # @option options [String] _run_as User to run as using privilege escalation.
19
- # @option options [Hash] _env_vars Map of environment variables to set.
19
+ # @option options [Hash[String, Any]] _env_vars Map of environment variables to set.
20
20
  # @return A list of results, one entry per target.
21
21
  # @example Run a local script on Linux targets as 'root'
22
22
  # run_script('/var/tmp/myscript', $targets, '_run_as' => 'root')
@@ -44,7 +44,7 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
44
44
  # Cannot be used with `arguments`.
45
45
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
46
46
  # @option options [String] _run_as User to run as using privilege escalation.
47
- # @option options [Hash] _env_vars Map of environment variables to set.
47
+ # @option options [Hash[String, Any]] _env_vars Map of environment variables to set.
48
48
  # @return A list of results, one entry per target.
49
49
  # @example Run a script
50
50
  # run_script('/var/tmp/myscript', $targets, 'Downloading my application')
@@ -85,6 +85,23 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
85
85
  options[:description] = description if description
86
86
  options[:pwsh_params] = pwsh_params if pwsh_params
87
87
 
88
+ # Ensure env_vars is a hash and that each hash value is transformed to JSON
89
+ # so we don't accidentally pass Ruby-style data to the target.
90
+ if options[:env_vars]
91
+ unless options[:env_vars].is_a?(Hash)
92
+ raise Bolt::ValidationError, "Option 'env_vars' must be a hash"
93
+ end
94
+
95
+ if (bad_keys = options[:env_vars].keys.reject { |k| k.is_a?(String) }).any?
96
+ raise Bolt::ValidationError,
97
+ "Keys for option 'env_vars' must be strings: #{bad_keys.map(&:inspect).join(', ')}"
98
+ end
99
+
100
+ options[:env_vars] = options[:env_vars].transform_values do |val|
101
+ [Array, Hash].include?(val.class) ? val.to_json : val
102
+ end
103
+ end
104
+
88
105
  executor = Puppet.lookup(:bolt_executor)
89
106
  inventory = Puppet.lookup(:bolt_inventory)
90
107
 
@@ -11,12 +11,24 @@ Puppet::Functions.create_function(:prompt) do
11
11
  # @option options [Boolean] sensitive Disable echo back and mark the response as sensitive.
12
12
  # The returned value will be wrapped by the `Sensitive` data type. To access the raw
13
13
  # value, use the `unwrap` function (i.e. `$sensitive_value.unwrap`).
14
+ # @option options [String] default The default value to return if the user does not provide
15
+ # input or if stdin is not a tty.
14
16
  # @return The response to the prompt.
15
17
  # @example Prompt the user if plan execution should continue
16
18
  # $response = prompt('Continue executing plan? [Y\N]')
17
19
  # @example Prompt the user for sensitive information
18
20
  # $password = prompt('Enter your password', 'sensitive' => true)
19
21
  # out::message("Password is: ${password.unwrap}")
22
+ # @example Prompt the user and provide a default value
23
+ # $user = prompt('Enter your login username', 'default' => 'root')
24
+ # @example Prompt the user for sensitive information, returning a sensitive default if one is not provided
25
+ # $token = prompt('Enter token', 'default' => lookup('default_token'), 'sensitive' => true)
26
+ # out::message("Token is: ${token.unwrap}")
27
+ # @example Prompt the user and fail with a custom message if no input was provided
28
+ # $response = prompt('Enter your name', 'default' => '')
29
+ # if $response.empty {
30
+ # fail_plan('Must provide your name')
31
+ # }
20
32
  dispatch :prompt do
21
33
  param 'String', :prompt
22
34
  optional_param 'Hash[String[1], Any]', :options
@@ -30,14 +42,20 @@ Puppet::Functions.create_function(:prompt) do
30
42
  action: 'prompt')
31
43
  end
32
44
 
33
- options = options.transform_keys(&:to_sym)
34
-
45
+ options = options.transform_keys(&:to_sym)
35
46
  executor = Puppet.lookup(:bolt_executor)
47
+
36
48
  # Send analytics report
37
49
  executor.report_function_call(self.class.name)
38
50
 
51
+ # Require default to be a string value
52
+ if options.key?(:default) && !options[:default].is_a?(String)
53
+ raise Bolt::ValidationError, "Option 'default' must be a string"
54
+ end
55
+
39
56
  response = executor.prompt(prompt, options)
40
57
 
58
+ # If sensitive, wrap it
41
59
  if options[:sensitive]
42
60
  Puppet::Pops::Types::PSensitiveType::Sensitive.new(response)
43
61
  else
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Display a menu prompt and wait for a response. Continues to prompt
6
+ # until an option from the menu is selected.
7
+ #
8
+ # > **Note:** Not available in apply block
9
+ Puppet::Functions.create_function(:'prompt::menu') do
10
+ # Select from a list of options.
11
+ # @param prompt The prompt to display.
12
+ # @param menu A list of options to choose from.
13
+ # @param options A hash of additional options.
14
+ # @option options [String] default The default option to return if the user does not provide
15
+ # input or if standard in (stdin) is not a tty. Must be an option present in the menu.
16
+ # @return The selected option.
17
+ # @example Prompt the user to select from a list of options
18
+ # $selection = prompt::menu('Select a fruit', ['apple', 'banana', 'carrot'])
19
+ # @example Prompt the user to select from a list of options with a default value
20
+ # $selection = prompt::menu('Select environment', ['development', 'production'], 'default' => 'development')
21
+ dispatch :prompt_menu_array do
22
+ param 'String', :prompt
23
+ param 'Array[Variant[Target, Data]]', :menu
24
+ optional_param 'Hash[String[1], Variant[Target, Data]]', :options
25
+ return_type 'Variant[Target, Data]'
26
+ end
27
+
28
+ # Select from a list of options with custom inputs.
29
+ # @param prompt The prompt to display.
30
+ # @param menu A hash of options to choose from, where keys are the input used to select a value.
31
+ # @param options A hash of additional options.
32
+ # @option options [String] default The default option to return if the user does not provide
33
+ # input or if standard in (stdin) is not a tty. Must be an option present in the menu.
34
+ # @return The selected option.
35
+ # @example Prompt the user to select from a list of options with custom inputs
36
+ # $menu = { 'y' => 'yes', 'n' => 'no' }
37
+ # $selection = prompt::menu('Install Puppet?', $menu)
38
+ dispatch :prompt_menu do
39
+ param 'String', :prompt
40
+ param 'Hash[String[1], Variant[Target, Data]]', :menu
41
+ optional_param 'Hash[String[1], Variant[Target, Data]]', :options
42
+ return_type 'Variant[TargetSpec, Data]'
43
+ end
44
+
45
+ def prompt_menu_array(prompt, menu, options = {})
46
+ menu_hash = menu.map.with_index { |value, index| [(index + 1).to_s, value] }.to_h
47
+ prompt_menu(prompt, menu_hash, options)
48
+ end
49
+
50
+ def prompt_menu(prompt, menu, options = {})
51
+ unless Puppet[:tasks]
52
+ raise Puppet::ParseErrorWithIssue
53
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
54
+ action: 'prompt::menu')
55
+ end
56
+
57
+ options = options.transform_keys(&:to_sym)
58
+ executor = Puppet.lookup(:bolt_executor)
59
+
60
+ # Send analytics report
61
+ executor.report_function_call(self.class.name)
62
+
63
+ # Error if there are no options
64
+ if menu.empty?
65
+ raise Bolt::ValidationError, "Menu cannot be empty"
66
+ end
67
+
68
+ # Error if the default value is not on the menu
69
+ if options.key?(:default) && !menu.value?(options[:default])
70
+ raise Bolt::ValidationError, "Default value '#{options[:default]}' is not one of the provided menu options"
71
+ end
72
+
73
+ # The first prompt should include the menu
74
+ to_prompt = format_menu(menu) + prompt
75
+
76
+ # Request input from the user until they provide a valid option
77
+ loop do
78
+ selection = executor.prompt(to_prompt, options)
79
+
80
+ return menu[selection] if menu.key?(selection)
81
+ return selection if options.key?(:default) && options[:default] == selection
82
+
83
+ # Only reprint the prompt, not the menu
84
+ to_prompt = "Invalid option, try again. #{prompt}"
85
+ end
86
+ end
87
+
88
+ # Builds the menu string. Aligns all the values by padding input keys.
89
+ #
90
+ private def format_menu(menu)
91
+ # Find the longest input and add 2 for wrapping parenthesis
92
+ key_length = menu.keys.max_by(&:length).length + 2
93
+
94
+ menu_string = +''
95
+
96
+ menu.each do |key, value|
97
+ key = "(#{key})".ljust(key_length)
98
+ menu_string << "#{key} #{value}\n"
99
+ end
100
+
101
+ menu_string
102
+ end
103
+ end
data/lib/bolt/cli.rb CHANGED
@@ -96,6 +96,48 @@ module Bolt
96
96
  finalize_setup
97
97
  end
98
98
 
99
+ # Prints a welcome message when users first install Bolt and run `bolt`, `bolt help` or `bolt --help`
100
+ def welcome_message
101
+ bolt = <<~BOLT
102
+ `.::-`
103
+ `.-:///////-.`
104
+ `-:////:. `-:///:- /ooo. .ooo/
105
+ `.-:///::///:-` `-//: ymmm- :mmmy .---.
106
+ :///:-. `.:////. -//: ymmm- :mmmy +mmm+
107
+ ://. ///. -//: ymmm--/++/- `-/++/:` :mmmy-:smmms::-
108
+ ://. ://. .://: ymmmdmmmmmmdo` .smmmmmmmmh: :mmmysmmmmmmmms
109
+ ://. ://:///:-. ymmmh/--/hmmmy -mmmd/-.:hmmm+:mmmy.-smmms--.
110
+ ://:.` .-////:-` ymmm- ymmm:hmmm- `dmmm/mmmy +mmm+
111
+ `-:///:-..:///:-.` ymmm- ommm/dmmm` hmmm+mmmy +mmm+
112
+ `.-:////:-` ymmm+ /mmmm.ommms` /mmmh:mmmy +mmmo
113
+ `-.` ymmmmmhhmmmmd: ommmmhydmmmy`:mmmy -mmmmdhd
114
+ oyyy+shddhs/` .+shddhy+- -yyyo .ohddhs
115
+
116
+
117
+ BOLT
118
+ example_cmd = if Bolt::Util.windows?
119
+ "Invoke-BoltCommand -Command 'hostname' -Targets localhost"
120
+ else
121
+ "bolt command run 'hostname' --target localhost"
122
+ end
123
+ prev_cmd = String.new("bolt")
124
+ prev_cmd << " #{@argv[0]}" unless @argv.empty?
125
+
126
+ message = <<~MSG
127
+ 🎉 Welcome to Bolt #{VERSION}
128
+ 😌 We're here to help bring order to the chaos
129
+ 📖 Find our documentation at https://bolt.guide
130
+ 🙋 Ask a question in #bolt on https://slack.puppet.com/
131
+ 🔩 Contribute at https://github.com/puppetlabs/bolt/
132
+ 💡 Not sure where to start? Try "#{example_cmd}"
133
+
134
+ We only print this message once. Run "#{prev_cmd}" again for help text.
135
+ MSG
136
+
137
+ $stdout.print "\033[36m#{bolt}\033[0m"
138
+ $stdout.print message
139
+ end
140
+
99
141
  # Parses the command and validates options. All errors that are raised here
100
142
  # are not handled by the outputter, as it relies on config being loaded.
101
143
  def parse_command
@@ -107,6 +149,16 @@ module Bolt
107
149
  # help text
108
150
  options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
109
151
 
152
+ if Bolt::Util.first_run?
153
+ FileUtils.mkdir_p(Bolt::Util.first_runs_free.dirname)
154
+ FileUtils.touch(Bolt::Util.first_runs_free)
155
+
156
+ if options[:subcommand].nil? && $stdout.isatty
157
+ welcome_message
158
+ raise Bolt::CLIExit
159
+ end
160
+ end
161
+
110
162
  # Update the parser for the subcommand (or lack thereof)
111
163
  parser.update
112
164
  puts parser.help
@@ -9,11 +9,13 @@ module Bolt
9
9
  class LXD < Base
10
10
  OPTIONS = %w[
11
11
  cleanup
12
+ remote
12
13
  tmpdir
13
14
  ].freeze
14
15
 
15
16
  DEFAULTS = {
16
- 'cleanup' => true
17
+ 'cleanup' => true,
18
+ 'remote' => 'local'
17
19
  }.freeze
18
20
  end
19
21
  end
@@ -266,6 +266,13 @@ module Bolt
266
266
  _plugin: true,
267
267
  _example: "BOLT.PRODUCTION"
268
268
  },
269
+ "remote" => {
270
+ type: String,
271
+ description: "The LXD remote host to use.",
272
+ _default: "local",
273
+ _plugin: false,
274
+ _example: 'myremote'
275
+ },
269
276
  "run-as" => {
270
277
  type: String,
271
278
  description: "The user to run commands as after login. The run-as user must be different than the "\
data/lib/bolt/executor.rb CHANGED
@@ -484,22 +484,30 @@ module Bolt
484
484
  end
485
485
 
486
486
  def prompt(prompt, options)
487
- @prompting = true
488
487
  unless $stdin.tty?
488
+ return options[:default] if options[:default]
489
489
  raise Bolt::Error.new('STDIN is not a tty, unable to prompt', 'bolt/no-tty-error')
490
490
  end
491
491
 
492
- $stderr.print("#{prompt}: ")
492
+ @prompting = true
493
+
494
+ if options[:default] && !options[:sensitive]
495
+ $stderr.print("#{prompt} [#{options[:default]}]: ")
496
+ else
497
+ $stderr.print("#{prompt}: ")
498
+ end
493
499
 
494
500
  value = if options[:sensitive]
495
501
  $stdin.noecho(&:gets).to_s.chomp
496
502
  else
497
503
  $stdin.gets.to_s.chomp
498
504
  end
505
+
499
506
  @prompting = false
500
507
 
501
508
  $stderr.puts if options[:sensitive]
502
509
 
510
+ value = options[:default] if value.empty?
503
511
  value
504
512
  end
505
513
 
@@ -6,13 +6,23 @@ require 'bolt/result'
6
6
  module Bolt
7
7
  class Node
8
8
  class Output
9
- attr_reader :stdout, :stderr
9
+ attr_reader :stderr, :stdout, :merged_output
10
10
  attr_accessor :exit_code
11
11
 
12
12
  def initialize
13
- @stdout = StringIO.new
14
- @stderr = StringIO.new
15
- @exit_code = 'unknown'
13
+ @stdout = StringIO.new
14
+ @stderr = StringIO.new
15
+ @merged_output = StringIO.new
16
+ @exit_code = 'unknown'
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ 'stdout' => @stdout.string,
22
+ 'stderr' => @stderr.string,
23
+ 'merged_output' => @merged_output.string,
24
+ 'exit_code' => @exit_code
25
+ }
16
26
  end
17
27
  end
18
28
  end
@@ -142,16 +142,9 @@ module Bolt
142
142
  end
143
143
 
144
144
  # Use special handling if the result looks like a command or script result
145
- if result.generic_value.keys == %w[stdout stderr exit_code]
145
+ if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
146
146
  safe_value = result.safe_value
147
- unless safe_value['stdout'].strip.empty?
148
- @stream.puts(indent(2, "STDOUT:"))
149
- @stream.puts(indent(4, safe_value['stdout']))
150
- end
151
- unless safe_value['stderr'].strip.empty?
152
- @stream.puts(indent(2, "STDERR:"))
153
- @stream.puts(indent(4, safe_value['stderr']))
154
- end
147
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
155
148
  elsif result.generic_value.any?
156
149
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
157
150
  end
@@ -282,7 +275,7 @@ module Bolt
282
275
  pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
283
276
  pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
284
277
  pretty_params << " #{v['description']}\n" if v['description']
285
- usage << if v['type'].start_with?("Optional")
278
+ usage << if v['type']&.start_with?("Optional")
286
279
  " [#{k}=<value>]"
287
280
  else
288
281
  " #{k}=<value>"
@@ -290,7 +283,7 @@ module Bolt
290
283
  end
291
284
 
292
285
  if task.supports_noop
293
- usage << Bolt::Util.powershell? ? '[-Noop]' : '[--noop]'
286
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]')
294
287
  end
295
288
 
296
289
  task_info << "\n#{task.name}"
data/lib/bolt/pal.rb CHANGED
@@ -521,10 +521,30 @@ module Bolt
521
521
  end
522
522
  end
523
523
 
524
- def convert_plan(plan_path)
524
+ def convert_plan(plan)
525
+ path = File.expand_path(plan)
526
+
527
+ # If the path doesn't exist, check if it's a plan name
528
+ unless File.exist?(path)
529
+ in_bolt_compiler do |compiler|
530
+ sig = compiler.plan_signature(plan)
531
+
532
+ # If the plan was loaded, look for it on the module loader
533
+ # There has to be an easier way to do this...
534
+ if sig
535
+ type = compiler.list_plans.find { |p| p.name == plan }
536
+ path = sig.instance_variable_get(:@plan_func)
537
+ .loader
538
+ .find(type)
539
+ .origin
540
+ .first
541
+ end
542
+ end
543
+ end
544
+
525
545
  Puppet[:tasks] = true
526
546
  transpiler = YamlPlan::Transpiler.new
527
- transpiler.transpile(plan_path)
547
+ transpiler.transpile(path)
528
548
  end
529
549
 
530
550
  # Returns a mapping of all modules available to the Bolt compiler
@@ -13,6 +13,14 @@ module Bolt
13
13
  Set['command', 'targets']
14
14
  end
15
15
 
16
+ def self.validate_step_keys(body, number)
17
+ super
18
+
19
+ if body.key?('env_vars') && ![Hash, String].include?(body['env_vars'].class)
20
+ raise StepError.new('env_vars key must be a hash or evaluable string', body['name'], number)
21
+ end
22
+ end
23
+
16
24
  # Returns an array of arguments to pass to the step's function call
17
25
  #
18
26
  private def format_args(body)
@@ -27,6 +27,10 @@ module Bolt
27
27
  if body.key?('pwsh_params') && !body['pwsh_params'].nil? && !body['pwsh_params'].is_a?(Hash)
28
28
  raise StepError.new('pwsh_params key must be a hash', body['name'], number)
29
29
  end
30
+
31
+ if body.key?('env_vars') && ![Hash, String].include?(body['env_vars'].class)
32
+ raise StepError.new('env_vars key must be a hash or evaluable string', body['name'], number)
33
+ end
30
34
  end
31
35
 
32
36
  # Returns an array of arguments to pass to the step's function call
@@ -14,8 +14,8 @@ module Bolt
14
14
  end
15
15
  end
16
16
 
17
- def transpile(relative_path)
18
- @plan_path = File.expand_path(relative_path)
17
+ def transpile(plan_path)
18
+ @plan_path = plan_path
19
19
  @modulename = Bolt::Util.module_name(@plan_path)
20
20
  @filename = @plan_path.split(File::SEPARATOR)[-1]
21
21
  validate_path
data/lib/bolt/result.rb CHANGED
@@ -28,20 +28,14 @@ module Bolt
28
28
  %w[file line].zip(position).to_h.compact
29
29
  end
30
30
 
31
- def self.for_command(target, stdout, stderr, exit_code, action, command, position)
32
- value = {
33
- 'stdout' => stdout,
34
- 'stderr' => stderr,
35
- 'exit_code' => exit_code
36
- }
37
-
31
+ def self.for_command(target, value, action, command, position)
38
32
  details = create_details(position)
39
- unless exit_code == 0
40
- details['exit_code'] = exit_code
33
+ unless value['exit_code'] == 0
34
+ details['exit_code'] = value['exit_code']
41
35
  value['_error'] = {
42
36
  'kind' => 'puppetlabs.tasks/command-error',
43
37
  'issue_code' => 'COMMAND_ERROR',
44
- 'msg' => "The command failed with exit code #{exit_code}",
38
+ 'msg' => "The command failed with exit code #{value['exit_code']}",
45
39
  'details' => details
46
40
  }
47
41
  end
@@ -24,9 +24,7 @@ module Bolt
24
24
  running_as(options[:run_as]) do
25
25
  output = execute(command, environment: options[:env_vars], sudoable: true)
26
26
  Bolt::Result.for_command(target,
27
- output.stdout.string,
28
- output.stderr.string,
29
- output.exit_code,
27
+ output.to_h,
30
28
  'command',
31
29
  command,
32
30
  position)
@@ -98,9 +96,7 @@ module Bolt
98
96
  dir.chown(run_as)
99
97
  output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
100
98
  Bolt::Result.for_command(target,
101
- output.stdout.string,
102
- output.stderr.string,
103
- output.exit_code,
99
+ output.to_h,
104
100
  'script',
105
101
  script,
106
102
  position)
@@ -149,21 +145,21 @@ module Bolt
149
145
 
150
146
  remote_task_path = write_executable(task_dir, executable)
151
147
 
148
+ execute_options[:stdin] = stdin
149
+
152
150
  # Avoid the horrors of passing data on stdin via a tty on multiple platforms
153
151
  # by writing a wrapper script that directs stdin to the task.
154
152
  if stdin && target.options['tty']
155
153
  wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
154
+ # Wrapper script handles interpreter and stdin. Delete these execute options
156
155
  execute_options.delete(:interpreter)
156
+ execute_options.delete(:stdin)
157
157
  execute_options[:wrapper] = true
158
158
  remote_task_path = write_executable(dir, wrapper, 'wrapper.sh')
159
159
  end
160
160
 
161
161
  dir.chown(run_as)
162
162
 
163
- # Don't pass parameters on stdin if using a tty, as the parameters are
164
- # already part of the wrapper script.
165
- execute_options[:stdin] = stdin unless stdin && target.options['tty']
166
-
167
163
  execute_options[:sudoable] = true if run_as
168
164
  output = execute(remote_task_path, **execute_options)
169
165
  end
@@ -424,7 +420,8 @@ module Bolt
424
420
  @stream_logger.warn(formatted)
425
421
  end
426
422
 
427
- read_streams[stream] << to_print
423
+ read_streams[stream] << to_print
424
+ result_output.merged_output << to_print
428
425
  rescue EOFError
429
426
  end
430
427
 
@@ -474,7 +471,8 @@ module Bolt
474
471
  when 126
475
472
  msg = "\n\nThis might be caused by the default tmpdir being mounted "\
476
473
  "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
477
- result_output.stderr << msg
474
+ result_output.stderr << msg
475
+ result_output.merged_output << msg
478
476
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
479
477
  else
480
478
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
@@ -195,9 +195,7 @@ module Bolt
195
195
  wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
196
196
  output = execute(command, wrap_command)
197
197
  Bolt::Result.for_command(target,
198
- output.stdout.string,
199
- output.stderr.string,
200
- output.exit_code,
198
+ output.to_h,
201
199
  'command',
202
200
  command,
203
201
  position)
@@ -224,9 +222,7 @@ module Bolt
224
222
  output = execute([shell_init, *env_assignments, command].join("\r\n"))
225
223
 
226
224
  Bolt::Result.for_command(target,
227
- output.stdout.string,
228
- output.stderr.string,
229
- output.exit_code,
225
+ output.to_h,
230
226
  'script',
231
227
  script,
232
228
  position)
@@ -322,7 +318,8 @@ module Bolt
322
318
  end.join("\n")
323
319
  @stream_logger.warn(formatted)
324
320
  end
325
- result.stdout << to_print
321
+ result.stdout << to_print
322
+ result.merged_output << to_print
326
323
  end
327
324
  stderr = Thread.new do
328
325
  encoding = err.external_encoding
@@ -334,7 +331,8 @@ module Bolt
334
331
  end.join("\n")
335
332
  @stream_logger.warn(formatted)
336
333
  end
337
- result.stderr << to_print
334
+ result.stderr << to_print
335
+ result.merged_output << to_print
338
336
  end
339
337
 
340
338
  stdout.join
@@ -24,17 +24,17 @@ module Bolt
24
24
  end
25
25
 
26
26
  def container_id
27
- "local:#{@target.host}"
27
+ "#{@target.transport_config['remote']}:#{@target.host}"
28
28
  end
29
29
 
30
30
  def connect
31
- out, err, status = execute_local_command(%w[list --format json])
31
+ out, err, status = execute_local_command(%W[list #{container_id} --format json])
32
32
  unless status.exitstatus.zero?
33
33
  raise "Error listing available containers: #{err}"
34
34
  end
35
- containers = JSON.parse(out).map { |c| c['name'] }
36
- unless containers.include?(@target.host)
37
- raise "Could not find a container with name or ID matching '#{@target.host}'"
35
+ containers = JSON.parse(out)
36
+ if containers.empty?
37
+ raise "Could not find a container with name or ID matching '#{container_id}'"
38
38
  end
39
39
  @logger.trace("Opened session")
40
40
  true
@@ -59,6 +59,18 @@ module Bolt
59
59
  # the result otherwise make sure an error is generated
60
60
  if state == 'finished' || (result && result['_error'])
61
61
  if result['_error']
62
+ unless result['_error'].is_a?(Hash)
63
+ result['_error'] = { 'kind' => 'puppetlabs.tasks/task-error',
64
+ 'issue_code' => 'TASK_ERROR',
65
+ 'msg' => result['_error'],
66
+ 'details' => {} }
67
+ end
68
+
69
+ result['_error']['details'] ||= {}
70
+ unless result['_error']['details'].is_a?(Hash)
71
+ deets = result['_error']['details']
72
+ result['_error']['details'] = { 'msg' => deets }
73
+ end
62
74
  file_line = %w[file line].zip(position).to_h.compact
63
75
  result['_error']['details'].merge!(file_line) unless result['_error']['details']['file']
64
76
  end
@@ -252,11 +264,7 @@ module Bolt
252
264
 
253
265
  # If we get here, there's no error so we don't need the file or line
254
266
  # number
255
- Bolt::Result.for_command(target,
256
- result.value['stdout'],
257
- result.value['stderr'],
258
- result.value['exit_code'],
259
- action, obj, [])
267
+ Bolt::Result.for_command(target, result.value, action, obj, [])
260
268
  end
261
269
  end
262
270
  end
data/lib/bolt/util.rb CHANGED
@@ -77,6 +77,14 @@ module Bolt
77
77
  File.exist?(path) ? read_yaml_hash(path, file_name) : {}
78
78
  end
79
79
 
80
+ def first_runs_free
81
+ Bolt::Config.user_path + '.first_runs_free'
82
+ end
83
+
84
+ def first_run?
85
+ Bolt::Config.user_path && !File.exist?(first_runs_free)
86
+ end
87
+
80
88
  # Accepts a path with either 'plans' or 'tasks' in it and determines
81
89
  # the name of the module
82
90
  def module_name(path)
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.3.0'
4
+ VERSION = '3.4.0'
5
5
  end
@@ -29,7 +29,14 @@ module BoltSpec
29
29
  end
30
30
 
31
31
  def result_for(target, stdout: '', stderr: '')
32
- Bolt::Result.for_command(target, stdout, stderr, 0, 'command', '', [])
32
+ value = {
33
+ 'stdout' => stdout,
34
+ 'stderr' => stderr,
35
+ 'merged_output' => "#{stdout}\n#{stderr}".strip,
36
+ 'exit_code' => 0
37
+ }
38
+
39
+ Bolt::Result.for_command(target, value, 'command', '', [])
33
40
  end
34
41
 
35
42
  # Public methods
@@ -35,7 +35,14 @@ module BoltSpec
35
35
  end
36
36
 
37
37
  def result_for(target, stdout: '', stderr: '')
38
- Bolt::Result.for_command(target, stdout, stderr, 0, 'script', '', [])
38
+ value = {
39
+ 'stdout' => stdout,
40
+ 'stderr' => stderr,
41
+ 'merged_output' => "#{stdout}\n#{stderr}".strip,
42
+ 'exit_code' => 0
43
+ }
44
+
45
+ Bolt::Result.for_command(target, value, 'script', '', [])
39
46
  end
40
47
 
41
48
  # Public methods
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.3.0
4
+ version: 3.4.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-15 00:00:00.000000000 Z
11
+ date: 2021-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -444,6 +444,7 @@ files:
444
444
  - bolt-modules/file/lib/puppet/functions/file/write.rb
445
445
  - bolt-modules/out/lib/puppet/functions/out/message.rb
446
446
  - bolt-modules/prompt/lib/puppet/functions/prompt.rb
447
+ - bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb
447
448
  - bolt-modules/system/lib/puppet/functions/system/env.rb
448
449
  - exe/bolt
449
450
  - guides/inventory.txt