bolt 2.31.0 → 2.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.

Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +7 -7
  3. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  5. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  13. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  14. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  15. data/guides/logging.txt +18 -0
  16. data/guides/module.txt +19 -0
  17. data/guides/modulepath.txt +25 -0
  18. data/lib/bolt/bolt_option_parser.rb +6 -1
  19. data/lib/bolt/cli.rb +70 -144
  20. data/lib/bolt/config/options.rb +35 -17
  21. data/lib/bolt/config/transport/options.rb +1 -1
  22. data/lib/bolt/error.rb +37 -3
  23. data/lib/bolt/executor.rb +111 -13
  24. data/lib/bolt/inventory/group.rb +2 -1
  25. data/lib/bolt/module_installer.rb +71 -115
  26. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  27. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  28. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  29. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  30. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  31. data/lib/bolt/module_installer/resolver.rb +76 -0
  32. data/lib/bolt/module_installer/specs.rb +93 -0
  33. data/lib/bolt/module_installer/specs/forge_spec.rb +85 -0
  34. data/lib/bolt/module_installer/specs/git_spec.rb +179 -0
  35. data/lib/bolt/outputter.rb +0 -47
  36. data/lib/bolt/outputter/human.rb +23 -11
  37. data/lib/bolt/outputter/json.rb +1 -1
  38. data/lib/bolt/pal.rb +48 -30
  39. data/lib/bolt/pal/yaml_plan.rb +11 -2
  40. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  41. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  42. data/lib/bolt/plan_creator.rb +160 -0
  43. data/lib/bolt/plugin.rb +1 -1
  44. data/lib/bolt/project.rb +5 -10
  45. data/lib/bolt/project_migrator/config.rb +2 -1
  46. data/lib/bolt/project_migrator/inventory.rb +2 -2
  47. data/lib/bolt/project_migrator/modules.rb +10 -8
  48. data/lib/bolt/puppetdb/client.rb +3 -2
  49. data/lib/bolt/puppetdb/config.rb +8 -6
  50. data/lib/bolt/result.rb +23 -11
  51. data/lib/bolt/shell/bash.rb +11 -6
  52. data/lib/bolt/shell/powershell.rb +12 -7
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/base.rb +18 -18
  55. data/lib/bolt/transport/docker.rb +23 -6
  56. data/lib/bolt/transport/orch.rb +23 -19
  57. data/lib/bolt/transport/orch/connection.rb +10 -3
  58. data/lib/bolt/transport/remote.rb +3 -3
  59. data/lib/bolt/transport/simple.rb +6 -6
  60. data/lib/bolt/util.rb +5 -0
  61. data/lib/bolt/version.rb +1 -1
  62. data/lib/bolt/yarn.rb +23 -0
  63. data/lib/bolt_server/file_cache.rb +2 -0
  64. data/lib/bolt_server/schemas/partials/task.json +17 -2
  65. data/lib/bolt_server/transport_app.rb +38 -7
  66. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  67. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  68. data/lib/bolt_spec/plans/mock_executor.rb +9 -6
  69. metadata +25 -8
  70. data/lib/bolt/puppetfile.rb +0 -149
  71. data/lib/bolt/puppetfile/module.rb +0 -93
  72. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -45,6 +45,13 @@ module Bolt
45
45
  used_names = Set.new(@parameters.map(&:name))
46
46
 
47
47
  @steps = plan['steps'].each_with_index.map do |step, index|
48
+ unless step.is_a?(Hash)
49
+ raise Bolt::Error.new(
50
+ "Parse error in step number #{index + 1}: Plan step must be an object with valid step keys.",
51
+ 'bolt/invalid-plan'
52
+ )
53
+ end
54
+
48
55
  # Step keys also aren't allowed to be code and neither is the value of "name"
49
56
  stringified_step = Bolt::Util.walk_keys(step) { |key| stringify(key) }
50
57
  stringified_step['name'] = stringify(stringified_step['name']) if stringified_step.key?('name')
@@ -98,10 +105,12 @@ module Bolt
98
105
  # subclasses this parent class in order to implement its own evaluation
99
106
  # logic.
100
107
  class EvaluableString
101
- attr_reader :value
108
+ attr_reader :file, :line, :value
102
109
 
103
- def initialize(value)
110
+ def initialize(value, file = nil, line = nil)
104
111
  @value = value
112
+ @file = file
113
+ @line = line
105
114
  end
106
115
 
