bolt 2.44.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +11 -9
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
- data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
- data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +9 -123
- data/lib/bolt/cli.rb +125 -127
- data/lib/bolt/config.rb +39 -214
- data/lib/bolt/config/options.rb +34 -125
- data/lib/bolt/config/transport/local.rb +1 -0
- data/lib/bolt/config/transport/lxd.rb +23 -0
- data/lib/bolt/config/transport/options.rb +9 -2
- data/lib/bolt/executor.rb +20 -5
- data/lib/bolt/logger.rb +9 -1
- data/lib/bolt/module_installer.rb +2 -2
- data/lib/bolt/module_installer/puppetfile.rb +2 -2
- data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
- data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
- data/lib/bolt/node/output.rb +14 -4
- data/lib/bolt/outputter/human.rb +52 -24
- data/lib/bolt/outputter/json.rb +16 -16
- data/lib/bolt/pal.rb +26 -5
- data/lib/bolt/pal/yaml_plan.rb +1 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -153
- data/lib/bolt/pal/yaml_plan/step.rb +91 -52
- data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
- data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
- data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
- data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
- data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
- data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
- data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
- data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
- data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
- data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
- data/lib/bolt/plan_creator.rb +1 -1
- data/lib/bolt/plugin/module.rb +0 -23
- data/lib/bolt/plugin/puppet_connect_data.rb +45 -3
- data/lib/bolt/project.rb +16 -56
- data/lib/bolt/project_manager.rb +5 -4
- data/lib/bolt/project_manager/module_migrator.rb +7 -6
- data/lib/bolt/result.rb +10 -11
- data/lib/bolt/shell.rb +16 -0
- data/lib/bolt/shell/bash.rb +61 -31
- data/lib/bolt/shell/bash/tmpdir.rb +2 -2
- data/lib/bolt/shell/powershell.rb +35 -14
- data/lib/bolt/shell/powershell/snippets.rb +37 -150
- data/lib/bolt/task.rb +1 -1
- data/lib/bolt/transport/base.rb +0 -9
- data/lib/bolt/transport/docker.rb +1 -125
- data/lib/bolt/transport/docker/connection.rb +86 -161
- data/lib/bolt/transport/local.rb +1 -9
- data/lib/bolt/transport/lxd.rb +26 -0
- data/lib/bolt/transport/lxd/connection.rb +99 -0
- data/lib/bolt/transport/orch.rb +13 -5
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/transport/winrm/connection.rb +1 -1
- data/lib/bolt/util.rb +8 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +61 -33
- data/lib/bolt_spec/bolt_context.rb +9 -4
- data/lib/bolt_spec/plans.rb +1 -109
- data/lib/bolt_spec/plans/action_stubs.rb +1 -1
- data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
- data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
- data/lib/bolt_spec/plans/mock_executor.rb +4 -0
- data/modules/aggregate/plans/count.pp +21 -0
- data/modules/aggregate/plans/targets.pp +21 -0
- data/modules/puppet_connect/plans/test_input_data.pp +67 -0
- data/modules/puppetdb_fact/plans/init.pp +10 -0
- metadata +7 -3
- data/modules/aggregate/plans/nodes.pp +0 -36
data/lib/bolt/shell.rb
CHANGED
@@ -8,6 +8,22 @@ module Bolt
|
|
8
8
|
@target = target
|
9
9
|
@conn = conn
|
10
10
|
@logger = Bolt::Logger.logger(@target.safe_name)
|
11
|
+
|
12
|
+
if Bolt::Logger.stream
|
13
|
+
Bolt::Logger.warn_once("stream_experimental",
|
14
|
+
"The 'stream' option is experimental, and might "\
|
15
|
+
"include breaking changes between minor versions.")
|
16
|
+
@stream_logger = Bolt::Logger.logger(:stream)
|
17
|
+
# Don't send stream messages to the parent logger
|
18
|
+
@stream_logger.additive = false
|
19
|
+
|
20
|
+
# Log stream messages without any other data or color
|
21
|
+
pattern = Logging.layouts.pattern(pattern: '%m\n')
|
22
|
+
@stream_logger.appenders = Logging.appenders.stdout(
|
23
|
+
'console',
|
24
|
+
layout: pattern
|
25
|
+
)
|
26
|
+
end
|
11
27
|
end
|
12
28
|
|
13
29
|
def run_command(*_args)
|
data/lib/bolt/shell/bash.rb
CHANGED
@@ -12,7 +12,6 @@ module Bolt
|
|
12
12
|
super
|
13
13
|
|
14
14
|
@run_as = nil
|
15
|
-
|
16
15
|
@sudo_id = SecureRandom.uuid
|
17
16
|
@sudo_password = @target.options['sudo-password'] || @target.password
|
18
17
|
end
|
@@ -25,9 +24,7 @@ module Bolt
|
|
25
24
|
running_as(options[:run_as]) do
|
26
25
|
output = execute(command, environment: options[:env_vars], sudoable: true)
|
27
26
|
Bolt::Result.for_command(target,
|
28
|
-
output.
|
29
|
-
output.stderr.string,
|
30
|
-
output.exit_code,
|
27
|
+
output.to_h,
|
31
28
|
'command',
|
32
29
|
command,
|
33
30
|
position)
|
@@ -54,19 +51,35 @@ module Bolt
|
|
54
51
|
|
55
52
|
def download(source, destination, options = {})
|
56
53
|
running_as(options[:run_as]) do
|
57
|
-
# Target OS may be either Unix or Windows. Without knowing the target OS before-hand
|
58
|
-
# we can't assume whether the path separator is '/' or '\'. Assume we're connecting
|
59
|
-
# to a target with Unix and then check if the path exists after downloading.
|
60
54
|
download = File.join(destination, Bolt::Util.unix_basename(source))
|
61
55
|
|
62
|
-
|
56
|
+
# If using run-as, the file is copied to a tmpdir and chowned to the
|
57
|
+
# connecting user. This is a workaround for limitations in net-ssh that
|
58
|
+
# only allow for downloading files as the connecting user, which is a
|
59
|
+
# problem for users who cannot connect to targets as the root user.
|
60
|
+
# This temporary copy should *always* be deleted.
|
61
|
+
if run_as
|
62
|
+
with_tmpdir(force_cleanup: true) do |dir|
|
63
|
+
tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source))
|
64
|
+
|
65
|
+
result = execute(['cp', '-r', source, dir.to_s], sudoable: true)
|
66
|
+
|
67
|
+
if result.exit_code != 0
|
68
|
+
message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}"
|
69
|
+
raise Bolt::Node::FileError.new(message, 'CP_ERROR')
|
70
|
+
end
|
63
71
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
72
|
+
# We need to force the chown, otherwise this will just return
|
73
|
+
# without doing anything since the chown user is the same as the
|
74
|
+
# connecting user.
|
75
|
+
dir.chown(conn.user, force: true)
|
76
|
+
|
77
|
+
conn.download_file(tmpfile, destination, download)
|
78
|
+
end
|
79
|
+
# If not using run-as, we can skip creating a temporary copy and just
|
80
|
+
# download the file directly.
|
81
|
+
else
|
82
|
+
conn.download_file(source, destination, download)
|
70
83
|
end
|
71
84
|
|
72
85
|
Bolt::Result.for_download(target, source, destination, download)
|
@@ -83,9 +96,7 @@ module Bolt
|
|
83
96
|
dir.chown(run_as)
|
84
97
|
output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
|
85
98
|
Bolt::Result.for_command(target,
|
86
|
-
output.
|
87
|
-
output.stderr.string,
|
88
|
-
output.exit_code,
|
99
|
+
output.to_h,
|
89
100
|
'script',
|
90
101
|
script,
|
91
102
|
position)
|
@@ -134,18 +145,21 @@ module Bolt
|
|
134
145
|
|
135
146
|
remote_task_path = write_executable(task_dir, executable)
|
136
147
|
|
148
|
+
execute_options[:stdin] = stdin
|
149
|
+
|
137
150
|
# Avoid the horrors of passing data on stdin via a tty on multiple platforms
|
138
151
|
# by writing a wrapper script that directs stdin to the task.
|
139
152
|
if stdin && target.options['tty']
|
140
153
|
wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
|
154
|
+
# Wrapper script handles interpreter and stdin. Delete these execute options
|
141
155
|
execute_options.delete(:interpreter)
|
156
|
+
execute_options.delete(:stdin)
|
142
157
|
execute_options[:wrapper] = true
|
143
158
|
remote_task_path = write_executable(dir, wrapper, 'wrapper.sh')
|
144
159
|
end
|
145
160
|
|
146
161
|
dir.chown(run_as)
|
147
162
|
|
148
|
-
execute_options[:stdin] = stdin
|
149
163
|
execute_options[:sudoable] = true if run_as
|
150
164
|
output = execute(remote_task_path, **execute_options)
|
151
165
|
end
|
@@ -291,12 +305,12 @@ module Bolt
|
|
291
305
|
|
292
306
|
# A helper to create and delete a tmpdir on the remote system. Yields the
|
293
307
|
# directory name.
|
294
|
-
def with_tmpdir
|
308
|
+
def with_tmpdir(force_cleanup: false)
|
295
309
|
dir = make_tmpdir
|
296
310
|
yield dir
|
297
311
|
ensure
|
298
312
|
if dir
|
299
|
-
if target.options['cleanup']
|
313
|
+
if target.options['cleanup'] || force_cleanup
|
300
314
|
dir.delete
|
301
315
|
else
|
302
316
|
Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
|
@@ -331,10 +345,15 @@ module Bolt
|
|
331
345
|
# together multiple commands into a single sh invocation
|
332
346
|
commands = [inject_interpreter(options[:interpreter], command)]
|
333
347
|
|
348
|
+
# Let the transport handle adding environment variables if it's custom.
|
334
349
|
if options[:environment]
|
335
|
-
|
336
|
-
|
337
|
-
|
350
|
+
if defined? conn.add_env_vars
|
351
|
+
conn.add_env_vars(options[:environment])
|
352
|
+
else
|
353
|
+
env_decl = options[:environment].map do |env, val|
|
354
|
+
"#{env}=#{Shellwords.shellescape(val)}"
|
355
|
+
end.join(' ')
|
356
|
+
end
|
338
357
|
end
|
339
358
|
|
340
359
|
if escalate
|
@@ -385,14 +404,24 @@ module Bolt
|
|
385
404
|
# See if we can read from out or err, or write to in
|
386
405
|
ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
|
387
406
|
|
388
|
-
# Read from out and err
|
389
407
|
ready_read&.each do |stream|
|
408
|
+
stream_name = stream == out ? 'out' : 'err'
|
390
409
|
# Check for sudo prompt
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
410
|
+
to_print = if use_sudo
|
411
|
+
check_sudo(stream, inp, options[:stdin])
|
412
|
+
else
|
413
|
+
stream.readpartial(CHUNK_SIZE)
|
414
|
+
end
|
415
|
+
|
416
|
+
if !to_print.chomp.empty? && @stream_logger
|
417
|
+
formatted = to_print.lines.map do |msg|
|
418
|
+
"[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
|
419
|
+
end.join("\n")
|
420
|
+
@stream_logger.warn(formatted)
|
421
|
+
end
|
422
|
+
|
423
|
+
read_streams[stream] << to_print
|
424
|
+
result_output.merged_output << to_print
|
396
425
|
rescue EOFError
|
397
426
|
end
|
398
427
|
|
@@ -440,9 +469,10 @@ module Bolt
|
|
440
469
|
when 0
|
441
470
|
@logger.trace { "Command `#{command_str}` returned successfully" }
|
442
471
|
when 126
|
443
|
-
msg = "\n\nThis
|
472
|
+
msg = "\n\nThis might be caused by the default tmpdir being mounted "\
|
444
473
|
"using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
|
445
|
-
result_output.stderr
|
474
|
+
result_output.stderr << msg
|
475
|
+
result_output.merged_output << msg
|
446
476
|
@logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
|
447
477
|
else
|
448
478
|
@logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
|
@@ -24,8 +24,8 @@ module Bolt
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def chown(owner)
|
28
|
-
return if owner.nil? || owner == @owner
|
27
|
+
def chown(owner, force: false)
|
28
|
+
return if owner.nil? || (owner == @owner && !force)
|
29
29
|
|
30
30
|
result = @shell.execute(['id', '-g', owner])
|
31
31
|
if result.exit_code != 0
|
@@ -23,8 +23,7 @@ module Bolt
|
|
23
23
|
# This lets us know how many targets have Powershell 2, and lets the
|
24
24
|
# user know how many targets they have with PS2
|
25
25
|
msg = "Detected PowerShell 2 on one or more targets.\nPowerShell 2 "\
|
26
|
-
"is
|
27
|
-
"bolt-debug.log or run with '--log-level debug' to see the full "\
|
26
|
+
"is unsupported. See bolt-debug.log or run with '--log-level debug' to see the full "\
|
28
27
|
"list of targets with PowerShell 2."
|
29
28
|
|
30
29
|
Bolt::Logger.deprecate_once("powershell_2", msg)
|
@@ -196,9 +195,7 @@ module Bolt
|
|
196
195
|
wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
|
197
196
|
output = execute(command, wrap_command)
|
198
197
|
Bolt::Result.for_command(target,
|
199
|
-
output.
|
200
|
-
output.stderr.string,
|
201
|
-
output.exit_code,
|
198
|
+
output.to_h,
|
202
199
|
'command',
|
203
200
|
command,
|
204
201
|
position)
|
@@ -209,20 +206,23 @@ module Bolt
|
|
209
206
|
arguments = unwrap_sensitive_args(arguments)
|
210
207
|
with_tmpdir do |dir|
|
211
208
|
script_path = write_executable(dir, script)
|
212
|
-
command = if powershell_file?(script_path)
|
209
|
+
command = if powershell_file?(script_path) && options[:pwsh_params]
|
210
|
+
# Scripts run with pwsh_params can be run like tasks
|
211
|
+
Snippets.ps_task(script_path, options[:pwsh_params])
|
212
|
+
elsif powershell_file?(script_path)
|
213
213
|
Snippets.run_script(arguments, script_path)
|
214
214
|
else
|
215
215
|
path, args = *process_from_extension(script_path)
|
216
216
|
args += escape_arguments(arguments)
|
217
217
|
execute_process(path, args)
|
218
218
|
end
|
219
|
-
|
219
|
+
env_assignments = options[:env_vars] ? env_declarations(options[:env_vars]) : []
|
220
|
+
shell_init = options[:pwsh_params] ? Snippets.shell_init : ''
|
221
|
+
|
222
|
+
output = execute([shell_init, *env_assignments, command].join("\r\n"))
|
220
223
|
|
221
|
-
output = execute(command)
|
222
224
|
Bolt::Result.for_command(target,
|
223
|
-
output.
|
224
|
-
output.stderr.string,
|
225
|
-
output.exit_code,
|
225
|
+
output.to_h,
|
226
226
|
'script',
|
227
227
|
script,
|
228
228
|
position)
|
@@ -275,7 +275,12 @@ module Bolt
|
|
275
275
|
[]
|
276
276
|
end
|
277
277
|
|
278
|
-
output = execute([
|
278
|
+
output = execute([
|
279
|
+
Snippets.shell_init,
|
280
|
+
Snippets.append_ps_module_path(dir),
|
281
|
+
*env_assignments,
|
282
|
+
command
|
283
|
+
].join("\n"))
|
279
284
|
|
280
285
|
Bolt::Result.for_task(target, output.stdout.string,
|
281
286
|
output.stderr.string,
|
@@ -306,12 +311,28 @@ module Bolt
|
|
306
311
|
# the proper encoding so the string isn't later misinterpreted
|
307
312
|
encoding = out.external_encoding
|
308
313
|
out.binmode
|
309
|
-
|
314
|
+
to_print = out.read.force_encoding(encoding)
|
315
|
+
if !to_print.chomp.empty? && @stream_logger
|
316
|
+
formatted = to_print.lines.map do |msg|
|
317
|
+
"[#{@target.safe_name}] out: #{msg.chomp}"
|
318
|
+
end.join("\n")
|
319
|
+
@stream_logger.warn(formatted)
|
320
|
+
end
|
321
|
+
result.stdout << to_print
|
322
|
+
result.merged_output << to_print
|
310
323
|
end
|
311
324
|
stderr = Thread.new do
|
312
325
|
encoding = err.external_encoding
|
313
326
|
err.binmode
|
314
|
-
|
327
|
+
to_print = err.read.force_encoding(encoding)
|
328
|
+
if !to_print.chomp.empty? && @stream_logger
|
329
|
+
formatted = to_print.lines.map do |msg|
|
330
|
+
"[#{@target.safe_name}] err: #{msg.chomp}"
|
331
|
+
end.join("\n")
|
332
|
+
@stream_logger.warn(formatted)
|
333
|
+
end
|
334
|
+
result.stderr << to_print
|
335
|
+
result.merged_output << to_print
|
315
336
|
end
|
316
337
|
|
317
338
|
stdout.join
|
@@ -55,27 +55,59 @@ module Bolt
|
|
55
55
|
}
|
56
56
|
#{build_arg_list}
|
57
57
|
|
58
|
+
switch -regex ( Get-ExecutionPolicy )
|
59
|
+
{
|
60
|
+
'^AllSigned'
|
61
|
+
{
|
62
|
+
if ((Get-AuthenticodeSignature -File "#{script_path}").Status -ne 'Valid') {
|
63
|
+
$Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} and script '#{script_path}' does not contain a valid signature.")
|
64
|
+
exit 1;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
'^Restricted'
|
68
|
+
{
|
69
|
+
$Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} which denies running any scripts on the target.")
|
70
|
+
exit 1;
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
if([string]::IsNullOrEmpty($invokeArgs.ScriptBlock)){
|
75
|
+
$Host.UI.WriteErrorLine("Error: Failed to obtain scriptblock from '#{script_path}'. Running scripts might be disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170");
|
76
|
+
exit 1;
|
77
|
+
}
|
78
|
+
|
58
79
|
try
|
59
80
|
{
|
60
81
|
Invoke-Command @invokeArgs
|
61
82
|
}
|
62
83
|
catch
|
63
84
|
{
|
64
|
-
|
65
|
-
exit 1
|
85
|
+
$Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
|
86
|
+
exit 1;
|
66
87
|
}
|
67
88
|
PS
|
68
89
|
end
|
69
90
|
|
91
|
+
def append_ps_module_path(directory)
|
92
|
+
<<~PS
|
93
|
+
$env:PSModulePath += ";#{directory}"
|
94
|
+
PS
|
95
|
+
end
|
96
|
+
|
70
97
|
def ps_task(path, arguments)
|
71
98
|
<<~PS
|
72
99
|
$private:tempArgs = Get-ContentAsJson (
|
73
|
-
|
100
|
+
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
|
74
101
|
)
|
75
102
|
$allowedArgs = (Get-Command "#{path}").Parameters.Keys
|
76
103
|
$private:taskArgs = @{}
|
77
104
|
$private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
|
78
|
-
try {
|
105
|
+
try {
|
106
|
+
& "#{path}" @taskArgs
|
107
|
+
} catch {
|
108
|
+
$Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
|
109
|
+
exit 1;
|
110
|
+
}
|
79
111
|
PS
|
80
112
|
end
|
81
113
|
|
@@ -102,151 +134,11 @@ module Bolt
|
|
102
134
|
"${boltBaseDir}\\hiera\\lib;" +
|
103
135
|
$ENV:RUBYLIB
|
104
136
|
|
105
|
-
Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
|
106
|
-
$utf8 = [System.Text.Encoding]::UTF8
|
107
|
-
|
108
|
-
function Write-Stream {
|
109
|
-
PARAM(
|
110
|
-
[Parameter(Position=0)] $stream,
|
111
|
-
[Parameter(ValueFromPipeline=$true)] $string
|
112
|
-
)
|
113
|
-
PROCESS {
|
114
|
-
$bytes = $utf8.GetBytes($string)
|
115
|
-
$stream.Write( $bytes, 0, $bytes.Length )
|
116
|
-
}
|
117
|
-
}
|
118
|
-
|
119
|
-
function Convert-JsonToXml {
|
120
|
-
PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
|
121
|
-
BEGIN {
|
122
|
-
$mStream = New-Object System.IO.MemoryStream
|
123
|
-
}
|
124
|
-
PROCESS {
|
125
|
-
$json | Write-Stream -Stream $mStream
|
126
|
-
}
|
127
|
-
END {
|
128
|
-
$mStream.Position = 0
|
129
|
-
try {
|
130
|
-
$jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
|
131
|
-
$xml = New-Object Xml.XmlDocument
|
132
|
-
$xml.Load($jsonReader)
|
133
|
-
$xml
|
134
|
-
} finally {
|
135
|
-
$jsonReader.Close()
|
136
|
-
$mStream.Dispose()
|
137
|
-
}
|
138
|
-
}
|
139
|
-
}
|
140
|
-
|
141
|
-
Function ConvertFrom-Xml {
|
142
|
-
[CmdletBinding(DefaultParameterSetName="AutoType")]
|
143
|
-
PARAM(
|
144
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
|
145
|
-
[Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
|
146
|
-
[Switch] $ForceType
|
147
|
-
)
|
148
|
-
PROCESS{
|
149
|
-
if (Get-Member -InputObject $xml -Name root) {
|
150
|
-
return $xml.root.Objects | ConvertFrom-Xml
|
151
|
-
} elseif (Get-Member -InputObject $xml -Name Objects) {
|
152
|
-
return $xml.Objects | ConvertFrom-Xml
|
153
|
-
}
|
154
|
-
$propbag = @{}
|
155
|
-
foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^(__.*|type)$"} | Select-Object -ExpandProperty name) {
|
156
|
-
Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
|
157
|
-
$propbag."$Name" = Convert-Properties $xml."$name"
|
158
|
-
}
|
159
|
-
if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
|
160
|
-
if ($ForceType -and $Type) {
|
161
|
-
try {
|
162
|
-
$output = New-Object $Type -Property $propbag
|
163
|
-
} catch {
|
164
|
-
$output = New-Object PSObject -Property $propbag
|
165
|
-
$output.PsTypeNames.Insert(0, $xml.__type)
|
166
|
-
}
|
167
|
-
} elseif ($propbag.Count -ne 0) {
|
168
|
-
$output = New-Object PSObject -Property $propbag
|
169
|
-
if ($Type) {
|
170
|
-
$output.PsTypeNames.Insert(0, $Type)
|
171
|
-
}
|
172
|
-
}
|
173
|
-
return $output
|
174
|
-
}
|
175
|
-
}
|
176
|
-
|
177
|
-
Function Convert-Properties {
|
178
|
-
PARAM($InputObject)
|
179
|
-
switch ($InputObject.type) {
|
180
|
-
"object" {
|
181
|
-
return (ConvertFrom-Xml -Xml $InputObject)
|
182
|
-
}
|
183
|
-
"string" {
|
184
|
-
$MightBeADate = $InputObject.get_InnerText() -as [DateTime]
|
185
|
-
## Strings that are actually dates (*grumble* JSON is crap)
|
186
|
-
if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
|
187
|
-
return $MightBeADate
|
188
|
-
} else {
|
189
|
-
return $InputObject.get_InnerText()
|
190
|
-
}
|
191
|
-
}
|
192
|
-
"number" {
|
193
|
-
$number = $InputObject.get_InnerText()
|
194
|
-
if ($number -eq ($number -as [int])) {
|
195
|
-
return $number -as [int]
|
196
|
-
} elseif ($number -eq ($number -as [double])) {
|
197
|
-
return $number -as [double]
|
198
|
-
} else {
|
199
|
-
return $number -as [decimal]
|
200
|
-
}
|
201
|
-
}
|
202
|
-
"boolean" {
|
203
|
-
return [bool]::parse($InputObject.get_InnerText())
|
204
|
-
}
|
205
|
-
"null" {
|
206
|
-
return $null
|
207
|
-
}
|
208
|
-
"array" {
|
209
|
-
[object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
|
210
|
-
Convert-Properties $item
|
211
|
-
})
|
212
|
-
return $Items
|
213
|
-
}
|
214
|
-
default {
|
215
|
-
return $InputObject
|
216
|
-
}
|
217
|
-
}
|
218
|
-
}
|
219
|
-
|
220
|
-
Function ConvertFrom-Json2 {
|
221
|
-
[CmdletBinding()]
|
222
|
-
PARAM(
|
223
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
|
224
|
-
[Parameter(Mandatory=$true)] [Type] $Type,
|
225
|
-
[Switch] $ForceType
|
226
|
-
)
|
227
|
-
PROCESS {
|
228
|
-
$null = $PSBoundParameters.Remove("InputObject")
|
229
|
-
[Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
|
230
|
-
if ($xml) {
|
231
|
-
if ($xml.Objects) {
|
232
|
-
$xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
|
233
|
-
} elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
|
234
|
-
$xml.Item | ConvertFrom-Xml @PSBoundParameters
|
235
|
-
} else {
|
236
|
-
$xml | ConvertFrom-Xml @PSBoundParameters
|
237
|
-
}
|
238
|
-
} else {
|
239
|
-
Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
|
240
|
-
}
|
241
|
-
}
|
242
|
-
}
|
243
|
-
|
244
137
|
function ConvertFrom-PSCustomObject
|
245
138
|
{
|
246
139
|
PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
|
247
140
|
PROCESS {
|
248
141
|
if ($null -eq $InputObject) { return $null }
|
249
|
-
|
250
142
|
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
|
251
143
|
$collection = @(
|
252
144
|
foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
|
@@ -274,12 +166,7 @@ module Bolt
|
|
274
166
|
[Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
|
275
167
|
)
|
276
168
|
|
277
|
-
|
278
|
-
if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
|
279
|
-
$Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
|
280
|
-
} else {
|
281
|
-
$Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
|
282
|
-
}
|
169
|
+
$Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
|
283
170
|
}
|
284
171
|
PS
|
285
172
|
end
|