bolt 2.32.0 → 2.36.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +6 -6
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  13. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  14. data/guides/logging.txt +18 -0
  15. data/lib/bolt/analytics.rb +27 -8
  16. data/lib/bolt/apply_result.rb +3 -3
  17. data/lib/bolt/bolt_option_parser.rb +43 -15
  18. data/lib/bolt/cli.rb +79 -227
  19. data/lib/bolt/config.rb +131 -52
  20. data/lib/bolt/config/options.rb +46 -8
  21. data/lib/bolt/config/transport/base.rb +10 -19
  22. data/lib/bolt/config/transport/local.rb +0 -7
  23. data/lib/bolt/config/transport/options.rb +1 -1
  24. data/lib/bolt/config/transport/ssh.rb +8 -14
  25. data/lib/bolt/config/validator.rb +231 -0
  26. data/lib/bolt/error.rb +37 -3
  27. data/lib/bolt/executor.rb +103 -17
  28. data/lib/bolt/inventory/group.rb +2 -1
  29. data/lib/bolt/module_installer.rb +2 -1
  30. data/lib/bolt/module_installer/specs/forge_spec.rb +5 -4
  31. data/lib/bolt/module_installer/specs/git_spec.rb +4 -3
  32. data/lib/bolt/outputter/human.rb +21 -9
  33. data/lib/bolt/outputter/rainbow.rb +1 -1
  34. data/lib/bolt/pal.rb +48 -30
  35. data/lib/bolt/pal/yaml_plan.rb +11 -2
  36. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  37. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  38. data/lib/bolt/plan_creator.rb +160 -0
  39. data/lib/bolt/plugin.rb +1 -8
  40. data/lib/bolt/project.rb +30 -36
  41. data/lib/bolt/project_manager.rb +199 -0
  42. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +43 -5
  43. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +5 -5
  44. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  45. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  46. data/lib/bolt/puppetdb/client.rb +3 -2
  47. data/lib/bolt/puppetdb/config.rb +9 -8
  48. data/lib/bolt/result.rb +23 -11
  49. data/lib/bolt/shell/bash.rb +12 -7
  50. data/lib/bolt/shell/powershell.rb +12 -7
  51. data/lib/bolt/task/run.rb +1 -1
  52. data/lib/bolt/transport/base.rb +18 -18
  53. data/lib/bolt/transport/docker.rb +23 -6
  54. data/lib/bolt/transport/orch.rb +23 -19
  55. data/lib/bolt/transport/orch/connection.rb +10 -3
  56. data/lib/bolt/transport/remote.rb +3 -3
  57. data/lib/bolt/transport/simple.rb +6 -6
  58. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  59. data/lib/bolt/util.rb +19 -7
  60. data/lib/bolt/version.rb +1 -1
  61. data/lib/bolt/yarn.rb +23 -0
  62. data/lib/bolt_server/base_config.rb +3 -1
  63. data/lib/bolt_server/config.rb +3 -1
  64. data/lib/bolt_server/file_cache.rb +2 -0
  65. data/lib/bolt_server/schemas/partials/task.json +2 -2
  66. data/lib/bolt_server/transport_app.rb +42 -11
  67. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  68. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  69. data/lib/bolt_spec/plans/mock_executor.rb +9 -6
  70. data/libexec/apply_catalog.rb +1 -1
  71. data/libexec/custom_facts.rb +1 -1
  72. data/libexec/query_resources.rb +1 -1
  73. metadata +12 -14
  74. data/lib/bolt/project_migrator.rb +0 -80
  75. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -241,10 +241,11 @@ module Bolt
241
241
  end
242
242
 
243
243
  if input.key?('nodes')
244
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
244
245
  msg = <<~MSG.chomp
245
246
  Found 'nodes' key in group #{@name}. This looks like a v1 inventory file, which is
246
247
  no longer supported by Bolt. Migrate to a v2 inventory file automatically using
247
- 'bolt project migrate'.
248
+ '#{command}'.
248
249
  MSG
249
250
  raise ValidationError.new(msg, nil)
