bolt 2.7.0 → 2.8.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.

@@ -9,12 +9,12 @@ module Bolt
9
9
  class Base
10
10
  attr_reader :input
11
11
 
12
- def initialize(data = {}, boltdir = nil)
12
+ def initialize(data = {}, project = nil)
13
13
  assert_hash_or_config(data)
14
14
  @input = data
15
15
  @resolved = !Bolt::Util.references?(input)
16
16
  @config = resolved? ? Bolt::Util.deep_merge(defaults, filter(input)) : defaults
17
- @boltdir = boltdir
17
+ @project = project
18
18
 
19
19
  validate if resolved?
20
20
  end
@@ -62,7 +62,7 @@ module Bolt
62
62
  Bolt::Util.deep_merge(acc, layer_data)
63
63
  end
64
64
 
65
- self.class.new(merged, @boltdir)
65
+ self.class.new(merged, @project)
66
66
  end
67
67
 
68
68
  # Resolve any references in the input data, then remerge it with the defaults
@@ -32,12 +32,12 @@ module Bolt
32
32
  super
33
33
 
34
34
  if @config['cacert']
35
- @config['cacert'] = File.expand_path(@config['cacert'], @boltdir)
35
+ @config['cacert'] = File.expand_path(@config['cacert'], @project)
36
36
  Bolt::Util.validate_file('cacert', @config['cacert'])
37
37
  end
38
38
 
39
39
  if @config['token-file']
40
- @config['token-file'] = File.expand_path(@config['token-file'], @boltdir)
40
+ @config['token-file'] = File.expand_path(@config['token-file'], @project)
41
41
  Bolt::Util.validate_file('token-file', @config['token-file'])
42
42
  end
43
43
  end
@@ -99,7 +99,7 @@ module Bolt
99
99
  end
100
100
 
101
101
  if key_opt.instance_of?(String)
102
- @config['private-key'] = File.expand_path(key_opt, @boltdir)
102
+ @config['private-key'] = File.expand_path(key_opt, @project)
103
103
  end
104
104
  end
105
105
 
@@ -71,7 +71,7 @@ module Bolt
71
71
  end
72
72
 
73
73
  if @config['cacert']
74
- @config['cacert'] = File.expand_path(@config['cacert'], @boltdir)
74
+ @config['cacert'] = File.expand_path(@config['cacert'], @project)
75
75
  Bolt::Util.validate_file('cacert', @config['cacert'])
76
76
  end
77
77
  end
@@ -278,6 +278,22 @@ module Bolt
278
278
  end
279
279
  end
280
280
 
281
+ def run_task_with(target_mapping, task, options = {})
282
+ targets = target_mapping.keys
283
+ description = options.fetch(:description, "task #{task.name}")
284
+
285
+ log_action(description, targets) do
286
+ options[:run_as] = run_as if run_as && !options.key?(:run_as)
287
+ target_mapping.each_value { |arguments| arguments['_task'] = task.name }
288
+
289
+ batch_execute(targets) do |transport, batch|
290
+ with_node_logging("Running task #{task.name}'", batch) do
291
+ transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
292
+ end
293
+ end
294
+ end
295
+ end
296
+
281
297
  def upload_file(targets, source, destination, options = {})
282
298
  description = options.fetch(:description, "file upload from #{source} to #{destination}")
283
299
  log_action(description, targets) do
@@ -40,7 +40,7 @@ module Bolt
40
40
  attr_reader :modulepath
41
41
 
42
42
  def initialize(modulepath, hiera_config, resource_types, max_compiles = Etc.nprocessors,
43
- trusted_external = nil, apply_settings = {})
43
+ trusted_external = nil, apply_settings = {}, project = nil)
44
44
  # Nothing works without initialized this global state. Reinitializing
45
45
  # is safe and in practice only happens in tests
46
46
  self.class.load_puppet
@@ -52,6 +52,7 @@ module Bolt
52
52
  @apply_settings = apply_settings
53
53
  @max_compiles = max_compiles
54
54
  @resource_types = resource_types
55
+ @project = project
55
56
 
56
57
  @logger = Logging.logger[self]
57
58
  if modulepath && !modulepath.empty?
@@ -114,7 +115,7 @@ module Bolt
114
115
  compiler.evaluate_string('type PlanResult = Boltlib::PlanResult')
115
116
  end
116
117
 
117
- # Register all resource types defined in $Boltdir/.resource_types as well as
118
+ # Register all resource types defined in $Project/.resource_types as well as
118
119
  # the built in types registered with the runtime_3_init method.
