bolt 2.19.0 → 2.24.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  5. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  8. data/exe/bolt +1 -0
  9. data/guides/inventory.txt +19 -0
  10. data/guides/project.txt +22 -0
  11. data/lib/bolt/analytics.rb +5 -5
  12. data/lib/bolt/applicator.rb +4 -3
  13. data/lib/bolt/bolt_option_parser.rb +100 -27
  14. data/lib/bolt/catalog.rb +12 -3
  15. data/lib/bolt/cli.rb +356 -156
  16. data/lib/bolt/config.rb +2 -2
  17. data/lib/bolt/config/options.rb +18 -4
  18. data/lib/bolt/executor.rb +30 -7
  19. data/lib/bolt/inventory/group.rb +6 -5
  20. data/lib/bolt/inventory/inventory.rb +4 -3
  21. data/lib/bolt/logger.rb +3 -4
  22. data/lib/bolt/module.rb +2 -1
  23. data/lib/bolt/outputter.rb +56 -0
  24. data/lib/bolt/outputter/human.rb +10 -9
  25. data/lib/bolt/outputter/json.rb +11 -4
  26. data/lib/bolt/outputter/logger.rb +2 -2
  27. data/lib/bolt/outputter/rainbow.rb +18 -2
  28. data/lib/bolt/pal.rb +13 -11
  29. data/lib/bolt/pal/yaml_plan/evaluator.rb +22 -1
  30. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  31. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  32. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  33. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  34. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  35. data/lib/bolt/plugin/prompt.rb +3 -3
  36. data/lib/bolt/plugin/puppetdb.rb +3 -2
  37. data/lib/bolt/project.rb +7 -4
  38. data/lib/bolt/project_migrate.rb +138 -0
  39. data/lib/bolt/puppetdb/client.rb +2 -0
  40. data/lib/bolt/puppetdb/config.rb +16 -0
  41. data/lib/bolt/result.rb +7 -0
  42. data/lib/bolt/shell/bash.rb +31 -11
  43. data/lib/bolt/shell/powershell.rb +10 -4
  44. data/lib/bolt/transport/base.rb +24 -0
  45. data/lib/bolt/transport/docker.rb +8 -0
  46. data/lib/bolt/transport/docker/connection.rb +28 -10
  47. data/lib/bolt/transport/local/connection.rb +15 -2
  48. data/lib/bolt/transport/orch.rb +15 -3
  49. data/lib/bolt/transport/simple.rb +6 -0
  50. data/lib/bolt/transport/ssh/connection.rb +13 -5
  51. data/lib/bolt/transport/ssh/exec_connection.rb +24 -3
  52. data/lib/bolt/transport/winrm/connection.rb +125 -15
  53. data/lib/bolt/util.rb +27 -12
  54. data/lib/bolt/util/puppet_log_level.rb +4 -3
  55. data/lib/bolt/version.rb +1 -1
  56. data/lib/bolt_server/base_config.rb +1 -1
  57. data/lib/bolt_server/pe/pal.rb +1 -1
  58. data/lib/bolt_server/transport_app.rb +79 -2
  59. data/lib/bolt_spec/bolt_context.rb +7 -2
  60. data/lib/bolt_spec/plans.rb +16 -3
  61. data/lib/bolt_spec/plans/action_stubs.rb +3 -2
  62. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  63. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  64. data/lib/bolt_spec/run.rb +22 -0
  65. data/libexec/apply_catalog.rb +2 -2
  66. data/libexec/bolt_catalog +4 -3
  67. data/libexec/custom_facts.rb +1 -1
  68. data/libexec/query_resources.rb +1 -1
  69. data/modules/secure_env_vars/plans/init.pp +20 -0
  70. metadata +11 -2
@@ -65,8 +65,8 @@ module Bolt
65
65
  ssh_cmd << command
66
66
  end
67
67
 
68
- def copy_file(source, dest)
69
- @logger.debug { "Uploading #{source}, to #{userhost}:#{dest}" } unless source.is_a?(StringIO)
68
+ def upload_file(source, dest)
69
+ @logger.trace { "Uploading #{source} to #{dest}" } unless source.is_a?(StringIO)
70
70
 