250
251
  end
@@ -63,7 +63,7 @@ module Bolt
63
63
 
64
64
  data = Bolt::Util.read_yaml_hash(config_path, 'project')
65
65
  data['modules'] ||= []
66
- data['modules'] << name
66
+ data['modules'] << name.tr('-', '/')
67
67
 
68
68
  begin
69
69
  File.write(config_path, data.to_yaml)
@@ -187,6 +187,7 @@ module Bolt
187
187
  ok = Installer.new(config).install(path, moduledir)
188
188
 
189
189
  # Automatically generate types after installing modules
190
+ @outputter.print_action_step("Generating type references")
190
191
  @pal.generate_types
191
192
 
192
193
  @outputter.print_puppetfile_result(ok, path, moduledir)
@@ -11,7 +11,7 @@ module Bolt
11
11
  class ModuleInstaller
12
12
  class Specs
13
13
  class ForgeSpec
14
- NAME_REGEX = %r{\A[a-z][a-z0-9_]*[-/](?<name>[a-z][a-z0-9_]*)\z}.freeze
14
+ NAME_REGEX = %r{\A[a-zA-Z0-9]+[-/](?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[name]).freeze
16
16
  KNOWN_KEYS = Set.new(%w[name version_requirement]).freeze
17
17
 
@@ -33,8 +33,9 @@ module Bolt
33
33
  unless (match = name.match(NAME_REGEX))
34
34
  raise Bolt::ValidationError,
35
35
  "Invalid name for Forge module specification: #{name}. Name must match "\
36
- "'owner/name', must start with a lowercase letter, and may only include "\
37
- "lowercase letters, digits, and underscores."
36
+ "'owner/name'. Owner segment may only include letters or digits. Name "\
37
+ "segment must start with a lowercase letter and may only include lowercase "\
38
+ "letters, digits, and underscores."
38
39
  end
39
40
 
40
41
  [name.tr('-', '/'), match[:name]]
@@ -54,7 +55,7 @@ module Bolt
54
55
  #
55
56
  def satisfied_by?(mod)
56
57
  @type == mod.type &&
57
- @full_name == mod.full_name &&
58
+ @full_name.downcase == mod.full_name.downcase &&
58
59
  !mod.version.nil? &&
59
60
  @semantic_version.cover?(mod.version)
60
61
  end
@@ -11,7 +11,7 @@ module Bolt
11
11
  class ModuleInstaller
12
12
  class Specs
13
13
  class GitSpec
14
- NAME_REGEX = %r{\A(?:[a-z][a-z0-9_]*[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
14
+ NAME_REGEX = %r{\A(?:[a-zA-Z0-9]+[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[git ref]).freeze
16
16
 
17
17
  attr_reader :git, :ref, :type
@@ -36,8 +36,9 @@ module Bolt
36
36
  unless (match = name.match(NAME_REGEX))
37
37
  raise Bolt::ValidationError,
38
38
  "Invalid name for Git module specification: #{name}. Name must match "\
39
- "'name' or 'owner/name', must start with a lowercase letter, and may "\
40
- "only include lowercase letters, digits, and underscores."
39
+ "'name' or 'owner/name'. Owner segment may only include letters or digits. "\
40
+ "Name segment must start with a lowercase letter and may only include "\
41
+ "lowercase letters, digits, and underscores."
41
42
  end
42
43
 
43
44
  match[:name]
@@ -214,9 +214,10 @@ module Bolt
214
214
  end
215
215
 
216
216
  def print_tasks(tasks, modulepath)
217
- print_table(tasks)
217
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
218
+ tasks.any? ? print_table(tasks) : print_message('No available tasks')
218
219
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
219
- "\nUse `bolt task show <task-name>` to view "\
220
+ "\nUse '#{command}' to view "\
220
221
  "details and parameters for a specific task.")
221
222
  end
222
223
 
@@ -225,20 +226,26 @@ module Bolt
225
226
  # Building lots of strings...
226
227
  pretty_params = +""
227
228
  task_info = +""
228
- usage = +"bolt task run --targets <node-name> #{task.name}"
229
+ usage = if Bolt::Util.powershell?
230
+ +"Invoke-BoltTask -Name #{task.name} -Targets <targets>"
231
+ else
232
+ +"bolt task run #{task.name} --targets <targets>"
233
+ end
229
234
 
230
235
  task.parameters&.each do |k, v|
231
236
  pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
232
237
  pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
233
238
  pretty_params << " #{v['description']}\n" if v['description']
234
- usage << if v['type'].is_a?(Puppet::Pops::Types::POptionalType)
239
+ usage << if v['type'].start_with?("Optional")
235
240
  " [#{k}=<value>]"
236
241
  else
237
242
  " #{k}=<value>"
238
243
  end
239
244
  end
240
245
 
241
- usage << " [--noop]" if task.supports_noop
246
+ if task.supports_noop
247
+ usage << Bolt::Util.powershell? ? '[-Noop]' : '[--noop]'
248
+ end
242
249
 
243
250
  task_info << "\n#{task.name}"
244
251
  task_info << " - #{task.description}" if task.description
@@ -261,7 +268,11 @@ module Bolt
261
268
  # Building lots of strings...
262
269
  pretty_params = +""
263
270
  plan_info = +""
264
- usage = +"bolt plan run #{plan['name']}"
271
+ usage = if Bolt::Util.powershell?
272
+ +"Invoke-BoltPlan -Name #{plan['name']}"
273
+ else
274
+ +"bolt plan run #{plan['name']}"
275
+ end
265
276
 
266
277
  plan['parameters'].each do |name, p|
267
278
  pretty_params << "- #{name}: #{p['type']}\n"
@@ -287,16 +298,17 @@ module Bolt
287
298
  end
288
299
 
289
300
  def print_plans(plans, modulepath)
290
- print_table(plans)
301
+ command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
302
+ plans.any? ? print_table(plans) : print_message('No available plans')
291
303
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
292
- "\nUse `bolt plan show <plan-name>` to view "\
304
+ "\nUse '#{command}' to view "\
293
305
  "details and parameters for a specific plan.")
294
306
  end
295
307
 
296
308
  def print_topics(topics)
297
309
  print_message("Available topics are:")
298
310
  print_message(topics.join("\n"))
299
- print_message("\nUse `bolt guide <topic>` to view a specific guide.")
311
+ print_message("\nUse 'bolt guide <TOPIC>' to view a specific guide.")
300
312
  end
301
313
 
302
314
  def print_guide(guide, _topic)
@@ -53,7 +53,7 @@ module Bolt
53
53
  @state = :normal if c == 'm'
54
54
  end
55
55
  end
56
- a.join('')
56
+ a.join
57
57
  else
58
58
  "\033[#{COLORS[color]}m#{string}\033[0m"
59
59
  end
@@ -14,17 +14,11 @@ module Bolt
14
14
  # Bolt::Errors
15
15
  class PALError < Bolt::Error
16
16
  def self.from_preformatted_error(err)
17
- if err.cause.is_a? Bolt::Error
18
- err.cause
19
- else
20
- from_error(err)
21
- end
22
- end
23
-
24
- # Generate a Bolt::Pal::PALError for non-bolt errors
25
- def self.from_error(err)
26
- # Use the original error message if available
27
- message = err.cause ? err.cause.message : err.message
17
+ error = if err.cause.is_a? Bolt::Error
18
+ err.cause
19
+ else
20
+ from_error(err)
21
+ end
28
22
 
29
23
  # Provide the location of an error if it came from a plan
30
24
  details = {}
@@ -32,8 +26,15 @@ module Bolt
32
26
  details[:line] = err.line if defined?(err.line)
33
27
  details[:column] = err.pos if defined?(err.pos)
34
28
 
35
- e = new(message, details.compact)
29
+ error.add_filelineno(details.compact)
30
+ error
31
+ end
36
32
 
33
+ # Generate a Bolt::Pal::PALError for non-bolt errors
34
+ def self.from_error(err)
35
+ # Use the original error message if available
36
+ message = err.cause ? err.cause.message : err.message
37
+ e = new(message)
37
38
  e.set_backtrace(err.backtrace)
38
39
  e
39
40
  end
@@ -256,19 +257,24 @@ module Bolt
256
257
 
257
258
  # TODO: PUP-8553 should replace this
258
259
  def with_puppet_settings
259
- Dir.mktmpdir('bolt') do |dir|
260
- cli = []
261
- Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
262
- cli << "--#{setting}" << dir
263
- end
264
- Puppet.settings.send(:clear_everything_for_tests)
265
- Puppet.initialize_settings(cli)
266
- Puppet::GettextConfig.create_default_text_domain
267
- Puppet[:trusted_external_command] = @trusted_external
268
- Puppet.settings[:hiera_config] = @hiera_config
269
- self.class.configure_logging
270
- yield
260
+ dir = Dir.mktmpdir('bolt')
261
+
262
+ cli = []
263
+ Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
264
+ cli << "--#{setting}" << dir
271
265
  end
266
+ Puppet.settings.send(:clear_everything_for_tests)
267
+ Puppet.initialize_settings(cli)
268
+ Puppet::GettextConfig.create_default_text_domain
269
+ Puppet[:trusted_external_command] = @trusted_external
270
+ Puppet.settings[:hiera_config] = @hiera_config
271
+ self.class.configure_logging
272
+ yield
273
+ ensure
274
+ # Delete the tmpdir if it still exists. This check is needed to
275
+ # prevent Bolt from erroring if the tmpdir is somehow deleted
276
+ # before reaching this point.
277
+ FileUtils.remove_entry_secure(dir) if File.exist?(dir)
272
278
  end
273
279
 
274
280
  # Parses a snippet of Puppet manifest code and returns the AST represented
@@ -280,15 +286,26 @@ module Bolt
280
286
  raise Bolt::PAL::PALError, "Failed to parse manifest: #{e}"
281
287
  end
282
288
 
283
- def list_tasks
289
+ # Filters content by a list of names and glob patterns specified in project
290
+ # configuration.
291
+ def filter_content(content, patterns)
292
+ return content unless content && patterns
293
+
294
+ content.select do |name,|
295
+ patterns.any? { |pattern| File.fnmatch?(pattern, name, File::FNM_EXTGLOB) }
296
+ end
297
+ end
298
+
299
+ def list_tasks(filter_content: false)
284
300
  in_bolt_compiler do |compiler|
285
- tasks = compiler.list_tasks
286
- tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
301
+ tasks = compiler.list_tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
287
302
  task_sig = compiler.task_signature(task_name)
288
303
  unless task_sig.task_hash['metadata']['private']
289
304
  data << [task_name, task_sig.task_hash['metadata']['description']]
290
305
  end
291
306
  end
307
+
308
+ filter_content ? filter_content(tasks, @project&.tasks) : tasks
292
309
  end
293
310
  end
294
311
 
@@ -340,14 +357,15 @@ module Bolt
340
357
  Bolt::Task.from_task_signature(task)
341
358
  end
342
359
 
343
- def list_plans
360
+ def list_plans(filter_content: false)
344
361
  in_bolt_compiler do |compiler|
345
362
  errors = []
346
363
  plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
347
364
  errors.each do |error|
348
365
  @logger.warn(error.details['original_error'])
349
366
  end
350
- plans
367
+
368
+ filter_content ? filter_content(plans, @project&.plans) : plans
351
369
  end
352
370
  end
353
371
 
@@ -384,7 +402,7 @@ module Bolt
384
402
  plan.docstring
385
403
  end
386
404
 
387
- defaults = plan.parameters.reject { |_, value| value.nil? }.to_h
405
+ defaults = plan.parameters.to_h.compact
388
406
  signature_params = Set.new(plan.parameters.map(&:first))
389
407
  parameters = plan.tags(:param).each_with_object({}) do |param, params|
390
408
  name = param.name
@@ -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