bolt 2.34.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d33993b3430c33f173a3a2d07220d175d4545c1992e9bed3bd75a7be7bae9be
4
- data.tar.gz: 68fe674fd2d8ec196149938668d68b3e4c2becd3a97639c49ee85a637bb2e5a8
3
+ metadata.gz: d05af3f780d500b91be547a73123d24c2da041a8ef60ba14114921358b120800
4
+ data.tar.gz: e8534835e69d1e5e57a353ff89a7d2667cf6d0894e8a07c1e5812282ed85b2cd
5
5
  SHA512:
6
- metadata.gz: 89e8fd86df6d1427fa61df4e54b0a1973ec607bcd46744a5fa4f0316f217ae02fa0b72428527a6d3ef15303f9fae371c62954d5b53a600d4d71589a3b2108656
7
- data.tar.gz: e40ee91802e4dcb6a5d542f131800b28f4916ad2245837b34f26bcef128d99485c3d96ab02e498033476fe01fe6e5740be74b985370fd390ded7b8b0683b0e04
6
+ metadata.gz: 97b3faac5a1e423dcebf8e4fa92f55bbc36bd1c9735fc38332db40a2bb0db0e1ddc8e1e815aa29f31f9eb3601972a8d6e07b39d0c5dc6cd2762ea87a91ddec92
7
+ data.tar.gz: 15963b1bb77ee04a1db4f54996964b32e599e4a0cc422dc77892151b221c1a17df9e6a1c74f160a30e9146402d48819b07b3b9d2ee9c96d7e763bba97389ccc8
@@ -44,9 +44,7 @@ Puppet::Functions.create_function(:catch_errors) do
44
44
  yield
45
45
  rescue Puppet::PreformattedError => e
46
46
  if e.cause.is_a?(Bolt::Error)
47
- if error_types.nil?
48
- e.cause.to_puppet_error
49
- elsif error_types.include?(e.cause.to_h['kind'])
47
+ if error_types.nil? || error_types.include?(e.cause.to_h['kind'])
50
48
  e.cause.to_puppet_error
51
49
  else
52
50
  raise e
@@ -110,14 +110,25 @@ Puppet::Functions.create_function(:download_file, Puppet::Functions::InternalFun
110
110
  targets = inventory.get_targets(targets)
111
111
  if targets.empty?
112
112
  call_function('debug', "Simulating file download of '#{source}' - no targets given - no action taken")
113
- r = Bolt::ResultSet.new([])
113
+ Bolt::ResultSet.new([])
114
114
  else
115
- r = executor.download_file(targets, source, destination, options, Puppet::Pops::PuppetStack.top_of_stack)
116
- end
115
+ r = if executor.in_parallel
116
+ require 'concurrent'
117
+ require 'fiber'
118
+ future = Concurrent::Future.execute do
119
+ executor.download_file(targets, source, destination, options, Puppet::Pops::PuppetStack.top_of_stack)
120
+ end
121
+
122
+ Fiber.yield('unfinished') while future.incomplete?
123
+ future.value || future.reason
124
+ else
125
+ executor.download_file(targets, source, destination, options, Puppet::Pops::PuppetStack.top_of_stack)
126
+ end
117
127
 
118
- if !r.ok && !options[:catch_errors]
119
- raise Bolt::RunFailure.new(r, 'download_file', source)
128
+ if !r.ok && !options[:catch_errors]
129
+ raise Bolt::RunFailure.new(r, 'download_file', source)
130
+ end
131
+ r
120
132
  end
121
- r
122
133
  end
123
134
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/yarn'
4
+
5
+ # Map a code block onto an array, where each array element executes in parallel.
6
+ # This function is experimental.
7
+ #
8
+ # > **Note:** Not available in apply block.
9
+ Puppet::Functions.create_function(:parallelize, Puppet::Functions::InternalFunction) do
10
+ # Map a block onto an array, where each array element executes in parallel.
11
+ # This function is experimental.
12
+ # @param data The array to apply the block to.
13
+ # @return [Array] An array of PlanResult objects. Each input from the input
14
+ # array returns a corresponding PlanResult object.
15
+ # @example Execute two tasks on multiple targets. Once the task finishes on one
16
+ # target, that target can move to the next step without waiting for the task
17
+ # to finish on the second target.
18
+ # $targets = get_targets(["host1", "host2"])
19
+ # $result = parallelize ($targets) |$t| {
20
+ # run_task('a', $t)
21
+ # run_task('b', $t)
22
+ # }
23
+ dispatch :parallelize do
24
+ scope_param
25
+ param 'Array[Any]', :data
26
+ block_param 'Callable[Any]', :block
27
+ return_type 'Array[Boltlib::PlanResult]'
28
+ end
29
+
30
+ def parallelize(scope, data, &block)
31
+ unless Puppet[:tasks]
32
+ raise Puppet::ParseErrorWithIssue
33
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'parallelize')
34
+ end
35
+
36
+ executor = Puppet.lookup(:bolt_executor)
37
+ executor.report_function_call(self.class.name)
38
+
39
+ skein = data.each_with_index.map do |object, index|
40
+ executor.create_yarn(scope, block, object, index)
41
+ end
42
+
43
+ result = executor.round_robin(skein)
44
+
45
+ failed_indices = result.each_index.select do |i|
46
+ result[i].is_a?(Bolt::Error)
47
+ end
48
+
49
+ # TODO: Inner catch errors block?
50
+ if failed_indices.any?
51
+ raise Bolt::ParallelFailure.new(result, failed_indices)
52
+ end
53
+
54
+ result
55
+ end
56
+ end
@@ -67,14 +67,32 @@ Puppet::Functions.create_function(:run_command) do
67
67
 
