bolt 2.24.1 → 2.29.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/lib/bolt/analytics.rb +7 -3
  7. data/lib/bolt/applicator.rb +21 -21
  8. data/lib/bolt/bolt_option_parser.rb +77 -26
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +135 -147
  11. data/lib/bolt/config.rb +48 -25
  12. data/lib/bolt/config/options.rb +34 -2
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +8 -1
  15. data/lib/bolt/inventory/group.rb +1 -1
  16. data/lib/bolt/inventory/inventory.rb +1 -1
  17. data/lib/bolt/inventory/target.rb +1 -1
  18. data/lib/bolt/logger.rb +35 -21
  19. data/lib/bolt/outputter/logger.rb +1 -1
  20. data/lib/bolt/pal.rb +21 -10
  21. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  22. data/lib/bolt/plugin/puppetdb.rb +1 -1
  23. data/lib/bolt/project.rb +62 -17
  24. data/lib/bolt/puppetdb/client.rb +1 -1
  25. data/lib/bolt/puppetdb/config.rb +1 -1
  26. data/lib/bolt/puppetfile.rb +160 -0
  27. data/lib/bolt/puppetfile/installer.rb +43 -0
  28. data/lib/bolt/puppetfile/module.rb +89 -0
  29. data/lib/bolt/r10k_log_proxy.rb +1 -1
  30. data/lib/bolt/rerun.rb +2 -2
  31. data/lib/bolt/result.rb +23 -0
  32. data/lib/bolt/shell.rb +1 -1
  33. data/lib/bolt/task.rb +1 -1
  34. data/lib/bolt/transport/base.rb +5 -5
  35. data/lib/bolt/transport/docker/connection.rb +1 -1
  36. data/lib/bolt/transport/local/connection.rb +1 -1
  37. data/lib/bolt/transport/ssh.rb +1 -1
  38. data/lib/bolt/transport/ssh/connection.rb +1 -1
  39. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  40. data/lib/bolt/transport/winrm.rb +1 -1
  41. data/lib/bolt/transport/winrm/connection.rb +1 -1
  42. data/lib/bolt/util.rb +30 -11
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -12
  47. data/lib/bolt_server/transport_app.rb +125 -26
  48. data/lib/bolt_spec/bolt_context.rb +4 -4
  49. data/lib/bolt_spec/run.rb +3 -0
  50. metadata +11 -14
@@ -7,7 +7,7 @@ module Bolt
7
7
  def initialize
8
8
  super('bolt')
9
9
 
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
13
13
  def canonical_log(event)
@@ -8,7 +8,7 @@ module Bolt
8
8
  def initialize(path, save_failures)
9
9
  @path = path
10
10
  @save_failures = save_failures
11
- @logger = Logging.logger[self]
11
+ @logger = Bolt::Logger.logger(self)
12
12
  end
13
13
 
14
14
  def data
@@ -53,7 +53,7 @@ module Bolt
53
53
  end
54
54
  end
55
55
  rescue StandardError => e
56
- @logger.warn("Failed to save result to #{@path}: #{e.message}")
56
+ Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
57
57
  end
58
58
  end
59
59
  end
@@ -65,6 +65,25 @@ module Bolt
65
65
  'msg' => msg,
66
66
  'details' => { 'exit_code' => exit_code } }
67
67
  end
68
+
69
+ if value.key?('_error')
70
+ unless value['_error'].is_a?(Hash) && value['_error'].key?('msg')
71
+ value['_error'] = {
72
+ 'msg' => "Invalid error returned from task #{task}: #{value['_error'].inspect}. Error "\
73
+ "must be an object with a msg key.",
74
+ 'kind' => 'bolt/invalid-task-error',
75
+ 'details' => { 'original_error' => value['_error'] }
76
+ }
77
+ end
78
+
79
+ value['_error']['kind'] ||= 'bolt/error'
80
+ value['_error']['details'] ||= {}
81
+ end
82
+
83
+ if value.key?('_sensitive')
84
+ value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value['_sensitive'])
85
+ end
86
+
68
87
  new(target, value: value, action: 'task', object: task)
69
88
  end
70
89
 
@@ -205,5 +224,9 @@ module Bolt
205
224
 
206
225
  end
207
226
  end
227
+
228
+ def sensitive
229
+ value['_sensitive']
230
+ end
208
231
  end
209
232
  end
