bolt 2.6.0 → 2.11.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +192 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
  11. data/bolt-modules/boltlib/types/planresult.pp +12 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  14. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  17. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +43 -0
  18. data/lib/bolt/analytics.rb +1 -1
  19. data/lib/bolt/applicator.rb +3 -2
  20. data/lib/bolt/apply_inventory.rb +1 -1
  21. data/lib/bolt/apply_result.rb +1 -1
  22. data/lib/bolt/apply_target.rb +11 -2
  23. data/lib/bolt/bolt_option_parser.rb +27 -7
  24. data/lib/bolt/catalog.rb +32 -3
  25. data/lib/bolt/cli.rb +52 -22
  26. data/lib/bolt/config.rb +51 -27
  27. data/lib/bolt/config/transport/base.rb +3 -3
  28. data/lib/bolt/config/transport/docker.rb +7 -1
  29. data/lib/bolt/config/transport/local.rb +9 -1
  30. data/lib/bolt/config/transport/orch.rb +4 -2
  31. data/lib/bolt/config/transport/remote.rb +2 -0
  32. data/lib/bolt/config/transport/ssh.rb +81 -3
  33. data/lib/bolt/config/transport/winrm.rb +6 -1
  34. data/lib/bolt/executor.rb +38 -0
  35. data/lib/bolt/inventory.rb +2 -1
  36. data/lib/bolt/inventory/group.rb +1 -0
  37. data/lib/bolt/inventory/inventory.rb +9 -0
  38. data/lib/bolt/inventory/target.rb +17 -1
  39. data/lib/bolt/node/output.rb +1 -1
  40. data/lib/bolt/outputter/human.rb +5 -4
  41. data/lib/bolt/outputter/json.rb +1 -1
  42. data/lib/bolt/pal.rb +32 -14
  43. data/lib/bolt/pal/yaml_plan.rb +1 -0
  44. data/lib/bolt/plugin.rb +14 -8
  45. data/lib/bolt/plugin/env_var.rb +2 -1
  46. data/lib/bolt/plugin/module.rb +40 -7
  47. data/lib/bolt/plugin/prompt.rb +1 -1
  48. data/lib/bolt/plugin/puppetdb.rb +5 -2
  49. data/lib/bolt/project.rb +135 -0
  50. data/lib/bolt/puppetdb/config.rb +16 -28
  51. data/lib/bolt/rerun.rb +1 -1
  52. data/lib/bolt/resource_instance.rb +126 -0
  53. data/lib/bolt/result.rb +46 -23
  54. data/lib/bolt/result_set.rb +2 -5
  55. data/lib/bolt/secret.rb +20 -4
  56. data/lib/bolt/shell/bash.rb +27 -14
  57. data/lib/bolt/shell/bash/tmpdir.rb +1 -1
  58. data/lib/bolt/shell/powershell.rb +43 -15
  59. data/lib/bolt/shell/powershell/snippets.rb +1 -1
  60. data/lib/bolt/target.rb +18 -2
  61. data/lib/bolt/transport/base.rb +24 -8
  62. data/lib/bolt/transport/docker.rb +3 -3
  63. data/lib/bolt/transport/docker/connection.rb +11 -7
  64. data/lib/bolt/transport/local/connection.rb +13 -7
  65. data/lib/bolt/transport/orch.rb +5 -1
  66. data/lib/bolt/transport/ssh.rb +6 -2
  67. data/lib/bolt/transport/ssh/connection.rb +26 -1
  68. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  69. data/lib/bolt/transport/winrm/connection.rb +10 -2
  70. data/lib/bolt/version.rb +1 -1
  71. data/lib/bolt_server/pe/pal.rb +1 -38
  72. data/lib/bolt_server/transport_app.rb +7 -7
  73. data/lib/bolt_spec/bolt_context.rb +3 -6
  74. data/lib/bolt_spec/plans.rb +78 -8
  75. data/lib/bolt_spec/plans/action_stubs.rb +37 -7
  76. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  77. data/lib/bolt_spec/plans/mock_executor.rb +62 -2
  78. data/lib/bolt_spec/run.rb +10 -13
  79. metadata +26 -7
  80. data/lib/bolt/boltdir.rb +0 -54
  81. data/lib/bolt/plugin/pkcs7.rb +0 -104
  82. data/lib/bolt/secret/base.rb +0 -41
@@ -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
@@ -210,6 +210,10 @@ module Bolt
210
210
  end
211
211
  end
212
212
 
213
+ def batch_task_with(_targets, _task, _target_mapping, _options = {})
214
+ raise NotImplementedError, "pcp transport does not support run_task_with()"
215
+ end
216
+
213
217
  def batch_connected?(targets)
214
218
  resp = get_connection(targets.first.options).query_inventory(targets)
215
219
  resp['items'].all? { |node| node['connected'] }