68
68
  if targets.empty?
69
69
  call_function('debug', "Simulating run_command('#{command}') - no targets given - no action taken")
70
- r = Bolt::ResultSet.new([])
70
+ Bolt::ResultSet.new([])
71
71
  else
72
- r = executor.run_command(targets, command, options, Puppet::Pops::PuppetStack.top_of_stack)
73
- end
72
+ r = if executor.in_parallel
73
+ require 'concurrent'
74
+ require 'fiber'
75
+ future = Concurrent::Future.execute do
76
+ executor.run_command(targets,
77
+ command,
78
+ options,
79
+ Puppet::Pops::PuppetStack.top_of_stack)
80
+ end
81
+
82
+ Fiber.yield('unfinished') while future.incomplete?
83
+ future.value || future.reason
84
+ else
85
+ executor.run_command(targets,
86
+ command,
87
+ options,
88
+ Puppet::Pops::PuppetStack.top_of_stack)
89
+ end
90
+
91
+ if !r.ok && !options[:catch_errors]
92
+ raise Bolt::RunFailure.new(r, 'run_command', command)
93
+ end
74
94
 
75
- if !r.ok && !options[:catch_errors]
76
- raise Bolt::RunFailure.new(r, 'run_command', command)
95
+ r
77
96
  end
78
- r
79
97
  end
80
98
  end
@@ -84,15 +84,34 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
84
84
  # Ensure that given targets are all Target instances)
85
85
  targets = inventory.get_targets(targets)
86
86
 
87
- r = if targets.empty?
88
- Bolt::ResultSet.new([])
89
- else
90
- executor.run_script(targets, found, arguments, options, Puppet::Pops::PuppetStack.top_of_stack)
91
- end
87
+ if targets.empty?
88
+ Bolt::ResultSet.new([])
89
+ else
90
+ r = if executor.in_parallel
91
+ require 'concurrent'
92
+ require 'fiber'
93
+ future = Concurrent::Future.execute do
94
+ executor.run_script(targets,
95
+ found,
96
+ arguments,
97
+ options,
98
+ Puppet::Pops::PuppetStack.top_of_stack)
99
+ end
92
100
 
93
- if !r.ok && !options[:catch_errors]
94
- raise Bolt::RunFailure.new(r, 'run_script', script)
101
+ Fiber.yield('unfinished') while future.incomplete?
102
+ future.value || future.reason
103
+ else
104
+ executor.run_script(targets,
105
+ found,
106
+ arguments,
107
+ options,
108
+ Puppet::Pops::PuppetStack.top_of_stack)
109
+ end
110
+
111
+ if !r.ok && !options[:catch_errors]
112
+ raise Bolt::RunFailure.new(r, 'run_script', script)
113
+ end
114
+ r
95
115
  end
