bolt 2.36.0 → 2.37.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1881e4097a89e606b9326f93e2e6fd9d934a7db1eb8609e6f9a09ffa322ac7c4
4
- data.tar.gz: '02509b1919e57f137bf4e868332be3b73238d22d83251680dc29c7c93ef90a19'
3
+ metadata.gz: 2cbd90e42181c76bad4eee913ee67369202296852cfa5153a1f5490def4e77d3
4
+ data.tar.gz: 87ac43d10edeebefaa31f581f49cbcf9f729e25f5d7efec583e09dcf04f97be6
5
5
  SHA512:
6
- metadata.gz: 1fc1215430ac99b765c24c87ee210afb03a247a6c0baac9af3b00e34d56cfb58bacb2140dcfafbe197fd655602713463e2697c2df5083c259b66c8d8cd7bc112
7
- data.tar.gz: 445a5f77a5beaf8a1a3c4cb2193e973ada23cb4a7a5f3a8caba2ccc47be4fc8a8fdef3ba797e082c0df3b9553c2f2e67c06084f2c2b2684347d2b9a86e45bde0
6
+ metadata.gz: 392f0d453c49a536852692e873e5c5f55807294fa0bd914593722c26d39459e494b83453a3b995319b04b8f2e6f3a1f04f7661400841192bdffc56df47e4170e
7
+ data.tar.gz: 750cd8e9c29008e8bb4dfc7350d52a268c01d91fa088e9007ff241648e31a90e76d9cabfef9f1db5675d33f6b82b2898a54f758bc21c17c18d888919d0441807
@@ -14,7 +14,7 @@ module Bolt
14
14
  global_config_setters: PROJECT_PATHS + %w[modulepath],
15
15
  transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command],
16
16
  display: %w[format color verbose trace],
17
- global: %w[help version debug log-level] }.freeze
17
+ global: %w[help version debug log-level clear-cache] }.freeze
18
18
 
19
19
  ACTION_OPTS = OPTIONS.values.flatten.freeze
20
20
 
@@ -969,8 +969,8 @@ module Bolt
969
969
  end
970
970
 
971
971
  separator "\nPLAN OPTIONS"
972
- define('--pp', 'Create a new Puppet language plan.') do |pp|
973
- @options[:puppet] = pp
972
+ define('--pp', 'Create a new Puppet language plan.') do |_|
973
+ @options[:puppet] = true
974
974
  end
975
975
 
976
976
  separator "\nDISPLAY OPTIONS"
@@ -1025,6 +1025,10 @@ module Bolt
1025
1025
  "trace, debug, info, warn, error, fatal, any.") do |level|
1026
1026
  @options[:log] = { 'console' => { 'level' => level } }
1027
1027
  end
1028
+ define('--clear-cache',
1029
+ "Clear plugin cache before executing") do |_|
1030
+ @options[:clear_cache] = true
1031
+ end
1028
1032
  define('--plugin PLUGIN', 'Select the plugin to use') do |plug|
1029
1033
  @options[:plugin] = plug
1030
1034
  end
@@ -197,7 +197,12 @@ module Bolt
197
197
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
198
198
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
199
199
 
200
+ if options[:clear_cache] && File.exist?(config.project.cache_file)
201
+ FileUtils.rm(config.project.cache_file)
202
+ end
203
+
200
204
  warn_inventory_overrides_cli(options)
205
+ validate_ps_version
201
206
 
202
207
  options
203
208
  rescue Bolt::Error => e
@@ -205,6 +210,17 @@ module Bolt
205
210
  raise e
206
211
  end
207
212
 
213
+ private def validate_ps_version
214
+ if Bolt::Util.powershell?
215
+ target = inventory.get_target('localhost')
216
+ Bolt::Transport::Local.new.with_connection(target) do |conn|
217
+ # This will automatically validate the powershell version on the Bolt
218
+ # controller
219
+ Bolt::Shell::Powershell.new(target, conn)
220
+ end
221
+ end
222
+ end
223
+
208
224
  def update_targets(options)
