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.

Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +11 -9
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
  8. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  10. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  11. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
  12. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  13. data/lib/bolt/apply_result.rb +1 -1
  14. data/lib/bolt/bolt_option_parser.rb +9 -123
  15. data/lib/bolt/cli.rb +125 -127
  16. data/lib/bolt/config.rb +39 -214
  17. data/lib/bolt/config/options.rb +34 -125
  18. data/lib/bolt/config/transport/local.rb +1 -0
  19. data/lib/bolt/config/transport/lxd.rb +23 -0
  20. data/lib/bolt/config/transport/options.rb +9 -2
  21. data/lib/bolt/executor.rb +20 -5
  22. data/lib/bolt/logger.rb +9 -1
  23. data/lib/bolt/module_installer.rb +2 -2
  24. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  25. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  26. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  27. data/lib/bolt/node/output.rb +14 -4
  28. data/lib/bolt/outputter/human.rb +52 -24
  29. data/lib/bolt/outputter/json.rb +16 -16
  30. data/lib/bolt/pal.rb +26 -5
  31. data/lib/bolt/pal/yaml_plan.rb +1 -2
  32. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -153
  33. data/lib/bolt/pal/yaml_plan/step.rb +91 -52
  34. data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
  35. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  36. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  37. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  38. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  39. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  40. data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
  41. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  42. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  43. data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
  44. data/lib/bolt/plan_creator.rb +1 -1
  45. data/lib/bolt/plugin/module.rb +0 -23
  46. data/lib/bolt/plugin/puppet_connect_data.rb +45 -3
  47. data/lib/bolt/project.rb +16 -56
  48. data/lib/bolt/project_manager.rb +5 -4
  49. data/lib/bolt/project_manager/module_migrator.rb +7 -6
  50. data/lib/bolt/result.rb +10 -11
  51. data/lib/bolt/shell.rb +16 -0
  52. data/lib/bolt/shell/bash.rb +61 -31
  53. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  54. data/lib/bolt/shell/powershell.rb +35 -14
  55. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  56. data/lib/bolt/task.rb +1 -1
  57. data/lib/bolt/transport/base.rb +0 -9
  58. data/lib/bolt/transport/docker.rb +1 -125
  59. data/lib/bolt/transport/docker/connection.rb +86 -161
  60. data/lib/bolt/transport/local.rb +1 -9
  61. data/lib/bolt/transport/lxd.rb +26 -0
  62. data/lib/bolt/transport/lxd/connection.rb +99 -0
  63. data/lib/bolt/transport/orch.rb +13 -5
  64. data/lib/bolt/transport/ssh/connection.rb +1 -1
  65. data/lib/bolt/transport/winrm/connection.rb +1 -1
  66. data/lib/bolt/util.rb +8 -0
  67. data/lib/bolt/version.rb +1 -1
  68. data/lib/bolt_server/transport_app.rb +61 -33
  69. data/lib/bolt_spec/bolt_context.rb +9 -4
  70. data/lib/bolt_spec/plans.rb +1 -109
  71. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  72. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
  73. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
  74. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  75. data/modules/aggregate/plans/count.pp +21 -0
  76. data/modules/aggregate/plans/targets.pp +21 -0
  77. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  78. data/modules/puppetdb_fact/plans/init.pp +10 -0
  79. metadata +7 -3
  80. 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)
@@ -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.stdout.string,
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
- conn.download_file(source, destination, download)
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
- # If the download path doesn't exist, then the file was likely downloaded from Windows
65
- # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
66
- # should be saved to the expected location, so update the download path assuming a
67
- # Windows basename so the result shows the correct local path.
68
- unless File.exist?(download)
69
- download = File.join(destination, Bolt::Util.windows_basename(source))
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.stdout.string,
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
- env_decl = options[:environment].map do |env, val|
336
- "#{env}=#{Shellwords.shellescape(val)}"
337
- end.join(' ')
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
- read_streams[stream] << if use_sudo
392
- check_sudo(stream, inp, options[:stdin])
393
- else
394
- stream.readpartial(CHUNK_SIZE)
395
- end
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 may be caused by the default tmpdir being mounted "\
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 << msg
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 deprecated, and support will be removed in Bolt 3.0. See "\
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.stdout.string,
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
- command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
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.stdout.string,
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([Snippets.shell_init, *env_assignments, command].join("\n"))
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
- result.stdout << out.read.force_encoding(encoding)
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
- result.stderr << err.read.force_encoding(encoding)
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
- Write-Error $_.Exception
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
- $utf8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
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 { & "#{path}" @taskArgs } catch { Write-Error $_.Exception; exit 1 }
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
- # using polyfill cmdlet on PS2, so pass type info
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