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.
- checksums.yaml +4 -4
- data/Puppetfile +4 -3
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +27 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +192 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
- data/bolt-modules/boltlib/types/planresult.pp +12 -1
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
- data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
- data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
- data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +43 -0
- data/lib/bolt/analytics.rb +1 -1
- data/lib/bolt/applicator.rb +3 -2
- data/lib/bolt/apply_inventory.rb +1 -1
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/apply_target.rb +11 -2
- data/lib/bolt/bolt_option_parser.rb +27 -7
- data/lib/bolt/catalog.rb +32 -3
- data/lib/bolt/cli.rb +52 -22
- data/lib/bolt/config.rb +51 -27
- data/lib/bolt/config/transport/base.rb +3 -3
- data/lib/bolt/config/transport/docker.rb +7 -1
- data/lib/bolt/config/transport/local.rb +9 -1
- data/lib/bolt/config/transport/orch.rb +4 -2
- data/lib/bolt/config/transport/remote.rb +2 -0
- data/lib/bolt/config/transport/ssh.rb +81 -3
- data/lib/bolt/config/transport/winrm.rb +6 -1
- data/lib/bolt/executor.rb +38 -0
- data/lib/bolt/inventory.rb +2 -1
- data/lib/bolt/inventory/group.rb +1 -0
- data/lib/bolt/inventory/inventory.rb +9 -0
- data/lib/bolt/inventory/target.rb +17 -1
- data/lib/bolt/node/output.rb +1 -1
- data/lib/bolt/outputter/human.rb +5 -4
- data/lib/bolt/outputter/json.rb +1 -1
- data/lib/bolt/pal.rb +32 -14
- data/lib/bolt/pal/yaml_plan.rb +1 -0
- data/lib/bolt/plugin.rb +14 -8
- data/lib/bolt/plugin/env_var.rb +2 -1
- data/lib/bolt/plugin/module.rb +40 -7
- data/lib/bolt/plugin/prompt.rb +1 -1
- data/lib/bolt/plugin/puppetdb.rb +5 -2
- data/lib/bolt/project.rb +135 -0
- data/lib/bolt/puppetdb/config.rb +16 -28
- data/lib/bolt/rerun.rb +1 -1
- data/lib/bolt/resource_instance.rb +126 -0
- data/lib/bolt/result.rb +46 -23
- data/lib/bolt/result_set.rb +2 -5
- data/lib/bolt/secret.rb +20 -4
- data/lib/bolt/shell/bash.rb +27 -14
- data/lib/bolt/shell/bash/tmpdir.rb +1 -1
- data/lib/bolt/shell/powershell.rb +43 -15
- data/lib/bolt/shell/powershell/snippets.rb +1 -1
- data/lib/bolt/target.rb +18 -2
- data/lib/bolt/transport/base.rb +24 -8
- data/lib/bolt/transport/docker.rb +3 -3
- data/lib/bolt/transport/docker/connection.rb +11 -7
- data/lib/bolt/transport/local/connection.rb +13 -7
- data/lib/bolt/transport/orch.rb +5 -1
- data/lib/bolt/transport/ssh.rb +6 -2
- data/lib/bolt/transport/ssh/connection.rb +26 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
- data/lib/bolt/transport/winrm/connection.rb +10 -2
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/pe/pal.rb +1 -38
- data/lib/bolt_server/transport_app.rb +7 -7
- data/lib/bolt_spec/bolt_context.rb +3 -6
- data/lib/bolt_spec/plans.rb +78 -8
- data/lib/bolt_spec/plans/action_stubs.rb +37 -7
- data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
- data/lib/bolt_spec/plans/mock_executor.rb +62 -2
- data/lib/bolt_spec/run.rb +10 -13
- metadata +26 -7
- data/lib/bolt/boltdir.rb +0 -54
- data/lib/bolt/plugin/pkcs7.rb +0 -104
- 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
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -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'] }
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -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 =
|
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 ||=
|
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
|
data/lib/bolt/version.rb
CHANGED
data/lib/bolt_server/pe/pal.rb
CHANGED
@@ -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(
|
61
|
-
result[
|
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
|
90
|
+
def result_set_to_data(result_set, aggregate: false)
|
91
91
|
scrubbed_results = result_set.map do |result|
|
92
|
-
scrub_stack_trace(result.
|
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[
|
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,
|
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,
|
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::
|
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.
|
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|
|
data/lib/bolt_spec/plans.rb
CHANGED
@@ -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.
|
169
|
-
result =
|
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
|
-
#
|
200
|
-
#
|
201
|
-
#
|
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
|