bolt 1.25.0 → 1.26.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: 22679235d7475d2cc80d765770e6b81b0ed0852d4563a889c00043fe61f359f1
4
- data.tar.gz: a04c075cb1551ab90f6e6e9f0e6e02670b6081475a9d8f0d49fd9e7f046d0992
3
+ metadata.gz: 2431380225c5f60abf51caa85cfa1ee3aeadcf1ef494192e93bfb8f7e175d7f9
4
+ data.tar.gz: a49aa9489c0992bcaab219b54fe054dd3ef8d078231cf64ed7e039c8c580bb46
5
5
  SHA512:
6
- metadata.gz: 4633d131919822b23981f8533b580e727817c95aa78a1afc05a4ca66bd14e08f3fd8f9c116f89e8726e171616c5cee1138d27260972dfc5e0718a9cb7a6096fb
7
- data.tar.gz: 6a1f67bf10a65e26cd56cc9a82ad050a9023d5738b5d129cf43e203a50c1403c7cef4dd67085774dfba1d853f10174983b8e09716cc0a9d8441a24c69c62fe8b
6
+ metadata.gz: 2a28ce31a001a91307ce8e31cf4307a7eac3f5027143444eae22a49e406820e808bdffa668a9daad5b52bec76413af55a28c50b2aa439d4516951370d7f15210
7
+ data.tar.gz: fa64ecb34705bb81e728ce9067928bcde35b51630d672d8666d09766f941c26811466608f03927852061a5848135b7ee2d6725224770f0a743404a6e8840422f
@@ -28,6 +28,9 @@ module Bolt
28
28
  when 'file'
29
29
  { flags: ACTION_OPTS + %w[tmpdir],
30
30
  banner: FILE_HELP }
31
+ when 'inventory'
32
+ { flags: OPTIONS[:inventory] + OPTIONS[:global] + %w[format inventoryfile boltdir configfile],
33
+ banner: INVENTORY_HELP }
31
34
  when 'plan'
32
35
  case action
33
36
  when 'convert'
@@ -110,6 +113,7 @@ Available subcommands:
110
113
  bolt secret createkeys Create new encryption keys
111
114
  bolt secret encrypt <plaintext> Encrypt a value
112
115
  bolt secret decrypt <encrypted> Decrypt a value
116
+ bolt inventory show Show the list of targets an action would run on
113
117
 
114
118
  Run `bolt <subcommand> --help` to view specific examples.
115
119
 
@@ -263,9 +267,19 @@ Available options are:
263
267
  createkeys Create new encryption keys
264
268
  encrypt Encrypt a value
265
269
  decrypt Decrypt a value
270
+
266
271
  Available options are:
267
272
  SECRET_HELP
268
273
 
274
+ INVENTORY_HELP = <<~INVENTORY_HELP
275
+ Usage: bolt inventory <action>
276
+
277
+ Available actions are:
278
+ show Show the list of targets an action would run on
279
+
280
+ Available options are:
281
+ INVENTORY_HELP
282
+
269
283
  def initialize(options)
270
284
  super()
271
285
 
@@ -366,7 +380,8 @@ Available options are:
366
380
  @options[:'compile-concurrency'] = concurrency
367
381
  end
368
382
  define('-m', '--modulepath MODULES',
369
- "List of directories containing modules, separated by '#{File::PATH_SEPARATOR}'") do |modulepath|
383
+ "List of directories containing modules, separated by '#{File::PATH_SEPARATOR}'",
384
+ 'Directories are case-sensitive') do |modulepath|
370
385
  # When specified from the CLI, modulepath entries are relative to pwd
371
386
  @options[:modulepath] = modulepath.split(File::PATH_SEPARATOR).map do |moduledir|
372
387
  File.expand_path(moduledir)
@@ -35,6 +35,7 @@ module Bolt
35
35
  'file' => %w[upload],
36
36
  'puppetfile' => %w[install show-modules],
37
37
  'secret' => %w[encrypt decrypt createkeys],
38
+ 'inventory' => %w[show],
38
39
  'apply' => %w[] }.freeze
39
40
 
40
41
  attr_reader :config, :options
@@ -117,6 +118,9 @@ module Bolt
117
118
 
118
119
  Bolt::Logger.configure(config.log, config.color)
119
120
 
121
+ # Logger must be configured before checking path case, otherwise warnings will not display
122
+ @config.check_path_case('modulepath', @config.modulepath)
123
+
120
124
  # After validation, initialize inventory and targets. Errors here are better to catch early.
121
125
  # After this step
122
126
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
@@ -136,7 +140,7 @@ module Bolt
136
140
 
137
141
  options
138
142
  rescue Bolt::Error => e
139
- warn e.message
143
+ outputter.fatal_error(e)
140
144
  raise e
141
145
  end
142
146
 
@@ -296,6 +300,8 @@ module Bolt
296
300
  else
297
301
  list_plans
298
302
  end
303
+ elsif options[:subcommand] == 'inventory'
304
+ list_targets
299
305
  end
