bolt 2.26.0 → 2.31.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 +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/lib/bolt/analytics.rb +4 -0
  5. data/lib/bolt/applicator.rb +19 -18
  6. data/lib/bolt/bolt_option_parser.rb +112 -22
  7. data/lib/bolt/catalog.rb +1 -1
  8. data/lib/bolt/cli.rb +210 -174
  9. data/lib/bolt/config.rb +22 -2
  10. data/lib/bolt/config/modulepath.rb +30 -0
  11. data/lib/bolt/config/options.rb +30 -0
  12. data/lib/bolt/config/transport/options.rb +1 -1
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +11 -10
  15. data/lib/bolt/logger.rb +26 -19
  16. data/lib/bolt/module_installer.rb +242 -0
  17. data/lib/bolt/outputter.rb +4 -0
  18. data/lib/bolt/outputter/human.rb +77 -17
  19. data/lib/bolt/outputter/json.rb +21 -6
  20. data/lib/bolt/outputter/logger.rb +2 -2
  21. data/lib/bolt/pal.rb +46 -25
  22. data/lib/bolt/plugin.rb +1 -1
  23. data/lib/bolt/plugin/module.rb +1 -1
  24. data/lib/bolt/project.rb +62 -12
  25. data/lib/bolt/project_migrator.rb +80 -0
  26. data/lib/bolt/project_migrator/base.rb +39 -0
  27. data/lib/bolt/project_migrator/config.rb +67 -0
  28. data/lib/bolt/project_migrator/inventory.rb +67 -0
  29. data/lib/bolt/project_migrator/modules.rb +198 -0
  30. data/lib/bolt/puppetfile.rb +149 -0
  31. data/lib/bolt/puppetfile/installer.rb +43 -0
  32. data/lib/bolt/puppetfile/module.rb +93 -0
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/result.rb +15 -0
  35. data/lib/bolt/shell/bash.rb +4 -3
  36. data/lib/bolt/transport/base.rb +4 -4
  37. data/lib/bolt/transport/ssh/connection.rb +1 -1
  38. data/lib/bolt/util.rb +51 -10
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/acl.rb +2 -2
  41. data/lib/bolt_server/base_config.rb +3 -3
  42. data/lib/bolt_server/config.rb +1 -1
  43. data/lib/bolt_server/file_cache.rb +11 -11
  44. data/lib/bolt_server/transport_app.rb +206 -27
  45. data/lib/bolt_spec/bolt_context.rb +8 -6
  46. data/lib/bolt_spec/plans.rb +1 -1
  47. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  48. data/lib/bolt_spec/run.rb +1 -1
  49. metadata +14 -6
  50. data/lib/bolt/project_migrate.rb +0 -138
  51. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.26.0'
4
+ VERSION = '2.31.0'
5
5
  end
@@ -16,9 +16,9 @@ module BoltServer
16
16
  end
17
17
  end
18
18
 
19
- def initialize(app, whitelist)
19
+ def initialize(app, allowlist)
20
20
  acls = []
