bolt 2.17.0 → 2.22.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/lib/bolt/applicator.rb +19 -14
  8. data/lib/bolt/apply_result.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +68 -13
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +232 -47
  12. data/lib/bolt/config.rb +34 -13
  13. data/lib/bolt/config/options.rb +16 -1
  14. data/lib/bolt/config/transport/options.rb +16 -10
  15. data/lib/bolt/config/transport/ssh.rb +24 -10
  16. data/lib/bolt/executor.rb +21 -0
  17. data/lib/bolt/inventory/group.rb +3 -2
  18. data/lib/bolt/inventory/inventory.rb +4 -3
  19. data/lib/bolt/logger.rb +21 -0
  20. data/lib/bolt/module.rb +2 -1
  21. data/lib/bolt/outputter/rainbow.rb +9 -2
  22. data/lib/bolt/pal.rb +8 -2
  23. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  24. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  25. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  26. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  27. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  28. data/lib/bolt/plugin/module.rb +2 -4
  29. data/lib/bolt/plugin/puppetdb.rb +3 -2
  30. data/lib/bolt/project.rb +25 -11
  31. data/lib/bolt/puppetdb/client.rb +2 -0
  32. data/lib/bolt/puppetdb/config.rb +16 -0
  33. data/lib/bolt/result.rb +7 -0
  34. data/lib/bolt/shell/bash.rb +24 -4
  35. data/lib/bolt/shell/powershell.rb +10 -4
  36. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  37. data/lib/bolt/transport/base.rb +24 -0
  38. data/lib/bolt/transport/docker.rb +8 -0
  39. data/lib/bolt/transport/docker/connection.rb +20 -2
  40. data/lib/bolt/transport/local/connection.rb +14 -1
  41. data/lib/bolt/transport/orch.rb +12 -0
  42. data/lib/bolt/transport/simple.rb +6 -0
  43. data/lib/bolt/transport/ssh.rb +7 -1
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  46. data/lib/bolt/transport/winrm/connection.rb +118 -8
  47. data/lib/bolt/util.rb +26 -11
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/transport_app.rb +3 -2
  50. data/lib/bolt_spec/bolt_context.rb +7 -2
  51. data/lib/bolt_spec/plans.rb +15 -2
  52. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  53. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  54. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  55. data/lib/bolt_spec/run.rb +22 -0
  56. data/libexec/bolt_catalog +3 -2
  57. data/modules/secure_env_vars/plans/init.pp +20 -0
  58. metadata +8 -2
@@ -73,7 +73,7 @@ module Bolt
73
73
  end
74
74
 
75
75
  def upload_step(scope, step)
76
- source = step['source']
76
+ source = step['upload'] || step['source']
77
77
  destination = step['destination']
78
78
  targets = step['targets'] || step['target']
79
79
  description = step['description']
@@ -83,6 +83,17 @@ module Bolt
83
83
  scope.call_function('upload_file', args)
84
84
  end
85
85
 
86
+ def download_step(scope, step)
87
+ source = step['download']
88
+ destination = step['destination']
89
+ targets = step['targets'] || step['target']
90
+ description = step['description']
91
+
92
+ args = [source, destination, targets]
93
+ args << description if description
94
+ scope.call_function('download_file', args)
95
+ end
96
+
86
97
  def eval_step(_scope, step)
87
98
  step['eval']
88
99
  end
@@ -97,6 +108,10 @@ module Bolt
97
108
  apply_manifest(scope, targets, manifest)
98
109
  end
99
110
 
111
+ def message_step(scope, step)
112
+ scope.call_function('out::message', step['message'])
113
+ end
114
+
100
115
  def generate_manifest(resources)
101
116
  # inspect returns the Ruby representation of the resource hashes,
102
117
  # which happens to be the same as the Puppet representation
@@ -142,7 +157,13 @@ module Bolt
142
157
  if plan.steps.any? { |step| step.body.key?('target') }
143
158
  msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
144
159
  "in a future version of Bolt. Use the 'targets' parameter instead."
145
- @logger.warn(msg)
160
+ Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
161
+ end
162
+
163
+ if plan.steps.any? { |step| step.body.key?('source') }
164
+ msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
165
+ "in a future version of Bolt. Use the 'upload' parameter instead."
166
+ Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
146
167
  end
147
168
 
148
169
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
@@ -12,7 +12,19 @@ module Bolt
12
12
  Set['name', 'description', 'target', 'targets']
13
13
  end
14
14
 
15
- STEP_KEYS = %w[command script task plan source destination eval resources].freeze
15
+ STEP_KEYS = %w[
16
+ command
17
+ destination
18
+ download
19
+ eval
20
+ message
21
+ plan
22
+ resources
23
+ script
24
+ source
25
+ task
26
+ upload
27
+ ].freeze
16
28
 
