bolt 2.34.0 → 2.40.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  12. data/lib/bolt/analytics.rb +27 -8
  13. data/lib/bolt/apply_result.rb +3 -3
  14. data/lib/bolt/bolt_option_parser.rb +45 -18
  15. data/lib/bolt/cli.rb +98 -116
  16. data/lib/bolt/config.rb +184 -80
  17. data/lib/bolt/config/options.rb +148 -87
  18. data/lib/bolt/config/transport/base.rb +10 -19
  19. data/lib/bolt/config/transport/local.rb +1 -7
  20. data/lib/bolt/config/transport/options.rb +12 -69
  21. data/lib/bolt/config/transport/ssh.rb +8 -19
  22. data/lib/bolt/error.rb +24 -0
  23. data/lib/bolt/executor.rb +92 -18
  24. data/lib/bolt/inventory.rb +25 -0
  25. data/lib/bolt/inventory/group.rb +0 -8
  26. data/lib/bolt/inventory/options.rb +130 -0
  27. data/lib/bolt/inventory/target.rb +10 -11
  28. data/lib/bolt/module_installer.rb +21 -13
  29. data/lib/bolt/module_installer/resolver.rb +1 -1
  30. data/lib/bolt/outputter.rb +19 -5
  31. data/lib/bolt/outputter/human.rb +22 -3
  32. data/lib/bolt/outputter/json.rb +1 -1
  33. data/lib/bolt/outputter/logger.rb +1 -1
  34. data/lib/bolt/outputter/rainbow.rb +13 -2
  35. data/lib/bolt/pal.rb +18 -6
  36. data/lib/bolt/pal/yaml_plan.rb +7 -0
  37. data/lib/bolt/plugin.rb +41 -12
  38. data/lib/bolt/plugin/cache.rb +76 -0
  39. data/lib/bolt/plugin/module.rb +4 -4
  40. data/lib/bolt/plugin/puppetdb.rb +1 -1
  41. data/lib/bolt/project.rb +59 -40
  42. data/lib/bolt/project_manager.rb +201 -0
  43. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +49 -4
  44. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  45. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  46. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +5 -3
  47. data/lib/bolt/puppetdb/client.rb +11 -2
  48. data/lib/bolt/puppetdb/config.rb +4 -3
  49. data/lib/bolt/rerun.rb +1 -5
  50. data/lib/bolt/shell/bash.rb +8 -2
  51. data/lib/bolt/shell/powershell.rb +21 -3
  52. data/lib/bolt/target.rb +4 -0
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/local.rb +13 -0
  55. data/lib/bolt/transport/orch.rb +0 -5
  56. data/lib/bolt/transport/orch/connection.rb +10 -3
  57. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  58. data/lib/bolt/util.rb +36 -7
  59. data/lib/bolt/validator.rb +227 -0
  60. data/lib/bolt/version.rb +1 -1
  61. data/lib/bolt/yarn.rb +23 -0
  62. data/lib/bolt_server/base_config.rb +3 -1
  63. data/lib/bolt_server/config.rb +3 -1
  64. data/lib/bolt_server/plugin.rb +13 -0
  65. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  66. data/lib/bolt_server/schemas/connect-data.json +22 -0
  67. data/lib/bolt_server/schemas/partials/task.json +2 -2
  68. data/lib/bolt_server/transport_app.rb +82 -23
  69. data/lib/bolt_spec/plans/mock_executor.rb +4 -1
  70. data/libexec/apply_catalog.rb +1 -1
  71. data/libexec/custom_facts.rb +1 -1
  72. data/libexec/query_resources.rb +1 -1
  73. metadata +22 -14
  74. data/lib/bolt/project_migrator.rb +0 -80
@@ -21,7 +21,7 @@ require 'bolt/outputter'
21
21
  require 'bolt/pal'
22
22
  require 'bolt/plan_creator'
23
23
  require 'bolt/plugin'
24
- require 'bolt/project_migrator'
24
+ require 'bolt/project_manager'
25
25
  require 'bolt/puppetdb'
26
26
  require 'bolt/rerun'
27
27
  require 'bolt/secret'
@@ -30,6 +30,7 @@ require 'bolt/version'
30
30
 
31
31
  module Bolt
32
32
  class CLIExit < StandardError; end
33
+
33
34
  class CLI