300
306
  return 0
301
307
  elsif options[:action] == 'show-modules'
@@ -393,6 +399,11 @@ module Bolt
393
399
  outputter.print_plans(pal.list_plans, pal.list_modulepath)
394
400
  end
395
401
 
402
+ def list_targets
403
+ update_targets(options)
404
+ outputter.print_targets(options)
405
+ end
406
+
396
407
  def run_plan(plan_name, plan_arguments, nodes, options)
397
408
  unless nodes.empty?
398
409
  if plan_arguments['nodes']
@@ -286,5 +286,27 @@ module Bolt
286
286
  impl.validate(@transports[transport])
287
287
  end
288
288
  end
289
+
290
+ # Check if there is a case-insensitive match to the path
291
+ def check_path_case(type, paths)
292
+ return if paths.nil?
293
+ matches = matching_paths(paths)
294
+
295
+ if matches.any?
296
+ msg = "WARNING: Bolt is case sensitive when specifying a #{type}. Did you mean:\n"
297
+ matches.each { |path| msg += " #{path}\n" }
298
+ @logger.warn msg
299
+ end
300
+ end
301
+
302
+ def matching_paths(paths)
303
+ [*paths].map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| [*paths].include?(p) }
304
+ end
305
+
306
+ def casefold(path)
307
+ path.chars.map do |l|
308
+ l =~ /[A-Za-z]/ ? "[#{l.upcase}#{l.downcase}]" : l
309
+ end.join
310
+ end
289
311
  end
290
312
  end
@@ -106,7 +106,11 @@ module Bolt
106
106
  end
107
107
  plugin.validate_inventory_config(value) if plugin.respond_to?(:validate_inventory_config)
108
108
  Concurrent::Delay.new do
109
- plugin.inventory_config(value)
109
+ begin
110
+ plugin.inventory_config(value)
111
+ rescue StandardError => e
112
+ raise Bolt::Plugin::PluginError.new(e.message, plugin, "inventory_targets in #{@name}")
113
+ end
110
114
  end
111
115
  else
112
116
  value
@@ -203,7 +207,12 @@ module Bolt
203
207
  raise ValidationError.new("#{plugin.name} does not support inventory_targets.", @name)
204
208
  end
205
209
 
206
- targets = plugin.inventory_targets(lookup)
210
+ begin
211
+ targets = plugin.inventory_targets(lookup)
212
+ rescue StandardError => e
213
+ raise Bolt::Plugin::PluginError.new(e.message, plugin, "inventory_targets in #{@name}")
214
+ end
215
+
207
216
  targets.each { |target| add_target(target) }
208
217
  end
209
218
 
@@ -69,7 +69,7 @@ module Bolt
69
69
  def self.console_layout(color)
70
70
  color_scheme = :bolt if color
71
71
  Logging.layouts.pattern(
72
- pattern: '%m\n',
72
+ pattern: '%m\e[0m\n',
73
73
  color_scheme: color_scheme
74
74
  )
75
75
  end
@@ -311,6 +311,13 @@ module Bolt
311
311
  end
312
312
  end
313
313
 
314
+ def print_targets(options)
315
+ targets = options[:targets].map(&:name)
316
+ count = "#{targets.count} target#{'s' unless targets.count == 1}"
317
+ @stream.puts targets.join("\n")
318
+ @stream.puts colorize(:green, count)
319
+ end
320
+
314
321
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
315
322
  def print_apply_result(apply_result, elapsed_time)
316
323
  print_summary(apply_result, elapsed_time)
@@ -87,6 +87,13 @@ module Bolt
87
87
  "moduledir": moduledir }.to_json)
88
88
  end
89
89
 
90
+ def print_targets(options)
91
+ targets = options[:targets].map(&:name)
92
+ count = targets.count
93
+ @stream.puts({ "targets": targets,
94
+ "count": count }.to_json)
95
+ end
96
+
90
97
  def fatal_error(err)
91
98
  @stream.puts "],\n" if @items_open
92
99
  @stream.puts '"_error": ' if @object_open
@@ -240,10 +240,14 @@ module Bolt
240
240
  end
241
241
  end
242
242
 
243
- def get_task_info(task_name)
244
- task = in_bolt_compiler do |compiler|
243
+ def task_signature(task_name)
244
+ in_bolt_compiler do |compiler|
245
245
  compiler.task_signature(task_name)
246
246
  end
247
+ end
248
+
249
+ def get_task_info(task_name)
250
+ task = task_signature(task_name)
247
251
 
248
252
  if task.nil?
249
253
  raise Bolt::Error.new(Bolt::Error.unknown_task(task_name), 'bolt/unknown-task')
@@ -48,19 +48,18 @@ module Bolt
48
48
  stringified_step = Bolt::Util.walk_keys(step) { |key| stringify(key) }
49
49
  stringified_step['name'] = stringify(stringified_step['name']) if stringified_step.key?('name')
50
50
 
