bolt 2.35.0 → 2.36.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  3. data/lib/bolt/analytics.rb +27 -8
  4. data/lib/bolt/apply_result.rb +3 -3
  5. data/lib/bolt/bolt_option_parser.rb +38 -15
  6. data/lib/bolt/cli.rb +13 -87
  7. data/lib/bolt/config.rb +131 -52
  8. data/lib/bolt/config/options.rb +42 -4
  9. data/lib/bolt/config/transport/base.rb +10 -19
  10. data/lib/bolt/config/transport/local.rb +0 -7
  11. data/lib/bolt/config/transport/ssh.rb +8 -14
  12. data/lib/bolt/config/validator.rb +231 -0
  13. data/lib/bolt/executor.rb +5 -17
  14. data/lib/bolt/outputter/rainbow.rb +1 -1
  15. data/lib/bolt/plugin.rb +0 -7
  16. data/lib/bolt/project.rb +30 -36
  17. data/lib/bolt/project_manager.rb +199 -0
  18. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +41 -4
  19. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  20. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  21. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  22. data/lib/bolt/puppetdb/config.rb +1 -2
  23. data/lib/bolt/shell/bash.rb +1 -1
  24. data/lib/bolt/task/run.rb +1 -1
  25. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  26. data/lib/bolt/util.rb +14 -7
  27. data/lib/bolt/version.rb +1 -1
  28. data/lib/bolt_server/base_config.rb +3 -1
  29. data/lib/bolt_server/config.rb +3 -1
  30. data/lib/bolt_server/schemas/partials/task.json +2 -2
  31. data/lib/bolt_server/transport_app.rb +5 -5
  32. data/libexec/apply_catalog.rb +1 -1
  33. data/libexec/custom_facts.rb +1 -1
  34. data/libexec/query_resources.rb +1 -1
  35. metadata +8 -13
  36. data/lib/bolt/project_migrator.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d05af3f780d500b91be547a73123d24c2da041a8ef60ba14114921358b120800
4
- data.tar.gz: e8534835e69d1e5e57a353ff89a7d2667cf6d0894e8a07c1e5812282ed85b2cd
3
+ metadata.gz: 1881e4097a89e606b9326f93e2e6fd9d934a7db1eb8609e6f9a09ffa322ac7c4
4
+ data.tar.gz: '02509b1919e57f137bf4e868332be3b73238d22d83251680dc29c7c93ef90a19'
5
5
  SHA512:
6
- metadata.gz: 97b3faac5a1e423dcebf8e4fa92f55bbc36bd1c9735fc38332db40a2bb0db0e1ddc8e1e815aa29f31f9eb3601972a8d6e07b39d0c5dc6cd2762ea87a91ddec92
7
- data.tar.gz: 15963b1bb77ee04a1db4f54996964b32e599e4a0cc422dc77892151b221c1a17df9e6a1c74f160a30e9146402d48819b07b3b9d2ee9c96d7e763bba97389ccc8
6
+ metadata.gz: 1fc1215430ac99b765c24c87ee210afb03a247a6c0baac9af3b00e34d56cfb58bacb2140dcfafbe197fd655602713463e2697c2df5083c259b66c8d8cd7bc112
7
+ data.tar.gz: 445a5f77a5beaf8a1a3c4cb2193e973ada23cb4a7a5f3a8caba2ccc47be4fc8a8fdef3ba797e082c0df3b9553c2f2e67c06084f2c2b2684347d2b9a86e45bde0
@@ -12,6 +12,7 @@ Puppet::DataTypes.create_type('ApplyResult') do
12
12
  message => Callable[[], Optional[String]],
13
13
  action => Callable[[], String],
14
14
  to_data => Callable[[], Hash],
15
+ value => Callable[[], Hash]
15
16
  }
16
17
  PUPPET
17
18
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bolt/util'
4
4
  require 'bolt/version'