209
225
  target_opts = options.keys.select { |opt| %i[query rerun targets].include?(opt) }
210
226
  target_string = "'--targets', '--rerun', or '--query'"
@@ -216,8 +216,8 @@ module Bolt
216
216
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
217
217
  logs = [{ debug: "Loaded configuration from #{filepath}" }]
218
218
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
219
- msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
220
- "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
219
+ msg: "Configuration file #{filepath} is deprecated and will be removed in Bolt 3.0. "\
220
+ "See https://pup.pt/update-bolt-config for how to update to the latest Bolt practices." }]
221
221
 
222
222
  # Validate the config against the schema. This will raise a single error
223
223
  # with all validation errors.
@@ -448,10 +448,18 @@ module Bolt
448
448
  raise Bolt::ValidationError,
449
449
  "level of log #{name} must be a String or Symbol, received #{v.class} #{v.inspect}"
450
450
  end
451
+
451
452
  unless Bolt::Logger.valid_level?(v)
452
453
  raise Bolt::ValidationError,
453
454
  "level of log #{name} must be one of #{Bolt::Logger.levels.join(', ')}; received #{v}"
454
455
  end
456
+
457
+ if v == 'notice'
458
+ @deprecations << {
459
+ type: 'notice log level',
460
+ msg: "Log level 'notice' is deprecated and will be removed in Bolt 3.0. Use 'info' instead."
461
+ }
462
+ end
455
463
  end
456
464
 
457
465
  if (v = acc[name][:append]) && v != true && v != false
@@ -521,6 +529,10 @@ module Bolt
521
529
  @data['modulepath'] = value
522
530
  end
523
531
 
532
+ def plugin_cache
533
+ @project.plugin_cache || @data['plugin-cache'] || {}
534
+ end
535
+
524
536
  def concurrency
525
537
  @data['concurrency']
526
538
  end
@@ -33,6 +33,18 @@ module Bolt
33
33
  "_plugin" => {
34
34
  description: "The name of the plugin.",
35
35
  type: "string"
36
+ },
37
+ "_cache" => {
38
+ description: "This feature is experimental. Enable plugin caching and set a time-to-live.",
39
+ type: "object",
40
+ required: ["ttl"],
41
+ properties: {
42
+ "ttl" => {
43
+ description: "Time in seconds to keep the plugin cache.",
44
+ type: "integer",
45
+ minimum: 0
46
+ }
47
+ }
36
48
  }
37
49
  }
38
50
  }
@@ -190,6 +202,20 @@ module Bolt
190
202
  _example: "~/.puppetlabs/bolt/inventory.yaml",
191
203
  _default: "project/inventory.yaml"
192
204
  },
205
+ "plugin-cache" => {
206
+ description: "This feature is experimental. Enable plugin caching and set the time-to-live.",
207
+ type: Hash,
208
+ required: ["ttl"],
209
+ properties: {
210
+ "ttl" => {
211
+ description: "Time in seconds to keep the plugin cache.",
212
+ type: Integer,
213
+ minimum: 0
214
+ }
215
+ },
216
+ _plugin: false,
217
+ _example: { "ttl" => 3600 }
218
+ },
193
219
  "log" => {
194
220
  description: "A map of configuration for the logfile output. Under `log`, you can configure log options "\
195
221
  "for `console` and add configuration for individual log files, such as "\
@@ -207,7 +233,7 @@ module Bolt
207
233
  "level" => {
208
234
  description: "The type of information to log.",
209
235
  type: String,
210
- enum: %w[trace debug error info warn fatal any],
236
+ enum: %w[trace debug error info notice warn fatal any],
211
237
  _default: "warn"
212
238
  }
213
239
  }
@@ -226,7 +252,7 @@ module Bolt
226
252
  "level" => {
227
253
  description: "The type of information to log.",
228
254
  type: String,
229
- enum: %w[trace debug error info warn fatal any],
255
+ enum: %w[trace debug error info notice warn fatal any],
230
256
  _default: "warn"
231
257
  }
232
258
  }
@@ -540,6 +566,7 @@ module Bolt
540
566
  format
