bolt 2.42.0 → 3.3.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +21 -19
  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/parallelize.rb +6 -8
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -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/lib/bolt/analytics.rb +3 -2
  12. data/lib/bolt/applicator.rb +11 -1
  13. data/lib/bolt/apply_result.rb +1 -1
  14. data/lib/bolt/bolt_option_parser.rb +9 -116
  15. data/lib/bolt/catalog.rb +10 -29
  16. data/lib/bolt/cli.rb +90 -154
  17. data/lib/bolt/config.rb +66 -239
  18. data/lib/bolt/config/options.rb +79 -102
  19. data/lib/bolt/config/transport/local.rb +1 -0
  20. data/lib/bolt/config/transport/lxd.rb +21 -0
  21. data/lib/bolt/config/transport/options.rb +9 -2
  22. data/lib/bolt/config/transport/orch.rb +1 -0
  23. data/lib/bolt/executor.rb +23 -6
  24. data/lib/bolt/inventory.rb +1 -1
  25. data/lib/bolt/inventory/group.rb +7 -4
  26. data/lib/bolt/logger.rb +123 -11
  27. data/lib/bolt/module_installer.rb +6 -4
  28. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  29. data/lib/bolt/module_installer/resolver.rb +59 -14
  30. data/lib/bolt/module_installer/specs/forge_spec.rb +10 -4
  31. data/lib/bolt/module_installer/specs/git_spec.rb +19 -4
  32. data/lib/bolt/outputter/human.rb +56 -17
  33. data/lib/bolt/outputter/json.rb +16 -16
  34. data/lib/bolt/outputter/rainbow.rb +3 -3
  35. data/lib/bolt/pal.rb +95 -15
  36. data/lib/bolt/pal/yaml_plan.rb +9 -4
  37. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -153
  38. data/lib/bolt/pal/yaml_plan/step.rb +91 -52
  39. data/lib/bolt/pal/yaml_plan/step/command.rb +16 -16
  40. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  41. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  42. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  43. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  44. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  45. data/lib/bolt/pal/yaml_plan/step/script.rb +32 -17
  46. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  47. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  48. data/lib/bolt/pal/yaml_plan/transpiler.rb +2 -1
  49. data/lib/bolt/plan_creator.rb +1 -1
  50. data/lib/bolt/plugin.rb +2 -2
  51. data/lib/bolt/plugin/cache.rb +7 -7
  52. data/lib/bolt/plugin/module.rb +0 -23
  53. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  54. data/lib/bolt/plugin/puppetdb.rb +1 -1
  55. data/lib/bolt/project.rb +54 -81
  56. data/lib/bolt/project_manager.rb +5 -4
  57. data/lib/bolt/project_manager/module_migrator.rb +7 -6
  58. data/lib/bolt/rerun.rb +1 -1
  59. data/lib/bolt/result.rb +6 -1
  60. data/lib/bolt/shell.rb +16 -0
  61. data/lib/bolt/shell/bash.rb +57 -25
  62. data/lib/bolt/shell/bash/tmpdir.rb +6 -3
  63. data/lib/bolt/shell/powershell.rb +33 -10
  64. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  65. data/lib/bolt/task.rb +2 -2
  66. data/lib/bolt/transport/base.rb +0 -9
  67. data/lib/bolt/transport/docker.rb +1 -125
  68. data/lib/bolt/transport/docker/connection.rb +86 -161
  69. data/lib/bolt/transport/local.rb +1 -9
  70. data/lib/bolt/transport/lxd.rb +26 -0
  71. data/lib/bolt/transport/lxd/connection.rb +99 -0
  72. data/lib/bolt/transport/orch/connection.rb +1 -1
  73. data/lib/bolt/transport/ssh.rb +1 -2
  74. data/lib/bolt/transport/ssh/connection.rb +2 -2
  75. data/lib/bolt/transport/winrm/connection.rb +1 -1
  76. data/lib/bolt/validator.rb +2 -2
  77. data/lib/bolt/version.rb +1 -1
  78. data/lib/bolt_server/config.rb +1 -1
  79. data/lib/bolt_server/transport_app.rb +61 -32
  80. data/lib/bolt_spec/bolt_context.rb +9 -4
  81. data/lib/bolt_spec/plans.rb +1 -109
  82. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  83. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  84. data/libexec/bolt_catalog +1 -1
  85. data/modules/aggregate/plans/count.pp +21 -0
  86. data/modules/aggregate/plans/targets.pp +21 -0
  87. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  88. data/modules/puppetdb_fact/plans/init.pp +10 -0
  89. metadata +13 -9
  90. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -143,7 +143,7 @@ module Bolt