5
+ require 'find'
5
6
  require 'json'
6
7
  require 'logging'
7
8
  require 'securerandom'
@@ -23,14 +24,16 @@ module Bolt
23
24
  plan_steps: :cd8,
24
25
  return_type: :cd9,
25
26
  inventory_version: :cd10,
26
- boltdir_type: :cd11
27
+ boltdir_type: :cd11,
28
+ puppet_plan_count: :cd12,
29
+ yaml_plan_count: :cd13
27
30
  }.freeze
28
31
 
29
32
  def self.build_client
30
33
  logger = Bolt::Logger.logger(self)
31
34
  begin
32
- config_file = config_path(logger)
33
- config = load_config(config_file, logger)
35
+ config_file = config_path
36
+ config = load_config(config_file)
34
37
  rescue ArgumentError
35
38
  config = { 'disabled' => true }
36
39
  end
@@ -51,7 +54,7 @@ module Bolt
51
54
  NoopClient.new
52
55
  end
53
56
 
54
- def self.config_path(logger)
57
+ def self.config_path
55
58
  path = File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'analytics.yaml'))
56
59
  old_path = File.expand_path(File.join('~', '.puppetlabs', 'bolt', 'analytics.yaml'))
57
60
 
@@ -59,7 +62,7 @@ module Bolt
59
62
  if File.exist?(old_path)
60
63
  message = "Detected analytics configuration files at '#{old_path}' and '#{path}'. Loading "\
61
64
  "analytics configuration from '#{path}'."
62
- logger.warn(message)
65
+ Bolt::Logger.warn_once('duplicate_analytics', message)
63
66
  end
64
67
 
65
68
  path
@@ -70,12 +73,12 @@ module Bolt
70
73
  end
71
74
  end
72
75
 
73
- def self.load_config(filename, logger)
76
+ def self.load_config(filename)
74
77
  if File.exist?(filename)
75
78
  Bolt::Util.read_optional_yaml_hash(filename, 'analytics')
76
79
  else
77
80
  unless ENV['BOLT_DISABLE_ANALYTICS']
78
- logger.warn <<~ANALYTICS
81
+ Bolt::Logger.warn_once('analytics_opt_out', <<~ANALYTICS)
79
82
  Bolt collects data about how you use it. You can opt out of providing this data.
80
83
 
81
84
  To disable analytics data collection, add this line to ~/.puppetlabs/etc/bolt/analytics.yaml :
@@ -134,11 +137,23 @@ module Bolt
134
137
  end
135
138
 
136
139
  def report_bundled_content(mode, name)
137
- if bundled_content[mode.split(' ').first]&.include?(name)
140
+ if bundled_content[mode.split.first]&.include?(name)
138
141
  event('Bundled Content', mode, label: name)
139
142
  end
140
143
  end
141
144
 
