bolt 1.34.0 → 1.35.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.

@@ -8,10 +8,10 @@ module Bolt
8
8
  def self.discover(modulepath)
9
9
  modulepath.each_with_object({}) do |path, mods|
10
10
  next unless File.exist?(path) && File.directory?(path)
11
- Dir.children(path)
12
- .map { |dir| File.join(path, dir) }
13
- .select { |dir| File.directory?(dir) }
14
- .each do |dir|
11
+ (Dir.entries(path) - %w[. ..])
12
+ .map { |dir| File.join(path, dir) }
13
+ .select { |dir| File.directory?(dir) }
14
+ .each do |dir|
15
15
  module_name = File.basename(dir)
16
16
  if module_name =~ MODULE_NAME_REGEX
17
17
  # Puppet will load some objects from shadowed modules but this won't
@@ -123,13 +123,10 @@ module Bolt
123
123
  # PDB is special do we want to expose the default client to the context?
124
124
  plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
125
125
 
126
- plugins.add_ruby_plugin('Bolt::Plugin::AwsInventory')
127
126
  plugins.add_ruby_plugin('Bolt::Plugin::InstallAgent')
128
127
  plugins.add_ruby_plugin('Bolt::Plugin::Task')
129
- plugins.add_ruby_plugin('Bolt::Plugin::Terraform')
130
128
  plugins.add_ruby_plugin('Bolt::Plugin::Pkcs7')
131
129
  plugins.add_ruby_plugin('Bolt::Plugin::Prompt')
132
- plugins.add_ruby_plugin('Bolt::Plugin::Vault')
133
130
 
134
131
  plugins
135
132
  end
@@ -35,9 +35,18 @@ module Bolt
35
35
  # This method interacts with the module on disk so it's separate from initialize
36
36
  def setup
37
37
  @data = load_data
38
- @config_schema = process_schema(@data['config'] || {})
39
-
40
38
  @hook_map = find_hooks(@data['hooks'] || {})
39
+ # If there is a config section in bolt_plugin.json, validate against that and send
40
+ # validated values nested under `_config` key. Otherwise validate againsts the intersection
41
+ # of all task schemas.
42
+ # TODO: remove @send_config when deprecated
43
+ schema = if @data['config']
44
+ @send_config = true
45
+ @data['config']
46
+ else
47
+ extract_task_parameter_schema
48
+ end
49
+ @config_schema = process_schema(schema)
41
50
 
42
51
  validate_config(@config, @config_schema)
43
52
  end
@@ -150,15 +159,45 @@ module Bolt
150
159
  # handled previously. That may not always be the case so filter them
151
160
  # out now.
152
161
  _meta, params = opts.partition { |key, _val| key.start_with?('_') }.map(&:to_h)
153
-
154
162
  metaparams = {}
155
- metaparams['_config'] = config if config?
163
+ # Send config with `_config` when config is defined in bolt_plugin.json
164
+ # Otherwise, merge config with params
165
+ # TODO: remove @send_config when deprecated
166
+ if @send_config
167
+ metaparams['_config'] = config if config?
168
+ else
169
+ params = @config ? config.merge(params) : params
170
+ end
156
171
  metaparams['_boltdir'] = @context.boltdir
157
172
 
158
173
  validate_params(task, params)
159
174
  [params, metaparams]
160
175
  end
161
176
 
177
+ def extract_task_parameter_schema
178
+ # Get the intersection of expected types (using Set)
179
+ type_set = @hook_map.each_with_object({}) do |(_hook, task), acc|
180
+ next unless (schema = task['task'].metadata['parameters'])
181
+ schema.each do |param, scheme|
182
+ next unless scheme['type'].is_a?(String)
183
+ scheme['type'] = Set.new([scheme['type']])
184
+ if acc.dig(param, 'type').is_a?(Set)
185
+ scheme['type'].merge(acc[param]['type'])
186
+ end
187
+ end
188
+ acc.merge!(schema)
189
+ end
190
+ # Convert Set to string
191
+ type_set.each do |_param, schema|
192
+ next unless schema['type']
193
+ schema['type'] = if schema['type'].size > 1
194
+ "Optional[Variant[#{schema['type'].to_a.join(', ')}]]"
195
+ else
196
+ "Optional[#{schema['type'].to_a.first}]"
197
+ end
198
+ end
199
+ end
200
+
162
201
  def run_task(task, opts)