143
143
  @outputter.print_message("Migrating project #{@config.project.path}\n\n")
144
144
 
145
145
  @outputter.print_action_step(
146
- "Migrating a Bolt project may make irreversible changes to the project's "\
146
+ "Migrating a Bolt project might make irreversible changes to the project's "\
147
147
  "configuration and inventory files. Before continuing, make sure the "\
148
148
  "project has a backup or uses a version control system."
149
149
  )
@@ -166,10 +166,11 @@ module Bolt
166
166
  # Migrates the project-level configuration file to the latest version.
167
167
  #
168
168
  private def migrate_config
169
- migrator = ConfigMigrator.new(@outputter)
169
+ migrator = ConfigMigrator.new(@outputter)
170
+ configfile = @config.project.path + 'bolt.yaml'
170
171
 
171
172
  migrator.migrate(
172
- @config.project.config_file,
173
+ configfile,
173
174
  @config.project.project_file,
174
175
  @config.inventoryfile || @config.project.inventory_file,
175
176
  @config.project.backup_dir
@@ -194,7 +195,7 @@ module Bolt
194
195
 
195
196
  migrator.migrate(
196
197
  @config.project,
197
- @config.modulepath
198
+ @config.modulepath[0...-1]
198
199
  )
199
200
  end
200
201
  end
@@ -6,19 +6,20 @@ module Bolt
6
6
  class ProjectManager
7
7
  class ModuleMigrator < Migrator
8
8
  def migrate(project, configured_modulepath)
9
- return true unless project.modules.nil?
9
+ return true if project.managed_moduledir.exist?
10
10
 
11
11
  @outputter.print_message "Migrating project modules\n\n"
12
12
 
13
13
  config = project.project_file
14
14
  puppetfile = project.puppetfile
15
15
  managed_moduledir = project.managed_moduledir
16
- modulepath = [(project.path + 'modules').to_s,
16
+ new_modulepath = [(project.path + 'modules').to_s]
17
+ old_modulepath = [(project.path + 'modules').to_s,
17
18
  (project.path + 'site-modules').to_s,
18
19
  (project.path + 'site').to_s]
19
20
 
20
21
  # Notify user to manually migrate modules if using non-default modulepath
21
- if configured_modulepath != modulepath
22
+ if configured_modulepath != new_modulepath && configured_modulepath != old_modulepath
22
23
  @outputter.print_action_step(
23
24
  "Project has a non-default configured modulepath, unable to automatically "\
24
25
  "migrate project modules. To migrate project modules manually, see "\
@@ -27,10 +28,10 @@ module Bolt
27
28
  true
28
29
  # Migrate modules from Puppetfile
29
30
  elsif File.exist?(puppetfile)
30
- migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, modulepath)
31
+ migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, old_modulepath)
31
32
  # Migrate modules to updated modulepath
32
33
  else
33
- consolidate_modules(modulepath)
34
+ consolidate_modules(old_modulepath)
34
35
  update_project_config([], config)
35
36
  end
36
37
  end
@@ -65,7 +66,7 @@ module Bolt
65
66
  # Attempt to resolve dependencies
66
67
  begin
67
68
  @outputter.print_message('')
68
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
69
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
69
70
  puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
70
71
  rescue Bolt::Error => e
71
72
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
data/lib/bolt/rerun.rb CHANGED
@@ -49,7 +49,7 @@ module Bolt
49
49
  end
50
50
  end
51
51
  rescue StandardError => e
52
- Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
52
+ Bolt::Logger.warn_once("unwriteable_file", "Failed to save result to #{@path}: #{e.message}")
53
53
  end
54
54
  end
55
55
  end
data/lib/bolt/result.rb CHANGED
@@ -203,12 +203,17 @@ module Bolt
203
203
  end
204
204
 
205
205
  def to_data
206
+ serialized_value = safe_value
207
+ if serialized_value.key?('_sensitive') &&
208
+ serialized_value['_sensitive'].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
209
+ serialized_value['_sensitive'] = serialized_value['_sensitive'].to_s
210
+ end
206
211
  {
207
212
  "target" => @target.name,
208
213
  "action" => action,
209
214
  "object" => object,
210
215
  "status" => status,
211
- "value" => safe_value
216
+ "value" => serialized_value
212
217
  }
213
218
  end
214
219
 
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
@@ -54,19 +53,35 @@ module Bolt
54
53
 
55
54
  def download(source, destination, options = {})
56
55
  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
56
  download = File.join(destination, Bolt::Util.unix_basename(source))
61
57
 
62
- conn.download_file(source, destination, download)
58
+ # If using run-as, the file is copied to a tmpdir and chowned to the
59
+ # connecting user. This is a workaround for limitations in net-ssh that
60
+ # only allow for downloading files as the connecting user, which is a
61
+ # problem for users who cannot connect to targets as the root user.
62
+ # This temporary copy should *always* be deleted.
63
+ if run_as
64
+ with_tmpdir(force_cleanup: true) do |dir|
65
+ tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source))
66
+
67
+ result = execute(['cp', '-r', source, dir.to_s], sudoable: true)
68
+
69
+ if result.exit_code != 0
70
+ message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}"
71
+ raise Bolt::Node::FileError.new(message, 'CP_ERROR')
72
+ end
73
+
74
+ # We need to force the chown, otherwise this will just return
75
+ # without doing anything since the chown user is the same as the
76
+ # connecting user.
77
+ dir.chown(conn.user, force: true)
63
78
 
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))
79
+ conn.download_file(tmpfile, destination, download)
80
+ end
81
+ # If not using run-as, we can skip creating a temporary copy and just
82
+ # download the file directly.
83
+ else
84
+ conn.download_file(source, destination, download)
70
85
  end
