bolt 2.6.0 → 2.7.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: ca1602020625348ee5ba03e246feeb9263ca10fdcda10e13bba36eb674dcdfba
4
- data.tar.gz: bdde3554a2406f427f57d02f2e0063e592b7ceba235656c406e9f1ece3899c75
3
+ metadata.gz: 9354d4101556422d8d97c758b64a4e717a9b24488d11cfe48042ebe79e1a28fa
4
+ data.tar.gz: 3ca45b8616f2ff1aa53c67cccd150d06c7a2d8897a11b3ffdfda352d6d10ec0c
5
5
  SHA512:
6
- metadata.gz: 13bcc53364488a1f37ebc19821c6429bcbcc055a543f9b581352b03c403793579290af88be0b92416506c3ddd9f23e775c7d9b9a6d2369aed3c38b0ef162da8f
7
- data.tar.gz: ea09c6b92cc08d1cd893c136775070df2f6d79097663c58a96212bfdcf23984fe39fd5690d706f4802a61cd384bfe8139e446914506938605ba6d4771f272d63
6
+ metadata.gz: 8afb009f9b4d602b3255d86dac20551fae9389782abd2dbb3bb256eb3431a33ad7e8bfa8c87d3ffcb14d9c0aad551a5ddb2df5df80a01d8e41b4ebe49a4a2ff5
7
+ data.tar.gz: b3b471d539c81f37bfcfe1b2b7a3e4a84db0ada296e4300a56830d5a41ad91db96efb9583b84027f85a69d0588f7c0a2fb977d6da24aa1f08e16aea856fb6d03
@@ -117,7 +117,7 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
117
117
  # undef/nil
118
118
  result = catch(:return) do
119
119
  scope.with_global_scope do |global_scope|
120
- closure.call_by_name_with_scope(global_scope, params, true)
120
+ executor.run_plan(global_scope, closure, params)
121
121
  end
122
122
  nil
123
123
  end&.value
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Display a prompt and wait for a response.
6
+ #
7
+ # > **Note:** Not available in apply block
8
+ Puppet::Functions.create_function(:prompt) do
9
+ # @param prompt The prompt to display.
10
+ # @param options A hash of additional options.
11
+ # @option options [Boolean] sensitive Disable echo back and mark the response as sensitive.
12
+ # @return The response to the prompt.
13
+ # @example Prompt the user if plan execution should continue
14
+ # $response = prompt('Continue executing plan? [Y\N]')
15
+ # @example Prompt the user for sensitive information
16
+ # $password = prompt('Enter your password', 'sensitive' => true)
17
+ dispatch :prompt do
18
+ param 'String', :prompt
19
+ optional_param 'Hash[String[1], Any]', :options
20
+ return_type 'Variant[String, Sensitive]'
21
+ end
22
+
23
+ def prompt(prompt, options = {})
24
+ unless Puppet[:tasks]
25
+ raise Puppet::ParseErrorWithIssue
26
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
27
+ action: 'prompt')
28
+ end
29
+
30
+ options = options.transform_keys(&:to_sym)
31
+
32
+ executor = Puppet.lookup(:bolt_executor)
33
+ executor.report_function_call(self.class.name)
34
+
35
+ response = executor.prompt(prompt, options)
36
+
37
+ if options[:sensitive]
38
+ Puppet::Pops::Types::PSensitiveType::Sensitive.new(response)
39
+ else
40
+ response
41
+ end
42
+ end
43
+ end
@@ -63,7 +63,7 @@ module Bolt
63
63
  disabled: true
64
64
 
65
65
  Read more about what data Bolt collects and why here:
66
- https://puppet.com/docs/bolt/latest/bolt_installing.html#concept-8242
66
+ https://puppet.com/docs/bolt/latest/bolt_installing.html#analytics-data-collection
67
67
  ANALYTICS
68
68
  end
69
69
 
@@ -9,7 +9,7 @@ module Bolt
9
9
  OPTIONS = { inventory: %w[targets query rerun description],
10
10
  authentication: %w[user password password-prompt private-key host-key-check ssl ssl-verify],
11
11
  escalation: %w[run-as sudo-password sudo-password-prompt sudo-executable],
12
- run_context: %w[concurrency inventoryfile save-rerun],
12
+ run_context: %w[concurrency inventoryfile save-rerun cleanup],
13
13
  global_config_setters: %w[modulepath boltdir configfile],
14
14
  transports: %w[transport connect-timeout tty],
15
15
  display: %w[format color verbose trace],
@@ -695,6 +695,10 @@ module Bolt
695
695
  'Maximum number of simultaneous manifest block compiles (default: number of cores)') do |concurrency|
696
696
  @options[:'compile-concurrency'] = concurrency
697
697
  end
698
+ define('--[no-]cleanup',
699
+ 'Whether to clean up temporary files created on targets') do |cleanup|
700
+ @options[:cleanup] = cleanup
701
+ end
698
702
  define('-m', '--modulepath MODULES',
699
703
  "List of directories containing modules, separated by '#{File::PATH_SEPARATOR}'",
700
704
  'Directories are case-sensitive') do |modulepath|
@@ -70,9 +70,38 @@ module Bolt
70
70
  bolt_inventory: inv) do
71
71
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
72
72
  pal.with_catalog_compiler do |compiler|
73
- # This needs to happen inside the catalog compiler so loaders are initialized for loading
74
- vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
75
- pal.send(:add_variables, compiler.send(:topscope), target['variables'].merge(vars))
73
+ # Deserializing needs to happen inside the catalog compiler so
74
+ # loaders are initialized for loading
75
+ plan_vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
76
+
77
+ # Facts will be set by the catalog compiler, so we need to ensure
78
+ # that any plan or target variables with the same name are not
79
+ # passed into the apply block to avoid a redefinition error.
80
+ # Filter out plan and target vars separately and raise a Puppet
81
+ # warning if there are any collisions for either. Puppet warning
82
+ # is the only way to log a message that will make it back to Bolt
83
+ # to be printed.
84
+ pv_collisions, pv_filtered = plan_vars.partition do |k, _|
85
+ target['facts'].keys.include?(k)
86
+ end.map(&:to_h)
87
+ unless pv_collisions.empty?
88
+ print_pv = pv_collisions.keys.map { |k| "$#{k}" }.join(', ')
89
+ plural = pv_collisions.keys.length == 1 ? '' : 's'
90
+ Puppet.warning("Plan variable#{plural} #{print_pv} will be overridden by fact#{plural} " \
91
+ "of the same name in the apply block")
92
+ end
93
+
94
+ tv_collisions, tv_filtered = target['variables'].partition do |k, _|
95
+ target['facts'].keys.include?(k)
96
+ end.map(&:to_h)
97
+ unless tv_collisions.empty?
98
+ print_tv = tv_collisions.keys.map { |k| "$#{k}" }.join(', ')
99
+ plural = tv_collisions.keys.length == 1 ? '' : 's'
100
+ Puppet.warning("Target variable#{plural} #{print_tv} " \
101
+ "will be overridden by fact#{plural} of the same name in the apply block")
102
+ end
103
+
104
+ pal.send(:add_variables, compiler.send(:topscope), tv_filtered.merge(pv_filtered))
76
105
 