163
202
  params, metaparams = process_params(task, opts)
164
203
  params = params.merge(metaparams)
@@ -191,7 +230,15 @@ module Bolt
191
230
  end
192
231
 
193
232
  def validate_resolve_reference(opts)
194
- params = opts.reject { |k, _v| k.start_with?('_') }
233
+ # Send config with `_config` when config is defined in bolt_plugin.json
234
+ # Otherwise, merge config with params
235
+ # TODO: remove @send_config when deprecated
236
+ if @send_config
237
+ params = opts.reject { |k, _v| k.start_with?('_') }
238
+ else
239
+ merged = @config.merge(opts)
240
+ params = merged.reject { |k, _v| k.start_with?('_') }
241
+ end
195
242
  sig = @hook_map[:resolve_reference]['task']
196
243
  if sig
197
244
  validate_params(sig, params)
@@ -48,7 +48,11 @@ module Bolt
48
48
 
49
49
  def puppet_library(opts, target, apply_prep)
50
50
  params = opts['parameters'] || {}
51
- task = apply_prep.get_task(opts['task'], params)
51
+ begin
52
+ task = apply_prep.get_task(opts['task'], params)
53
+ rescue Bolt::Error => e
54
+ raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, name, 'puppet_library')
55
+ end
52
56
  proc do
53
57
  apply_prep.run_task([target], task, params).first
54
58
  end
@@ -10,10 +10,12 @@ module Bolt
10
10
  # Target.new from a plan initialized with a hash
11
11
  def self.from_asserted_hash(hash)
12
12
  inventory = Puppet.lookup(:bolt_inventory)
13
- inventory.create_target_from_plan(hash)
13
+ target = inventory.create_target_from_plan(hash)
14
+ new(target.name, inventory)
14
15
  end
15
16
 
16
- # Target.new from a plan with just a uri
17
+ # Target.new from a plan with just a uri. Puppet requires the arguments to
18
+ # this method to match (by name) the attributes defined on the datatype.
17
19
  # rubocop:disable Lint/UnusedMethodArgument
18
20
  def self.from_asserted_args(uri = nil,
19
21
  name = nil,
@@ -23,37 +25,15 @@ module Bolt
23
25
  vars = nil,
24
26
  features = nil,
25
27
  plugin_hooks = nil)
26
- inventory = Puppet.lookup(:bolt_inventory)
27
- inventory.create_target_from_plan('uri' => uri)
28
+ from_asserted_hash('uri' => uri)
28
29
  end
29
30
 
30
- # URI can be passes as nil
31
- def initialize(uri = nil,
32
- name = nil,
33
- target_alias = nil,
34
- config = nil,
35
- facts = nil,
36
- vars = nil,
37
- features = nil,
38
- plugin_hooks = nil)
31
+ def initialize(name, inventory = nil)
39
32
  @name = name
33
+ @inventory = inventory
40
34
  end
41
35
  # rubocop:enable Lint/UnusedMethodArgument
42
36
 
43
- # Used for munging target + group data
44
- def target_data_hash
45
- {
46
- 'config' => @inventory.targets[@name]['config'],
47
- 'vars' => @inventory.targets[@name]['vars'],
48
- 'facts' => @inventory.targets[@name]['facts'],
49
- 'features' => @inventory.targets[@name]['features'].to_a,
50
- 'plugin_hooks' => @inventory.targets[@name]['plugin_hooks'],
51
- 'name' => @inventory.targets[@name]['name'],
52
- 'uri' => @inventory.targets[@name]['uri'],
53
- 'alias' => @inventory.targets[@name]['target_alias']
54
- }
55
- end
56
-
57
37
  # features returns an array to be compatible with plans
