openbolt 5.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/Puppetfile +52 -0
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +60 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +71 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +55 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +65 -0
  9. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +93 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +33 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +38 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +208 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +62 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +57 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +130 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +31 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +52 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +87 -0
  19. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +34 -0
  20. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +35 -0
  21. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +74 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +97 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +47 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +52 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +40 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +42 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +106 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
  30. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +291 -0
  31. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +145 -0
  32. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +164 -0
  33. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +211 -0
  34. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +48 -0
  35. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +43 -0
  36. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +145 -0
  37. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +38 -0
  38. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +101 -0
  39. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +29 -0
  40. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +131 -0
  41. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +59 -0
  42. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +39 -0
  43. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +50 -0
  44. data/bolt-modules/boltlib/types/planresult.pp +18 -0
  45. data/bolt-modules/boltlib/types/targetspec.pp +7 -0
  46. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +42 -0
  47. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +20 -0
  48. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  49. data/bolt-modules/file/lib/puppet/functions/file/delete.rb +21 -0
  50. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +28 -0
  51. data/bolt-modules/file/lib/puppet/functions/file/join.rb +20 -0
  52. data/bolt-modules/file/lib/puppet/functions/file/read.rb +33 -0
  53. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +28 -0
  54. data/bolt-modules/file/lib/puppet/functions/file/write.rb +24 -0
  55. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
  56. data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
  57. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
  58. data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
  59. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
  60. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
  61. data/bolt-modules/out/lib/puppet/functions/out/message.rb +36 -0
  62. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
  63. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  64. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +65 -0
  65. data/bolt-modules/system/lib/puppet/functions/system/env.rb +20 -0
  66. data/exe/bolt +17 -0
  67. data/guides/debugging.yaml +27 -0
  68. data/guides/inventory.yaml +23 -0
  69. data/guides/links.yaml +12 -0
  70. data/guides/logging.yaml +17 -0
  71. data/guides/module.yaml +18 -0
  72. data/guides/modulepath.yaml +24 -0
  73. data/guides/project.yaml +21 -0
  74. data/guides/targets.yaml +28 -0
  75. data/guides/transports.yaml +22 -0
  76. data/lib/bolt/analytics.rb +233 -0
  77. data/lib/bolt/application.rb +806 -0
  78. data/lib/bolt/applicator.rb +368 -0
  79. data/lib/bolt/apply_inventory.rb +93 -0
  80. data/lib/bolt/apply_result.rb +154 -0
  81. data/lib/bolt/apply_target.rb +90 -0
  82. data/lib/bolt/bolt_option_parser.rb +1226 -0
  83. data/lib/bolt/catalog/logging.rb +15 -0
  84. data/lib/bolt/catalog.rb +144 -0
  85. data/lib/bolt/cli.rb +949 -0
  86. data/lib/bolt/config/modulepath.rb +30 -0
  87. data/lib/bolt/config/options.rb +673 -0
  88. data/lib/bolt/config/transport/base.rb +133 -0
  89. data/lib/bolt/config/transport/docker.rb +34 -0
  90. data/lib/bolt/config/transport/jail.rb +33 -0
  91. data/lib/bolt/config/transport/local.rb +39 -0
  92. data/lib/bolt/config/transport/lxd.rb +34 -0
  93. data/lib/bolt/config/transport/options.rb +431 -0
  94. data/lib/bolt/config/transport/orch.rb +41 -0
  95. data/lib/bolt/config/transport/podman.rb +33 -0
  96. data/lib/bolt/config/transport/remote.rb +24 -0
  97. data/lib/bolt/config/transport/ssh.rb +138 -0
  98. data/lib/bolt/config/transport/winrm.rb +63 -0
  99. data/lib/bolt/config.rb +515 -0
  100. data/lib/bolt/container_result.rb +105 -0
  101. data/lib/bolt/error.rb +194 -0
  102. data/lib/bolt/executor.rb +539 -0
  103. data/lib/bolt/fiber_executor.rb +190 -0
  104. data/lib/bolt/inventory/group.rb +446 -0
  105. data/lib/bolt/inventory/inventory.rb +391 -0
  106. data/lib/bolt/inventory/options.rb +139 -0
  107. data/lib/bolt/inventory/target.rb +293 -0
  108. data/lib/bolt/inventory.rb +120 -0
  109. data/lib/bolt/logger.rb +252 -0
  110. data/lib/bolt/module.rb +54 -0
  111. data/lib/bolt/module_installer/installer.rb +44 -0
  112. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  113. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  114. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  115. data/lib/bolt/module_installer/puppetfile.rb +131 -0
  116. data/lib/bolt/module_installer/resolver.rb +129 -0
  117. data/lib/bolt/module_installer/specs/forge_spec.rb +91 -0
  118. data/lib/bolt/module_installer/specs/git_spec.rb +150 -0
  119. data/lib/bolt/module_installer/specs/id/base.rb +116 -0
  120. data/lib/bolt/module_installer/specs/id/gitclone.rb +120 -0
  121. data/lib/bolt/module_installer/specs/id/github.rb +90 -0
  122. data/lib/bolt/module_installer/specs/id/gitlab.rb +92 -0
  123. data/lib/bolt/module_installer/specs.rb +95 -0
  124. data/lib/bolt/module_installer.rb +208 -0
  125. data/lib/bolt/node/errors.rb +55 -0
  126. data/lib/bolt/node/output.rb +29 -0
  127. data/lib/bolt/outputter/human.rb +958 -0
  128. data/lib/bolt/outputter/json.rb +205 -0
  129. data/lib/bolt/outputter/logger.rb +76 -0
  130. data/lib/bolt/outputter/rainbow.rb +118 -0
  131. data/lib/bolt/outputter.rb +57 -0
  132. data/lib/bolt/pal/issues.rb +19 -0
  133. data/lib/bolt/pal/logging.rb +17 -0
  134. data/lib/bolt/pal/yaml_plan/evaluator.rb +83 -0
  135. data/lib/bolt/pal/yaml_plan/loader.rb +94 -0
  136. data/lib/bolt/pal/yaml_plan/parameter.rb +63 -0
  137. data/lib/bolt/pal/yaml_plan/step/command.rb +45 -0
  138. data/lib/bolt/pal/yaml_plan/step/download.rb +37 -0
  139. data/lib/bolt/pal/yaml_plan/step/eval.rb +42 -0
  140. data/lib/bolt/pal/yaml_plan/step/message.rb +31 -0
  141. data/lib/bolt/pal/yaml_plan/step/plan.rb +42 -0
  142. data/lib/bolt/pal/yaml_plan/step/resources.rb +170 -0
  143. data/lib/bolt/pal/yaml_plan/step/script.rb +62 -0
  144. data/lib/bolt/pal/yaml_plan/step/task.rb +42 -0
  145. data/lib/bolt/pal/yaml_plan/step/upload.rb +37 -0
  146. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  147. data/lib/bolt/pal/yaml_plan/step.rb +223 -0
  148. data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
  149. data/lib/bolt/pal/yaml_plan.rb +172 -0
  150. data/lib/bolt/pal.rb +847 -0
  151. data/lib/bolt/plan_creator.rb +219 -0
  152. data/lib/bolt/plan_future.rb +86 -0
  153. data/lib/bolt/plan_result.rb +44 -0
  154. data/lib/bolt/plugin/cache.rb +76 -0
  155. data/lib/bolt/plugin/env_var.rb +54 -0
  156. data/lib/bolt/plugin/module.rb +276 -0
  157. data/lib/bolt/plugin/prompt.rb +36 -0
  158. data/lib/bolt/plugin/puppet_connect_data.rb +84 -0
  159. data/lib/bolt/plugin/puppetdb.rb +124 -0
  160. data/lib/bolt/plugin/task.rb +72 -0
  161. data/lib/bolt/plugin.rb +380 -0
  162. data/lib/bolt/project.rb +219 -0
  163. data/lib/bolt/project_manager/config_migrator.rb +113 -0
  164. data/lib/bolt/project_manager/inventory_migrator.rb +67 -0
  165. data/lib/bolt/project_manager/migrator.rb +39 -0
  166. data/lib/bolt/project_manager/module_migrator.rb +203 -0
  167. data/lib/bolt/project_manager.rb +221 -0
  168. data/lib/bolt/puppetdb/client.rb +153 -0
  169. data/lib/bolt/puppetdb/config.rb +176 -0
  170. data/lib/bolt/puppetdb/instance.rb +146 -0
  171. data/lib/bolt/puppetdb.rb +15 -0
  172. data/lib/bolt/r10k_log_proxy.rb +30 -0
  173. data/lib/bolt/rerun.rb +55 -0
  174. data/lib/bolt/resource_instance.rb +133 -0
  175. data/lib/bolt/result.rb +247 -0
  176. data/lib/bolt/result_set.rb +128 -0
  177. data/lib/bolt/shell/bash/tmpdir.rb +62 -0
  178. data/lib/bolt/shell/bash.rb +516 -0
  179. data/lib/bolt/shell/powershell/snippets.rb +181 -0
  180. data/lib/bolt/shell/powershell.rb +365 -0
  181. data/lib/bolt/shell.rb +105 -0
  182. data/lib/bolt/target.rb +174 -0
  183. data/lib/bolt/task/puppet_server.rb +27 -0
  184. data/lib/bolt/task/run.rb +55 -0
  185. data/lib/bolt/task.rb +163 -0
  186. data/lib/bolt/transport/base.rb +252 -0
  187. data/lib/bolt/transport/docker/connection.rb +150 -0
  188. data/lib/bolt/transport/docker.rb +23 -0
  189. data/lib/bolt/transport/jail/connection.rb +81 -0
  190. data/lib/bolt/transport/jail.rb +21 -0
  191. data/lib/bolt/transport/local/connection.rb +106 -0
  192. data/lib/bolt/transport/local.rb +20 -0
  193. data/lib/bolt/transport/lxd/connection.rb +115 -0
  194. data/lib/bolt/transport/lxd.rb +26 -0
  195. data/lib/bolt/transport/orch/connection.rb +111 -0
  196. data/lib/bolt/transport/orch.rb +271 -0
  197. data/lib/bolt/transport/podman/connection.rb +102 -0
  198. data/lib/bolt/transport/podman.rb +19 -0
  199. data/lib/bolt/transport/remote.rb +41 -0
  200. data/lib/bolt/transport/simple.rb +54 -0
  201. data/lib/bolt/transport/ssh/connection.rb +321 -0
  202. data/lib/bolt/transport/ssh/exec_connection.rb +140 -0
  203. data/lib/bolt/transport/ssh.rb +48 -0
  204. data/lib/bolt/transport/winrm/connection.rb +378 -0
  205. data/lib/bolt/transport/winrm.rb +33 -0
  206. data/lib/bolt/util/format.rb +68 -0
  207. data/lib/bolt/util/puppet_log_level.rb +21 -0
  208. data/lib/bolt/util.rb +465 -0
  209. data/lib/bolt/validator.rb +227 -0
  210. data/lib/bolt/version.rb +5 -0
  211. data/lib/bolt.rb +8 -0
  212. data/lib/bolt_server/acl.rb +39 -0
  213. data/lib/bolt_server/base_config.rb +112 -0
  214. data/lib/bolt_server/config.rb +64 -0
  215. data/lib/bolt_server/file_cache.rb +200 -0
  216. data/lib/bolt_server/request_error.rb +11 -0
  217. data/lib/bolt_server/schemas/action-check_node_connections.json +14 -0
  218. data/lib/bolt_server/schemas/action-run_command.json +12 -0
  219. data/lib/bolt_server/schemas/action-run_script.json +47 -0
  220. data/lib/bolt_server/schemas/action-run_task.json +20 -0
  221. data/lib/bolt_server/schemas/action-upload_file.json +47 -0
  222. data/lib/bolt_server/schemas/partials/target-any.json +10 -0
  223. data/lib/bolt_server/schemas/partials/target-ssh.json +88 -0
  224. data/lib/bolt_server/schemas/partials/target-winrm.json +67 -0
  225. data/lib/bolt_server/schemas/partials/task.json +94 -0
  226. data/lib/bolt_server/schemas/transport-ssh.json +25 -0
  227. data/lib/bolt_server/schemas/transport-winrm.json +19 -0
  228. data/lib/bolt_server/transport_app.rb +554 -0
  229. data/lib/bolt_spec/bolt_context.rb +226 -0
  230. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +51 -0
  231. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  232. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  233. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +59 -0
  234. data/lib/bolt_spec/plans/action_stubs/task_stub.rb +57 -0
  235. data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +65 -0
  236. data/lib/bolt_spec/plans/action_stubs.rb +196 -0
  237. data/lib/bolt_spec/plans/mock_executor.rb +361 -0
  238. data/lib/bolt_spec/plans/publish_stub.rb +49 -0
  239. data/lib/bolt_spec/plans.rb +190 -0
  240. data/lib/bolt_spec/run.rb +246 -0
  241. data/lib/logging_extensions/logging.rb +13 -0
  242. data/libexec/apply_catalog.rb +130 -0
  243. data/libexec/bolt_catalog +68 -0
  244. data/libexec/custom_facts.rb +63 -0
  245. data/libexec/query_resources.rb +75 -0
  246. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +21 -0
  247. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +22 -0
  248. data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
  249. data/modules/aggregate/plans/count.pp +56 -0
  250. data/modules/aggregate/plans/targets.pp +56 -0
  251. data/modules/canary/lib/puppet/functions/canary/merge.rb +13 -0
  252. data/modules/canary/lib/puppet/functions/canary/random_split.rb +22 -0
  253. data/modules/canary/lib/puppet/functions/canary/skip.rb +25 -0
  254. data/modules/canary/plans/init.pp +100 -0
  255. data/modules/puppet_connect/plans/test_input_data.pp +94 -0
  256. data/modules/puppetdb_fact/plans/init.pp +20 -0
  257. data/resources/bolt_bash_completion.sh +214 -0
  258. metadata +735 -0
