bolt 3.1.0 → 3.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +8 -8
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -5
  6. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  7. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
  8. data/lib/bolt/apply_result.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +6 -3
  10. data/lib/bolt/cli.rb +37 -12
  11. data/lib/bolt/config.rb +4 -0
  12. data/lib/bolt/config/options.rb +21 -3
  13. data/lib/bolt/config/transport/lxd.rb +21 -0
  14. data/lib/bolt/config/transport/options.rb +1 -1
  15. data/lib/bolt/executor.rb +10 -3
  16. data/lib/bolt/logger.rb +8 -0
  17. data/lib/bolt/module_installer.rb +2 -2
  18. data/lib/bolt/module_installer/puppetfile.rb +2 -2
  19. data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
  20. data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
  21. data/lib/bolt/outputter/human.rb +47 -12
  22. data/lib/bolt/pal.rb +2 -2
  23. data/lib/bolt/pal/yaml_plan.rb +1 -2
  24. data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
  25. data/lib/bolt/pal/yaml_plan/step.rb +91 -31
  26. data/lib/bolt/pal/yaml_plan/step/command.rb +16 -16
  27. data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
  28. data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
  29. data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
  30. data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
  31. data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
  32. data/lib/bolt/pal/yaml_plan/step/script.rb +32 -17
  33. data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
  34. data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  36. data/lib/bolt/plan_creator.rb +1 -1
  37. data/lib/bolt/project_manager.rb +1 -1
  38. data/lib/bolt/project_manager/module_migrator.rb +1 -1
  39. data/lib/bolt/shell.rb +16 -0
  40. data/lib/bolt/shell/bash.rb +48 -21
  41. data/lib/bolt/shell/bash/tmpdir.rb +2 -2
  42. data/lib/bolt/shell/powershell.rb +24 -5
  43. data/lib/bolt/task.rb +1 -1
  44. data/lib/bolt/transport/lxd.rb +26 -0
  45. data/lib/bolt/transport/lxd/connection.rb +99 -0
  46. data/lib/bolt/transport/ssh/connection.rb +1 -1
  47. data/lib/bolt/transport/winrm/connection.rb +1 -1
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/transport_app.rb +13 -1
  50. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  51. data/lib/bolt_spec/plans/mock_executor.rb +4 -0
  52. metadata +5 -2
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class LXD < Base
10
+ OPTIONS = %w[
11
+ cleanup
12
+ tmpdir
13
+ ].freeze
14
+
15
+ DEFAULTS = {
16
+ 'cleanup' => true
17
+ }.freeze
18
+ end
19
+ end
20
+ end
21
+ end
@@ -32,7 +32,7 @@ module Bolt
32
32
  "cleanup" => {
33
33
  type: [TrueClass, FalseClass],
34
34
  description: "Whether to clean up temporary files created on targets. When running commands on a target, "\
35
- "Bolt may create temporary files. After completing the command, these files are "\
35
+ "Bolt might create temporary files. After completing the command, these files are "\
36
36
  "automatically deleted. This value can be set to 'false' if you wish to leave these "\
37
37
  "temporary files on the target.",
38
38
  _plugin: true,
data/lib/bolt/executor.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'English'
5
5
  require 'json'
6
6
  require 'logging'
7
+ require 'pathname'
7
8
  require 'set'
8
9
  require 'bolt/analytics'
9
10
  require 'bolt/result'
@@ -15,6 +16,7 @@ require 'bolt/transport/ssh'
15
16
  require 'bolt/transport/winrm'
16
17
  require 'bolt/transport/orch'
17
18
  require 'bolt/transport/local'
19
+ require 'bolt/transport/lxd'
18
20
  require 'bolt/transport/docker'
19
21
  require 'bolt/transport/remote'
20
22
  require 'bolt/yarn'
@@ -25,6 +27,7 @@ module Bolt
25
27
  winrm: Bolt::Transport::WinRM,
26
28
  pcp: Bolt::Transport::Orch,
27
29
  local: Bolt::Transport::Local,
30
+ lxd: Bolt::Transport::LXD,
28
31
  docker: Bolt::Transport::Docker,
29
32
  remote: Bolt::Transport::Remote
30
33
  }.freeze
