bolt 3.8.1 → 3.11.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -4
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +61 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +5 -9
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +28 -13
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +5 -15
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +5 -17
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +8 -17
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -15
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +5 -17
  12. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +91 -0
  13. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  14. data/guides/debugging.txt +28 -0
  15. data/guides/inventory.txt +5 -0
  16. data/lib/bolt/applicator.rb +3 -2
  17. data/lib/bolt/bolt_option_parser.rb +51 -4
  18. data/lib/bolt/cli.rb +38 -9
  19. data/lib/bolt/config/transport/docker.rb +1 -1
  20. data/lib/bolt/config/transport/lxd.rb +1 -1
  21. data/lib/bolt/config/transport/podman.rb +1 -1
  22. data/lib/bolt/error.rb +11 -1
  23. data/lib/bolt/executor.rb +55 -72
  24. data/lib/bolt/fiber_executor.rb +141 -0
  25. data/lib/bolt/module_installer/installer.rb +1 -1
  26. data/lib/bolt/outputter/human.rb +46 -2
  27. data/lib/bolt/outputter/json.rb +9 -0
  28. data/lib/bolt/pal.rb +117 -17
  29. data/lib/bolt/plan_future.rb +66 -0
  30. data/lib/bolt/plugin.rb +38 -0
  31. data/lib/bolt/plugin/env_var.rb +8 -1
  32. data/lib/bolt/plugin/module.rb +1 -1
  33. data/lib/bolt/plugin/prompt.rb +8 -1
  34. data/lib/bolt/plugin/puppet_connect_data.rb +8 -1
  35. data/lib/bolt/plugin/puppetdb.rb +7 -1
  36. data/lib/bolt/plugin/task.rb +9 -1
  37. data/lib/bolt/project.rb +2 -1
  38. data/lib/bolt/task.rb +7 -0
  39. data/lib/bolt/transport/docker/connection.rb +5 -2
  40. data/lib/bolt/transport/lxd/connection.rb +4 -0
  41. data/lib/bolt/transport/podman/connection.rb +4 -0
  42. data/lib/bolt/version.rb +1 -1
  43. data/lib/bolt_server/config.rb +1 -1
  44. data/lib/bolt_server/request_error.rb +11 -0
  45. data/lib/bolt_server/transport_app.rb +133 -95
  46. data/lib/bolt_spec/plans/mock_executor.rb +40 -45
  47. data/lib/bolt_spec/run.rb +4 -1
  48. data/modules/puppet_connect/plans/test_input_data.pp +8 -3
  49. data/resources/bolt_bash_completion.sh +214 -0
  50. metadata +10 -3
  51. data/lib/bolt/yarn.rb +0 -23
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module Bolt
6
+ class PlanFuture
7
+ attr_reader :fiber, :id
8
+ attr_accessor :value
9
+
10
+ def initialize(fiber, id, name = nil)
11
+ @fiber = fiber
12
+ @id = id
13
+ @name = name
14
+ @value = nil
15
+ end
16
+
17
+ def name
18
+ @name || @id
19
+ end
20
+
21
+ def to_s
22
+ "Future '#{name}'"
23
+ end
24
+
25
+ def alive?
26
+ fiber.alive?
27
+ end
28
+
29
+ def raise(exception)
30
+ # Make sure the value gets set
31
+ @value = exception
32
+ # This was introduced in Ruby 2.7
33
+ begin
34
+ # Raise an exception to kill the Fiber. If the Fiber has not been
35
+ # resumed yet, or is already terminated this will raise a FiberError.
36
+ # We don't especially care about the FiberError, as long as the Fiber
37
+ # doesn't report itself as alive.
38
+ fiber.raise(exception)
39
+ rescue FiberError
40
+ # If the Fiber is still alive, resume it with a block to raise the
41
+ # exception which will terminate it.
42
+ if fiber.alive?
43
+ fiber.resume { raise(exception) }
44
+ end
45
+ end
46
+ end
47
+
48
+ def resume
49
+ if fiber.alive?
50
+ @value = fiber.resume
51
+ else
52
+ @value
53
+ end
54
+ end
55
+
56
+ def state
57
+ if fiber.alive?
58
+ "running"
59
+ elsif value.is_a?(Exception)
60
+ "error"
61
+ else
62
+ "done"
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/bolt/plugin.rb CHANGED
@@ -250,6 +250,44 @@ module Bolt
250
250
  end