34
35
  COMMANDS = {
35
36
  'command' => %w[run],
@@ -164,8 +165,9 @@ module Bolt
164
165
  elsif options[:configfile]
165
166
  Bolt::Config.from_file(options[:configfile], options)
166
167
  else
167
- project = if options[:boltdir]
168
- dir = Pathname.new(options[:boltdir])
168
+ cli_flag = options[:project] || options[:boltdir]
169
+ project = if cli_flag
170
+ dir = Pathname.new(cli_flag)
169
171
  if (dir + Bolt::Project::BOLTDIR_NAME).directory?
170
172
  Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
171
173
  else
@@ -195,7 +197,12 @@ module Bolt
195
197
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
196
198
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
197
199
 
200
+ if options[:clear_cache] && File.exist?(config.project.cache_file)
201
+ FileUtils.rm(config.project.cache_file)
202
+ end
203
+
198
204
  warn_inventory_overrides_cli(options)
205
+ validate_ps_version
199
206
 
200
207
  options
201
208
  rescue Bolt::Error => e
@@ -203,6 +210,17 @@ module Bolt
203
210
  raise e
204
211
  end
205
212
 
213
+ private def validate_ps_version
214
+ if Bolt::Util.powershell?
215
+ target = inventory.get_target('localhost')
216
+ Bolt::Transport::Local.new.with_connection(target) do |conn|
217
+ # This will automatically validate the powershell version on the Bolt
218
+ # controller
219
+ Bolt::Shell::Powershell.new(target, conn)
220
+ end
221
+ end
222
+ end
223
+
206
224
  def update_targets(options)
207
225
  target_opts = options.keys.select { |opt| %i[query rerun targets].include?(opt) }
208
226
  target_string = "'--targets', '--rerun', or '--query'"
@@ -293,7 +311,7 @@ module Bolt
293
311
  "Unknown argument(s) #{options[:leftovers].join(', ')}"
294
312
  end
295
313
 
296
- if options[:boltdir] && options[:configfile]
314
+ if options.slice(:boltdir, :configfile, :project).length > 1
297
315
  raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
298
316
  end
299
317
 
@@ -340,15 +358,10 @@ module Bolt
340
358
  def warn_inventory_overrides_cli(opts)
341
359
  inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
342
360
  Bolt::Inventory::ENVIRONMENT_VAR
343
- elsif config.inventoryfile && Bolt::Util.file_stat(config.inventoryfile)
361
+ elsif config.inventoryfile
344
362
  config.inventoryfile
345
- else
346
- begin
347
- Bolt::Util.file_stat(config.default_inventoryfile)
348
- config.default_inventoryfile
349
- rescue Errno::ENOENT
350
- nil
351
- end
363
+ elsif File.exist?(config.default_inventoryfile)
364
+ config.default_inventoryfile
352
365
  end
353
366
 
354
367
  inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
@@ -392,7 +405,7 @@ module Bolt
392
405
  output_format: config.format,
393
406
  # For continuity
394
407
  boltdir_type: config.project.type
395
- }
408
+ }.merge!(analytics.plan_counts(config.project.plans_path))
396
409
 
397
410
  # Only include target and inventory info for commands that take a targets
398
411
  # list. This avoids loading inventory for commands that don't need it.
@@ -456,15 +469,14 @@ module Bolt
456
469
  when 'project'
457
470
  case options[:action]
458
471
  when 'init'
459
- code = initialize_project
472
+ code = Bolt::ProjectManager.new(config, outputter, pal)
473
+ .create(Dir.pwd, options[:object], options[:modules])
460
474
  when 'migrate'
461
- code = Bolt::ProjectMigrator.new(config, outputter).migrate
475
+ code = Bolt::ProjectManager.new(config, outputter, pal).migrate
462
476
  end
463
477
  when 'plan'
464
478
  case options[:action]
465
479
  when 'new'
466
- command = Bolt::Util.powershell? ? 'New-BoltPlan' : 'bolt plan new'
467
- @logger.warn("Command '#{command}' is experimental and subject to changes.")
468
480
  plan_name = options[:object]
469
481
 
470
482
  # If this passes validation, it will return the path to the plan to create
@@ -479,9 +491,9 @@ module Bolt
479
491
  when 'module'
480
492
  case options[:action]
481
493
  when 'add'
482
- code = add_project_module(options[:object], config.project)
494
+ code = add_project_module(options[:object], config.project, config.module_install)
483
495
  when 'install'