541
567
  inventory-config
542
568
  log
569
+ plugin-cache
543
570
  plugin-hooks
544
571
  plugin_hooks
545
572
  plugins
@@ -563,6 +590,7 @@ module Bolt
563
590
  modules
564
591
  name
565
592
  plans
593
+ plugin-cache
566
594
  plugin-hooks
567
595
  plugin_hooks
568
596
  plugins
@@ -4,6 +4,7 @@ require 'bolt/inventory'
4
4
  require 'bolt/executor'
5
5
  require 'bolt/module'
6
6
  require 'bolt/pal'
7
+ require 'bolt/plugin/cache'
7
8
  require 'bolt/plugin/puppetdb'
8
9
 
9
10
  module Bolt
@@ -36,6 +37,13 @@ module Bolt
36
37
  super("Plugin #{plugin_name} does not support #{hook}", 'bolt/unsupported-hook')
37
38
  end
38
39
  end
40
+
41
+ class LoadingDisabled < PluginError
42
+ def initialize(plugin_name)
43
+ msg = "Cannot load plugin #{plugin_name}: plugin loading is disabled"
44
+ super(msg, 'bolt/plugin-loading-disabled', { 'plugin_name' => plugin_name })
45
+ end
46
+ end
39
47
  end
40
48
 
41
49
  class PluginContext
@@ -119,8 +127,8 @@ module Bolt
119
127
  end
120
128
  end
121
129
 
122
- def self.setup(config, pal, analytics = Bolt::Analytics::NoopClient.new)
123
- plugins = new(config, pal, analytics)
130
+ def self.setup(config, pal, analytics = Bolt::Analytics::NoopClient.new, **opts)
131
+ plugins = new(config, pal, analytics, **opts)
124
132
 
125
133
  config.plugins.each_key do |plugin|
126
134
  plugins.by_name(plugin)
@@ -141,12 +149,13 @@ module Bolt
141
149
 
142
150
  private_class_method :new
143
151
 
144
- def initialize(config, pal, analytics)
152
+ def initialize(config, pal, analytics, load_plugins: true)
145
153
  @config = config
146
154
  @analytics = analytics
147
155
  @plugin_context = PluginContext.new(config, pal, self)
148
156
  @plugins = {}
149
157
  @pal = pal
158
+ @load_plugins = load_plugins
150
159
  @unknown = Set.new
151
160
  @resolution_stack = []
152
161
  @unresolved_plugin_configs = config.plugins.dup
@@ -169,6 +178,8 @@ module Bolt
169
178
  end
170
179
 
171
180
  def add_ruby_plugin(plugin_name)
181
+ raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
182
+
172
183
  cls_name = Bolt::Util.snake_name_to_class_name(plugin_name)
173
184
  filename = "bolt/plugin/#{plugin_name}"
174
185
  require filename
@@ -185,10 +196,17 @@ module Bolt
185
196
  def add_module_plugin(plugin_name)
186
197
  opts = {
187
198
  context: @plugin_context,
199
+ # Make sure that the plugin's config is validated _before_ the unknown-plugin
200
+ # and loading-disabled checks. This way, we can fail early on invalid plugin
201
+ # config instead of _after_ loading the modulepath (which can be expensive).
188
202
  config: config_for_plugin(plugin_name)
189
203
  }
190
204
 
191
- plugin = Bolt::Plugin::Module.load(plugin_name, modules, opts)
205
+ mod = modules[plugin_name]
206
+ raise PluginError::Unknown, plugin_name unless mod&.plugin?
207
+ raise PluginError::LoadingDisabled, plugin_name unless @load_plugins
208
+
209
+ plugin = Bolt::Plugin::Module.load(mod, opts)
192
210
  add_plugin(plugin)
193
211
  end
194
212
 
@@ -277,6 +295,16 @@ module Bolt
277
295
  # Evaluates a single reference. The value returned may be another
278
296
  # reference.
279
297
  def resolve_single_reference(reference)