119
120
  def register_resource_types(loaders)
120
121
  static_loader = loaders.static_loader
@@ -136,19 +137,34 @@ module Bolt
136
137
  # TODO: If we always call this inside a bolt_executor we can remove this here
137
138
  setup
138
139
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
139
- pal.with_script_compiler do |compiler|
140
- alias_types(compiler)
141
- register_resource_types(Puppet.lookup(:loaders)) if @resource_types
142
- begin
143
- Puppet.override(yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
140
+ Puppet.override(bolt_project: @project,
141
+ yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
142
+ pal.with_script_compiler do |compiler|
143
+ alias_types(compiler)
144
+ register_resource_types(Puppet.lookup(:loaders)) if @resource_types
145
+ begin
144
146
  yield compiler
147
+ rescue Bolt::Error => e
148
+ e
149
+ rescue Puppet::DataBinding::LookupError => e
150
+ if e.issue_code == :HIERA_UNDEFINED_VARIABLE
151
+ message = "Interpolations are not supported in lookups outside of an apply block: #{e.message}"
152
+ PALError.new(message)
153
+ else
154
+ PALError.from_preformatted_error(e)
155
+ end
156
+ rescue Puppet::PreformattedError => e
157
+ if e.issue_code == :UNKNOWN_VARIABLE &&
158
+ %w[facts trusted server_facts settings].include?(e.arguments[:name])
159
+ message = "Evaluation Error: Variable '#{e.arguments[:name]}' is not available in the current scope "\
160
+ "unless explicitly defined. (file: #{e.file}, line: #{e.line}, column: #{e.pos})"
161
+ PALError.new(message)
162
+ else
163
+ PALError.from_preformatted_error(e)
164
+ end
165
+ rescue StandardError => e
166
+ PALError.from_preformatted_error(e)
145
167
  end
146
- rescue Bolt::Error => e
147
- e
148
- rescue Puppet::PreformattedError => e
149
- PALError.from_preformatted_error(e)
150
- rescue StandardError => e
151
- PALError.from_preformatted_error(e)
152
168
  end
153
169
  end
154
170
  end
@@ -215,6 +231,7 @@ module Bolt
215
231
  Puppet.initialize_settings(cli)
216
232
  Puppet::GettextConfig.create_default_text_domain
217
233
  Puppet[:trusted_external_command] = @trusted_external
234
+ Puppet.settings[:hiera_config] = @hiera_config
218
235
  self.class.configure_logging
219
236
  yield
220
237
  end
@@ -115,7 +115,7 @@ module Bolt
115
115
  end
116
116
 
117
117
  def boltdir
118
- @config.boltdir.path
118
+ @config.project.path
119
119
  end
120
120
  end
121
121
 
@@ -142,7 +142,7 @@ module Bolt
142
142
  plugins
143
143
  end
144
144
 
145
- RUBY_PLUGINS = %w[task pkcs7 prompt env_var].freeze
145
+ RUBY_PLUGINS = %w[task prompt env_var].freeze
146
146
  BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory
147
147
  yaml env_var gcloud_inventory].freeze
148
148
  DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }.freeze
@@ -30,6 +30,10 @@ module Bolt
30
30
  @module = mod
31
31
  @config = config
32
32
  @context = context
33
+
34
+ if @module.name == 'pkcs7'
35
+ @config = handle_deprecated_pkcs7_keys(@config)
36
+ end
33
37
  end
34
38
 
35
39
  # This method interacts with the module on disk so it's separate from initialize
@@ -155,7 +159,21 @@ module Bolt
155
159
  # handled previously. That may not always be the case so filter them
156
160
  # out now.
157
161
  meta, params = opts.partition { |key, _val| key.start_with?('_') }.map(&:to_h)
158
- params = config.merge(params)
162
+
163
+ if task.module_name == 'pkcs7'
164
+ params = handle_deprecated_pkcs7_keys(params)
165
+ end
166
+
167
+ # Reject parameters from config that are not accepted by the task and
168
+ # merge in parameter defaults
169
+ params = if task.parameters
170
+ task.parameter_defaults
171
+ .merge(config.slice(*task.parameters.keys))
172
+ .merge(params)
173
+ else
174
+ config.merge(params)
175
+ end
176
+
159
177
  validate_params(task, params)
160
178
 
161
179
  meta['_boltdir'] = @context.boltdir.to_s
@@ -163,6 +181,23 @@ module Bolt
163
181
  [params, meta]
164
182
  end
165
183
 