484
- code = install_project_modules(config.project, options[:force], options[:resolve])
496
+ code = install_project_modules(config.project, config.module_install, options[:force], options[:resolve])
485
497
  when 'generate-types'
486
498
  code = generate_types
487
499
  end
@@ -526,12 +538,15 @@ module Bolt
526
538
  validate_file('script', script)
527
539
  executor.run_script(targets, script, options[:leftovers], executor_opts)
528
540
  when 'task'
529
- pal.run_task(options[:object],
530
- targets,
531
- options[:task_options],
532
- executor,
533
- inventory,
534
- options[:description])
541
+ r = outputter.spin do
542
+ pal.run_task(options[:object],
543
+ targets,
544
+ options[:task_options],
545
+ executor,
546
+ inventory,
547
+ options[:description])
548
+ end
549
+ r
535
550
  when 'file'
536
551
  src = options[:object]
537
552
  dest = options[:leftovers].first
@@ -575,10 +590,15 @@ module Bolt
575
590
  outputter.print_task_info(pal.get_task(task_name))
576
591
  end
577
592
 
593
+ # Filters a list of content by matching substring.
594
+ #
595
+ private def filter_content(content, filter)
596
+ return content unless content && filter
597
+ content.select { |name,| name.include?(filter) }
598
+ end
599
+
578
600
  def list_tasks
579
- tasks = pal.list_tasks
580
- tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
581
- tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
601
+ tasks = filter_content(pal.list_tasks(filter_content: true), options[:filter])
582
602
  outputter.print_tasks(tasks, pal.user_modulepath)
583
603
  end
584
604
 
@@ -587,9 +607,7 @@ module Bolt
587
607
  end
588
608
 
589
609
  def list_plans
590
- plans = pal.list_plans
591
- plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
592
- plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
610
+ plans = filter_content(pal.list_plans(filter_content: true), options[:filter])
593
611
  outputter.print_plans(plans, pal.user_modulepath)
594
612
  end
595
613
 
@@ -663,7 +681,9 @@ module Bolt
663
681
 
664
682
  executor.subscribe(log_outputter)
665
683
  executor.start_plan(plan_context)
666
- result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
684
+ result = outputter.spin do
685
+ pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
686
+ end
667
687
 
668
688
  # If a non-bolt exception bubbles up the plan won't get finished
669
689
  executor.finish_plan(result)
@@ -726,79 +746,9 @@ module Bolt
726
746
  0
727
747
  end
728
748
 
729
- # Initializes a specified directory as a Bolt project and installs any modules
730
- # specified by the user, along with their dependencies
731
- def initialize_project
732
- # Dir.pwd will return backslashes on Windows, but Pathname always uses
733
- # forward slashes to concatenate paths. This results in paths like
734
- # C:\User\Administrator/modules, which fail module install. This ensure
735
- # forward slashes in the cwd path.
736
- dir = File.expand_path(Dir.pwd)
737
- name = options[:object] || File.basename(dir)
738
- if name !~ Bolt::Module::MODULE_NAME_REGEX
739
- if options[:object]
740
- raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
741
- "project name must begin with a lowercase letter and can include lowercase "\
742
- "letters, numbers, and underscores."
743
- else
744
- command = Bolt::Util.powershell? ? 'New-BoltProject -Name <NAME>' : 'bolt project init <NAME>'
745
- raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
746
- "project name. Please specify a name using '#{command}'."
747
- end
748
- end
749
-
750
- project = Pathname.new(dir)
751
- old_config = project + 'bolt.yaml'
752
- config = project + 'bolt-project.yaml'
753
- puppetfile = project + 'Puppetfile'
754
- moduledir = project + 'modules'
755
-
756
- # Warn the user if the project directory already exists. We don't error
757
- # here since users might not have installed any modules yet. If both
758
- # bolt.yaml and bolt-project.yaml exist, this will just warn about
759
- # bolt-project.yaml and subsequent Bolt actions will warn about both files
760
- # existing.
761
- if config.exist?
762
- @logger.warn "Found existing project directory at #{project}. Skipping file creation."
763
- elsif old_config.exist?
764
- @logger.warn "Found existing #{old_config.basename} at #{project}. "\
765
- "#{old_config.basename} is deprecated, please rename to #{config.basename}."
766
- end
767
-
768
- # If modules were specified, first check if there is already a Puppetfile
769
- # at the project directory, erroring if there is. If there is no
770
- # Puppetfile, install the specified modules. The module installer will
771
- # resolve dependencies, generate a Puppetfile, and install the modules.
772
- if options[:modules]
773
- if puppetfile.exist?
774
- raise Bolt::CLIError,
775
- "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
776
- "project with modules."
777
- end
778
-
779
- installer = Bolt::ModuleInstaller.new(outputter, pal)
780
- installer.install(options[:modules], puppetfile, moduledir)
781
- end
782
-
783
- # If either bolt.yaml or bolt-project.yaml exist, the user has already
784
- # been warned and we can just finish project creation. Otherwise, create a
785
- # bolt-project.yaml with the project name in it.
786
- unless config.exist? || old_config.exist?
787
- begin
788
- content = { 'name' => name }
789
- File.write(config.to_path, content.to_yaml)
790
- outputter.print_message "Successfully created Bolt project at #{project}"
791
- rescue StandardError => e
792
- raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
793
- end
794
- end
795
-
796
- 0
797
- end
798
-
799
749
  # Installs modules declared in the project configuration file.