51
- step = Step.new(stringified_step, index + 1)
52
- # Send object instead of just name so that step number is printed
53
- duplicate_check(used_names, step)
51
+ step = Step.create(stringified_step, index + 1)
52
+ duplicate_check(used_names, stringified_step['name'], index + 1)
54
53
  used_names << stringified_step['name'] if stringified_step['name']
55
54
  step
56
55
  end.freeze
57
56
  @return = plan['return']
58
57
  end
59
58
 
60
- def duplicate_check(used_names, step)
61
- if used_names.include?(step.name)
62
- error_message = "Duplicate step name or parameter detected: #{step.name.inspect}"
63
- err = step.step_err_msg(error_message)
59
+ def duplicate_check(used_names, name, step_number)
60
+ if used_names.include?(name)
61
+ error_message = "Duplicate step name or parameter detected: #{name.inspect}"
62
+ err = Step.step_error(error_message, name, step_number)
64
63
  raise Bolt::Error.new(err, "bolt/invalid-plan")
65
64
  end
66
65
  end
@@ -102,6 +101,10 @@ module Bolt
102
101
  def initialize(value)
103
102
  @value = value
104
103
  end
104
+
105
+ def ==(other)
106
+ self.class == other.class && @value == other.value
107
+ end
105
108
  end
106
109
 
107
110
  # This class represents a double-quoted YAML string, which is interpreted
@@ -13,23 +13,13 @@ module Bolt
13
13
  end
14
14
 
15
15
  def dispatch_step(scope, step)
16
- step_type = step.type
17
16
  step_body = evaluate_code_blocks(scope, step.body)
18
17
 
19
- case step_type
20
- when 'task'
21
- task_step(scope, step_body)
22
- when 'command'
23
- command_step(scope, step_body)
24
- when 'plan'
25
- plan_step(scope, step_body)
26
- when 'script'
27
- script_step(scope, step_body)
28
- when 'source'
29
- upload_file_step(scope, step_body)
30
- when 'eval'
31
- eval_step(scope, step_body)
32
- end
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)
33
23
  end
34
24
 
35
25
  def task_step(scope, step)
@@ -82,7 +72,7 @@ module Bolt
82
72
  scope.call_function('run_command', args)
83
73
  end
84
74
 
85
- def upload_file_step(scope, step)
75
+ def upload_step(scope, step)
86
76
  source = step['source']
87
77
  destination = step['destination']
88
78
  target = step['target']
@@ -97,6 +87,51 @@ module Bolt
97
87
  step['eval']
98
88
  end
99
89
 
90
+ def resources_step(scope, step)
91
+ manifest = generate_manifest(step['resources'])
92
+
93
+ apply_manifest(scope, step['target'], manifest)
94
+ end
95
+
96
+ def generate_manifest(resources)
97
+ # inspect returns the Ruby representation of the resource hashes,
98
+ # which happens to be the same as the Puppet representation
99
+ puppet_resources = resources.inspect
100
+
101
+ # Because the :tasks setting globally controls which mode the parser
102
+ # is in, we need to make this snippet of non-tasks manifest code
103
+ # parseable in tasks mode. The way to do that is by putting it in an
104
+ # apply statement and taking the body.
105
+ <<~MANIFEST
106
+ apply('placeholder') {
107
+ $resources = #{puppet_resources}
108
+ $resources.each |$res| {
109
+ Resource[$res['type']] { $res['title']:
110
+ * => $res['parameters'],
111
+ }
112
+ }
113
+
114
+ # Add relationships if there is more than one resource
115
+ if $resources.length > 1 {
116
+ ($resources.length - 1).each |$index| {
117
+ $lhs = $resources[$index]
118
+ $rhs = $resources[$index+1]
119
+ $lhs_resource = Resource[$lhs['type'] , $lhs['title']]
120
+ $rhs_resource = Resource[$rhs['type'] , $rhs['title']]
121
+ $lhs_resource -> $rhs_resource
122
+ }
123
+ }
124
+ }
125
+ MANIFEST
126
+ end
127
+
128
+ def apply_manifest(scope, target, manifest)
129
+ ast = @evaluator.parse_string(manifest)
130
+ apply_block = ast.body.body
131
+ applicator = Puppet.lookup(:apply_executor)
132
+ applicator.apply([target], apply_block, scope)
133
+ end
134
+
100
135
  # This is the method that Puppet calls to evaluate the plan. The name
101
136
  # makes more sense for .pp plans.
102
137
  def evaluate_block_with_bindings(closure_scope, args_hash, plan)
@@ -8,152 +8,89 @@ module Bolt
8
8
  class Step
9
9
  attr_reader :name, :type, :body, :target
10
10
 
