bolt 2.11.0 → 2.15.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +3 -2
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +52 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +65 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +4 -2
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -2
  10. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +65 -43
  11. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -1
  14. data/lib/bolt/analytics.rb +21 -2
  15. data/lib/bolt/applicator.rb +19 -7
  16. data/lib/bolt/apply_inventory.rb +4 -0
  17. data/lib/bolt/apply_target.rb +4 -0
  18. data/lib/bolt/bolt_option_parser.rb +4 -3
  19. data/lib/bolt/catalog.rb +81 -68
  20. data/lib/bolt/cli.rb +16 -5
  21. data/lib/bolt/config.rb +216 -75
  22. data/lib/bolt/config/transport/ssh.rb +130 -91
  23. data/lib/bolt/executor.rb +14 -1
  24. data/lib/bolt/inventory/group.rb +1 -1
  25. data/lib/bolt/inventory/inventory.rb +4 -0
  26. data/lib/bolt/inventory/target.rb +4 -0
  27. data/lib/bolt/outputter.rb +3 -0
  28. data/lib/bolt/outputter/rainbow.rb +80 -0
  29. data/lib/bolt/pal.rb +3 -0
  30. data/lib/bolt/project.rb +48 -11
  31. data/lib/bolt/resource_instance.rb +10 -3
  32. data/lib/bolt/shell/powershell/snippets.rb +8 -0
  33. data/lib/bolt/transport/local/connection.rb +2 -1
  34. data/lib/bolt/transport/ssh/connection.rb +35 -0
  35. data/lib/bolt/version.rb +1 -1
  36. data/lib/bolt_spec/bolt_context.rb +1 -1
  37. data/lib/bolt_spec/run.rb +1 -1
  38. metadata +21 -5
@@ -14,14 +14,16 @@ require 'open3'
14
14
 
15
15
  module Bolt
16
16
  class Applicator
17
- def initialize(inventory, executor, modulepath, plugin_dirs, pdb_client, hiera_config, max_compiles, apply_settings)
17
+ def initialize(inventory, executor, modulepath, plugin_dirs, project,
18
+ pdb_client, hiera_config, max_compiles, apply_settings)
18
19
  # lazy-load expensive gem code
19
20
  require 'concurrent'
20
21
 
21
22
  @inventory = inventory
22
23
  @executor = executor
23
- @modulepath = modulepath
24
+ @modulepath = modulepath || []
24
25
  @plugin_dirs = plugin_dirs
26
+ @project = project
25
27
  @pdb_client = pdb_client
26
28
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
27
29
  @apply_settings = apply_settings || {}
@@ -104,6 +106,16 @@ module Bolt
104
106
  out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
105
107
  ENV['PATH'] = old_path
106
108
 
109
+ # If bolt_catalog does not return valid JSON, we should print stderr to
110
+ # see what happened
111
+ print_logs = stat.success?
112
+ result = begin
113
+ JSON.parse(out)
114
+ rescue JSON::ParserError
115
+ print_logs = true
116
+ { 'message' => "Something's gone terribly wrong! STDERR is logged." }
117
+ end
118
+
107
119
  # Any messages logged by Puppet will be on stderr as JSON hashes, so we
108
120
  # parse those and store them here. Any message on stderr that is not
109
121
  # properly JSON formatted is assumed to be an error message. If
@@ -117,17 +129,15 @@ module Bolt
117
129
  { 'level' => 'err', 'message' => line }
118
130
  end
119
131
 
120
- result = JSON.parse(out)
121
- if stat.success?
132
+ if print_logs
122
133
  logs.each do |log|
123
134
  bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[log['level'].to_sym]
124
135
  message = log['message'].chomp
125
136
  @logger.send(bolt_level, "#{target.name}: #{message}")
126
137
  end
127
- result
128
- else
129
- raise ApplyError.new(target.name, result['message'])
130
138
  end
139
+ raise ApplyError.new(target.name, result['message']) unless stat.success?
140
+ result
131
141
  end
132
142
 
133
143
  def validate_hiera_config(hiera_config)
@@ -144,6 +154,7 @@ module Bolt
144
154
 
145
155
  def apply(args, apply_body, scope)