298
+ plugin_cache = if cache?(reference)
299
+ cache = Bolt::Plugin::Cache.new(reference,
300
+ @config.project.cache_file,
301
+ @config.plugin_cache)
302
+ entry = cache.read_and_clean_cache
303
+ return entry unless entry.nil?
304
+
305
+ cache
306
+ end
307
+
280
308
  plugin_name = reference['_plugin']
281
309
  hook = get_hook(plugin_name, :resolve_reference)
282
310
 
@@ -288,16 +316,24 @@ module Bolt
288
316
 
289
317
  validate_proc.call(reference)
290
318
 
291
- begin
319
+ result = begin
292
320
  # Evaluate the plugin and then recursively evaluate any plugin returned by it.
293
321
  hook.call(reference)
294
322
  rescue StandardError => e
295
323
  loc = "resolve_reference in #{plugin_name}"
296
324
  raise PluginError::ExecutionError.new(e.message, plugin_name, loc)
297
325
  end
326
+
327
+ plugin_cache.write_cache(result) if cache?(reference)
328
+
329
+ result
298
330
  end
299
331
  private :resolve_single_reference
300
332
 
333
+ private def cache?(reference)
334
+ reference.key?('_cache') || @config.plugin_cache.key?('ttl')
335
+ end
336
+
301
337
  # Checks whether a given value is a _plugin reference
302
338
  def reference?(input)
303
339
  input.is_a?(Hash) && input.key?('_plugin')
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'bolt/error'
5
+ require 'bolt/util'
6
+
7
+ module Bolt
8
+ class Plugin
9
+ class Cache
10
+ attr_reader :reference, :cache_file, :default_config, :id
11
+
12
+ def initialize(reference, cache_file, default_config)
13
+ @reference = reference
14
+ @cache_file = cache_file
15
+ @default_config = default_config
16
+ end
17
+
18
+ def read_and_clean_cache
19
+ return if ttl == 0
20
+ validate
21
+
22
+ # Luckily we don't need to use a serious hash algorithm
23
+ require 'digest/bubblebabble'
24
+ r = reference.select { |k, _| k == '_cache' }.sort.to_s
25
+ @id = Digest::SHA2.bubblebabble(r)[0..20]
26
+
27
+ unmodified = true
28
+ # First remove any cache entries past their ttl
29
+ # This prevents removing plugins from leaving orphaned cache entries
30
+ cache.delete_if do |_, entry|
31
+ expired = Time.now - Time.parse(entry['mtime']) >= entry['ttl']
32
+ unmodified = false if expired
33
+ expired
34
+ end
35
+ File.write(cache_file, cache.to_json) unless cache.empty? || unmodified
36
+
37
+ cache.dig(id, 'result')
38
+ end
39
+
40
+ private def cache
41
+ @cache ||= Bolt::Util.read_optional_json_file(@cache_file, 'cache')
42
+ end
43
+
44
+ def write_cache(result)
45
+ cache.merge!({ id => { 'result' => result,
46
+ 'mtime' => Time.now,
47
+ 'ttl' => ttl } })
48
+ FileUtils.touch(cache_file)
49
+ File.write(cache_file, cache.to_json)
50
+ end
51
+
52
+ def validate
53
+ # The default cache `plugin-cache` will be validated by the config
54
+ # validator
55
+ return if reference['_cache'].nil?
56
+ r = reference['_cache']
57
+ unless r.is_a?(Hash)
58
+ raise Bolt::ValidationError,
59
+ "_cache must be a Hash, received #{r.class}: #{r.inspect}"
60
+ end
61
+
62
+ unless r.key?('ttl')
63
+ raise Bolt::ValidationError, "_cache must set 'ttl' key."
64
+ end
65
+
66
+ unless r['ttl'] >= 0
67
+ raise Bolt::ValidationError, "'ttl' key under '_cache' must be a minimum of 0."
68
+ end
69
+ end
70
+
71
+ private def ttl
72
+ @ttl ||= reference.dig('_cache', 'ttl') || default_config['ttl']
73
+ end
74
+ end
75
+ end
76
+ end
@@ -12,15 +12,15 @@ module Bolt
12
12
  end