107
116
  def ==(other)
@@ -191,7 +191,11 @@ module Bolt
191
191
  o[key] = evaluate_code_blocks(scope, v)
192
192
  end
193
193
  when EvaluableString
194
- value.evaluate(scope, @evaluator)
194
+ begin
195
+ value.evaluate(scope, @evaluator)
196
+ rescue StandardError => e
197
+ raise format_evaluate_error(e, value)
198
+ end
195
199
  else
196
200
  value
197
201
  end
@@ -203,6 +207,24 @@ module Bolt
203
207
  def evaluate(value, _scope)
204
208
  value
205
209
  end
210
+
211
+ def format_evaluate_error(error, value)
212
+ # The Puppet::PreformattedError includes the line number of the
213
+ # evaluable string that caused the error, while the value includes the
214
+ # line number of the YAML plan that the string began on. To get the
215
+ # actual line number of the error, add these two numbers together.
216
+ line = error.line + value.line
217
+
218
+ # If the evaluable string is not a scalar literal, correct for it
219
+ # being on the same line as the step key.
220
+ line -= 1 if value.is_a?(BareString)
221
+
222
+ Bolt::PlanFailure.new(
223
+ error.basic_message,
224
+ 'bolt/evaluation-error',
225
+ { file: value.file, line: line }
226
+ )
227
+ end
206
228
  end
207
229
  end
208
230
  end
@@ -9,10 +9,15 @@ module Bolt
9
9
  class YamlPlan
10
10
  class Loader
11
11
  class PuppetVisitor < Psych::Visitors::NoAliasRuby
12
- def self.create_visitor
12
+ def initialize(scanner, class_loader, file)
13
+ super(scanner, class_loader)
14
+ @file = file
15
+ end
16
+
17
+ def self.create_visitor(source_ref)
13
18
  class_loader = Psych::ClassLoader::Restricted.new([], [])
14
19
  scanner = Psych::ScalarScanner.new(class_loader)
15
- new(scanner, class_loader)
20
+ new(scanner, class_loader, source_ref)
16
21
  end
17
22
 
18
23
  def deserialize(node)
@@ -23,18 +28,18 @@ module Bolt
23
28
  # @ss is a ScalarScanner, from the base ToRuby visitor class
24
29
  node.value
25
30
  when Psych::Nodes::Scalar::DOUBLE_QUOTED
26
- DoubleQuotedString.new(node.value)
27
- # | style string or > style string
28
- when Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED
29
- CodeLiteral.new(node.value)
30
- # This one shouldn't be possible
31
+ DoubleQuotedString.new(node.value, @file, node.start_line + 1)
32
+ # | style string
33
+ when Psych::Nodes::Scalar::LITERAL
34
+ CodeLiteral.new(node.value, @file, node.start_line + 1)
35
+ # > style string
31
36
  else
32
37
  @ss.tokenize(node.value)
33
38
  end
34
39
  else
35
40
  value = @ss.tokenize(node.value)
36
41
  if value.is_a?(String)
37
- BareString.new(value)
42
+ BareString.new(value, @file, node.start_line + 1)
38
43
  else
39
44
  value
40
45
  end
@@ -50,7 +55,7 @@ module Bolt
50
55
  else
51
56
  Psych.parse(yaml_string, source_ref)
52
57
  end
53
- PuppetVisitor.create_visitor.accept(parse_tree)
58
+ PuppetVisitor.create_visitor(source_ref).accept(parse_tree)
54
59
  end
55
60
 
