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
data/lib/bolt/util.rb ADDED
@@ -0,0 +1,465 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ module Util
5
+ class << self
6
+ # Gets input for an argument.
7
+ def get_arg_input(value)
8
+ if value.start_with?('@')
9
+ file = value.sub(/^@/, '')
10
+ read_arg_file(file)
11
+ elsif value == '-'
12
+ $stdin.read
13
+ else
14
+ value
15
+ end
16
+ end
17
+
18
+ # Reads a file passed as an argument to a command.
19
+ def read_arg_file(file)
20
+ File.read(File.expand_path(file))
21
+ rescue StandardError => e
22
+ raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
23
+ end
24
+
25
+ def read_json_file(path, filename)
26
+ require 'json'
27
+
28
+ logger = Bolt::Logger.logger(self)
29
+ path = File.expand_path(path)
30
+ content = JSON.parse(File.read(path))
31
+ logger.trace("Loaded #{filename} from #{path}")
32
+ content
33
+ rescue Errno::ENOENT
34
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}", path)
35
+ rescue JSON::ParserError => e
36
+ msg = "Unable to parse #{filename} file at #{path} as JSON: #{e.message}"
37
+ raise Bolt::FileError.new(msg, path)
38
+ rescue IOError, SystemCallError => e
39
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}\n#{e.message}",
40
+ path)
41
+ end
42
+
43
+ def read_optional_json_file(path, file_name)
44
+ File.exist?(path) && !File.zero?(path) ? read_yaml_hash(path, file_name) : {}
45
+ end
46
+
47
+ def read_yaml_hash(path, file_name)
48
+ require 'yaml'
49
+
50
+ logger = Bolt::Logger.logger(self)
51
+ path = File.expand_path(path)
52
+ content = File.open(path, "r:UTF-8") { |f| YAML.safe_load(f.read) } || {}
53
+ unless content.is_a?(Hash)
54
+ raise Bolt::FileError.new(
55
+ "Invalid content for #{file_name} file at #{path}\nContent should be a Hash or empty, "\
56
+ "not #{content.class}",
57
+ path
58
+ )
59
+ end
60
+ logger.trace("Loaded #{file_name} from #{path}")
61
+ content
62
+ rescue Errno::ENOENT
63
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}", path)
64
+ rescue Psych::SyntaxError => e
65
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}, line #{e.line}, "\
66
+ "column #{e.column}\n#{e.problem}",
67
+ path)
68
+ rescue Psych::BadAlias => e
69
+ raise Bolt::FileError.new('Bolt does not support the use of aliases in YAML files. Alias '\
70
+ "detected in #{file_name} file at #{path}\n#{e.message}", path)
71
+ rescue Psych::Exception => e
72
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}\n#{e.message}",
73
+ path)
74
+ rescue IOError, SystemCallError => e
75
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}\n#{e.message}",
76
+ path)
77
+ end
78
+
79
+ def read_optional_yaml_hash(path, file_name)
80
+ File.exist?(path) ? read_yaml_hash(path, file_name) : {}
81
+ end
82
+
83
+ def first_runs_free
84
+ # If this fails, use the system path instead
85
+ FileUtils.mkdir_p(Bolt::Config.user_path)
86
+ Bolt::Config.user_path + '.first_runs_free'
87
+ rescue StandardError
88
+ begin
89
+ # If using the system path fails, then don't bother with the welcome
90
+ # message
91
+ FileUtils.mkdir_p(Bolt::Config.system_path)
92
+ Bolt::Config.system_path + '.first_runs_free'
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ end
97
+
98
+ def first_run?
99
+ !first_runs_free.nil? &&
100
+ !File.exist?(first_runs_free)
101
+ end
102
+
103
+ # If Puppet is loaded, we aleady have the path to the module and should
104
+ # just get it. This takes the path to a file provided by the user and a
105
+ # Puppet Parser scope object and tries to find the file, either as an
106
+ # absolute path or Puppet module syntax lookup. Returns the path to the
107
+ # file if found, or nil.
108
+ #
109
+ def find_file_from_scope(file, scope)
110
+ # If we got an absolute path, just return that.
111
+ return file if Pathname.new(file).absolute?
112
+
113
+ module_name, file_pattern = Bolt::Util.split_path(file)
114
+ # Get the absolute path to the module root from the scope
115
+ mod_path = scope.compiler.environment.module(module_name)&.path
116
+
117
+ # Search the module for the file, falling back to new-style paths if enabled.
118
+ search_module(mod_path, file_pattern) if mod_path
119
+ end
120
+
121
+ # This searches a module for files under 'files/' or 'scripts/', falling
122
+ # back to the new style of file loading. It takes the absolute path to the
123
+ # module root and the relative path provided by the user.
124
+ #
125
+ def search_module(module_path, module_file)
126
+ if File.exist?(File.join(module_path, 'files', module_file))
127
+ File.join(module_path, 'files', module_file)
128
+ elsif File.exist?(File.join(module_path, module_file))
129
+ File.join(module_path, module_file)
130
+ end
131
+ end
132
+ alias find_file_in_module search_module
133
+
134
+ # Copied directly from puppet/lib/puppet/parser/files.rb
135
+ #
136
+ def split_path(path)
137
+ path.split(File::SEPARATOR, 2)
138
+ end
139
+
140
+ # Accepts a path with either 'plans' or 'tasks' in it and determines
141
+ # the name of the module
142
+ def module_name(path)
143
+ # Remove extra dots and slashes
144
+ path = Pathname.new(path).cleanpath.to_s
145
+ fs = File::SEPARATOR
146
+ regex = Regexp.new("#{fs}plans#{fs}|#{fs}tasks#{fs}")
147
+
148
+ # Only accept paths with '/plans/' or '/tasks/'
149
+ unless path.match?(regex)
150
+ msg = "Could not determine module from #{path}. "\
151
+ "The path must include 'plans' or 'tasks' directory"
152
+ raise Bolt::Error.new(msg, 'bolt/modulepath-error')
153
+ end
154
+
155
+ # Split the path on the first instance of /plans/ or /tasks/
156
+ parts = path.split(regex, 2)
157
+ # Module name is the last entry before 'plans' or 'tasks'
158
+ modulename = parts[0].split(fs)[-1]
159
+ filename = File.basename(path).split('.')[0]
160
+ # Remove "/init.*" if filename is init or just remove the file
161
+ # extension
162
+ if filename == 'init'
163
+ parts[1].chomp!(File.basename(path))
164
+ else
165
+ parts[1].chomp!(File.extname(path))
166
+ end
167
+
168
+ # The plan or task name is the rest of the path
169
+ [modulename, parts[1].split(fs)].flatten.join('::')
170
+ end
171
+
172
+ def to_code(string)
173
+ case string
174
+ when Bolt::PAL::YamlPlan::DoubleQuotedString
175
+ string.value.inspect
176
+ when Bolt::PAL::YamlPlan::BareString
177
+ if string.value.start_with?('$')
178
+ string.value.to_s
179
+ else
180
+ "'#{string.value}'"
181
+ end
182
+ when Bolt::PAL::YamlPlan::EvaluableString, Bolt::PAL::YamlPlan::CodeLiteral
183
+ string.value.to_s
184
+ when String
185
+ "'#{string}'"
186
+ when Hash
187
+ formatted = String.new("{")
188
+ string.each do |k, v|
189
+ formatted << "#{to_code(k)} => #{to_code(v)}, "
190
+ end
191
+ formatted.chomp!(", ")
192
+ formatted << "}"
193
+ formatted
194
+ when Array
195
+ formatted = String.new("[")
196
+ formatted << string.map { |str| to_code(str) }.join(', ')
197
+ formatted << "]"
198
+ formatted
199
+ else
200
+ string
201
+ end
202
+ end
203
+
204
+ def deep_merge(hash1, hash2)
205
+ recursive_merge = proc do |_key, h1, h2|
206
+ if h1.is_a?(Hash) && h2.is_a?(Hash)
207
+ h1.merge(h2, &recursive_merge)
208
+ else
209
+ h2
210
+ end
211
+ end
212
+ hash1.merge(hash2, &recursive_merge)
213
+ end
214
+
215
+ def deep_merge!(hash1, hash2)
216
+ recursive_merge = proc do |_key, h1, h2|
217
+ if h1.is_a?(Hash) && h2.is_a?(Hash)
218
+ h1.merge!(h2, &recursive_merge)
219
+ else
220
+ h2
221
+ end
222
+ end
223
+ hash1.merge!(hash2, &recursive_merge)
224
+ end
225
+
226
+ # Accepts a Data object and returns a copy with all hash keys
227
+ # modified by block. use &:to_s to stringify keys or &:to_sym to symbolize them
228
+ def walk_keys(data, &block)
229
+ case data
230
+ when Hash
231
+ data.each_with_object({}) do |(k, v), acc|
232
+ v = walk_keys(v, &block)
233
+ acc[yield(k)] = v
234
+ end
235
+ when Array
236
+ data.map { |v| walk_keys(v, &block) }
237
+ else
238
+ data
239
+ end
240
+ end
241
+
242
+ # Accepts a Data object and returns a copy with all hash and array values
243
+ # Arrays and hashes including the initial object are modified before
244
+ # their descendants are.
245
+ def walk_vals(data, skip_top = false, &block)
246
+ data = yield(data) unless skip_top
247
+ case data
248
+ when Hash
249
+ data.transform_values { |v| walk_vals(v, &block) }
250
+ when Array
251
+ data.map { |v| walk_vals(v, &block) }
252
+ else
253
+ data
254
+ end
255
+ end
256
+
257
+ # Accepts a Data object and returns a copy with all hash and array values
258
+ # modified by the given block. Descendants are modified before their
259
+ # parents.
260
+ def postwalk_vals(data, skip_top = false, &block)
261
+ new_data = case data
262
+ when Hash
263
+ data.transform_values { |v| postwalk_vals(v, &block) }
264
+ when Array
265
+ data.map { |v| postwalk_vals(v, &block) }
266
+ else
267
+ data
268
+ end
269
+ if skip_top
270
+ new_data
271
+ else
272
+ yield(new_data)
273
+ end
274
+ end
275
+
276
+ # Performs a deep_clone, using an identical copy if the cloned structure contains multiple
277
+ # references to the same object and prevents endless recursion.
278
+ # Credit to Jan Molic via https://github.com/rubyworks/facets/blob/master/LICENSE.txt
279
+ def deep_clone(obj, cloned = {})
280
+ return cloned[obj.object_id] if cloned.include?(obj.object_id)
281
+
282
+ # The `defined?` method will not reliably find the Java::JavaLang::CloneNotSupportedException constant
283
+ # presumably due to some sort of optimization that short-cuts doing a bunch of Java introspection.
284
+ # Java::JavaLang::<...> IS defining the constant (via const_missing or const_get magic perhaps) so
285
+ # it is safe to reference it in the error_types array when a JRuby interpreter is evaluating the code
286
+ # (detected by RUBY_PLATFORM == `java`). SO instead of conditionally adding the CloneNotSupportedException
287
+ # constant to the error_types array based on `defined?` detecting the Java::JavaLang constant it is added
288
+ # based on detecting a JRuby interpreter.
289
+ # TypeError handles unclonable Ruby ojbects (TrueClass, Fixnum, ...)
290
+ # CloneNotSupportedException handles uncloneable Java objects (JRuby only)
291
+ error_types = [TypeError]
292
+ error_types << Java::JavaLang::CloneNotSupportedException if RUBY_PLATFORM == 'java'
293
+
294
+ begin
295
+ # We can't recurse on frozen objects to populate them with cloned
296
+ # data. Instead we store the freeze-state of the original object,
297
+ # deep_clone, then set the cloned object to frozen if the original
298
+ # object was frozen
299
+ frozen = obj.frozen?
300
+ cl = begin
301
+ obj.clone(freeze: false)
302
+ # Some datatypes, such as FalseClass, can't be unfrozen. These
303
+ # aren't the types we recurse on, so we can leave them frozen
304
+ rescue ArgumentError => e
305
+ if e.message =~ /can't unfreeze/
306
+ obj.clone
307
+ else
308
+ raise e
309
+ end
310
+ end
311
+ rescue *error_types
312
+ cloned[obj.object_id] = obj
313
+ obj
314
+ else
315
+ cloned[obj.object_id] = cl
316
+ cloned[cl.object_id] = cl
317
+
318
+ case cl
319
+ when Hash
320
+ obj.each { |k, v| cl[k] = deep_clone(v, cloned) }
321
+ when Array
322
+ cl.collect! { |v| deep_clone(v, cloned) }
323
+ when Struct
324
+ obj.each_pair { |k, v| cl[k] = deep_clone(v, cloned) }
325
+ end
326
+
327
+ cl.instance_variables.each do |var|
328
+ v = cl.instance_variable_get(var)
329
+ v_cl = deep_clone(v, cloned)
330
+ cl.instance_variable_set(var, v_cl)
331
+ end
332
+
333
+ cl.freeze if frozen
334
+ cl
335
+ end
336
+ end
337
+
338
+ # This is stubbed for testing validate_file
339
+ def file_stat(path)
340
+ File.stat(File.expand_path(path))
341
+ end
342
+
343
+ def snake_name_to_class_name(snake_name)
344
+ snake_name.split('_').map(&:capitalize).join
345
+ end
346
+
347
+ def class_name_to_file_name(cls_name)
348
+ # Note this turns Bolt::CLI -> 'bolt/cli' not 'bolt/c_l_i'
349
+ # this won't handle Bolt::Inventory2Foo
350
+ cls_name.gsub(/([a-z])([A-Z])/, '\1_\2').gsub('::', '/').downcase
351
+ end
352
+
353
+ def validate_file(type, path, allow_dir = false)
354
+ stat = file_stat(path)
355
+
356
+ if !stat.readable?
357
+ raise Bolt::FileError.new("The #{type} '#{path}' is unreadable", path)
358
+ elsif !allow_dir && stat.directory?
359
+ expected = allow_dir ? 'file or directory' : 'file'
360
+ raise Bolt::FileError.new("The #{type} '#{path}' is not a #{expected}", path)
361
+ elsif stat.directory?
362
+ Dir.foreach(path) do |file|
363
+ next if %w[. ..].include?(file)
364
+ validate_file(type, File.join(path, file), allow_dir)
365
+ end
366
+ end
367
+ rescue Errno::ENOENT
368
+ raise Bolt::FileError.new("The #{type} '#{path}' does not exist", path)
369
+ end
370
+
371
+ # Returns true if windows false if not.
372
+ def windows?
373
+ !!File::ALT_SEPARATOR
374
+ end
375
+
376
+ # Returns true if running in PowerShell.
377
+ def powershell?
378
+ !!ENV['PSModulePath']
379
+ end
380
+
381
+ # Accept hash and return hash with top level keys of type "String" converted to symbols.
382
+ def symbolize_top_level_keys(hsh)
383
+ hsh.each_with_object({}) { |(k, v), h| k.is_a?(String) ? h[k.to_sym] = v : h[k] = v }
384
+ end
385
+
386
+ # Recursively searches a data structure for plugin references
387
+ def references?(input)
388
+ case input
389
+ when Hash
390
+ input.key?('_plugin') || input.values.any? { |v| references?(v) }
391
+ when Array
392
+ input.any? { |v| references?(v) }
393
+ else
394
+ false
395
+ end
396
+ end
397
+
398
+ # Executes a Docker CLI command. This is useful for running commands as
399
+ # part of this class without having to go through the `execute`
400
+ # function and manage pipes.
401
+ #
402
+ # @param cmd [String] The docker command and arguments to run
403
+ # e.g. 'cp <src> <dest>' for `docker cp <src> <dest>`
404
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
405
+ def exec_docker(cmd, env = {})
406
+ Open3.capture3(env, 'docker', *cmd, { binmode: true })
407
+ end
408
+
409
+ # Executes a Podman CLI command. This is useful for running commands as
410
+ # part of this class without having to go through the `execute`
411
+ # function and manage pipes.
412
+ #
413
+ # @param cmd [String] The podman command and arguments to run
414
+ # e.g. 'cp <src> <dest>' for `podman cp <src> <dest>`
415
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
416
+ def exec_podman(cmd, env = {})
417
+ Open3.capture3(env, 'podman', *cmd, { binmode: true })
418
+ end
419
+
420
+ # Formats a map of environment variables to be passed to a command that
421
+ # accepts repeated `--env` flags
422
+ #
423
+ # @param env_vars [Hash] A map of environment variables keys and their values
424
+ # @return [String]
425
+ def format_env_vars_for_cli(env_vars)
426
+ @env_vars = env_vars.each_with_object([]) do |(key, value), acc|
427
+ acc << "--env"
428
+ acc << "#{key}=#{value}"
429
+ end
430
+ end
431
+
432
+ def unix_basename(path)
433
+ raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
434
+ path.split('/').last
435
+ end
436
+
437
+ def windows_basename(path)
438
+ raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
439
+ path.split(%r{[/\\]}).last
440
+ end
441
+
442
+ # Prompts yes or no, returning true for yes and false for no.
443
+ #
444
+ def prompt_yes_no(prompt, outputter)
445
+ choices = {
446
+ 'y' => true,
447
+ 'yes' => true,
448
+ 'n' => false,
449
+ 'no' => false
450
+ }
451
+
452
+ loop do
453
+ outputter.print_prompt("#{prompt} ([y]es/[n]o) ")
454
+ response = $stdin.gets.to_s.downcase.chomp
455
+
456
+ if choices.key?(response)
457
+ return choices[response]
458
+ else
459
+ outputter.print_prompt_error("Invalid response, must pick [y]es or [n]o")
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end
465
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../bolt/error'
4
+
5
+ # This class validates config against a schema, raising an error that includes
6
+ # details about any invalid configuration.
7
+ #
8
+ module Bolt
9
+ class Validator
10
+ attr_reader :deprecations, :warnings
11
+
12
+ def initialize
13
+ @errors = []
14
+ @deprecations = []
15
+ @warnings = []
16
+ @path = []
17
+ end
18
+
19
+ # This is the entry method for validating data against the schema.
20
+ #
21
+ def validate(data, schema, location = nil)
22
+ @schema = schema
23
+ @location = location
24
+
25
+ validate_value(data, schema)
26
+
27
+ raise_error
28
+ end
29
+
30
+ # Raises a ValidationError if there are any errors. All error messages
31
+ # created during validation are concatenated into a single error
32
+ # message.
33
+ #
34
+ private def raise_error
35
+ return unless @errors.any?
36
+
37
+ message = "Invalid configuration"
38
+ message += " at #{@location}" if @location
39
+ message += ":\n"
40
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
41
+
42
+ raise Bolt::ValidationError, message
43
+ end
44
+
45
+ # Validate an individual value. This performs validation that is
46
+ # common to all values, including type validation. After validating
47
+ # the value's type, the value is passed off to an individual
48
+ # validation method for the value's type.
49
+ #
50
+ private def validate_value(value, definition, plugin_supported = false)
51
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
52
+ plugin_supported = definition[:_plugin] if definition.key?(:_plugin)
53
+
54
+ return if plugin_reference?(value, plugin_supported)
55
+ return unless valid_type?(value, definition)
56
+
57
+ case value
58
+ when Hash
59
+ validate_hash(value, definition, plugin_supported)
60
+ when Array
61
+ validate_array(value, definition, plugin_supported)
62
+ when String
63
+ validate_string(value, definition)
64
+ when Numeric
65
+ validate_number(value, definition)
66
+ end
67
+ end
68
+
69
+ # Validates a hash value, logging errors for any validations that fail.
70
+ # This will enumerate each key-value pair in the hash and validate each
71
+ # value individually.
72
+ #
73
+ private def validate_hash(value, definition, plugin_supported)
74
+ properties = definition[:properties] ? definition[:properties].keys : []
75
+
76
+ if definition[:properties] && definition[:additionalProperties].nil?
77
+ validate_keys(value.keys, properties)
78
+ end
79
+
80
+ if definition[:required] && (definition[:required] - value.keys).any?
81
+ missing = definition[:required] - value.keys
82
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
83
+ end
84
+
85
+ value.each_pair do |key, val|
86
+ @path.push(key)
87
+
88
+ if properties.include?(key)
89
+ check_deprecated(key, definition[:properties][key])
90
+ validate_value(val, definition[:properties][key], plugin_supported)
91
+ elsif definition[:additionalProperties].is_a?(Hash)
92
+ validate_value(val, definition[:additionalProperties], plugin_supported)
93
+ end
94
+ ensure
95
+ @path.pop
96
+ end
97
+ end
98
+
99
+ # Validates an array value, logging errors for any validations that fail.
100
+ # This will enumerate the items in the array and validate each item
101
+ # individually.
102
+ #
103
+ private def validate_array(value, definition, plugin_supported)
104
+ if definition[:uniqueItems] && value.size != value.uniq.size
105
+ @errors << "Value at '#{path}' must not include duplicate elements"
106
+ return
107
+ end
108
+
109
+ return unless definition.key?(:items)
110
+
111
+ value.each_with_index do |item, index|
112
+ @path.push(index)
113
+ validate_value(item, definition[:items], plugin_supported)
114
+ ensure
115
+ @path.pop
116
+ end
117
+ end
118
+
119
+ # Validates a string value, logging errors for any validations that fail.
120
+ #
121
+ private def validate_string(value, definition)
122
+ if definition.key?(:enum) && !definition[:enum].include?(value)
123
+ message = "Value at '#{path}' must be "
124
+ message += "one of " if definition[:enum].count > 1
125
+ message += definition[:enum].join(', ')
126
+ multitype_error(message, value, definition)
127
+ end
128
+ end
129
+
130
+ # Validates a numeric value, logging errors for any validations that fail.
131
+ #
132
+ private def validate_number(value, definition)
133
+ if definition.key?(:minimum) && value < definition[:minimum]
134
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
135
+ end
136
+
137
+ if definition.key?(:maximum) && value > definition[:maximum]
138
+ @errors << "Value at '#{path}' must be a maximum of #{definition[:maximum]}"
139
+ end
140
+ end
141
+
142
+ # Adds warnings for unknown config options.
143
+ #
144
+ private def validate_keys(keys, known_keys)
145
+ (keys - known_keys).each do |key|
146
+ message = "Unknown option '#{key}'"
147
+ message += " at '#{path}'" if @path.any?
148
+ message += " at #{@location}" if @location
149
+ message += "."
150
+ @warnings << { id: 'unknown_option', msg: message }
151
+ end
152
+ end
153
+
154
+ # Adds a warning if the given option is deprecated.
155
+ #
156
+ private def check_deprecated(key, definition)
157
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
158
+
159
+ if definition.key?(:_deprecation)
160
+ message = "Option '#{path}' "
161
+ message += "at #{@location} " if @location
162
+ message += "is deprecated. #{definition[:_deprecation]}"
163
+ @deprecations << { id: "#{key}_option", msg: message }
164
+ end
165
+ end
166
+
167
+ # Returns true if a value is a plugin reference. This also validates whether
168
+ # a value can be a plugin reference in the first place. If the value is a
169
+ # plugin reference but cannot be one according to the schema, then this will
170
+ # log an error.
171
+ #
172
+ private def plugin_reference?(value, plugin_supported)
173
+ if value.is_a?(Hash) && value.key?('_plugin')
174
+ unless plugin_supported
175
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
176
+ "this location"
177
+ end
178
+
179
+ true
180
+ else
181
+ false
182
+ end
183
+ end
184
+
185
+ # Asserts the type for each option against the type specified in the schema
186
+ # definition. The schema definition can specify multiple valid types, so the
187
+ # value needs to only match one of the types to be valid. Returns early if
188
+ # there is no type in the definition (in practice this shouldn't happen, but
189
+ # this will safeguard against any dev mistakes).
190
+ #
191
+ private def valid_type?(value, definition)
192
+ return unless definition.key?(:type)
193
+
194
+ types = Array(definition[:type])
195
+
196
+ if types.include?(value.class)
197
+ true
198
+ else
199
+ if types.include?(TrueClass) || types.include?(FalseClass)
200
+ types = types - [TrueClass, FalseClass] + ['Boolean']
201
+ end
202
+
203
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
204
+
205
+ false
206
+ end
207
+ end
208
+
209
+ # Adds an error that includes additional helpful information for values
210
+ # that accept multiple types.
211
+ #
212
+ private def multitype_error(message, value, definition)
213
+ if Array(definition[:type]).count > 1
214
+ types = Array(definition[:type]) - [value.class]
215
+ message += " or must be of type #{types.join(' or ')}"
216
+ end
217
+
218
+ @errors << message
219
+ end
220
+
221
+ # Returns the formatted path for the key.
222
+ #
223
+ private def path
224
+ @path.join('.')
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ VERSION = '5.0.0.rc1'
5
+ end
data/lib/bolt.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # loads Logging gem, patching it for perf reasons to disable plugins
4
+ require 'logging_extensions/logging'
5
+
6
+ module Bolt
7
+ require_relative 'bolt/executor'
8
+ end