800
750
  #
801
- def install_project_modules(project, force, resolve)
751
+ def install_project_modules(project, config, force, resolve)
802
752
  assert_project_file(project)
803
753
  assert_puppetfile_or_module_command(project.modules)
804
754
 
@@ -808,30 +758,55 @@ module Bolt
808
758
  return 0
809
759
  end
810
760
 
761
+ if resolve != false && config.any?
762
+ @logger.warn(
763
+ "Detected configuration for 'module-install'. This configuration is currently "\
764
+ "only supported when installing modules, not when resolving module dependencies. "\
765
+ "For more information, see https://pup.pt/bolt-module-install"
766
+ )
767
+ end
768
+
769
+ modules = project.modules || []
811
770
  installer = Bolt::ModuleInstaller.new(outputter, pal)
812
771
 
813
- ok = installer.install(project.modules,
814
- project.puppetfile,
815
- project.managed_moduledir,
816
- force: force,
817
- resolve: resolve)
772
+ ok = outputter.spin do
773
+ installer.install(modules,
774
+ project.puppetfile,
775
+ project.managed_moduledir,
776
+ config,
777
+ force: force,
778
+ resolve: resolve)
779
+ end
780
+
818
781
  ok ? 0 : 1
819
782
  end
820
783
 
821
784
  # Adds a single module to the project.
822
785
  #
823
- def add_project_module(name, project)
786
+ def add_project_module(name, project, config)
824
787
  assert_project_file(project)
825
788
  assert_puppetfile_or_module_command(project.modules)
826
789
 
790
+ if config.any?
791
+ @logger.warn(
792
+ "Detected configuration for 'module-install'. This configuration is currently "\
793
+ "only supported when installing modules, not when resolving module dependencies. "\
794
+ "For more information, see https://pup.pt/bolt-module-install"
795
+ )
796
+ end
797
+
827
798
  modules = project.modules || []
828
799
  installer = Bolt::ModuleInstaller.new(outputter, pal)
829
800
 
830
- ok = installer.add(name,
831
- modules,
832
- project.puppetfile,
833
- project.managed_moduledir,
834
- project.project_file)
801
+ ok = outputter.spin do
802
+ installer.add(name,
803
+ modules,
804
+ project.puppetfile,
805
+ project.managed_moduledir,
806
+ project.project_file,
807
+ config)
808
+ end
809
+
835
810
  ok ? 0 : 1
836
811
  end
837
812
 
@@ -860,7 +835,10 @@ module Bolt
860
835
 
861
836
  outputter.print_message("Installing modules from Puppetfile")
862
837
  installer = Bolt::ModuleInstaller.new(outputter, pal)
863
- ok = installer.install_puppetfile(puppetfile, moduledir, puppetfile_config)
838
+ ok = outputter.spin do
839
+ installer.install_puppetfile(puppetfile, moduledir, puppetfile_config)
840
+ end
841
+
864
842
  ok ? 0 : 1
865
843
  end
866
844
 
@@ -966,7 +944,11 @@ module Bolt
966
944
  end
967
945
 
968
946
  def outputter
969
- @outputter ||= Bolt::Outputter.for_format(config.format, config.color, options[:verbose], config.trace)
947
+ @outputter ||= Bolt::Outputter.for_format(config.format,
948
+ config.color,
949
+ options[:verbose],
950
+ config.trace,
951
+ config.spinner)
970
952
  end
971
953
 
972
954
  def log_outputter