96
- r
97
116
  end
98
117
  end
@@ -133,7 +133,27 @@ Puppet::Functions.create_function(:run_task) do
133
133
  if targets.empty?
134
134
  Bolt::ResultSet.new([])
135
135
  else
136
- result = executor.run_task(targets, task, params, options, Puppet::Pops::PuppetStack.top_of_stack)
136
+ result = if executor.in_parallel
137
+ require 'concurrent'
138
+ require 'fiber'
139
+ future = Concurrent::Future.execute do
140
+ executor.run_task(targets,
141
+ task,
142
+ params,
143
+ options,
144
+ Puppet::Pops::PuppetStack.top_of_stack)
145
+ end
146
+
147
+ Fiber.yield('unfinished') while future.incomplete?
148
+ future.value || future.reason
149
+ else
150
+ executor.run_task(targets,
151
+ task,
152
+ params,
153
+ options,
154
+ Puppet::Pops::PuppetStack.top_of_stack)
155
+ end
156
+
137
157
  if !result.ok && !options[:catch_errors]
138
158
  raise Bolt::RunFailure.new(result, 'run_task', task_name)
139
159
  end
@@ -180,7 +180,24 @@ Puppet::Functions.create_function(:run_task_with) do
180
180
  else
181
181
  # Combine the results from the task run with any failing results that were
182
182
  # generated earlier when creating the target mapping
183
- task_result = executor.run_task_with(target_mapping, task, options, Puppet::Pops::PuppetStack.top_of_stack)
183
+ task_result = if executor.in_parallel
184
+ require 'concurrent'
185
+ require 'fiber'
186
+ future = Concurrent::Future.execute do
187
+ executor.run_task_with(target_mapping,
188
+ task,
189
+ options,
190
+ Puppet::Pops::PuppetStack.top_of_stack)
191
+ end
192
+
193
+ Fiber.yield('unfinished') while future.incomplete?
194
+ future.value || future.reason
195
+ else
196
+ executor.run_task_with(target_mapping,
197
+ task,
198
+ options,
199
+ Puppet::Pops::PuppetStack.top_of_stack)
200
+ end
184
201
  result = Bolt::ResultSet.new(task_result.results + error_set)
185
202
 
186
203
  if !result.ok && !options[:catch_errors]
@@ -81,14 +81,32 @@ Puppet::Functions.create_function(:upload_file, Puppet::Functions::InternalFunct
81
81
  targets = inventory.get_targets(targets)
82
82
  if targets.empty?
83
83
  call_function('debug', "Simulating file upload of '#{found}' - no targets given - no action taken")
84
- r = Bolt::ResultSet.new([])
84
+ Bolt::ResultSet.new([])
85
85
  else
86
- r = executor.upload_file(targets, found, destination, options, Puppet::Pops::PuppetStack.top_of_stack)
87
- end
86
+ r = if executor.in_parallel
87
+ require 'concurrent'
88
+ require 'fiber'
89
+ future = Concurrent::Future.execute do
90
+ executor.upload_file(targets,
91
+ found,
92
+ destination,
93
+ options,
94
+ Puppet::Pops::PuppetStack.top_of_stack)
95
+ end
88
96
 
89
- if !r.ok && !options[:catch_errors]
90
- raise Bolt::RunFailure.new(r, 'upload_file', source)
97
+ Fiber.yield('unfinished') while future.incomplete?
98
+ future.value || future.reason
99
+ else
100
+ executor.upload_file(targets,
101
+ found,
102
+ destination,
103
+ options,
104
+ Puppet::Pops::PuppetStack.top_of_stack)
105
+ end
106
+ if !r.ok && !options[:catch_errors]
107
+ raise Bolt::RunFailure.new(r, 'upload_file', source)
108
+ end
109
+ r
91
110
  end
92
- r
93
111
  end
94
112
  end
@@ -575,10 +575,15 @@ module Bolt
575
575
  outputter.print_task_info(pal.get_task(task_name))
576
576
  end
577
577
 
578
+ # Filters a list of content by matching substring.
579
+ #
580
+ private def filter_content(content, filter)
581
+ return content unless content && filter
582
+ content.select { |name,| name.include?(filter) }
583
+ end
584
+
578
585
  def list_tasks
579
- tasks = pal.list_tasks
580
- tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
581
- tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
586
+ tasks = filter_content(pal.list_tasks(filter_content: true), options[:filter])
582
587
  outputter.print_tasks(tasks, pal.user_modulepath)
583
588
  end
584
589
 
@@ -587,9 +592,7 @@ module Bolt
587
592
  end
588
593
 
589
594
  def list_plans
590
- plans = pal.list_plans
591
- plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
592
- plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
595
+ plans = filter_content(pal.list_plans(filter_content: true), options[:filter])
593
596
  outputter.print_plans(plans, pal.user_modulepath)
594
597
  end
595
598
 
@@ -283,7 +283,7 @@ module Bolt
283
283
  _example: "myproject"
284
284
  },
