bolt 2.13.0 → 2.18.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  6. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +1 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +1 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +1 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +3 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +7 -2
  19. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +7 -4
  20. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -1
  21. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +1 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +1 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +1 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +1 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +1 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +1 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +1 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  30. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  31. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +2 -0
  32. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +2 -1
  33. data/bolt-modules/file/lib/puppet/functions/file/join.rb +2 -0
  34. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -1
  35. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  36. data/bolt-modules/file/lib/puppet/functions/file/write.rb +2 -0
  37. data/bolt-modules/out/lib/puppet/functions/out/message.rb +2 -0
  38. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +1 -0
  39. data/bolt-modules/system/lib/puppet/functions/system/env.rb +2 -0
  40. data/lib/bolt/applicator.rb +36 -21
  41. data/lib/bolt/apply_inventory.rb +4 -0
  42. data/lib/bolt/apply_result.rb +1 -1
  43. data/lib/bolt/apply_target.rb +4 -0
  44. data/lib/bolt/bolt_option_parser.rb +27 -16
  45. data/lib/bolt/catalog.rb +79 -69
  46. data/lib/bolt/cli.rb +78 -58
  47. data/lib/bolt/config.rb +163 -136
  48. data/lib/bolt/config/options.rb +474 -0
  49. data/lib/bolt/config/transport/base.rb +16 -16
  50. data/lib/bolt/config/transport/docker.rb +9 -23
  51. data/lib/bolt/config/transport/local.rb +6 -44
  52. data/lib/bolt/config/transport/options.rb +460 -0
  53. data/lib/bolt/config/transport/orch.rb +9 -18
  54. data/lib/bolt/config/transport/remote.rb +3 -6
  55. data/lib/bolt/config/transport/ssh.rb +78 -119
  56. data/lib/bolt/config/transport/winrm.rb +18 -47
  57. data/lib/bolt/executor.rb +14 -1
  58. data/lib/bolt/inventory/group.rb +1 -1
  59. data/lib/bolt/inventory/inventory.rb +4 -14
  60. data/lib/bolt/inventory/target.rb +22 -5
  61. data/lib/bolt/logger.rb +15 -1
  62. data/lib/bolt/outputter.rb +3 -0
  63. data/lib/bolt/outputter/rainbow.rb +84 -0
  64. data/lib/bolt/pal.rb +23 -9
  65. data/lib/bolt/project.rb +75 -55
  66. data/lib/bolt/resource_instance.rb +5 -1
  67. data/lib/bolt/shell/bash.rb +30 -42
  68. data/lib/bolt/shell/powershell.rb +13 -8
  69. data/lib/bolt/shell/powershell/snippets.rb +23 -6
  70. data/lib/bolt/transport/docker.rb +9 -5
  71. data/lib/bolt/transport/local/connection.rb +2 -1
  72. data/lib/bolt/transport/orch.rb +8 -0
  73. data/lib/bolt/transport/ssh.rb +7 -1
  74. data/lib/bolt/transport/ssh/connection.rb +35 -0
  75. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  76. data/lib/bolt/version.rb +1 -1
  77. data/lib/bolt_spec/bolt_context.rb +1 -1
  78. data/lib/bolt_spec/run.rb +1 -1
  79. metadata +22 -18
@@ -46,7 +46,6 @@ module Bolt
46
46
  Bolt::Logger.initialize_logging
47
47
  @logger = Logging.logger[self]
48
48
  @argv = argv
49
- @config = Bolt::Config.default
50
49
  @options = {}
51
50
  end
52
51
 
@@ -77,56 +76,74 @@ module Bolt
77
76
  private :help?
78
77
 
79
78
  def parse
