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 +4 -4
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +1 -1
- data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +43 -0
- data/lib/bolt/analytics.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +5 -1
- data/lib/bolt/catalog.rb +32 -3
- data/lib/bolt/config/transport/docker.rb +5 -1
- data/lib/bolt/config/transport/local.rb +7 -1
- data/lib/bolt/config/transport/ssh.rb +30 -1
- data/lib/bolt/config/transport/winrm.rb +3 -0
- data/lib/bolt/executor.rb +22 -0
- data/lib/bolt/inventory/inventory.rb +4 -0
- data/lib/bolt/plugin/env_var.rb +2 -1
- data/lib/bolt/plugin/prompt.rb +1 -1
- data/lib/bolt/shell/bash.rb +15 -9
- data/lib/bolt/shell/bash/tmpdir.rb +1 -1
- data/lib/bolt/shell/powershell.rb +31 -11
- data/lib/bolt/shell/powershell/snippets.rb +1 -1
- data/lib/bolt/target.rb +2 -1
- 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 +1 -1
- data/lib/bolt/transport/ssh/connection.rb +22 -1
- data/lib/bolt/transport/winrm/connection.rb +4 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/plans.rb +77 -7
- 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 +61 -2
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9354d4101556422d8d97c758b64a4e717a9b24488d11cfe48042ebe79e1a28fa
|
4
|
+
data.tar.gz: 3ca45b8616f2ff1aa53c67cccd150d06c7a2d8897a11b3ffdfda352d6d10ec0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/bolt/analytics.rb
CHANGED
@@ -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#
|
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|
|
data/lib/bolt/catalog.rb
CHANGED
@@ -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
|
-
#
|
74
|
-
|
75
|
-
|
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 = {
|
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 = {
|
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,
|
data/lib/bolt/executor.rb
CHANGED
@@ -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
|
data/lib/bolt/plugin/env_var.rb
CHANGED
@@ -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
|
data/lib/bolt/plugin/prompt.rb
CHANGED
data/lib/bolt/shell/bash.rb
CHANGED
@@ -34,7 +34,7 @@ module Bolt
|
|
34
34
|
|
35
35
|
def upload(source, destination, options = {})
|
36
36
|
running_as(options[:run_as]) do
|
37
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
259
|
+
# A helper to create and delete a tmpdir on the remote system. Yields the
|
260
260
|
# directory name.
|
261
|
-
def
|
262
|
-
dir =
|
261
|
+
def with_tmpdir
|
262
|
+
dir = make_tmpdir
|
263
263
|
yield dir
|
264
264
|
ensure
|
265
|
-
dir
|
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
|
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
|
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
|
122
|
+
def make_tmpdir
|
123
123
|
find_parent = target.options['tmpdir'] ? "\"#{target.options['tmpdir']}\"" : '[System.IO.Path]::GetTempPath()'
|
124
|
-
result = execute(Snippets.
|
124
|
+
result = execute(Snippets.make_tmpdir(find_parent))
|
125
125
|
if result.exit_code != 0
|
126
|
-
raise Bolt::Node::FileError.new("Could not make
|
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
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/bolt/target.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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
|
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
|
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
|
118
|
-
dir =
|
117
|
+
def with_remote_tmpdir
|
118
|
+
dir = make_tmpdir
|
119
119
|
yield dir
|
120
120
|
ensure
|
121
121
|
if dir
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
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
@@ -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 ||=
|
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
|
data/lib/bolt/version.rb
CHANGED
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.
|
@@ -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 =
|
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
|
@@ -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
|
-
|
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
|
-
|
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] =
|
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.
|
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-
|
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
|