71
71
  cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
72
72
  cp_cmd = Array(cp_conf)
@@ -87,7 +87,28 @@ module Bolt
87
87
  end
88
88
 
89
89
  if stat.success?
90
- @logger.debug "Successfully uploaded #{source} to #{dest}"
90
+ @logger.trace "Successfully uploaded #{source} to #{dest}"
91
+ else
92
+ message = "Could not copy file to #{dest}: #{err}"
93
+ raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
94
+ end
95
+ end
96
+
97
+ def download_file(source, dest, _download)
98
+ @logger.trace { "Downloading #{userhost}:#{source} to #{dest}" }
99
+
100
+ FileUtils.mkdir_p(dest)
101
+
102
+ cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
103
+ cp_cmd = Array(cp_conf)
104
+ cp_cmd += ssh_opts
105
+ cp_cmd << "#{userhost}:#{Shellwords.escape(source)}"
106
+ cp_cmd << dest
107
+
108
+ _, err, stat = Open3.capture3(*cp_cmd)
109
+
110
+ if stat.success?
111
+ @logger.trace "Successfully downloaded #{userhost}:#{source} to #{dest}"
91
112
  else
92
113
  message = "Could not copy file to #{dest}: #{err}"
93
114
  raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
@@ -19,7 +19,7 @@ module Bolt
19
19
  # Build set of extensions from extensions config as well as interpreters
20
20
 
21
21
  @logger = Logging.logger[@target.safe_name]
22
- logger.debug("Initializing winrm connection to #{@target.safe_name}")
22
+ logger.trace("Initializing winrm connection to #{@target.safe_name}")
23
23
  @transport_logger = transport_logger
24
24
  end
25
25
 
@@ -55,7 +55,7 @@ module Bolt
55
55
 
56
56
  @session = @connection.shell(:powershell)
57
57
  @session.run('$PSVersionTable.PSVersion')
58
- @logger.debug { "Opened session" }
58
+ @logger.trace { "Opened session" }
59
59
  end
60
60
  rescue Timeout::Error
61
61
  # If we're using the default port with SSL, a timeout probably means the
@@ -97,11 +97,11 @@ module Bolt
97
97
  def disconnect
98
98
  @session&.close
99
99
  @client&.disconnect!
100
- @logger.debug { "Closed session" }
100
+ @logger.trace { "Closed session" }
101
101
  end
102
102
 
103
103
  def execute(command)
104
- @logger.debug { "Executing command: #{command}" }
104
+ @logger.trace { "Executing command: #{command}" }
105
105
 
106
106
  inp = StringIO.new
107
107
  # This transport doesn't accept stdin, so close the stream to ensure
@@ -111,12 +111,21 @@ module Bolt
111
111
  out_rd, out_wr = IO.pipe('UTF-8')
112
112
  err_rd, err_wr = IO.pipe('UTF-8')
113
113
  th = Thread.new do
114
+ # By default, any exception raised in a thread will be reported to
115
+ # stderr as a stacktrace. Since we know these errors are going to
116
+ # propagate to the main thread via the shell, there's no chance
117
+ # they will be unhandled, so the default stack trace is unneeded.
118
+ Thread.current.report_on_exception = false
114
119
  result = @session.run(command)
115
120
  out_wr << result.stdout
116
121
  err_wr << result.stderr
117
122
  out_wr.close
118
123
  err_wr.close
119
124
  result.exitcode
125
+ ensure
126
+ # Close the streams to avoid the caller deadlocking
127
+ out_wr.close
128
+ err_wr.close
120
129
  end
121
130
 
122
131
  [inp, out_rd, err_rd, th]
@@ -125,27 +134,27 @@ module Bolt
125
134
  "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
126
135
  raise Bolt::Error.new(msg, 'bolt/too-many-files')
127
136
  rescue StandardError
128
- @logger.debug { "Command aborted" }
137
+ @logger.trace { "Command aborted" }
129
138
  raise
130
139
  end
131
140
 
132
- def copy_file(source, destination)
133
- @logger.debug { "Uploading #{source}, to #{destination}" }
141
+ def upload_file(source, destination)
142
+ @logger.trace { "Uploading #{source} to #{destination}" }
134
143
  if target.options['file-protocol'] == 'smb'