17
29
  def self.create(step_body, step_number)
18
30
  type_keys = (STEP_KEYS & step_body.keys)
@@ -22,8 +34,10 @@ module Bolt
22
34
  when 1
23
35
  type = type_keys.first
24
36
  else
25
- if type_keys.to_set == Set['source', 'destination']
37
+ if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
26
38
  type = 'upload'
39
+ elsif type_keys.to_set == Set['download', 'destination']
40
+ type = 'download'
27
41
  else
28
42
  raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
29
43
  end
@@ -89,6 +103,12 @@ module Bolt
89
103
  missing_keys -= ['targets']
90
104
  end
91
105
 
106
+ # Handle cases where upload step uses deprecated 'source' key instead of 'upload'
107
+ # TODO: Remove when 'source' is removed
108
+ if body.include?('source')
109
+ missing_keys -= ['upload']
110
+ end
111
+
92
112
  if missing_keys.any?
93
113
  error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
94
114
  err = step_error(error_message, body['name'], step_number)
@@ -156,3 +176,5 @@ require 'bolt/pal/yaml_plan/step/resources'
156
176
  require 'bolt/pal/yaml_plan/step/script'
157
177
  require 'bolt/pal/yaml_plan/step/task'
158
178
  require 'bolt/pal/yaml_plan/step/upload'
179
+ require 'bolt/pal/yaml_plan/step/download'
180
+ require 'bolt/pal/yaml_plan/step/message'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Download < Step
8
+ def self.allowed_keys
9
+ super + Set['download', 'destination']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['download', 'destination', 'targets']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @source = step_body['download']
19
+ @destination = step_body['destination']
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'download_file'
27
+ args = [@source, @destination, @targets]
28
+ args << @description if @description
29
+
30
+ code << function_call(fn, args)
31
+
32
+ code << "\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Message < Step
8
+ def self.allowed_keys
9
+ super + Set['message']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['message']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @message = step_body['message']
19
+ end
20
+
21
+ def transpile
22
+ code = String.new(" ")
23
+ code << function_call('out::message', [@message])
24
+ code << "\n"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -6,16 +6,16 @@ module Bolt
6
6
  class Step
7
7
  class Upload < Step
8
8
  def self.allowed_keys
9
- super + Set['source', 'destination']
9
+ super + Set['source', 'destination', 'upload']
10
10
  end
11
11
 
12
12
  def self.required_keys
13
- Set['destination', 'source', 'targets']
13
+ Set['upload', 'destination', 'targets']
14
14
  end
15
15
 
16
16
  def initialize(step_body)
17
17
  super
18
- @source = step_body['source']
18
+ @source = step_body['upload'] || step_body['source']
19
19
  @destination = step_body['destination']
20
20
  end
21
21
 
@@ -184,12 +184,10 @@ module Bolt
184
184
  # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
185
  # modifies the keys so they are the correct format
186
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
-
187
+ if params.key?('private-key') || params.key?('public-key')
190
188
  message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
191
189
  "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
192
- Logging.logger[self].warn(message)
190
+ Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
193
191
  end
194
192
 
195
193
  params['private_key'] = params.delete('private-key') if params.key?('private-key')
@@ -85,7 +85,8 @@ module Bolt
85
85
 
86
86
  def resolve_facts(config, certname, target_data)
87
87
  Bolt::Util.walk_vals(config) do |value|
88
- if value.is_a?(String)
88
+ case value
89
+ when String
89
90
  if value == 'certname'
90
91
  certname
91
92
  else
@@ -94,7 +95,7 @@ module Bolt
94
95
  # If there's no fact data this will be nil
95
96
  data&.fetch('value', nil)
96
97
  end
97
- elsif value.is_a?(Array) || value.is_a?(Hash)
98
+ when Array, Hash
98
99
  value
99
100
  else
100
101
  raise FactLookupError.new(value, "fact lookups must be a string")
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
- require 'bolt/pal'
5
4
  require 'bolt/config'
5
+ require 'bolt/pal'
6
+ require 'bolt/module'
6
7
 
7
8
  module Bolt
8
9
  class Project
@@ -16,7 +17,8 @@ module Bolt
16
17
  }.freeze
17
18
 
18
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
20
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
21
+ :deprecations, :downloads, :plans_path
20
22
 
21
23
  def self.default_project
22
24
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -31,6 +33,7 @@ module Bolt
31
33
  # hierarchy, falling back to the default if we reach the root.
32
34
  def self.find_boltdir(dir)
33
35
  dir = Pathname.new(dir)
36
+
34
37
  if (dir + BOLTDIR_NAME).directory?