80
- parser = BoltOptionParser.new(options)
81
- # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
82
- remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
83
- if @argv.empty? || help?(remaining)
84
- # Update the parser for the subcommand (or lack thereof)
85
- parser.update
86
- puts parser.help
87
- raise Bolt::CLIExit
88
- end
89
-
90
- options[:object] = remaining.shift
91
-
92
- # Only parse task_options for task or plan
93
- if %w[task plan].include?(options[:subcommand])
94
- task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
95
- if options[:task_options]
96
- unless task_options.empty?
97
- raise Bolt::CLIError,
98
- "Parameters must be specified through either the --params " \
99
- "option or param=value pairs, not both"
79
+ begin
80
+ parser = BoltOptionParser.new(options)
81
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
82
+ remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
83
+ if @argv.empty? || help?(remaining)
84
+ # Update the parser for the subcommand (or lack thereof)
85
+ parser.update
86
+ puts parser.help
87
+ raise Bolt::CLIExit
88
+ end
89
+
90
+ options[:object] = remaining.shift
91
+
92
+ # Only parse task_options for task or plan
93
+ if %w[task plan].include?(options[:subcommand])
94
+ task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
95
+ if options[:task_options]
96
+ unless task_options.empty?
97
+ raise Bolt::CLIError,
98
+ "Parameters must be specified through either the --params " \
99
+ "option or param=value pairs, not both"
100
+ end
101
+ options[:params_parsed] = true
102
+ elsif task_options.any?
103
+ options[:params_parsed] = false
104
+ options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
105
+ else
106
+ options[:params_parsed] = true
107
+ options[:task_options] = {}
100
108
  end
101
- options[:params_parsed] = true
102
- elsif task_options.any?
103
- options[:params_parsed] = false
104
- options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
109
+ end
110
+ options[:leftovers] = remaining
111
+
112
+ validate(options)
113
+
114
+ @config = if ENV['BOLT_PROJECT']
115
+ project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
116
+ Bolt::Config.from_project(project, options)
117
+ elsif options[:configfile]
118
+ Bolt::Config.from_file(options[:configfile], options)
119
+ else
120
+ project = if options[:boltdir]
121
+ dir = Pathname.new(options[:boltdir])
122
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
123
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
124
+ else
125
+ Bolt::Project.create_project(dir)
126
+ end
127
+ else
128
+ Bolt::Project.find_boltdir(Dir.pwd)
129
+ end
130
+ Bolt::Config.from_project(project, options)
131
+ end
132
+
133
+ Bolt::Logger.configure(config.log, config.color)
134
+ rescue Bolt::Error => e
135
+ if $stdout.isatty
136
+ # Print the error message in red, mimicking outputter.fatal_error
137
+ $stdout.puts("\033[31m#{e.message}\033[0m")
105
138
  else
106
- options[:params_parsed] = true
107
- options[:task_options] = {}
139
+ $stdout.puts(e.message)
108
140
  end
141
+ raise e
109
142
  end
110
- options[:leftovers] = remaining
111
-
112
- validate(options)
113
-
114
- @config = if options[:configfile]
115
- Bolt::Config.from_file(options[:configfile], options)
116
- else
117
- project = if options[:boltdir]
118
- Bolt::Project.new(options[:boltdir])
119
- else
120
- Bolt::Project.find_boltdir(Dir.pwd)
121
- end
122
- Bolt::Config.from_project(project, options)
123
- end
124
-
125
- Bolt::Logger.configure(config.log, config.color)
126
143
 
127
144
  # Logger must be configured before checking path case and project file, otherwise warnings will not display
128
- @config.check_path_case('modulepath', @config.modulepath)
129
- @config.project.check_deprecated_file
145
+ config.check_path_case('modulepath', config.modulepath)
146
+ config.project.check_deprecated_file
130
147
 
131
148
  # Log the file paths for loaded config files
132
149
  config_loaded
@@ -219,7 +236,7 @@ module Bolt
219
236
  end
220
237
 
221
238
  if options[:boltdir] && options[:configfile]
222
- raise Bolt::CLIError, "Only one of '--boltdir' or '--configfile' may be specified"
239
+ raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
223
240
  end
224
241
 
225
242
  if options[:noop] &&
@@ -245,6 +262,10 @@ module Bolt
245
262
  !options[:object]
246
263
  raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
247
264
  end