@@ -39,7 +42,6 @@ module Bolt
39
42
  modified_concurrency = false)
40
43
  # lazy-load expensive gem code
41
44
  require 'concurrent'
42
-
43
45
  @analytics = analytics
44
46
  @logger = Bolt::Logger.logger(self)
45
47
 
@@ -121,8 +123,8 @@ module Bolt
121
123
  def queue_execute(targets)
122
124
  if @warn_concurrency && targets.length > @concurrency
123
125
  @warn_concurrency = false
124
- msg = "The ulimit is low, which may cause file limit issues. Default concurrency has been set to "\
125
- "'#{@concurrency}' to mitigate those issues, which may cause Bolt to run slow. "\
126
+ msg = "The ulimit is low, which might cause file limit issues. Default concurrency has been set to "\
127
+ "'#{@concurrency}' to mitigate those issues, which might cause Bolt to run slow. "\
126
128
  "Disable this warning by configuring ulimit using 'ulimit -n <limit>' in your shell "\
127
129
  "configuration, or by configuring Bolt's concurrency. "\
128
130
  "See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
@@ -231,6 +233,11 @@ module Bolt
231
233
  @analytics.report_bundled_content(mode, name)
232
234
  end
233
235
 
236
+ def report_file_source(plan_function, source)
237
+ label = Pathname.new(source).absolute? ? 'absolute' : 'module'
238
+ @analytics&.event('Plan', plan_function, label: label)
239
+ end
240
+
234
241
  def report_apply(statement_count, resource_counts)
235
242
  data = { statement_count: statement_count }
236
243
 
data/lib/bolt/logger.rb CHANGED
@@ -91,6 +91,14 @@ module Bolt
91
91
  Logging.logger[:root].appenders.any?
92
92
  end
93
93
 
94
+ def self.stream
95
+ @stream
96
+ end
97
+
98
+ def self.stream=(stream)
99
+ @stream = stream
100
+ end
101
+
94
102
  # A helper to ensure the Logging library is always initialized with our
95
103
  # custom log levels before retrieving a Logger instance.
96
104
  def self.logger(name)
@@ -45,7 +45,7 @@ module Bolt
45
45
  # specss. If that fails, fall back to resolving from project specs.
46
46
  # This prevents Bolt from modifying installed modules unless there is
47
47
  # a version conflict.
48
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
48
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
49
49
 
50
50
  @outputter.start_spin
51
51
  begin
@@ -156,7 +156,7 @@ module Bolt
156
156
  # If forcibly installing or if there is no Puppetfile, resolve
157
157
  # and write a Puppetfile.
158
158
  if force || !path.exist?
159
- @outputter.print_action_step("Resolving module dependencies, this may take a moment")
159
+ @outputter.print_action_step("Resolving module dependencies, this might take a moment")
160
160
 
161
161
  # This doesn't use the block as it's more testable to just mock *_spin
162
162
  @outputter.start_spin
@@ -36,7 +36,7 @@ module Bolt
36
36
  raise Bolt::ValidationError, <<~MSG
37
37
  Unable to parse Puppetfile #{path}:
38
38
  #{parsed.validation_errors.join("\n\n")}.
39
- This may not be a Puppetfile managed by Bolt.
39
+ This Puppetfile might not be managed by Bolt.
40
40
  MSG
41
41
  end
42
42
 
@@ -106,7 +106,7 @@ module Bolt
106
106
 
107
107
  #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
108
 
109
- This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
109
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
110
  Puppetfile, run '#{command}'.
111
111
  MESSAGE
112
112
 
@@ -39,8 +39,8 @@ module Bolt
39
39
  unless (match = name.match(NAME_REGEX))
40
40
  raise Bolt::ValidationError,