184
+ # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
+ # modifies the keys so they are the correct format
186
+ def handle_deprecated_pkcs7_keys(params)
187
+ if (params.key?('private-key') || params.key?('public-key')) && !@deprecation_warning_issued
188
+ @deprecation_warning_issued = true
189
+
190
+ message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
191
+ "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
192
+ Logging.logger[self].warn(message)
193
+ end
194
+
195
+ params['private_key'] = params.delete('private-key') if params.key?('private-key')
196
+ params['public_key'] = params.delete('public-key') if params.key?('public-key')
197
+
198
+ params
199
+ end
200
+
166
201
  def extract_task_parameter_schema
167
202
  # Get the intersection of expected types (using Set)
168
203
  type_set = @hook_map.each_with_object({}) do |(_hook, task), acc|
@@ -220,13 +255,11 @@ module Bolt
220
255
  end
221
256
 
222
257
  def validate_resolve_reference(opts)
223
- # Merge config with params
224
- merged = @config.merge(opts)
225
- params = merged.reject { |k, _v| k.start_with?('_') }
258
+ task = @hook_map[:resolve_reference]['task']
259
+ params, _metaparams = process_params(task, opts)
226
260
 
227
- sig = @hook_map[:resolve_reference]['task']
228
- if sig
229
- validate_params(sig, params)
261
+ if task
262
+ validate_params(task, params)
230
263
  end
231
264
 
232
265
  if @hook_map.include?(:validate_resolve_reference)
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'bolt/pal'
5
+
6
+ module Bolt
7
+ class Project
8
+ BOLTDIR_NAME = 'Boltdir'
9
+ PROJECT_SETTINGS = {
10
+ "name" => "The name of the project",
11
+ "plans" => "An array of plan names to whitelist. Whitelisted plans are included in `bolt plan show` output",
12
+ "tasks" => "An array of task names to whitelist. Whitelisted plans are included in `bolt task show` output"
13
+ }.freeze
14
+
15
+ attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
16
+ :puppetfile, :rerunfile, :type, :resource_types
17
+
18
+ def self.default_project
19
+ Project.new(File.join('~', '.puppetlabs', 'bolt'), 'user')
20
+ end
21
+
22
+ # Search recursively up the directory hierarchy for the Project. Look for a
23
+ # directory called Boltdir or a file called bolt.yaml (for a control repo
24
+ # type Project). Otherwise, repeat the check on each directory up the
25
+ # hierarchy, falling back to the default if we reach the root.
26
+ def self.find_boltdir(dir)
27
+ dir = Pathname.new(dir)
28
+ if (dir + BOLTDIR_NAME).directory?
29
+ new(dir + BOLTDIR_NAME, 'embedded')
30
+ elsif (dir + 'bolt.yaml').file?
31
+ new(dir, 'local')
32
+ elsif dir.root?
33
+ default_project
34
+ else
35
+ find_boltdir(dir.parent)
36
+ end
37
+ end
38
+
39
+ def initialize(path, type = 'option')
40
+ @path = Pathname.new(path).expand_path
41
+ @config_file = @path + 'bolt.yaml'
42
+ @inventory_file = @path + 'inventory.yaml'
43
+ @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
44
+ @hiera_config = @path + 'hiera.yaml'
45
+ @puppetfile = @path + 'Puppetfile'
46
+ @rerunfile = @path + '.rerun.json'
47
+ @resource_types = @path + '.resource_types'
48
+ @type = type
49
+
50
+ @project_file = @path + 'project.yaml'
51
+ @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
52
+ validate if load_as_module?
53
+ end
54
+
55
+ def to_s
56
+ @path.to_s
57
+ end
58
+
59
+ # This API is used to prepend the project as a module to Puppet's internal
60
+ # module_references list. CHANGE AT YOUR OWN RISK
61
+ def to_h
62
+ { path: @path, name: name }
63
+ end
64
+
65
+ def eql?(other)
66
+ path == other.path
67
+ end
68
+ alias == eql?
69
+
70
+ def load_as_module?
71
+ @project_file.file?
72
+ end
73
+
74
+ def name
75
+ # If the project is in mymod/Boltdir/project.yaml, use mymod as the project name
76
+ dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
77
+ pname = @data['name'] || dirname
78
+ pname.include?('-') ? pname.split('-', 2)[1] : pname
79
+ end
80
+
81
+ def tasks
82
+ @data['tasks']
83
+ end
84
+
85
+ def plans
86
+ @data['plans']
87
+ end
88
+
89
+ def project_directory_name?(name)
90
+ # it must match an installed project name according to forge validator
91
+ name =~ /^[a-z][a-z0-9_]*$/
92
+ end
93
+
94
+ def project_namespaced_name?(name)
95
+ # it must match the full project name according to forge validator
96
+ name =~ /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
97
+ end
98
+
99
+ def validate
100
+ n = @data['name']
101
+ if n && !project_directory_name?(n) && !project_namespaced_name?(n)
102
+ raise Bolt::ValidationError, <<~ERROR_STRING
103
+ Invalid project name '#{n}' in project.yaml; project names must match either:
104
+ An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
105
+ A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
106
+ ERROR_STRING
107
+ elsif !project_directory_name?(name) && !project_namespaced_name?(name)
108
+ raise Bolt::ValidationError, <<~ERROR_STRING
109
+ Invalid project name '#{name}'; project names must match either:
110
+ A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
111
+ A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
112
+
113
+ Configure project name in <project_dir>/project.yaml
114
+ ERROR_STRING
115
+ # If the project name is the same as one of the built-in modules raise a warning
116
+ elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
117
+ raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
118
+ "with a built-in Bolt module of the same name."
119
+ end
120
+
121
+ %w[tasks plans].each do |conf|
122
+ unless @data.fetch(conf, []).is_a?(Array)
123
+ raise Bolt::ValidationError, "'#{conf}' in project.yaml must be an array"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -22,7 +22,7 @@ module Bolt
22
22
  File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs/client-tools/puppetdb.conf'))