11
- COMMON_STEP_KEYS = %w[name description target].freeze
12
- STEP_KEYS = {
13
- 'command' => {
14
- 'allowed_keys' => Set['command'].merge(COMMON_STEP_KEYS),
15
- 'required_keys' => Set['target']
16
- },
17
- 'script' => {
18
- 'allowed_keys' => Set['script', 'parameters', 'arguments'].merge(COMMON_STEP_KEYS),
19
- 'required_keys' => Set['target']
20
- },
21
- 'task' => {
22
- 'allowed_keys' => Set['task', 'parameters'].merge(COMMON_STEP_KEYS),
23
- 'required_keys' => Set['target']
24
- },
25
- 'plan' => {
26
- 'allowed_keys' => Set['plan', 'parameters'].merge(COMMON_STEP_KEYS),
27
- 'required_keys' => Set.new
28
- },
29
- 'source' => {
30
- 'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
31
- 'required_keys' => Set['target', 'source', 'destination']
32
- },
33
- 'destination' => {
34
- 'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
35
- 'required_keys' => Set['target', 'source', 'destination']
36
- },
37
- 'eval' => {
38
- 'allowed_keys' => Set['eval', 'name', 'description'],
39
- 'required_keys' => Set.new
40
- }
41
- }.freeze
42
-
43
- def initialize(step_body, step_number)
44
- @body = step_body
45
- @name = @body['name']
46
- # For error messages
47
- @step_number = step_number
48
- validate_step
49
-
50
- @type = STEP_KEYS.keys.find { |key| @body.key?(key) }
51
- @target = @body['target']
11
+ def self.allowed_keys
12
+ Set['name', 'description', 'target']
52
13
  end
53
14
 
54
- def transpile(plan_path)
55
- result = String.new(" ")
56
- result << "$#{@name} = " if @name
57
-
58
- description = body.fetch('description', nil)
59
- parameters = body.fetch('parameters', {})
60
- if @type == 'script' && body.key?('arguments')
61
- parameters['arguments'] = body['arguments']
62
- end
63
-
64
- case @type
65
- when 'command', 'task', 'script', 'plan'
66
- result << "run_#{@type}(#{Bolt::Util.to_code(body[@type])}"
67
- result << ", #{Bolt::Util.to_code(@target)}" if @target
68
- result << ", #{Bolt::Util.to_code(description)}" if description && type != 'plan'
69
- result << ", #{Bolt::Util.to_code(parameters)}" unless parameters.empty?
70
- result << ")"
71
- when 'source'
72
- result << "upload_file(#{Bolt::Util.to_code(body['source'])}, #{Bolt::Util.to_code(body['destination'])}"
73
- result << ", #{Bolt::Util.to_code(@target)}" if @target
74
- result << ", #{Bolt::Util.to_code(description)}" if description
75
- result << ")"
76
- when 'eval'
77
- # We have to do a little extra parsing here, since we only need
78
- # with() for eval blocks
79
- code = Bolt::Util.to_code(body['eval'])
80
- if @name && code.lines.count > 1
81
- # A little indented niceness
82
- indented = code.gsub(/\n/, "\n ").chomp(" ")
83
- result << "with() || {\n #{indented}}"
15
+ COMMON_STEP_KEYS = %w[name description target].freeze
16
+ STEP_KEYS = %w[command script task plan source destination eval resources].freeze
17
+
18
+ def self.create(step_body, step_number)
19
+ type_keys = (STEP_KEYS & step_body.keys)
20
+ case type_keys.length
21
+ when 0
22
+ raise step_error("No valid action detected", step_body['name'], step_number)
23
+ when 1
24
+ type = type_keys.first
25
+ else
26
+ if type_keys.to_set == Set['source', 'destination']
27
+ type = 'upload'
84
28
  else
85
- result << code
29
+ raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
86
30
  end
87
- else
88
- # We should never get here
89
- raise Bolt::YamlTranspiler::ConvertError.new("Can't convert unsupported step type #{@name}", plan_path)
90
31
  end
91
- result << "\n"
92
- result
32
+
33
+ step_class = const_get("Bolt::PAL::YamlPlan::Step::#{type.capitalize}")
34
+ step_class.validate(step_body, step_number)
35
+ step_class.new(step_body)
93
36
  end
94
37
 
95
- def validate_step
96
- validate_step_keys
38
+ def initialize(step_body)
39
+ @name = step_body['name']
40
+ @description = step_body['description']
41
+ @target = step_body['target']
42
+ @body = step_body
43
+ end
44
+
45
+ def transpile
46
+ raise NotImplementedError, "Step #{@name} does not supported conversion to Puppet plan language"
47
+ end
48
+
49
+ def self.validate(body, step_number)
50
+ validate_step_keys(body, step_number)
97
51
 
98
52
  begin
99
- @body.each { |k, v| validate_puppet_code(k, v) }
53
+ body.each { |k, v| validate_puppet_code(k, v) }
100
54
  rescue Bolt::Error => e
101
- err = step_err_msg(e.msg)
102
- raise Bolt::Error.new(err, 'bolt/invalid-plan')
55
+ raise step_error(e.msg, body['name'], step_number)
103
56
  end
104
57
 
105
58
  unless body.fetch('parameters', {}).is_a?(Hash)
106
59
  msg = "Parameters key must be a hash"
