bolt 1.30.1 → 1.31.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.

@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Bolt
6
+ class Plugin
7
+ class AwsInventory
8
+ attr_accessor :client
9
+ attr_reader :config
10
+
11
+ def initialize(config:, **_kwargs)
12
+ require 'aws-sdk-ec2'
13
+ @config = config
14
+ @logger = Logging.logger[self]
15
+ end
16
+
17
+ def name
18
+ 'aws_inventory'
19
+ end
20
+
21
+ def hooks
22
+ [:resolve_reference]
23
+ end
24
+
25
+ def config_client(opts)
26
+ return client if client
27
+
28
+ options = {}
29
+
30
+ if opts.key?('region')
31
+ options[:region] = opts['region']
32
+ end
33
+ if opts.key?('profile')
34
+ options[:profile] = opts['profile']
35
+ end
36
+
37
+ if config['credentials']
38
+ creds = File.expand_path(config['credentials'])
39
+ if File.exist?(creds)
40
+ options[:credentials] = ::Aws::SharedCredentials.new(path: creds)
41
+ else
42
+ raise Bolt::ValidationError, "Cannot load credentials file #{config['credentials']}"
43
+ end
44
+ end
45
+
46
+ ::Aws::EC2::Client.new(options)
47
+ end
48
+
49
+ def resolve_reference(opts)
50
+ client = config_client(opts)
51
+ resource = ::Aws::EC2::Resource.new(client: client)
52
+
53
+ # Retrieve a list of EC2 instances and create a list of targets
54
+ # Note: It doesn't seem possible to filter stubbed responses...
55
+ resource.instances(filters: opts['filters']).map do |instance|
56
+ next unless instance.state.name == 'running'
57
+ target = {}
58
+
59
+ if opts.key?('uri')
60
+ uri = lookup(instance, opts['uri'])
61
+ target['uri'] = uri if uri
62
+ end
63
+ if opts.key?('name')
64
+ real_name = lookup(instance, opts['name'])
65
+ target['name'] = real_name if real_name
66
+ end
67
+ if opts.key?('config')
68
+ target['config'] = resolve_config(instance, opts['config'])
69
+ end
70
+
71
+ target if target['uri'] || target['name']
72
+ end.compact
73
+ end
74
+
75
+ # Look for an instance attribute specified in the inventory file
76
+ def lookup(instance, attribute)
77
+ value = instance.data.respond_to?(attribute) ? instance.data[attribute] : nil
78
+ unless value
79
+ warn_missing_attribute(instance, attribute)
80
+ end
81
+ value
82
+ end
83
+
84
+ def warn_missing_attribute(instance, attribute)
85
+ @logger.warn("Could not find attribute #{attribute} of instance #{instance.instance_id}")
86
+ end
87
+
88
+ # Walk the "template" config mapping provided in the plugin config and
89
+ # replace all values with the corresponding value from the resource
90
+ # parameters.
91
+ def resolve_config(name, config_template)
92
+ Bolt::Util.walk_vals(config_template) do |value|
93
+ if value.is_a?(String)
94
+ lookup(name, value)
95
+ else
96
+ value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -4,13 +4,15 @@ module Bolt
4
4
  class Plugin
5
5
  class InstallAgent
6
6
  def hooks
7
- %w[puppet_library]
7
+ %i[puppet_library]
8
8
  end
9
9
 
10
10
  def name
11
11
  'install_agent'
12
12
  end
13
13
 
14
+ def initialize(*args); end
15
+
14
16
  def puppet_library(_opts, target, apply_prep)
15
17
  install_task = apply_prep.get_task("puppet_agent::install")