41
41
  "Invalid name for Forge module specification: #{name}. Name must match "\
42
- "'owner/name'. Owner segment may only include letters or digits. Name "\
43
- "segment must start with a lowercase letter and may only include lowercase "\
42
+ "'owner/name'. Owner segment can only include letters or digits. Name "\
43
+ "segment must start with a lowercase letter and can only include lowercase "\
44
44
  "letters, digits, and underscores."
45
45
  end
46
46
 
@@ -49,8 +49,8 @@ module Bolt
49
49
  unless (match = name.match(NAME_REGEX))
50
50
  raise Bolt::ValidationError,
51
51
  "Invalid name for Git module specification: #{name}. Name must match "\
52
- "'name' or 'owner/name'. Owner segment may only include letters or digits. "\
53
- "Name segment must start with a lowercase letter and may only include "\
52
+ "'name' or 'owner/name'. Owner segment can only include letters or digits. "\
53
+ "Name segment must start with a lowercase letter and can only include "\
54
54
  "lowercase letters, digits, and underscores."
55
55
  end
56
56
 
@@ -53,10 +53,21 @@ module Bolt
53
53
  string.sub(/\s\z/, '')
54
54
  end
55
55
 
56
+ # Wraps a string to the specified width. Lines only wrap
57
+ # at whitespace.
58
+ #
56
59
  def wrap(string, width = 80)
60
+ return string unless string.is_a?(String)
57
61
  string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
58
62
  end
59
63
 
64
+ # Trims a string to a specified width, adding an ellipsis if it's longer.
65
+ #
66
+ def truncate(string, width = 80)
67
+ return string unless string.is_a?(String) && string.length > width
68
+ string.lines.first[0...width].gsub(/\s\w+\s*$/, '...')
69
+ end
70
+
60
71
  def handle_event(event)
61
72
  case event[:type]
62
73
  when :enable_default_output
@@ -218,11 +229,11 @@ module Bolt
218
229
  @stream.puts total_msg
219
230
  end
220
231
 
221
- def print_table(results, padding_left = 0, padding_right = 3)
232
+ def format_table(results, padding_left = 0, padding_right = 3)
222
233
  # lazy-load expensive gem code
223
234
  require 'terminal-table'
224
235
 