107
- raise Bolt::Error.new(step_err_msg(msg), "bolt/invalid-plan")
60
+ raise step_error(msg, body['name'], step_number)
108
61
  end
109
62
 
110
- if @name
111
- unless @name.is_a?(String) && @name.match?(Bolt::PAL::YamlPlan::VAR_NAME_PATTERN)
112
- error_message = "Invalid step name: #{@name.inspect}"
113
- err = step_err_msg(error_message)
114
- raise Bolt::Error.new(err, "bolt/invalid-plan")
63
+ if body.key?('name')
64
+ name = body['name']
65
+ unless name.is_a?(String) && name.match?(Bolt::PAL::YamlPlan::VAR_NAME_PATTERN)
66
+ error_message = "Invalid step name: #{name.inspect}"
67
+ raise step_error(error_message, body['name'], step_number)
115
68
  end
116
69
  end
117
70
  end
118
71
 
119
- def validate_step_keys
120
- step_keys = @body.keys.to_set
121
- action = step_keys.intersection(STEP_KEYS.keys.to_set).to_a
122
- unless action.count == 1
123
- if action.count > 1
124
- # Upload step is special in that it is identified by both `source` and `destination`
125
- unless action.to_set == Set['source', 'destination']
126
- error_message = "Multiple action keys detected: #{action.inspect}"
127
- err = step_err_msg(error_message)
128
- raise Bolt::Error.new(err, "bolt/invalid-plan")
129
- end
130
- else
131
- error_message = "No valid action detected"
132
- err = step_err_msg(error_message)
133
- raise Bolt::Error.new(err, "bolt/invalid-plan")
134
- end
135
- end
72
+ def self.validate_step_keys(body, step_number)
73
+ step_type = name.split('::').last.downcase
136
74
 
137
75
  # For validated step action, ensure only valid keys
138
- unless STEP_KEYS[action.first]['allowed_keys'].superset?(step_keys)
139
- illegal_keys = step_keys - STEP_KEYS[action.first]['allowed_keys']
140
- error_message = "The #{action.first.inspect} step does not support: #{illegal_keys.to_a.inspect} key(s)"
141
- err = step_err_msg(error_message)
76
+ illegal_keys = body.keys.to_set - allowed_keys
77
+ if illegal_keys.any?
78
+ error_message = "The #{step_type.inspect} step does not support: #{illegal_keys.to_a.inspect} key(s)"
79
+ err = step_error(error_message, body['name'], step_number)
142
80
  raise Bolt::Error.new(err, "bolt/invalid-plan")
143
81
  end
144
82
 
145
83
  # Ensure all required keys are present
146
- STEP_KEYS[action.first]['required_keys'].each do |k|
147
- next if step_keys.include?(k)
148
- missing_keys = STEP_KEYS[action.first]['required_keys'] - step_keys
149
- error_message = "The #{action.first.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
150
- err = step_err_msg(error_message)
84
+ missing_keys = required_keys - body.keys
85
+ if missing_keys.any?
86
+ error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
87
+ err = step_error(error_message, body['name'], step_number)
151
88
  raise Bolt::Error.new(err, "bolt/invalid-plan")
152
89
  end
153
90
  end
154
91
 
155
92
  # Recursively ensure all puppet code can be parsed
156
- def validate_puppet_code(step_key, value)
93
+ def self.validate_puppet_code(step_key, value)
157
94
  case value
158
95
  when Array
159
96
  value.map { |element| validate_puppet_code(step_key, element) }
@@ -180,16 +117,14 @@ module Bolt
180
117
  raise Bolt::Error.new("Error parsing #{step_key.inspect}: #{e.basic_message}", "bolt/invalid-plan")
181
118
  end
182
119
 
183
- def step_err_msg(message)
184
- if @name
185
- "Parse error in step number #{@step_number} with name #{@name.inspect}: \n #{message}"
186
- else
187
- "Parse error in step number #{@step_number}: \n #{message}"
188
- end
120
+ def self.step_error(message, name, step_number)
121
+ identifier = name ? name.inspect : "number #{step_number}"
122
+ error = "Parse error in step #{identifier}: \n #{message}"
123
+ Bolt::Error.new(error, 'bolt/invalid-plan')
189
124
  end
190
125
 
191
126
  # Parses the an evaluable string, optionally quote it before parsing
192
- def parse_code_string(code, quote = false)
127
+ def self.parse_code_string(code, quote = false)
193
128
  if quote
194
129
  quoted = Puppet::Pops::Parser::EvaluatingParser.quote(code)
195
130
  Puppet::Pops::Parser::EvaluatingParser.new.parse_string(quoted)
@@ -197,7 +132,20 @@ module Bolt
197
132
  Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code)
198
133
  end
199
134
  end
135
+
136
+ def function_call(function, args)
137
+ code_args = args.map { |arg| Bolt::Util.to_code(arg) }
138
+ "#{function}(#{code_args.join(', ')})"
139
+ end
200
140
  end
201
141
  end
202
142
  end
203
143
  end