135
- copy_file_smb(source, destination)
144
+ upload_file_smb(source, destination)
136
145
  else
137
- copy_file_winrm(source, destination)
146
+ upload_file_winrm(source, destination)
138
147
  end
139
148
  end
140
149
 
141
- def copy_file_winrm(source, destination)
150
+ def upload_file_winrm(source, destination)
142
151
  fs = ::WinRM::FS::FileManager.new(@connection)
143
152
  fs.upload(source, destination)
144
153
  rescue StandardError => e
145
154
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
146
155
  end
147
156
 
148
- def copy_file_smb(source, destination)
157
+ def upload_file_smb(source, destination)
149
158
  # lazy-load expensive gem code
150
159
  require 'ruby_smb'
151
160
 
@@ -165,7 +174,7 @@ module Bolt
165
174
  client = smb_client_login
166
175
  tree = client.tree_connect(path)
167
176
  begin
168
- copy_file_smb_recursive(tree, source, dest)
177
+ upload_file_smb_recursive(tree, source, dest)
169
178
  ensure
170
179
  tree.disconnect!
171
180
  end
@@ -175,6 +184,61 @@ module Bolt
175
184
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
176
185
  end
177
186
 
187
+ def download_file(source, destination, download)
188
+ @logger.trace { "Downloading #{source} to #{destination}" }
189
+ if target.options['file-protocol'] == 'smb'
190
+ download_file_smb(source, destination)
191
+ else
192
+ download_file_winrm(source, destination, download)
193
+ end
194
+ end
195
+
196
+ def download_file_winrm(source, destination, download)
197
+ # The winrm gem doesn't create the destination directory if it's missing,
198
+ # so create it here
199
+ FileUtils.mkdir_p(destination)
200
+ fs = ::WinRM::FS::FileManager.new(@connection)
201
+ # params: source, destination, chunksize, first
202
+ # first needs to be set to false, otherwise if the source is a directory it
203
+ # will be nested inside a directory with the same name
204
+ fs.download(source, download, 1024 * 1024, false)
205
+ rescue StandardError => e
206
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
207
+ end
208
+
209
+ def download_file_smb(source, destination)
210
+ # lazy-load expensive gem code
211
+ require 'ruby_smb'
212
+
213
+ win_source = source.tr('/', '\\')
214
+ if (md = win_source.match(/^([a-z]):\\(.*)/i))
215
+ # if drive, use admin share for that drive, so path is '\\host\C$'
216
+ path = "\\\\#{@target.host}\\#{md[1]}$"
217
+ src = md[2]
218
+ elsif (md = win_source.match(/^(\\\\[^\\]+\\[^\\]+)\\(.*)/))
219
+ # if unc, path is '\\host\share'
220
+ path = md[1]
221
+ src = md[2]
222
+ else
223
+ raise ArgumentError, "Unknown source '#{source}'"
224
+ end
225
+
226
+ client = smb_client_login
227
+ tree = client.tree_connect(path)
228
+
229
+ begin
230
+ # Make sure the root download directory for the target exists
231
+ FileUtils.mkdir_p(destination)
232
+ download_file_smb_recursive(tree, src, destination)
233
+ ensure
234
+ tree.disconnect!
235
+ end
236
+ rescue ::RubySMB::Error::UnexpectedStatusCode => e
237
+ raise Bolt::Node::FileError.new("SMB Error: #{e.message}", 'DOWNLOAD_ERROR')
238
+ rescue StandardError => e
239
+ raise Bolt::Node::FileError.new(e.message, 'DOWNLOAD_ERROR')
240
+ end
241
+
178
242
  def shell
179
243
  @shell ||= Bolt::Shell::Powershell.new(target, self)
180
244
  end
@@ -193,7 +257,7 @@ module Bolt
193
257
  status = @client.login
194
258
  case status
195
259
  when WindowsError::NTStatus::STATUS_SUCCESS
196
- @logger.debug { "Connected to #{@client.dns_host_name}" }
260
+ @logger.trace { "Connected to #{@client.dns_host_name}" }
197
261
  when WindowsError::NTStatus::STATUS_LOGON_FAILURE