146
156
  raise(ArgumentError, 'apply requires a TargetSpec') if args.empty?
157
+ raise(ArgumentError, 'apply requires at least one statement in the apply block') if apply_body.nil?
147
158
  type0 = Puppet.lookup(:pal_script_compiler).type('TargetSpec')
148
159
  Puppet::Pal.assert_type(type0, args[0], 'apply targets')
149
160
 
@@ -188,6 +199,7 @@ module Bolt
188
199
  scope = {
189
200
  code_ast: ast,
190
201
  modulepath: @modulepath,
202
+ project: @project.to_h,
191
203
  pdb_config: @pdb_client.config.to_hash,
192
204
  hiera_config: @hiera_config,
193
205
  plan_vars: plan_vars,
@@ -70,6 +70,10 @@ module Bolt
70
70
  @targets[target.name].features
71
71
  end
72
72
 
73
+ def resource(target, type, title)
74
+ @targets[target.name].resource(type, title)
75
+ end
76
+
73
77
  def add_to_group(*_params)
74
78
  raise InvalidFunctionCall, 'add_to_group'
75
79
  end
@@ -62,6 +62,10 @@ module Bolt
62
62
  @safe_name
63
63
  end
64
64
 
65
+ def resource(type, title)
66
+ resources[Bolt::ResourceInstance.format_reference(type, title)]
67
+ end
68
+
65
69
  def parse_uri(string)
66
70
  require 'addressable/uri'
67
71
  if string.nil?
@@ -20,7 +20,7 @@ module Bolt
20
20
  def get_help_text(subcommand, action = nil)
21
21
  case subcommand
22
22
  when 'apply'
23
- { flags: ACTION_OPTS + %w[noop execute compile-concurrency],
23
+ { flags: ACTION_OPTS + %w[noop execute compile-concurrency hiera-config],
24
24
  banner: APPLY_HELP }
25
25
  when 'command'
26
26
  case action
@@ -172,13 +172,14 @@ module Bolt
172
172
  apply
173
173
 
174
174
  USAGE
175
- bolt apply <manifest.pp> [options]
175
+ bolt apply [manifest.pp] [options]
176
176
 
177
177
  DESCRIPTION
178
178
  Apply Puppet manifest code on the specified targets.
179
179
 
180
180
  EXAMPLES
181
- bolt apply manifest.pp --targets target1,target2
181
+ bolt apply manifest.pp -t target
182
+ bolt apply -e "file { '/etc/puppetlabs': ensure => present }" -t target
182
183
  HELP
183
184
 
184
185
  COMMAND_HELP = <<~HELP
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/apply_inventory'
3
4
  require 'bolt/apply_target'
4
5
  require 'bolt/config'
5
6
  require 'bolt/error'
6
7
  require 'bolt/inventory'
7
- require 'bolt/apply_inventory'
8
8
  require 'bolt/pal'
9
9
  require 'bolt/puppetdb'
10
10
  require 'bolt/util'
@@ -19,7 +19,7 @@ module Bolt
19
19
  @log_level = log_level
20
20
  end
21
21
 
22
- def with_puppet_settings(hiera_config = {})
22
+ def with_puppet_settings(overrides = {})
23
23
  Dir.mktmpdir('bolt') do |dir|
24
24
  cli = []
25
25
  Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
@@ -31,7 +31,9 @@ module Bolt
31
31
  Puppet.settings.override_default(:vendormoduledir, '')
32
32
 
33
33
  Puppet.initialize_settings(cli)
34
- Puppet.settings[:hiera_config] = hiera_config
34
+ overrides.each do |setting, value|
35
+ Puppet.settings[setting] = value
36
+ end
35
37
 
36
38
  # Use a special logdest that serializes all log messages and their level to stderr.
37
39
  Puppet::Util::Log.newdestination(:stderr)
@@ -54,80 +56,51 @@ module Bolt
54
56
  end
55
57
 
56
58
  def compile_catalog(request)
57
- pal_main = request['code_ast'] || request['code_string']
58
- target = request['target']
59
59
  pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
60
- options = request['puppet_config'] || {}
61
- with_puppet_settings(request['hiera_config']) do
62
- Puppet[:rich_data] = true
63
- Puppet[:node_name_value] = target['name']
64
- env_conf = { modulepath: request['modulepath'] || [],
65
- facts: target['facts'] || {} }
66
- env_conf[:variables] = {}
60
+ project = request['project'] || {}
61
+ bolt_project = Struct.new(:name, :path).new(project['name'], project['path']) unless project.empty?
62
+ inv = Bolt::ApplyInventory.new(request['config'])
63
+ puppet_overrides = {
64
+ bolt_pdb_client: pdb_client,
65
+ bolt_inventory: inv,
66
+ bolt_project: bolt_project
67
+ }
68
+
69
+ # Facts will be set by the catalog compiler, so we need to ensure
70
+ # that any plan or target variables with the same name are not
71
+ # passed into the apply block to avoid a redefinition error.
72
+ # Filter out plan and target vars separately and raise a Puppet
73
+ # warning if there are any collisions for either. Puppet warning
74
+ # is the only way to log a message that will make it back to Bolt
75
+ # to be printed.
76
+ target = request['target']
77
+ plan_vars = shadow_vars('plan', request['plan_vars'], target['facts'])
78
+ target_vars = shadow_vars('target', target['variables'], target['facts'])
79
+ topscope_vars = target_vars.merge(plan_vars)
80
+ env_conf = { modulepath: request['modulepath'],
81
+ facts: target['facts'],
82
+ variables: topscope_vars }
83
+
84
+ puppet_settings = {
85
+ node_name_value: target['name'],
86
+ hiera_config: request['hiera_config']
87
+ }
88
+
89
+ with_puppet_settings(puppet_settings) do
67
90
  Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
68
- inv = Bolt::ApplyInventory.new(request['config'])
69
- Puppet.override(bolt_pdb_client: pdb_client,
70
- bolt_inventory: inv) do
91
+ Puppet.override(puppet_overrides) do
71
92
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
72
93
  pal.with_catalog_compiler do |compiler|
73
- # Deserializing needs to happen inside the catalog compiler so
74
- # loaders are initialized for loading
75
- plan_vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
76
-
77
- # Facts will be set by the catalog compiler, so we need to ensure
78
- # that any plan or target variables with the same name are not
79
- # passed into the apply block to avoid a redefinition error.
80
- # Filter out plan and target vars separately and raise a Puppet
81
- # warning if there are any collisions for either. Puppet warning
82
- # is the only way to log a message that will make it back to Bolt
83
- # to be printed.
84
- pv_collisions, pv_filtered = plan_vars.partition do |k, _|
85
- target['facts'].keys.include?(k)
86
- end.map(&:to_h)
87
- unless pv_collisions.empty?
88
- print_pv = pv_collisions.keys.map { |k| "$#{k}" }.join(', ')
89
- plural = pv_collisions.keys.length == 1 ? '' : 's'
90
- Puppet.warning("Plan variable#{plural} #{print_pv} will be overridden by fact#{plural} " \
91
- "of the same name in the apply block")
92
- end
93
-
94
- tv_collisions, tv_filtered = target['variables'].partition do |k, _|
95
- target['facts'].keys.include?(k)
96
- end.map(&:to_h)
97
- unless tv_collisions.empty?
98
- print_tv = tv_collisions.keys.map { |k| "$#{k}" }.join(', ')
99
- plural = tv_collisions.keys.length == 1 ? '' : 's'
100
- Puppet.warning("Target variable#{plural} #{print_tv} " \
101
- "will be overridden by fact#{plural} of the same name in the apply block")
102
- end
103
-
104
- pal.send(:add_variables, compiler.send(:topscope), tv_filtered.merge(pv_filtered))
105
-
94
+ options = request['puppet_config'] || {}
106
95
  # Configure language strictness in the CatalogCompiler. We want Bolt to be able
107
96
  # to compile most Puppet 4+ manifests, so we default to allowing deprecated functions.
108
97
  Puppet[:strict] = options['strict'] || :warning
109
98
  Puppet[:strict_variables] = options['strict_variables'] || false
110
- ast = Puppet::Pops::Serialization::FromDataConverter.convert(pal_main)
111
- # This will be a Program when running via `bolt apply`, but will
112
- # only be a subset of the AST when compiling an apply block in a
113
- # plan. In that case, we need to discover the definitions (which
114
- # would ordinarily be stored on the Program) and construct a Program object.
115
- unless ast.is_a?(Puppet::Pops::Model::Program)
116
- # Node definitions must be at the top level of the apply block.
117
- # That means the apply body either a) consists of just a
118
- # NodeDefinition, b) consists of a BlockExpression which may
119
- # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
120
- definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
121
- ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
122
- elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
123
- [ast]
124
- else
125
- []
126
- end
127
- ast = Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
128
- end
99
+
100
+ pal_main = request['code_ast'] || request['code_string']
101
+ ast = build_program(pal_main)
129
102
  compiler.evaluate(ast)
130
- compiler.instance_variable_get(:@internal_compiler).send(:evaluate_ast_node)
103
+ compiler.evaluate_ast_node
131
104
  compiler.compile_additions
132
105
  compiler.with_json_encoding(&:encode)
133
106
  end
@@ -135,5 +108,45 @@ module Bolt
135
108
  end
136
109
  end
137
110
  end
111
+
112
+ # Warn and remove variables that will be shadowed by facts of the same
113
+ # name, which are set in scope earlier.
114
+ def shadow_vars(type, vars, facts)
115
+ collisions, valid = vars.partition do |k, _|
116
+ facts.include?(k)
117
+ end
118
+ if collisions.any?
119
+ names = collisions.map { |k, _| "$#{k}" }.join(', ')
120
+ plural = collisions.length == 1 ? '' : 's'
121
+ Puppet.warning("#{type.capitalize} variable#{plural} #{names} will be overridden by fact#{plural} " \
122
+ "of the same name in the apply block")
123
+ end
124
+ valid.to_h
125
+ end
126
+
127
+ def build_program(code)
128
+ ast = Puppet::Pops::Serialization::FromDataConverter.convert(code)
129
+
130
+ # This will be a Program when running via `bolt apply`, but will
131
+ # only be a subset of the AST when compiling an apply block in a
132
+ # plan. In that case, we need to discover the definitions (which
133
+ # would ordinarily be stored on the Program) and construct a Program object.
134
+ if ast.is_a?(Puppet::Pops::Model::Program)
135
+ ast
136
+ else
137
+ # Node definitions must be at the top level of the apply block.
138
+ # That means the apply body either a) consists of just a
139
+ # NodeDefinition, b) consists of a BlockExpression which may
140
+ # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
141
+ definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
142
+ ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
143
+ elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
144
+ [ast]
145
+ else
146
+ []
147
+ end
148
+ Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
149
+ end
150
+ end
138
151
  end
139
152
  end
@@ -115,7 +115,7 @@ module Bolt
115
115
  Bolt::Config.from_file(options[:configfile], options)
116
116
  else
117
117
  project = if options[:boltdir]
118
- Bolt::Project.new(options[:boltdir])
118
+ Bolt::Project.create_project(options[:boltdir])
119
119
  else
120
120
  Bolt::Project.find_boltdir(Dir.pwd)
121
121
  end
@@ -392,7 +392,7 @@ module Bolt
392
392
  end
393
393
  code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
394
394
  else
395
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
395
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
396
396
  targets = options[:targets]
397
397
 
398
398
  results = nil
@@ -512,7 +512,7 @@ module Bolt
512
512
  params: plan_arguments }
513
513
  plan_context[:description] = options[:description] if options[:description]
514
514
 
515
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
515
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
516
516
  if options.fetch(:format, 'human') == 'human'
517
517
  executor.subscribe(outputter)
518
518
  else
@@ -537,7 +537,18 @@ module Bolt
537
537
  Puppet[:tasks] = false
538
538
  ast = pal.parse_manifest(code, filename)
539
539
 
540
- executor = Bolt::Executor.new(config.concurrency, analytics, noop)
540
+ if defined?(ast.body) &&
541
+ (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
542
+ ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
543
+ message = "Manifest only contains definitions and will result in no changes on the targets. "\
544
+ "Definitions must be declared for their resources to be applied. You can read more "\
545
+ "about defining and declaring classes and types in the Puppet documentation at "\
546
+ "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
547
+ "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
548
+ @logger.warn(message)
549
+ end
550
+
551
+ executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
541
552
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
542
553
  executor.subscribe(log_outputter)
543
554
  # apply logging looks like plan logging, so tell the outputter we're in a
@@ -760,7 +771,7 @@ module Bolt
760
771
  end
761
772
 
762
773
  def pal
763
- project = config.project.load_as_module? ? config.project : nil
774
+ project = config.project.project_file? ? config.project : nil
764
775
  @pal ||= Bolt::PAL.new(config.modulepath,
765
776
  config.hiera_config,
766
777
  config.project.resource_types,
@@ -23,8 +23,13 @@ module Bolt
23
23
  end
24
24
 
25
25
  class Config
26
- attr_reader :config_files, :warnings, :data, :transports, :project
26
+ attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency
27
27
 
28
+ BOLT_CONFIG_NAME = 'bolt.yaml'
29
+ BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
30
+
31
+ # Transport config classes. Used to load default transport config which
32
+ # gets passed along to the inventory.
28
33
  TRANSPORT_CONFIG = {
29
34
  'ssh' => Bolt::Config::Transport::SSH,
30
35
  'winrm' => Bolt::Config::Transport::WinRM,
@@ -34,45 +39,84 @@ module Bolt
34
39
  'remote' => Bolt::Config::Transport::Remote
35
40
  }.freeze
36
41
 
37
- # NOTE: All configuration options should have a corresponding schema property
38
- # in schemas/bolt-config.schema.json
39
- OPTIONS = {
42
+ # Options that configure Bolt. These options are used in bolt.yaml and
43
+ # bolt-defaults.yaml.
44
+ BOLT_CONFIG = {
45
+ "color" => "Whether to use colored output when printing messages to the console.",
46
+ "compile-concurrency" => "The maximum number of simultaneous manifest block compiles.",
47
+ "concurrency" => "The number of threads to use when executing on remote targets.",
48
+ "format" => "The format to use when printing results. Options are `human` and `json`.",
49
+ "plugin_hooks" => "Which plugins a specific hook should use.",
50
+ "plugins" => "A map of plugins and their configuration data.",
51
+ "puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
52
+ "puppetfile" => "A map containing options for the `bolt puppetfile install` command.",
53
+ "save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
54
+ "your target names include passwords, set this value to `false` to avoid "\
55
+ "writing passwords to disk."
56
+ }.freeze
57
+
58
+ # These options are only available to bolt-defaults.yaml.
59
+ DEFAULTS_CONFIG = {
60
+ "inventory-config" => "A map of default configuration options for the inventory. This includes options "\
61
+ "for setting the default transport to use when connecting to targets, as well as "\
62
+ "options for configuring the default behavior of each transport."
63
+ }.freeze
64
+
65
+ # Options that configure the inventory, specifically the default transport
66
+ # used by targets and the transports themselves. These options are used in
67
+ # bolt.yaml, inventory.yaml, and under the inventory-config key in
68
+ # bolt-defaults.yaml.
69
+ INVENTORY_CONFIG = {
70
+ "transport" => "The default transport to use when the transport for a target is not specified in the URI.",
71
+ "docker" => "A map of configuration options for the docker transport.",
72
+ "local" => "A map of configuration options for the local transport.",
73
+ "pcp" => "A map of configuration options for the pcp transport.",
74
+ "remote" => "A map of configuration options for the remote transport.",
75
+ "ssh" => "A map of configuration options for the ssh transport.",
76
+ "winrm" => "A map of configuration options for the winrm transport."
77
+ }.freeze
78
+
79
+ # Options that configure the project, such as paths to files used for a
80
+ # specific project. These settings are used in bolt.yaml and bolt-project.yaml.
81
+ PROJECT_CONFIG = {
40
82
  "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
83
  "hiera-config" => "The path to your Hiera config.",
46
84
  "inventoryfile" => "The path to a structured data inventory file used to refer to groups of "\
47
85
  "targets on the command line and from plans.",
48
86
  "log" => "The configuration of the logfile output. Configuration can be set for "\
49
87
  "`console` and the path to a log file, such as `~/.puppetlabs/bolt/debug.log`.",
50
- "modulepath" => "The module path for loading tasks and plan code. This is either an array "\
51
- "of directories or a string containing a list of directories separated by the "\
52
- "OS-specific PATH separator.",
53
- "plugin_hooks" => "Which plugins a specific hook should use.",
54
- "plugins" => "A map of plugins and their configuration data.",
55
- "puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
56
- "puppetfile" => "A map containing options for the `bolt puppetfile install` command.",
57
- "save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
58
- "your target names include passwords, set this value to `false` to avoid "\
59
- "writing passwords to disk.",
60
- "transport" => "The default transport to use when the transport for a target is not "\
61
- "specified in the URL or inventory.",
88
+ "modulepath" => "An array of directories that Bolt loads content (e.g. plans and tasks) from.",
62
89
  "trusted-external-command" => "The path to an executable on the Bolt controller that can produce "\
63
90
  "external trusted facts. **External trusted facts are experimental in both "\
64
91
  "Puppet and Bolt and this API may change or be removed.**"
65
92
  }.freeze
66
93
 
94
+ # A combined map of all configuration options that can be set in this class.
95
+ # Includes all options except 'inventory-config', which is munged when loading
96
+ # a bolt-defaults.yaml file.
97
+ OPTIONS = BOLT_CONFIG.merge(INVENTORY_CONFIG).merge(PROJECT_CONFIG).freeze
98
+
99
+ # Default values for select options. These do not set the default values in Bolt
100
+ # and are only used for documentation.
67
101
  DEFAULT_OPTIONS = {
68
- "color" => true,
102
+ "color" => true,
69
103
  "compile-concurrency" => "Number of cores",
70
- "concurrency" => "100 or one-third of the ulimit, whichever is lower",
71
- "format" => "human",
72
- "hiera-config" => "Boltdir/hiera.yaml",
73
- "inventoryfile" => "Boltdir/inventory.yaml",
74
- "modulepath" => ["Boltdir/modules", "Boltdir/site-modules", "Boltdir/site"],
75
- "save-rerun" => true
104
+ "concurrency" => "100 or one-seventh of the ulimit, whichever is lower",
105
+ "format" => "human",
106
+ "hiera-config" => "Boltdir/hiera.yaml",
107
+ "inventoryfile" => "Boltdir/inventory.yaml",
108
+ "modulepath" => ["Boltdir/modules", "Boltdir/site-modules", "Boltdir/site"],
109
+ "save-rerun" => true,
110
+ "transport" => "ssh"
111
+ }.freeze
112
+
113
+ PUPPETDB_OPTIONS = {
114
+ "cacert" => "The path to the ca certificate for PuppetDB.",
115
+ "cert" => "The path to the client certificate file to use for authentication.",
116
+ "key" => "The private key for the certificate.",
117
+ "server_urls" => "An array containing the PuppetDB host to connect to. Include the protocol `https` and "\
118
+ "the port, which is usually `8081`. For example, `https://my-master.example.com:8081`.",
119
+ "token" => "The path to the PE RBAC Token."
76
120
  }.freeze
77
121
 
78
122
  PUPPETFILE_OPTIONS = {
@@ -106,61 +150,154 @@ module Bolt
106
150
  DEFAULT_DEFAULT_CONCURRENCY = 100
107
151
 
108
152
  def self.default
109
- new(Bolt::Project.new('.'), {})
153
+ new(Bolt::Project.create_project('.'), {})
110
154
  end
111
155
 
112
156
  def self.from_project(project, overrides = {})
113
- data = {
114
- filepath: project.config_file,
115
- data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
116
- }
157
+ conf = if project.project_file == project.config_file
158
+ project.data
159
+ else
160
+ Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
161
+ end
117
162
 
118
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
163
+ data = load_defaults(project).push(
164
+ filepath: project.config_file,
165
+ data: conf,
166
+ warnings: []
167
+ )
119
168
 
120
169
  new(project, data, overrides)
121
170
  end
122
171
 
123
172
  def self.from_file(configfile, overrides = {})
124
- project = Bolt::Project.new(Pathname.new(configfile).expand_path.dirname)
173
+ project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
125
174
 
126
- data = {
175
+ conf = if project.project_file == project.config_file
176
+ project.data
177
+ else
178
+ Bolt::Util.read_yaml_hash(configfile, 'config')
179
+ end
180
+
181
+ data = load_defaults(project).push(
127
182
  filepath: project.config_file,
128
- data: Bolt::Util.read_yaml_hash(configfile, 'config')
129
- }
130
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
183
+ data: conf,
184
+ warnings: []
185
+ )
131
186
 
132
187
  new(project, data, overrides)
133
188
  end
134
189
 
135
- def self.load_defaults
190
+ def self.system_path
136
191
  # Lazy-load expensive gem code
137
192
  require 'win32/dir' if Bolt::Util.windows?
138
193
 
139
- system_path = if Bolt::Util.windows?
140
- Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc', 'bolt.yaml'))
141
- else
142
- Pathname.new(File.join('/etc', 'puppetlabs', 'bolt', 'bolt.yaml'))
143
- end
144
- user_path = begin
145
- Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'bolt.yaml')))
146
- rescue ArgumentError
147
- nil
148
- end
149
-
150
- confs = [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
151
- confs << { filepath: user_path, data: Bolt::Util.read_optional_yaml_hash(user_path, 'config') } if user_path
194
+ if Bolt::Util.windows?
195
+ Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc'))
196
+ else
197
+ Pathname.new(File.join('/etc', 'puppetlabs', 'bolt'))
198
+ end
199
+ end
200
+
201
+ def self.user_path
202
+ Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt')))
203
+ rescue StandardError
204
+ nil
205
+ end
206
+
207
+ # Loads a 'bolt-defaults.yaml' file, which contains default configuration that applies to all
208
+ # projects. This file does not allow project-specific configuration such as 'hiera-config' and
209
+ # 'inventoryfile', and nests all default inventory configuration under an 'inventory-config' key.
210
+ def self.load_bolt_defaults_yaml(dir)
211
+ filepath = dir + BOLT_DEFAULTS_NAME
212
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
213
+ warnings = []
214
+
215
+ # Warn if 'bolt.yaml' detected in same directory.
216
+ if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
217
+ warnings.push(
218
+ msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
219
+ "will be ignored."
220
+ )
221
+ end
222
+
223
+ # Remove project-specific config such as hiera-config, etc.
224
+ project_config = data.slice(*PROJECT_CONFIG.keys)
225
+
226
+ if project_config.any?
227
+ data.reject! { |key, _| project_config.include?(key) }
228
+ warnings.push(
229
+ msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
230
+ "Project configuration should be set in 'bolt-project.yaml'."
231
+ )
232
+ end
233
+
234
+ # Remove top-level transport config such as transport, ssh, etc.
235
+ transport_config = data.slice(*INVENTORY_CONFIG.keys)
236
+
237
+ if transport_config.any?
238
+ data.reject! { |key, _| transport_config.include?(key) }
239
+ warnings.push(
240
+ msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
241
+ "Transport configuration should be set under the 'inventory-config' option or "\
242
+ "in 'inventory.yaml'."
243
+ )
244
+ end
245
+
246
+ # Move data under transport-config to top-level so it can be easily merged with
247
+ # config from other sources.
248
+ if data.key?('inventory-config')
249
+ data = data.merge(data.delete('inventory-config'))
250
+ end
251
+
252
+ { filepath: filepath, data: data, warnings: warnings }
253
+ end
254
+
255
+ # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
256
+ # here since Bolt::Config will just ignore any invalid keys.
257
+ def self.load_bolt_yaml(dir)
258
+ filepath = dir + BOLT_CONFIG_NAME
259
+ data = Bolt::Util.read_yaml_hash(filepath, 'config')
260
+ warnings = [msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
261
+ "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead."]
262
+
263
+ { filepath: filepath, data: data, warnings: warnings }
264
+ end
265
+
266
+ def self.load_defaults(project)
267
+ confs = []
268
+
269
+ # Load system-level config. Prefer a 'bolt-defaults.yaml' file, but fall back to the
270
+ # legacy 'bolt.yaml' file. If the project-level config file is also the system-level
271
+ # config file, don't load it a second time.
272
+ if File.exist?(system_path + BOLT_DEFAULTS_NAME)
273
+ confs << load_bolt_defaults_yaml(system_path)
274
+ elsif File.exist?(system_path + BOLT_CONFIG_NAME) &&
275
+ (system_path + BOLT_CONFIG_NAME) != project.config_file
276
+ confs << load_bolt_yaml(system_path)
277
+ end
278
+
279
+ # Load user-level config if there is a homedir. Prefer a 'bolt-defaults.yaml' file, but
280
+ # fall back to the legacy 'bolt.yaml' file.
281
+ if user_path
282
+ if File.exist?(user_path + BOLT_DEFAULTS_NAME)
283
+ confs << load_bolt_defaults_yaml(user_path)
284
+ elsif File.exist?(user_path + BOLT_CONFIG_NAME)
285
+ confs << load_bolt_yaml(user_path)
286
+ end
287
+ end
288
+
152
289
  confs
153
290
  end
154
291
 
155
292
  def initialize(project, config_data, overrides = {})
156
293
  unless config_data.is_a?(Array)
157
- config_data = [{ filepath: project.config_file, data: config_data }]
294
+ config_data = [{ filepath: project.config_file, data: config_data, warnings: [] }]
158
295
  end
159
296
 
160
- @logger = Logging.logger[self]
161
- @warnings = []
162
- @project = project
163
- @transports = {}
297
+ @logger = Logging.logger[self]
298
+ @project = project
299
+ @warnings = @project.warnings.dup
300
+ @transports = {}
164
301
  @config_files = []
165
302
 
166
303
  default_data = {
@@ -178,24 +315,22 @@ module Bolt
178
315
  'transport' => 'ssh'
179
316
  }
180
317
 
181
- loaded_data = config_data.map do |config|
182
- @config_files.push(config[:filepath])
183
- config[:data]
318
+ loaded_data = config_data.each_with_object([]) do |data, acc|
319
+ @warnings.concat(data[:warnings]) if data[:warnings].any?
320
+
321
+ if data[:data].any?
322
+ @config_files.push(data[:filepath])
323
+ acc.push(data[:data])
324
+ end
184
325
  end
185
326
 
186
327
  override_data = normalize_overrides(overrides)
187
328
 
188
329
  # If we need to lower concurrency and concurrency is not configured
189
330
  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
331
+ @modified_concurrency = default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
332
+ !ld_concurrency &&
333
+ !override_data.key?('concurrency')
199
334
 
200
335
  @data = merge_config_layers(default_data, *loaded_data, override_data)
201
336
 
@@ -351,7 +486,7 @@ module Bolt
351
486
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
352
487
  end
353
488
 
354
- unless %w[human json].include? format
489
+ if (format == 'rainbow' && Bolt::Util.windows?) || !(%w[human json rainbow].include? format)
355
490
  raise Bolt::ValidationError, "Unsupported format: '#{format}'"
356
491
  end
357
492
 
@@ -473,12 +608,18 @@ module Bolt
473
608
  end.join
474
609
  end
475
610
 
611
+ # Etc::SC_OPEN_MAX is meaningless on windows, not defined in PE Jruby and not available
612
+ # on some platforms. This method holds the logic to decide whether or not to even consider it.
613
+ def sc_open_max_available?
614
+ !Bolt::Util.windows? && defined?(Etc::SC_OPEN_MAX) && Etc.sysconf(Etc::SC_OPEN_MAX)
615
+ end
616
+
476
617
  def default_concurrency
477
- if Bolt::Util.windows? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300 || Etc.sysconf(Etc::SC_OPEN_MAX).nil?
478
- DEFAULT_DEFAULT_CONCURRENCY
479
- else
480
- (Etc.sysconf(Etc::SC_OPEN_MAX) / 3).floor
481
- end
618
+ @default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
619
+ DEFAULT_DEFAULT_CONCURRENCY
620
+ else
621
+ Etc.sysconf(Etc::SC_OPEN_MAX) / 7
622
+ end
482
623
  end
483
624
  end
484
625
  end