23
23
  end
24
24
 
25
- def self.load_config(filename, options, boltdir_path = nil)
25
+ def self.load_config(filename, options, project_path = nil)
26
26
  config = {}
27
27
  global_path = Bolt::Util.windows? ? default_windows_config : DEFAULT_CONFIG[:global]
28
28
  if filename
@@ -46,12 +46,12 @@ module Bolt
46
46
  end
47
47
 
48
48
  config = config.fetch('puppetdb', {})
49
- new(config.merge(options), boltdir_path)
49
+ new(config.merge(options), project_path)
50
50
  end
51
51
 
52
- def initialize(settings, boltdir_path = nil)
52
+ def initialize(settings, project_path = nil)
53
53
  @settings = settings
54
- @boltdir_path = boltdir_path
54
+ @project_path = project_path
55
55
  expand_paths
56
56
  end
57
57
 
@@ -71,8 +71,8 @@ module Bolt
71
71
  def expand_paths
72
72
  %w[cacert cert key token].each do |file|
73
73
  next unless @settings[file]
74
- @settings[file] = if @boltdir_path
75
- File.expand_path(@settings[file], @boltdir_path)
74
+ @settings[file] = if @project_path
75
+ File.expand_path(@settings[file], @project_path)
76
76
  else
77
77
  File.expand_path(@settings[file])
78
78
  end
@@ -1,17 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/plugin'
4
+
3
5
  module Bolt
4
6
  class Secret
7
+ KNOWN_KEYS = {
8
+ 'createkeys' => %w[keysize private_key public_key],
9
+ 'encrypt' => %w[public_key],
10
+ 'decrypt' => %w[private_key public_key]
11
+ }.freeze
12
+
5
13
  def self.execute(plugins, outputter, options)
6
- plugin = options[:plugin] || 'pkcs7'
14
+ name = options[:plugin] || 'pkcs7'
15
+ plugin = plugins.by_name(name)
16
+
17
+ unless plugin
18
+ raise Bolt::Plugin::PluginError::Unknown, name
19
+ end
20
+
7
21
  case options[:action]
8
22
  when 'createkeys'
9
- plugins.get_hook(plugin, :secret_createkeys).call
23
+ opts = { 'force' => options[:force] }.compact
24
+ result = plugins.get_hook(name, :secret_createkeys).call(opts)
25
+ outputter.print_message(result)
10
26
  when 'encrypt'
11
- encrypted = plugins.get_hook(plugin, :secret_encrypt).call('plaintext_value' => options[:object])
27
+ encrypted = plugins.get_hook(name, :secret_encrypt).call('plaintext_value' => options[:object])
12
28
  outputter.print_message(encrypted)
13
29
  when 'decrypt'
14
- decrypted = plugins.get_hook(plugin, :secret_decrypt).call('encrypted_value' => options[:object])
30
+ decrypted = plugins.get_hook(name, :secret_decrypt).call('encrypted_value' => options[:object])
15
31
  outputter.print_message(decrypted)
16
32
  end
17
33