265
+
266
+ if options.key?(:debug) && options.key?(:log)
267
+ raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
268
+ end
248
269
  end
249
270
 
250
271
  def handle_parser_errors
@@ -272,12 +293,12 @@ module Bolt
272
293
  def warn_inventory_overrides_cli(opts)
273
294
  inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
274
295
  Bolt::Inventory::ENVIRONMENT_VAR
275
- elsif @config.inventoryfile && Bolt::Util.file_stat(@config.inventoryfile)
276
- @config.inventoryfile
296
+ elsif config.inventoryfile && Bolt::Util.file_stat(config.inventoryfile)
297
+ config.inventoryfile
277
298
  else
278
299
  begin
279
- Bolt::Util.file_stat(@config.default_inventoryfile)
280
- @config.default_inventoryfile
300
+ Bolt::Util.file_stat(config.default_inventoryfile)
301
+ config.default_inventoryfile
281
302
  rescue Errno::ENOENT
282
303
  nil
283
304
  end
@@ -381,7 +402,7 @@ module Bolt
381
402
  if options[:action] == 'generate-types'
382
403
  code = generate_types
383
404
  elsif options[:action] == 'install'
384
- code = install_puppetfile(@config.puppetfile_config, @config.puppetfile, @config.modulepath)
405
+ code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
385
406
  end
386
407
  when 'secret'
387
408
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -392,7 +413,7 @@ module Bolt
392
413
  end
393
414
  code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
394
415
  else
395
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
416
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
396
417
  targets = options[:targets]
397
418
 
398
419
  results = nil
@@ -512,8 +533,8 @@ module Bolt
512
533
  params: plan_arguments }
513
534
  plan_context[:description] = options[:description] if options[:description]
514
535
 
515
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
516
- if options.fetch(:format, 'human') == 'human'
536
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
537
+ if %w[human rainbow].include?(options.fetch(:format, 'human'))
517
538
  executor.subscribe(outputter)
518
539
  else
519
540
  # Only subscribe to out::message events for JSON outputter
@@ -548,7 +569,7 @@ module Bolt
548
569
  @logger.warn(message)
549
570
  end
550
571
 
551
- executor = Bolt::Executor.new(config.concurrency, analytics, noop)
572
+ executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
552
573
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
553
574
  executor.subscribe(log_outputter)
554
575
  # apply logging looks like plan logging, so tell the outputter we're in a
@@ -771,14 +792,13 @@ module Bolt
771
792
  end
772
793
 
773
794
  def pal
774
- project = config.project.load_as_module? ? config.project : nil
775
795
  @pal ||= Bolt::PAL.new(config.modulepath,
776
796
  config.hiera_config,
777
797
  config.project.resource_types,
778
798
  config.compile_concurrency,
779
799
  config.trusted_external,
780
800
  config.apply_settings,
781
- project)
801
+ config.project)
782
802
  end
783
803
 
784
804
  def convert_plan(plan)
@@ -794,7 +814,7 @@ module Bolt
794
814
  end
795
815
 
796
816
  def rerun
797
- @rerun ||= Bolt::Rerun.new(@config.rerunfile, @config.save_rerun)
817
+ @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
798
818
  end
799
819
 
800
820
  def outputter
@@ -6,13 +6,7 @@ require 'pathname'
6
6
  require 'bolt/project'
7
7
  require 'bolt/logger'
8
8
  require 'bolt/util'
9
- # Transport config objects
10
- require 'bolt/config/transport/ssh'
11
- require 'bolt/config/transport/winrm'
12
- require 'bolt/config/transport/orch'
13
- require 'bolt/config/transport/local'
14
- require 'bolt/config/transport/docker'
15
- require 'bolt/config/transport/remote'
9
+ require 'bolt/config/options'
16
10
 
17
11
  module Bolt
18
12
  class UnknownTransportError < Bolt::Error
@@ -23,144 +17,178 @@ module Bolt
23
17
  end
24
18
 
25
19
  class Config