145
+ def plan_counts(plans_path)
146
+ pp_count, yaml_count = if File.exist?(plans_path)
147
+ %w[pp yaml].map do |extension|
148
+ Find.find(plans_path.to_s).grep(/.*\.#{extension}/).length
149
+ end
150
+ else
151
+ [0, 0]
152
+ end
153
+
154
+ { puppet_plan_count: pp_count, yaml_plan_count: yaml_count }
155
+ end
156
+
142
157
  def event(category, action, label: nil, value: nil, **kwargs)
143
158
  custom_dimensions = Bolt::Util.walk_keys(kwargs) do |k|
144
159
  CUSTOM_DIMENSIONS[k] || raise("Unknown analytics key '#{k}'")
@@ -224,6 +239,10 @@ module Bolt
224
239
 
225
240
  def report_bundled_content(mode, name); end
226
241
 
242
+ def plan_counts(_)
243
+ {}
244
+ end
245
+
227
246
  def event(category, action, **_kwargs)
228
247
  @logger.trace "Skipping submission of '#{category} #{action}' event because analytics is disabled"
229
248
  end
@@ -96,9 +96,9 @@ module Bolt
96
96
  @target = target
97
97
  @value = {}
98
98
  @action = 'apply'
99
- value['report'] = report if report
100
- value['_error'] = error if error
101
- value['_output'] = metrics_message if metrics_message
99
+ @value['report'] = report if report
100
+ @value['_error'] = error if error
101
+ @value['_output'] = metrics_message if metrics_message
102
102
  end
103
103
 
104
104
  def event_metrics
@@ -6,11 +6,12 @@ require 'optparse'
6
6
 
7
7
  module Bolt
8
8
  class BoltOptionParser < OptionParser
9
+ PROJECT_PATHS = %w[project configfile boltdir].freeze
9
10
  OPTIONS = { inventory: %w[targets query rerun description],
10
11
  authentication: %w[user password password-prompt private-key host-key-check ssl ssl-verify],
11
12
  escalation: %w[run-as sudo-password sudo-password-prompt sudo-executable],
12
13
  run_context: %w[concurrency inventoryfile save-rerun cleanup],
13
- global_config_setters: %w[modulepath project configfile],
14
+ global_config_setters: PROJECT_PATHS + %w[modulepath],
14
15
  transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command],
15
16
  display: %w[format color verbose trace],
16
17
  global: %w[help version debug log-level] }.freeze
@@ -46,7 +47,8 @@ module Bolt
46
47
  when 'inventory'
47
48
  case action
48
49
  when 'show'