56
61
  def self.from_string(name, yaml_string, source_ref)
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/logger'
5
+ require 'bolt/module'
6
+ require 'bolt/util'
7
+
8
+ module Bolt
9
+ module PlanCreator
10
+ def self.validate_input(project, plan_name)
11
+ if project.name.nil?
12
+ raise Bolt::Error.new(
13
+ "Project directory '#{project.path}' is not a named project. Unable to create "\
14
+ "a project-level plan. To name a project, set the 'name' key in the 'bolt-project.yaml' "\
15
+ "configuration file.",
16
+ "bolt/unnamed-project-error"
17
+ )
18
+ end
19
+
20
+ if plan_name !~ Bolt::Module::CONTENT_NAME_REGEX
21
+ message = <<~MESSAGE.chomp
22
+ Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
23
+ separated by double colons '::'.
24
+
25
+ Each name segment must begin with a lowercase letter, and may only include lowercase
26
+ letters, digits, and underscores.
27
+
28
+ Examples of valid plan names:
29
+ - #{project.name}
30
+ - #{project.name}::my_plan
31
+ MESSAGE
32
+
33
+ raise Bolt::ValidationError, message
34
+ end
35
+
36
+ prefix, _, basename = segment_plan_name(plan_name)
37
+
38
+ unless prefix == project.name
39
+ message = "First segment of plan name '#{plan_name}' must match project name '#{project.name}'. "\
40
+ "Did you mean '#{project.name}::#{plan_name}'?"
41
+
42
+ raise Bolt::ValidationError, message
43
+ end
44
+
45
+ %w[pp yaml].each do |ext|
46
+ next unless (path = project.plans_path + "#{basename}.#{ext}").exist?
47
+ raise Bolt::Error.new(
48
+ "A plan with the name '#{plan_name}' already exists at '#{path}', nothing to do.",
49
+ 'bolt/existing-plan-error'
50
+ )
51
+ end
52
+ end
53
+
54
+ def self.create_plan(plans_path, plan_name, outputter, is_puppet)
55
+ _, name_segments, basename = segment_plan_name(plan_name)
56
+ dir_path = plans_path.join(*name_segments)
57
+
58
+ begin
59
+ FileUtils.mkdir_p(dir_path)
60
+ rescue Errno::EEXIST => e
61
+ raise Bolt::Error.new(
62
+ "#{e.message}; unable to create plan directory '#{dir_path}'",
63
+ 'bolt/existing-file-error'
64
+ )
65
+ end
66
+
67
+ type = is_puppet ? 'pp' : 'yaml'
68
+ plan_path = dir_path + "#{basename}.#{type}"
69
+ plan_template = is_puppet ? puppet_plan(plan_name) : yaml_plan(plan_name)
70
+
71
+ begin
72
+ File.write(plan_path, plan_template)
73
+ rescue Errno::EACCES => e
74
+ raise Bolt::FileError.new(
75
+ "#{e.message}; unable to create plan",
76
+ plan_path
77
+ )
78
+ end
79
+
80
+ if Bolt::Util.powershell?
81
+ show_command = 'Get-BoltPlan -Name '
82
+ run_command = 'Invoke-BoltPlan -Name '
83
+ else
84
+ show_command = 'bolt plan show'
85
+ run_command = 'bolt plan run'
86
+ end
87
+
88
+ output = <<~OUTPUT
89
+ Created plan '#{plan_name}' at '#{plan_path}'
90
+
91
+ Show this plan with:
92
+ #{show_command} #{plan_name}
93
+ Run this plan with:
94
+ #{run_command} #{plan_name}
95
+ OUTPUT
96
+
97
+ outputter.print_message(output)
98
+ 0
99
+ end
100
+
101
+ def self.segment_plan_name(plan_name)
102
+ prefix, *name_segments, basename = plan_name.split('::')
103
+
104
+ # If the plan name is just the project name, then create an 'init' plan.
105
+ # Otherwise, use the last name segment for the plan's filename.
106
+ basename ||= 'init'
107
+
108
+ [prefix, name_segments, basename]
109
+ end
110
+
111
+ def self.yaml_plan(plan_name)
112
+ <<~YAML
113
+ # This is the structure of a simple plan. To learn more about writing
114
+ # YAML plans, see the documentation: http://pup.pt/bolt-yaml-plans
115
+
116
+ # The description sets the description of the plan that will appear
117
+ # in 'bolt plan show' output.
118
+ description: A plan created with bolt plan new
119
+
120
+ # The parameters key defines the parameters that can be passed to
121
+ # the plan.
122
+ parameters:
123
+ targets:
124
+ type: TargetSpec
125
+ description: A list of targets to run actions on
126
+ default: localhost
127
+
128
+ # The steps key defines the actions the plan will take in order.
129
+ steps:
130
+ - message: Hello from #{plan_name}
131
+ - name: command_step
132
+ command: whoami
133
+ targets: $targets
134
+
135
+ # The return key sets the return value of the plan.
136
+ return: $command_step
137
+ YAML
138
+ end
139
+
140
+ def self.puppet_plan(plan_name)
141
+ <<~PUPPET
142
+ # This is the structure of a simple plan. To learn more about writing
143
+ # Puppet plans, see the documentation: http://pup.pt/bolt-puppet-plans
144
+
145
+ # The summary sets the description of the plan that will appear
146
+ # in 'bolt plan show' output. Bolt uses puppet-strings to parse the
147
+ # summary and parameters from the plan.
148
+ # @summary A plan created with bolt plan new.
149
+ # @param targets The targets to run on.
150
+ plan #{plan_name} (
151
+ TargetSpec $targets = "localhost"
152
+ ) {
153
+ out::message("Hello from #{plan_name}")
154
+ $command_result = run_command('whoami', $targets)
155
+ return $command_result
156
+ }
157
+ PUPPET
158
+ end
159
+ end
160
+ end
@@ -290,7 +290,7 @@ module Bolt
290
290
  begin