198
262
  raise Bolt::Node::ConnectError.new(
199
263
  "SMB authentication failed for #{target.safe_name}",
@@ -230,13 +294,13 @@ module Bolt
230
294
  )
231
295
  end
232
296
 
233
- def copy_file_smb_recursive(tree, source, dest)
297
+ def upload_file_smb_recursive(tree, source, dest)
234
298
  if Dir.exist?(source)
235
299
  tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
236
300
 
237
301
  Dir.children(source).each do |child|
238
302
  child_dest = dest + '\\' + child
239
- copy_file_smb_recursive(tree, File.join(source, child), child_dest)
303
+ upload_file_smb_recursive(tree, File.join(source, child), child_dest)
240
304
  end
241
305
  return
242
306
  end
@@ -255,6 +319,52 @@ module Bolt
255
319
  file.close
256
320
  end
257
321
  end
322
+
323
+ def download_file_smb_recursive(tree, source, destination)
324
+ dest = File.expand_path(Bolt::Util.windows_basename(source), destination)
325
+
326
+ # Check if the source is a directory by attempting to list its children.
327
+ # If the source is a directory, create the directory on the host and then
328
+ # recurse through the children.
329
+ if (children = list_directory_children_smb(tree, source))
330
+ FileUtils.mkdir_p(dest)
331
+
332
+ children.each do |child|
333
+ # File names are encoded UTF_16LE.
334
+ filename = child.file_name.encode(Encoding::UTF_8)
335
+
336
+ next if %w[. ..].include?(filename)
337
+
338
+ src = source + '\\' + filename
339
+ download_file_smb_recursive(tree, src, dest)
340
+ end
341
+ # If the source wasn't a directory and just returns 'STATUS_NOT_A_DIRECTORY, then
342
+ # it is a file. Write it to the host.
343
+ else
344
+ begin
345
+ file = tree.open_file(filename: source)
346
+ data = file.read
347
+
348
+ # Files may be encoded UTF_16LE
349
+ data = data.encode(Encoding::UTF_8) if data.encoding == Encoding::UTF_16LE
350
+
351
+ File.write(dest, data)
352
+ ensure
353
+ file.close
354
+ end
355
+ end
356
+ end
357
+
358
+ # Lists the children of a directory using rb_smb
359
+ # Returns an array of RubySMB::Fscc::FileInformation::FileIdFullDirectoryInformation objects
360
+ # if the source is a directory, or raises RubySMB::Error::UnexpectedStatusCode otherwise.
361
+ def list_directory_children_smb(tree, source)
362
+ tree.list(directory: source)
363
+ rescue RubySMB::Error::UnexpectedStatusCode => e
364
+ unless e.message == 'STATUS_NOT_A_DIRECTORY'
365
+ raise e
366
+ end
367
+ end
258
368
  end
259
369
  end
260
370
  end
@@ -13,7 +13,7 @@ module Bolt
13
13
  msg = "Invalid content for #{file_name} file: #{path} should be a Hash or empty, not #{content.class}"
14
14
  raise Bolt::FileError.new(msg, path)
15
15
  end
16
- logger.debug("Loaded #{file_name} from #{path}")
16
+ logger.trace("Loaded #{file_name} from #{path}")
17
17
  content
18
18
  rescue Errno::ENOENT
19
19
  raise Bolt::FileError.new("Could not read #{file_name} file: #{path}", path)
@@ -107,12 +107,13 @@ module Bolt
107
107
  # Accepts a Data object and returns a copy with all hash keys
108
108
  # modified by block. use &:to_s to stringify keys or &:to_sym to symbolize them
109
109
  def walk_keys(data, &block)
110
- if data.is_a? Hash
110
+ case data
111
+ when Hash
111
112
  data.each_with_object({}) do |(k, v), acc|
112
113
  v = walk_keys(v, &block)
113
114
  acc[yield(k)] = v
114
115
  end
115
- elsif data.is_a? Array
116
+ when Array
116
117
  data.map { |v| walk_keys(v, &block) }
117
118
  else
118
119
  data
@@ -124,9 +125,10 @@ module Bolt
124
125
  # their descendants are.