@@ -16,13 +16,16 @@ module Bolt
16
16
  rescue LoadError
17
17
  logger.debug("Authentication method 'gssapi-with-mic' (Kerberos) is not available.")
18
18
  end
19
-
20
19
  @transport_logger = Logging.logger[Net::SSH]
21
20
  @transport_logger.level = :warn
22
21
  end
23
22
 
24
23
  def with_connection(target)
25
- conn = Connection.new(target, @transport_logger)
24
+ conn = if target.transport_config['ssh-command']
25
+ ExecConnection.new(target)
26
+ else
27
+ Connection.new(target, @transport_logger)
28
+ end
26
29
  conn.connect
27
30
  yield conn
28
31
  ensure
@@ -37,3 +40,4 @@ module Bolt
37
40
  end
38
41
 
39
42
  require 'bolt/transport/ssh/connection'
43
+ require 'bolt/transport/ssh/exec_connection'
@@ -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(
@@ -206,6 +207,10 @@ module Bolt
206
207
  err_wr.close
207
208
  end
208
209
  [in_wr, out_rd, err_rd, th]
210
+ rescue Errno::EMFILE => e
211
+ msg = "#{e.message}. This may be resolved by increasing your user limit "\
212
+ "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
213
+ raise Bolt::Error.new(msg, 'bolt/too-many-files')
209
214
  end
210
215
 
211
216
  def copy_file(source, destination)
@@ -242,7 +247,11 @@ module Bolt
242
247
  end
243
248
 
244
249
  def shell
245
- @shell ||= Bolt::Shell::Bash.new(target, self)
250
+ @shell ||= if target.options['login-shell'] == 'powershell'
251
+ Bolt::Shell::Powershell.new(target, self)
252
+ else
253
+ Bolt::Shell::Bash.new(target, self)
254
+ end
246
255
  end
247
256
 
248
257
  # This is used by the Bash shell to decide whether to `cd` before
@@ -250,6 +259,22 @@ module Bolt
250
259
  def reset_cwd?
251
260
  true
252
261
  end
262
+
263
+ def max_command_length
264
+ if target.options['login-shell'] == 'powershell'
265
+ 32000
266
+ end
267
+ end
268
+
269
+ def validate_ssh_version
270
+ remote_version = @session.transport.server_version.version
271
+ return unless target.options['login-shell'] && remote_version
272
+
273
+ match = remote_version.match(/OpenSSH_for_Windows_(\d+\.\d+)/)
274
+ if match && match[1].to_f < 7.9
275
+ raise "Powershell over SSH requires OpenSSH server >= 7.9, target is running #{match[1]}"
276
+ end
277
+ end
253
278
  end
254
279
  end
255
280
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Bolt
6
+ module Transport
7
+ class SSH < Simple
8
+ class ExecConnection
9
+ attr_reader :user, :target
10
+
11
+ def initialize(target)
12
+ raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
13
+
14
+ @target = target
15
+ ssh_config = Net::SSH::Config.for(target.host)
16
+ @user = @target.user || ssh_config[:user] || Etc.getlogin
17
+ @logger = Logging.logger[self]
18
+ end
19
+
20
+ # This is used to verify we can connect to targets with `connected?`
21
+ def connect
22
+ cmd = build_ssh_command('exit')
23
+ _, err, stat = Open3.capture3(*cmd)
24
+ unless stat.success?
25
+ raise Bolt::Node::ConnectError.new(
26
+ "Failed to connect to #{@target.safe_name}: #{err}",
27
+ 'CONNECT_ERROR'
28
+ )
29
+ end
30
+ end
31
+
32
+ def disconnect; end
33
+
34
+ def shell
35
+ Bolt::Shell::Bash.new(@target, self)
36
+ end
37
+
38
+ def userhost
39
+ "#{@user}@#{@target.host}"
40
+ end
41
+
42
+ def ssh_opts
43
+ cmd = []
44
+ # BatchMode is SSH's noninteractive option: if key authentication
45
+ # fails it will error out instead of falling back to password prompt
46
+ cmd += %w[-o BatchMode=yes]
47
+ cmd += %W[-o Port=#{@target.port}] if @target.port
48
+
49
+ if @target.transport_config.key?('host-key-check')
50
+ hkc = @target.transport_config['host-key-check'] ? 'yes' : 'no'
51
+ cmd += %W[-o StrictHostKeyChecking=#{hkc}]
52
+ end
53
+
54
+ if (key = target.transport_config['private-key'])
55
+ cmd += ['-i', key]
56
+ end
57
+ cmd
58
+ end
59
+
60
+ def build_ssh_command(command)
61
+ ssh_conf = @target.transport_config['ssh-command']
62
+ ssh_cmd = Array(ssh_conf)
63
+ ssh_cmd += ssh_opts
64
+ ssh_cmd << userhost
65
+ ssh_cmd << command
66
+ end
67
+
68
+ def copy_file(source, dest)
69
+ @logger.debug { "Uploading #{source}, to #{userhost}:#{dest}" } unless source.is_a?(StringIO)
70
+
71
+ cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
72
+ cp_cmd = Array(cp_conf)
73
+ cp_cmd += ssh_opts
74
+
75
+ _, err, stat = if source.is_a?(StringIO)
76
+ Tempfile.create(File.basename(dest)) do |f|
77
+ f.write(source.read)
78
+ f.close
79
+ cp_cmd << f.path
80
+ cp_cmd << "#{userhost}:#{Shellwords.escape(dest)}"
81
+ Open3.capture3(*cp_cmd)
82
+ end
83
+ else
84
+ cp_cmd << source
85
+ cp_cmd << "#{userhost}:#{Shellwords.escape(dest)}"
86
+ Open3.capture3(*cp_cmd)
87
+ end
88
+
89
+ if stat.success?
90
+ @logger.debug "Successfully uploaded #{source} to #{dest}"
91
+ else
92
+ message = "Could not copy file to #{dest}: #{err}"
93
+ raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
94
+ end
95
+ end
96
+
97
+ def execute(command)
98
+ cmd_array = build_ssh_command(command)
99
+ Open3.popen3(*cmd_array)
100
+ end
101
+
102
+ # This is used by the Bash shell to decide whether to `cd` before
103
+ # executing commands as a run-as user
104
+ def reset_cwd?
105
+ true
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -108,8 +108,8 @@ module Bolt
108
108
  # it will fail if the shell attempts to provide stdin
109
109
  inp.close
110
110
 
111
- out_rd, out_wr = IO.pipe
112
- err_rd, err_wr = IO.pipe
111
+ out_rd, out_wr = IO.pipe('UTF-8')
112
+ err_rd, err_wr = IO.pipe('UTF-8')
113
113
  th = Thread.new do
114
114
  result = @session.run(command)
115
115
  out_wr << result.stdout
@@ -120,6 +120,10 @@ module Bolt
120
120
  end
121
121
 
122
122
  [inp, out_rd, err_rd, th]
123
+ rescue Errno::EMFILE => e
124
+ msg = "#{e.message}. This may be resolved by increasing your user limit "\
125
+ "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
126
+ raise Bolt::Error.new(msg, 'bolt/too-many-files')
123
127
  rescue StandardError
124
128
  @logger.debug { "Command aborted" }
125
129
  raise
@@ -175,6 +179,10 @@ module Bolt
175
179
  @shell ||= Bolt::Shell::Powershell.new(target, self)
176
180
  end
177
181
 
182
+ def max_command_length
183
+ nil
184
+ end
185
+
178
186
  private
179
187
 
180
188
  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.11.0'
5
5
  end
@@ -51,50 +51,13 @@ module BoltServer
51
51
  basemodulepath = plan_executor_config['basemodulepath'] || "#{codedir}/modules:/opt/puppetlabs/puppet/modules"
52
52
 
53
53
  with_pe_pal_init_settings(codedir, environmentpath, basemodulepath) do
54
- modulepath_dirs = []
55
- modulepath_setting_from_bolt = nil
56
54
  environment = Puppet.lookup(:environments).get!(environment_name)
57
- path_to_env = environment.configuration.path_to_env
58
-
59
- # In the instance where the environment is "production" but no production dir
60
- # exists, the lookup will succeed, but the configuration will be mostly empty.
61
- # For other environments the lookup will fail, but for production we don't
62
- # want cryptic messages sent to the user about combining `nil` with a string.
63
- # Thus if we do get here and `path_to_env` is empty, just assume it's the
64
- # default production environment and continue.
65
- #
66
- # This should hopefully match puppet's behavior for the default 'production'
67
- # environment: _technically_ that environment always exists, but if the dir
68
- # isn't there it won't find the module and fail with "plan not found" rather
69
- # than "environment doesn't exist"
70
- if path_to_env
71
- bolt_yaml = File.join(environment.configuration.path_to_env, 'bolt.yaml')
72
- modulepath_setting_from_bolt = Bolt::Util.read_optional_yaml_hash(bolt_yaml, 'config')['modulepath']
73
- end
74
-
75
- # If we loaded a bolt.yaml in the environment root and it contained a modulepath setting:
76
- # we will use that modulepath rather than the one loaded through puppet. modulepath will
77
- # be the _only_ setting that will work from bolt.yaml in plans in PE.
78
- if modulepath_setting_from_bolt
79
- modulepath_setting_from_bolt.split(File::PATH_SEPARATOR).each do |path|
80
- if Pathname.new(path).absolute? && File.exist?(path)
81
- modulepath_dirs << path
82
- elsif File.exist?(File.join(path_to_env, path))
83
- modulepath_dirs << File.join(path_to_env, path)
84
- end
85
- end
86
-
87
- # Append the basemodulepath to include "built-in" modules.
88
- modulepath_dirs.concat(basemodulepath.split(File::PATH_SEPARATOR))
89
- else
90
- modulepath_dirs = environment.modulepath
91
- end
92
-
93
55
  # A new modulepath is created from scratch (rather than using super's @modulepath)
94
56
  # so that we can have full control over all the entries in modulepath. In the future
95
57
  # it's likely we will need to preceed _both_ Bolt::PAL::BOLTLIB_PATH _and_
96
58
  # Bolt::PAL::MODULES_PATH which would be more complex if we tried to use @modulepath since
97
59
  # we need to append our modulepaths and exclude modules shiped in bolt gem code
60
+ modulepath_dirs = environment.modulepath
98
61
  @original_modulepath = modulepath_dirs
99
62
  @modulepath = [PE_BOLTLIB_PATH, Bolt::PAL::BOLTLIB_PATH, *modulepath_dirs]
100
63
  end
@@ -57,8 +57,8 @@ module BoltServer
57
57
  end
58
58
 
59
59
  def scrub_stack_trace(result)
60
- if result.dig(:value, '_error', 'details', 'stack_trace')
61
- result[:value]['_error']['details'].reject! { |k| k == 'stack_trace' }
60
+ if result.dig('value', '_error', 'details', 'stack_trace')
61
+ result['value']['_error']['details'].reject! { |k| k == 'stack_trace' }
62
62
  end
63
63
  result
64
64
  end
@@ -87,14 +87,14 @@ module BoltServer
87
87
  # If the `result_set` contains only one item, it will be returned
88
88
  # as a single result object. Set `aggregate` to treat it as a set
89
89
  # of results with length 1 instead.
90
- def result_set_to_status_hash(result_set, aggregate: false)
90
+ def result_set_to_data(result_set, aggregate: false)
91
91
  scrubbed_results = result_set.map do |result|
92
- scrub_stack_trace(result.status_hash)
92
+ scrub_stack_trace(result.to_data)
93
93
  end
94
94
 
95
95
  if aggregate || scrubbed_results.length > 1
96
96
  # For actions that act on multiple targets, construct a status hash for the aggregate result
97
- all_succeeded = scrubbed_results.all? { |r| r[:status] == 'success' }
97
+ all_succeeded = scrubbed_results.all? { |r| r['status'] == 'success' }
98
98
  {
99
99
  status: all_succeeded ? 'success' : 'failure',
100
100
  result: scrubbed_results
@@ -297,7 +297,7 @@ module BoltServer
297
297
  return [400, error.to_json] unless error.nil?
298
298
 
299
299
  aggregate = body['target'].nil?
300
- [200, result_set_to_status_hash(result_set, aggregate: aggregate).to_json]
300
+ [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
301
301
  end
302
302
 
303
303
  def make_winrm_target(target_hash)
@@ -337,7 +337,7 @@ module BoltServer
337
337
  return [400, error.to_json] if error
338
338
 
339
339
  aggregate = body['target'].nil?
340
- [200, result_set_to_status_hash(result_set, aggregate: aggregate).to_json]
340
+ [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
341
341
  end
342
342
 
343
343
  # Fetches the metadata for a single plan
@@ -138,21 +138,18 @@ module BoltSpec
138
138
  # Override in your tests
139
139
  def config
140
140
  @config ||= begin
141
- conf = Bolt::Config.new(Bolt::Boltdir.new('.'), {})
141
+ conf = Bolt::Config.new(Bolt::Project.new('.'), {})
142
142
  conf.modulepath = [modulepath].flatten
143
143
  conf
144
144
  end
145
145
  end
146
146
 
147
147
  def plugins
148
- @plugins ||= Bolt::Plugin.setup(config,
149
- pal,
150
- nil,
151
- Bolt::Analytics::NoopClient.new)
148
+ @plugins ||= Bolt::Plugin.setup(config, pal)
152
149
  end
153
150
 
154
151
  def pal
155
- @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.boltdir.resource_types)
152
+ @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.project.resource_types)
156
153
  end
157
154
 
158
155
  BoltSpec::Plans::MOCKED_ACTIONS.each do |action|
@@ -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.
@@ -165,8 +219,10 @@ module BoltSpec
165
219
  end
166
220
 
167
221
  def run_plan(name, params)
168
- pal = Bolt::PAL.new(config.modulepath, config.hiera_config, config.boltdir.resource_types)
169
- result = pal.run_plan(name, params, executor, inventory, puppetdb_client)
222
+ pal = Bolt::PAL.new(config.modulepath, config.hiera_config, config.project.resource_types)
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