@@ -7,7 +7,7 @@ module Bolt
7
7
  def initialize(target, conn)
8
8
  @target = target
9
9
  @conn = conn
10
- @logger = Logging.logger[@target.safe_name]
10
+ @logger = Bolt::Logger.logger(@target.safe_name)
11
11
  end
12
12
 
13
13
  def run_command(*_args)
@@ -26,7 +26,7 @@ module Bolt
26
26
  @metadata = metadata
27
27
  @files = files
28
28
  @remote = remote
29
- @logger = Logging.logger[self]
29
+ @logger = Bolt::Logger.logger(self)
30
30
 
31
31
  validate_metadata
32
32
  end
@@ -40,17 +40,17 @@ module Bolt
40
40
  attr_reader :logger
41
41
 
42
42
  def initialize
43
- @logger = Logging.logger[self]
43
+ @logger = Bolt::Logger.logger(self)
44
44
  end
45
45
 
46
46
  def with_events(target, callback, action)
47
47
  callback&.call(type: :node_start, target: target)
48
48
 
49
49
  result = begin
50
- yield
51
- rescue StandardError, NotImplementedError => e
52
- Bolt::Result.from_exception(target, e, action: action)
53
- end
50
+ yield
51
+ rescue StandardError, NotImplementedError => e
52
+ Bolt::Result.from_exception(target, e, action: action)
53
+ end
54
54
 
55
55
  callback&.call(type: :node_result, result: result)
56
56
  result
@@ -10,7 +10,7 @@ module Bolt
10
10
  def initialize(target)
11
11
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
12
12
  @target = target
13
- @logger = Logging.logger[target.safe_name]
13
+ @logger = Bolt::Logger.logger(target.safe_name)
14
14
  @docker_host = @target.options['service-url']
15
15
  @logger.trace("Initializing docker connection to #{@target.safe_name}")
16
16
  end
@@ -16,7 +16,7 @@ module Bolt
16
16
  @target = target
17
17
  # The familiar problem: Etc.getlogin is broken on osx
18
18
  @user = ENV['USER'] || Etc.getlogin
19
- @logger = Logging.logger[self]
19
+ @logger = Bolt::Logger.logger(self)
20
20
  end
21
21
 
22
22
  def shell
@@ -17,7 +17,7 @@ module Bolt
17
17
  rescue LoadError
18
18
  logger.debug("Authentication method 'gssapi-with-mic' (Kerberos) is not available.")
19
19
  end
20
- @transport_logger = Logging.logger[Net::SSH]
20
+ @transport_logger = Bolt::Logger.logger(Net::SSH)
21
21
  @transport_logger.level = :warn
22
22
  end
23
23
 
@@ -26,7 +26,7 @@ module Bolt
26
26
  @user = @target.user || ssh_config[:user] || Etc.getlogin
27
27
  @strict_host_key_checking = ssh_config[:strict_host_key_checking]
28
28
 
29
- @logger = Logging.logger[@target.safe_name]
29
+ @logger = Bolt::Logger.logger(@target.safe_name)
30
30
  @transport_logger = transport_logger
31
31
  @logger.trace("Initializing ssh connection to #{@target.safe_name}")
32
32
 
@@ -14,7 +14,7 @@ module Bolt
14
14
  @target = target
15
15
  ssh_config = Net::SSH::Config.for(target.host)
16
16
  @user = @target.user || ssh_config[:user] || Etc.getlogin
17
- @logger = Logging.logger[self]
17
+ @logger = Bolt::Logger.logger(self)
18
18
  end
19
19
 
20
20
  # This is used to verify we can connect to targets with `connected?`
@@ -11,7 +11,7 @@ module Bolt
11
11
  require 'winrm'
12
12
  require 'winrm-fs'
13
13
 
14
- @transport_logger = Logging.logger[::WinRM]
14
+ @transport_logger = Bolt::Logger.logger(::WinRM)
15
15
  @transport_logger.level = :warn
16
16
  end
17
17
 
@@ -18,7 +18,7 @@ module Bolt
18
18
  @user = @target.user
19
19
  # Build set of extensions from extensions config as well as interpreters
20
20
 
21
- @logger = Logging.logger[@target.safe_name]
21
+ @logger = Bolt::Logger.logger(@target.safe_name)
22
22
  logger.trace("Initializing winrm connection to #{@target.safe_name}")
23
23
  @transport_logger = transport_logger