@@ -7,6 +7,7 @@ require 'bolt/project'
7
7
  require 'bolt/logger'
8
8
  require 'bolt/util'
9
9
  require 'bolt/config/options'
10
+ require 'bolt/validator'
10
11
 
11
12
  module Bolt
12
13
  class UnknownTransportError < Bolt::Error
@@ -32,53 +33,110 @@ module Bolt
32
33
  end
33
34
 
34
35
  def self.from_project(project, overrides = {})
35
- logs = []
36
+ logs = []
37
+ deprecations = []
38
+
36
39
  conf = if project.project_file == project.config_file
37
40
  project.data
38
41
  else
39
42
  c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
43
+
44
+ # Validate the config against the schema. This will raise a single error
45
+ # with all validation errors.
46
+ Bolt::Validator.new.tap do |validator|
47
+ validator.validate(c, bolt_schema, project.config_file.to_s)
48
+
49
+ validator.warnings.each { |warning| logs << { warn: warning } }
50
+
51
+ validator.deprecations.each do |dep|
52
+ deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
53
+ end
54
+ end
55
+
40
56
  logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
41
57
  c
42
58
  end
43
-
44
59
  data = load_defaults(project).push(
45
- filepath: project.config_file,
46
- data: conf,
47
- logs: logs,
48
- deprecations: []
60
+ filepath: project.config_file,
61
+ data: conf,
62
+ logs: logs,
63
+ deprecations: deprecations
49
64
  )
50
65
 
51
66
  new(project, data, overrides)
52
67
  end
53
68
 
54
69
  def self.from_file(configfile, overrides = {})
55
- project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
- logs = []
70
+ project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
71
+ logs = []
72
+ deprecations = []
57
73
 
58
74
  conf = if project.project_file == project.config_file
59
75
  project.data
60
76
  else
61
77
  c = Bolt::Util.read_yaml_hash(configfile, 'config')
78
+
79
+ # Validate the config against the schema. This will raise a single error
80
+ # with all validation errors.
81
+ Bolt::Validator.new.tap do |validator|
82
+ validator.validate(c, bolt_schema, project.config_file.to_s)
83
+
84
+ validator.warnings.each { |warning| logs << { warn: warning } }
85
+
86
+ validator.deprecations.each do |dep|
87
+ deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
88
+ end
89
+ end
90
+
62
91
  logs << { debug: "Loaded configuration from #{configfile}" }
63
92
  c
64
93
  end
65
94
 
66
95
  data = load_defaults(project).push(
67
- filepath: configfile,
68
- data: conf,
69
- logs: logs,
70
- deprecations: []
96
+ filepath: configfile,
97
+ data: conf,
98
+ logs: logs,
99
+ deprecations: deprecations
71
100
  )
72
101
 
73
102
  new(project, data, overrides)
74
103
  end
75
104
 
76
- def self.system_path
77
- # Lazy-load expensive gem code
78
- require 'win32/dir' if Bolt::Util.windows?
105
+ # Builds a hash of definitions for transport configuration.
106
+ #
107
+ def self.transport_definitions
108
+ INVENTORY_OPTIONS.each_with_object({}) do |(option, definition), acc|
109
+ acc[option] = TRANSPORT_CONFIG.key?(option) ? definition.merge(TRANSPORT_CONFIG[option].schema) : definition
110
+ end
111
+ end
112
+
113
+ # Builds the schema for bolt-defaults.yaml used by the validator.
114
+ #
115
+ def self.defaults_schema
116
+ schema = {
117
+ type: Hash,
118
+ properties: BOLT_DEFAULTS_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
119
+ definitions: OPTIONS.merge(transport_definitions)
120
+ }
121
+
122
+ schema[:definitions]['inventory-config'][:properties] = transport_definitions
123
+
124
+ schema
125
+ end
79
126
 
127
+ # Builds the schema for bolt.yaml used by the validator.
128
+ #
129
+ def self.bolt_schema
130
+ {
131
+ type: Hash,
132
+ properties: (BOLT_OPTIONS + INVENTORY_OPTIONS.keys).map { |opt| [opt, _ref: opt] }.to_h,
133
+ definitions: OPTIONS.merge(transport_definitions)
134
+ }
135
+ end
136
+
137
+ def self.system_path
80
138
  if Bolt::Util.windows?
81
- Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc'))
139
+ Pathname.new(File.join(ENV['ALLUSERSPROFILE'], 'PuppetLabs', 'bolt', 'etc'))
82
140
  else