144
+
145
+ require 'bolt/pal/yaml_plan/step/command'
146
+ require 'bolt/pal/yaml_plan/step/eval'
147
+ require 'bolt/pal/yaml_plan/step/plan'
148
+ require 'bolt/pal/yaml_plan/step/resources'
149
+ require 'bolt/pal/yaml_plan/step/script'
150
+ require 'bolt/pal/yaml_plan/step/task'
151
+ require 'bolt/pal/yaml_plan/step/upload'
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Command < Step
8
+ def self.allowed_keys
9
+ super + Set['command']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['target']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @command = step_body['command']
19
+ end
20
+
21
+ def transpile
22
+ code = String.new(" ")
23
+ code << "$#{@name} = " if @name
24
+
25
+ fn = 'run_command'
26
+ args = [@command, @target]
27
+ args << @description if @description
28
+
29
+ code << function_call(fn, args)
30
+
31
+ code << "\n"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Eval < Step
8
+ def self.allowed_keys
9
+ super + Set['eval']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set.new
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @eval = step_body['eval']
19
+ end
20
+
21
+ def transpile
22
+ code = String.new(" ")
23
+ code << "$#{@name} = " if @name
24
+
25
+ code_body = Bolt::Util.to_code(@eval)
26
+
27
+ # If we're trying to assign the result of a multi-line eval to a name
28
+ # variable, we need to wrap it in `with()`.
29
+ if @name && code_body.lines.count > 1
30
+ indented = code_body.gsub(/\n/, "\n ").chomp(" ")
31
+ code << "with() || {\n #{indented}}"
32
+ else
33
+ code << code_body
34
+ end
35
+
36
+ code << "\n"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Plan < Step
8
+ def self.allowed_keys
9
+ super + Set['plan', 'parameters']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set.new
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @plan = step_body['plan']
19
+ @parameters = step_body.fetch('parameters', {})
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'run_plan'
27
+ args = [@plan]
28
+ args << @parameters unless @parameters.empty?
29
+
30
+ code << function_call(fn, args)
31
+
32
+ code << "\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Resources < Step
8
+ def self.allowed_keys
9
+ super + Set['resources']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['target']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @resources = step_body['resources']
19
+ @normalized_resources = normalize_resources(@resources)
20
+ end
21
+
22
+ def self.validate(body, step_number)
23
+ super
24
+
25
+ body['resources'].each do |resource|
26
+ if resource['type'] || resource['title']
27
+ if !resource['type']
28
+ err = "Resource declaration must include type key if title key is set"
29
+ raise step_error(err, body['name'], step_number)
30
+ elsif !resource['title']
31
+ err = "Resource declaration must include title key if type key is set"
32
+ raise step_error(err, body['name'], step_number)
33
+ end
34
+ else
35
+ type_keys = (resource.keys - ['parameters'])
36
+ if type_keys.empty?
37
+ err = "Resource declaration is missing a type"
38
+ raise step_error(err, body['name'], step_number)
39
+ elsif type_keys.length > 1
40
+ err = "Resource declaration has ambiguous type: could be #{type_keys.join(' or ')}"
41
+ raise step_error(err, body['name'], step_number)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # What if this comes from a code block?
48
+ def normalize_resources(resources)
49
+ resources.map do |resource|
50
+ if resource['type'] && resource['title']
51
+ type = resource['type']
52
+ title = resource['title']
53
+ else
54
+ type, = (resource.keys - ['parameters'])
55
+ title = resource[type]
56
+ end
57
+
58
+ { 'type' => type, 'title' => title, 'parameters' => resource['parameters'] || {} }
59
+ end
60
+ end
61
+
62
+ def body
63
+ @body.merge('resources' => @normalized_resources)
64
+ end
65
+
66
+ def transpile
67
+ code = StringIO.new
68
+ code.print " "
69
+ code.print "$#{@name} = " if @name
70
+
71
+ code.puts "apply(#{Bolt::Util.to_code(@target)}) {"
72
+
73
+ declarations = @normalized_resources.map do |resource|
74
+ type = resource['type'].is_a?(EvaluableString) ? resource['type'].value : resource['type']
75
+ title = Bolt::Util.to_code(resource['title'])
76
+ parameters = Bolt::Util.map_vals(resource['parameters']) do |val|
77
+ Bolt::Util.to_code(val)
78
+ end
79
+
80
+ resource_str = StringIO.new
81
+ if parameters.empty?
82
+ resource_str.puts " #{type} { #{title}: }"
83
+ else
84
+ resource_str.puts " #{type} { #{title}:"
85
+ parameters.each do |key, val|
86
+ resource_str.puts " #{key} => #{val},"
87
+ end
88
+ resource_str.puts " }"
89
+ end
90
+ resource_str.string
91
+ end
92
+
93
+ code.puts declarations.join(" ->\n")
94
+
95
+ code.puts " }\n"
96
+ code.string
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Script < Step
8
+ def self.allowed_keys
9
+ super + Set['script', 'parameters', 'arguments']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['target']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @script = step_body['script']
19
+ @parameters = step_body.fetch('parameters', {})
20
+ @arguments = step_body.fetch('arguments', [])
21
+ end
22
+
23
+ def transpile
24
+ code = String.new(" ")
25
+ code << "$#{@name} = " if @name
26
+
27
+ options = @parameters.dup
28
+ options['arguments'] = @arguments unless @arguments.empty?
29
+
30
+ fn = 'run_script'
31
+ args = [@script, @target]
32
+ args << @description if @description
33
+ args << options unless options.empty?
34
+
35
+ code << function_call(fn, args)
36
+
37
+ code << "\n"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Task < Step
8
+ def self.allowed_keys
9
+ super + Set['task', 'parameters']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['target']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @task = step_body['task']
19
+ @parameters = step_body.fetch('parameters', {})
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'run_task'
27
+ args = [@task, @target]
28
+ args << @description if @description
29
+ args << @parameters unless @parameters.empty?
30
+
31
+ code << function_call(fn, args)
32
+
33
+ code << "\n"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Upload < Step
8
+ def self.allowed_keys
9
+ super + Set['source', 'destination']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['target', 'source', 'destination']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @source = step_body['source']
19
+ @destination = step_body['destination']
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'upload_file'
27
+ args = [@source, @destination, @target]
28
+ args << @description if @description
29
+
30
+ code << function_call(fn, args)
31
+
32
+ code << "\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -37,8 +37,7 @@ module Bolt
37
37
  plan_string << ") {\n"