26
- attr_reader :config_files, :warnings, :data, :transports, :project
27
-
28
- TRANSPORT_CONFIG = {
29
- 'ssh' => Bolt::Config::Transport::SSH,
30
- 'winrm' => Bolt::Config::Transport::WinRM,
31
- 'pcp' => Bolt::Config::Transport::Orch,
32
- 'local' => Bolt::Config::Transport::Local,
33
- 'docker' => Bolt::Config::Transport::Docker,
34
- 'remote' => Bolt::Config::Transport::Remote
35
- }.freeze
36
-
37
- # NOTE: All configuration options should have a corresponding schema property
38
- # in schemas/bolt-config.schema.json
39
- OPTIONS = {
40
- "apply_settings" => "A map of Puppet settings to use when applying Puppet code",
41
- "color" => "Whether to use colored output when printing messages to the console.",
42
- "compile-concurrency" => "The maximum number of simultaneous manifest block compiles.",
43
- "concurrency" => "The number of threads to use when executing on remote targets.",
44
- "format" => "The format to use when printing results. Options are `human` and `json`.",
45
- "hiera-config" => "The path to your Hiera config.",
46
- "inventoryfile" => "The path to a structured data inventory file used to refer to groups of "\
47
- "targets on the command line and from plans.",
48
- "log" => "The configuration of the logfile output. Configuration can be set for "\
49
- "`console` and the path to a log file, such as `~/.puppetlabs/bolt/debug.log`.",
50
- "modulepath" => "An array of directories that Bolt loads content (e.g. plans and tasks) from.",
51
- "plugin_hooks" => "Which plugins a specific hook should use.",
52
- "plugins" => "A map of plugins and their configuration data.",
53
- "puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
54
- "puppetfile" => "A map containing options for the `bolt puppetfile install` command.",
55
- "save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
56
- "your target names include passwords, set this value to `false` to avoid "\
57
- "writing passwords to disk.",
58
- "transport" => "The default transport to use when the transport for a target is not "\
59
- "specified in the URL or inventory.",
60
- "trusted-external-command" => "The path to an executable on the Bolt controller that can produce "\
61
- "external trusted facts. **External trusted facts are experimental in both "\
62
- "Puppet and Bolt and this API may change or be removed.**"
63
- }.freeze
64
-
65
- DEFAULT_OPTIONS = {
66
- "color" => true,
67
- "compile-concurrency" => "Number of cores",
68
- "concurrency" => "100 or one-third of the ulimit, whichever is lower",
69
- "format" => "human",
70
- "hiera-config" => "Boltdir/hiera.yaml",
71
- "inventoryfile" => "Boltdir/inventory.yaml",
72
- "modulepath" => ["Boltdir/modules", "Boltdir/site-modules", "Boltdir/site"],
73
- "save-rerun" => true
74
- }.freeze
75
-
76
- PUPPETFILE_OPTIONS = {
77
- "forge" => "A subsection that can have its own `proxy` setting to set an HTTP proxy for Forge operations "\
78
- "only, and a `baseurl` setting to specify a different Forge host.",
79
- "proxy" => "The HTTP proxy to use for Git and Forge operations."
80
- }.freeze
81
-
82
- LOG_OPTIONS = {
83
- "append" => "Add output to an existing log file. Available only for logs output to a "\
84
- "filepath.",
85
- "level" => "The type of information in the log. Either `debug`, `info`, `notice`, "\
86
- "`warn`, or `error`."
87
- }.freeze
88
-
89
- DEFAULT_LOG_OPTIONS = {
90
- "append" => true,
91
- "level" => "`warn` for console, `notice` for file"
92
- }.freeze
93
-
94
- APPLY_SETTINGS = {
95
- "show_diff" => "Whether to log and report a contextual diff when files are being replaced. "\
96
- "See [Puppet documentation](https://puppet.com/docs/puppet/latest/configuration.html#showdiff) "\
97
- "for details"
98
- }.freeze
99
-
100
- DEFAULT_APPLY_SETTINGS = {
101
- "show_diff" => false
102
- }.freeze
20
+ include Bolt::Config::Options
103
21
 