58
38
  def features
59
39
  @inventory.features(self).to_a
@@ -77,15 +57,15 @@ module Bolt
77
57
  end
78
58
 
79
59
  def config
80
- @inventory.target_config(self)
60
+ inventory_target.config
81
61
  end
82
62
 
83
63
  def safe_name
84
- @inventory.targets[@name]['safe_name']
64
+ inventory_target.safe_name
85
65
  end
86
66
 
87
67
  def target_alias
88
- @inventory.targets[@name]['target_alias']
68
+ inventory_target.target_alias
89
69
  end
90
70
 
91
71
  def to_h
@@ -100,53 +80,51 @@ module Bolt
100
80
  )
101
81
  end
102
82
 
83
+ def inventory_target
84
+ @inventory.targets[@name]
85
+ end
86
+
103
87
  def host
104
- @inventory.targets[@name]['uri_obj']&.hostname || @inventory.targets[@name]['host']
88
+ inventory_target.host
105
89
  end
106
90
 
107
91
  attr_reader :name
108
92
 
109
93
  def uri
110
- @inventory.targets[@name]['uri']
94
+ inventory_target.uri
111
95
  end
112
96
 
113
97
  def remote?
114
- @inventory.targets[@name]['uri_obj']&.scheme == 'remote' || @inventory.targets[@name]['protocol'] == 'remote'
98
+ protocol == 'remote'
115
99
  end
116
100
 
117
101
  def port
118
- @inventory.targets[@name]['uri_obj']&.port || @inventory.targets[@name]['port']
102
+ inventory_target.port
119
103
  end
120
104
 
121
- # transport is separate from protocol for remote targets.
122
105
  def transport
123
- remote? ? 'remote' : protocol
106
+ inventory_target.protocol
124
107
  end
125
108
 
126
109
  def protocol
127
- @inventory.targets[@name]['uri_obj']&.scheme || @inventory.targets[@name]['protocol']
110
+ inventory_target.protocol
128
111
  end
129
112
 
130
113
  def user
131
- unencode(@inventory.targets[@name]['uri_obj']&.user) || @inventory.targets[@name]['user']
114
+ inventory_target.user
132
115
  end
133
116
 
134
117
  def password
135
- unencode(@inventory.targets[@name]['uri_obj']&.password) || @inventory.targets[@name]['password']
118
+ inventory_target.password
136
119
  end
137
120
 
138
121
  def options
139
- @inventory.targets[@name]['options']
122
+ inventory_target.options
140
123
  end
141
124
 
142
125
  def plugin_hooks
143
- @inventory.targets[@name]['cached_state']['plugin_hooks']
144
- end
145
-
146
- def unencode(component)
147
- Addressable::URI.unencode_component(component)
126
+ inventory_target.plugin_hooks
148
127
  end
149
- private :unencode
150
128
 
151
129
  def eql?(other)
152
130
  self.class.equal?(other.class) && @name == other.name
@@ -147,6 +147,24 @@ module Bolt
147
147
  end
148
148
  end
149
149
 
150
+ # Accepts a Data object and returns a copy with all hash and array values
151
+ # modified by the given block. Descendants are modified before their
152
+ # parents.
153
+ def postwalk_vals(data, skip_top = false, &block)
154
+ new_data = if data.is_a? Hash
155
+ map_vals(data) { |v| postwalk_vals(v, &block) }
156
+ elsif data.is_a? Array
157
+ data.map { |v| postwalk_vals(v, &block) }
158
+ else
159
+ data
160
+ end
161
+ if skip_top
162
+ new_data
163
+ else
164
+ yield(new_data)
165
+ end
166
+ end
167
+
150
168
  # Performs a deep_clone, using an identical copy if the cloned structure contains multiple