13
13
  end
14
14
 
15
- def self.load(name, modules, opts)
16
- mod = modules[name]
17
- if mod&.plugin?
15
+ # mod should not be nil
16
+ def self.load(mod, opts)
17
+ if mod.plugin?
18
18
  opts[:mod] = mod
19
19
  plugin = Bolt::Plugin::Module.new(**opts)
20
20
  plugin.setup
21
21
  plugin
22
22
  else
23
- raise PluginError::Unknown, name
23
+ raise PluginError::Unknown, mod.name
24
24
  end
25
25
  end
26
26
 
@@ -14,7 +14,7 @@ module Bolt
14
14
  attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
15
15
  :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
16
16
  :deprecations, :downloads, :plans_path, :modulepath, :managed_moduledir,
17
- :backup_dir
17
+ :backup_dir, :cache_file
18
18
 
19
19
  def self.default_project(logs = [])
20
20
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
@@ -27,23 +27,31 @@ module Bolt
27
27
  # directory called Boltdir or a file called bolt.yaml (for a control repo
28
28
  # type Project). Otherwise, repeat the check on each directory up the
29
29
  # hierarchy, falling back to the default if we reach the root.
30
- def self.find_boltdir(dir, logs = [])
30
+ def self.find_boltdir(dir, logs = [], deprecations = [])
31
31
  dir = Pathname.new(dir)
32
32
 
33
33
  if (dir + BOLTDIR_NAME).directory?
34
34
  create_project(dir + BOLTDIR_NAME, 'embedded', logs)
35
- elsif (dir + 'bolt.yaml').file? || (dir + CONFIG_NAME).file?
35
+ elsif (dir + 'bolt.yaml').file?
36
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
37
+ msg = "Configuration file #{dir + 'bolt.yaml'} is deprecated and will be "\
38
+ "removed in Bolt 3.0.\nUpdate your Bolt project to the latest Bolt practices "\
39
+ "using #{command}"
40
+ deprecations << { type: "Project level bolt.yaml",
41
+ msg: msg }
42
+ create_project(dir, 'local', logs, deprecations)
43
+ elsif (dir + CONFIG_NAME).file?
36
44
  create_project(dir, 'local', logs)
37
45
  elsif dir.root?
38
46
  default_project(logs)
39
47
  else
40
48
  logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
41
49
  "This directory won't be loaded as a project." }
42
- find_boltdir(dir.parent, logs)
50
+ find_boltdir(dir.parent, logs, deprecations)
43
51
  end
44
52
  end
45
53
 
46
- def self.create_project(path, type = 'option', logs = [])
54
+ def self.create_project(path, type = 'option', logs = [], deprecations = [])
47
55
  fullpath = Pathname.new(path).expand_path
48
56
 
49
57
  if type == 'user'
@@ -72,7 +80,6 @@ module Bolt
72
80
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
73
81
  default = type =~ /user|system/ ? 'default ' : ''
74
82
  exist = File.exist?(File.expand_path(project_file))
75
- deprecations = []
76
83
 
77
84
  logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
78
85
 
@@ -116,6 +123,7 @@ module Bolt
116
123
  @plans_path = @path + 'plans'
117
124
  @managed_moduledir = @path + '.modules'
118
125
  @backup_dir = @path + '.bolt-bak'
126
+ @cache_file = @path + '.plugin_cache.json'
119
127
 
120
128
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
121
129
  if tc.any?
@@ -184,6 +192,10 @@ module Bolt
184
192
  @data['plans']
185
193
  end
186
194
 
195
+ def plugin_cache
196
+ @data['plugin-cache']
197
+ end
198
+
187
199
  def modules
188
200
  @modules ||= @data['modules']&.map do |mod|
189
201
  if mod.is_a?(String)
@@ -12,15 +12,11 @@ module Bolt
12
12
  end
13
13
 
14
14
  def data
15
- @data ||= JSON.parse(File.read(@path))
15
+ @data ||= Bolt::Util.read_json_file(@path, 'rerun')
16
16
  unless @data.is_a?(Array) && @data.all? { |r| r['target'] && r['status'] }