125
126
  def walk_vals(data, skip_top = false, &block)
126
127
  data = yield(data) unless skip_top
127
- if data.is_a? Hash
128
+ case data
129
+ when Hash
128
130
  data.transform_values { |v| walk_vals(v, &block) }
129
- elsif data.is_a? Array
131
+ when Array
130
132
  data.map { |v| walk_vals(v, &block) }
131
133
  else
132
134
  data
@@ -137,9 +139,10 @@ module Bolt
137
139
  # modified by the given block. Descendants are modified before their
138
140
  # parents.
139
141
  def postwalk_vals(data, skip_top = false, &block)
140
- new_data = if data.is_a? Hash
142
+ new_data = case data
143
+ when Hash
141
144
  data.transform_values { |v| postwalk_vals(v, &block) }
142
- elsif data.is_a? Array
145
+ when Array
143
146
  data.map { |v| postwalk_vals(v, &block) }
144
147
  else
145
148
  data
@@ -193,11 +196,12 @@ module Bolt
193
196
  cloned[obj.object_id] = cl
194
197
  cloned[cl.object_id] = cl
195
198
 
196
- if cl.is_a? Hash
199
+ case cl
200
+ when Hash
197
201
  obj.each { |k, v| cl[k] = deep_clone(v, cloned) }
198
- elsif cl.is_a? Array
202
+ when Array
199
203
  cl.collect! { |v| deep_clone(v, cloned) }
200
- elsif cl.is_a? Struct
204
+ when Struct
201
205
  obj.each_pair { |k, v| cl[k] = deep_clone(v, cloned) }
202
206
  end
203
207
 
@@ -257,14 +261,25 @@ module Bolt
257
261
 
258
262
  # Recursively searches a data structure for plugin references
259
263
  def references?(input)
260
- if input.is_a?(Hash)
264
+ case input
265
+ when Hash
261
266
  input.key?('_plugin') || input.values.any? { |v| references?(v) }
262
- elsif input.is_a?(Array)
267
+ when Array
263
268
  input.any? { |v| references?(v) }
264
269
  else
265
270
  false
266
271
  end
267
272
  end
273
+
274
+ def unix_basename(path)
275
+ raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
276
+ path.split('/').last
277
+ end
278
+
279
+ def windows_basename(path)
280
+ raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
281
+ path.split(%r{[/\\]}).last
282
+ end
268
283
  end
269
284
  end
270
285
  end
@@ -4,9 +4,10 @@ module Bolt
4
4
  module Util
5
5
  module PuppetLogLevel