71
86
 
72
87
  Bolt::Result.for_download(target, source, destination, download)
@@ -145,7 +160,10 @@ module Bolt
145
160
 
146
161
  dir.chown(run_as)
147
162
 
148
- execute_options[:stdin] = stdin
163
+ # Don't pass parameters on stdin if using a tty, as the parameters are
164
+ # already part of the wrapper script.
165
+ execute_options[:stdin] = stdin unless stdin && target.options['tty']
166
+
149
167
  execute_options[:sudoable] = true if run_as
150
168
  output = execute(remote_task_path, **execute_options)
151
169
  end
@@ -291,15 +309,15 @@ module Bolt
291
309
 
292
310
  # A helper to create and delete a tmpdir on the remote system. Yields the
293
311
  # directory name.
294
- def with_tmpdir
312
+ def with_tmpdir(force_cleanup: false)
295
313
  dir = make_tmpdir
296
314
  yield dir
297
315
  ensure
298
316
  if dir
299
- if target.options['cleanup']
317
+ if target.options['cleanup'] || force_cleanup
300
318
  dir.delete
301
319
  else
302
- @logger.warn("Skipping cleanup of tmpdir #{dir}")
320
+ Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
303
321
  end
304
322
  end
305
323
  end
@@ -331,10 +349,15 @@ module Bolt
331
349
  # together multiple commands into a single sh invocation
332
350
  commands = [inject_interpreter(options[:interpreter], command)]
333
351
 
352
+ # Let the transport handle adding environment variables if it's custom.
334
353
  if options[:environment]
335
- env_decl = options[:environment].map do |env, val|
336
- "#{env}=#{Shellwords.shellescape(val)}"
337
- end.join(' ')
354
+ if defined? conn.add_env_vars
355
+ conn.add_env_vars(options[:environment])
356
+ else
357
+ env_decl = options[:environment].map do |env, val|
358
+ "#{env}=#{Shellwords.shellescape(val)}"
359
+ end.join(' ')
360
+ end
338
361
  end
339
362
 
340
363
  if escalate
@@ -385,14 +408,23 @@ module Bolt
385
408
  # See if we can read from out or err, or write to in
386
409
  ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
387
410
 
388
- # Read from out and err
389
411
  ready_read&.each do |stream|
412
+ stream_name = stream == out ? 'out' : 'err'
390
413
  # 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
414
+ to_print = if use_sudo
415
+ check_sudo(stream, inp, options[:stdin])
416
+ else
417
+ stream.readpartial(CHUNK_SIZE)
418
+ end
419
+
420
+ if !to_print.chomp.empty? && @stream_logger
421
+ formatted = to_print.lines.map do |msg|
422
+ "[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
423
+ end.join("\n")
424
+ @stream_logger.warn(formatted)
425
+ end
426
+
427
+ read_streams[stream] << to_print
396
428
  rescue EOFError