35
38
  create_project(dir + BOLTDIR_NAME, 'embedded')
36
39
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
@@ -44,6 +47,15 @@ module Bolt
44
47
 
45
48
  def self.create_project(path, type = 'option')
46
49
  fullpath = Pathname.new(path).expand_path
50
+
51
+ if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
52
+ raise Bolt::Error.new(
53
+ "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
54
+ "BOLT_PROJECT='#{fullpath}' to force the use of this project directory.",
55
+ "bolt/world-writable-error"
56
+ )
57
+ end
58
+
47
59
  project_file = File.join(fullpath, 'bolt-project.yaml')
48
60
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
49
61
  new(data, path, type)
@@ -51,14 +63,16 @@ module Bolt
51
63
 
52
64
  def initialize(raw_data, path, type = 'option')
53
65
  @path = Pathname.new(path).expand_path
66
+
54
67
  @project_file = @path + 'bolt-project.yaml'
55
68
 
56
69
  @warnings = []
70
+ @deprecations = []
57
71
  if (@path + 'bolt.yaml').file? && project_file?
58
72
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
59
73
  "Transport config should be set in inventory.yaml, all other config should be set in "\
60
74
  "bolt-project.yaml."
61
- @warnings << { msg: msg }
75
+ @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
62
76
  end
63
77
 
64
78
  @inventory_file = @path + 'inventory.yaml'
@@ -68,6 +82,8 @@ module Bolt
68
82
  @rerunfile = @path + '.rerun.json'
69
83
  @resource_types = @path + '.resource_types'
70
84
  @type = type
85
+ @downloads = @path + 'downloads'
86
+ @plans_path = @path + 'plans'
71
87
 
72
88
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
73
89
  if tc.any?
@@ -98,7 +114,7 @@ module Bolt
98
114
  # This API is used to prepend the project as a module to Puppet's internal
99
115
  # module_references list. CHANGE AT YOUR OWN RISK
100
116
  def to_h
101
- { path: @path, name: name }
117
+ { path: @path.to_s, name: name }
102
118
  end
103
119
 
104
120
  def eql?(other)
@@ -124,11 +140,9 @@ module Bolt
124
140
 
125
141
  def validate
126
142
  if name
127
- name_regex = /^[a-z][a-z0-9_]*$/
128
- if name !~ name_regex
129
- raise Bolt::ValidationError, <<~ERROR_STRING
130
- Invalid project name '#{name}' in bolt-project.yaml; project name must match #{name_regex.inspect}
131
- ERROR_STRING
143
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
144
+ raise Bolt::ValidationError, "Invalid project name #{name.inspect.tr('"', "'")} in bolt-project.yaml; "\
145
+ "project name must match #{Bolt::Module::MODULE_NAME_REGEX.inspect}"
132
146
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
133
147
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
134
148
  "with a built-in Bolt module of the same name."
@@ -147,8 +161,8 @@ module Bolt
147
161
 
148
162
  def check_deprecated_file
149
163
  if (@path + 'project.yaml').file?
150
- logger = Logging.logger[self]
151
- logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
164
+ msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
165
+ Bolt::Logger.deprecation_warning('Using project.yaml instead of bolt-project.yaml', msg)
152
166
  end
153
167
  end
154
168
  end
@@ -96,6 +96,8 @@ module Bolt
96
96
  @http = HTTPClient.new
97
97
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
98
98
  @http.ssl_config.add_trust_ca(@config.cacert)
99
+ @http.connect_timeout = @config.connect_timeout if @config.connect_timeout
100
+ @http.receive_timeout = @config.read_timeout if @config.read_timeout
99
101
 
100
102
  @http
101
103
  end
@@ -132,6 +132,22 @@ module Bolt
132
132
  end
133
133
  end
134
134
 
135
+ def connect_timeout
136
+ validate_timeout('connect_timeout')
137
+ @settings['connect_timeout']
138
+ end
139
+
140
+ def read_timeout
141
+ validate_timeout('read_timeout')
142
+ @settings['read_timeout']
143
+ end
144
+
145
+ def validate_timeout(timeout)
146
+ unless @settings[timeout].nil? || (@settings[timeout].is_a?(Integer) && @settings[timeout] > 0)
147
+ raise Bolt::PuppetDBError, "#{timeout} must be a positive integer, received #{@settings[timeout]}"
148
+ end
149
+ end
150
+
135
151
  def to_hash
136
152
  @settings.dup
137
153
  end
@@ -79,6 +79,13 @@ module Bolt
79
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
80
80
  end
81
81
 