151
169
  # references to the same object and prevents endless recursion.
152
170
  # Credit to Jan Molic via https://github.com/rubyworks/facets/blob/master/LICENSE.txt
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.34.0'
4
+ VERSION = '1.35.0'
5
5
  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: 1.34.0
4
+ version: 1.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-17 00:00:00.000000000 Z
11
+ date: 2019-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -373,6 +373,7 @@ files:
373
373
  - lib/bolt/inventory/group.rb
374
374
  - lib/bolt/inventory/group2.rb
375
375
  - lib/bolt/inventory/inventory2.rb
376
+ - lib/bolt/inventory/target.rb
376
377
  - lib/bolt/logger.rb
377
378
  - lib/bolt/module.rb
378
379
  - lib/bolt/node/errors.rb
@@ -399,15 +400,12 @@ files:
399
400
  - lib/bolt/pal/yaml_plan/transpiler.rb
400
401
  - lib/bolt/plan_result.rb
401
402
  - lib/bolt/plugin.rb
402
- - lib/bolt/plugin/aws_inventory.rb
403
403
  - lib/bolt/plugin/install_agent.rb
404
404
  - lib/bolt/plugin/module.rb
405
405
  - lib/bolt/plugin/pkcs7.rb
406
406
  - lib/bolt/plugin/prompt.rb
407
407
  - lib/bolt/plugin/puppetdb.rb
408
408
  - lib/bolt/plugin/task.rb
409
- - lib/bolt/plugin/terraform.rb
410
- - lib/bolt/plugin/vault.rb
411
409
  - lib/bolt/puppetdb.rb
412
410
  - lib/bolt/puppetdb/client.rb
413
411
  - lib/bolt/puppetdb/config.rb