251
251
  end
252
252
 
253
+ # Loads all plugins and returns a map of plugin names to hooks.
254
+ #
255
+ def list_plugins
256
+ load_all_plugins
257
+
258
+ hooks = KNOWN_HOOKS.map { |hook| [hook, {}] }.to_h
259
+
260
+ @plugins.sort.each do |name, plugin|
261
+ # Don't show the Puppet Connect plugin for now.
262
+ next if name == 'puppet_connect_data'
263
+
264
+ case plugin
265
+ when Bolt::Plugin::Module
266
+ plugin.hook_map.each do |hook, spec|
267
+ next unless hooks.include?(hook)
268
+ hooks[hook][name] = spec['task'].description
269
+ end
270
+ else
271
+ plugin.hook_descriptions.each do |hook, description|
272
+ hooks[hook][name] = description
273
+ end
274
+ end
275
+ end
276
+
277
+ hooks
278
+ end
279
+
280
+ # Loads all plugins available to the project.
281
+ #
282
+ private def load_all_plugins
283
+ modules.each do |name, mod|
284
+ next unless mod.plugin?
285
+ by_name(name)
286
+ end
287
+
288
+ RUBY_PLUGINS.each { |name| by_name(name) }
289
+ end
290
+
253
291
  def puppetdb_client
254
292
  by_name('puppetdb').puppetdb_client
255
293
  end
@@ -10,7 +10,14 @@ module Bolt
10
10
  end
11
11
 
12
12
  def hooks
13
- %i[resolve_reference validate_resolve_reference]
13
+ hook_descriptions.keys
14
+ end
15
+
16
+ def hook_descriptions
17
+ {
18
+ resolve_reference: 'Read values stored in environment variables.',
19
+ validate_resolve_reference: nil
20
+ }
14
21
  end
15
22
 
16
23
  def validate_resolve_reference(opts)
@@ -24,7 +24,7 @@ module Bolt
24
24
  end
25
25
  end
26
26
 
27
- attr_reader :config
27
+ attr_reader :config, :hook_map
28
28
 
29
29
  def initialize(mod:, context:, config:, **_opts)
30
30
  @module = mod
@@ -10,7 +10,14 @@ module Bolt
10
10
  end
11
11
 
12
12
  def hooks
13
- %i[resolve_reference validate_resolve_reference]
13
+ hook_descriptions.keys
14
+ end
15
+
16
+ def hook_descriptions
17
+ {
18
+ resolve_reference: 'Prompt the user for a sensitive value.',
19
+ validate_resolve_reference: nil
20
+ }
14
21
  end
15
22
 
16
23
  def validate_resolve_reference(opts)
@@ -49,7 +49,14 @@ module Bolt
49
49
  end
50
50
 
51
51
  def hooks
52
- %i[resolve_reference validate_resolve_reference]
52
+ hook_descriptions.keys
53
+ end
54
+
55
+ def hook_descriptions
56
+ {
57
+ resolve_reference: nil,
58
+ validate_resolve_reference: nil
59
+ }
53
60
  end
54
61
 
55
62
  def resolve_reference(opts)
@@ -27,7 +27,13 @@ module Bolt
27
27
  end
28
28
 
29
29
  def hooks
30
- [:resolve_reference]
30
+ hook_descriptions.keys
31
+ end
32
+
33
+ def hook_descriptions
34
+ {
35
+ resolve_reference: 'Query PuppetDB for a group of targets.'
36
+ }
31
37
  end
32
38
 
33
39
  def warn_missing_fact(certname, fact)
@@ -4,7 +4,15 @@ module Bolt
4
4
  class Plugin
5
5
  class Task
6
6
  def hooks
7
- %i[validate_resolve_reference puppet_library resolve_reference]
7
+ hook_descriptions.keys
8
+ end
9
+
10
+ def hook_descriptions
11
+ {
12
+ puppet_library: 'Run a task to install the Puppet agent package.',
13
+ resolve_reference: 'Run a task as a plugin.',
14
+ validate_resolve_reference: nil
15
+ }
8
16
  end