24
24
  end
@@ -3,10 +3,29 @@
3
3
  module Bolt
4
4
  module Util
5
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
+
6
25
  def read_yaml_hash(path, file_name)
7
26
  require 'yaml'
8
27
 
9
- logger = Logging.logger[self]
28
+ logger = Bolt::Logger.logger(self)
10
29
  path = File.expand_path(path)
11
30
  content = File.open(path, "r:UTF-8") { |f| YAML.safe_load(f.read) } || {}
12
31
  unless content.is_a?(Hash)
@@ -179,16 +198,16 @@ module Bolt
179
198
  # object was frozen
180
199
  frozen = obj.frozen?
181
200
  cl = begin
182
- obj.clone(freeze: false)
183
- # Some datatypes, such as FalseClass, can't be unfrozen. These
184
- # aren't the types we recurse on, so we can leave them frozen
185
- rescue ArgumentError => e
186
- if e.message =~ /can't unfreeze/
187
- obj.clone
188
- else
189
- raise e
190
- end
191
- end
201
+ obj.clone(freeze: false)
202
+ # Some datatypes, such as FalseClass, can't be unfrozen. These
203
+ # aren't the types we recurse on, so we can leave them frozen
204
+ rescue ArgumentError => e
205
+ if e.message =~ /can't unfreeze/
206
+ obj.clone
207
+ else
208
+ raise e
209
+ end
210
+ end
192
211
  rescue *error_types
193
212
  cloned[obj.object_id] = obj
194
213
  obj
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.24.1'
4
+ VERSION = '2.29.0'
5
5
  end
@@ -7,7 +7,7 @@ module BoltServer
7
7
  class BaseConfig
8
8
  def config_keys
9
9
  %w[host port ssl-cert ssl-key ssl-ca-cert
10
- ssl-cipher-suites loglevel logfile whitelist]
10
+ ssl-cipher-suites loglevel logfile whitelist projects-dir]
11
11
  end
12
12
 
13
13
  def env_keys
@@ -7,7 +7,7 @@ require 'bolt/error'
7
7
  module BoltServer
8
8
  class Config < BoltServer::BaseConfig
9
9
  def config_keys
10
- super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri]
10
+ super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri projects-dir]
11
11
  end
12
12
 
13
13
  def env_keys
@@ -33,7 +33,7 @@ module BoltServer
33
33
  @executor = executor
34
34
  @cache_dir = config['cache-dir']
35
35
  @config = config
36
- @logger = Logging.logger[self]
36
+ @logger = Bolt::Logger.logger(self)
37
37
  @cache_dir_mutex = cache_dir_mutex
38
38
 
39
39
  if do_purge
@@ -64,17 +64,17 @@ module BoltServer
64
64
 
65
65
  def client
66
66
  @client ||= begin
67
- uri = URI(@config['file-server-uri'])
68
- https = Net::HTTP.new(uri.host, uri.port)
69
- https.use_ssl = true
70
- https.ssl_version = :TLSv1_2
71
- https.ca_file = @config['ssl-ca-cert']
72
- https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
73
- https.key = OpenSSL::PKey::RSA.new(ssl_key)
74
- https.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
- https.open_timeout = @config['file-server-conn-timeout']
76
- https
77
- end
67
+ uri = URI(@config['file-server-uri'])
68
+ https = Net::HTTP.new(uri.host, uri.port)
69
+ https.use_ssl = true
70
+ https.ssl_version = :TLSv1_2
71
+ https.ca_file = @config['ssl-ca-cert']
72
+ https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
73
+ https.key = OpenSSL::PKey::RSA.new(ssl_key)
74
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
+ https.open_timeout = @config['file-server-conn-timeout']
76
+ https
77
+ end
78
78
  end
79
79
 
80
80
  def request_file(path, params, file)
@@ -5,6 +5,7 @@ require 'addressable/uri'
5
5
  require 'bolt'
6
6
  require 'bolt/error'
7
7
  require 'bolt/inventory'
8
+ require 'bolt/project'
8
9
  require 'bolt/target'
9
10
  require 'bolt_server/file_cache'
10
11
  require 'bolt/task/puppet_server'
@@ -191,20 +192,44 @@ module BoltServer
191
192
  end
192
193
 
193
194
  def in_pe_pal_env(environment)