21
- whitelist.each do |entry|
21
+ allowlist.each do |entry|
22
22
  acls << {
23
23
  'resources' => [
24
24
  {
@@ -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 allowlist projects-dir]
11
11
  end
12
12
 
13
13
  def env_keys
@@ -98,8 +98,8 @@ module BoltServer
98
98
  raise Bolt::ValidationError, "Configured 'ssl-cipher-suites' must be an array of cipher suite names"
99
99
  end
100
100
 
101
- unless @data['whitelist'].nil? || @data['whitelist'].is_a?(Array)
102
- raise Bolt::ValidationError, "Configured 'whitelist' must be an array of names"
101
+ unless @data['allowlist'].nil? || @data['allowlist'].is_a?(Array)
102
+ raise Bolt::ValidationError, "Configured 'allowlist' must be an array of names"
103
103
  end
104
104
  end
105
105
 
@@ -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
@@ -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'
@@ -13,7 +14,9 @@ require 'json-schema'
13
14
 
14
15
  # These are only needed for the `/plans` endpoint.
15
16
  require 'puppet'
16
- require 'bolt_server/pe/pal'
17
+
18
+ # Needed by the `/project_file_metadatas` endpoint
19
+ require 'puppet/file_serving/fileset'
17
20
 
18
21
  module BoltServer
19
22
  class TransportApp < Sinatra::Base
@@ -34,6 +37,17 @@ module BoltServer
34
37
  transport-winrm
35
38
  ].freeze
36
39
 
40
+ # PE_BOLTLIB_PATH is intended to function exactly like the BOLTLIB_PATH used
41
+ # in Bolt::PAL. Paths and variable names are similar to what exists in
42
+ # Bolt::PAL, but with a 'PE' prefix.
43
+ PE_BOLTLIB_PATH = '/opt/puppetlabs/server/apps/bolt-server/pe-bolt-modules'
44
+
45
+ # For now at least, we maintain an entirely separate codedir from
46
+ # puppetserver by default, so that filesync can work properly. If filesync
47
+ # is not used, this can instead match the usual puppetserver codedir.
48
+ # See the `orchestrator.bolt.codedir` tk config setting.
49
+ DEFAULT_BOLT_CODEDIR = '/opt/puppetlabs/server/data/orchestration-services/code'
50
+
37
51
  def initialize(config)
38
52
  @config = config
39
53
  @schemas = Hash[REQUEST_SCHEMAS.map do |basename|
@@ -190,21 +204,82 @@ module BoltServer
190
204
  [@executor.run_script(target, file_location, body['arguments'])]
191
205
  end
192
206
 
193
- 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
+ # This function is nearly identical to Bolt::Pal's `with_puppet_settings` with the
208
+ # one difference that we set the codedir to point to actual code, rather than the
209
+ # tmpdir. We only use this funtion inside the Modulepath initializer so that Puppet
210
+ # is correctly configured to pull environment configuration correctly. If we don't
211
+ # set codedir in this way: when we try to load and interpolate the modulepath it
212
+ # won't correctly load.
213
+ #
214
+ # WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
215
+ def with_pe_pal_init_settings(codedir, environmentpath, basemodulepath)
216
+ Dir.mktmpdir('pe-bolt') do |dir|
217
+ cli = []
218
+ Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
219
+ dir = setting == :codedir ? codedir : dir
220
+ cli << "--#{setting}" << dir
207
221
  end
222
+ cli << "--environmentpath" << environmentpath
223
+ cli << "--basemodulepath" << basemodulepath
224
+ Puppet.settings.send(:clear_everything_for_tests)
225
+ Puppet.initialize_settings(cli)
226
+ yield
227
+ end
228
+ end
229
+
230
+ # Use puppet to identify the modulepath from an environment.
231
+ #
232
+ # WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
233
+ def modulepath_from_environment(environment_name)
234
+ codedir = DEFAULT_BOLT_CODEDIR
235
+ environmentpath = "#{codedir}/environments"
236
+ basemodulepath = "#{codedir}/modules:/opt/puppetlabs/puppet/modules"
237
+ modulepath_dirs = nil
238
+ with_pe_pal_init_settings(codedir, environmentpath, basemodulepath) do
239
+ environment = Puppet.lookup(:environments).get!(environment_name)
240
+ modulepath_dirs = environment.modulepath
241
+ end
242
+ modulepath_dirs
243
+ end
244
+
245
+ def in_pe_pal_env(environment)
246
+ return [400, '`environment` is a required argument'] if environment.nil?
247
+ @pal_mutex.synchronize do
248
+ modulepath_obj = Bolt::Config::Modulepath.new(
249
+ modulepath_from_environment(environment),
250
+ boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
251
+ )
252
+ pal = Bolt::PAL.new(modulepath_obj, nil, nil)
253
+ yield pal
254
+ rescue Puppet::Environments::EnvironmentNotFound
255
+ [400, {
256
+ "class" => 'bolt/unknown-environment',
257
+ "message" => "Environment #{environment} not found"
258
+ }.to_json]
259
+ rescue Bolt::Error => e
260
+ [400, e.to_json]
261
+ end
262
+ end
263
+
264
+ def in_bolt_project(bolt_project)
265
+ return [400, '`project_ref` is a required argument'] if bolt_project.nil?
266
+ project_dir = File.join(@config['projects-dir'], bolt_project)
267
+ return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
268
+ @pal_mutex.synchronize do
269
+ project = Bolt::Project.create_project(project_dir)
270
+ bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
271
+ modulepath_object = Bolt::Config::Modulepath.new(
272
+ bolt_config.modulepath,
273
+ boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
274
+ )
275
+ pal = Bolt::PAL.new(modulepath_object, nil, nil, nil, nil, nil, bolt_config.project)
276
+ context = {
277
+ pal: pal,
278
+ config: bolt_config
279
+ }
280
+ yield context
281
+ rescue Bolt::Error => e
282
+ [400, e.to_json]
208
283
  end
209
284
  end
210
285
 
@@ -221,14 +296,12 @@ module BoltServer
221
296
  plan_info
222
297
  end
223
298
 
224
- def build_puppetserver_uri(file_identifier, module_name, environment)
299
+ def build_puppetserver_uri(file_identifier, module_name, parameters)
225
300
  segments = file_identifier.split('/', 3)
226
301
  if segments.size == 1
227
302
  {
228
303
  'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
229
- 'params' => {
230
- 'environment' => environment
231
- }
304
+ 'params' => parameters
232
305
  }
233
306
  else
234
307
  module_segment, mount_segment, name_segment = *segments
@@ -241,14 +314,12 @@ module BoltServer
241
314
  when 'lib'
242
315
  "/puppet/v3/file_content/plugins/#{name_segment}"
243
316
  end,
244
- 'params' => {
245
- 'environment' => environment
246
- }
317
+ 'params' => parameters
247
318
  }
248
319
  end
249
320
  end
250
321
 
251
- def pe_task_info(pal, module_name, task_name, environment)
322
+ def pe_task_info(pal, module_name, task_name, parameters)
252
323
  # Handle case where task name is simply module name with special `init` task
253
324
  task_name = if task_name == 'init' || task_name.nil?
254
325
  module_name
@@ -261,7 +332,7 @@ module BoltServer
261
332
  'filename' => file_hash['name'],
262
333
  'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
263
334
  'size_bytes' => File.size(file_hash['path']),
264
- 'uri' => build_puppetserver_uri(file_hash['name'], module_name, environment)
335
+ 'uri' => build_puppetserver_uri(file_hash['name'], module_name, parameters)
265
336
  }