77
106
  # Configure language strictness in the CatalogCompiler. We want Bolt to be able
78
107
  # to compile most Puppet 4+ manifests, so we default to allowing deprecated functions.
@@ -8,6 +8,8 @@ module Bolt
8
8
  module Transport
9
9
  class Docker < Base
10
10
  OPTIONS = {
11
+ "cleanup" => { type: TrueClass,
12
+ desc: "Whether to clean up temporary files created on targets." },
11
13
  "host" => { type: String,
12
14
  desc: "Host name." },
13
15
  "interpreters" => { type: Hash,
@@ -27,7 +29,9 @@ module Bolt
27
29
  desc: "Whether to enable tty on exec commands." }
28
30
  }.freeze
29
31
 
30
- DEFAULTS = {}.freeze
32
+ DEFAULTS = {
33
+ 'cleanup' => true
34
+ }.freeze
31
35
 
32
36
  private def validate
33
37
  super
@@ -8,6 +8,8 @@ module Bolt
8
8
  module Transport
9
9
  class Local < Base
10
10
  OPTIONS = {
11
+ "cleanup" => { type: TrueClass,
12
+ desc: "Whether to clean up temporary files created on targets." },
11
13
  "interpreters" => { type: Hash,
12
14
  desc: "A map of an extension name to the absolute path of an executable, "\
13
15
  "enabling you to override the shebang defined in a task executable. The "\
@@ -36,6 +38,8 @@ module Bolt
36
38
  }.freeze
37
39
 
38
40
  WINDOWS_OPTIONS = {
41
+ "cleanup" => { type: TrueClass,
42
+ desc: "Whether to clean up temporary files created on targets." },
39
43
  "interpreters" => { type: Hash,
40
44
  desc: "A map of an extension name to the absolute path of an executable, "\
41
45
  "enabling you to override the shebang defined in a task executable. The "\
@@ -47,7 +51,9 @@ module Bolt
47
51
  desc: "The directory to copy and execute temporary files." }
48
52
  }.freeze
49
53
 
50
- DEFAULTS = {}.freeze
54
+ DEFAULTS = {
55
+ 'cleanup' => true
56
+ }.freeze
51
57
 
52
58
  def self.options
53
59
  Bolt::Util.windows? ? WINDOWS_OPTIONS : OPTIONS
@@ -7,7 +7,10 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class SSH < Base
10
+ LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
10
11
  OPTIONS = {
12
+ "cleanup" => { type: TrueClass,
13
+ desc: "Whether to clean up temporary files created on targets." },
11
14
  "connect-timeout" => { type: Integer,
12
15
  desc: "How long to wait when establishing connections." },
13
16
  "disconnect-timeout" => { type: Integer,
@@ -16,6 +19,12 @@ module Bolt
16
19
  desc: "Host name." },
17
20
  "host-key-check" => { type: TrueClass,
18
21
  desc: "Whether to perform host key validation when connecting." },
22
+ "extensions" => { type: Array,
23
+ desc: "List of file extensions that are accepted for scripts or tasks on Windows. "\
24
+ "Scripts with these file extensions rely on the target's file type "\
25
+ "association to run. For example, if Python is installed on the system, "\
26
+ "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
27
+ "`.pp` are always allowed and run via hard-coded executables." },
19
28
  "interpreters" => { type: Hash,
20
29
  desc: "A map of an extension name to the absolute path of an executable, "\
21
30
  "enabling you to override the shebang defined in a task executable. The "\
@@ -25,6 +34,10 @@ module Bolt
25
34
  "Bolt Ruby interpreter by default." },
26
35
  "load-config" => { type: TrueClass,
27
36
  desc: "Whether to load system SSH configuration." },
37
+ "login-shell" => { type: String,
38
+ desc: "Which login shell Bolt should expect on the target. "\
39
+ "Supported shells are #{LOGIN_SHELLS.join(', ')}. "\
40
+ "**This option is experimental.**" },
28
41
  "password" => { type: String,
29
42
  desc: "Login password." },
30
43
  "port" => { type: Integer,
@@ -67,10 +80,12 @@ module Bolt
67
80
  }.freeze
68
81
 
69
82
  DEFAULTS = {
83
+ "cleanup" => true,
70
84
  "connect-timeout" => 10,
71
85
  "tty" => false,
72
86
  "load-config" => true,
73
- "disconnect-timeout" => 5
87
+ "disconnect-timeout" => 5,
88
+ "login-shell" => 'bash'
74
89
  }.freeze
75
90
 
76
91
  private def validate
@@ -92,12 +107,26 @@ module Bolt
92
107
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
93
108
  end
94
109
 
110
+ if @config['login-shell'] && !LOGIN_SHELLS.include?(@config['login-shell'])
111
+ raise Bolt::ValidationError,
112
+ "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
113
+ end
114
+
95
115
  if (run_as_cmd = @config['run-as-command'])
96
116
  unless run_as_cmd.all? { |n| n.is_a?(String) }
97
117
  raise Bolt::ValidationError,
98
118
  "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
99
119
  end
100
120
  end
121
+
122
+ if @config['login-shell'] == 'powershell'
123
+ %w[tty run-as].each do |key|
124
+ if @config[key]
125
+ raise Bolt::ValidationError,
126
+ "#{key} is not supported when using PowerShell"
127
+ end
128
+ end
129
+ end
101
130
  end
102
131
  end
103
132
  end
@@ -12,6 +12,8 @@ module Bolt
12
12
  desc: "Force basic authentication. This option is only available when using SSL." },
13
13
  "cacert" => { type: String,
14
14
  desc: "The path to the CA certificate." },
15
+ "cleanup" => { type: TrueClass,
16
+ desc: "Whether to clean up temporary files created on targets." },
15
17
  "connect-timeout" => { type: Integer,
16
18
  desc: "How long Bolt should wait when establishing connections." },
17
19
  "extensions" => { type: Array,
@@ -53,6 +55,7 @@ module Bolt
53
55
 
54
56
  DEFAULTS = {
55
57
  "basic-auth-only" => false,
58
+ "cleanup" => true,
56
59
  "connect-timeout" => 10,
57
60
  "ssl" => true,
58
61
  "ssl-verify" => true,
@@ -291,6 +291,10 @@ module Bolt
291
291
  end
292
292
  end
293
293
 
294
+ def run_plan(scope, plan, params)
295
+ plan.call_by_name_with_scope(scope, params, true)
296
+ end
297
+
294
298
  class TimeoutError < RuntimeError; end
295
299
 
296
300
  def wait_until_available(targets,
@@ -326,6 +330,24 @@ module Bolt
326
330
  end
327
331
  end
328
332
 
333
+ def prompt(prompt, options)
334
+ unless STDIN.tty?
335
+ raise Bolt::Error.new('STDIN is not a tty, unable to prompt', 'bolt/no-tty-error')
336
+ end
337
+
338
+ STDERR.print("#{prompt}: ")
339
+
340
+ value = if options[:sensitive]
341
+ STDIN.noecho(&:gets).to_s.chomp
342
+ else
343
+ STDIN.gets.to_s.chomp
344
+ end
345
+
346
+ STDERR.puts if options[:sensitive]
347
+
348
+ value
349
+ end
350
+
329
351
  # Plan context doesn't make sense for most transports but it is tightly
330
352
  # coupled with the orchestrator transport since the transport behaves
331
353
  # differently when a plan is running. In order to limit how much this
@@ -50,6 +50,10 @@ module Bolt
50
50
  @group_lookup.keys
51
51
  end
52
52
 
53
+ def group_names_for(target_name)
54
+ group_data_for(target_name).fetch('groups', [])
55
+ end
56
+
53
57
  def target_names
54
58
  @groups.all_targets
55
59
  end
@@ -17,13 +17,14 @@ module Bolt
17
17
  unless opts['var']
18
18
  raise Bolt::ValidationError, "env_var plugin requires that the 'var' is specified"
19
19
  end
20
+ return if opts['optional'] || opts['default']
20
21
  unless ENV[opts['var']]
21
22
  raise Bolt::ValidationError, "env_var plugin requires that the var '#{opts['var']}' be set"
22
23
  end
23
24
  end
24
25
 
25
26
  def resolve_reference(opts)
26
- ENV[opts['var']]
27
+ ENV[opts['var']] || opts['default']
27
28
  end
28
29
  end
29
30
  end
@@ -19,7 +19,7 @@ module Bolt
19
19
 
20
20
  def resolve_reference(opts)
21
21
  STDERR.print("#{opts['message']}: ")
22
- value = STDIN.noecho(&:gets).chomp
22
+ value = STDIN.noecho(&:gets).to_s.chomp
23
23
  STDERR.puts
24
24
 
25
25
  value
@@ -34,7 +34,7 @@ module Bolt
34
34
 
35
35
  def upload(source, destination, options = {})
36
36
  running_as(options[:run_as]) do
37
- with_tempdir do |dir|
37
+ with_tmpdir do |dir|
38
38
  basename = File.basename(destination)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
40
  conn.copy_file(source, tmpfile)
@@ -55,7 +55,7 @@ module Bolt
55
55
  arguments = unwrap_sensitive_args(arguments)
56
56
 
57
57
  running_as(options[:run_as]) do
58
- with_tempdir do |dir|
58
+ with_tmpdir do |dir|
59
59
  path = write_executable(dir.to_s, script)
60
60
  dir.chown(run_as)
61
61
  output = execute([path, *arguments], sudoable: true)
@@ -86,7 +86,7 @@ module Bolt
86
86
  # unpack any Sensitive data
87
87
  arguments = unwrap_sensitive_args(arguments)
88
88
 
89
- with_tempdir do |dir|
89
+ with_tmpdir do |dir|
90
90
  if extra_files.empty?
91
91
  task_dir = dir
92
92
  else
@@ -234,7 +234,7 @@ module Bolt
234
234
  end
235
235
  end
236
236
 
237
- def make_tempdir
237
+ def make_tmpdir
238
238
  tmpdir = @target.options.fetch('tmpdir', '/tmp')
239
239
  script_dir = @target.options.fetch('script-dir', SecureRandom.uuid)
240
240
  tmppath = File.join(tmpdir, script_dir)
@@ -242,7 +242,7 @@ module Bolt
242
242
 
243
243
  result = execute(command)
244
244
  if result.exit_code != 0
245
- raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
245
+ raise Bolt::Node::FileError.new("Could not make tmpdir: #{result.stderr.string}", 'TMPDIR_ERROR')
246
246
  end
247
247
  path = tmppath || result.stdout.string.chomp
248
248
  Bolt::Shell::Bash::Tmpdir.new(self, path)
@@ -256,13 +256,19 @@ module Bolt
256
256
  remote_path
257
257
  end
258
258
 
259
- # A helper to create and delete a tempdir on the remote system. Yields the
259
+ # A helper to create and delete a tmpdir on the remote system. Yields the
260
260
  # directory name.
261
- def with_tempdir
262
- dir = make_tempdir
261
+ def with_tmpdir
262
+ dir = make_tmpdir
263
263
  yield dir
264
264
  ensure
265
- dir&.delete
265
+ if dir
266
+ if target.options['cleanup']
267
+ dir.delete
268
+ else
269
+ @logger.warn("Skipping cleanup of tmpdir #{dir}")
270
+ end
271
+ end
266
272
  end
267
273
 
268
274
  # In the case where a task is run with elevated privilege and needs stdin
@@ -48,7 +48,7 @@ module Bolt
48
48
  def delete
49
49
  result = @shell.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
50
50
  if result.exit_code != 0
51
- @logger.warn("Failed to clean up tempdir '#{@path}': #{result.stderr.string}")
51
+ @logger.warn("Failed to clean up tmpdir '#{@path}': #{result.stderr.string}")
52
52
  end
53
53
  # For testing
54
54
  result.stderr.string
@@ -6,7 +6,7 @@ module Bolt
6
6
  class Shell
7
7
  class Powershell < Shell
8
8
  DEFAULT_EXTENSIONS = Set.new(%w[.ps1 .rb .pp])
9
- PS_ARGS = %w[-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -File].freeze
9
+ PS_ARGS = %w[-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass].freeze
10
10
 
11
11
  def initialize(target, conn)
12
12
  super
@@ -45,7 +45,7 @@ module Bolt
45
45
  when '.ps1'
46
46
  [
47
47
  'powershell.exe',
48
- [*PS_ARGS, path]
48
+ [*PS_ARGS, '-File', path]
49
49
  ]
50
50
  when '.pp'
51
51
  [
@@ -119,11 +119,11 @@ module Bolt
119
119
  end
120
120
  end
121
121
 
122
- def make_tempdir
122
+ def make_tmpdir
123
123
  find_parent = target.options['tmpdir'] ? "\"#{target.options['tmpdir']}\"" : '[System.IO.Path]::GetTempPath()'
124
- result = execute(Snippets.make_tempdir(find_parent))
124
+ result = execute(Snippets.make_tmpdir(find_parent))
125
125
  if result.exit_code != 0
126
- raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
126
+ raise Bolt::Node::FileError.new("Could not make tmpdir: #{result.stderr.string}", 'TMPDIR_ERROR')
127
127
  end
128
128
  result.stdout.string.chomp
129
129
  end
@@ -132,11 +132,21 @@ module Bolt
132
132
  execute(Snippets.rmdir(dir))
133
133
  end
134
134
 
135
- def with_tempdir
136
- dir = make_tempdir
137
- yield dir
135
+ def with_tmpdir
136
+ unless @tmpdir
137
+ # Only cleanup the directory afterward if we made it to begin with
138
+ owner = true
139
+ @tmpdir = make_tmpdir
140
+ end
141
+ yield @tmpdir
138
142
  ensure
139
- rmdir(dir)
143
+ if owner && @tmpdir
144
+ if target.options['cleanup']
145
+ rmdir(@tmpdir)
146
+ else
147
+ @logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'")
148
+ end
149
+ end
140
150
  end
141
151
 
142
152
  def run_ps_task(task_path, arguments, input_method)
@@ -167,7 +177,7 @@ module Bolt
167
177
  def run_script(script, arguments, _options = {})
168
178
  # unpack any Sensitive data
169
179
  arguments = unwrap_sensitive_args(arguments)
170
- with_tempdir do |dir|
180
+ with_tmpdir do |dir|
171
181
  script_path = write_executable(dir, script)
172
182
  command = if powershell_file?(script_path)
173
183
  Snippets.run_script(arguments, script_path)
@@ -194,7 +204,7 @@ module Bolt
194
204
 
195
205
  # unpack any Sensitive data
196
206
  arguments = unwrap_sensitive_args(arguments)
197
- with_tempdir do |dir|
207
+ with_tmpdir do |dir|
198
208
  if extra_files.empty?
199
209
  task_dir = dir
200
210
  else
@@ -243,6 +253,16 @@ module Bolt
243
253
  end
244
254
 
245
255
  def execute(command)
256
+ if conn.max_command_length && command.length > conn.max_command_length
257
+ return with_tmpdir do |dir|
258
+ command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
259
+ script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
260
+ conn.copy_file(StringIO.new(command), script_file)
261
+ args = escape_arguments([script_file])
262
+ script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
263
+ execute(script_invocation)
264
+ end
265
+ end
246
266
  inp, out, err, t = conn.execute(command)
247
267
 
248
268
  result = Bolt::Node::Output.new
@@ -20,7 +20,7 @@ module Bolt
20
20
  PS
21
21
  end
22
22
 
23
- def make_tempdir(parent)
23
+ def make_tmpdir(parent)
24
24
  <<~PS
25
25
  $parent = #{parent}
26
26
  $name = [System.IO.Path]::GetRandomFileName()
@@ -99,7 +99,8 @@ module Bolt
99
99
  'vars' => vars,
100
100
  'features' => features,
101
101
  'facts' => facts,
102
- 'plugin_hooks' => plugin_hooks
102
+ 'plugin_hooks' => plugin_hooks,
103
+ 'groups' => @inventory.group_names_for(name)
103
104
  }
104
105
  end
105
106
 
@@ -19,7 +19,7 @@ module Bolt
19
19
 
20
20
  def upload(target, source, destination, _options = {})
21
21
  with_connection(target) do |conn|
22
- conn.with_remote_tempdir do |dir|
22
+ conn.with_remote_tmpdir do |dir|
23
23
  basename = File.basename(destination)
24
24
  tmpfile = "#{dir}/#{basename}"
25
25
  if File.directory?(source)
@@ -57,7 +57,7 @@ module Bolt
57
57
  arguments = unwrap_sensitive_args(arguments)
58
58
 
59
59
  with_connection(target) do |conn|
60
- conn.with_remote_tempdir do |dir|
60
+ conn.with_remote_tmpdir do |dir|
61
61
  remote_path = conn.write_remote_executable(dir, script)
62
62
  stdout, stderr, exitcode = conn.execute(remote_path, *arguments, {})
63
63
  Bolt::Result.for_command(target, stdout, stderr, exitcode, 'script', script)
@@ -77,7 +77,7 @@ module Bolt
77
77
  with_connection(target) do |conn|
78
78
  execute_options = {}
79
79
  execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
80
- conn.with_remote_tempdir do |dir|
80
+ conn.with_remote_tmpdir do |dir|
81
81
  if extra_files.empty?
82
82
  task_dir = dir
83
83
  else
@@ -103,25 +103,29 @@ module Bolt
103
103
  end
104
104
  end
105
105
 
106
- def make_tempdir
106
+ def make_tmpdir
107
107
  tmpdir = @target.options.fetch('tmpdir', container_tmpdir)
108
108
  tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
109
109
 
110
110
  stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
111
111
  if exitcode != 0
112
- raise Bolt::Node::FileError.new("Could not make tempdir: #{stderr}", 'TEMPDIR_ERROR')
112
+ raise Bolt::Node::FileError.new("Could not make tmpdir: #{stderr}", 'TMPDIR_ERROR')
113
113
  end
114
114
  tmppath || stdout.first
115
115
  end
116
116
 
117
- def with_remote_tempdir
118
- dir = make_tempdir
117
+ def with_remote_tmpdir
118
+ dir = make_tmpdir
119
119
  yield dir
120
120
  ensure
121
121
  if dir
122
- _, stderr, exitcode = execute('rm', '-rf', dir, {})
123
- if exitcode != 0
124
- @logger.warn("Failed to clean up tempdir '#{dir}': #{stderr}")
122
+ if @target.options['cleanup']
123
+ _, stderr, exitcode = execute('rm', '-rf', dir, {})
124
+ if exitcode != 0
125
+ @logger.warn("Failed to clean up tmpdir '#{dir}': #{stderr}")
126
+ end
127
+ else
128
+ @logger.warn("Skipping cleanup of tmpdir '#{dir}'")
125
129
  end
126
130
  end
127
131
  end
@@ -32,7 +32,8 @@ module Bolt
32
32
  if source.is_a?(StringIO)
33
33
  Tempfile.create(File.basename(dest)) do |f|
34
34
  f.write(source.read)
35
- FileUtils.mv(t, dest)
35
+ f.close
36
+ FileUtils.mv(f, dest)
36
37
  end
37
38
  else
38
39
  # Mimic the behavior of `cp --remove-destination`
@@ -46,12 +47,11 @@ module Bolt
46
47
 
47
48
  def execute(command)
48
49
  if Bolt::Util.windows?
49
- command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
50
- script_file = Tempfile.new(['wrapper', '.ps1'], target.options['tmpdir'])
51
- File.write(script_file, command)
52
- script_file.close
53
-
54
- command = ['powershell.exe', *Bolt::Shell::Powershell::PS_ARGS, script_file.path]
50
+ # If it's already a powershell command then invoke it normally.
51
+ # Otherwise, wrap it in powershell.exe.
52
+ unless command.start_with?('powershell.exe')
53
+ command = ['powershell.exe', *Bolt::Shell::Powershell::PS_ARGS, '-Command', command]
54
+ end
55
55
  end
56
56
 
57
57
  Open3.popen3(*command)
@@ -62,6 +62,12 @@ module Bolt
62
62
  def reset_cwd?
63
63
  false
64
64
  end
65
+
66
+ def max_command_length
67
+ if Bolt::Util.windows?
68
+ 32000
69
+ end
70
+ end
65
71
  end
66
72
  end
67
73
  end
@@ -197,7 +197,7 @@ module Bolt
197
197
  end
198
198
  rescue StandardError => e
199
199
  targets.map do |target|
200
- Bolt::Result.from_exception(target, e, 'task')
200
+ Bolt::Result.from_exception(target, e, action: 'task')
201
201
  end
202
202
  end
203
203
  end
@@ -108,6 +108,7 @@ module Bolt
108
108
  end
109
109
 
110
110
  @session = Net::SSH.start(target.host, @user, options)
111
+ validate_ssh_version
111
112
  @logger.debug { "Opened session" }
112
113
  rescue Net::SSH::AuthenticationFailed => e
113
114
  raise Bolt::Node::ConnectError.new(
@@ -242,7 +243,11 @@ module Bolt
242
243
  end
243
244
 
244
245
  def shell
245
- @shell ||= Bolt::Shell::Bash.new(target, self)
246
+ @shell ||= if target.options['login-shell'] == 'powershell'
247
+ Bolt::Shell::Powershell.new(target, self)
248
+ else
249
+ Bolt::Shell::Bash.new(target, self)
250
+ end
246
251
  end
247
252
 
248
253
  # This is used by the Bash shell to decide whether to `cd` before
@@ -250,6 +255,22 @@ module Bolt
250
255
  def reset_cwd?
251
256
  true
252
257
  end
258
+
259
+ def max_command_length
260
+ if target.options['login-shell'] == 'powershell'
261
+ 32000
262
+ end
263
+ end
264
+
265
+ def validate_ssh_version
266
+ remote_version = @session.transport.server_version.version
267
+ return unless target.options['login-shell'] && remote_version
268
+
269
+ match = remote_version.match(/OpenSSH_for_Windows_(\d+\.\d+)/)
270
+ if match && match[1].to_f < 7.9
271
+ raise "Powershell over SSH requires OpenSSH server >= 7.9, target is running #{match[1]}"
272
+ end
273
+ end
253
274
  end
254
275
  end
255
276
  end
@@ -175,6 +175,10 @@ module Bolt
175
175
  @shell ||= Bolt::Shell::Powershell.new(target, self)
176
176
  end
177
177
 
178
+ def max_command_length
179
+ nil
180
+ end
181
+
178
182
  private
179
183
 
180
184
  def smb_client_login
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.6.0'
4
+ VERSION = '2.7.0'
5
5
  end
@@ -40,6 +40,23 @@ require 'bolt/pal'
40
40
  # an otherwise empty bolt config and inventory. To create your own values for
41
41
  # these override the modulepath, config, or inventory methods.
42
42
  #
43
+ # Sub-plan Execution
44
+ #
45
+ # When testing a plan, often times those plans call other plans in order to
46
+ # build complex workflows. To support this we offer running in two different
47
+ # modes:
48
+ # execute_any_plan (default) - This mode will execute any plan that is encountered
49
+ # without having to be stubbed/mocked. This default mode allows for plan control
50
+ # flow to behave as normal. If you choose to stub/mock out a sub-plan in this mode
51
+ # that will be honored and the sub-plan will not be executed. We will use the modifiers
52
+ # on the stub to check for the conditions specified (example: be_called_times(3))
53
+ #
54
+ # execute_no_plan - This mode will not execute a plans that it encounters. Instead, when
55
+ # a plan is encountered it will throw an error unless the plan is mocked out. This
56
+ # mode is useful for ensuring that there are no plans called that you do not expect.
57
+ # This plan requires authors to mock out all sub-plans that may be invoked when running
58
+ # tests.
59
+ #
43
60
  # TODO:
44
61
  # - Allow description based stub matching
45
62
  # - Better testing of plan errors
@@ -47,15 +64,20 @@ require 'bolt/pal'
47
64
  # - Allow stubbing with a block(at the double level? As a matched stub?)
48
65
  # - package code so that it can be used for testing modules outside of this repo
49
66
  # - set subject from describe and provide matchers similar to rspec puppets function tests
67
+ # - Allow specific plans to be executed when running in execute_no_plan mode.
50
68
  #
51
69
  # MAYBE TODO?:
52
- # - allow stubbing for subplans
53
70
  # - validate call expectations at the end of the example instead of in run_plan
54
71
  # - resultset matchers to help testing canary like plans?
55
72
  # - inventory matchers to help testing plans that change inventory
56
73
  #
74
+ # Flags:
75
+ # - execute_any_plan: execute any plan that is encountered unless it is mocked (default)
76
+ # - execute_no_plan: throw an error if a plan is encountered that is not stubbed
77
+ #
57
78
  # Stubs:
58
79
  # - allow_command(cmd), expect_command(cmd): expect the exact command
80
+ # - allow_plan(plan), expect_plan(plan): expect the named plan
59
81
  # - allow_script(script), expect_script(script): expect the script as <module>/path/to/file
60
82
  # - allow_task(task), expect_task(task): expect the named task
61
83
  # - allow_upload(file), expect_upload(file): expect the identified source file
@@ -69,22 +91,28 @@ require 'bolt/pal'
69
91
  # if expected, fail unless the action is called 'n' times
70
92
  # - not_be_called: fail if the action is called
71
93
  # - with_targets(targets): target or list of targets that you expect to be passed to the action
94
+ # plan: does not support this modifier
72
95
  # - with_params(params): list of params and metaparams (or options) that you expect to be passed to the action.
73
96
  # Corresponds to the action's last argument.
74
97
  # - with_destination(dest): for upload_file, the expected destination path
75
98
  # - always_return(value): return a Bolt::ResultSet of Bolt::Result objects with the specified value Hash
99
+ # plan: returns a Bolt::PlanResult with the specified value with a status of 'success'
76
100
  # command and script: only accept 'stdout' and 'stderr' keys
77
101
  # upload: does not support this modifier
78
102
  # - return_for_targets(targets_to_values): return a Bolt::ResultSet of Bolt::Result objects from the Hash mapping
79
103
  # targets to their value Hashes
80
104
  # command and script: only accept 'stdout' and 'stderr' keys
81
105
  # upload: does not support this modifier
106
+ # plan: does not support this modifier
82
107
  # - return(&block): invoke the block to construct a Bolt::ResultSet. The blocks parameters differ based on action
83
108
  # command: `{ |targets:, command:, params:| ... }`
109
+ # plan: `{ |plan:, params:| ... }`
84
110
  # script: `{ |targets:, script:, params:| ... }`
85
111
  # task: `{ |targets:, task:, params:| ... }`
86
112
  # upload: `{ |targets:, source:, destination:, params:| ... }`
87
113
  # - error_with(err): return a failing Bolt::ResultSet, with Bolt::Result objects with the identified err hash
114
+ # plans will throw a Bolt::PlanFailure that will be returned as the value of
115
+ # the Bolt::PlanResult object with a status of 'failure'.
88
116
  #
89
117
  # Example:
90
118
  # describe "my_plan" do
@@ -128,12 +156,38 @@ require 'bolt/pal'
128
156
  # end
129
157
  # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
130
158
  # end
131
-
159
+ #
132
160
  # it 'expects multiple messages to out::message' do
133
161
  # expect_out_message.be_called_times(2).with_params(message)
134
162
  # result = run_plan(plan_name, 'messages' => [message, message])
135
163
  # expect(result).to be_ok
136
164
  # end
165
+ #
166
+ # it 'expects a sub-plan to be called' do
167
+ # expect_plan('module::sub_plan').with_params('targets' => ['foo']).be_called_times(1)
168
+ # result = run_plan('module::main_plan', 'targets' => ['foo'])
169
+ # expect(result).to be_ok
170
+ # expect(result.class).to eq(Bolt::PlanResult)
171
+ # expect(result.value).to eq('foo' => 'is_good')
172
+ # expect(result.status).to eq('success')
173
+ # end
174
+ #
175
+ # it 'error when sub-plan is called' do
176
+ # execute_no_plan
177
+ # err = 'Unexpected call to 'run_plan(module::sub_plan, {\"targets\"=>[\"foo\"]})'
178
+ # expect { run_plan('module::main_plan', 'targets' => ['foo']) }
179
+ # .to raise_error(RuntimeError, err)
180
+ # end
181
+ #
182
+ # it 'errors when plan calls fail_plan()' do
183
+ # result = run_plan('module::calls_fail_plan', {})
184
+ # expect(result).not_to be_ok
185
+ # expect(result.class).to eq(Bolt::PlanResult)
186
+ # expect(result.status).to eq('failure')
187
+ # expect(result.value.class).to eq(Bolt::PlanFailure)
188
+ # expect(result.value.msg).to eq('failure message passed to fail_plan()')
189
+ # expect(result.value.kind).to eq('bolt/plan-failure')
190
+ # end
137
191
  # end
138
192
  #
139
193
  # See spec/bolt_spec/plan_spec.rb for more examples.
@@ -166,7 +220,9 @@ module BoltSpec
166
220
 
167
221
  def run_plan(name, params)
168
222
  pal = Bolt::PAL.new(config.modulepath, config.hiera_config, config.boltdir.resource_types)
169
- result = pal.run_plan(name, params, executor, inventory, puppetdb_client)
223
+ result = executor.with_plan_allowed_exec(name, params) do
224
+ pal.run_plan(name, params, executor, inventory, puppetdb_client)
225
+ end
170
226
 
171
227
  if executor.error_message
172
228
  raise executor.error_message
@@ -175,7 +231,7 @@ module BoltSpec
175
231
  begin
176
232
  executor.assert_call_expectations
177
233
  rescue StandardError => e
178
- raise "#{e.message}\nPlan result: #{result}"
234
+ raise "#{e.message}\nPlan result: #{result}\n#{e.backtrace.join("\n")}"
179
235
  end
180
236
 
181
237
  result
@@ -196,9 +252,23 @@ module BoltSpec
196
252
  nil
197
253
  end
198
254
 
199
- # Plan execution does not flow through the executor mocking may make sense but
200
- # will be a separate effort.
201
- # def allow_plan(plan_name)
255
+ # Flag for the default behavior of executing sub-plans during testing
256
+ # By *default* we allow any sub-plan to be executed, no mocking required.
257
+ # Users can still mock out plans in this mode and the mocks will check for
258
+ # parameters and return values like normal. However, if a plan isn't explicitly
259
+ # mocked out, it will be executed.
260
+ def execute_any_plan
261
+ executor.execute_any_plan = true
262
+ end
263
+
264
+ # If you want to explicitly mock out all of the sub-plan calls, then
265
+ # call this prior to calling `run_plan()` along with setting up any
266
+ # mocks that you require.
267
+ # In this mode, any plan that is not explicitly mocked out will not be executed
268
+ # and an error will be thrown.
269
+ def execute_no_plan
270
+ executor.execute_any_plan = false
271
+ end
202
272
 
203
273
  # intended to be private below here
204
274
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/plan_result'
3
4
  require 'bolt/result'
4
5
  require 'bolt/util'
5
6
 
@@ -23,8 +24,8 @@ module BoltSpec
23
24
  @stubs.each { |s| s.assert_called(object) }
24
25
  end
25
26
 
26
- def add_stub
27
- stub = Plans.const_get(@action_stub).new
27
+ def add_stub(inventory = nil)
28
+ stub = Plans.const_get(@action_stub).new(false, inventory)
28
29
  @stubs.unshift stub
29
30
  stub
30
31
  end
@@ -33,7 +34,7 @@ module BoltSpec
33
34
  class ActionStub
34
35
  attr_reader :invocation
35
36
 
36
- def initialize(expect = false)
37
+ def initialize(expect = false, inventory = nil)
37
38
  @calls = 0
38
39
  @expect = expect
39
40
  @expected_calls = nil
@@ -41,6 +42,7 @@ module BoltSpec
41
42
  @invocation = {}
42
43
  # return value
43
44
  @data = { default: {} }
45
+ @inventory = inventory
44
46
  end
45
47
 
46
48
  def assert_called(object)
@@ -55,7 +57,16 @@ module BoltSpec
55
57
  end
56
58
  message = "Expected #{object} to be called #{times} times"
57
59
  message += " with targets #{@invocation[:targets]}" if @invocation[:targets]
58
- message += " with parameters #{parameters}" if parameters
60
+ if parameters
61
+ # Print the parameters hash by converting it to JSON and then re-parsing.
62
+ # This prevents issues in Bolt data types, such as Targets, from generating
63
+ # gigantic, unreadable, data when converted to string by interpolation.
64
+ # Targets exhibit this behavior because they have a reference to @inventory.
65
+ # When the target is converted into a string, it converts the full Inventory
66
+ # into a string recursively.
67
+ parameters_str = JSON.parse(parameters.to_json)
68
+ message += " with parameters #{parameters_str}"
69
+ end
59
70
  raise message
60
71
  end
61
72
  end
@@ -71,6 +82,14 @@ module BoltSpec
71
82
  # Used to create a valid Bolt::Result object from result data.
72
83
  def default_for(target)
73
84
  case @data[:default]
85
+ when Bolt::PlanFailure
86
+ # Bolt::PlanFailure needs to be declared before Bolt::Error because
87
+ # Bolt::PlanFailure is an instance of Bolt::Error, so it can match both
88
+ # in this case we need to treat Bolt::PlanFailure's in a different way
89
+ #
90
+ # raise Bolt::PlanFailure errors so that the PAL can catch them and wrap
91
+ # them into Bolt::PlanResult's for us.
92
+ raise @data[:default]
74
93
  when Bolt::Error
75
94
  Bolt::Result.from_exception(target, @data[:default])
76
95
  when Hash
@@ -87,6 +106,13 @@ module BoltSpec
87
106
  result_set
88
107
  end
89
108
 
109
+ def check_plan_result(plan_result, plan_clj)
110
+ unless plan_result.is_a?(Bolt::PlanResult)
111
+ raise "Return block for #{plan_clj.closure_name} did not return a Bolt::PlanResult"
112
+ end
113
+ plan_result
114
+ end
115
+
90
116
  # Below here are the intended 'public' methods of the stub
91
117
 
92
118
  # Restricts the stub to only match invocations with
@@ -127,7 +153,10 @@ module BoltSpec
127
153
  def return_for_targets(data)
128
154
  data.each_with_object(@data) do |(target, result), hsh|
129
155
  raise "Mocked results must be hashes: #{target}: #{result}" unless result.is_a? Hash
130
- hsh[target] = result_for(Bolt::Target.new(target), Bolt::Util.walk_keys(result, &:to_sym))
156
+ # set the inventory from the BoltSpec::Plans, otherwise if we try to convert
157
+ # this target to a string, it will fail to string conversion because the
158
+ # inventory is nil
159
+ hsh[target] = result_for(Bolt::Target.new(target, @inventory), Bolt::Util.walk_keys(result, &:to_sym))
131
160
  end
132
161
  raise "Cannot set return values and return block." if @return_block
133
162
  @data_set = true
@@ -143,10 +172,10 @@ module BoltSpec
143
172
  end
144
173
 
145
174
  # Set a default error result for all targets.
146
- def error_with(data)
175
+ def error_with(data, clazz = Bolt::Error)
147
176
  data = Bolt::Util.walk_keys(data, &:to_s)
148
177
  if data['msg'] && data['kind'] && (data.keys - %w[msg kind details issue_code]).empty?
149
- @data[:default] = Bolt::Error.new(data['msg'], data['kind'], data['details'], data['issue_code'])
178
+ @data[:default] = clazz.new(data['msg'], data['kind'], data['details'], data['issue_code'])
150
179
  else
151
180
  STDERR.puts "In the future 'error_with()' may require msg and kind, and " \
152
181
  "optionally accept only details and issue_code."
@@ -160,6 +189,7 @@ module BoltSpec
160
189
  end
161
190
 
162
191
  require_relative 'action_stubs/command_stub'
192
+ require_relative 'action_stubs/plan_stub'
163
193
  require_relative 'action_stubs/script_stub'
164
194
  require_relative 'action_stubs/task_stub'
165
195
  require_relative 'action_stubs/upload_stub'
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltSpec
4
+ module Plans
5
+ class PlanStub < ActionStub
6
+ def matches(_scope, _plan, params)
7
+ targets = params.fetch('nodes', params.fetch('targets', nil))
8
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets)
9
+ return false
10
+ end
11
+
12
+ if @invocation[:params] && params != @invocation[:params]
13
+ return false
14
+ end
15
+
16
+ true
17
+ end
18
+
19
+ def call(_scope, plan, params)
20
+ @calls += 1
21
+ if @return_block
22
+ check_plan_result(@return_block.call(plan: plan, params: params), plan)
23
+ else
24
+ default_for(nil)
25
+ end
26
+ end
27
+
28
+ def parameters
29
+ @invocation[:params]
30
+ end
31
+
32
+ # Allow any data.
33
+ def result_for(_target, data)
34
+ Bolt::PlanResult.new(Bolt::Util.walk_keys(data, &:to_s), 'success')
35
+ end
36
+
37
+ # Public methods
38
+
39
+ # Restricts the stub to only match invocations with certain parameters.
40
+ # All parameters must match exactly.
41
+ def with_params(params)
42
+ @invocation[:params] = params
43
+ self
44
+ end
45
+
46
+ def return_for_targets(_data)
47
+ raise "return_for_targets is not implemented for plan spec tests (allow_plan, expect_plan, allow_any_plan, etc)"
48
+ end
49
+
50
+ def error_with(data, clazz = Bolt::PlanFailure)
51
+ super(data, clazz)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,6 +3,7 @@
3
3
  require 'bolt_spec/plans/action_stubs'
4
4
  require 'bolt_spec/plans/publish_stub'
5
5
  require 'bolt/error'
6
+ require 'bolt/executor'
6
7
  require 'bolt/result_set'
7
8
  require 'bolt/result'
8
9
  require 'pathname'
@@ -10,14 +11,14 @@ require 'set'
10
11
 
11
12
  module BoltSpec
12
13
  module Plans
13
- MOCKED_ACTIONS = %i[command script task upload].freeze
14
+ MOCKED_ACTIONS = %i[command plan script task upload].freeze
14
15
 
15
16
  class UnexpectedInvocation < ArgumentError; end
16
17
 
17
18
  # Nothing on the executor is 'public'
18
19
  class MockExecutor
19
20
  attr_reader :noop, :error_message
20
- attr_accessor :run_as, :transport_features
21
+ attr_accessor :run_as, :transport_features, :execute_any_plan
21
22
 
22
23
  def initialize(modulepath)
23
24
  @noop = false
@@ -28,6 +29,13 @@ module BoltSpec
28
29
  MOCKED_ACTIONS.each { |action| instance_variable_set(:"@#{action}_doubles", {}) }
29
30
  @stub_out_message = nil
30
31
  @transport_features = ['puppet-agent']
32
+ @executor_real = Bolt::Executor.new
33
+ # by default, we want to execute any plan that we come across without error
34
+ # or mocking. users can toggle this behavior so that plans will either need to
35
+ # be mocked out, or an error will be thrown.
36
+ @execute_any_plan = true
37
+ # plans that are allowed to be executed by the @executor_real
38
+ @allowed_exec_plans = {}
31
39
  end
32
40
 
33
41
  def module_file_id(file)
@@ -96,6 +104,57 @@ module BoltSpec
96
104
  result
97
105
  end
98
106
 
107
+ def with_plan_allowed_exec(plan_name, params)
108
+ @allowed_exec_plans[plan_name] = params
109
+ result = yield
110
+ @allowed_exec_plans.delete(plan_name)
111
+ result
112
+ end
113
+
114
+ def run_plan(scope, plan_clj, params)
115
+ result = nil
116
+ plan_name = plan_clj.closure_name
117
+
118
+ # get the mock object either by plan name, or the default in case allow_any_plan
119
+ # was called, if both are nil / don't exist, then dub will be nil and we'll fall
120
+ # through to another conditional statement
121
+ doub = @plan_doubles[plan_name] || @plan_doubles[:default]
122
+
123
+ # High level:
124
+ # - If we've explicitly allowed execution of the plan (normally the main plan
125
+ # passed into BoltSpec::Plan::run_plan()), then execute it
126
+ # - If we've explicitly "allowed/expected" the plan (mocked),
127
+ # then run it through the mock object
128
+ # - If we're allowing "any" plan to be executed,
129
+ # then execute it
130
+ # - Otherwise we have an error
131
+ if @allowed_exec_plans.key?(plan_name) && @allowed_exec_plans[plan_name] == params
132
+ # This plan's name + parameters were explicitly allowed to be executed.
133
+ # run it with the real executor.
134
+ # We require this functionality so that the BoltSpec::Plans.run_plan()
135
+ # function can kick off the initial plan. In reality, no other plans should
136
+ # be in this hash.
137
+ result = @executor_real.run_plan(scope, plan_clj, params)
138
+ elsif doub
139
+ result = doub.process(scope, plan_clj, params)
140
+ # the throw here is how Puppet exits out of a closure and returns a result
141
+ # it throws this special symbol with a result object that is captured by
142
+ # the run_plan Puppet function
143
+ throw :return, result
144
+ elsif @execute_any_plan
145
+ # if the plan wasn't allowed or mocked out, and we're allowing any plan to be
146
+ # executed, then execute the plan
147
+ result = @executor_real.run_plan(scope, plan_clj, params)
148
+ else
149
+ # convert to JSON and back so that we get the ruby representation with all keys and
150
+ # values converted to a string .to_s instead of their ruby object notation
151
+ params_str = JSON.parse(params.to_json)
152
+ @error_message = "Unexpected call to 'run_plan(#{plan_name}, #{params_str})'"
153
+ raise UnexpectedInvocation, @error_message
154
+ end
155
+ result
156
+ end
157
+
99
158
  def assert_call_expectations
100
159
  MOCKED_ACTIONS.each do |action|
101
160
  instance_variable_get(:"@#{action}_doubles").map do |object, doub|
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: 2.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-20 00:00:00.000000000 Z
11
+ date: 2020-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -324,6 +324,20 @@ dependencies:
324
324
  - - ">="
325
325
  - !ruby/object:Gem::Version
326
326
  version: '1.14'
327
+ - !ruby/object:Gem::Dependency
328
+ name: octokit
329
+ requirement: !ruby/object:Gem::Requirement
330
+ requirements:
331
+ - - "~>"
332
+ - !ruby/object:Gem::Version
333
+ version: '4.0'
334
+ type: :development
335
+ prerelease: false
336
+ version_requirements: !ruby/object:Gem::Requirement
337
+ requirements:
338
+ - - "~>"
339
+ - !ruby/object:Gem::Version
340
+ version: '4.0'
327
341
  - !ruby/object:Gem::Dependency
328
342
  name: puppetlabs_spec_helper
329
343
  requirement: !ruby/object:Gem::Requirement
@@ -414,6 +428,7 @@ files:
414
428
  - bolt-modules/file/lib/puppet/functions/file/readable.rb
415
429
  - bolt-modules/file/lib/puppet/functions/file/write.rb
416
430
  - bolt-modules/out/lib/puppet/functions/out/message.rb
431
+ - bolt-modules/prompt/lib/puppet/functions/prompt.rb
417
432
  - bolt-modules/system/lib/puppet/functions/system/env.rb
418
433
  - exe/bolt
419
434
  - lib/bolt.rb
@@ -528,6 +543,7 @@ files:
528
543
  - lib/bolt_spec/plans.rb
529
544
  - lib/bolt_spec/plans/action_stubs.rb
530
545
  - lib/bolt_spec/plans/action_stubs/command_stub.rb
546
+ - lib/bolt_spec/plans/action_stubs/plan_stub.rb
531
547
  - lib/bolt_spec/plans/action_stubs/script_stub.rb
532
548
  - lib/bolt_spec/plans/action_stubs/task_stub.rb
533
549
  - lib/bolt_spec/plans/action_stubs/upload_stub.rb