49
- { flags: OPTIONS[:inventory] + OPTIONS[:global] + %w[format inventoryfile boltdir configfile detail],
50
+ { flags: OPTIONS[:inventory] + OPTIONS[:global] +
51
+ PROJECT_PATHS + %w[format inventoryfile detail],
50
52
  banner: INVENTORY_SHOW_HELP }
51
53
  else
52
54
  { flags: OPTIONS[:global],
@@ -55,7 +57,7 @@ module Bolt
55
57
  when 'group'
56
58
  case action
57
59
  when 'show'
58
- { flags: OPTIONS[:global] + %w[format inventoryfile boltdir configfile],
60
+ { flags: OPTIONS[:global] + PROJECT_PATHS + %w[format inventoryfile],
59
61
  banner: GROUP_SHOW_HELP }
60
62
  else
61
63
  { flags: OPTIONS[:global],
@@ -67,13 +69,13 @@ module Bolt
67
69
  when 'module'
68
70
  case action
69
71
  when 'add'
70
- { flags: OPTIONS[:global] + %w[configfile project],
72
+ { flags: OPTIONS[:global] + PROJECT_PATHS,
71
73
  banner: MODULE_ADD_HELP }
72
74
  when 'generate-types'
73
75
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
74
76
  banner: MODULE_GENERATETYPES_HELP }
75
77
  when 'install'
76
- { flags: OPTIONS[:global] + %w[configfile force project resolve],
78
+ { flags: OPTIONS[:global] + PROJECT_PATHS + %w[force resolve],
77
79
  banner: MODULE_INSTALL_HELP }
78
80
  when 'show'
79
81
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
@@ -88,7 +90,7 @@ module Bolt
88
90
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
89
91
  banner: PLAN_CONVERT_HELP }
90
92
  when 'new'
91
- { flags: OPTIONS[:global] + %w[configfile project pp],
93
+ { flags: OPTIONS[:global] + PROJECT_PATHS + %w[pp],
92
94
  banner: PLAN_NEW_HELP }
93
95
  when 'run'
94
96
  { flags: ACTION_OPTS + %w[params compile-concurrency tmpdir hiera-config],
@@ -106,7 +108,7 @@ module Bolt
106
108
  { flags: OPTIONS[:global] + %w[modules],
107
109
  banner: PROJECT_INIT_HELP }
108
110
  when 'migrate'
109
- { flags: OPTIONS[:global] + %w[inventoryfile project configfile],
111
+ { flags: OPTIONS[:global] + PROJECT_PATHS + %w[inventoryfile],
110
112
  banner: PROJECT_MIGRATE_HELP }
111
113
  else
112
114
  { flags: OPTIONS[:global],
@@ -793,7 +795,10 @@ module Bolt
793
795
  @options[:noop] = true
794
796
  end
795
797
  define('--description DESCRIPTION',
796
- 'Description to use for the job') do |description|
798
+ 'Deprecated. Description to use for the job') do |description|
799
+ msg = "Command line option '--description' is deprecated, and will be "\
800
+ "removed in Bolt 3.0."
801
+ @deprecations << { type: 'Using --description', msg: msg }
797
802
  @options[:description] = description
798
803
  end
799
804
  define('--params PARAMETERS',
@@ -873,13 +878,25 @@ module Bolt
873
878
  File.expand_path(moduledir)
874
879
  end
875
880
  end
876
- define('--project PATH', '--boltdir PATH',
877
- 'Specify what project to load config from (default: autodiscovered from current working dir)') do |path|
881
+ define('--boltdir PATH',
882
+ 'Deprecated. Specify what project to load config from (default:',
883
+ 'autodiscovered from current working dir)') do |path|
884
+ msg = "Command line option '--boltdir' is deprecated, use '--project' instead."
885
+ @deprecations << { type: 'Using --boltdir', msg: msg }
878
886
  @options[:boltdir] = path
879
887
  end
888
+ define('--project PATH',
889
+ 'Path to load the Bolt project from (default: autodiscovered from current dir)') do |path|
890
+ @options[:project] = path
891
+ end
880
892
  define('--configfile PATH',
881
- 'Specify where to load config from (default: ~/.puppetlabs/bolt/bolt.yaml).',
882
- 'Directory containing bolt.yaml will be used as the project directory.') do |path|
893
+ 'Deprecated. Specify where to load config from (default:',
894
+ '~/.puppetlabs/bolt/bolt.yaml). Directory containing bolt.yaml will be',
895
+ 'used as the project directory.') do |path|
896
+ msg = "Command line option '--configfile' is deprecated, and " \
897
+ "will be removed in Bolt 3.0. Use '--project' and provide the "\
898
+ "directory path instead."
899
+ @deprecations << { type: 'Using --configfile', msg: msg }
883
900
  @options[:configfile] = path
884
901
  end
885
902
  define('--hiera-config PATH',
@@ -891,12 +908,18 @@ module Bolt
891
908
  if ENV.include?(Bolt::Inventory::ENVIRONMENT_VAR)
892
909
  raise Bolt::CLIError, "Cannot pass inventory file when #{Bolt::Inventory::ENVIRONMENT_VAR} is set"
893
910
  end
894
- @options[:inventoryfile] = Pathname.new(File.expand_path(path))
911
+ @options[:inventoryfile] = File.expand_path(path)
895
912
  end
896
913
  define('--puppetfile PATH',
897
- 'Specify a Puppetfile to use when installing modules. (default: ~/.puppetlabs/bolt/Puppetfile)',
914
+ 'Deprecated. Specify a Puppetfile to use when installing modules.',
915
+ ' (default: ~/.puppetlabs/bolt/Puppetfile)',
898
916
  'Modules are installed in the current project.') do |path|
899
- @options[:puppetfile_path] = Pathname.new(File.expand_path(path))
917
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
918
+ msg = "Command line option '--puppetfile' is deprecated, and will be removed "\
919
+ "in Bolt 3.0. You can migrate to using the new module management "\
920
+ "workflow using '#{command}'."
921
+ @deprecations << { type: 'Using --puppetfile', msg: msg }
922
+ @options[:puppetfile_path] = File.expand_path(path)
900
923
  end
901
924
  define('--[no-]save-rerun', 'Whether to update the rerun file after this command.') do |save|
902
925
  @options[:'save-rerun'] = save
@@ -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
@@ -293,7 +295,7 @@ module Bolt
293
295
  "Unknown argument(s) #{options[:leftovers].join(', ')}"
294
296
  end
295
297
 
296
- if options[:boltdir] && options[:configfile]
298
+ if options.slice(:boltdir, :configfile, :project).length > 1
297
299
  raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
298
300
  end
299
301
 
@@ -340,15 +342,10 @@ module Bolt
340
342
  def warn_inventory_overrides_cli(opts)
341
343
  inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
342
344
  Bolt::Inventory::ENVIRONMENT_VAR
343
- elsif config.inventoryfile && Bolt::Util.file_stat(config.inventoryfile)
345
+ elsif config.inventoryfile
344
346
  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
347
+ elsif File.exist?(config.default_inventoryfile)
348
+ config.default_inventoryfile
352
349
  end
353
350
 
354
351
  inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
@@ -392,7 +389,7 @@ module Bolt
392
389
  output_format: config.format,
393
390
  # For continuity
394
391
  boltdir_type: config.project.type
395
- }
392
+ }.merge!(analytics.plan_counts(config.project.plans_path))
396
393
 
397
394
  # Only include target and inventory info for commands that take a targets
398
395
  # list. This avoids loading inventory for commands that don't need it.
@@ -456,15 +453,14 @@ module Bolt
456
453
  when 'project'
457
454
  case options[:action]
458
455
  when 'init'
459
- code = initialize_project
456
+ code = Bolt::ProjectManager.new(config, outputter, pal)
457
+ .create(Dir.pwd, options[:object], options[:modules])
460
458
  when 'migrate'
461
- code = Bolt::ProjectMigrator.new(config, outputter).migrate
459
+ code = Bolt::ProjectManager.new(config, outputter, pal).migrate
462
460
  end
463
461
  when 'plan'
464
462
  case options[:action]
465
463
  when 'new'
466
- command = Bolt::Util.powershell? ? 'New-BoltPlan' : 'bolt plan new'
467
- @logger.warn("Command '#{command}' is experimental and subject to changes.")
468
464
  plan_name = options[:object]
469
465
 
470
466
  # If this passes validation, it will return the path to the plan to create
@@ -729,76 +725,6 @@ module Bolt
729
725
  0
730
726
  end
731
727
 
732
- # Initializes a specified directory as a Bolt project and installs any modules
733
- # specified by the user, along with their dependencies
734
- def initialize_project
735
- # Dir.pwd will return backslashes on Windows, but Pathname always uses
736
- # forward slashes to concatenate paths. This results in paths like
737
- # C:\User\Administrator/modules, which fail module install. This ensure
738
- # forward slashes in the cwd path.
739
- dir = File.expand_path(Dir.pwd)
740
- name = options[:object] || File.basename(dir)
741
- if name !~ Bolt::Module::MODULE_NAME_REGEX
742
- if options[:object]
743
- raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
744
- "project name must begin with a lowercase letter and can include lowercase "\
745
- "letters, numbers, and underscores."
746
- else
747
- command = Bolt::Util.powershell? ? 'New-BoltProject -Name <NAME>' : 'bolt project init <NAME>'
748
- raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
749
- "project name. Please specify a name using '#{command}'."
750
- end
751
- end
752
-
753
- project = Pathname.new(dir)
754
- old_config = project + 'bolt.yaml'
755
- config = project + 'bolt-project.yaml'
756
- puppetfile = project + 'Puppetfile'
757
- moduledir = project + 'modules'
758
-
759
- # Warn the user if the project directory already exists. We don't error
760
- # here since users might not have installed any modules yet. If both
761
- # bolt.yaml and bolt-project.yaml exist, this will just warn about
762
- # bolt-project.yaml and subsequent Bolt actions will warn about both files
763
- # existing.
764
- if config.exist?
765
- @logger.warn "Found existing project directory at #{project}. Skipping file creation."
766
- elsif old_config.exist?
767
- @logger.warn "Found existing #{old_config.basename} at #{project}. "\
768
- "#{old_config.basename} is deprecated, please rename to #{config.basename}."
769
- end
770
-
771
- # If modules were specified, first check if there is already a Puppetfile
772
- # at the project directory, erroring if there is. If there is no
773
- # Puppetfile, install the specified modules. The module installer will
774
- # resolve dependencies, generate a Puppetfile, and install the modules.
775
- if options[:modules]
776
- if puppetfile.exist?
777
- raise Bolt::CLIError,
778
- "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
779
- "project with modules."
780
- end
781
-
782
- installer = Bolt::ModuleInstaller.new(outputter, pal)
783
- installer.install(options[:modules], puppetfile, moduledir)
784
- end
785
-
786
- # If either bolt.yaml or bolt-project.yaml exist, the user has already
787
- # been warned and we can just finish project creation. Otherwise, create a
788
- # bolt-project.yaml with the project name in it.
789
- unless config.exist? || old_config.exist?
790
- begin
791
- content = { 'name' => name }
792
- File.write(config.to_path, content.to_yaml)
793
- outputter.print_message "Successfully created Bolt project at #{project}"
794
- rescue StandardError => e
795
- raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
796
- end
797
- end
798
-
799
- 0
800
- end
801
-
802
728
  # Installs modules declared in the project configuration file.
803
729
  #
804
730
  def install_project_modules(project, force, resolve)
@@ -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/config/validator'
10
11
 
11
12
  module Bolt
12
13
  class UnknownTransportError < Bolt::Error
@@ -32,53 +33,97 @@ 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
+ 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
59
 
44
60
  data = load_defaults(project).push(
45
- filepath: project.config_file,
46
- data: conf,
47
- logs: logs,
48
- deprecations: []
61
+ filepath: project.config_file,
62
+ data: conf,
63
+ logs: logs,
64
+ deprecations: deprecations
49
65
  )
50
66
 
51
67
  new(project, data, overrides)
52
68
  end
53
69
 
54
70
  def self.from_file(configfile, overrides = {})
55
- project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
- logs = []
71
+ project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
72
+ logs = []
73
+ deprecations = []
57
74
 
58
75
  conf = if project.project_file == project.config_file
59
76
  project.data
60
77
  else
61
78
  c = Bolt::Util.read_yaml_hash(configfile, 'config')
79
+
80
+ # Validate the config against the schema. This will raise a single error
81
+ # with all validation errors.
82
+ Validator.new.tap do |validator|
83
+ validator.validate(c, bolt_schema, project.config_file.to_s)
84
+
85
+ validator.warnings.each { |warning| logs << { warn: warning } }
86
+
87
+ validator.deprecations.each do |dep|
88
+ deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
89
+ end
90
+ end
91
+
62
92
  logs << { debug: "Loaded configuration from #{configfile}" }
63
93
  c
64
94
  end
65
95
 
66
96
  data = load_defaults(project).push(
67
- filepath: configfile,
68
- data: conf,
69
- logs: logs,
70
- deprecations: []
97
+ filepath: configfile,
98
+ data: conf,
99
+ logs: logs,
100
+ deprecations: deprecations
71
101
  )
72
102
 
73
103
  new(project, data, overrides)
74
104
  end
75
105
 
76
- def self.system_path
77
- # Lazy-load expensive gem code
78
- require 'win32/dir' if Bolt::Util.windows?
106
+ def self.defaults_schema
107
+ base = OPTIONS.slice(*BOLT_DEFAULTS_OPTIONS)
108
+ inventory = 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
+
112
+ base['inventory-config'][:properties] = inventory
113
+ base
114
+ end
79
115
 
116
+ def self.bolt_schema
117
+ inventory = INVENTORY_OPTIONS.each_with_object({}) do |(option, definition), acc|
118
+ acc[option] = TRANSPORT_CONFIG.key?(option) ? definition.merge(TRANSPORT_CONFIG[option].schema) : definition
119
+ end
120
+
121
+ OPTIONS.slice(*BOLT_OPTIONS).merge(inventory)
122
+ end
123
+
124
+ def self.system_path
80
125
  if Bolt::Util.windows?
81
- Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc'))
126
+ Pathname.new(File.join(ENV['ALLUSERSPROFILE'], 'PuppetLabs', 'bolt', 'etc'))
82
127
  else
83
128
  Pathname.new(File.join('/etc', 'puppetlabs', 'bolt'))
84
129
  end
@@ -94,9 +139,10 @@ module Bolt
94
139
  # projects. This file does not allow project-specific configuration such as 'hiera-config' and
95
140
  # 'inventoryfile', and nests all default inventory configuration under an 'inventory-config' key.
96
141
  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}" }]
142
+ filepath = dir + BOLT_DEFAULTS_NAME
143
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
144
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
145
+ deprecations = []
100
146
 
101
147
  # Warn if 'bolt.yaml' detected in same directory.
102
148
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
@@ -106,6 +152,18 @@ module Bolt
106
152
  )
107
153
  end
108
154
 
155
+ # Validate the config against the schema. This will raise a single error
156
+ # with all validation errors.
157
+ Validator.new.tap do |validator|
158
+ validator.validate(data, defaults_schema, filepath)
159
+
160
+ validator.warnings.each { |warning| logs << { warn: warning } }
161
+
162
+ validator.deprecations.each do |dep|
163
+ deprecations << { type: "#{BOLT_DEFAULTS_NAME} #{dep[:option]}", msg: dep[:message] }
164
+ end
165
+ end
166
+
109
167
  # Remove project-specific config such as hiera-config, etc.
110
168
  project_config = data.slice(*(BOLT_PROJECT_OPTIONS - BOLT_DEFAULTS_OPTIONS))
111
169
 
@@ -148,19 +206,31 @@ module Bolt
148
206
  data = data.merge(data.delete('inventory-config'))
149
207
  end
150
208
 
151
- { filepath: filepath, data: data, logs: logs, deprecations: [] }
209
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
152
210
  end
153
211
 
154
212
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
155
213
  # here since Bolt::Config will just ignore any invalid keys.
156
214
  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}" }]
215
+ filepath = dir + BOLT_CONFIG_NAME
216
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
217
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
160
218
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
161
219
  msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
162
220
  "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
163
221
 
222
+ # Validate the config against the schema. This will raise a single error
223
+ # with all validation errors.
224
+ Validator.new.tap do |validator|
225
+ validator.validate(data, bolt_schema, filepath)
226
+
227
+ validator.warnings.each { |warning| logs << { warn: warning } }
228
+
229
+ validator.deprecations.each do |dep|
230
+ deprecations << { type: "#{BOLT_CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
231
+ end
232
+ end
233
+
164
234
  { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
165
235
  end
166
236
 
@@ -206,12 +276,14 @@ module Bolt
206
276
  @config_files = []
207
277
 
208
278
  default_data = {
279
+ 'apply-settings' => {},
209
280
  'apply_settings' => {},
210
281
  'color' => true,
211
282
  'compile-concurrency' => Etc.nprocessors,
212
283
  'concurrency' => default_concurrency,
213
284
  'format' => 'human',
214
285
  'log' => { 'console' => {} },
286
+ 'plugin-hooks' => {},
215
287
  'plugin_hooks' => {},
216
288
  'plugins' => {},
217
289
  'puppetdb' => {},
@@ -271,7 +343,7 @@ module Bolt
271
343
 
272
344
  # Set console log to debug if in debug mode
273
345
  if options[:debug]
274
- overrides['log'] = { 'console' => { 'level' => :debug } }
346
+ overrides['log'] = { 'console' => { 'level' => 'debug' } }
275
347
  end
276
348
 
277
349
  if options[:puppetfile_path]
@@ -280,6 +352,9 @@ module Bolt
280
352
 
281
353
  overrides['trace'] = opts['trace'] if opts.key?('trace')
282
354
 
355
+ # Validate the overrides
356
+ Validator.new.validate(overrides, OPTIONS, 'command line')
357
+
283
358
  overrides
284
359
  end
285
360
 
@@ -296,7 +371,7 @@ module Bolt
296
371
  when *TRANSPORT_CONFIG.keys
297
372
  Bolt::Util.deep_merge(val1, val2)
298
373
  # Hash values are shallow merged
299
- when 'puppetdb', 'plugin_hooks', 'apply_settings', 'log'
374
+ when 'puppetdb', 'plugin-hooks', 'plugin_hooks', 'apply-settings', 'apply_settings', 'log'
300
375
  val1.merge(val2)
301
376
  # All other values are overwritten
302
377
  else
@@ -333,8 +408,9 @@ module Bolt
333
408
  end
334
409
 
335
410
  # 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)
411
+ %w[apply-settings apply_settings puppetfile].each do |opt|
412
+ @data[opt] = @data[opt].slice(*OPTIONS.dig(opt, :properties).keys)
413
+ end
338
414
  end
339
415
 
340
416
  private def normalize_log(target)
@@ -397,37 +473,18 @@ module Bolt
397
473
  "is automatically appended to the modulepath and cannot be configured."
398
474
  end
399
475
 
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
476
  compile_limit = 2 * Etc.nprocessors
421
477
  unless compile_concurrency < compile_limit
422
478
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
423
479
  end
424
480
 
425
- unless %w[human json rainbow].include? format
426
- raise Bolt::ValidationError, "Unsupported format: '#{format}'"
481
+ %w[hiera-config trusted-external-command inventoryfile].each do |opt|
482
+ Bolt::Util.validate_file(opt, @data[opt]) if @data[opt]
427
483
  end
428
484
 
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
485
+ if File.exist?(default_inventoryfile)
486
+ Bolt::Util.validate_file('inventory file', default_inventoryfile)
487
+ end
431
488
 
432
489
  unless TRANSPORT_CONFIG.include?(transport)
433
490
  raise UnknownTransportError, transport
@@ -513,7 +570,18 @@ module Bolt
513
570
  end
514
571
 
515
572
  def plugin_hooks
516
- @data['plugin_hooks']
573
+ if @data['plugin-hooks'].any? && @data['plugin_hooks'].any?
574
+ Bolt::Logger.warn_once(
575
+ "plugin-hooks and plugin_hooks set",
576
+ "Detected configuration for 'plugin-hooks' and 'plugin_hooks'. Bolt will ignore 'plugin_hooks'."
577
+ )
578
+
579
+ @data['plugin-hooks']
580
+ elsif @data['plugin-hooks'].any?
581
+ @data['plugin-hooks']
582
+ else
583
+ @data['plugin_hooks']
584
+ end
517
585
  end
518
586
 
519
587
  def trusted_external
@@ -521,7 +589,18 @@ module Bolt
521
589
  end
522
590
 
523
591
  def apply_settings
524
- @data['apply_settings']
592
+ if @data['apply-settings'].any? && @data['apply_settings'].any?
593
+ Bolt::Logger.warn_once(
594
+ "apply-settings and apply_settings set",
595
+ "Detected configuration for 'apply-settings' and 'apply_settings'. Bolt will ignore 'apply_settings'."
596
+ )
597
+
598
+ @data['apply-settings']
599
+ elsif @data['apply-settings'].any?
600
+ @data['apply-settings']
601
+ else
602
+ @data['apply_settings']
603
+ end
525
604
  end
526
605
 
527
606
  def transport