266
337
  end
267
338
  {
@@ -271,6 +342,38 @@ module BoltServer
271
342
  }
272
343
  end
273
344
 
345
+ def allowed_helper(metadata, allowlist)
346
+ allowed = allowlist.nil? || allowlist.include?(metadata['name']) ? true : false
347
+ metadata.merge({ 'allowed' => allowed })
348
+ end
349
+
350
+ def task_list(pal)
351
+ tasks = pal.list_tasks
352
+ tasks.map { |task_name, _description| { 'name' => task_name } }
353
+ end
354
+
355
+ def plan_list(pal)
356
+ plans = pal.list_plans.flatten
357
+ plans.map { |plan_name| { 'name' => plan_name } }
358
+ end
359
+
360
+ def file_metadatas(pal, module_name, file)
361
+ pal.in_bolt_compiler do
362
+ mod = Puppet.lookup(:current_environment).module(module_name)
363
+ raise ArgumentError, "`module_name`: #{module_name} does not exist" unless mod
364
+ abs_file_path = mod.file(file)
365
+ raise ArgumentError, "`file`: #{file} does not exist inside the module's 'files' directory" unless abs_file_path
366
+ fileset = Puppet::FileServing::Fileset.new(abs_file_path, 'recurse' => 'yes')
367
+ Puppet::FileServing::Fileset.merge(fileset).collect do |relative_file_path, base_path|
368
+ metadata = Puppet::FileServing::Metadata.new(base_path, relative_path: relative_file_path)
369
+ metadata.checksum_type = 'sha256'
370
+ metadata.links = 'follow'
371
+ metadata.collect
372
+ metadata.to_data_hash
373
+ end
374
+ end
375
+ end
376
+
274
377
  get '/' do
275
378
  200
276
379
  end
@@ -401,12 +504,40 @@ module BoltServer
401
504
  end
402
505
  end
403
506
 
507
+ # Fetches the metadata for a single plan
508
+ #
509
+ # @param project_ref [String] the project to fetch the plan from
510
+ get '/project_plans/:module_name/:plan_name' do
511
+ in_bolt_project(params['project_ref']) do |context|
512
+ plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
513
+ plan_info = allowed_helper(plan_info, context[:config].project.plans)
514
+ [200, plan_info.to_json]
515
+ end
516
+ end
517
+
404
518
  # Fetches the metadata for a single task
405
519
  #
406
520
  # @param environment [String] the environment to fetch the task from
407
521
  get '/tasks/:module_name/:task_name' do
408
522
  in_pe_pal_env(params['environment']) do |pal|
409
- task_info = pe_task_info(pal, params[:module_name], params[:task_name], params['environment'])
523
+ ps_parameters = {
524
+ 'environment' => params['environment']
525
+ }
526
+ task_info = pe_task_info(pal, params[:module_name], params[:task_name], ps_parameters)
527
+ [200, task_info.to_json]
528
+ end
529
+ end
530
+
531
+ # Fetches the metadata for a single task
532
+ #
533
+ # @param bolt_project_ref [String] the reference to the bolt-project directory to load task metadata from
534
+ get '/project_tasks/:module_name/:task_name' do
535
+ in_bolt_project(params['project_ref']) do |context|
536
+ ps_parameters = {
537
+ 'project' => params['project_ref']
538
+ }
539
+ task_info = pe_task_info(context[:pal], params[:module_name], params[:task_name], ps_parameters)
540
+ task_info = allowed_helper(task_info, context[:config].project.tasks)
410
541
  [200, task_info.to_json]
411
542
  end
412
543
  end
@@ -435,13 +566,30 @@ module BoltServer
435
566
  end
436
567
  end
437
568
 