285
285
  "plans" => {
286
- description: "A list of plan names to show in `bolt plan show` output, if they exist. This option is used "\
286
+ description: "A list of plan names and glob patterns to filter the project's plans by. This option is used "\
287
287
  "to limit the visibility of plans for users of the project. For example, project authors "\
288
288
  "might want to limit the visibility of plans that are bundled with Bolt or plans that should "\
289
289
  "only be run as part of another plan. When this option is not configured, all plans are "\
@@ -291,7 +291,7 @@ module Bolt
291
291
  "list.",
292
292
  type: Array,
293
293
  _plugin: false,
294
- _example: ["myproject", "myproject::foo", "myproject::bar"]
294
+ _example: ["myproject", "myproject::foo", "myproject::bar", "myproject::deploy::*"]
295
295
  },
296
296
  "plugin_hooks" => {
297
297
  description: "A map of [plugin hooks](writing_plugins.md#hooks) and which plugins a hook should use. "\
@@ -402,7 +402,7 @@ module Bolt
402
402
  _default: true
403
403
  },
404
404
  "tasks" => {
405
- description: "A list of task names to show in `bolt task show` output, if they exist. This option is used "\
405
+ description: "A list of task names and glob patterns to filter the project's tasks by. This option is used "\
406
406
  "to limit the visibility of tasks for users of the project. For example, project authors "\
407
407
  "might want to limit the visibility of tasks that are bundled with Bolt or plans that should "\
408
408
  "only be run as part of a larger workflow. When this option is not configured, all tasks "\
@@ -413,7 +413,7 @@ module Bolt
413
413
  type: String
414
414
  },
415
415
  _plugin: false,
416
- _example: ["myproject", "myproject::foo", "myproject::bar"]
416
+ _example: ["myproject", "myproject::foo", "myproject::bar", "myproject::deploy_*"]
417
417
  },
418
418
  "trusted-external-command" => {
419
419
  description: "The path to an executable on the Bolt controller that can produce external trusted facts. "\
@@ -357,7 +357,7 @@ module Bolt
357
357
  description: "The URL of the host used for API requests.",
358
358
  format: "uri",
359
359
  _plugin: true,
360
- _example: "https://api.example.com:<port>"
360
+ _example: "https://api.example.com:8143"
361
361
  },