194
- if environment.nil?
195
- [400, '`environment` is a required argument']
196
- else
197
- @pal_mutex.synchronize do
198
- pal = BoltServer::PE::PAL.new({}, environment)
199
- yield pal
200
- rescue Puppet::Environments::EnvironmentNotFound
201
- [400, {
202
- "class" => 'bolt/unknown-environment',
203
- "message" => "Environment #{environment} not found"
204
- }.to_json]
205
- rescue Bolt::Error => e
206
- [400, e.to_json]
207
- end
195
+ return [400, '`environment` is a required argument'] if environment.nil?
196
+ @pal_mutex.synchronize do
197
+ pal = BoltServer::PE::PAL.new({}, environment)
198
+ yield pal
199
+ rescue Puppet::Environments::EnvironmentNotFound
200
+ [400, {
201
+ "class" => 'bolt/unknown-environment',
202
+ "message" => "Environment #{environment} not found"
203
+ }.to_json]
204
+ rescue Bolt::Error => e
205
+ [400, e.to_json]
206
+ end
207
+ end
208
+
209
+ def in_bolt_project(bolt_project)
210
+ return [400, '`project_ref` is a required argument'] if bolt_project.nil?
211
+ project_dir = File.join(@config['projects-dir'], bolt_project)
212
+ return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
213
+ @pal_mutex.synchronize do
214
+ project = Bolt::Project.create_project(project_dir)
215
+ bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
216
+ pal = Bolt::PAL.new(bolt_config.modulepath, nil, nil, nil, nil, nil, bolt_config.project)
217
+ module_path = [
218
+ BoltServer::PE::PAL::PE_BOLTLIB_PATH,
219
+ Bolt::PAL::BOLTLIB_PATH,
220
+ *bolt_config.modulepath,
221
+ Bolt::PAL::MODULES_PATH
222
+ ]
223
+ # CODEREVIEW: I *think* this is the only thing we need to make different between bolt's PAL. Is it acceptable
224
+ # to hack this? Modulepath is currently a readable attribute, could we make it writeable?
225
+ pal.instance_variable_set(:@modulepath, module_path)
226
+ context = {
227
+ pal: pal,
228
+ config: bolt_config
229
+ }
230
+ yield context
231
+ rescue Bolt::Error => e
232
+ [400, e.to_json]
208
233
  end
209
234
  end
210
235
 
@@ -221,14 +246,12 @@ module BoltServer
221
246
  plan_info
222
247
  end
223
248
 
224
- def build_puppetserver_uri(file_identifier, module_name, environment)
249
+ def build_puppetserver_uri(file_identifier, module_name, parameters)
225
250
  segments = file_identifier.split('/', 3)
226
251
  if segments.size == 1
227
252
  {
228
253
  'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
229
- 'params' => {
230
- 'environment' => environment
231
- }
254
+ 'params' => parameters
232
255
  }
233
256
  else
234
257
  module_segment, mount_segment, name_segment = *segments
@@ -241,14 +264,12 @@ module BoltServer
241
264
  when 'lib'
242
265
  "/puppet/v3/file_content/plugins/#{name_segment}"
243
266
  end,
244
- 'params' => {
245
- 'environment' => environment
246
- }
267
+ 'params' => parameters
247
268
  }
248
269
  end
249
270
  end
250
271
 
251
- def pe_task_info(pal, module_name, task_name, environment)
272
+ def pe_task_info(pal, module_name, task_name, parameters)
252
273
  # Handle case where task name is simply module name with special `init` task
253
274
  task_name = if task_name == 'init' || task_name.nil?
254
275
  module_name
@@ -261,7 +282,7 @@ module BoltServer
261
282
  'filename' => file_hash['name'],
262
283
  'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
263
284
  'size_bytes' => File.size(file_hash['path']),
264
- 'uri' => build_puppetserver_uri(file_hash['name'], module_name, environment)
285
+ 'uri' => build_puppetserver_uri(file_hash['name'], module_name, parameters)
265
286
  }
266
287
  end
267
288
  {
@@ -271,6 +292,21 @@ module BoltServer
271
292
  }
272
293
  end
273
294
 