291
291
  validate_proc = get_hook(plugin_name, :validate_resolve_reference)
292
292
  rescue PluginError
293
- validate_proc = proc { |*args| }
293
+ validate_proc = proc { |*args| } # Nothing to do
294
294
  end
295
295
 
296
296
  validate_proc.call(reference)
@@ -11,9 +11,9 @@ module Bolt
11
11
  PROJECT_SETTINGS = {
12
12
  "name" => "The name of the project",
13
13
  "plans" => "An array of plan names to show, if they exist in the project."\
14
- "These plans are included in `bolt plan show` output",
14
+ "These plans are included in `bolt plan show` and `Get-BoltPlan` output",
15
15
  "tasks" => "An array of task names to show, if they exist in the project."\
16
- "These tasks are included in `bolt task show` output"
16
+ "These tasks are included in `bolt task show` and `Get-BoltTask` output"
17
17
  }.freeze
18
18
 
19
19
  attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
@@ -210,14 +210,9 @@ module Bolt
210
210
  raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
211
211
  end
212
212
 
213
- @data['modules'].each do |mod|
214
- next if (mod.is_a?(Hash) && mod.key?('name')) || mod.is_a?(String)
215
- raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash with a name key"
216
- end
217
-
218
- unknown_keys = modules.flat_map(&:keys).uniq - %w[name version_requirement]
219
- if unknown_keys.any?
220
- @logs << { warn: "Ignoring unknown keys in module declarations: #{unknown_keys.join(', ')}." }
213
+ @data['modules'].each do |spec|
214
+ next if spec.is_a?(Hash) || spec.is_a?(String)
215
+ raise Bolt::ValidationError, "Module specification #{spec.inspect} must be a hash or string"
221
216
  end
222
217
  end
223
218
  end
@@ -54,10 +54,11 @@ module Bolt
54
54
  @outputter.print_action_step("Renaming bolt.yaml to bolt-project.yaml")
55
55
  FileUtils.mv(config_file, project_file)
56
56
 
57
+ command = Bolt::Util.powershell? ? 'Get-Help about_bolt_project' : 'bolt guide project'
57
58
  @outputter.print_action_step(
58
59
  "Successfully migrated config. Please add a 'name' key to bolt-project.yaml "\
59
60
  "to use project-level tasks and plans. Learn more about projects by running "\
60
- "'bolt guide project'."
61
+ "'#{command}'."
61
62
  )
62
63
 
63
64
  true
@@ -6,12 +6,12 @@ module Bolt
6
6
  class ProjectMigrator
7
7
  class Inventory < Base
8
8
  def migrate(inventory_file, backup_dir)
9
- inventory_1_to_2(inventory_file, backup_dir)
9
+ inventory1to2(inventory_file, backup_dir)
10
10
  end
11
11
 
12
12
  # Migrates an inventory v1 file to inventory v2.
13
13
  #
14
- private def inventory_1_to_2(inventory_file, backup_dir)
14
+ private def inventory1to2(inventory_file, backup_dir)
15
15
  unless File.exist?(inventory_file)
16
16
  return true
17
17
  end
@@ -42,12 +42,14 @@ module Bolt
42
42
  # to the new moduledir.
43
43
  #
44
44
  private def migrate_modules_from_puppetfile(config, puppetfile_path, managed_moduledir, modulepath)
45
- require 'bolt/puppetfile'
46
- require 'bolt/puppetfile/installer'
45
+ require 'bolt/module_installer/installer'
46
+ require 'bolt/module_installer/puppetfile'
47
+ require 'bolt/module_installer/resolver'
48
+ require 'bolt/module_installer/specs'
47
49
 
48
50
  begin
49
51
  @outputter.print_action_step("Parsing Puppetfile at #{puppetfile_path}")