362
362
  "shell-command" => {
363
363
  type: String,
@@ -90,6 +90,20 @@ module Bolt
90
90
  end
91
91
  end
92
92
 
93
+ class ParallelFailure < Bolt::Error
94
+ def initialize(results, failed_indices)
95
+ details = {
96
+ 'action' => 'parallelize',
97
+ 'failed_indices' => failed_indices,
98
+ 'results' => results
99
+ }
100
+ message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
101
+ message += "s" unless failed_indices.length == 1
102
+ super(message, 'bolt/parallel-failure', details)
103
+ @error_code = 2
104
+ end
105
+ end
106
+
93
107
  class PlanFailure < Error
94
108
  def initialize(*args)
95
109
  super(*args)
@@ -131,6 +145,16 @@ module Bolt
131
145
  end
132
146
  end
133
147
 
148
+ class InvalidParallelResult < Error
149
+ def initialize(result_str, file, line)
150
+ super("Parallel block returned an invalid result: #{result_str}",
151
+ 'bolt/invalid-plan-result',
152
+ { 'file' => file,
153
+ 'line' => line,
154
+ 'result_string' => result_str })
155
+ end
156
+ end
157
+
134
158
  class ValidationError < Bolt::Error
135
159
  def initialize(msg)
136
160
  super(msg, 'bolt/validation-error')
@@ -17,6 +17,7 @@ require 'bolt/transport/orch'
17
17
  require 'bolt/transport/local'
18
18
  require 'bolt/transport/docker'
19
19
  require 'bolt/transport/remote'
20
+ require 'bolt/yarn'
20
21
 
21
22
  module Bolt
22
23
  TRANSPORTS = {
@@ -29,7 +30,7 @@ module Bolt
29
30
  }.freeze
30
31
 
31
32
  class Executor
32
- attr_reader :noop, :transports
33
+ attr_reader :noop, :transports, :in_parallel
33
34
  attr_accessor :run_as
34
35
 
35
36
  def initialize(concurrency = 1,
@@ -60,6 +61,7 @@ module Bolt
60
61
 
61
62
  @noop = noop
62
63
  @run_as = nil
64
+ @in_parallel = false
63
65
  @pool = if concurrency > 0
64
66
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
65
67
  else
@@ -84,6 +86,14 @@ module Bolt
84
86
  self
85
87
  end
86
88
 
89
+ def unsubscribe(subscriber, types = nil)
90
+ if types.nil? || types.sort == @subscribers[subscriber]&.sort
91
+ @subscribers.delete(subscriber)
92
+ elsif @subscribers[subscriber].is_a?(Array)
93
+ @subscribers[subscriber] = @subscribers[subscriber] - types
94
+ end
95
+ end
96
+
87
97
  def publish_event(event)
88
98
  @subscribers.each do |subscriber, types|
89
99
  # If types isn't set or if the subscriber is subscribed to
@@ -359,6 +369,82 @@ module Bolt
359
369
  plan.call_by_name_with_scope(scope, params, true)
360
370
  end
361
371
 
372
+ def create_yarn(scope, block, object, index)
373
+ fiber = Fiber.new do
374
+ # Create the new scope
375
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
376
+ local = Puppet::Parser::Scope::LocalScope.new
377
+
378
+ # Compress the current scopes into a single vars hash to add to the new scope
379
+ current_scope = scope.effective_symtable(true)
380
+ until current_scope.nil?
381
+ current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
382
+ current_scope = current_scope.parent
383
+ end
384
+ newscope.push_ephemerals([local])
385
+
386
+ begin
387
+ result = catch(:return) do
388
+ args = { block.parameters[0][1].to_s => object }
389
+ block.closure.call_by_name_with_scope(newscope, args, true)
390
+ end
391
+
392
+ # If we got a return from the block, get it's value
393
+ # Otherwise the result is the last line from the block
394
+ result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
395
+
396
+ # Validate the result is a PlanResult
397
+ unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
398
+ raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
399
+ end
400
+
401
+ result
402
+ rescue Puppet::PreformattedError => e
403
+ if e.cause.is_a?(Bolt::Error)
404
+ e.cause
405
+ else
406
+ raise e
407
+ end
408
+ end
409
+ end
410
+
411
+ Bolt::Yarn.new(fiber, index)
412
+ end
413
+
414
+ def handle_event(event)
415
+ case event[:type]
416
+ when :node_result
417
+ @thread_completed = true
418
+ end
419
+ end
420
+
421
+ def round_robin(skein)
422
+ subscribe(self, [:node_result])
423
+ results = Array.new(skein.length)
424
+ @in_parallel = true
425
+
426
+ until skein.empty?
427
+ @thread_completed = false
428
+ r = nil
429
+
430
+ skein.each do |yarn|
431
+ if yarn.alive?
432
+ r = yarn.resume
433
+ else
434
+ results[yarn.index] = yarn.value
435
+ skein.delete(yarn)
436
+ end
437
+ end
438
+
439
+ next unless r == 'unfinished'
440
+ sleep(0.1) until @thread_completed || skein.empty?
441
+ end
442
+
443
+ @in_parallel = false
444
+ unsubscribe(self, [:node_result])
445
+ results
446
+ end
447
+
362
448
  class TimeoutError < RuntimeError; end
363
449
 
364
450
  def wait_until_available(targets,
@@ -215,7 +215,7 @@ module Bolt
215
215
 
216
216
  def print_tasks(tasks, modulepath)
217
217
  command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
218
- print_table(tasks)
218
+ tasks.any? ? print_table(tasks) : print_message('No available tasks')
219
219
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
220
220
  "\nUse '#{command}' to view "\
221
221
  "details and parameters for a specific task.")
@@ -299,7 +299,7 @@ module Bolt
299
299
 
300
300
  def print_plans(plans, modulepath)
301
301
  command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
302
- print_table(plans)
302
+ plans.any? ? print_table(plans) : print_message('No available plans')
303
303
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
304
304
  "\nUse '#{command}' to view "\
305
305
  "details and parameters for a specific plan.")
@@ -26,7 +26,7 @@ module Bolt
26
26
  details[:line] = err.line if defined?(err.line)
27
27
  details[:column] = err.pos if defined?(err.pos)
28
28
 
29
- error.add_filelineno(details)
29
+ error.add_filelineno(details.compact)
30
30
  error
31
31
  end
32
32
 
@@ -286,15 +286,26 @@ module Bolt
286
286
  raise Bolt::PAL::PALError, "Failed to parse manifest: #{e}"
287
287
  end
288
288
 
289
- 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)
290
300
  in_bolt_compiler do |compiler|