6
6
  MAPPING = {
7
- debug: :debug,
8
- info: :info,
9
- notice: :notice,
7
+ # Demote Puppet's logs by one level, since Puppet is an implementation detail of Bolt
8
+ debug: :trace,
9
+ info: :debug,
10
+ notice: :info,
10
11
  warning: :warn,
11
12
  err: :error,
12
13
  # The following are used by Puppet functions of the same name, and are all treated as
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.19.0'
4
+ VERSION = '2.24.0'
5
5
  end
@@ -16,7 +16,7 @@ module BoltServer
16
16
 
17
17
  def defaults
18
18
  { 'host' => '127.0.0.1',
19
- 'loglevel' => 'notice',
19
+ 'loglevel' => 'warn',
20
20
  'ssl-cipher-suites' => %w[ECDHE-ECDSA-AES256-GCM-SHA384
21
21
  ECDHE-RSA-AES256-GCM-SHA384
22
22
  ECDHE-ECDSA-CHACHA20-POLY1305
@@ -58,7 +58,7 @@ module BoltServer
58
58
  # Bolt::PAL::MODULES_PATH which would be more complex if we tried to use @modulepath since
59
59
  # we need to append our modulepaths and exclude modules shiped in bolt gem code
60
60
  modulepath_dirs = environment.modulepath
61
- @original_modulepath = modulepath_dirs
61
+ @user_modulepath = modulepath_dirs
62
62
  @modulepath = [PE_BOLTLIB_PATH, Bolt::PAL::BOLTLIB_PATH, *modulepath_dirs]
63
63
  end
64
64
  end
@@ -151,13 +151,14 @@ module BoltServer
151
151
  sha256 = file['sha256']
152
152
  kind = file['kind']
153
153
  path = File.join(cache_dir, relative_path)
154
- if kind == 'file'
154
+ case kind
155
+ when 'file'
155
156
  # The parent should already be created by `directory` entries,
156
157
  # but this is to be on the safe side.
157
158
  parent = File.dirname(path)
158
159
  FileUtils.mkdir_p(parent)
159
160
  @file_cache.serial_execute { @file_cache.download_file(path, sha256, uri) }
160
- elsif kind == 'directory'
161
+ when 'directory'
161
162
  # Create directory in cache so we can move files in.
162
163
  FileUtils.mkdir_p(path)
163
164
  else
@@ -220,6 +221,56 @@ module BoltServer
220
221
  plan_info
221
222
  end
222
223
 
224
+ def build_puppetserver_uri(file_identifier, module_name, environment)
225
+ segments = file_identifier.split('/', 3)
226
+ if segments.size == 1
227
+ {
228
+ 'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
229
+ 'params' => {
230
+ 'environment' => environment
231
+ }
232
+ }
233
+ else
234
+ module_segment, mount_segment, name_segment = *segments
235
+ {
236
+ 'path' => case mount_segment
237
+ when 'files'
238
+ "/puppet/v3/file_content/modules/#{module_segment}/#{name_segment}"
239
+ when 'tasks'
240
+ "/puppet/v3/file_content/tasks/#{module_segment}/#{name_segment}"
241
+ when 'lib'
242
+ "/puppet/v3/file_content/plugins/#{name_segment}"
243
+ end,
244
+ 'params' => {
245
+ 'environment' => environment
246
+ }
247
+ }
248
+ end
249
+ end
250
+
251
+ def pe_task_info(pal, module_name, task_name, environment)
252
+ # Handle case where task name is simply module name with special `init` task
253
+ task_name = if task_name == 'init' || task_name.nil?
254
+ module_name
255
+ else
256
+ "#{module_name}::#{task_name}"
257
+ end
258
+ task = pal.get_task(task_name)
259
+ files = task.files.map do |file_hash|
260
+ {
261
+ 'filename' => file_hash['name'],
262
+ 'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
263
+ 'size_bytes' => File.size(file_hash['path']),
264
+ 'uri' => build_puppetserver_uri(file_hash['name'], module_name, environment)
265
+ }
266
+ end
267
+ {
268
+ 'metadata' => task.metadata,
269
+ 'name' => task.name,
270
+ 'files' => files
271
+ }
272
+ end
273
+
223
274
  get '/' do
224
275
  200
225
276
  end
@@ -350,6 +401,16 @@ module BoltServer
350
401
  end
351
402
  end
352
403
 
404
+ # Fetches the metadata for a single task
405
+ #
406
+ # @param environment [String] the environment to fetch the task from
407
+ get '/tasks/:module_name/:task_name' do
408
+ in_pe_pal_env(params['environment']) do |pal|
409
+ task_info = pe_task_info(pal, params[:module_name], params[:task_name], params['environment'])
410
+ [200, task_info.to_json]
411
+ end
412
+ end
413
+
353
414
  # Fetches the list of plans for an environment, optionally fetching all metadata for each plan
354
415
  #
355
416
  # @param environment [String] the environment to fetch the list of plans from
@@ -374,6 +435,22 @@ module BoltServer
374
435
  end
375
436
  end
376
437
 
438
+ # Fetches the list of tasks for an environment
439
+ #
440
+ # @param environment [String] the environment to fetch the list of tasks from
441
+ get '/tasks' do
442
+ in_pe_pal_env(params['environment']) do |pal|
443
+ tasks = pal.list_tasks
444
+ tasks_response = tasks.map { |task_name, _description| { 'name' => task_name } }.to_json
445
+
446
+ # We structure this array of tasks to be an array of hashes so that it matches the structure
447
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
448
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
449
+ # to bolt-server smaller/simpler.
450
+ [200, tasks_response]
451
+ end
452
+ end
453
+
377
454
  error 404 do
378
455
  err = Bolt::Error.new("Could not find route #{request.path}",
379
456
  'boltserver/not-found')