17
17
  raise Bolt::FileError.new("Missing data in rerun file: #{@path}", @path)
18
18
  end
19
19
  @data
20
- rescue JSON::ParserError
21
- raise Bolt::FileError.new("Could not parse rerun file: #{@path}", @path)
22
- rescue IOError, SystemCallError
23
- raise Bolt::FileError.new("Could not read rerun file: #{@path}", @path)
24
20
  end
25
21
 
26
22
  def get_targets(filter)
@@ -436,8 +436,14 @@ module Bolt
436
436
  result_output.stderr << read_streams[err]
437
437
  result_output.exit_code = t.value.respond_to?(:exitstatus) ? t.value.exitstatus : t.value
438
438
 
439
- if result_output.exit_code == 0
439
+ case result_output.exit_code
440
+ when 0
440
441
  @logger.trace { "Command `#{command_str}` returned successfully" }
442
+ when 126
443
+ msg = "\n\nThis may be caused by the default tmpdir being mounted "\
444
+ "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
445
+ result_output.stderr << msg
446
+ @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
441
447
  else
442
448
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
443
449
  end
@@ -14,6 +14,22 @@ module Bolt
14
14
  extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] == '.' ? ext : '.' + ext }
15
15
  extensions += target.options['interpreters'].keys if target.options['interpreters']
16
16
  @extensions = DEFAULT_EXTENSIONS + extensions
17
+ validate_ps_version
18
+ end
19
+
20
+ def validate_ps_version
21
+ version = execute("$PSVersionTable.PSVersion.Major").stdout.string.chomp
22
+ if !version.empty? && version.to_i < 3
23
+ # This lets us know how many targets have Powershell 2, and lets the
24
+ # user know how many targets they have with PS2
25
+ msg = "Detected PowerShell 2 on one or more targets.\nPowerShell 2 "\
26
+ "is deprecated, and support will be removed in Bolt 3.0. See "\
27
+ "bolt-debug.log or run with '--log-level debug' to see the full "\
28
+ "list of targets with PowerShell 2."
29
+
30
+ Bolt::Logger.deprecation_warning("PowerShell 2", msg)
31
+ @logger.debug("Detected PowerShell 2 on #{target}.")
32
+ end
17
33
  end
18
34
 
19
35
  def provided_features
@@ -22,6 +22,28 @@ module Bolt
22
22
  raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
23
23
  end
24
24
 
25
+ def read_json_file(path, filename)
26
+ require 'json'
27
+
28
+ logger = Bolt::Logger.logger(self)
29
+ path = File.expand_path(path)
30
+ content = JSON.parse(File.read(path))
31
+ logger.trace("Loaded #{filename} from #{path}")
32
+ content
33
+ rescue Errno::ENOENT
34
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}", path)
35
+ rescue JSON::ParserError => e
36
+ msg = "Unable to parse #{filename} file at #{path} as JSON: #{e.message}"
37
+ raise Bolt::FileError.new(msg, path)
38
+ rescue IOError, SystemCallError => e
39
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}\n#{e.message}",
40
+ path)
41
+ end
42
+
43
+ def read_optional_json_file(path, file_name)
44
+ File.exist?(path) && !File.zero?(path) ? read_yaml_hash(path, file_name) : {}
45
+ end
46
+
25
47
  def read_yaml_hash(path, file_name)
26
48
  require 'yaml'