291
- tasks = compiler.list_tasks
292
- 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|
293
302
  task_sig = compiler.task_signature(task_name)
294
303
  unless task_sig.task_hash['metadata']['private']
295
304
  data << [task_name, task_sig.task_hash['metadata']['description']]
296
305
  end
297
306
  end
307
+
308
+ filter_content ? filter_content(tasks, @project&.tasks) : tasks
298
309
  end
299
310
  end
300
311
 
@@ -346,14 +357,15 @@ module Bolt
346
357
  Bolt::Task.from_task_signature(task)
347
358
  end
348
359
 
349
- def list_plans
360
+ def list_plans(filter_content: false)
350
361
  in_bolt_compiler do |compiler|
351
362
  errors = []
352
363
  plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
353
364
  errors.each do |error|
354
365
  @logger.warn(error.details['original_error'])
355
366
  end
356
- plans
367
+
368
+ filter_content ? filter_content(plans, @project&.plans) : plans
357
369
  end
358
370
  end
359
371
 
@@ -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')
@@ -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
@@ -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
@@ -10,11 +10,6 @@ require 'bolt/transport/orch/connection'
10
10
  module Bolt
11
11
  module Transport
12
12
  class Orch < Base
13
- CONF_FILE = if ENV['HOME'].nil?
14
- '/etc/puppetlabs/client-tools/orchestrator.conf'
15
- else
16
- File.expand_path('~/.puppetlabs/client-tools/orchestrator.conf')
17
- end
18
13
  BOLT_COMMAND_TASK = Struct.new(:name).new('bolt_shim::command').freeze
19
14
  BOLT_SCRIPT_TASK = Struct.new(:name).new('bolt_shim::script').freeze
20
15
  BOLT_UPLOAD_TASK = Struct.new(:name).new('bolt_shim::upload').freeze
@@ -17,13 +17,20 @@ module Bolt
17
17
  end
18
18
 
19
19
  def initialize(opts, plan_context, logger)
20
+ require 'addressable/uri'
21
+
20
22
  @logger = logger
21
23
  @key = self.class.get_key(opts)
22
- client_keys = %w[service-url token-file cacert job-poll-interval job-poll-timeout]
23
- client_opts = client_keys.each_with_object({}) do |k, acc|
24
- acc[k] = opts[k] if opts.include?(k)
24
+ client_opts = opts.slice('token-file', 'cacert', 'job-poll-interval', 'job-poll-timeout')
25
+
26
+ if opts['service-url']
27
+ uri = Addressable::URI.parse(opts['service-url'])
28
+ uri&.port ||= 8143
29
+ client_opts['service-url'] = uri.to_s
25
30
  end