16
18
  service_task = apply_prep.get_task("service", 'action' => 'stop', 'name' => 'puppet')
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/task/run'
4
+
5
+ module Bolt
6
+ class Plugin
7
+ class Module
8
+ class InvalidPluginData < Bolt::Plugin::PluginError
9
+ def initialize(plugin, msg)
10
+ msg = "Invalid Plugin Data for #{plugin}: #{msg}"
11
+ super(msg, 'bolt/invalid-plugin-data')
12
+ end
13
+ end
14
+
15
+ def self.load(name, modules, opts)
16
+ mod = modules[name]
17
+ if mod&.plugin?
18
+ opts[:mod] = mod
19
+ plugin = Bolt::Plugin::Module.new(opts)
20
+ plugin.setup
21
+ plugin
22
+ else
23
+ raise PluginError::Unknown, name
24
+ end
25
+ end
26
+
27
+ attr_reader :config
28
+
29
+ def initialize(mod:, context:, config:, **_opts)
30
+ @module = mod
31
+ @config = config
32
+ @context = context
33
+ end
34
+
35
+ # This method interacts with the module on disk so it's separate from initialize
36
+ def setup
37
+ @data = load_data
38
+ @config_schema = process_schema(@data['config'] || {})
39
+
40
+ @hook_map = find_hooks(@data['hooks'] || {})
41
+
42
+ validate_config(@config, @config_schema)
43
+ end
44
+
45
+ def name
46
+ @module.name
47
+ end
48
+
49
+ def hooks
50
+ (@hook_map.keys + [:validate_resolve_reference]).uniq
51
+ end
52
+
53
+ def config?
54
+ @data.include?('config') && !@data['config'].empty?
55
+ end
56
+
57
+ def load_data
58
+ JSON.parse(File.read(@module.plugin_data_file))
59
+ rescue JSON::ParserError => e
60
+ raise InvalidPluginData.new(e.message, name)
61
+ end
62
+
63
+ def process_schema(schema)
64
+ raise InvalidPluginData.new('config specification is not an object', name) unless schema.is_a?(Hash)
65
+ schema.each do |key, val|
66
+ unless key =~ /\A[a-z][a-z0-9_]*\z/
67
+ raise InvalidPluginData.new("config specification key, '#{key}', is not allowed", name)
68
+ end
69
+
70
+ unless val.is_a?(Hash) && (val['type'] || '').is_a?(String)
71
+ raise InvalidPluginData.new("config specification #{val.to_json} is not allowed", name)
72
+ end
73
+
74
+ type_string = val['type'] || 'Any'
75
+ begin
76
+ val['pcore_type'] = Puppet::Pops::Types::TypeParser.singleton.parse(type_string)
77
+ if val['pcore_type'].is_a? Puppet::Pops::Types::PTypeReferenceType
78
+ raise InvalidPluginData.new("Could not find type '#{type_string}' for #{key}", name)
79
+ end
80
+ rescue Puppet::ParseError
81
+ raise InvalidPluginData.new("Could not parse type '#{type_string}' for #{key}", name)
82
+ end
83
+ end
84
+
85
+ schema
86
+ end
87
+
88
+ def validate_config(config, config_schema)
89
+ config.keys.each do |key|
90
+ msg = "Config for #{name} plugin contains unexpected key #{key}"
91
+ raise Bolt::ValidationError, msg unless config_schema.include?(key)
92
+ end
93
+
94
+ config_schema.each do |key, spec|
95
+ val = config[key]
96
+
97
+ unless spec['pcore_type'].instance?(val)
98
+ raise Bolt::ValidationError, "#{name} plugin expects a #{spec['type']} for key #{key}, got: #{val}"
99
+ end
100
+ val.nil?
101
+ end
102
+ nil
103
+ end
104
+
105
+ def find_hooks(hook_data)
106
+ raise InvalidPluginData.new("'hooks' must be a hash", name) unless hook_data.is_a?(Hash)
107
+
108
+ hooks = {}
109
+ # Load hooks specified in the config
110
+ hook_data.each do |hook_name, hook_spec|
111
+ unless hook_spec.is_a?(Hash) && hook_spec['task'].is_a?(String)
112
+ msg = "Unexpected hook specification #{hook_spec.to_json} in #{@name} for hook #{hook_name}"
113
+ raise InvalidPluginData.new(msg, name)
114
+ end
115
+
116
+ begin
117
+ task = @context.get_validated_task(hook_spec['task'])
118
+ rescue Bolt::Error => e
119
+ msg = if e.kind == 'bolt/unknown-task'
120
+ "Plugin #{name} specified an unkown task '#{hook_spec['task']}' for a hook"
121
+ else
122
+ "Plugin #{name} could not load task '#{hook_spec['task']}': #{e.message}"
123
+ end
124
+ raise InvalidPluginData.new(msg, name)
125
+ end
126
+
127
+ hooks[hook_name.to_sym] = { 'task' => task }
128
+ end
129
+
130
+ # Check for tasks for any hooks not already defined
131
+ (Set.new(KNOWN_HOOKS.map) - hooks.keys).each do |hook_name|
132
+ task_name = "#{name}::#{hook_name}"
133
+ begin
134
+ task = @context.get_validated_task(task_name)
135
+ rescue Bolt::Error => e
136
+ raise e unless e.kind == 'bolt/unknown-task'
137
+ end
138
+ hooks[hook_name] = { 'task' => task } if task
139
+ end
140
+
141
+ Bolt::Util.symbolize_top_level_keys(hooks)
142
+ end
143
+
144
+ def validate_params(task, params)
145
+ @context.validate_params(task.name, params)
146
+ end
147
+
148
+ def process_params(task, opts)
149
+ # opts are passed directly from inventory but all of the _ options are
150
+ # handled previously. That may not always be the case so filter them
151
+ # out now.
152
+ _meta, params = opts.partition { |key, _val| key.start_with?('_') }.map(&:to_h)
153
+
154
+ metaparams = {}
155
+ metaparams['_config'] = config if config?
156
+ metaparams['_boltdir'] = @context.boltdir
157
+
158
+ validate_params(task, params)
159
+ [params, metaparams]
160
+ end
161
+
162
+ def run_task(task, opts)
163
+ params, metaparams = process_params(task, opts)
164
+ params = params.merge(metaparams)
165
+
166
+ # There are no executor options to pass now.
167
+ options = { "_catch_errors" => true }
168
+
169
+ result = @context.run_local_task(task,
170
+ params,
171
+ options).first
172
+
173
+ raise Bolt::Error.new(result.error_hash['msg'], result.error_hash['kind']) unless result.ok
174
+ result.value
175
+ end
176
+
177
+ def run_hook(hook_name, opts, value = true)
178
+ hook = @hook_map[hook_name]
179
+ # This shouldn't happen if the Plugin api is used
180
+ raise PluginError::UnsupportedHook.new(name, hook_name) unless hook
181
+ result = run_task(hook['task'], opts)
182
+
183
+ if value
184
+ unless result.include?('value')
185
+ msg = "Plugin #{name} result did not include a value, got #{result}"
186
+ raise Bolt::Plugin::PluginError::ExecutionError.new(msg, name, hook_name)
187
+ end
188
+
189
+ result['value']
190
+ end
191
+ end
192
+
193
+ def validate_resolve_reference(opts)
194
+ params = opts.reject { |k, _v| k.start_with?('_') }
195
+ sig = @hook_map[:resolve_reference]['task']
196
+ if sig
197
+ validate_params(sig, params)
198
+ end
199
+
200
+ if @hook_map.include?(:validate_resolve_reference)
201
+ run_hook(:validate_resolve_reference, opts, false)
202
+ end
203
+ end
204
+
205
+ # These are all the same but are defined explicitly for clarity
206
+ def resolve_reference(opts)
207
+ run_hook(__method__, opts)
208
+ end
209
+
210
+ def secret_encrypt(opts)
211
+ run_hook(__method__, opts)
212
+ end
213
+
214
+ def secret_decrypt(opts)
215
+ run_hook(__method__, opts)
216
+ end
217
+
218
+ def secret_createkeys(opts = {})
219
+ run_hook(__method__, opts)
220
+ end
221
+
222
+ def puppet_library(opts, target, apply_prep)
223
+ tasksig = @hook_map[:puppet_library]
224
+
225
+ # this also validates
226
+ params, meta_params = process_params(tasksig, opts)
227
+
228
+ # our metaparams are meant for the task not the executor
229
+ params = params.merge(meta_params)
230
+ task = Bolt::Task.new(tasksig)
231
+
232
+ proc do
233
+ apply_prep.run_task([target], task, params).first
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  module Bolt
7
7
  class Plugin