22
+ attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency
23
+
24
+ BOLT_CONFIG_NAME = 'bolt.yaml'
25
+ BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
26
+
27
+ # The default concurrency value that is used when the ulimit is not low (i.e. < 700)
104
28
  DEFAULT_DEFAULT_CONCURRENCY = 100
105
29
 
106
30
  def self.default
107
- new(Bolt::Project.new('.'), {})
31
+ new(Bolt::Project.default_project, {})
108
32
  end
109
33
 
110
34
  def self.from_project(project, overrides = {})
111
- data = {
112
- filepath: project.config_file,
113
- data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
114
- }
35
+ conf = if project.project_file == project.config_file
36
+ project.data
37
+ else
38
+ Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
39
+ end
115
40
 
116
- data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
41
+ data = load_defaults(project).push(
42
+ filepath: project.config_file,
43
+ data: conf,
44
+ warnings: []
45
+ )
117
46
 
118
47
  new(project, data, overrides)
119
48
  end
120
49
 
121
50
  def self.from_file(configfile, overrides = {})
122
- project = Bolt::Project.new(Pathname.new(configfile).expand_path.dirname)
51
+ project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
52
+
53
+ conf = if project.project_file == project.config_file
54
+ project.data
55
+ else
56
+ Bolt::Util.read_yaml_hash(configfile, 'config')
57
+ end
123
58
 
124
- data = {
59
+ data = load_defaults(project).push(
125
60
  filepath: project.config_file,
126
- data: Bolt::Util.read_yaml_hash(configfile, 'config')
127
- }
128
- data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
61
+ data: conf,
62
+ warnings: []
63
+ )
129
64
 
130
65
  new(project, data, overrides)
131
66
  end
132
67
 
133
- def self.load_defaults(project)
68
+ def self.system_path
134
69
  # Lazy-load expensive gem code
135
70
  require 'win32/dir' if Bolt::Util.windows?
136
71
 