569
+ # Fetches the list of plans for a project
570
+ #
571
+ # @param project_ref [String] the project to fetch the list of plans from
572
+ get '/project_plans' do
573
+ in_bolt_project(params['project_ref']) do |context|
574
+ plans_response = plan_list(context[:pal])
575
+
576
+ # Dig in context for the allowlist of plans from project object
577
+ plans_response.map! { |metadata| allowed_helper(metadata, context[:config].project.plans) }
578
+
579
+ # We structure this array of plans to be an array of hashes so that it matches the structure
580
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
581
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
582
+ # to bolt-server smaller/simpler.
583
+ [200, plans_response.to_json]
584
+ end
585
+ end
586
+
438
587
  # Fetches the list of tasks for an environment
439
588
  #
440
589
  # @param environment [String] the environment to fetch the list of tasks from
441
590
  get '/tasks' do
442
591
  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
592
+ tasks_response = task_list(pal).to_json
445
593
 
446
594
  # We structure this array of tasks to be an array of hashes so that it matches the structure
447
595
  # returned by the puppetserver API that serves data like this. Structuring the output this way
@@ -451,6 +599,37 @@ module BoltServer
451
599
  end
452
600
  end
453
601
 
602
+ # Fetches the list of tasks for a bolt-project
603
+ #
604
+ # @param project_ref [String] the project to fetch the list of tasks from
605
+ get '/project_tasks' do
606
+ in_bolt_project(params['project_ref']) do |context|
607
+ tasks_response = task_list(context[:pal])
608
+
609
+ # Dig in context for the allowlist of tasks from project object
610
+ tasks_response.map! { |metadata| allowed_helper(metadata, context[:config].project.tasks) }
611
+
612
+ # We structure this array of tasks to be an array of hashes so that it matches the structure
613
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
614
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
615
+ # to bolt-server smaller/simpler.
616
+ [200, tasks_response.to_json]
617
+ end
618
+ end
619
+
620
+ # Implements puppetserver's file_metadatas endpoint for projects.
621
+ #
622
+ # @param project_ref [String] the project_ref to fetch the file metadatas from
623
+ get '/project_file_metadatas/:module_name/*' do
624
+ in_bolt_project(params['project_ref']) do |context|
625
+ file = params[:splat].first
626
+ metadatas = file_metadatas(context[:pal], params[:module_name], file)
627
+ [200, metadatas.to_json]
628
+ end
629
+ rescue ArgumentError => e
630
+ [400, e.message]
631
+ end
632
+
454
633
  error 404 do
455
634
  err = Bolt::Error.new("Could not find route #{request.path}",
456
635
  'boltserver/not-found')
@@ -105,7 +105,7 @@ module BoltSpec
105
105
 
106
106
  # Set the things
107
107
  Puppet[:tasks] = true
108
- RSpec.configuration.module_path = [modulepath, Bolt::PAL::BOLTLIB_PATH].join(File::PATH_SEPARATOR)
108
+ RSpec.configuration.module_path = [modulepath, Bolt::Config::Modulepath::BOLTLIB_PATH].join(File::PATH_SEPARATOR)
109
109
  opts = {
110
110
  bolt_executor: executor,
111
111
  bolt_inventory: inventory,
@@ -142,10 +142,10 @@ module BoltSpec
142
142
  # Override in your tests
143
143
  def config
144
144
  @config ||= begin
145
- conf = Bolt::Config.default
146
- conf.modulepath = [modulepath].flatten
147
- conf
148
- end
145
+ conf = Bolt::Config.default
146
+ conf.modulepath = [modulepath].flatten
147
+ conf
148
+ end
149
149
  end
150
150
 
151
151
  def plugins
@@ -153,7 +153,9 @@ module BoltSpec
153
153
  end
154
154
 
155
155
  def pal
156
- @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.project.resource_types)
156
+ @pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
157
+ config.hiera_config,
158
+ config.project.resource_types)
157
159
  end
158
160
 
159
161
  BoltSpec::Plans::MOCKED_ACTIONS.each do |action|
@@ -224,7 +224,7 @@ module BoltSpec
224
224
 
225
225
  def run_plan(name, params)
226
226
  pal = Bolt::PAL.new(
227
- config.modulepath,
227
+ Bolt::Config::Modulepath.new(config.modulepath),
228
228
  config.hiera_config,
229
229
  config.project.resource_types,
230
230
  config.compile_concurrency,
@@ -40,7 +40,7 @@ module BoltSpec
40
40
 
41
41
  def module_file_id(file)
42
42
  modpath = @modulepath.select { |path| file =~ /^#{path}/ }
43
- raise "Could not identify module path containing #{file}: #{modpath}" unless modpath.size == 1
43
+ raise "Could not identify modulepath containing #{file}: #{modpath}" unless modpath.size == 1
44
44
 
45
45
  path = Pathname.new(file)
46
46
  relative = path.relative_path_from(Pathname.new(modpath.first))