bolt 2.24.1 → 2.29.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/lib/bolt/analytics.rb +7 -3
  7. data/lib/bolt/applicator.rb +21 -21
  8. data/lib/bolt/bolt_option_parser.rb +77 -26
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +135 -147
  11. data/lib/bolt/config.rb +48 -25
  12. data/lib/bolt/config/options.rb +34 -2
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +8 -1
  15. data/lib/bolt/inventory/group.rb +1 -1
  16. data/lib/bolt/inventory/inventory.rb +1 -1
  17. data/lib/bolt/inventory/target.rb +1 -1
  18. data/lib/bolt/logger.rb +35 -21
  19. data/lib/bolt/outputter/logger.rb +1 -1
  20. data/lib/bolt/pal.rb +21 -10
  21. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  22. data/lib/bolt/plugin/puppetdb.rb +1 -1
  23. data/lib/bolt/project.rb +62 -17
  24. data/lib/bolt/puppetdb/client.rb +1 -1
  25. data/lib/bolt/puppetdb/config.rb +1 -1
  26. data/lib/bolt/puppetfile.rb +160 -0
  27. data/lib/bolt/puppetfile/installer.rb +43 -0
  28. data/lib/bolt/puppetfile/module.rb +89 -0
  29. data/lib/bolt/r10k_log_proxy.rb +1 -1
  30. data/lib/bolt/rerun.rb +2 -2
  31. data/lib/bolt/result.rb +23 -0
  32. data/lib/bolt/shell.rb +1 -1
  33. data/lib/bolt/task.rb +1 -1
  34. data/lib/bolt/transport/base.rb +5 -5
  35. data/lib/bolt/transport/docker/connection.rb +1 -1
  36. data/lib/bolt/transport/local/connection.rb +1 -1
  37. data/lib/bolt/transport/ssh.rb +1 -1
  38. data/lib/bolt/transport/ssh/connection.rb +1 -1
  39. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  40. data/lib/bolt/transport/winrm.rb +1 -1
  41. data/lib/bolt/transport/winrm/connection.rb +1 -1
  42. data/lib/bolt/util.rb +30 -11
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -12
  47. data/lib/bolt_server/transport_app.rb +125 -26
  48. data/lib/bolt_spec/bolt_context.rb +4 -4
  49. data/lib/bolt_spec/run.rb +3 -0
  50. metadata +11 -14
@@ -19,7 +19,7 @@ module Bolt
19
19
  class Config
20
20
  include Bolt::Config::Options
21
21
 
22
- attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency, :deprecations
22
+ attr_reader :config_files, :logs, :data, :transports, :project, :modified_concurrency, :deprecations
23
23
 
24
24
  BOLT_CONFIG_NAME = 'bolt.yaml'
25
25
  BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
@@ -32,16 +32,19 @@ module Bolt
32
32
  end
33
33
 
34
34
  def self.from_project(project, overrides = {})
35
+ logs = []
35
36
  conf = if project.project_file == project.config_file
36
37
  project.data
37
38
  else
38
- Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
39
+ c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
40
+ logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
41
+ c
39
42
  end
40
43
 
41
44
  data = load_defaults(project).push(
42
45
  filepath: project.config_file,
43
46
  data: conf,
44
- warnings: [],
47
+ logs: logs,
45
48
  deprecations: []
46
49
  )
47
50
 
@@ -50,17 +53,20 @@ module Bolt
50
53
 
51
54
  def self.from_file(configfile, overrides = {})
52
55
  project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
+ logs = []
53
57
 
54
58
  conf = if project.project_file == project.config_file
55
59
  project.data
56
60
  else
57
- Bolt::Util.read_yaml_hash(configfile, 'config')
61
+ c = Bolt::Util.read_yaml_hash(configfile, 'config')
62
+ logs << { debug: "Loaded configuration from #{configfile}" }
63
+ c
58
64
  end
59
65
 