397
429
  end
398
430
 
@@ -440,7 +472,7 @@ module Bolt
440
472
  when 0
441
473
  @logger.trace { "Command `#{command_str}` returned successfully" }
442
474
  when 126
443
- msg = "\n\nThis may be caused by the default tmpdir being mounted "\
475
+ msg = "\n\nThis might be caused by the default tmpdir being mounted "\
444
476
  "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
445
477
  result_output.stderr << msg
446
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
@@ -48,7 +48,10 @@ 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 tmpdir '#{@path}': #{result.stderr.string}")
51
+ Bolt::Logger.warn(
52
+ "fail_cleanup",
53
+ "Failed to clean up tmpdir '#{@path}': #{result.stderr.string}"
54
+ )
52
55
  end
53
56
  # For testing
54
57
  result.stderr.string
@@ -23,11 +23,10 @@ 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
- Bolt::Logger.deprecation_warning("PowerShell 2", msg)
29
+ Bolt::Logger.deprecate_once("powershell_2", msg)
31
30
  @logger.debug("Detected PowerShell 2 on #{target}.")
32
31
  end
33
32
  end
@@ -163,7 +162,7 @@ module Bolt
163
162
  if target.options['cleanup']
164
163
  rmdir(@tmpdir)
165
164
  else
166
- @logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'")
165
+ Bolt::Logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'", "skip_cleanup")
167
166
  end
168
167
  end
169
168
  end
@@ -209,16 +208,21 @@ module Bolt
209
208
  arguments = unwrap_sensitive_args(arguments)
210
209
  with_tmpdir do |dir|
211
210
  script_path = write_executable(dir, script)
212
- command = if powershell_file?(script_path)
211
+ command = if powershell_file?(script_path) && options[:pwsh_params]
212
+ # Scripts run with pwsh_params can be run like tasks
213
+ Snippets.ps_task(script_path, options[:pwsh_params])
214
+ elsif powershell_file?(script_path)
213
215
  Snippets.run_script(arguments, script_path)
214
216
  else
215
217
  path, args = *process_from_extension(script_path)
216
218
  args += escape_arguments(arguments)
217
219
  execute_process(path, args)
218
220
  end
219
- command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
221
+ env_assignments = options[:env_vars] ? env_declarations(options[:env_vars]) : []
222
+ shell_init = options[:pwsh_params] ? Snippets.shell_init : ''
223
+
224
+ output = execute([shell_init, *env_assignments, command].join("\r\n"))
220
225
 
221
- output = execute(command)
222
226
  Bolt::Result.for_command(target,
223
227
  output.stdout.string,
224
228
  output.stderr.string,
@@ -275,7 +279,12 @@ module Bolt
275
279
  []
276
280
  end
277
281
 
278
- output = execute([Snippets.shell_init, *env_assignments, command].join("\n"))
282
+ output = execute([
283
+ Snippets.shell_init,
284
+ Snippets.append_ps_module_path(dir),
285
+ *env_assignments,
286
+ command
287
+ ].join("\n"))
279
288
 
280
289
  Bolt::Result.for_task(target, output.stdout.string,
281
290
  output.stderr.string,
@@ -306,12 +315,26 @@ module Bolt
306
315
  # the proper encoding so the string isn't later misinterpreted
307
316
  encoding = out.external_encoding
308
317
  out.binmode
309
- result.stdout << out.read.force_encoding(encoding)
318
+ to_print = out.read.force_encoding(encoding)
319
+ if !to_print.chomp.empty? && @stream_logger
320
+ formatted = to_print.lines.map do |msg|
321
+ "[#{@target.safe_name}] out: #{msg.chomp}"
322
+ end.join("\n")
323
+ @stream_logger.warn(formatted)
324
+ end
325
+ result.stdout << to_print
310
326
  end
311
327
  stderr = Thread.new do
312
328
  encoding = err.external_encoding
313
329
  err.binmode
314
- result.stderr << err.read.force_encoding(encoding)
330
+ to_print = err.read.force_encoding(encoding)
331
+ if !to_print.chomp.empty? && @stream_logger
332
+ formatted = to_print.lines.map do |msg|
333
+ "[#{@target.safe_name}] err: #{msg.chomp}"
334
+ end.join("\n")
335
+ @stream_logger.warn(formatted)
336
+ end
337
+ result.stderr << to_print
315
338
  end
316
339
 
317
340
  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