9
17
 
10
18
  def name
data/lib/bolt/project.rb CHANGED
@@ -14,7 +14,7 @@ module Bolt
14
14
  attr_reader :path, :data, :inventory_file, :hiera_config,
15
15
  :puppetfile, :rerunfile, :type, :resource_types, :project_file,
16
16
  :downloads, :plans_path, :modulepath, :managed_moduledir,
17
- :backup_dir, :plugin_cache_file, :plan_cache_file
17
+ :backup_dir, :plugin_cache_file, :plan_cache_file, :task_cache_file
18
18
 
19
19
  def self.default_project
20
20
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -114,6 +114,7 @@ module Bolt
114
114
  @backup_dir = @path + '.bolt-bak'
115
115
  @plugin_cache_file = @path + '.plugin_cache.json'
116
116
  @plan_cache_file = @path + '.plan_cache.json'
117
+ @task_cache_file = @path + '.task_cache.json'
117
118
  @modulepath = [(@path + 'modules').to_s]
118
119
 
119
120
  if (tc = Bolt::Config::INVENTORY_OPTIONS.keys & data.keys).any?
data/lib/bolt/task.rb CHANGED
@@ -17,6 +17,7 @@ module Bolt
17
17
  remote supports_noop].freeze
18
18
 
19
19
  attr_reader :name, :files, :metadata, :remote
20
+ attr_accessor :mtime
20
21
 
21
22
  # name [String] name of the task
22
23
  # files [Array<Hash>] where each entry includes `name` and `path`
@@ -77,6 +78,12 @@ module Bolt
77
78
  file_map[file_name]['path']
78
79
  end
79
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
+
80
87
  def implementations
81
88
  metadata['implementations']
82
89
  end
@@ -27,6 +27,10 @@ module Bolt
27
27
  end
28
28
  end
29
29
 
30
+ def reset_cwd?
31
+ true
32
+ end
33
+
30
34
  # The full ID of the target container
31
35
  #
32
36
  # @return [String] The full ID of the target container
@@ -74,7 +78,6 @@ module Bolt
74
78
  # CODEREVIEW: Is it always safe to pass --interactive?
75
79
  args += %w[--interactive]
76
80
  args += %w[--tty] if target.options['tty']