83
141
  Pathname.new(File.join('/etc', 'puppetlabs', 'bolt'))
84
142
  end
@@ -94,9 +152,10 @@ module Bolt
94
152
  # projects. This file does not allow project-specific configuration such as 'hiera-config' and
95
153
  # 'inventoryfile', and nests all default inventory configuration under an 'inventory-config' key.
96
154
  def self.load_bolt_defaults_yaml(dir)
97
- filepath = dir + BOLT_DEFAULTS_NAME
98
- data = Bolt::Util.read_yaml_hash(filepath, 'config')
99
- logs = [{ debug: "Loaded configuration from #{filepath}" }]
155
+ filepath = dir + BOLT_DEFAULTS_NAME
156
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
157
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
158
+ deprecations = []
100
159
 
101
160
  # Warn if 'bolt.yaml' detected in same directory.
102
161
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
@@ -106,6 +165,18 @@ module Bolt
106
165
  )
107
166
  end
108
167
 
168
+ # Validate the config against the schema. This will raise a single error
169
+ # with all validation errors.
170
+ Bolt::Validator.new.tap do |validator|
171
+ validator.validate(data, defaults_schema, filepath)
172
+
173
+ validator.warnings.each { |warning| logs << { warn: warning } }
174
+
175
+ validator.deprecations.each do |dep|
176
+ deprecations << { type: "#{BOLT_DEFAULTS_NAME} #{dep[:option]}", msg: dep[:message] }
177
+ end
178
+ end
179
+
109
180
  # Remove project-specific config such as hiera-config, etc.
110
181
  project_config = data.slice(*(BOLT_PROJECT_OPTIONS - BOLT_DEFAULTS_OPTIONS))
111
182
 
@@ -148,18 +219,30 @@ module Bolt
148
219
  data = data.merge(data.delete('inventory-config'))
149
220
  end
150
221
 
151
- { filepath: filepath, data: data, logs: logs, deprecations: [] }
222
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
152
223
  end
153
224
 
154
225
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
155
226
  # here since Bolt::Config will just ignore any invalid keys.
156
227
  def self.load_bolt_yaml(dir)
157
- filepath = dir + BOLT_CONFIG_NAME
158
- data = Bolt::Util.read_yaml_hash(filepath, 'config')
159
- logs = [{ debug: "Loaded configuration from #{filepath}" }]
228
+ filepath = dir + BOLT_CONFIG_NAME
229
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
230
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
160
231
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
161
- msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
162
- "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
232
+ msg: "Configuration file #{filepath} is deprecated and will be removed in Bolt 3.0. "\
233
+ "See https://pup.pt/update-bolt-config for how to update to the latest Bolt practices." }]
234
+
235
+ # Validate the config against the schema. This will raise a single error
236
+ # with all validation errors.
237
+ Bolt::Validator.new.tap do |validator|
238
+ validator.validate(data, bolt_schema, filepath)
239
+
240
+ validator.warnings.each { |warning| logs << { warn: warning } }
241
+
242
+ validator.deprecations.each do |dep|
243
+ deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
244
+ end
245
+ end
163
246
 