82
+ def self.for_download(target, source, destination, download)
83
+ msg = "Downloaded '#{target.host}:#{source}' to '#{destination}'"
84
+ value = { 'path' => download }
85
+
86
+ new(target, value: value, message: msg, action: 'download', object: source)
87
+ end
88
+
82
89
  # Satisfies the Puppet datatypes API
83
90
  def self.from_asserted_args(target, value)
84
91
  new(target, value: value)
@@ -37,7 +37,7 @@ module Bolt
37
37
  with_tmpdir do |dir|
38
38
  basename = File.basename(source)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
- conn.copy_file(source, tmpfile)
40
+ conn.upload_file(source, tmpfile)
41
41
  # pass over file ownership if we're using run-as to be a different user
42
42
  dir.chown(run_as)
43
43
  result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
@@ -50,6 +50,27 @@ module Bolt
50
50
  end
51
51
  end
52
52
 
53
+ def download(source, destination, options = {})
54
+ running_as(options[:run_as]) do
55
+ # Target OS may be either Unix or Windows. Without knowing the target OS before-hand
56
+ # we can't assume whether the path separator is '/' or '\'. Assume we're connecting
57
+ # to a target with Unix and then check if the path exists after downloading.
58
+ download = File.join(destination, Bolt::Util.unix_basename(source))
59
+
60
+ conn.download_file(source, destination, download)
61
+
62
+ # If the download path doesn't exist, then the file was likely downloaded from Windows
63
+ # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
64
+ # should be saved to the expected location, so update the download path assuming a
65
+ # Windows basename so the result shows the correct local path.
66
+ unless File.exist?(download)
67
+ download = File.join(destination, Bolt::Util.windows_basename(source))
68
+ end
69
+
70
+ Bolt::Result.for_download(target, source, destination, download)
71
+ end
72
+ end
73
+
53
74
  def run_script(script, arguments, options = {})
54
75
  # unpack any Sensitive data
55
76
  arguments = unwrap_sensitive_args(arguments)
@@ -95,7 +116,7 @@ module Bolt
95
116
  task_dir = File.join(dir.to_s, task.tasks_dir)
96
117
  dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
97
118
  extra_files.each do |file|
98
- conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
119
+ conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
99
120
  end
100
121
  end
101
122
 
@@ -257,7 +278,7 @@ module Bolt
257
278
  def write_executable(dir, file, filename = nil)
258
279
  filename ||= File.basename(file)
259
280
  remote_path = File.join(dir.to_s, filename)
260
- conn.copy_file(file, remote_path)
281
+ conn.upload_file(file, remote_path)
261
282
  make_executable(remote_path)
262
283
  remote_path
263
284
  end
@@ -314,7 +335,6 @@ module Bolt
314
335
  sudo_str = if use_sudo
315
336
  sudo_exec = target.options['sudo-executable'] || "sudo"
316
337
  sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
317
- sudo_flags += ["-E"] if options[:environment]
318
338
  Shellwords.shelljoin(sudo_flags)
319
339
  else
320
340
  Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
@@ -85,7 +85,7 @@ module Bolt
85
85
  filename ||= File.basename(file)
86
86
  validate_extensions(File.extname(filename))
87
87
  destination = "#{dir}\\#{filename}"
88
- conn.copy_file(file, destination)
88
+ conn.upload_file(file, destination)
89
89
  destination
90
90
  end
91
91
 
@@ -164,10 +164,16 @@ module Bolt
164
164
  end
165
165
 
166
166
  def upload(source, destination, _options = {})
167
- conn.copy_file(source, destination)
167
+ conn.upload_file(source, destination)
168
168
  Bolt::Result.for_upload(target, source, destination)
169
169
  end
170
170
 
171
+ def download(source, destination, _options = {})
172
+ download = File.join(destination, Bolt::Util.windows_basename(source))
173
+ conn.download_file(source, destination, download)
174
+ Bolt::Result.for_download(target, source, destination, download)
175
+ end
176
+
171
177
  def run_command(command, options = {})
172
178
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
173
179
 
@@ -220,7 +226,7 @@ module Bolt
220
226
  task_dir = File.join(dir, task.tasks_dir)
221
227
  mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
222
228
  extra_files.each do |file|
223
- conn.copy_file(file['path'], File.join(dir, file['name']))
229
+ conn.upload_file(file['path'], File.join(dir, file['name']))
224
230
  end
225
231
  end
226
232
 
@@ -262,7 +268,7 @@ module Bolt
262
268
  return with_tmpdir do |dir|
263
269
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
264
270
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
265
- conn.copy_file(StringIO.new(command), script_file)
271
+ conn.upload_file(StringIO.new(command), script_file)
266
272
  args = escape_arguments([script_file])
267
273
  script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
268
274
  execute(script_invocation)