27
49
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.36.0'
4
+ VERSION = '2.37.0'
5
5
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ module BoltServer
6
+ class Plugin
7
+ class PluginNotSupported < Bolt::Error
8
+ def initialize(msg, plugin_name)
9
+ super(msg, 'bolt/plugin-not-supported', { "plugin_name" => plugin_name })
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltServer
4
+ class Plugin
5
+ class PuppetConnectData
6
+ def initialize(data, **_opts)
7
+ @data = data
8
+ end
9
+
10
+ def name
11
+ 'puppet_connect_data'
12
+ end
13
+
14
+ def hooks
15
+ %i[resolve_reference validate_resolve_reference]
16
+ end
17
+
18
+ def resolve_reference(opts)
19
+ key = opts['key']
20
+
21
+ @data.dig(key, 'value')
22
+ end
23
+
24
+ def validate_resolve_reference(opts)
25
+ unless opts['key']
26
+ raise Bolt::ValidationError,
27
+ "puppet_connect_data plugin requires that 'key' be specified"
28
+ end
29
+
30
+ unless @data.key?(opts['key'])
31
+ raise Bolt::ValidationError,
32
+ "puppet_connect_data plugin tried to lookup key '#{opts['key']}' but no value was found"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "project_inventory_targets connect plugin data",
4
+ "description": "POST project_inventory_targets connect plugin data",
5
+ "type": "object",
6
+ "properties": {
7
+ "puppet_connect_data": {
8
+ "type": "object",
9
+ "patternProperties": {
10
+ ".*": {
11
+ "type": "object",
12
+ "properties": {
13
+ "value": {}
14
+ },
15
+ "required": ["value"]
16
+ }
17
+ },
18
+ "additionalProperties": false
19
+ }
20
+ },
21
+ "required": ["puppet_connect_data"]
22
+ }
@@ -8,6 +8,8 @@ require 'bolt/inventory'
8
8
  require 'bolt/project'
9
9
  require 'bolt/target'
10
10
  require 'bolt_server/file_cache'
11
+ require 'bolt_server/plugin'
12
+ require 'bolt_server/plugin/puppet_connect_data'
11
13
  require 'bolt/task/puppet_server'
12
14
  require 'json'
13
15
  require 'json-schema'
@@ -35,6 +37,7 @@ module BoltServer
35
37
  action-upload_file
36
38
  transport-ssh
37
39
  transport-winrm
40
+ connect-data
38
41
  ].freeze
39
42
 
40
43
  # PE_BOLTLIB_PATH is intended to function exactly like the BOLTLIB_PATH used
@@ -285,8 +288,6 @@ module BoltServer
285
288
  config: bolt_config
286
289
  }
287
290
  yield context
288
- rescue Bolt::Error => e
289
- [400, e.to_json]
290
291
  end
291
292
  end
292
293
 
@@ -521,6 +522,8 @@ module BoltServer
521
522
  plan_info = allowed_helper(plan_info, context[:config].project.plans)
522
523
  [200, plan_info.to_json]
523
524
  end
525
+ rescue Bolt::Error => e
526
+ [400, e.to_json]
524
527
  end
525
528
 
526
529
  # Fetches the metadata for a single task
@@ -549,6 +552,8 @@ module BoltServer
549
552
  task_info = allowed_helper(task_info, context[:config].project.tasks)
550
553
  [200, task_info.to_json]
551
554
  end
555
+ rescue Bolt::Error => e
556
+ [400, e.to_json]
552
557
  end
553
558
 
554
559
  # Fetches the list of plans for an environment, optionally fetching all metadata for each plan
@@ -592,6 +597,8 @@ module BoltServer
592
597
  # to bolt-server smaller/simpler.
593
598
  [200, plans_response.to_json]
594
599
  end
600
+ rescue Bolt::Error => e
601
+ [400, e.to_json]
595
602
  end
596
603
 
597
604
  # Fetches the list of tasks for an environment
@@ -626,6 +633,8 @@ module BoltServer
626
633
  # to bolt-server smaller/simpler.
627
634
  [200, tasks_response.to_json]
628
635
  end
636
+ rescue Bolt::Error => e
637
+ [400, e.to_json]
629
638
  end
630
639
 
631
640
  # Implements puppetserver's file_metadatas endpoint for projects.
@@ -638,6 +647,8 @@ module BoltServer
638
647
  metadatas = file_metadatas(context[:pal], params[:module_name], file)
639
648
  [200, metadatas.to_json]
640
649
  end
650
+ rescue Bolt::Error => e
651
+ [400, e.to_json]
641
652
  rescue ArgumentError => e