50
- puppetfile = Bolt::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
52
+ puppetfile = Bolt::ModuleInstaller::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
51
53
  rescue Bolt::Error => e
52
54
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
53
55
  return false
@@ -56,14 +58,14 @@ module Bolt
56
58
  # Prompt for direct dependencies
57
59
  modules = select_modules(puppetfile.modules)
58
60
 
59
- # Create new Puppetfile object
60
- puppetfile = Bolt::Puppetfile.new(modules)
61
+ # Create specs to resolve from
62
+ specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
61
63
 
62
64
  # Attempt to resolve dependencies
63
65
  begin
64
66
  @outputter.print_message('')
65
67
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
66
- puppetfile.resolve
68
+ puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
67
69
  rescue Bolt::Error => e
68
70
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
69
71
  return false
@@ -98,7 +100,7 @@ module Bolt
98
100
 
99
101
  # Install Puppetfile
100
102
  @outputter.print_action_step("Syncing modules from #{puppetfile_path} to #{managed_moduledir}")
101
- Bolt::Puppetfile::Installer.new({}).install(puppetfile_path, managed_moduledir)
103
+ Bolt::ModuleInstaller::Installer.new.install(puppetfile_path, managed_moduledir)
102
104
  else
103
105
  @outputter.print_action_step(
104
106
  "Project does not include any managed modules, deleting Puppetfile "\
@@ -123,7 +125,7 @@ module Bolt
123
125
  return modules if all
124
126
 
125
127
  modules.select do |mod|
126
- Bolt::Util.prompt_yes_no("Select #{mod.title}?", @outputter)
128
+ Bolt::Util.prompt_yes_no("Select #{mod.full_name}?", @outputter)
127
129
  end
128
130
  end
129
131
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'json'
4
4
  require 'logging'
5
- require 'uri'
6
5
 
7
6
  module Bolt
8
7
  module PuppetDB
@@ -108,13 +107,15 @@ module Bolt
108
107
  end
109
108
 
110
109
  def uri
110
+ require 'addressable/uri'
111
+
111
112
  @current_url ||= (@config.server_urls - @bad_urls).first
112
113
  unless @current_url
113
114
  msg = "Failed to connect to all PuppetDB server_urls: #{@config.server_urls.to_a.join(', ')}."
114
115
  raise Bolt::PuppetDBError, msg
115
116
  end
116
117
 
117
- uri = URI.parse(@current_url)
118
+ uri = Addressable::URI.parse(@current_url)
118
119
  uri.port ||= 8081
119
120
  uri
120
121
  end
@@ -6,14 +6,14 @@ require 'bolt/util'
6
6
  module Bolt
7
7
  module PuppetDB
8
8
  class Config
9
- if !ENV['HOME'].nil?
10
- DEFAULT_TOKEN = File.expand_path('~/.puppetlabs/token')
11
- DEFAULT_CONFIG = { user: File.expand_path('~/.puppetlabs/client-tools/puppetdb.conf'),
12
- global: '/etc/puppetlabs/client-tools/puppetdb.conf' }.freeze
13
- else
9
+ if ENV['HOME'].nil?
14
10
  DEFAULT_TOKEN = Bolt::Util.windows? ? 'nul' : '/dev/null'
15
11
  DEFAULT_CONFIG = { user: '/etc/puppetlabs/puppet/puppetdb.conf',
16
12
  global: '/etc/puppetlabs/puppet/puppetdb.conf' }.freeze
13
+ else
14
+ DEFAULT_TOKEN = File.expand_path('~/.puppetlabs/token')
15
+ DEFAULT_CONFIG = { user: File.expand_path('~/.puppetlabs/client-tools/puppetdb.conf'),
16
+ global: '/etc/puppetlabs/client-tools/puppetdb.conf' }.freeze
17
17
 
18
18
  end
19
19
 
@@ -89,6 +89,8 @@ module Bolt
89
89
 
90
90
  def uri
91
91
  return @uri if @uri
92
+ require 'addressable/uri'
93
+
92
94
  uri = case @settings['server_urls']
93
95
  when String
94
96
  @settings['server_urls']
@@ -100,7 +102,7 @@ module Bolt
100
102
  raise Bolt::PuppetDBError, "server_urls must be a string or array"
101
103
  end
102
104
 
103
- @uri = URI.parse(uri)
105
+ @uri = Addressable::URI.parse(uri)
104
106
  @uri.port ||= 8081
105
107
  @uri
106
108
  end