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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Task
5
+ module Run
6
+ module_function
7
+
8
+ # TODO: we should probably use a Bolt::Task for this
9
+ def validate_params(task_signature, params)
10
+ task_signature.runnable_with?(params) do |mismatch_message|
11
+ raise Bolt::ValidationError, mismatch_message
12
+ end || (raise Bolt::ValidationError, 'Task parameters do not match')
13
+
14
+ unless Puppet::Pops::Types::TypeFactory.data.instance?(params)
15
+ # generate a helpful error message about the type-mismatch between the type Data
16
+ # and the actual type of use_args
17
+ use_args_t = Puppet::Pops::Types::TypeCalculator.infer_set(params)
18
+ desc = Puppet::Pops::Types::TypeMismatchDescriber.singleton.describe_mismatch(
19
+ 'Task parameters are not of type Data. run_task()',
20
+ Puppet::Pops::Types::TypeFactory.data, use_args_t
21
+ )
22
+ raise Bolt::ValidationError, desc
23
+ end
24
+ nil
25
+ end
26
+
27
+ def wrap_sensitive(task, params)
28
+ if (spec = task.metadata['parameters'])
29
+ params.each_with_object({}) do |(param, val), wrapped|
30
+ wrapped[param] = if spec.dig(param, 'sensitive')
31
+ Puppet::Pops::Types::PSensitiveType::Sensitive.new(val)
32
+ else
33
+ val
34
+ end
35
+ end
36
+ else
37
+ params
38
+ end
39
+ end
40
+
41
+ def run_task(task, targets, params, options, executor)
42
+ if targets.empty?
43
+ Bolt::ResultSet.new([])
44
+ else
45
+ result = executor.run_task(targets, task, params, options, [], :trace)
46
+
47
+ if !result.ok && !options[:catch_errors]
48
+ raise Bolt::RunFailure.new(result, 'run_task', task.name)
49
+ end
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/bolt/task.rb ADDED
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class NoImplementationError < Bolt::Error
5
+ def initialize(target, task)
6
+ msg = "No suitable implementation of #{task.name} for #{target.name}"
7
+ super(msg, 'bolt/no-implementation')
8
+ end
9
+ end
10
+
11
+ class Task
12
+ STDIN_METHODS = %w[both stdin].freeze
13
+ ENVIRONMENT_METHODS = %w[both environment].freeze
14
+
15
+ METADATA_KEYS = %w[description extensions files implementations
16
+ input_method parameters private puppet_task_version
17
+ remote supports_noop].freeze
18
+
19
+ attr_reader :name, :files, :metadata, :remote
20
+ attr_accessor :mtime
21
+
22
+ # name [String] name of the task
23
+ # files [Array<Hash>] where each entry includes `name` and `path`
24
+ # metadata [Hash] task metadata
25
+ def initialize(name, metadata = {}, files = [], remote = false)
26
+ @name = name
27
+ @metadata = metadata
28
+ @files = files
29
+ @remote = remote
30
+ @logger = Bolt::Logger.logger(self)
31
+
32
+ validate_metadata
33
+ end
34
+
35
+ def self.from_task_signature(task_sig)
36
+ hash = task_sig.task_hash
37
+ new(hash['name'], hash.fetch('metadata', {}), hash.fetch('files', []))
38
+ end
39
+
40
+ def remote_instance
41
+ self.class.new(@name, @metadata, @files, true)
42
+ end
43
+
44
+ def description
45
+ metadata['description']
46
+ end
47
+
48
+ def parameters
49
+ metadata['parameters']
50
+ end
51
+
52
+ def parameter_defaults
53
+ (parameters || {}).each_with_object({}) do |(name, param_spec), defaults|
54
+ defaults[name] = param_spec['default'] if param_spec.key?('default')
55
+ end
56
+ end
57
+
58
+ def supports_noop
59
+ metadata['supports_noop']
60
+ end
61
+
62
+ def module_name
63
+ name.split('::').first
64
+ end
65
+
66
+ def tasks_dir
67
+ File.join(module_name, 'tasks')
68
+ end
69
+
70
+ def file_map
71
+ @file_map ||= files.each_with_object({}) { |file, hsh| hsh[file['name']] = file }
72
+ end
73
+ private :file_map
74
+
75
+ # This provides a method we can override in subclasses if the 'path' needs
76
+ # to be fetched or computed.
77
+ def file_path(file_name)
78
+ file_map[file_name]['path']
79
+ end
80
+
81
+ def add_mtimes
82
+ @files.each do |f|
83
+ f['mtime'] = File.mtime(f['path']) if File.exist?(f['path'])
84
+ end
85
+ end
86
+
87
+ def implementations
88
+ metadata['implementations']
89
+ end
90
+
91
+ # Returns a hash of implementation name, path to executable, input method (if defined),
92
+ # and any additional files (name and path)
93
+ def select_implementation(target, provided_features = [])
94
+ impl = if (impls = implementations)
95
+ available_features = target.feature_set + provided_features
96
+ impl = impls.find do |imp|
97
+ remote_impl = imp['remote']
98
+ remote_impl = metadata['remote'] if remote_impl.nil?
99
+ Set.new(imp['requirements']).subset?(available_features) && !!remote_impl == @remote
100
+ end
101
+ raise NoImplementationError.new(target, self) unless impl
102
+ impl = impl.dup
103
+ impl['path'] = file_path(impl['name'])
104
+ impl.delete('requirements')
105
+ impl
106
+ else
107
+ raise NoImplementationError.new(target, self) unless !!metadata['remote'] == @remote
108
+ name = files.first['name']
109
+ { 'name' => name, 'path' => file_path(name) }
110
+ end
111
+
112
+ inmethod = impl['input_method'] || metadata['input_method']
113
+ impl['input_method'] = inmethod unless inmethod.nil?
114
+
115
+ mfiles = impl.fetch('files', []) + metadata.fetch('files', [])
116
+ dirnames, filenames = mfiles.partition { |file| file.end_with?('/') }
117
+ impl['files'] = filenames.map do |file|
118
+ path = file_path(file)
119
+ raise "No file found for reference #{file}" if path.nil?
120
+ { 'name' => file, 'path' => path }
121
+ end
122
+
123
+ unless dirnames.empty?
124
+ files.each do |file|
125
+ name = file['name']
126
+ if dirnames.any? { |dirname| name.start_with?(dirname) }
127
+ impl['files'] << { 'name' => name, 'path' => file_path(name) }
128
+ end
129
+ end
130
+ end
131
+
132
+ impl
133
+ end
134
+
135
+ def eql?(other)
136
+ self.class == other.class &&
137
+ @name == other.name &&
138
+ @metadata == other.metadata &&
139
+ @files == other.files &&
140
+ @remote == other.remote
141
+ end
142
+
143
+ alias == :eql?
144
+
145
+ def to_h
146
+ {
147
+ name: @name,
148
+ files: @files,
149
+ metadata: @metadata
150
+ }
151
+ end
152
+
153
+ def validate_metadata
154
+ unknown_keys = metadata.keys - METADATA_KEYS
155
+
156
+ if unknown_keys.any?
157
+ msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
158
+ msg += " This could be a typo in the task metadata or might result in incorrect behavior."
159
+ Bolt::Logger.warn("unknown_task_metadata_keys", msg)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logging'
4
+ require_relative '../../bolt/result'
5
+
6
+ module Bolt
7
+ module Transport
8
+ # This class provides the default behavior for Transports. A Transport is
9
+ # responsible for uploading files and running commands, scripts, and tasks
10
+ # on Targets.
11
+ #
12
+ # Bolt executes work on the Transport in "batches". To do that, it calls
13
+ # the batches() method, which is responsible for dividing the list of
14
+ # Targets into batches according to how it wants to handle them. It will
15
+ # then call Transport#batch_task, or the corresponding method for another
16
+ # operation, passing a list of Targets. The Transport returns a list of
17
+ # Bolt::Result objects, one per Target. Each batch is executed on a
18
+ # separate thread, controlled by the `concurrency` setting, so many batches
19
+ # may be running in parallel.
20
+ #
21
+ # The default batch implementation splits the list of Targets into batches
22
+ # of 1. It then calls run_task(), or a corresponding method for other
23
+ # operations, passing in the single Target.
24
+ #
25
+ # Most Transport implementations, like the SSH and WinRM transports, don't
26
+ # need to do their own batching, since they only operate on a single Target
27
+ # at a time. Those Transports can implement the run_task() and related
28
+ # methods, which will automatically handle running many Targets in
29
+ # parallel, and will handle publishing start and finish events for each
30
+ # Target.
31
+ #
32
+ # Transports that need their own batching, like the Orch transport, can
33
+ # instead override the batches() method to split Targets into sets that can
34
+ # be executed together, and override the batch_task() and related methods
35
+ # to execute a batch of targets. In that case, those Transports should accept
36
+ # a block argument and call it with a :node_start event for each Target
37
+ # before executing, and a :node_result event for each Target after
38
+ # execution.
39
+ class Base
40
+ attr_reader :logger
41
+
42
+ def initialize
43
+ @logger = Bolt::Logger.logger(self)
44
+ end
45
+
46
+ def with_events(target, callback, action, position)
47
+ callback&.call(type: :node_start, target: target)
48
+
49
+ result = begin
50
+ yield
51
+ rescue StandardError, NotImplementedError => e
52
+ Bolt::Result.from_exception(target, e, action: action, position: position)
53
+ end
54
+
55
+ callback&.call(type: :node_result, result: result)
56
+ result
57
+ end
58
+
59
+ def provided_features
60
+ []
61
+ end
62
+
63
+ def default_input_method(_executable)
64
+ 'both'
65
+ end
66
+
67
+ def select_implementation(target, task)
68
+ impl = task.select_implementation(target, provided_features)
69
+ impl['input_method'] ||= default_input_method(impl['path'])
70
+ impl
71
+ end
72
+
73
+ def select_interpreter(executable, interpreters)
74
+ interpreters[Pathname(executable).extname] if interpreters
75
+ end
76
+
77
+ # Raises an error if more than one target was given in the batch.
78
+ #
79
+ # The default implementations of batch_* strictly assume the transport is
80
+ # using the default batch size of 1. This method ensures that is the
81
+ # case and raises an error if it's not.
82
+ def assert_batch_size_one(method, targets)
83
+ if targets.length > 1
84
+ message = "#{self.class.name} must implement #{method} to support batches (got #{targets.length} targets)"
85
+ raise NotImplementedError, message
86
+ end
87
+ end
88
+
89
+ # Runs the given task on a batch of targets.
90
+ #
91
+ # The default implementation only supports batches of size 1 and will fail otherwise.
92
+ #
93
+ # Transports may override this method to implement their own batch processing.
94
+ def batch_task(targets, task, arguments, options = {}, position = [], &callback)
95
+ assert_batch_size_one("batch_task()", targets)
96
+ target = targets.first
97
+ with_events(target, callback, 'task', position) do
98
+ @logger.debug { "Running task '#{task.name}' on #{target.safe_name}" }
99
+ run_task(target, task, arguments, options, position)
100
+ end
101
+ end
102
+
103
+ # Runs the given task on a batch of targets with variable parameters.
104
+ #
105
+ # The default implementation only supports batches of size 1 and will fail otherwise.
106
+ #
107
+ # Transports may override this method to implment their own batch processing.
108
+ def batch_task_with(targets, task, target_mapping, options = {}, position = [], &callback)
109
+ assert_batch_size_one("batch_task_with()", targets)
110
+ target = targets.first
111
+ arguments = target_mapping[target]
112
+
113
+ with_events(target, callback, 'task', position) do
114
+ @logger.debug { "Running task '#{task.name}' on #{target.safe_name} with '#{arguments.to_json}'" }
115
+ run_task(target, task, arguments, options, position)
116
+ end
117
+ end
118
+
119
+ # Runs the given command on a batch of targets.
120
+ #
121
+ # The default implementation only supports batches of size 1 and will fail otherwise.
122
+ #
123
+ # Transports may override this method to implement their own batch processing.
124
+ def batch_command(targets, command, options = {}, position = [], &callback)
125
+ assert_batch_size_one("batch_command()", targets)
126
+ target = targets.first
127
+ with_events(target, callback, 'command', position) do
128
+ @logger.debug("Running command '#{command}' on #{target.safe_name}")
129
+ run_command(target, command, options, position)
130
+ end
131
+ end
132
+
133
+ # Runs the given script on a batch of targets.
134
+ #
135
+ # The default implementation only supports batches of size 1 and will fail otherwise.
136
+ #
137
+ # Transports may override this method to implement their own batch processing.
138
+ def batch_script(targets, script, arguments, options = {}, position = [], &callback)
139
+ assert_batch_size_one("batch_script()", targets)
140
+ target = targets.first
141
+ with_events(target, callback, 'script', position) do
142
+ @logger.debug { "Running script '#{script}' on #{target.safe_name}" }
143
+ run_script(target, script, arguments, options, position)
144
+ end
145
+ end
146
+
147
+ # Uploads the given source file to the destination location on a batch of targets.
148
+ #
149
+ # The default implementation only supports batches of size 1 and will fail otherwise.
150
+ #
151
+ # Transports may override this method to implement their own batch processing.
152
+ def batch_upload(targets, source, destination, options = {}, position = [], &callback)
153
+ assert_batch_size_one("batch_upload()", targets)
154
+ target = targets.first
155
+ with_events(target, callback, 'upload', position) do
156
+ @logger.debug { "Uploading: '#{source}' to #{destination} on #{target.safe_name}" }
157
+ upload(target, source, destination, options)
158
+ end
159
+ end
160
+
161
+ # Downloads the given source file from a batch of targets to the destination location
162
+ # on the host.
163
+ #
164
+ # The default implementation only supports batches of size 1 and will fail otherwise.
165
+ #
166
+ # Transports may override this method to implement their own batch processing.
167
+ def batch_download(targets, source, destination, options = {}, position = [], &callback)
168
+ require 'erb'
169
+
170
+ assert_batch_size_one("batch_download()", targets)
171
+ target = targets.first
172
+ with_events(target, callback, 'download', position) do
173
+ escaped_name = ERB::Util.url_encode(target.safe_name)
174
+ target_destination = File.expand_path(escaped_name, destination)
175
+ @logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
176
+ download(target, source, target_destination, options)
177
+ end
178
+ end
179
+
180
+ def batch_connected?(targets)
181
+ assert_batch_size_one("connected?()", targets)
182
+ connected?(targets.first)
183
+ end
184
+
185
+ # Split the given list of targets into a list of batches. The default
186
+ # implementation returns single-target batches.
187
+ #
188
+ # Transports may override this method, and the corresponding batch_*
189
+ # methods, to implement their own batch processing.
190
+ def batches(targets)
191
+ targets.map { |target| [target] }
192
+ end
193
+
194
+ # Transports should override this method with their own implementation of running a command.
195
+ def run_command(*_args)
196
+ raise NotImplementedError, "run_command() must be implemented by the transport class"
197
+ end
198
+
199
+ # Transports should override this method with their own implementation of running a script.
200
+ def run_script(*_args)
201
+ raise NotImplementedError, "run_script() must be implemented by the transport class"
202
+ end
203
+
204
+ # Transports should override this method with their own implementation of running a task.
205
+ def run_task(*_args)
206
+ raise NotImplementedError, "run_task() must be implemented by the transport class"
207
+ end
208
+
209
+ # Transports should override this method with their own implementation of file upload.
210
+ def upload(*_args)
211
+ raise NotImplementedError, "upload() must be implemented by the transport class"
212
+ end
213
+
214
+ # Transports should override this method with their own implementation of file download.
215
+ def download(*_args)
216
+ raise NotImplementedError, "download() must be implemented by the transport class"
217
+ end
218
+
219
+ # Transports should override this method with their own implementation of a connection test.
220
+ def connected?(_targets)
221
+ raise NotImplementedError, "connected?() must be implemented by the transport class"
222
+ end
223
+
224
+ # Unwraps any Sensitive data in an arguments Hash, so the plain-text is passed
225
+ # to the Task/Script.
226
+ #
227
+ # This works on deeply nested data structures composed of Hashes, Arrays, and
228
+ # and plain-old data types (int, string, etc).
229
+ def unwrap_sensitive_args(arguments)
230
+ # Skip this if Puppet isn't loaded
231
+ return arguments unless defined?(Puppet::Pops::Types::PSensitiveType::Sensitive)
232
+
233
+ case arguments
234
+ when Array
235
+ # iterate over the array, unwrapping all elements
236
+ arguments.map { |x| unwrap_sensitive_args(x) }
237
+ when Hash
238
+ # iterate over the arguments hash and unwrap all keys and values
239
+ arguments.each_with_object({}) { |(k, v), h|
240
+ h[unwrap_sensitive_args(k)] = unwrap_sensitive_args(v)
241
+ }
242
+ when Puppet::Pops::Types::PSensitiveType::Sensitive
243
+ # this value is Sensitive, unwrap it
244
+ unwrap_sensitive_args(arguments.unwrap)
245
+ else
246
+ # unknown data type, just return it
247
+ arguments
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logging'
4
+ require_relative '../../../bolt/node/errors'
5
+
6
+ module Bolt
7
+ module Transport
8
+ class Docker < Simple
9
+ class Connection
10
+ attr_reader :user, :target
11
+
12
+ def initialize(target)
13
+ raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
14
+ @target = target
15
+ @user = ENV['USER'] || Etc.getlogin
16
+ @logger = Bolt::Logger.logger(target.safe_name)
17
+ @container_info = {}
18
+ @docker_host = target.options['service-url']
19
+ @logger.trace("Initializing docker connection to #{target.safe_name}")
20
+ end
21
+
22
+ def shell
23
+ @shell ||= if Bolt::Util.windows?
24
+ Bolt::Shell::Powershell.new(target, self)
25
+ else
26
+ Bolt::Shell::Bash.new(target, self)
27
+ end
28
+ end
29
+
30
+ def reset_cwd?
31
+ true
32
+ end
33
+
34
+ # The full ID of the target container
35
+ #
36
+ # @return [String] The full ID of the target container
37
+ def container_id
38
+ @container_info["Id"]
39
+ end
40
+
41
+ def run_cmd(cmd, env_vars)
42
+ Bolt::Util.exec_docker(cmd, env_vars)
43
+ end
44
+
45
+ private def env_hash
46
+ # Set the DOCKER_HOST if we are using a non-default service-url
47
+ @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
48
+ end
49
+
50
+ def connect
51
+ # We don't actually have a connection, but we do need to
52
+ # check that the container exists and is running.
53
+ output = execute_local_json_command('ps', ['--no-trunc'])
54
+ index = output.find_index { |item| item["ID"].start_with?(target.host) || item["Names"] == target.host }
55
+ raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
56
+ # Now find the indepth container information
57
+ output = execute_local_json_command('inspect', [output[index]["ID"]])
58
+ # Store the container information for later
59
+ @container_info = output[0]
60
+ @logger.trace { "Opened session" }
61
+ true
62
+ rescue StandardError => e
63
+ raise Bolt::Node::ConnectError.new(
64
+ "Failed to connect to #{target.safe_name}: #{e.message}",
65
+ 'CONNECT_ERROR'
66
+ )
67
+ end
68
+
69
+ def add_env_vars(env_vars)
70
+ @env_vars = Bolt::Util.format_env_vars_for_cli(env_vars)
71
+ end
72
+
73
+ # Executes a command inside the target container. This is called from the shell class.
74
+ #
75
+ # @param command [string] The command to run
76
+ def execute(command)
77
+ args = []
78
+ # CODEREVIEW: Is it always safe to pass --interactive?
79
+ args += %w[--interactive]
80
+ args += %w[--tty] if target.options['tty']
81
+ args += @env_vars if @env_vars
82
+
83
+ if target.options['shell-command'] && !target.options['shell-command'].empty?
84
+ # escape any double quotes in command
85
+ command = command.gsub('"', '\"')
86
+ command = "#{target.options['shell-command']} \"#{command}\""
87
+ end
88
+
89
+ docker_command = %w[docker exec] + args + [container_id] + Shellwords.split(command)
90
+ @logger.trace { "Executing: #{docker_command.join(' ')}" }
91
+
92
+ Open3.popen3(env_hash, *docker_command)
93
+ rescue StandardError
94
+ @logger.trace { "Command aborted" }
95
+ raise
96
+ end
97
+
98
+ def upload_file(source, destination)
99
+ @logger.trace { "Uploading #{source} to #{destination}" }
100
+ _out, err, stat = run_cmd(['cp', source, "#{container_id}:#{destination}"], env_hash)
101
+ unless stat.exitstatus.zero?
102
+ raise "Error writing to container #{container_id}: #{err}"
103
+ end
104
+ rescue StandardError => e
105
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
106
+ end
107
+
108
+ def download_file(source, destination, _download)
109
+ @logger.trace { "Downloading #{source} to #{destination}" }
110
+ # Create the destination directory, otherwise copying a source directory with Docker will
111
+ # copy the *contents* of the directory.
112
+ # https://docs.docker.com/engine/reference/commandline/cp/
113
+ FileUtils.mkdir_p(destination)
114
+ _out, err, stat = run_cmd(['cp', "#{container_id}:#{source}", destination], env_hash)
115
+ unless stat.exitstatus.zero?
116
+ raise "Error downloading content from container #{container_id}: #{err}"
117
+ end
118
+ rescue StandardError => e
119
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
120
+ end
121
+
122
+ # Executes a Docker CLI command and parses the output in JSON format
123
+ #
124
+ # @param subcommand [String] The docker subcommand to run
125
+ # e.g. 'inspect' for `docker inspect`
126
+ # @param arguments [Array] Arguments to pass to the docker command
127
+ # e.g. 'src' and 'dest' for `docker cp <src> <dest>
128
+ # @return [Object] Ruby object representation of the JSON string
129
+ def execute_local_json_command(subcommand, arguments = [])
130
+ cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
131
+ out, _err, _stat = run_cmd(cmd, env_hash)
132
+ extract_json(out)
133
+ end
134
+
135
+ # Converts the JSON encoded STDOUT string from the docker cli into ruby objects
136
+ #
137
+ # @param stdout [String] The string to convert
138
+ # @return [Object] Ruby object representation of the JSON string
139
+ private def extract_json(stdout)
140
+ # The output from the docker format command is a JSON string per line.
141
+ # We can't do a direct convert but this helper method will convert it into
142
+ # an array of Objects
143
+ stdout.split("\n")
144
+ .reject { |str| str.strip.empty? }
145
+ .map { |str| JSON.parse(str) }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'shellwords'
5
+ require_relative '../../bolt/transport/simple'
6
+
7
+ module Bolt
8
+ module Transport
9
+ class Docker < Simple
10
+ def provided_features
11
+ ['shell']
12
+ end
13
+
14
+ def with_connection(target)
15
+ conn = Connection.new(target)
16
+ conn.connect
17
+ yield conn
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative 'docker/connection'