225
- @stream.puts Terminal::Table.new(
236
+ Terminal::Table.new(
226
237
  rows: results,
227
238
  style: {
228
239
  border_x: '',
@@ -238,10 +249,22 @@ module Bolt
238
249
 
239
250
  def print_tasks(tasks, modulepath)
240
251
  command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
241
- tasks.any? ? print_table(tasks) : print_message('No available tasks')
242
- print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
243
- "\nUse '#{command}' to view "\
244
- "details and parameters for a specific task.")
252
+
253
+ tasks = tasks.map do |name, description|
254
+ description = truncate(description, 72)
255
+ [name, description]
256
+ end
257
+
258
+ @stream.puts colorize(:cyan, 'Tasks')
259
+ @stream.puts tasks.any? ? format_table(tasks, 2) : indent(2, 'No available tasks')
260
+ @stream.puts
261
+
262
+ @stream.puts colorize(:cyan, 'Modulepath')
263
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
264
+ @stream.puts
265
+
266
+ @stream.puts colorize(:cyan, 'Additional information')
267
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
245
268
  end
246
269
 
247
270
  # @param [Hash] task A hash representing the task
@@ -322,10 +345,22 @@ module Bolt
322
345
 
323
346
  def print_plans(plans, modulepath)
324
347
  command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
325
- plans.any? ? print_table(plans) : print_message('No available plans')
326
- print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
327
- "\nUse '#{command}' to view "\
328
- "details and parameters for a specific plan.")
348
+
349
+ plans = plans.map do |name, description|
350
+ description = truncate(description, 72)
351
+ [name, description]
352
+ end
353
+
354
+ @stream.puts colorize(:cyan, 'Plans')
355
+ @stream.puts plans.any? ? format_table(plans, 2) : indent(2, 'No available plans')
356
+ @stream.puts
357
+
358
+ @stream.puts colorize(:cyan, 'Modulepath')
359
+ @stream.puts indent(2, modulepath.join(File::PATH_SEPARATOR))
360
+ @stream.puts
361
+
362
+ @stream.puts colorize(:cyan, 'Additional information')
363
+ @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
329
364
  end
330
365
 
331
366
  def print_topics(topics)
@@ -359,7 +394,7 @@ module Bolt
359
394
  [m[:name], version]
360
395
  end
361
396
 
362
- print_table(module_info, 2, 1)
397
+ @stream.puts format_table(module_info, 2, 1)
363
398
  end
364
399
 
365
400
  @stream.write("\n")
@@ -374,7 +409,7 @@ module Bolt
374
409
  targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
375
410
 
376
411
  if targets.any?
377
- print_table(targets, 0, 2)
412
+ @stream.puts format_table(targets, 0, 2)
378
413
  @stream.puts
379
414
  end
380
415
 
data/lib/bolt/pal.rb CHANGED
@@ -385,7 +385,7 @@ module Bolt
385
385
  plan_cache[plan_name] = info
386
386
  end
387
387
 
388
- list << [plan_name] unless info['private']
388
+ list << [plan_name, info['description']] unless info['private']
389
389
  end
390
390
 
391
391
  File.write(@project.plan_cache_file, plan_cache.to_json) if updated
@@ -446,7 +446,7 @@ module Bolt
446
446
  params[name] = { 'type' => param.types.first }
447
447
  params[name]['sensitive'] = param.types.first =~ /\ASensitive(\[.*\])?\z/ ? true : false
448
448
  params[name]['default_value'] = defaults[name] if defaults.key?(name)
449
- params[name]['description'] = param.text unless param.text.empty?
449
+ params[name]['description'] = param.text if param.text && !param.text.empty?
450
450
  else
451
451
  Bolt::Logger.warn(
452
452
  "missing_plan_parameter",
@@ -73,8 +73,7 @@ module Bolt
73
73
  def duplicate_check(used_names, name, step_number)
74
74
  if used_names.include?(name)
75
75
  error_message = "Duplicate step name or parameter detected: #{name.inspect}"
76
- err = Step.step_error(error_message, name, step_number)
77
- raise Bolt::Error.new(err, "bolt/invalid-plan")
76
+ raise Step::StepError.new(error_message, name, step_number)
78
77
  end
79
78
  end
80
79
 
@@ -12,153 +12,15 @@ module Bolt
12
12
  @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
13
  end
14
14
 
15
- def dispatch_step(scope, step)
16
- step_body = evaluate_code_blocks(scope, step.body)
17
-
18
- # Dispatch based on the step class name
19
- step_type = step.class.name.split('::').last.downcase
20
- method = "#{step_type}_step"
21
-
22
- send(method, scope, step_body)
23
- end
24
-
25
- def task_step(scope, step)
26
- task = step['task']
27
- targets = step['targets']
28
- description = step['description']
29
- params = step['parameters'] || {}
30
-
31
- args = if description
32
- [task, targets, description, params]
33
- else
34
- [task, targets, params]
35
- end
36
-
37
- scope.call_function('run_task', args)
38
- end
39
-
40
- def plan_step(scope, step)
41
- plan = step['plan']
42
- parameters = step['parameters'] || {}
43
-
44
- args = [plan, parameters]
45
-
46
- scope.call_function('run_plan', args)
47
- end
48
-
49
- def script_step(scope, step)
50
- script = step['script']
51
- targets = step['targets']
52
- description = step['description']
53
- arguments = step['arguments'] || []
54
-
55
- options = { 'arguments' => arguments }
56
- args = if description
57
- [script, targets, description, options]
58
- else
59
- [script, targets, options]
60
- end
61
-
62
- scope.call_function('run_script', args)
63
- end
64
-
65
- def command_step(scope, step)
66
- command = step['command']
67
- targets = step['targets']
68
- description = step['description']
69
-
70
- args = [command, targets]
71
- args << description if description
72
- scope.call_function('run_command', args)
73
- end
74
-
75
- def upload_step(scope, step)
76
- source = step['upload']
77
- destination = step['destination']
78
- targets = step['targets']
79
- description = step['description']
80
-
81
- args = [source, destination, targets]
82
- args << description if description
83
- scope.call_function('upload_file', args)
84
- end
85
-
86
- def download_step(scope, step)
87
- source = step['download']
88
- destination = step['destination']
89
- targets = step['targets']
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
-
97
- def eval_step(_scope, step)
98
- step['eval']
99
- end
100
-
101
- def resources_step(scope, step)
102
- targets = step['targets']
103
-
104
- # TODO: Only call apply_prep when needed
105
- scope.call_function('apply_prep', targets)
106
- manifest = generate_manifest(step['resources'])
107
-
108
- apply_manifest(scope, targets, manifest)
109
- end
110
-
111
- def message_step(scope, step)
112
- scope.call_function('out::message', [step['message']])
113
- end
114
-
115
- def generate_manifest(resources)
116
- # inspect returns the Ruby representation of the resource hashes,
117
- # which happens to be the same as the Puppet representation
118
- puppet_resources = resources.inspect
119
-
120
- # Because the :tasks setting globally controls which mode the parser
121
- # is in, we need to make this snippet of non-tasks manifest code
122
- # parseable in tasks mode. The way to do that is by putting it in an
123
- # apply statement and taking the body.
124
- <<~MANIFEST
125
- apply('placeholder') {
126
- $resources = #{puppet_resources}
127
- $resources.each |$res| {
128
- Resource[$res['type']] { $res['title']:
129
- * => $res['parameters'],
130
- }
131
- }
132
-
133
- # Add relationships if there is more than one resource
134
- if $resources.length > 1 {
135
- ($resources.length - 1).each |$index| {
136
- $lhs = $resources[$index]
137
- $rhs = $resources[$index+1]
138
- $lhs_resource = Resource[$lhs['type'] , $lhs['title']]
139
- $rhs_resource = Resource[$rhs['type'] , $rhs['title']]
140
- $lhs_resource -> $rhs_resource
141
- }
142
- }
143
- }
144
- MANIFEST
145
- end
146
-
147
- def apply_manifest(scope, targets, manifest)
148
- ast = @evaluator.parse_string(manifest)
149
- apply_block = ast.body.body
150
- applicator = Puppet.lookup(:apply_executor)
151
- applicator.apply([targets], apply_block, scope)
152
- end
153
-
154
15
  # This is the method that Puppet calls to evaluate the plan. The name
155
16
  # makes more sense for .pp plans.
17
+ #
156
18
  def evaluate_block_with_bindings(closure_scope, args_hash, plan)
157
19
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
158
20
  plan.steps.each do |step|
159
- step_result = dispatch_step(scope, step)
21
+ step_result = step.evaluate(scope, self)
160
22
 
161
- scope.setvar(step.name, step_result) if step.name
23
+ scope.setvar(step.body['name'], step_result) if step.body['name']
162
24
  end
163
25
 
164
26
  evaluate_code_blocks(scope, plan.return)
@@ -168,6 +30,7 @@ module Bolt
168
30
  end
169
31
 
170
32
  # Recursively evaluate any EvaluableString instances in the object.
33
+ #
171
34
  def evaluate_code_blocks(scope, value)
172
35
  # XXX We should establish a local scope here probably
173
36
  case value
@@ -192,6 +55,7 @@ module Bolt
192
55
  # Occasionally the Closure will ask us to evaluate what it assumes are
193
56
  # AST objects. Because we've sidestepped the AST, they aren't, so just
194
57
  # return the values as already evaluated.
58
+ #
195
59
  def evaluate(value, _scope)
196
60
  value
197
61
  end