642
653
  [400, e.message]
643
654
  end
@@ -647,16 +658,33 @@ module BoltServer
647
658
  # @param project_ref [String] the project_ref to compute the inventory from
648
659
  post '/project_inventory_targets' do
649
660
  return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
650
- bolt_config = config_from_project(params['project_ref'])
651
- if bolt_config.inventoryfile && bolt_config.project.inventory_file.to_s != bolt_config.inventoryfile
652
- raise Bolt::ValidationError, "Project inventory must be defined in the " \
653
- "inventory.yaml file at the root of the project directory"
654
- end
655
- plugins = Bolt::Plugin.setup(bolt_config, nil)
656
- inventory = Bolt::Inventory.from_config(bolt_config, plugins)
657
- target_list = inventory.get_targets('all').map { |targ| targ.to_h.merge({ 'transport' => targ.transport }) }
661
+ content_type :json
662
+ body = JSON.parse(request.body.read)
663
+ error = validate_schema(@schemas["connect-data"], body)
664
+ return [400, error_result(error).to_json] unless error.nil?
665
+ in_bolt_project(params['project_ref']) do |context|
666
+ if context[:config].inventoryfile &&
667
+ context[:config].project.inventory_file.to_s !=
668
+ context[:config].inventoryfile
669
+ raise Bolt::ValidationError, "Project inventory must be defined in the " \
670
+ "inventory.yaml file at the root of the project directory"
671
+ end
672
+
673
+ Bolt::Util.validate_file('inventory file', context[:config].project.inventory_file)
674
+
675
+ begin
676
+ connect_plugin = BoltServer::Plugin::PuppetConnectData.new(body['puppet_connect_data'])
677
+ plugins = Bolt::Plugin.setup(context[:config], context[:pal], load_plugins: false)
678
+ plugins.add_plugin(connect_plugin)
679
+ inventory = Bolt::Inventory.from_config(context[:config], plugins)
680
+ target_list = inventory.get_targets('all').map { |targ| targ.to_h.merge({ 'transport' => targ.transport }) }
681
+ rescue Bolt::Plugin::PluginError::LoadingDisabled => e
682
+ msg = "Cannot load plugin #{e.details['plugin_name']}: plugin not supported"
683
+ raise BoltServer::Plugin::PluginNotSupported.new(msg, e.details['plugin_name'])
684
+ end
658
685
 
659
- [200, target_list.to_json]
686
+ [200, target_list.to_json]
687
+ end
660
688
  rescue Bolt::Error => e
661
689
  [500, e.to_json]
662
690
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.36.0
4
+ version: 2.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-30 00:00:00.000000000 Z
11
+ date: 2020-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -505,6 +505,7 @@ files:
505
505
  - lib/bolt/plan_creator.rb
506
506
  - lib/bolt/plan_result.rb
507
507
  - lib/bolt/plugin.rb
508
+ - lib/bolt/plugin/cache.rb
508
509
  - lib/bolt/plugin/env_var.rb
509
510
  - lib/bolt/plugin/module.rb
510
511
  - lib/bolt/plugin/prompt.rb
@@ -556,11 +557,14 @@ files:
556
557
  - lib/bolt_server/base_config.rb
557
558
  - lib/bolt_server/config.rb
558
559
  - lib/bolt_server/file_cache.rb
560
+ - lib/bolt_server/plugin.rb
561
+ - lib/bolt_server/plugin/puppet_connect_data.rb
559
562
  - lib/bolt_server/schemas/action-check_node_connections.json
560
563
  - lib/bolt_server/schemas/action-run_command.json
561
564
  - lib/bolt_server/schemas/action-run_script.json
562
565
  - lib/bolt_server/schemas/action-run_task.json
563
566
  - lib/bolt_server/schemas/action-upload_file.json
567
+ - lib/bolt_server/schemas/connect-data.json
564
568
  - lib/bolt_server/schemas/partials/target-any.json
565
569
  - lib/bolt_server/schemas/partials/target-ssh.json
566
570
  - lib/bolt_server/schemas/partials/target-winrm.json