38
38
 
39
39
  plan_object.steps&.each do |step|
40
- # This only needs the plan path for raising errors
41
- plan_string << step.transpile(@plan_path)
40
+ plan_string << step.transpile
42
41
  end
43
42
 
44
43
  plan_string << "\n return #{Bolt::Util.to_code(plan_object.return)}\n" if plan_object.return
@@ -4,15 +4,24 @@ require 'bolt/plugin/puppetdb'
4
4
  require 'bolt/plugin/terraform'
5
5
  require 'bolt/plugin/pkcs7'
6
6
  require 'bolt/plugin/prompt'
7
+ require 'bolt/plugin/task'
7
8
 
8
9
  module Bolt
9
10
  class Plugin
11
+ class PluginError < Bolt::Error
12
+ def initialize(msg, plugin, hook)
13
+ mess = "Error executing plugin: #{plugin.name} from #{hook}: #{msg}"
14
+ super(mess, 'bolt/plugin-error')
15
+ end
16
+ end
17
+
10
18
  def self.setup(config, pdb_client)
11
19
  plugins = new(config)
12
20
  plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
13
21
  plugins.add_plugin(Bolt::Plugin::Terraform.new)
14
22
  plugins.add_plugin(Bolt::Plugin::Prompt.new)
15
23
  plugins.add_plugin(Bolt::Plugin::Pkcs7.new(config.boltdir.path, config.plugins['pkcs7'] || {}))
24
+ plugins.add_plugin(Bolt::Plugin::Task.new(config))
16
25
  plugins
17
26
  end
18
27
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Plugin
5
+ class Task
6
+ def hooks
7
+ %w[inventory_targets inventory_config]
8
+ end
9
+
10
+ def name
11
+ 'task'
12
+ end
13
+
14
+ # This creates it's own PAL so we don't have to pass a promise around
15
+ #
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ attr_reader :config
21
+
22
+ def pal
23
+ # Hiera config should not be used yet.
24
+ @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config)
25
+ end
26
+
27
+ def executor
28
+ # Analytics should be handled at a higher level so create a new executor.
29
+ @executor ||= Bolt::Executor.new
30
+ end
31
+
32
+ def inventory
33
+ @inventory ||= Bolt::Inventory.new({}, config)
34
+ end
35
+
36
+ def run_task(opts)
37
+ result = pal.run_task(opts['task'],
38
+ 'localhost',
39
+ opts['parameters'] || {},
40
+ executor,
41
+ inventory).first
42
+
43
+ raise Bolt::Error.new(result.error_hash['msg'], result.error_hash['kind']) if result.error_hash
44
+ result
45
+ end
46
+
47
+ def validate_options(opts)
48
+ raise Bolt::ValidationError, "Task plugin requires that the 'task' is specified" unless opts['task']
49
+
50
+ task = pal.task_signature(opts['task'])
51
+
52
+ raise Bolt::ValidationError, "Could not find task #{opts['task']}" unless task
53
+
54
+ errors = []
55
+ unless task.runnable_with?(opts['parameters'] || {}) { |msg| errors << msg }
56
+ # This relies on runnable with printing a partial message before the first real error
57
+ raise Bolt::ValidationError, "Invalid parameters for #{errors.join("\n")}"
58
+ end
59
+ end
60
+ alias validate_inventory_config validate_options
61
+
62
+ def inventory_config(opts)
63
+ result = run_task(opts)
64
+
65
+ unless result.value.include?('config')
66
+ raise Bolt::ValidationError, "Task result did not return 'config': #{result.value}"
67
+ end
68
+
69
+ result['config']
70
+ end
71
+
72
+ def inventory_targets(opts)
73
+ raise Bolt::ValidationError, "Task plugin requires that the 'task' is specified" unless opts['task']
74
+
75
+ result = run_task(opts)
76
+
77
+ targets = result['targets']
78
+ unless targets.is_a?(Array)
79
+ raise Bolt::ValidationError, "Task result did not return a targets array: #{result.value}"
80
+ end
81
+
82
+ unless targets.all? { |t| t.is_a?(Hash) }
83
+ msg = "All targets returned by an inventory targets task must be hashes, got: #{targets}"
84
+ raise Bolt::ValidationError, msg
85
+ end
86
+
87
+ targets
88
+ end
89
+ end
90
+ end
91
+ end
@@ -22,7 +22,7 @@ module Bolt
22
22
  attr_writer :plan_context