137
- # Don't load /etc/puppetlabs/bolt/bolt.yaml twice
138
- confs = if project.path == Bolt::Project.system_path
139
- []
140
- else
141
- system_path = Pathname.new(File.join(Bolt::Project.system_path, 'bolt.yaml'))
142
- [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
143
- end
144
-
145
- user_path = begin
146
- Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'bolt.yaml')))
147
- rescue ArgumentError
148
- nil
149
- end
150
-
151
- confs << { filepath: user_path, data: Bolt::Util.read_optional_yaml_hash(user_path, 'config') } if user_path
72
+ if Bolt::Util.windows?
73
+ Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc'))
74
+ else
75
+ Pathname.new(File.join('/etc', 'puppetlabs', 'bolt'))
76
+ end
77
+ end
78
+
79
+ def self.user_path
80
+ Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt')))
81
+ rescue StandardError
82
+ nil
83
+ end
84
+
85
+ # Loads a 'bolt-defaults.yaml' file, which contains default configuration that applies to all
86
+ # projects. This file does not allow project-specific configuration such as 'hiera-config' and
87
+ # 'inventoryfile', and nests all default inventory configuration under an 'inventory-config' key.
88
+ def self.load_bolt_defaults_yaml(dir)
89
+ filepath = dir + BOLT_DEFAULTS_NAME
90
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
91
+ warnings = []
92
+
93
+ # Warn if 'bolt.yaml' detected in same directory.
94
+ if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
95
+ warnings.push(
96
+ msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
97
+ "will be ignored."
98
+ )
99
+ end
100
+
101
+ # Remove project-specific config such as hiera-config, etc.
102
+ project_config = data.slice(*(BOLT_PROJECT_OPTIONS - BOLT_DEFAULTS_OPTIONS))
103
+
104
+ if project_config.any?
105
+ data.reject! { |key, _| project_config.include?(key) }
106
+ warnings.push(
107
+ msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
108
+ "Project configuration should be set in 'bolt-project.yaml'."
109
+ )
110
+ end
111
+
112
+ # Remove top-level transport config such as transport, ssh, etc.
113
+ transport_config = data.slice(*INVENTORY_OPTIONS.keys)
114
+
115
+ if transport_config.any?
116
+ data.reject! { |key, _| transport_config.include?(key) }
117
+ warnings.push(
118
+ msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
119
+ "Transport configuration should be set under the 'inventory-config' option or "\
120
+ "in 'inventory.yaml'."
121
+ )
122
+ end
123
+
124
+ # Move data under inventory-config to top-level so it can be easily merged with
125
+ # config from other sources. Error early if inventory-config is not a hash or
126
+ # has a plugin reference.
127
+ if data.key?('inventory-config')
128
+ unless data['inventory-config'].is_a?(Hash)
129
+ raise Bolt::ValidationError,
130
+ "Option 'inventory-config' must be of type Hash, received #{data['inventory-config']} "\
131
+ "#{data['inventory-config']} (file: #{filepath})"
132
+ end
133
+
134
+ if data['inventory-config'].key?('_plugin')
135
+ raise Bolt::ValidationError,
136
+ "Found unsupported key '_plugin' for option 'inventory-config'; supported keys are "\
137
+ "'#{INVENTORY_OPTIONS.keys.join("', '")}' (file: #{filepath})"
138
+ end
139
+
140
+ data = data.merge(data.delete('inventory-config'))
141
+ end
142
+
143
+ { filepath: filepath, data: data, warnings: warnings }
144
+ end
145
+
146
+ # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
147
+ # here since Bolt::Config will just ignore any invalid keys.
148
+ def self.load_bolt_yaml(dir)
149
+ filepath = dir + BOLT_CONFIG_NAME
150
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
151
+ warnings = [msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
152
+ "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead."]
153
+
154
+ { filepath: filepath, data: data, warnings: warnings }
155
+ end
156
+
157
+ def self.load_defaults(project)
158
+ confs = []
159
+
160
+ # Load system-level config. Prefer a 'bolt-defaults.yaml' file, but fall back to the
161
+ # legacy 'bolt.yaml' file. If the project-level config file is also the system-level
162
+ # config file, don't load it a second time.
163
+ if File.exist?(system_path + BOLT_DEFAULTS_NAME)
164
+ confs << load_bolt_defaults_yaml(system_path)
165
+ elsif File.exist?(system_path + BOLT_CONFIG_NAME) &&
166
+ (system_path + BOLT_CONFIG_NAME) != project.config_file
167
+ confs << load_bolt_yaml(system_path)
168
+ end
169
+
170
+ # Load user-level config if there is a homedir. Prefer a 'bolt-defaults.yaml' file, but
171
+ # fall back to the legacy 'bolt.yaml' file.
172
+ if user_path
173
+ if File.exist?(user_path + BOLT_DEFAULTS_NAME)
174
+ confs << load_bolt_defaults_yaml(user_path)
175
+ elsif File.exist?(user_path + BOLT_CONFIG_NAME)
176
+ confs << load_bolt_yaml(user_path)
177
+ end
178
+ end
179
+
152
180
  confs
153
181
  end
154
182
 
155
183
  def initialize(project, config_data, overrides = {})
156
184
  unless config_data.is_a?(Array)
157
- config_data = [{ filepath: project.config_file, data: config_data }]
185
+ config_data = [{ filepath: project.config_file, data: config_data, warnings: [] }]
158
186
  end
159
187
 
160
- @logger = Logging.logger[self]
161
- @warnings = []
162
- @project = project
163
- @transports = {}
188
+ @logger = Logging.logger[self]
189
+ @project = project
190
+ @warnings = @project.warnings.dup
191
+ @transports = {}
164
192
  @config_files = []
165
193
 
166
194
  default_data = {
@@ -178,24 +206,22 @@ module Bolt
178
206
  'transport' => 'ssh'
179
207
  }
180
208
 