77
- args += %W[--env DOCKER_HOST=#{@docker_host}] if @docker_host
78
81
  args += @env_vars if @env_vars
79
82
 
80
83
  if target.options['shell-command'] && !target.options['shell-command'].empty?
@@ -86,7 +89,7 @@ module Bolt
86
89
  docker_command = %w[docker exec] + args + [container_id] + Shellwords.split(command)
87
90
  @logger.trace { "Executing: #{docker_command.join(' ')}" }
88
91
 
89
- Open3.popen3(*docker_command)
92
+ Open3.popen3(env_hash, *docker_command)
90
93
  rescue StandardError
91
94
  @logger.trace { "Command aborted" }
92
95
  raise
@@ -23,6 +23,10 @@ module Bolt
23
23
  Bolt::Shell::Bash.new(target, self)
24
24
  end
25
25
 
26
+ def reset_cwd?
27
+ true
28
+ end
29
+
26
30
  def container_id
27
31
  "#{@target.transport_config['remote']}:#{@target.host}"
28
32
  end
@@ -30,6 +30,10 @@ module Bolt
30
30
  end
31
31
  end
32
32
 
33
+ def reset_cwd?
34
+ true
35
+ end
36
+
33
37
  def connect
34
38
  # We don't actually have a connection, but we do need to
35
39
  # check that the container exists and is running.
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.8.1'
4
+ VERSION = '3.11.0'
5
5
  end
@@ -30,7 +30,7 @@ module BoltServer
30
30
  end
31
31
 
32
32
  def required_keys
33
- super + %w[file-server-uri]
33
+ super + %w[file-server-uri projects-dir]
34
34
  end
35
35
 
36
36
  def service_name
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ module BoltServer
6
+ class RequestError < Bolt::Error
7
+ def initialize(msg, details = {})
8
+ super(msg, 'bolt-server/request-error', details)
9
+ end
10
+ end
11
+ end
@@ -10,6 +10,7 @@ require 'bolt/target'
10
10
  require 'bolt_server/file_cache'
11
11
  require 'bolt_server/plugin'
12
12
  require 'bolt_server/plugin/puppet_connect_data'
13
+ require 'bolt_server/request_error'
13
14
  require 'bolt/task/puppet_server'
14
15
  require 'json'
15
16
  require 'json-schema'
@@ -20,6 +21,10 @@ require 'puppet'
20
21
  # Needed by the `/project_file_metadatas` endpoint
21
22
  require 'puppet/file_serving/fileset'
22
23
 
24
+ # Needed by the 'project_facts_plugin_tarball' endpoint
25
+ require 'minitar'
26
+ require 'zlib'
27
+
23
28
  module BoltServer
24
29
  class TransportApp < Sinatra::Base
25
30
  # This disables Sinatra's error page generation
@@ -51,10 +56,6 @@ module BoltServer
51
56
  # See the `orchestrator.bolt.codedir` tk config setting.
52
57
  DEFAULT_BOLT_CODEDIR = '/opt/puppetlabs/server/data/orchestration-services/code'
53
58
 
54
- MISSING_VERSIONED_PROJECT_RESPONSE = [
55
- 400, Bolt::ValidationError.new('`versioned_project` is a required argument').to_json
56
- ].freeze
57
-
58
59
  def initialize(config)
59
60
  @config = config
60
61
  @schemas = Hash[REQUEST_SCHEMAS.map do |basename|
@@ -74,6 +75,8 @@ module BoltServer
74
75
  # This is needed until the PAL is threadsafe.
75
76
  @pal_mutex = Mutex.new
76
77
 
78
+ @logger = Bolt::Logger.logger(self)
79
+
77
80
  super(nil)
78
81
  end
79
82
 
@@ -84,40 +87,29 @@ module BoltServer
84
87
  result
85
88
  end
86
89
 
87
- def error_result(error)
88
- {
89
- 'status' => 'failure',
90
- 'value' => { '_error' => error.to_h }
91
- }
92
- end
93
-
94
90
  def validate_schema(schema, body)
95
91
  schema_error = JSON::Validator.fully_validate(schema, body)
96
92
  if schema_error.any?
97
- Bolt::Error.new("There was an error validating the request body.",
98
- 'boltserver/schema-error',
99
- schema_error)
93
+ raise BoltServer::RequestError.new("There was an error validating the request body.",
94
+ schema_error)
100
95
  end
101
96
  end
102
97
 
103
98
  # Turns a Bolt::ResultSet object into a status hash that is fit
104
- # to return to the client in a response.
105
- #
106
- # If the `result_set` has more than one result, the status hash
107
- # will have a `status` value and a list of target `results`.
108
- # If the `result_set` contains only one item, it will be returned
109
- # as a single result object. Set `aggregate` to treat it as a set
110
- # of results with length 1 instead.
99
+ # to return to the client in a response. In the case of every action
100
+ # *except* check_node_connections the response will be a single serialized Result.
101
+ # In the check_node_connections case the response will be a hash with the top level "status"
102
+ # of the result and the serialized individual target results.
111
103
  def result_set_to_data(result_set, aggregate: false)
104
+ # use ResultSet#ok method to determine status of a (potentially) aggregate result before serializing
105
+ result_set_status = result_set.ok ? 'success' : 'failure'
112
106
  scrubbed_results = result_set.map do |result|
113
107
  scrub_stack_trace(result.to_data)
114
108
  end
115
109
 
116
- if aggregate || scrubbed_results.length > 1
117
- # For actions that act on multiple targets, construct a status hash for the aggregate result
118
- all_succeeded = scrubbed_results.all? { |r| r['status'] == 'success' }
110
+ if aggregate
119
111
  {
120
- status: all_succeeded ? 'success' : 'failure',
112
+ status: result_set_status,
121
113
  result: scrubbed_results
122
114
  }
123
115
  else
@@ -127,33 +119,26 @@ module BoltServer
127
119
  end
128
120
 
129
121
  def run_task(target, body)
130
- error = validate_schema(@schemas["action-run_task"], body)
131
- return [], error unless error.nil?
132
-
122
+ validate_schema(@schemas["action-run_task"], body)
133
123
  task_data = body['task']
134
124
  task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
135
125
  parameters = body['parameters'] || {}
136
- task_result = @executor.run_task(target, task, parameters)
137
- task_result.each do |result|
126
+ @executor.run_task(target, task, parameters).each do |result|
138
127
  value = result.value
139
128
  next unless value.is_a?(Hash)
140
129
  next unless value.key?('_sensitive')
141
130
  value['_sensitive'] = value['_sensitive'].unwrap
142
131
  end
143
- [task_result, nil]
144
132
  end
145
133
 
146
134
  def run_command(target, body)
147
- error = validate_schema(@schemas["action-run_command"], body)
148
- return [], error unless error.nil?
149
-
135
+ validate_schema(@schemas["action-run_command"], body)
150
136
  command = body['command']
151
- [@executor.run_command(target, command), nil]
137
+ @executor.run_command(target, command)
152
138
  end
153
139
 
154
140
  def check_node_connections(targets, body)
155
- error = validate_schema(@schemas["action-check_node_connections"], body)
156
- return [], error unless error.nil?
141
+ validate_schema(@schemas["action-check_node_connections"], body)
157
142
 
158
143
  # Puppet Enterprise's orchestrator service uses the
159
144
  # check_node_connections endpoint to check whether nodes that should be
@@ -161,13 +146,11 @@ module BoltServer
161
146
  # because the endpoint is meant to be used for a single check of all
162
147
  # nodes; External implementations of wait_until_available (like
163
148
  # orchestrator's) should contact the endpoint in their own loop.
164
- [@executor.wait_until_available(targets, wait_time: 0), nil]
149
+ @executor.wait_until_available(targets, wait_time: 0)
165
150
  end
166
151
 
167
152
  def upload_file(target, body)
168
- error = validate_schema(@schemas["action-upload_file"], body)
169
- return [], error unless error.nil?
170
-
153
+ validate_schema(@schemas["action-upload_file"], body)
171
154
  files = body['files']
172
155
  destination = body['destination']
173
156
  job_id = body['job_id']
@@ -190,8 +173,7 @@ module BoltServer
190
173
  # Create directory in cache so we can move files in.
191
174
  FileUtils.mkdir_p(path)
192
175
  else
193
- return [400, Bolt::Error.new("Invalid `kind` of '#{kind}' supplied. Must be `file` or `directory`.",
194
- 'boltserver/schema-error').to_json]
176
+ raise BoltServer::RequestError, "Invalid kind: '#{kind}' supplied. Must be 'file' or 'directory'."
195
177
  end
196
178
  end
197
179
  # We need to special case the scenario where only one file was
@@ -205,17 +187,14 @@ module BoltServer
205
187
  else
206
188
  cache_dir
207
189
  end
208
- [@executor.upload_file(target, upload_source, destination), nil]
190
+ @executor.upload_file(target, upload_source, destination)
209
191
  end
210
192
 
211
193
  def run_script(target, body)
212
- error = validate_schema(@schemas["action-run_script"], body)
213
- return [], error unless error.nil?
214
-
194
+ validate_schema(@schemas["action-run_script"], body)
215
195
  # Download the file onto the machine.
216
196
  file_location = @file_cache.update_file(body['script'])
217
-
218
- [@executor.run_script(target, file_location, body['arguments'])]
197
+ @executor.run_script(target, file_location, body['arguments'])
219
198
  end
220
199
 
221
200
  # This function is nearly identical to Bolt::Pal's `with_puppet_settings` with the
@@ -248,16 +227,14 @@ module BoltServer
248
227
  codedir = @config['environments-codedir'] || DEFAULT_BOLT_CODEDIR
249
228
  environmentpath = @config['environmentpath'] || "#{codedir}/environments"
250
229
  basemodulepath = @config['basemodulepath'] || "#{codedir}/modules:/opt/puppetlabs/puppet/modules"
251
- modulepath_dirs = nil
252
230
  with_pe_pal_init_settings(codedir, environmentpath, basemodulepath) do
253
231
  environment = Puppet.lookup(:environments).get!(environment_name)
254
- modulepath_dirs = environment.modulepath
232
+ environment.modulepath
255
233
  end
256
- modulepath_dirs
257
234
  end
258
235
 
259
236
  def in_pe_pal_env(environment)
260
- return [400, '`environment` is a required argument'] if environment.nil?
237
+ raise BoltServer::RequestError, "'environment' is a required argument" if environment.nil?
261
238
  @pal_mutex.synchronize do
262
239
  modulepath_obj = Bolt::Config::Modulepath.new(
263
240
  modulepath_from_environment(environment),
@@ -266,18 +243,16 @@ module BoltServer
266
243
  pal = Bolt::PAL.new(modulepath_obj, nil, nil)
267
244
  yield pal
268
245
  rescue Puppet::Environments::EnvironmentNotFound
269
- [400, {
270
- "class" => 'bolt/unknown-environment',
271
- "message" => "Environment #{environment} not found"
272
- }.to_json]
273
- rescue Bolt::Error => e
274
- [400, e.to_json]
246
+ raise BoltServer::RequestError, "environment: '#{environment}' does not exist"
275
247
  end
276
248
  end
277
249
 
278
250
  def config_from_project(versioned_project)
279
251
  project_dir = File.join(@config['projects-dir'], versioned_project)
280
- raise Bolt::ValidationError, "`versioned_project`: #{project_dir} does not exist" unless Dir.exist?(project_dir)
252
+ unless Dir.exist?(project_dir)
253
+ raise BoltServer::RequestError,
254
+ "versioned_project: '#{project_dir}' does not exist"
255
+ end
281
256
  project = Bolt::Project.create_project(project_dir)
282
257
  Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
283
258
  end
@@ -383,12 +358,15 @@ module BoltServer
383
358
  pal = pal_from_project_bolt_config(bolt_config)
384
359
  pal.in_bolt_compiler do
385
360
  mod = Puppet.lookup(:current_environment).module(module_name)
386
- raise ArgumentError, "`module_name`: #{module_name} does not exist" unless mod
361
+ raise BoltServer::RequestError, "module_name: '#{module_name}' does not exist" unless mod
387
362
  mod.file(file)
388
363
  end
389
364
  end
390
365
 
391
- raise ArgumentError, "`file`: #{file} does not exist inside the module's 'files' directory" unless abs_file_path
366
+ unless abs_file_path
367
+ raise BoltServer::RequestError,
368
+ "file: '#{file}' does not exist inside the module's 'files' directory"
369
+ end
392
370
 
393
371
  fileset = Puppet::FileServing::Fileset.new(abs_file_path, 'recurse' => 'yes')
394
372
  Puppet::FileServing::Fileset.merge(fileset).collect do |relative_file_path, base_path|
@@ -466,17 +444,15 @@ module BoltServer
466
444
  content_type :json
467
445
  body = JSON.parse(request.body.read)
468
446
 
469
- error = validate_schema(@schemas["transport-ssh"], body)
470
- return [400, error_result(error).to_json] unless error.nil?
447
+ validate_schema(@schemas["transport-ssh"], body)
471
448
 
472
449
  targets = (body['targets'] || [body['target']]).map do |target|
473
450
  make_ssh_target(target)
474
451
  end
475
452
 
476
- result_set, error = method(params[:action]).call(targets, body)
477
- return [400, error.to_json] unless error.nil?
453
+ result_set = method(params[:action]).call(targets, body)
478
454
 
479
- aggregate = body['target'].nil?
455
+ aggregate = params[:action] == 'check_node_connections'
480
456
  [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
481
457
  end
482
458
 
@@ -506,17 +482,15 @@ module BoltServer
506
482
  content_type :json
507
483
  body = JSON.parse(request.body.read)
508
484
 
509
- error = validate_schema(@schemas["transport-winrm"], body)
510
- return [400, error_result(error).to_json] unless error.nil?
485
+ validate_schema(@schemas["transport-winrm"], body)
511
486
 
512
487
  targets = (body['targets'] || [body['target']]).map do |target|
513
488
  make_winrm_target(target)
514
489
  end
515
490
 
516
- result_set, error = method(params[:action]).call(targets, body)
517
- return [400, error.to_json] if error
491
+ result_set = method(params[:action]).call(targets, body)
518
492
 
519
- aggregate = body['target'].nil?
493
+ aggregate = params[:action] == 'check_node_connections'
520
494
  [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
521
495
  end
522
496
 
@@ -534,14 +508,12 @@ module BoltServer
534
508
  #
535
509
  # @param versioned_project [String] the project to fetch the plan from
536
510
  get '/project_plans/:module_name/:plan_name' do
537
- return MISSING_VERSIONED_PROJECT_RESPONSE if params['versioned_project'].nil?
511
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
538
512
  in_bolt_project(params['versioned_project']) do |context|
539
513
  plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
540
514
  plan_info = allowed_helper(context[:pal], plan_info, context[:config].project.plans)
541
515
  [200, plan_info.to_json]
542
516
  end
543
- rescue Bolt::Error => e
544
- [400, e.to_json]
545
517
  end
546
518
 
547
519
  # Fetches the metadata for a single task
@@ -561,7 +533,7 @@ module BoltServer
561
533
  #
562
534
  # @param bolt_versioned_project [String] the reference to the bolt-project directory to load task metadata from
563
535
  get '/project_tasks/:module_name/:task_name' do
564
- return MISSING_VERSIONED_PROJECT_RESPONSE if params['versioned_project'].nil?
536
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
565
537
  in_bolt_project(params['versioned_project']) do |context|
566
538
  ps_parameters = {
567
539
  'versioned_project' => params['versioned_project']
@@ -570,8 +542,6 @@ module BoltServer
570
542
  task_info = allowed_helper(context[:pal], task_info, context[:config].project.tasks)
571
543
  [200, task_info.to_json]
572
544
  end
573
- rescue Bolt::Error => e
574
- [400, e.to_json]
575
545
  end
576
546
 
577
547
  # Fetches the list of plans for an environment, optionally fetching all metadata for each plan
@@ -602,7 +572,7 @@ module BoltServer
602
572
  #
603
573
  # @param versioned_project [String] the project to fetch the list of plans from
604
574
  get '/project_plans' do
605
- return MISSING_VERSIONED_PROJECT_RESPONSE if params['versioned_project'].nil?
575
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
606
576
  in_bolt_project(params['versioned_project']) do |context|
607
577
  plans_response = plan_list(context[:pal])
608
578
 
@@ -615,8 +585,6 @@ module BoltServer
615
585
  # to bolt-server smaller/simpler.
616
586
  [200, plans_response.to_json]
617
587
  end
618
- rescue Bolt::Error => e
619
- [400, e.to_json]
620
588
  end
621
589
 
622
590
  # Fetches the list of tasks for an environment
@@ -638,7 +606,7 @@ module BoltServer
638
606
  #
639
607
  # @param versioned_project [String] the project to fetch the list of tasks from
640
608
  get '/project_tasks' do
641
- return MISSING_VERSIONED_PROJECT_RESPONSE if params['versioned_project'].nil?
609
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
642
610
  in_bolt_project(params['versioned_project']) do |context|
643
611
  tasks_response = task_list(context[:pal])
644
612
 
@@ -651,34 +619,28 @@ module BoltServer
651
619
  # to bolt-server smaller/simpler.
652
620
  [200, tasks_response.to_json]
653
621
  end
654
- rescue Bolt::Error => e
655
- [400, e.to_json]
656
622
  end
657
623
 
658
624
  # Implements puppetserver's file_metadatas endpoint for projects.
659
625
  #
660
626
  # @param versioned_project [String] the versioned_project to fetch the file metadatas from
661
627
  get '/project_file_metadatas/:module_name/*' do
662
- versioned_project = params['versioned_project']
663
- return MISSING_VERSIONED_PROJECT_RESPONSE if versioned_project.nil?
628
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
664
629
  file = params[:splat].first
665
- metadatas = file_metadatas(versioned_project, params[:module_name], file)
630
+ metadatas = file_metadatas(params['versioned_project'], params[:module_name], file)
666
631
  [200, metadatas.to_json]
667
- rescue Bolt::Error => e
668
- [400, e.to_json]
669
632
  rescue ArgumentError => e
670
- [400, e.message]
633
+ [500, e.message]
671
634
  end
672
635
 
673
636
  # Returns a list of targets parsed from a Project inventory
674
637
  #
675
638
  # @param versioned_project [String] the versioned_project to compute the inventory from
676
639
  post '/project_inventory_targets' do
677
- return MISSING_VERSIONED_PROJECT_RESPONSE if params['versioned_project'].nil?
640
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
678
641
  content_type :json
679
642
  body = JSON.parse(request.body.read)
680
- error = validate_schema(@schemas["connect-data"], body)
681
- return [400, error_result(error).to_json] unless error.nil?
643
+ validate_schema(@schemas["connect-data"], body)
682
644
  in_bolt_project(params['versioned_project']) do |context|
683
645
  if context[:config].inventoryfile &&
684
646
  context[:config].project.inventory_file.to_s !=
@@ -717,8 +679,70 @@ module BoltServer
717
679
 
718
680
  [200, target_list.to_json]
719
681
  end
720
- rescue Bolt::Error => e
721
- [500, e.to_json]
682
+ end
683
+
684
+ # Returns the base64 encoded tar archive of plugin code that is needed to calculate
685
+ # custom facts
686
+ #
687
+ # @param versioned_project [String] the versioned_project to build the plugin tarball from
688
+ get '/project_facts_plugin_tarball' do
689
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
690
+ content_type :json
691
+
692
+ # Inspired by Bolt::Applicator.build_plugin_tarball
693
+ start_time = Time.now
694
+
695
+ # Fetch the plugin files
696
+ plugin_files = in_bolt_project(params['versioned_project']) do |context|
697
+ files = {}
698
+
699
+ # Bolt also sets plugin_modulepath to user modulepath so do it here too for
700
+ # consistency
701
+ plugin_modulepath = context[:pal].user_modulepath
702
+ Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
703
+ search_dirs = []
704
+ search_dirs << mod.plugins if mod.plugins?
705
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
706
+
707
+ files[mod] ||= []
708
+ Find.find(*search_dirs).each do |file|
709
+ files[mod] << file if File.file?(file)
710
+ end
711
+ end
712
+
713
+ files
714
+ end
715
+
716
+ # Pack the plugin files
717
+ sio = StringIO.new
718
+ begin
719
+ output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
720
+
721
+ plugin_files.each do |mod, files|
722
+ tar_dir = Pathname.new(mod.name)
723
+ mod_dir = Pathname.new(mod.path)
724
+
725
+ files.each do |file|
726
+ tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
727
+ stat = File.stat(file)
728
+ content = File.binread(file)
729
+ output.tar.add_file_simple(
730
+ tar_path.to_s,
731
+ data: content,
732
+ size: content.size,
733
+ mode: stat.mode & 0o777,
734
+ mtime: stat.mtime
735
+ )
736
+ end
737
+ end
738
+
739
+ duration = Time.now - start_time
740
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
741
+ ensure
742
+ output.close
743
+ end
744
+
745
+ [200, Base64.encode64(sio.string).to_json]
722
746
  end
723
747
 
724
748
  error 404 do
@@ -727,6 +751,20 @@ module BoltServer
727
751
  [404, err.to_json]
728
752
  end
729
753
 
754
+ error BoltServer::RequestError do |err|
755
+ [400, err.to_json]
756
+ end
757
+
758
+ error Bolt::Error do |err|
759
+ # In order to match the request code pattern, unknown plan/task content should 400. This also
760
+ # gives us an opportunity to trim the message instructing users to use CLI to show available content.
761
+ if ['bolt/unknown-plan', 'bolt/unknown-task'].include?(err.kind)
762
+ [404, BoltServer::RequestError.new(err.msg.split('.').first).to_json]
763
+ else
764
+ [500, err.to_json]
765
+ end
766
+ end
767
+
730
768
  error StandardError do
731
769
  e = env['sinatra.error']
732
770
  err = Bolt::Error.new("500: Unknown error: #{e.message}",