23
23
 
24
24
  def self.options
25
- %w[host service-url cacert token-file task-environment]
25
+ %w[host service-url cacert token-file task-environment job-poll-interval job-poll-timeout]
26
26
  end
27
27
 
28
28
  def self.default_options
@@ -19,7 +19,7 @@ module Bolt
19
19
  def initialize(opts, plan_context, logger)
20
20
  @logger = logger
21
21
  @key = self.class.get_key(opts)
22
- client_keys = %w[service-url token-file cacert]
22
+ client_keys = %w[service-url token-file cacert job-poll-interval job-poll-timeout]
23
23
  client_opts = client_keys.each_with_object({}) do |k, acc|
24
24
  acc[k] = opts[k] if opts.include?(k)
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.25.0'
4
+ VERSION = '1.26.0'
5
5
  end
@@ -62,11 +62,7 @@ module BoltServer
62
62
 
63
63
  task = Bolt::Task::PuppetServer.new(body['task'], @file_cache)
64
64
  parameters = body['parameters'] || {}
65
- results = @executor.run_task(target, task, parameters)
66
-
67
- # Since this will only be on one node we can just return the first result
68
- result = scrub_stack_trace(results.first.status_hash)
69
- [200, result.to_json]
65
+ @executor.run_task(target, task, parameters)
70
66
  end
71
67
 
72
68
  get '/' do
@@ -105,10 +101,13 @@ module BoltServer
105
101
  opts.delete('private-key-content')
106
102
  end
107
103
  opts['load-config'] = false
108
-
109
104
  target = [Bolt::Target.new(body['target']['hostname'], opts)]
110
105
 
111
- method(params[:action]).call(target, body)
106
+ results = method(params[:action]).call(target, body)
107
+
108
+ # Since this will only be on one node we can just return the first result
109
+ result = scrub_stack_trace(results.first.status_hash)
110
+ [200, result.to_json]
112
111
  end
113
112
 
114
113
  post '/winrm/:action' do
@@ -121,10 +120,13 @@ module BoltServer
121
120
  return [400, error.to_json] unless error.nil?
122
121
 
123
122
  opts = body['target'].clone.merge('protocol' => 'winrm')
124
-
125
123
  target = [Bolt::Target.new(body['target']['hostname'], opts)]
126
124
 
127
- method(params[:action]).call(target, body)
125
+ results = method(params[:action]).call(target, body)
126
+
127
+ # Since this will only be on one node we can just return the first result
128
+ result = scrub_stack_trace(results.first.status_hash)
129
+ [200, result.to_json]
128
130
  end
129
131
 
130
132
  error 404 do
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: 1.25.0
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-27 00:00:00.000000000 Z
11
+ date: 2019-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -386,12 +386,20 @@ files:
386
386
  - lib/bolt/pal/yaml_plan/loader.rb
387
387
  - lib/bolt/pal/yaml_plan/parameter.rb
388
388
  - lib/bolt/pal/yaml_plan/step.rb
389
+ - lib/bolt/pal/yaml_plan/step/command.rb
390
+ - lib/bolt/pal/yaml_plan/step/eval.rb
391
+ - lib/bolt/pal/yaml_plan/step/plan.rb
392
+ - lib/bolt/pal/yaml_plan/step/resources.rb
393
+ - lib/bolt/pal/yaml_plan/step/script.rb
394
+ - lib/bolt/pal/yaml_plan/step/task.rb
395
+ - lib/bolt/pal/yaml_plan/step/upload.rb
389
396
  - lib/bolt/pal/yaml_plan/transpiler.rb
390
397
  - lib/bolt/plan_result.rb
391
398
  - lib/bolt/plugin.rb
392
399
  - lib/bolt/plugin/pkcs7.rb
393
400
  - lib/bolt/plugin/prompt.rb
394
401
  - lib/bolt/plugin/puppetdb.rb
402
+ - lib/bolt/plugin/task.rb
395
403
  - lib/bolt/plugin/terraform.rb
396
404
  - lib/bolt/puppetdb.rb
397
405
  - lib/bolt/puppetdb/client.rb