295
+ def allowed_helper(metadata, allowlist)
296
+ allowed = allowlist.nil? || allowlist.include?(metadata['name']) ? true : false
297
+ metadata.merge({ 'allowed' => allowed })
298
+ end
299
+
300
+ def task_list(pal)
301
+ tasks = pal.list_tasks
302
+ tasks.map { |task_name, _description| { 'name' => task_name } }
303
+ end
304
+
305
+ def plan_list(pal)
306
+ plans = pal.list_plans.flatten
307
+ plans.map { |plan_name| { 'name' => plan_name } }
308
+ end
309
+
274
310
  get '/' do
275
311
  200
276
312
  end
@@ -401,12 +437,40 @@ module BoltServer
401
437
  end
402
438
  end
403
439
 
440
+ # Fetches the metadata for a single plan
441
+ #
442
+ # @param project_ref [String] the project to fetch the plan from
443
+ get '/project_plans/:module_name/:plan_name' do
444
+ in_bolt_project(params['project_ref']) do |context|
445
+ plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
446
+ plan_info = allowed_helper(plan_info, context[:config].project.plans)
447
+ [200, plan_info.to_json]
448
+ end
449
+ end
450
+
404
451
  # Fetches the metadata for a single task
405
452
  #
406
453
  # @param environment [String] the environment to fetch the task from
407
454
  get '/tasks/:module_name/:task_name' do
408
455
  in_pe_pal_env(params['environment']) do |pal|
409
- task_info = pe_task_info(pal, params[:module_name], params[:task_name], params['environment'])
456
+ ps_parameters = {
457
+ 'environment' => params['environment']
458
+ }
459
+ task_info = pe_task_info(pal, params[:module_name], params[:task_name], ps_parameters)
460
+ [200, task_info.to_json]
461
+ end
462
+ end
463
+
464
+ # Fetches the metadata for a single task
465
+ #
466
+ # @param bolt_project_ref [String] the reference to the bolt-project directory to load task metadata from
467
+ get '/project_tasks/:module_name/:task_name' do
468
+ in_bolt_project(params['project_ref']) do |context|
469
+ ps_parameters = {
470
+ 'project' => params['project_ref']
471
+ }
472
+ task_info = pe_task_info(context[:pal], params[:module_name], params[:task_name], ps_parameters)
473
+ task_info = allowed_helper(task_info, context[:config].project.tasks)
410
474
  [200, task_info.to_json]
411
475
  end
412
476
  end
@@ -435,13 +499,30 @@ module BoltServer
435
499
  end
436
500
  end
437
501
 
502
+ # Fetches the list of plans for a project
503
+ #
504
+ # @param project_ref [String] the project to fetch the list of plans from
505
+ get '/project_plans' do
506
+ in_bolt_project(params['project_ref']) do |context|
507
+ plans_response = plan_list(context[:pal])
508
+
509
+ # Dig in context for the allowlist of plans from project object
510
+ plans_response.map! { |metadata| allowed_helper(metadata, context[:config].project.plans) }
511
+
512
+ # We structure this array of plans to be an array of hashes so that it matches the structure
513
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
514
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
515
+ # to bolt-server smaller/simpler.
516
+ [200, plans_response.to_json]
517
+ end
518
+ end
519
+
438
520
  # Fetches the list of tasks for an environment
439
521
  #
440
522
  # @param environment [String] the environment to fetch the list of tasks from
441
523
  get '/tasks' do
442
524
  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
525
+ tasks_response = task_list(pal).to_json
445
526
 
446
527
  # We structure this array of tasks to be an array of hashes so that it matches the structure
447
528
  # returned by the puppetserver API that serves data like this. Structuring the output this way
@@ -451,6 +532,24 @@ module BoltServer
451
532
  end
452
533
  end
453
534
 
535
+ # Fetches the list of tasks for a bolt-project
536
+ #
537
+ # @param project_ref [String] the project to fetch the list of tasks from
538
+ get '/project_tasks' do
539
+ in_bolt_project(params['project_ref']) do |context|
540
+ tasks_response = task_list(context[:pal])
541
+
542
+ # Dig in context for the allowlist of tasks from project object
543
+ tasks_response.map! { |metadata| allowed_helper(metadata, context[:config].project.tasks) }
544
+
545
+ # We structure this array of tasks to be an array of hashes so that it matches the structure
546
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
547
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
548
+ # to bolt-server smaller/simpler.
549
+ [200, tasks_response.to_json]
550
+ end
551
+ end
552
+
454
553
  error 404 do
455
554
  err = Bolt::Error.new("Could not find route #{request.path}",
456
555
  'boltserver/not-found')