181
- loaded_data = config_data.map do |config|
182
- @config_files.push(config[:filepath])
183
- config[:data]
209
+ loaded_data = config_data.each_with_object([]) do |data, acc|
210
+ @warnings.concat(data[:warnings]) if data[:warnings].any?
211
+
212
+ if data[:data].any?
213
+ @config_files.push(data[:filepath])
214
+ acc.push(data[:data])
215
+ end
184
216
  end
185
217
 
186
218
  override_data = normalize_overrides(overrides)
187
219
 
188
220
  # If we need to lower concurrency and concurrency is not configured
189
221
  ld_concurrency = loaded_data.map(&:keys).flatten.include?('concurrency')
190
- if default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
191
- !ld_concurrency &&
192
- !override_data.key?('concurrency')
193
- concurrency_warning = { option: 'concurrency',
194
- msg: "Concurrency will default to #{default_concurrency} because ulimit "\
195
- "is low: #{Etc.sysconf(Etc::SC_OPEN_MAX)}. Set concurrency with "\
196
- "'--concurrency', or set your ulimit with 'ulimit -n <limit>'" }
197
- @warnings << concurrency_warning
198
- end
222
+ @modified_concurrency = default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
223
+ !ld_concurrency &&
224
+ !override_data.key?('concurrency')
199
225
 
200
226
  @data = merge_config_layers(default_data, *loaded_data, override_data)
201
227
 
@@ -212,12 +238,13 @@ module Bolt
212
238
  def normalize_overrides(options)
213
239
  opts = options.transform_keys(&:to_s)
214
240
 
215
- # Pull out config options
216
- overrides = opts.slice(*OPTIONS.keys)
241
+ # Pull out config options. We need to add 'transport' as it's not part of the
242
+ # OPTIONS hash but is a valid option that can be set with the --transport CLI option
243
+ overrides = opts.slice(*OPTIONS.keys, 'transport')
217
244
 
218
245
  # Pull out transport config options
219
246
  TRANSPORT_CONFIG.each do |transport, config|
220
- overrides[transport] = opts.slice(*config.options.keys)
247
+ overrides[transport] = opts.slice(*config.options)
221
248
  end
222
249
 
223
250
  # Set console log to debug if in debug mode
@@ -284,8 +311,8 @@ module Bolt
284
311
  end
285
312
 
286
313
  # Filter hashes to only include valid options
287
- @data['apply_settings'] = @data['apply_settings'].slice(*APPLY_SETTINGS.keys)
288
- @data['puppetfile'] = @data['puppetfile'].slice(*PUPPETFILE_OPTIONS.keys)
314
+ @data['apply_settings'] = @data['apply_settings'].slice(*OPTIONS['apply_settings'][:properties].keys)
315
+ @data['puppetfile'] = @data['puppetfile'].slice(*OPTIONS['puppetfile'][:properties].keys)
289
316
  end
290
317
 
291
318
  private def normalize_log(target)
@@ -299,7 +326,7 @@ module Bolt
299
326
  next unless val.is_a?(Hash)
300
327
 
301
328
  name = normalize_log(key)
302
- acc[name] = val.slice(*LOG_OPTIONS.keys)
329
+ acc[name] = val.slice('append', 'level')
303
330
  .transform_keys(&:to_sym)
304
331
 
305
332
  if (v = acc[name][:level])
@@ -351,7 +378,7 @@ module Bolt
351
378
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
352
379
  end
353
380
 
354
- unless %w[human json].include? format
381
+ if (format == 'rainbow' && Bolt::Util.windows?) || !(%w[human json rainbow].include? format)
355
382
  raise Bolt::ValidationError, "Unsupported format: '#{format}'"
356
383
  end
357
384
 
@@ -483,7 +510,7 @@ module Bolt
483
510
  @default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
484
511
  DEFAULT_DEFAULT_CONCURRENCY
485
512
  else
486
- Etc.sysconf(Etc::SC_OPEN_MAX) / 3
513
+ Etc.sysconf(Etc::SC_OPEN_MAX) / 7
487
514
  end
488
515
  end
489
516
  end