8
8
  class Pkcs7 < Bolt::Secret::Base
9
- def self.validate_config(config)
9
+ def self.validate_config(config = {})
10
10
  known_keys = %w[private-key public-key keysize]
11
11
  known_keys.each do |key|
12
12
  unless key.is_a? String
@@ -25,17 +25,21 @@ module Bolt
25
25
  'pkcs7'
26
26
  end
27
27
 
28
- def initialize(boltdir, options)
29
- self.class.validate_config(options)
28
+ def initialize(config:, context:, **_opts)
29
+ self.class.validate_config(config)
30
30
  require 'openssl'
31
- @boltdir = boltdir
32
- @options = options || {}
31
+ @context = context
32
+ @options = config || {}
33
33
  @logger = Logging.logger[self]
34
34
  end
35
35
 
36
+ def boltdir
37
+ @context.boltdir
38
+ end
39
+
36
40
  def private_key_path
37
41
  path = @options['private-key'] || 'keys/private_key.pkcs7.pem'
38
- path = File.absolute_path(path, @boltdir)
42
+ path = File.absolute_path(path, boltdir)
39
43
  @logger.debug("Using private-key: #{path}")
40
44
  path
41
45
  end
@@ -46,7 +50,7 @@ module Bolt
46
50
 
47
51
  def public_key_path