@@ -1,102 +0,0 @@
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
@@ -1,175 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Bolt
6
- class Plugin
7
- class Terraform
8
- KNOWN_KEYS = Set['_plugin', 'dir', 'resource_type', 'uri', 'name', 'statefile',
9
- 'config', 'backend']
10
- REQ_KEYS = Set['dir', 'resource_type']
11
-
12
- def initialize(*_args)
13
- @logger = Logging.logger[self]
14
- end
15
-
16
- def name
17
- 'terraform'
18
- end
19
-
20
- def hooks
21
- [:resolve_reference]
22
- end
23
-
24
- def warn_missing_property(name, property)
25
- @logger.warn("Could not find property #{property} of terraform resource #{name}")
26
- end
27
-
28
- # Make sure no unexpected keys are in the inventory config and
29
- # that required keys are present
30
- def validate_options(opts)
31
- opt_keys = opts.keys.to_set
32
-
33
- unless KNOWN_KEYS.superset?(opt_keys)
34
- keys = opt_keys - KNOWN_KEYS
35
- raise Bolt::ValidationError, "Unexpected key(s) in inventory config: #{keys.to_a.inspect}"
36
- end
37
-
38
- unless opt_keys.superset?(REQ_KEYS)
39
- keys = REQ_KEYS - opt_keys
40
- raise Bolt::ValidationError, "Expected key(s) in inventory config: #{keys.to_a.inspect}"
41
- end
42
- end
43
-
44
- def resolve_reference(opts)
45
- validate_options(opts)
46
-
47
- state = load_statefile(opts)
48
-
49
- resources = extract_resources(state)
50
-
51
- regex = Regexp.new(opts['resource_type'])
52
-
53
- resources.select do |name, _resource|
54
- name.match?(regex)
55
- end.map do |name, resource|
56
- target = {}
57
-
58
- if opts.key?('uri')
59
- uri = lookup(name, resource, opts['uri'])
60
- target['uri'] = uri if uri
61
- end
62
- if opts.key?('name')
63
- real_name = lookup(name, resource, opts['name'])
64
- target['name'] = real_name if real_name
65
- end
66
- if opts.key?('config')
67
- target['config'] = resolve_config(name, resource, opts['config'])
68
- end
69
- target
70
- end.compact
71
- end
72
-
73
- def load_statefile(opts)
74
- statefile = if opts['backend'] == 'remote'
75
- load_remote_statefile(opts)
76
- else
77
- load_local_statefile(opts)
78
- end
79
-
80
- JSON.parse(statefile)
81
- end
82
-
83
- # Uses the Terraform CLI to pull remote state files
84
- def load_remote_statefile(opts)
85
- dir = File.expand_path(opts['dir'])
86
-
87
- begin
88
- stdout_str, stderr_str, status = Open3.capture3('terraform state pull', chdir: dir)
89
- rescue Errno::ENOENT
90
- reason = if File.directory?(dir)
91
- "Could not find executable 'terraform'"
92
- else
93
- "Could not find directory '#{dir}'"
94
- end
95
- raise Bolt::Error.new(reason, 'FILE_ERROR')
96
- end
97
-
98
- unless status.success?
99
- err = stdout_str + stderr_str
100
- msg = "Could not pull Terraform remote state file for #{opts['dir']}:\n#{err}"
101
- raise Bolt::Error.new(msg, 'bolt/terraform-state-error')
102
- end
103
-
104
- stdout_str
105
- end
106
-
107
- def load_local_statefile(opts)
108
- dir = opts['dir']
109
- filename = opts.fetch('statefile', 'terraform.tfstate')
110
- File.read(File.expand_path(File.join(dir, filename)))
111
- rescue StandardError => e
112
- raise Bolt::FileError.new("Could not load Terraform state file #{filename}: #{e}", filename)
113
- end
114
-
115
- # Format the list of resources into a list of [name, attribute map]
116
- # pairs. This method handles both version 4 and earlier statefiles, doing
117
- # the appropriate munging based on the shape of the data.
118
- def extract_resources(state)
119
- if state['version'] >= 4
120
- state.fetch('resources', []).flat_map do |resource_set|
121
- prefix = "#{resource_set['type']}.#{resource_set['name']}"
122
- resource_set['instances'].map do |resource|
123
- instance_name = prefix
124
- instance_name += ".#{resource['index_key']}" if resource['index_key']
125
- # When using `terraform state pull` with terraform >= 0.12 version 3 statefiles
126
- # Will be converted to version 4. When converted attributes is converted to attributes_flat
127
- attributes = resource['attributes'] || resource['attributes_flat']
128
- [instance_name, attributes]
129
- end
130
- end
131
- else
132
- state.fetch('modules', {}).flat_map do |mod|
133
- mod.fetch('resources', {}).map do |name, resource|
134
- [name, resource.dig('primary', 'attributes')]
135
- end
136
- end
137
- end
138
- end
139
-
140
- # Look up a nested value from the resource attributes. The key is of the
141
- # form `foo.bar.0.baz`. For terraform statefile version 3, this will
142
- # exactly correspond to a key in the resource. In version 4, it will
143
- # correspond to a nested hash entry at {foo: {bar: [{baz: <value>}]}}
144
- # For simplicity's sake, we just check both.
145
- def lookup(name, resource, path)
146
- segments = path.split('.').map do |segment|
147
- begin
148
- Integer(segment)
149
- rescue ArgumentError
150
- segment
151
- end
152
- end
153
-
154
- value = resource[path] || resource.dig(*segments)
155
- unless value
156
- warn_missing_property(name, path)
157
- end
158
- value
159
- end
160
-
161
- # Walk the "template" config mapping provided in the plugin config and
162
- # replace all values with the corresponding value from the resource
163
- # parameters.
164
- def resolve_config(name, resource, config_template)
165
- Bolt::Util.walk_vals(config_template) do |value|
166
- if value.is_a?(String)
167
- lookup(name, resource, value)
168
- else
169
- value
170
- end
171
- end
172
- end
173
- end
174
- end
175
- end