@@ -0,0 +1,958 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../bolt/container_result'
4
+ require_relative '../../bolt/pal'
5
+ require_relative '../../bolt/util/format'
6
+
7
+ module Bolt
8
+ class Outputter
9
+ class Human < Bolt::Outputter
10
+ COLORS = {
11
+ dim: "2", # Dim, the other color of the rainbow
12
+ red: "31",
13
+ green: "32",
14
+ yellow: "33",
15
+ cyan: "36"
16
+ }.freeze
17
+
18
+ def print_head; end
19
+
20
+ def initialize(color, verbose, trace, spin, stream = $stdout)
21
+ super
22
+ # Plans and without_default_logging() calls can both be nested, so we
23
+ # track each of them with a "stack" consisting of an integer.
24
+ @plan_depth = 0
25
+ @disable_depth = 0
26
+ @pinwheel = %w[- \\ | /]
27
+ end
28
+
29
+ def colorize(color, string)
30
+ if @color && @stream.isatty
31
+ "\033[#{COLORS[color]}m#{string}\033[0m"
32
+ else
33
+ string
34
+ end
35
+ end
36
+
37
+ def start_spin
38
+ return unless @spin && @stream.isatty && !@spinning
39
+ @spinning = true
40
+ @spin_thread = Thread.new do
41
+ loop do
42
+ sleep(0.1)
43
+ @stream.print(colorize(:cyan, @pinwheel.rotate!.first + "\b"))
44
+ end
45
+ end
46
+ end
47
+
48
+ def stop_spin
49
+ return unless @spin && @stream.isatty && @spinning
50
+ @spinning = false
51
+ @spin_thread.terminate
52
+ @stream.print("\b")
53
+ end
54
+
55
+ def remove_trail(string)
56
+ string.sub(/\s\z/, '')
57
+ end
58
+
59
+ # Wraps a string to the specified width. Lines only wrap
60
+ # at whitespace.
61
+ #
62
+ def wrap(string, width = 80)
63
+ return string unless string.is_a?(String)
64
+ string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
65
+ end
66
+
67
+ # Trims a string to a specified width, adding an ellipsis if it's longer.
68
+ #
69
+ def truncate(string, width = 80)
70
+ return string unless string.is_a?(String) && string.length > width
71
+ string.lines.first[0...width].gsub(/\s\w+\s*$/, '...')
72
+ end
73
+
74
+ def handle_event(event)
75
+ case event[:type]
76
+ when :enable_default_output
77
+ @disable_depth -= 1
78
+ when :disable_default_output
79
+ @disable_depth += 1
80
+ when :message
81
+ print_message(event[:message])
82
+ when :verbose
83
+ print_message(event[:message]) if @verbose
84
+ end
85
+
86
+ if enabled?
87
+ case event[:type]
88
+ when :node_start
89
+ print_start(event[:target]) if @verbose
90
+ when :node_result
91
+ print_result(event[:result]) if @verbose
92
+ when :step_start
93
+ print_step_start(**event) if plan_logging?
94
+ when :step_finish
95
+ print_step_finish(**event) if plan_logging?
96
+ when :plan_start
97
+ print_plan_start(event)
98
+ when :plan_finish
99
+ print_plan_finish(event)
100
+ when :container_start
101
+ print_container_start(event) if plan_logging?
102
+ when :container_finish
103
+ print_container_finish(event) if plan_logging?
104
+ when :start_spin
105
+ start_spin
106
+ when :stop_spin
107
+ stop_spin
108
+ end
109
+ end
110
+ end
111
+
112
+ def enabled?
113
+ @disable_depth == 0
114
+ end
115
+
116
+ def plan_logging?
117
+ @plan_depth > 0
118
+ end
119
+
120
+ def print_start(target)
121
+ @stream.puts(colorize(:green, "Started on #{target.safe_name}..."))
122
+ end
123
+
124
+ def print_container_result(result)
125
+ if result.success?
126
+ @stream.puts(colorize(:green, "Finished running container #{result.object}:"))
127
+ else
128
+ @stream.puts(colorize(:red, "Failed running container #{result.object}:"))
129
+ end
130
+
131
+ if result.error_hash
132
+ @stream.puts(colorize(:red, remove_trail(indent(2, result.error_hash['msg']))))
133
+ return 0
134
+ end
135
+
136
+ # Only print results if there's something other than empty string and hash
137
+ safe_value = result.safe_value
138
+ if safe_value['stdout'].strip.empty? && safe_value['stderr'].strip.empty?
139
+ @stream.puts(indent(2, "Running container #{result.object} completed successfully with no result"))
140
+ else
141
+ unless safe_value['stdout'].strip && safe_value['stdout'].strip.empty?
142
+ @stream.puts(indent(2, "STDOUT:"))
143
+ @stream.puts(indent(4, safe_value['stdout']))
144
+ end
145
+ unless safe_value['stderr'].strip.empty?
146
+ @stream.puts(indent(2, "STDERR:"))
147
+ @stream.puts(indent(4, safe_value['stderr']))
148
+ end
149
+ end
150
+ end
151
+
152
+ def print_result(result)
153
+ if result.success?
154
+ @stream.puts(colorize(:green, "Finished on #{result.target.safe_name}:"))
155
+ else
156
+ @stream.puts(colorize(:red, "Failed on #{result.target.safe_name}:"))
157
+ end
158
+
159
+ if result.error_hash
160
+ @stream.puts(colorize(:red, remove_trail(indent(2, result.error_hash['msg']))))
161
+ end
162
+
163
+ if result.is_a?(Bolt::ApplyResult) && @verbose
164
+ result.resource_logs.each do |log|
165
+ # Omit low-level info/debug messages
166
+ next if %w[info debug].include?(log['level'])
167
+ message = format_log(log)
168
+ @stream.puts(indent(2, message))
169
+ end
170
+ end
171
+
172
+ # Only print results if there's something other than empty string and hash
173
+ if result.value.empty? || (result.value.keys == ['_output'] && !result.message?)
174
+ @stream.puts(indent(2, "#{result.action.capitalize} completed successfully with no result"))
175
+ else
176
+ # Only print messages that have something other than whitespace
177
+ if result.message?
178
+ @stream.puts(remove_trail(indent(2, result.message)))
179
+ end
180
+ case result.action
181
+ when 'command', 'script'
182
+ safe_value = result.safe_value
183
+ if safe_value["merged_output"]
184
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
185
+
186
+ else # output stdout or stderr
187
+ unless safe_value['stdout'].nil? || safe_value['stdout'].strip.empty?
188
+ @stream.puts(indent(2, "STDOUT:"))
189
+ @stream.puts(indent(4, safe_value['stdout']))
190
+ end
191
+ unless safe_value['stderr'].nil? || safe_value['stderr'].strip.empty?
192
+ @stream.puts(indent(2, "STDERR:"))
193
+ @stream.puts(indent(4, safe_value['stderr']))
194
+ end
195
+ end
196
+ when 'lookup'
197
+ @stream.puts(indent(2, Bolt::Util::Format.stringify(result['value'])))
198
+ else
199
+ if result.generic_value.any?
200
+ @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ def format_log(log)
207
+ color = case log['level']
208
+ when 'warn'
209
+ :yellow
210
+ when 'err'
211
+ :red
212
+ end
213
+ source = "#{log['source']}: " if log['source']
214
+ message = "#{log['level'].capitalize}: #{source}#{log['message']}"
215
+ message = colorize(color, message) if color
216
+ message
217
+ end
218
+
219
+ def print_step_start(description:, targets:, **_kwargs)
220
+ target_str = if targets.length > 5
221
+ "#{targets.count} targets"
222
+ else
223
+ targets.map(&:safe_name).join(', ')
224
+ end
225
+ @stream.puts(colorize(:green, "Starting: #{description} on #{target_str}"))
226
+ end
227
+
228
+ def print_step_finish(description:, result:, duration:, **_kwargs)
229
+ failures = result.error_set.length
230
+ plural = failures == 1 ? '' : 's'
231
+ message = "Finished: #{description} with #{failures} failure#{plural} in #{duration.round(2)} sec"
232
+ @stream.puts(colorize(:green, message))
233
+ end
234
+
235
+ def print_container_start(image:, **_kwargs)
236
+ @stream.puts(colorize(:green, "Starting: run container '#{image}'"))
237
+ end
238
+
239
+ def print_container_finish(event)
240
+ result = if event[:result].is_a?(Bolt::ContainerFailure)
241
+ event[:result].result
242
+ else
243
+ event[:result]
244
+ end
245
+
246
+ if result.success?
247
+ @stream.puts(colorize(:green, "Finished: run container '#{result.object}' succeeded."))
248
+ else
249
+ @stream.puts(colorize(:red, "Finished: run container '#{result.object}' failed."))
250
+ end
251
+ print_container_result(result) if @verbose
252
+ end
253
+
254
+ def print_plan_start(event)
255
+ @plan_depth += 1
256
+ # We use this event to both mark the start of a plan _and_ to enable
257
+ # plan logging for `apply`, so only log the message if we were called
258
+ # with a plan
259
+ if event[:plan]
260
+ @stream.puts(colorize(:green, "Starting: plan #{event[:plan]}"))
261
+ end
262
+ end
263
+
264
+ def print_plan_finish(event)
265
+ @plan_depth -= 1
266
+ plan = event[:plan]
267
+ duration = event[:duration]
268
+ @stream.puts(colorize(:green, "Finished: plan #{plan} in #{duration_to_string(duration)}"))
269
+ end
270
+
271
+ def print_summary(results, elapsed_time = nil)
272
+ ok_set = results.ok_set
273
+ unless ok_set.empty?
274
+ @stream.puts format('Successful on %<size>d target%<plural>s: %<names>s',
275
+ size: ok_set.size,
276
+ plural: ok_set.size == 1 ? '' : 's',
277
+ names: ok_set.targets.map(&:safe_name).join(','))
278
+ end
279
+
280
+ error_set = results.error_set
281
+ unless error_set.empty?
282
+ @stream.puts colorize(:red,
283
+ format('Failed on %<size>d target%<plural>s: %<names>s',
284
+ size: error_set.size,
285
+ plural: error_set.size == 1 ? '' : 's',
286
+ names: error_set.targets.map(&:safe_name).join(',')))
287
+ end
288
+
289
+ total_msg = format('Ran on %<size>d target%<plural>s',
290
+ size: results.size,
291
+ plural: results.size == 1 ? '' : 's')
292
+ total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
293
+ @stream.puts total_msg
294
+ end
295
+
296
+ def format_table(results, padding_left = 0, padding_right = 3)
297
+ # lazy-load expensive gem code
298
+ require 'terminal-table'
299
+
300
+ Terminal::Table.new(
301
+ rows: results,
302
+ style: {
303
+ border_x: '',
304
+ border_y: '',
305
+ border_i: '',
306
+ padding_left: padding_left,
307
+ padding_right: padding_right,
308
+ border_top: false,
309
+ border_bottom: false
310
+ }
311
+ )
312
+ end
313
+
314
+ # List available tasks.
315
+ #
316
+ # @param tasks [Array] A list of task names and descriptions.
317
+ # @param modulepath [Array] The modulepath.
318
+ #
319
+ def print_tasks(tasks:, modulepath:)
320
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
321
+
322
+ tasks = tasks.map do |name, description|
323
+ description = truncate(description, 72)
324
+ [name, description]
325
+ end
326
+
327
+ @stream.puts colorize(:cyan, 'Tasks')
328
+ @stream.puts tasks.any? ? format_table(tasks, 2) : indent(2, 'No available tasks')
329
+ @stream.puts
330
+
331
+ @stream.puts colorize(:cyan, 'Modulepath')
332
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
333
+ @stream.puts
334
+
335
+ @stream.puts colorize(:cyan, 'Additional information')
336
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
337
+ end
338
+
339
+ # Print information about a task.
340
+ #
341
+ # @param task [Bolt::Task] The task information.
342
+ #
343
+ def print_task_info(task:)
344
+ params = (task.parameters || []).sort
345
+
346
+ info = +''
347
+
348
+ # Add task name and description
349
+ info << colorize(:cyan, "#{task.name}\n")
350
+ info << if task.description
351
+ indent(2, task.description.chomp)
352
+ else
353
+ indent(2, 'No description available.')
354
+ end
355
+ info << "\n\n"
356
+
357
+ # Build usage string
358
+ usage = +''
359
+ usage << if Bolt::Util.powershell?
360
+ "Invoke-BoltTask -Name #{task.name} -Targets <targets>"
361
+ else
362
+ "bolt task run #{task.name} --targets <targets>"
363
+ end
364
+ usage << (Bolt::Util.powershell? ? ' [-Noop]' : ' [--noop]') if task.supports_noop
365
+ params.each do |name, data|
366
+ usage << if data['type']&.start_with?('Optional') || data.key?('default')
367
+ " [#{name}=<value>]"
368
+ else
369
+ " #{name}=<value>"
370
+ end
371
+ end
372
+
373
+ # Add usage
374
+ info << colorize(:cyan, "Usage\n")
375
+ info << indent(2, wrap(usage))
376
+ info << "\n"
377
+
378
+ # Add parameters, if any
379
+ if params.any?
380
+ info << colorize(:cyan, "Parameters\n")
381
+ params.each do |name, data|
382
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'] || 'Any')}\n")
383
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
384
+ info << indent(4, "Default: #{data['default'].inspect}\n") if data.key?('default')
385
+ info << "\n"
386
+ end
387
+ end
388
+
389
+ # Add module location
390
+ path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
391
+ info << colorize(:cyan, "Module\n")
392
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
393
+ indent(2, 'built-in module')
394
+ else
395
+ indent(2, path)
396
+ end
397
+
398
+ @stream.puts info
399
+ end
400
+
401
+ # @param [Hash] plan A hash representing the plan
402
+ def print_plan_info(plan)
403
+ params = plan['parameters'].sort
404
+
405
+ info = +''
406
+
407
+ # Add plan name and description
408
+ info << colorize(:cyan, "#{plan['name']}\n")
409
+
410
+ description = +''
411
+ description << "#{plan['summary']}\n\n" if plan['summary']
412
+ description << plan['docstring'] if plan['docstring']
413
+
414
+ info << if description.empty?
415
+ indent(2, 'No description available.')
416
+ else
417
+ indent(2, description.strip)
418
+ end
419
+ info << "\n\n"
420
+
421
+ # Build the usage string
422
+ usage = +''
423
+ usage << if Bolt::Util.powershell?
424
+ "Invoke-BoltPlan -Name #{plan['name']}"
425
+ else
426
+ "bolt plan run #{plan['name']}"
427
+ end
428
+ params.each do |name, data|
429
+ usage << (data.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
430
+ end
431
+
432
+ # Add usage
433
+ info << colorize(:cyan, "Usage\n")
434
+ info << indent(2, wrap(usage))
435
+ info << "\n"
436
+
437
+ # Add parameters, if any
438
+ if params.any?
439
+ info << colorize(:cyan, "Parameters\n")
440
+
441
+ params.each do |name, data|
442
+ info << indent(2, "#{colorize(:yellow, name)} #{colorize(:dim, data['type'])}\n")
443
+ info << indent(4, "#{wrap(data['description']).chomp}\n") if data['description']
444
+ info << indent(4, "Default: #{data['default_value']}\n") unless data['default_value'].nil?
445
+ info << "\n"
446
+ end
447
+ end
448
+
449
+ # Add module location
450
+ info << colorize(:cyan, "Module\n")
451
+ info << if plan['module'].start_with?(Bolt::Config::Modulepath::MODULES_PATH)
452
+ indent(2, 'built-in module')
453
+ else
454
+ indent(2, plan['module'])
455
+ end
456
+
457
+ @stream.puts info
458
+ end
459
+
460
+ def print_plans(plans:, modulepath:)
461
+ command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
462
+
463
+ plans = plans.map do |name, description|
464
+ description = truncate(description, 72)
465
+ [name, description]
466
+ end
467
+
468
+ @stream.puts colorize(:cyan, 'Plans')
469
+ @stream.puts plans.any? ? format_table(plans, 2) : indent(2, 'No available plans')
470
+ @stream.puts
471
+
472
+ @stream.puts colorize(:cyan, 'Modulepath')
473
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
474
+ @stream.puts
475
+
476
+ @stream.puts colorize(:cyan, 'Additional information')
477
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
478
+ end
479
+
480
+ # Print available guide topics.
481
+ #
482
+ # @param topics [Array] The available topics.
483
+ #
484
+ def print_topics(topics:, **_kwargs)
485
+ info = +"#{colorize(:cyan, 'Topics')}\n"
486
+ info << indent(2, topics.join("\n"))
487
+ info << "\n\n#{colorize(:cyan, 'Additional information')}\n"
488
+ info << indent(2, "Use 'bolt guide <TOPIC>' to view a specific guide.")
489
+ @stream.puts info
490
+ end
491
+
492
+ # Print the guide for the specified topic.
493
+ #
494
+ # @param guide [String] The guide.
495
+ #
496
+ def print_guide(topic:, guide:, documentation: nil, **_kwargs)
497
+ info = +"#{colorize(:cyan, topic)}\n"
498
+ info << indent(2, guide)
499
+
500
+ if documentation
501
+ info << "\n#{colorize(:cyan, 'Documentation')}\n"
502
+ info << indent(2, documentation.join("\n"))
503
+ end
504
+
505
+ @stream.puts info
506
+ end
507
+
508
+ def print_plan_lookup(value)
509
+ @stream.puts(value)
510
+ end
511
+
512
+ def print_module_list(module_list)
513
+ info = +''
514
+
515
+ module_list.each do |path, modules|
516
+ info << if (mod = modules.find { |m| m[:internal_module_group] })
517
+ colorize(:cyan, mod[:internal_module_group])
518
+ else
519
+ colorize(:cyan, path)
520
+ end
521
+
522
+ info << "\n"
523
+
524
+ if modules.empty?
525
+ info << '(no modules installed)'
526
+ else
527
+ module_info = modules.map do |m|
528
+ version = if m[:version].nil?
529
+ m[:internal_module_group].nil? ? '(no metadata)' : '(built-in)'
530
+ else
531
+ m[:version]
532
+ end
533
+
534
+ [m[:name], version]
535
+ end
536
+
537
+ info << format_table(module_info, 2, 1).to_s
538
+ end
539
+
540
+ info << "\n\n"
541
+ end
542
+
543
+ command = Bolt::Util.powershell? ? 'Get-BoltModule -Name <MODULE>' : 'bolt module show <MODULE>'
544
+ info << colorize(:cyan, "Additional information\n")
545
+ info << indent(2, "Use '#{command}' to view details for a specific module.")
546
+
547
+ @stream.puts info
548
+ end
549
+
550
+ # Prints detailed module information.
551
+ #
552
+ # @param name [String] The module's short name.
553
+ # @param metadata [Hash] The module's metadata.
554
+ # @param path [String] The path to the module.
555
+ # @param plans [Array] The module's plans.
556
+ # @param tasks [Array] The module's tasks.
557
+ #
558
+ def print_module_info(name:, metadata:, path:, plans:, tasks:, **_kwargs)
559
+ info = +''
560
+
561
+ info << colorize(:cyan, name)
562
+
563
+ info << colorize(:dim, " [#{metadata['version']}]") if metadata['version']
564
+ info << "\n"
565
+
566
+ info << if metadata['summary']
567
+ indent(2, wrap(metadata['summary'].strip, 76))
568
+ else
569
+ indent(2, "No description available.\n")
570
+ end
571
+ info << "\n"
572
+
573
+ if tasks.any?
574
+ length = tasks.map(&:first).map(&:length).max
575
+ data = tasks.map { |task, desc| [task, truncate(desc, 76 - length)] }
576
+ info << colorize(:cyan, "Tasks\n")
577
+ info << format_table(data, 2).to_s
578
+ info << "\n\n"
579
+ end
580
+
581
+ if plans.any?
582
+ length = plans.map(&:first).map(&:length).max
583
+ data = plans.map { |plan, desc| [plan, truncate(desc, 76 - length)] }
584
+ info << colorize(:cyan, "Plans\n")
585
+ info << format_table(data, 2).to_s
586
+ info << "\n\n"
587
+ end
588
+
589
+ if metadata['operatingsystem_support']&.any?
590
+ supported = metadata['operatingsystem_support'].map do |os|
591
+ [os['operatingsystem'], os['operatingsystemrelease']&.join(', ')]
592
+ end
593
+
594
+ info << colorize(:cyan, "Operating system support\n")
595
+ info << format_table(supported, 2).to_s
596
+ info << "\n\n"
597
+ end
598
+
599
+ if metadata['dependencies']&.any?
600
+ dependencies = metadata['dependencies'].map do |dep|
601
+ [dep['name'], dep['version_requirement']]
602
+ end
603
+
604
+ info << colorize(:cyan, "Dependencies\n")
605
+ info << format_table(dependencies, 2).to_s
606
+ info << "\n\n"
607
+ end
608
+
609
+ info << colorize(:cyan, "Path\n")
610
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH) ||
611
+ path.start_with?(Bolt::Config::Modulepath::BOLTLIB_PATH)
612
+ indent(2, 'built-in module')
613
+ else
614
+ indent(2, path)
615
+ end
616
+
617
+ @stream.puts info
618
+ end
619
+
620
+ def print_plugin_list(plugins:, modulepath:)
621
+ info = +''
622
+ length = plugins.values.map(&:keys).flatten.map(&:length).max + 4
623
+
624
+ plugins.each do |hook, plugin|
625
+ next if plugin.empty?
626
+ next if hook == :validate_resolve_reference
627
+
628
+ info << colorize(:cyan, "#{hook}\n")
629
+
630
+ plugin.each do |name, description|
631
+ info << indent(2, name.ljust(length))
632
+ info << truncate(description, 80 - length) if description
633
+ info << "\n"
634
+ end
635
+
636
+ info << "\n"
637
+ end
638
+
639
+ info << colorize(:cyan, "Modulepath\n")
640
+ info << indent(2, "#{modulepath.join(File::PATH_SEPARATOR)}\n\n")
641
+
642
+ info << colorize(:cyan, "Additional information\n")
643
+ info << indent(2, "For more information about using plugins see https://pup.pt/bolt-plugins")
644
+
645
+ @stream.puts info.chomp
646
+ end
647
+
648
+ def print_new_plan(name:, path:)
649
+ if Bolt::Util.powershell?
650
+ show_command = 'Get-BoltPlan -Name '
651
+ run_command = 'Invoke-BoltPlan -Name '
652
+ else
653
+ show_command = 'bolt plan show'
654
+ run_command = 'bolt plan run'
655
+ end
656
+
657
+ print_message(<<~OUTPUT)
658
+ Created plan '#{name}' at '#{path}'
659
+
660
+ Show this plan with:
661
+ #{show_command} #{name}
662
+ Run this plan with:
663
+ #{run_command} #{name}
664
+ OUTPUT
665
+ end
666
+
667
+ def print_new_policy(name:, path:)
668
+ if Bolt::Util.powershell?
669
+ apply_command = "Invoke-BoltPolicy -Name #{name} -Targets <TARGETS>"
670
+ show_command = 'Get-BoltPolicy'
671
+ else
672
+ apply_command = "bolt policy apply #{name} --targets <TARGETS>"
673
+ show_command = 'bolt policy show'
674
+ end
675
+
676
+ print_message(<<~OUTPUT)
677
+ Created policy '#{name}' at '#{path}'
678
+
679
+ Apply this policy with:
680
+ #{apply_command}
681
+ Show available policies with:
682
+ #{show_command}
683
+ OUTPUT
684
+ end
685
+
686
+ # Print policies and the modulepath they are loaded from.
687
+ #
688
+ # @param policies [Array] The list of available policies.
689
+ # @param modulepath [Array] The project's modulepath.
690
+ #
691
+ def print_policy_list(policies:, modulepath:)
692
+ info = +''
693
+
694
+ info << colorize(:cyan, "Policies\n")
695
+
696
+ if policies.any?
697
+ policies.sort.each { |policy| info << indent(2, "#{policy}\n") }
698
+ else
699
+ info << indent(2, "No available policies\n")
700
+ end
701
+
702
+ info << "\n"
703
+
704
+ info << colorize(:cyan, "Modulepath\n")
705
+ info << indent(2, modulepath.join(File::PATH_SEPARATOR).to_s)
706
+
707
+ @stream.puts info.chomp
708
+ end
709
+
710
+ # Print target names and where they came from.
711
+ #
712
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
713
+ # @param inventory [Hash] Targets provided from the inventory.
714
+ # @param flag [Boolean] Whether a targeting command-line option was used.
715
+ #
716
+ def print_targets(adhoc:, inventory:, flag:, **_kwargs)
717
+ adhoc_text = colorize(:yellow, "(Not found in inventory file)")
718
+
719
+ targets = []
720
+ targets += inventory[:targets].map { |target| [target['name'], nil] }
721
+ targets += adhoc[:targets].map { |target| [target['name'], adhoc_text] }
722
+
723
+ info = +''
724
+
725
+ # Add target list
726
+ info << colorize(:cyan, "Targets\n")
727
+ info << if targets.any?
728
+ format_table(targets, 2, 2).to_s
729
+ else
730
+ indent(2, 'No targets')
731
+ end
732
+ info << "\n\n"
733
+
734
+ info << format_inventory_source(inventory[:file], inventory[:default])
735
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, false)
736
+
737
+ @stream.puts info
738
+ end
739
+
740
+ # Print detailed target information.
741
+ #
742
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
743
+ # @param inventory [Hash] Targets provided from the inventory.
744
+ # @param flag [Boolean] Whether a targeting command-line option was used.
745
+ #
746
+ def print_target_info(adhoc:, inventory:, flag:, **_kwargs)
747
+ targets = (adhoc[:targets] + inventory[:targets]).sort_by { |t| t['name'] }
748
+
749
+ info = +''
750
+
751
+ if targets.any?
752
+ adhoc_text = colorize(:yellow, " (Not found in inventory file)")
753
+
754
+ targets.each do |target|
755
+ info << colorize(:cyan, target['name'])
756
+ info << adhoc_text if adhoc[:targets].include?(target)
757
+ info << "\n"
758
+ info << indent(2, target.to_yaml.lines.drop(1).join)
759
+ info << "\n"
760
+ end
761
+ else
762
+ info << colorize(:cyan, "Targets\n")
763
+ info << indent(2, "No targets\n\n")
764
+ end
765
+
766
+ info << format_inventory_source(inventory[:file], inventory[:default])
767
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, true)
768
+
769
+ @stream.puts info
770
+ end
771
+
772
+ private def format_inventory_source(inventory_source, default_inventory)
773
+ info = +''
774
+
775
+ # Add inventory file source
776
+ info << colorize(:cyan, "Inventory source\n")
777
+ info << if inventory_source
778
+ indent(2, "#{inventory_source}\n")
779
+ else
780
+ indent(2, wrap("Tried to load inventory from #{default_inventory}, but the file does not exist\n"))
781
+ end
782
+ info << "\n"
783
+ end
784
+
785
+ private def format_target_summary(inventory_count, adhoc_count, target_flag, detail_flag)
786
+ info = +''
787
+
788
+ # Add target count summary
789
+ count = "#{inventory_count + adhoc_count} total, "\
790
+ "#{inventory_count} from inventory, "\
791
+ "#{adhoc_count} adhoc"
792
+ info << colorize(:cyan, "Target count\n")
793
+ info << indent(2, count)
794
+
795
+ # Add filtering information
796
+ unless target_flag && detail_flag
797
+ info << colorize(:cyan, "\n\nAdditional information\n")
798
+
799
+ unless target_flag
800
+ opt = Bolt::Util.windows? ? "'-Targets', '-Query', or '-Rerun'" : "'--targets', '--query', or '--rerun'"
801
+ info << indent(2, "Use the #{opt} option to view specific targets\n")
802
+ end
803
+
804
+ unless detail_flag
805
+ opt = Bolt::Util.windows? ? '-Detail' : '--detail'
806
+ info << indent(2, "Use the '#{opt}' option to view target configuration and data")
807
+ end
808
+ end
809
+
810
+ info
811
+ end
812
+
813
+ # Print inventory group information.
814
+ #
815
+ # @param count [Integer] Number of groups in the inventory.
816
+ # @param groups [Array] Names of groups in the inventory.
817
+ # @param inventory [Hash] Where the inventory was loaded from.
818
+ #
819
+ def print_groups(count:, groups:, inventory:)
820
+ info = +''
821
+
822
+ # Add group list
823
+ info << colorize(:cyan, "Groups\n")
824
+ info << indent(2, groups.join("\n"))
825
+ info << "\n\n"
826
+
827
+ # Add inventory file source
828
+ info << format_inventory_source(inventory[:source], inventory[:default])
829
+
830
+ # Add group count summary
831
+ info << colorize(:cyan, "Group count\n")
832
+ info << indent(2, "#{count} total")
833
+
834
+ @stream.puts info
835
+ end
836
+
837
+ # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
838
+ def print_apply_result(apply_result)
839
+ print_summary(apply_result, apply_result.elapsed_time)
840
+ end
841
+
842
+ # @param [Bolt::PlanResult] plan_result A PlanResult object
843
+ def print_plan_result(plan_result)
844
+ value = plan_result.value
845
+ case value
846
+ when nil
847
+ @stream.puts("Plan completed successfully with no result")
848
+ when Bolt::ApplyFailure, Bolt::RunFailure
849
+ print_result_set(value.result_set)
850
+ when Bolt::ContainerResult
851
+ print_container_result(value)
852
+ when Bolt::ContainerFailure
853
+ print_container_result(value.result)
854
+ when Bolt::ResultSet
855
+ print_result_set(value)
856
+ when Bolt::Result
857
+ print_result(value)
858
+ when Bolt::ApplyResult
859
+ print_apply_result(value)
860
+ when Bolt::Error
861
+ print_bolt_error(**value.to_h.transform_keys(&:to_sym))
862
+ else
863
+ @stream.puts(::JSON.pretty_generate(plan_result, quirks_mode: true))
864
+ end
865
+ end
866
+
867
+ def print_result_set(result_set)
868
+ result_set.each { |result| print_result(result) }
869
+ print_summary(result_set)
870
+ end
871
+
872
+ def print_puppetfile_result(success, puppetfile, moduledir)
873
+ if success
874
+ @stream.puts("Successfully synced modules from #{puppetfile} to #{moduledir}")
875
+ else
876
+ @stream.puts(colorize(:red, "Failed to sync modules from #{puppetfile} to #{moduledir}"))
877
+ end
878
+ end
879
+
880
+ def fatal_error(err)
881
+ @stream.puts(colorize(:red, err.message))
882
+ if err.is_a? Bolt::RunFailure
883
+ @stream.puts ::JSON.pretty_generate(err.result_set)
884
+ end
885
+
886
+ if @trace && err.backtrace
887
+ err.backtrace.each do |line|
888
+ @stream.puts(colorize(:red, "\t#{line}"))
889
+ end
890
+ end
891
+ end
892
+
893
+ def print_message(message)
894
+ @stream.puts(Bolt::Util::Format.stringify(message))
895
+ end
896
+
897
+ def print_error(message)
898
+ @stream.puts(colorize(:red, message))
899
+ end
900
+
901
+ def print_bolt_error(msg:, details:, **_kwargs)
902
+ err = msg
903
+ if (f = details[:file])
904
+ err += "\n (file: #{f}"
905
+ err += ", line: #{details[:line]}" if details[:line]
906
+ err += ", column: #{details[:column]}" if details[:column]
907
+ err += ")"
908
+ end
909
+ @stream.puts(colorize(:red, err))
910
+ end
911
+
912
+ def print_prompt(prompt)
913
+ @stream.print(colorize(:cyan, indent(4, prompt)))
914
+ end
915
+
916
+ def print_prompt_error(message)
917
+ @stream.puts(colorize(:red, indent(4, message)))
918
+ end
919
+
920
+ def print_action_step(step)
921
+ first, *remaining = wrap(step, 76).lines
922
+
923
+ first = indent(2, "→ #{first}")
924
+ remaining = remaining.map { |line| indent(4, line) }
925
+ step = [first, *remaining, "\n"].join
926
+
927
+ @stream.puts(step)
928
+ end
929
+
930
+ def print_action_error(error)
931
+ # Running everything through 'wrap' messes with newlines. Separating
932
+ # into lines and wrapping each individually ensures separate errors are
933
+ # distinguishable.
934
+ first, *remaining = error.lines
935
+ first = colorize(:red, indent(2, "→ #{wrap(first, 76)}"))
936
+ wrapped = remaining.map { |l| wrap(l) }
937
+ to_print = wrapped.map { |line| colorize(:red, indent(4, line)) }
938
+ step = [first, *to_print, "\n"].join
939
+
940
+ @stream.puts(step)
941
+ end
942
+
943
+ def duration_to_string(duration)
944
+ hrs = (duration / 3600).floor
945
+ mins = ((duration % 3600) / 60).floor
946
+ secs = (duration % 60)
947
+ if hrs > 0
948
+ "#{hrs} hr, #{mins} min, #{secs.round} sec"
949
+ elsif mins > 0
950
+ "#{mins} min, #{secs.round} sec"
951
+ else
952
+ # Include 2 decimal places if the duration is under a minute
953
+ "#{secs.round(2)} sec"
954
+ end
955
+ end
956
+ end
957
+ end
958
+ end