48
52
  path = @options['public-key'] || 'keys/public_key.pkcs7.pem'
49
- path = File.absolute_path(path, @boltdir)
53
+ path = File.absolute_path(path, boltdir)
50
54
  @logger.debug("Using public-key: #{path}")
51
55
  path
52
56
  end
@@ -4,24 +4,21 @@ require 'concurrent/delay'
4
4
  module Bolt
5
5
  class Plugin
6
6
  class Prompt
7
- def initialize
8
- # Might not need this
9
- @logger = Logging.logger[self]
10
- end
7
+ def initialize(*_args); end
11
8
 
12
9
  def name
13
10
  'prompt'
14
11
  end
15
12
 
16
13
  def hooks
17
- ['inventory_config']
14
+ [:resolve_reference]
18
15
  end
19
16
 
20
- def validate_inventory_config(opts)
17
+ def validate_resolve_reference(opts)
21
18
  raise Bolt::ValidationError, "Prompt requires a 'message'" unless opts['message']
22
19
  end
23
20
 
24
- def inventory_config(opts)
21
+ def resolve_reference(opts)
25
22
  STDOUT.print "#{opts['message']}:"
26
23
  value = STDIN.noecho(&:gets).chomp
27
24
  STDOUT.puts
@@ -23,7 +23,7 @@ module Bolt
23
23
  end
24
24
 
25
25
  def hooks
26
- ['inventory_targets']
26
+ [:resolve_reference]
27
27
  end
28
28
 
29
29
  def warn_missing_fact(certname, fact)
@@ -41,7 +41,7 @@ module Bolt
41
41
  end
42
42
  end
43
43
 
44
- def inventory_targets(opts)
44
+ def resolve_reference(opts)
45
45
  targets = @puppetdb_client.query_certnames(opts['query'])
46
46
  facts = []
47
47