60
66
  data = load_defaults(project).push(
61
- filepath: project.config_file,
67
+ filepath: configfile,
62
68
  data: conf,
63
- warnings: [],
69
+ logs: logs,
64
70
  deprecations: []
65
71
  )
66
72
 
@@ -90,13 +96,13 @@ module Bolt
90
96
  def self.load_bolt_defaults_yaml(dir)
91
97
  filepath = dir + BOLT_DEFAULTS_NAME
92
98
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
93
- warnings = []
99
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
94
100
 
95
101
  # Warn if 'bolt.yaml' detected in same directory.
96
102
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
97
- warnings.push(
98
- msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
99
- "will be ignored."
103
+ logs.push(
104
+ warn: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
105
+ "will be ignored."
100
106
  )
101
107
  end
102
108
 
@@ -105,9 +111,9 @@ module Bolt
105
111
 
106
112
  if project_config.any?
107
113
  data.reject! { |key, _| project_config.include?(key) }
108
- warnings.push(
109
- msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
110
- "Project configuration should be set in 'bolt-project.yaml'."
114
+ logs.push(
115
+ warn: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
116
+ "Project configuration should be set in 'bolt-project.yaml'."
111
117
  )
112
118
  end
113
119
 
@@ -116,10 +122,10 @@ module Bolt
116
122
 
117
123
  if transport_config.any?
118
124
  data.reject! { |key, _| transport_config.include?(key) }
119
- warnings.push(
120
- msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
121
- "Transport configuration should be set under the 'inventory-config' option or "\
122
- "in 'inventory.yaml'."
125
+ logs.push(
126
+ warn: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
127
+ "Transport configuration should be set under the 'inventory-config' option or "\
128
+ "in 'inventory.yaml'."
123
129
  )
124
130
  end
125
131
 
@@ -142,7 +148,7 @@ module Bolt
142
148
  data = data.merge(data.delete('inventory-config'))
143
149
  end
144
150
 
145
- { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
151
+ { filepath: filepath, data: data, logs: logs, deprecations: [] }
146
152
  end
147
153
 
148
154
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -150,11 +156,12 @@ module Bolt
150
156
  def self.load_bolt_yaml(dir)
151
157
  filepath = dir + BOLT_CONFIG_NAME
152
158
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
159
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
153
160
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
154
161
  msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
155
162
  "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
156
163
 
157
- { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
164
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
158
165
  end
159
166
 
160
167
  def self.load_defaults(project)
@@ -187,13 +194,13 @@ module Bolt
187
194
  unless config_data.is_a?(Array)
188
195
  config_data = [{ filepath: project.config_file,
189
196
  data: config_data,
190
- warnings: [],
197
+ logs: [],
191
198
  deprecations: [] }]
192
199
  end
193
200
 
194
- @logger = Logging.logger[self]
201
+ @logger = Bolt::Logger.logger(self)
195
202
  @project = project
196
- @warnings = @project.warnings.dup
203
+ @logs = @project.logs.dup
197
204
  @deprecations = @project.deprecations.dup
198
205
  @transports = {}
199
206
  @config_files = []
@@ -221,7 +228,7 @@ module Bolt
221
228
  end
222
229
 
223
230
  loaded_data = config_data.each_with_object([]) do |data, acc|
224
- @warnings.concat(data[:warnings]) if data[:warnings].any?
231
+ @logs.concat(data[:logs]) if data[:logs].any?
225
232
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
226
233
 
227
234
  if data[:data].any?
@@ -337,10 +344,26 @@ module Bolt
337
344
  end
338
345
 
339
346
  private def update_logs(logs)
347
+ begin
348
+ if logs['bolt-debug.log'] && logs['bolt-debug.log'] != 'disable'
349
+ FileUtils.touch(File.expand_path('bolt-debug.log', @project.path))
350
+ end
351
+ rescue StandardError
352
+ logs.delete('bolt-debug.log')
353
+ end
354
+
340
355
  logs.each_with_object({}) do |(key, val), acc|
341
- next unless val.is_a?(Hash)
356
+ # Remove any disabled logs
357
+ next if val == 'disable'
342
358
 
343
359
  name = normalize_log(key)
360
+
361
+ # But otherwise it has to be a Hash
362
+ unless val.is_a?(Hash)
363
+ raise Bolt::ValidationError,
364
+ "config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
365
+ end
366
+
344
367
  acc[name] = val.slice('append', 'level')
345
368
  .transform_keys(&:to_sym)
346
369
 
@@ -365,7 +388,7 @@ module Bolt
365
388
  def validate
366
389
  if @data['future']
367
390
  msg = "Configuration option 'future' no longer exposes future behavior."
368
- @warnings << { option: 'future', msg: msg }
391
+ @logs << { warn: msg }
369
392
  end
370
393
 
371
394
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
@@ -176,7 +176,9 @@ module Bolt
176
176
  description: "A map of configuration for the logfile output. Under `log`, you can configure log options "\
177
177
  "for `console` and add configuration for individual log files, such as "\
178
178
  "`~/.puppetlabs/bolt/debug.log`. Individual log files must be valid filepaths. If the log "\
179
- "file does not exist, then Bolt will create it before logging information.",
179
+ "file does not exist, then Bolt will create it before logging information. Set the value to "\
180
+ "`disable` to remove a log file defined at an earlier level of the config hierarchy. By "\
181
+ "default, Bolt logs to a bolt-debug.log file in the Bolt project directory.",
180
182
  type: Hash,
181
183
  properties: {
182
184
  "console" => {
@@ -194,7 +196,8 @@ module Bolt
194
196
  },
195
197
  additionalProperties: {
196
198
  description: "Configuration for the logfile output.",
197
- type: Hash,
199
+ type: [String, Hash],
200
+ enum: ['disable'],
198
201
  properties: {
199
202
  "append" => {
200
203
  description: "Whether to append output to an existing log file.",
@@ -224,6 +227,34 @@ module Bolt
224
227
  _example: ["~/.puppetlabs/bolt/modules", "~/.puppetlabs/bolt/site-modules"],
225
228
  _default: ["project/modules", "project/site-modules", "project/site"]
226
229
  },
230
+ "modules" => {
231
+ description: "A list of module dependencies for the project. Each dependency is a map of data specifying "\
232
+ "the module to install. To install the project's module dependencies, run the `bolt module "\
233
+ "install` command.",
234
+ type: Array,
235
+ items: {
236
+ type: Hash,
237
+ required: ["name"],
238
+ properties: {
239
+ "name" => {
240
+ description: "The name of the module.",
241
+ type: String
242
+ },
243
+ "version_requirement" => {
244
+ description: "The version requirement for the module. Accepts a specific version (1.2.3), version "\
245
+ "shorthand (1.2.x), or a version range (>= 1.2.0).",
246
+ type: String
247
+ }
248
+ }
249
+ },
250
+ _plugin: false,
251
+ _example: [
252
+ { "name" => "puppetlabs-mysql" },
253
+ { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
254
+ { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
255
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" }
256
+ ]
257
+ },
227
258
  "name" => {
228
259
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
229
260
  "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
@@ -473,6 +504,7 @@ module Bolt
473
504
  inventoryfile
474
505
  log
475
506
  modulepath
507
+ modules
476
508
  name
477
509
  plans
478
510
  plugin_hooks
@@ -40,7 +40,7 @@ module Bolt
40
40
  require 'concurrent'
41
41
 
42
42
  @analytics = analytics
43
- @logger = Logging.logger[self]
43
+ @logger = Bolt::Logger.logger(self)
44
44
 
45
45
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
46
46
  coll[key.to_s] = if key == :remote
@@ -46,10 +46,13 @@ module Bolt
46
46
  end
47
47
 
48
48
  def self.from_config(config, plugins)
49
+ logger = Bolt::Logger.logger(self)
50
+
49
51
  if ENV.include?(ENVIRONMENT_VAR)
50
52
  begin
51
53
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
52
54
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
+ logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
53
56
  rescue Psych::Exception
54
57
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
55
58
  end
@@ -57,8 +60,12 @@ module Bolt
57
60
  data = if config.inventoryfile
58
61
  Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
59
62
  else
60
- Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
63
+ i = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
+ logger.debug("Loaded inventory from #{config.default_inventoryfile}") if i
65
+ i
61
66
  end
67
+ # This avoids rubocop complaining about identical conditionals
68
+ logger.debug("Loaded inventory from #{config.inventoryfile}") if config.inventoryfile
62
69
  end
63
70
 
64
71
  # Resolve plugin references from transport config
@@ -19,7 +19,7 @@ module Bolt
19
19
  CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
20
20
 
21
21
  def initialize(input, plugins)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  @plugins = plugins
24
24
 
25
25
  input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
@@ -16,7 +16,7 @@ module Bolt
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
18
  def initialize(data, transport, transports, plugins)
19
- @logger = Logging.logger[self]
19
+ @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
22
22
  @config = transports
@@ -13,7 +13,7 @@ module Bolt
13
13
  raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
14
14
  end
15
15
 
16
- @logger = Logging.logger[inventory]
16
+ @logger = Bolt::Logger.logger(inventory)
17
17
 
18
18
  # If the target isn't mentioned by any groups, it won't have a uri or
19
19
  # name and we will use the target_name as both
@@ -4,6 +4,10 @@ require 'logging'
4
4
 
5
5
  module Bolt
6
6
  module Logger
7
+ LEVELS = %w[trace debug info notice warn error fatal].freeze
8
+ @mutex = Mutex.new
9
+ @warnings = Set.new
10
+
7
11
  # This method provides a single point-of-entry to setup logging for both
8
12
  # the CLI and for tests. This is necessary because we define custom log
9
13
  # levels which create corresponding methods on the logger instances;
@@ -11,24 +15,29 @@ module Bolt
11
15
  # will fail.
12
16
  def self.initialize_logging
13
17
  # Initialization isn't idempotent and will result in warnings about const
14
- # redefs, so skip it if it's already been initialized
15
- return if Logging.initialized?
16
-
17
- Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
18
- @mutex = Mutex.new
19
-
20
- Logging.color_scheme(
21
- 'bolt',
22
- lines: {
23
- warn: :yellow,
24
- error: :red,
25
- fatal: %i[white on_red]
26
- }
27
- )
18
+ # redefs, so skip it if the log levels we expect are present. If it's
19
+ # already been initialized with an insufficient set of levels, go ahead
20
+ # and call init anyway or we'll have failures when calling log methods
21
+ # for missing levels.
22
+ unless levels & LEVELS == LEVELS
23
+ Logging.init(*LEVELS)
24
+ end
25
+
26
+ # As above, only create the color scheme if we haven't already created it.
27
+ unless Logging.color_scheme('bolt')
28
+ Logging.color_scheme(
29
+ 'bolt',
30
+ lines: {
31
+ warn: :yellow,
32
+ error: :red,
33
+ fatal: %i[white on_red]
34
+ }
35
+ )
36
+ end
28
37
  end
29
38
 
30
39
  def self.configure(destinations, color)
31
- root_logger = Logging.logger[:root]
40
+ root_logger = Bolt::Logger.logger(:root)
32
41
 
33
42
  root_logger.add_appenders Logging.appenders.stderr(
34
43
  'console',
@@ -66,6 +75,13 @@ module Bolt
66
75
  end
67
76
  end
68
77
 
78
+ # A helper to ensure the Logging library is always initialized with our
79
+ # custom log levels before retrieving a Logger instance.
80
+ def self.logger(name)
81
+ initialize_logging
82
+ Logging.logger[name]
83
+ end
84
+
69
85
  def self.analytics=(analytics)
70
86
  @analytics = analytics
71
87
  end
@@ -108,14 +124,12 @@ module Bolt
108
124
  end
109
125
 
110
126
  def self.warn_once(type, msg)
111
- @mutex.synchronize {
112
- @warnings ||= []
113
- @logger ||= Logging.logger[self]
114
- unless @warnings.include?(type)
127
+ @mutex.synchronize do
128
+ @logger ||= Bolt::Logger.logger(self)
129
+ if @warnings.add?(type)
115
130
  @logger.warn(msg)
116
- @warnings << type
117
131
  end
118
- }
132
+ end
119
133
  end
120
134
 
121
135
  def self.deprecation_warning(type, msg)
@@ -7,7 +7,7 @@ module Bolt
7
7
  class Logger < Bolt::Outputter
8
8
  def initialize(verbose, trace)
9
9
  super(false, verbose, trace)
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
13
13
  def handle_event(event)
@@ -65,7 +65,7 @@ module Bolt
65
65
  @resource_types = resource_types
66
66
  @project = project
67
67
 
68
- @logger = Logging.logger[self]
68
+ @logger = Bolt::Logger.logger(self)
69
69
  if modulepath && !modulepath.empty?
70
70
  @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
71
71
  end
@@ -76,7 +76,7 @@ module Bolt
76
76
  # Puppet logging is global so this is class method to avoid confusion
77
77
  def self.configure_logging
78
78
  Puppet::Util::Log.destinations.clear
79
- Puppet::Util::Log.newdestination(Logging.logger['Puppet'])
79
+ Puppet::Util::Log.newdestination(Bolt::Logger.logger('Puppet'))
80
80
  # Defer all log level decisions to the Logging library by telling Puppet
81
81
  # to log everything
82
82
  Puppet.settings[:log_level] = 'debug'
@@ -141,6 +141,19 @@ module Bolt
141
141
  end
142
142
  end
143
143
 
144
+ def detect_project_conflict(project, environment)
145
+ return unless project && project.load_as_module?
146
+ # The environment modulepath has stripped out non-existent directories,
147
+ # so we don't need to check for them
148
+ modules = environment.modulepath.flat_map do |path|
149
+ Dir.children(path).select { |name| Puppet::Module.is_module_directory?(name, path) }
150
+ end
151
+ if modules.include?(project.name)
152
+ Bolt::Logger.warn_once("project shadows module",
153
+ "The project '#{project.name}' shadows an existing module of the same name")
154
+ end
155
+ end
156
+
144
157
  # Runs a block in a PAL script compiler configured for Bolt. Catches
145
158
  # exceptions thrown by the block and re-raises them ensuring they are
146
159
  # Bolt::Errors since the script compiler block will squash all exceptions.
@@ -149,15 +162,13 @@ module Bolt
149
162
  setup
150
163
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
151
164
  # Only load the project if it a) exists, b) has a name it can be loaded with
152
- bolt_project = @project if @project&.name
153
- # Puppet currently won't receive the project unless it is a named project. Since
154
- # the download_file plan function needs access to the project path, add it to the
155
- # context.
156
- bolt_project_data = @project
157
- Puppet.override(bolt_project: bolt_project,
158
- bolt_project_data: bolt_project_data,
165
+ Puppet.override(bolt_project: @project,
159
166
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
160
- pal.with_script_compiler do |compiler|
167
+ # Because this has the side effect of loading and caching the list
168
+ # of modules, it must happen *after* we have overridden
169
+ # bolt_project or the project will be ignored
170
+ detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
171
+ pal.with_script_compiler(set_local_facts: false) do |compiler|
161
172
  alias_types(compiler)
162
173
  register_resource_types(Puppet.lookup(:loaders)) if @resource_types
163
174
  begin