164
247
  { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
165
248
  end
@@ -206,17 +289,21 @@ module Bolt
206
289
  @config_files = []
207
290
 
208
291
  default_data = {
292
+ 'apply-settings' => {},
209
293
  'apply_settings' => {},
210
294
  'color' => true,
211
295
  'compile-concurrency' => Etc.nprocessors,
212
296
  'concurrency' => default_concurrency,
213
297
  'format' => 'human',
214
298
  'log' => { 'console' => {} },
299
+ 'module-install' => {},
300
+ 'plugin-hooks' => {},
215
301
  'plugin_hooks' => {},
216
302
  'plugins' => {},
217
303
  'puppetdb' => {},
218
304
  'puppetfile' => {},
219
305
  'save-rerun' => true,
306
+ 'spinner' => true,
220
307
  'transport' => 'ssh'
221
308
  }
222
309
 
@@ -271,7 +358,7 @@ module Bolt
271
358
 
272
359
  # Set console log to debug if in debug mode
273
360
  if options[:debug]
274
- overrides['log'] = { 'console' => { 'level' => :debug } }
361
+ overrides['log'] = { 'console' => { 'level' => 'debug' } }
275
362
  end
276
363
 
277
364
  if options[:puppetfile_path]
@@ -280,6 +367,9 @@ module Bolt
280
367
 
281
368
  overrides['trace'] = opts['trace'] if opts.key?('trace')
282
369
 
370
+ # Validate the overrides
371
+ Bolt::Validator.new.validate(overrides, self.class.bolt_schema, 'command line')
372
+
283
373
  overrides
284
374
  end
285
375
 
@@ -296,7 +386,7 @@ module Bolt
296
386
  when *TRANSPORT_CONFIG.keys
297
387
  Bolt::Util.deep_merge(val1, val2)
298
388
  # Hash values are shallow merged
299
- when 'puppetdb', 'plugin_hooks', 'apply_settings', 'log'
389
+ when 'puppetdb', 'plugin-hooks', 'plugin_hooks', 'apply-settings', 'apply_settings', 'log'
300
390
  val1.merge(val2)
301
391
  # All other values are overwritten
302
392
  else
@@ -333,8 +423,9 @@ module Bolt
333
423
  end
334
424
 
335
425
  # Filter hashes to only include valid options
336
- @data['apply_settings'] = @data['apply_settings'].slice(*OPTIONS['apply_settings'][:properties].keys)
337
- @data['puppetfile'] = @data['puppetfile'].slice(*OPTIONS['puppetfile'][:properties].keys)
426
+ %w[apply-settings apply_settings module-install puppetfile].each do |opt|
427
+ @data[opt] = @data[opt].slice(*OPTIONS.dig(opt, :properties).keys)
428
+ end
338
429
  end
339
430
 
340
431
  private def normalize_log(target)
@@ -357,31 +448,14 @@ module Bolt
357
448
  next if val == 'disable'
358
449
 
359
450
  name = normalize_log(key)
451
+ acc[name] = val.slice('append', 'level').transform_keys(&:to_sym)
360
452
 
361
- # But otherwise it has to be a Hash
362
- unless val.is_a?(Hash)
363
- raise Bolt::ValidationError,
364
- "config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
365
- end
366
-
367
- acc[name] = val.slice('append', 'level')
368
- .transform_keys(&:to_sym)
453
+ next unless acc[name][:level] == 'notice'
369
454
 
370
- if (v = acc[name][:level])
371
- unless v.is_a?(String) || v.is_a?(Symbol)
372
- raise Bolt::ValidationError,
373
- "level of log #{name} must be a String or Symbol, received #{v.class} #{v.inspect}"
374
- end
375
- unless Bolt::Logger.valid_level?(v)
376
- raise Bolt::ValidationError,
377
- "level of log #{name} must be one of #{Bolt::Logger.levels.join(', ')}; received #{v}"
378
- end
379
- end
380
-
381
- if (v = acc[name][:append]) && v != true && v != false
382
- raise Bolt::ValidationError,
383
- "append flag of log #{name} must be a Boolean, received #{v.class} #{v.inspect}"
384
- end
455
+ @deprecations << {
456
+ type: 'notice log level',
457
+ msg: "Log level 'notice' is deprecated and will be removed in Bolt 3.0. Use 'info' instead."
458
+ }
385
459
  end
386
460
  end
387
461
 
@@ -397,40 +471,36 @@ module Bolt
397
471
  "is automatically appended to the modulepath and cannot be configured."
398
472
  end
399
473
 
400
- keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
401
- keys.each do |key|
402
- next unless Bolt::Util.references?(@data[key])
403
- valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks puppetdb]
404
- raise Bolt::ValidationError,
405
- "Found unsupported key _plugin in config setting #{key}. Plugins are only available in "\
406
- "#{valid_keys.join(', ')}."
407
- end
408
-
409
- unless concurrency.is_a?(Integer) && concurrency > 0
410
- raise Bolt::ValidationError,
411
- "Concurrency must be a positive Integer, received #{concurrency.class} #{concurrency}"
412
- end
413
-
414
- unless compile_concurrency.is_a?(Integer) && compile_concurrency > 0
415
- raise Bolt::ValidationError,
416
- "Compile concurrency must be a positive Integer, received #{compile_concurrency.class} "\
417
- "#{compile_concurrency}"
418
- end
419
-
420
474
  compile_limit = 2 * Etc.nprocessors
421
475
  unless compile_concurrency < compile_limit
422
476
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
423
477
  end
424
478
 