31
+
26
32
  client_opts['User-Agent'] = "Bolt/#{VERSION}"
33
+
27
34
  %w[token-file cacert].each do |f|
28
35
  client_opts[f] = File.expand_path(client_opts[f]) if client_opts[f]
29
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.34.0'
4
+ VERSION = '2.35.0'
5
5
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module Bolt
6
+ class Yarn
7
+ attr_reader :fiber, :value, :index
8
+
9
+ def initialize(fiber, index)
10
+ @fiber = fiber
11
+ @index = index
12
+ @value = nil
13
+ end
14
+
15
+ def alive?
16
+ fiber.alive?
17
+ end
18
+
19
+ def resume
20
+ @value = fiber.resume
21
+ end
22
+ end
23
+ end
@@ -48,6 +48,10 @@ module BoltServer
48
48
  # See the `orchestrator.bolt.codedir` tk config setting.
49
49
  DEFAULT_BOLT_CODEDIR = '/opt/puppetlabs/server/data/orchestration-services/code'
50
50
 
51
+ MISSING_PROJECT_REF_RESPONSE = [
52
+ 400, Bolt::ValidationError.new('`project_ref` is a required argument').to_json
53
+ ].freeze
54
+
51
55
  def initialize(config)
52
56
  @config = config
53
57
  @schemas = Hash[REQUEST_SCHEMAS.map do |basename|
@@ -261,13 +265,16 @@ module BoltServer
261
265
  end
262
266
  end
263
267
 
264
- def in_bolt_project(bolt_project)
265
- return [400, '`project_ref` is a required argument'] if bolt_project.nil?
266
- project_dir = File.join(@config['projects-dir'], bolt_project)
267
- return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
268
+ def config_from_project(project_ref)
269
+ project_dir = File.join(@config['projects-dir'], project_ref)
270
+ raise Bolt::ValidationError, "`project_ref`: #{project_dir} does not exist" unless Dir.exist?(project_dir)
271
+ project = Bolt::Project.create_project(project_dir)
272
+ Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
273
+ end
274
+
275
+ def in_bolt_project(project_ref)
268
276
  @pal_mutex.synchronize do
269
- project = Bolt::Project.create_project(project_dir)
270
- bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
277
+ bolt_config = config_from_project(project_ref)
271
278
  modulepath_object = Bolt::Config::Modulepath.new(
272
279
  bolt_config.modulepath,
273
280
  boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
@@ -508,6 +515,7 @@ module BoltServer
508
515
  #
509
516
  # @param project_ref [String] the project to fetch the plan from
510
517
  get '/project_plans/:module_name/:plan_name' do
518
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
511
519
  in_bolt_project(params['project_ref']) do |context|
512
520
  plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
513
521
  plan_info = allowed_helper(plan_info, context[:config].project.plans)
@@ -532,6 +540,7 @@ module BoltServer
532
540
  #
533
541
  # @param bolt_project_ref [String] the reference to the bolt-project directory to load task metadata from
534
542
  get '/project_tasks/:module_name/:task_name' do
543
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
535
544
  in_bolt_project(params['project_ref']) do |context|
536
545
  ps_parameters = {
537
546
  'project' => params['project_ref']
@@ -570,6 +579,7 @@ module BoltServer
570
579
  #
571
580
  # @param project_ref [String] the project to fetch the list of plans from
572
581
  get '/project_plans' do
582
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
573
583
  in_bolt_project(params['project_ref']) do |context|
574
584
  plans_response = plan_list(context[:pal])
575
585
 
@@ -603,6 +613,7 @@ module BoltServer
603
613
  #
604
614
  # @param project_ref [String] the project to fetch the list of tasks from
605
615
  get '/project_tasks' do
616
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
606
617
  in_bolt_project(params['project_ref']) do |context|
607
618
  tasks_response = task_list(context[:pal])
608
619
 
@@ -621,6 +632,7 @@ module BoltServer
621
632
  #
622
633
  # @param project_ref [String] the project_ref to fetch the file metadatas from
623
634
  get '/project_file_metadatas/:module_name/*' do
635
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
624
636
  in_bolt_project(params['project_ref']) do |context|
625
637
  file = params[:splat].first
626
638
  metadatas = file_metadatas(context[:pal], params[:module_name], file)
@@ -630,13 +642,32 @@ module BoltServer
630
642
  [400, e.message]
631
643
  end
632
644
 
645
+ # Returns a list of targets parsed from a Project inventory
646
+ #
647
+ # @param project_ref [String] the project_ref to compute the inventory from
648
+ get '/project_inventory_targets' do
649
+ return MISSING_PROJECT_REF_RESPONSE if params['project_ref'].nil?
650
+ bolt_config = config_from_project(params['project_ref'])
651
+ if bolt_config.inventoryfile && bolt_config.project.inventory_file.to_s != bolt_config.inventoryfile
652
+ raise Bolt::ValidationError, "Project inventory must be defined in the " \
653
+ "inventory.yaml file at the root of the project directory"
654
+ end
655
+ plugins = Bolt::Plugin.setup(bolt_config, nil)
656
+ inventory = Bolt::Inventory.from_config(bolt_config, plugins)
657
+ target_list = inventory.get_targets('all').map { |targ| targ.to_h.merge({ 'transport' => targ.transport }) }
658
+
659
+ [200, target_list.to_json]
660
+ rescue Bolt::Error => e
661
+ [500, e.to_json]
662
+ end
663
+
633
664
  error 404 do
634
665
  err = Bolt::Error.new("Could not find route #{request.path}",
635
666
  'boltserver/not-found')
636
667
  [404, err.to_json]
637
668
  end
638
669
 
639
- error 500 do
670
+ error StandardError do
640
671
  e = env['sinatra.error']
641
672
  err = Bolt::Error.new("500: Unknown error: #{e.message}",
642
673
  'boltserver/server-error')
@@ -17,12 +17,13 @@ module BoltSpec
17
17
 
18
18
  # Nothing on the executor is 'public'
19
19
  class MockExecutor
20
- attr_reader :noop, :error_message
20
+ attr_reader :noop, :error_message, :in_parallel
21
21
  attr_accessor :run_as, :transport_features, :execute_any_plan
22
22
 
23
23
  def initialize(modulepath)
24
24
  @noop = false
25
25
  @run_as = nil
26
+ @in_parallel = false
26
27
  @error_message = nil
27
28
  @allow_apply = false
28
29
  @modulepath = [modulepath].flatten.map { |path| File.absolute_path(path) }
@@ -133,6 +134,7 @@ module BoltSpec
133
134
  # through to another conditional statement
134
135
  doub = @plan_doubles[plan_name] || @plan_doubles[:default]
135
136
 
137
+ # rubocop:disable Lint/DuplicateBranch
136
138
  # High level:
137
139
  # - If we've explicitly allowed execution of the plan (normally the main plan
138
140
  # passed into BoltSpec::Plan::run_plan()), then execute it
@@ -165,6 +167,7 @@ module BoltSpec
165
167
  @error_message = "Unexpected call to 'run_plan(#{plan_name}, #{params_str})'"
166
168
  raise UnexpectedInvocation, @error_message
167
169
  end
170
+ # rubocop:enable Lint/DuplicateBranch
168
171
  result
169
172
  end
170
173
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.34.0
4
+ version: 2.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
11
+ date: 2020-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -404,6 +404,7 @@ files:
404
404
  - bolt-modules/boltlib/lib/puppet/functions/get_resources.rb
405
405
  - bolt-modules/boltlib/lib/puppet/functions/get_target.rb
406
406
  - bolt-modules/boltlib/lib/puppet/functions/get_targets.rb
407
+ - bolt-modules/boltlib/lib/puppet/functions/parallelize.rb
407
408
  - bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb
408
409
  - bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb
409
410
  - bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb
@@ -555,6 +556,7 @@ files:
555
556
  - lib/bolt/util.rb
556
557
  - lib/bolt/util/puppet_log_level.rb
557
558
  - lib/bolt/version.rb
559
+ - lib/bolt/yarn.rb
558
560
  - lib/bolt_server/acl.rb
559
561
  - lib/bolt_server/base_config.rb
560
562
  - lib/bolt_server/config.rb