425
- unless %w[human json rainbow].include? format
426
- raise Bolt::ValidationError, "Unsupported format: '#{format}'"
479
+ %w[hiera-config trusted-external-command inventoryfile].each do |opt|
480
+ Bolt::Util.validate_file(opt, @data[opt]) if @data[opt]
427
481
  end
428
482
 
429
- Bolt::Util.validate_file('hiera-config', @data['hiera-config']) if @data['hiera-config']
430
- Bolt::Util.validate_file('trusted-external-command', trusted_external) if trusted_external
483
+ if File.exist?(default_inventoryfile)
484
+ Bolt::Util.validate_file('inventory file', default_inventoryfile)
485
+ end
431
486
 
432
- unless TRANSPORT_CONFIG.include?(transport)
433
- raise UnknownTransportError, transport
487
+ # Warn the user how they should be using the 'puppetfile' or
488
+ # 'module-install' config options. We don't error here since these
489
+ # settings can be set at the user or system level.
490
+ if @project.modules && puppetfile_config.any? && module_install.empty?
491
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
492
+ @logs << { warn: "Detected configuration for 'puppetfile'. This setting is not "\
493
+ "used when 'modules' is configured. Use 'module-install' instead. "\
494
+ "To automatically update your project configuration, run '#{command}'." }
495
+ elsif @project.modules.nil? && puppetfile_config.empty? && module_install.any?
496
+ @logs << { warn: "Detected configuration for 'module-install'. This setting is not "\
497
+ "used when 'modules' is not configured. Use 'puppetfile' instead." }
498
+ elsif @project.modules && puppetfile_config.any? && module_install.any?
499
+ @logs << { warn: "Detected configuration for 'puppetfile' and 'module-install'. Using "\
500
+ "configuration for 'module-install' because 'modules' is also configured." }
501
+ elsif @project.modules.nil? && puppetfile_config.any? && module_install.any?
502
+ @logs << { warn: "Detected configuration for 'puppetfile' and 'module-install'. Using "\
503
+ "configuration for 'puppetfile' because 'modules' is not configured." }
434
504
  end
435
505
  end
436
506
 
@@ -464,6 +534,10 @@ module Bolt
464
534
  @data['modulepath'] = value
465
535
  end
466
536
 
537
+ def plugin_cache
538
+ @project.plugin_cache || @data['plugin-cache'] || {}
539
+ end
540
+
467
541
  def concurrency
468
542
  @data['concurrency']
469
543
  end
@@ -496,6 +570,10 @@ module Bolt
496
570
  @data['save-rerun']
497
571
  end
498
572
 
573
+ def spinner
574
+ @data['spinner']
575
+ end
576
+
499
577
  def inventoryfile
500
578
  @data['inventoryfile']
501
579
  end
@@ -513,7 +591,18 @@ module Bolt
513
591
  end
514
592
 
515
593
  def plugin_hooks
516
- @data['plugin_hooks']
594
+ if @data['plugin-hooks'].any? && @data['plugin_hooks'].any?
595
+ Bolt::Logger.warn_once(
596
+ "plugin-hooks and plugin_hooks set",
597
+ "Detected configuration for 'plugin-hooks' and 'plugin_hooks'. Bolt will ignore 'plugin_hooks'."
598
+ )
599
+
600
+ @data['plugin-hooks']
601
+ elsif @data['plugin-hooks'].any?
602
+ @data['plugin-hooks']
603
+ else
604
+ @data['plugin_hooks']
605
+ end
517
606
  end
518
607
 
519
608
  def trusted_external
@@ -521,13 +610,28 @@ module Bolt
521
610
  end
522
611
 
523
612
  def apply_settings
524
- @data['apply_settings']
613
+ if @data['apply-settings'].any? && @data['apply_settings'].any?
614
+ Bolt::Logger.warn_once(
615
+ "apply-settings and apply_settings set",
616
+ "Detected configuration for 'apply-settings' and 'apply_settings'. Bolt will ignore 'apply_settings'."
617
+ )
618
+
619
+ @data['apply-settings']
620
+ elsif @data['apply-settings'].any?
621
+ @data['apply-settings']
622
+ else
623
+ @data['apply_settings']
624
+ end
525
625
  end
526
626
 
527
627
  def transport
528
628
  @data['transport']
529
629
  end
530
630
 
631
+ def module_install
632
+ @project.module_install || @data['module-install']
633
+ end
634
+
531
635
  # Check if there is a case-insensitive match to the path
532
636
  def check_path_case(type, paths)
533
637
  return if paths.nil?