bolt 1.18.0 → 1.19.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: aa6e4eb68e71946db22097bb174c21cfd49be4541980b8e0690a52e72a7a9330
4
- data.tar.gz: 6e8cc701adcc4c3351e907e9d8550844bb014e3ab96d92afdca2115ed1b9ca41
3
+ metadata.gz: d56ec91fb5d2a032a164f87f30ece4f103b93e3957dbff221ea775e5c52af0e1
4
+ data.tar.gz: cad130dc12fcba0d3fa39feb61603b9fe801373c04c62a9306958dff7bac13cd
5
5
  SHA512:
6
- metadata.gz: 6f7a793488438e0fb03700c536e1dbd680604eed52ea51ae03d1d7aa4da55203be5f4bb19bd481aceef92d0cbd30d531adf3e139a8cb36b9706e33afc249fe4f
7
- data.tar.gz: a16943eac6c51d4e06df6bba47b81786053c3c1127ce26c3bbb5308ac88459c789ff3a327c24b192794f7e0c42222e5354b25faa4b8564d52e8bd1a7605b1abb
6
+ metadata.gz: 566a3c7f134067783fb458db050eee4058c43d97ac44f87caf598589f9a6761b68e6150b65bac8cb8f63e65708d1c73a9391501244f87732ae89802f9a169302
7
+ data.tar.gz: c69f7762b1f76c311526ac63ed01c60d993b2b638acc84e76920fb4971edc41aec1bb7a227cc4475c35189733fe50748d9c1c556d2dd7e290d35df7bd7141f99
@@ -27,6 +27,7 @@ Available subcommands:
27
27
  bolt task show Show list of available tasks
28
28
  bolt task show <task> Show documentation for task
29
29
  bolt task run <task> [params] Run a Puppet task
30
+ bolt plan convert <plan_path> Convert a YAML plan to a Puppet plan
30
31
  bolt plan show Show list of available plans
31
32
  bolt plan show <plan> Show details for plan
32
33
  bolt plan run <plan> [params] Run a Puppet task plan
@@ -77,6 +78,7 @@ Available options are:
77
78
  Usage: bolt plan <action> <plan> [options] [parameters]
78
79
 
79
80
  Available actions are:
81
+ convert <plan_path> Convert a YAML plan to a Puppet plan
80
82
  show Show list of available plans
81
83
  show <plan> Show details for plan
82
84
  run Run a Puppet task plan
@@ -30,7 +30,7 @@ module Bolt
30
30
  COMMANDS = { 'command' => %w[run],
31
31
  'script' => %w[run],
32
32
  'task' => %w[show run],
33
- 'plan' => %w[show run],
33
+ 'plan' => %w[show run convert],
34
34
  'file' => %w[upload],
35
35
  'puppetfile' => %w[install show-modules],
36
36
  'apply' => %w[] }.freeze
@@ -119,7 +119,10 @@ module Bolt
119
119
  # After this step
120
120
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
121
121
  # options[:targets] will contain a resolved set of Target objects
122
- unless options[:subcommand] == 'puppetfile' || options[:action] == 'show'
122
+ unless options[:subcommand] == 'puppetfile' ||
123
+ options[:action] == 'show' ||
124
+ options[:action] == 'convert'
125
+
123
126
  update_targets(options)
124
127
  end
125
128
 
@@ -204,6 +207,10 @@ module Bolt
204
207
  if options[:subcommand] == 'apply' && (!options[:object] && !options[:code])
205
208
  raise Bolt::CLIError, "a manifest file or --execute is required"
206
209
  end
210
+
211
+ if options[:subcommand] == 'command' && (!options[:object] || options[:object].empty?)
212
+ raise Bolt::CLIError, "Must specify a command to run"
213
+ end
207
214
  end
208
215
 
209
216
  def handle_parser_errors
@@ -240,6 +247,11 @@ module Bolt
240
247
  exit!
241
248
  end
242
249
 
250
+ if options[:action] == 'convert'
251
+ convert_plan(options[:object])
252
+ return 0
253
+ end
254
+
243
255
  @analytics = Bolt::Analytics.build_client
244
256
  @analytics.bundled_content = bundled_content
245
257
 
@@ -452,6 +464,10 @@ module Bolt
452
464
  @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.compile_concurrency)
453
465
  end
454
466
 
467
+ def convert_plan(plan)
468
+ pal.convert_plan(plan)
469
+ end
470
+
455
471
  def validate_file(type, path, allow_dir = false)
456
472
  if path.nil?
457
473
  raise Bolt::CLIError, "A #{type} must be specified"
@@ -37,6 +37,8 @@ module Bolt
37
37
  end
38
38
  end
39
39
 
40
+ attr_reader :modulepath
41
+
40
42
  def initialize(modulepath, hiera_config, max_compiles = Etc.nprocessors)
41
43
  # Nothing works without initialized this global state. Reinitializing
42
44
  # is safe and in practice only happen in tests
@@ -83,6 +85,7 @@ module Bolt
83
85
  require 'bolt/pal/logging'
84
86
  require 'bolt/pal/issues'
85
87
  require 'bolt/pal/yaml_plan/loader'
88
+ require 'bolt/pal/yaml_plan/transpiler'
86
89
 
87
90
  # Now that puppet is loaded we can include puppet mixins in data types
88
91
  Bolt::ResultSet.include_iterable
@@ -294,6 +297,12 @@ module Bolt
294
297
  plan_info
295
298
  end
296
299
 
300
+ def convert_plan(plan_path)
301
+ Puppet[:tasks] = true
302
+ transpiler = YamlPlan::Transpiler.new
303
+ transpiler.transpile(plan_path)
304
+ end
305
+
297
306
  # Returns a mapping of all modules available to the Bolt compiler
298
307
  #
299
308
  # @return [Hash{String => Array<Hash{Symbol => String,nil}>}]
@@ -1,47 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/pal/yaml_plan/parameter'
4
+ require 'bolt/pal/yaml_plan/step'
5
+
3
6
  module Bolt
4
7
  class PAL
5
8
  class YamlPlan
6
9
  PLAN_KEYS = Set['parameters', 'steps', 'return', 'version']
7
- PARAMETER_KEYS = Set['type', 'default', 'description']
8
- COMMON_STEP_KEYS = %w[name description target].freeze
9
- STEP_KEYS = {
10
- 'command' => {
11
- 'allowed_keys' => Set['command'].merge(COMMON_STEP_KEYS),
12
- 'required_keys' => Set['target']
13
- },
14
- 'script' => {
15
- 'allowed_keys' => Set['script', 'parameters', 'arguments'].merge(COMMON_STEP_KEYS),
16
- 'required_keys' => Set['target']
17
- },
18
- 'task' => {
19
- 'allowed_keys' => Set['task', 'parameters'].merge(COMMON_STEP_KEYS),
20
- 'required_keys' => Set['target']
21
- },
22
- 'plan' => {
23
- 'allowed_keys' => Set['plan', 'parameters'].merge(COMMON_STEP_KEYS),
24
- 'required_keys' => Set.new
25
- },
26
- 'source' => {
27
- 'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
28
- 'required_keys' => Set['target', 'source', 'destination']
29
- },
30
- 'destination' => {
31
- 'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
32
- 'required_keys' => Set['target', 'source', 'destination']
33
- },
34
- 'eval' => {
35
- 'allowed_keys' => Set['eval', 'name', 'description'],
36
- 'required_keys' => Set.new
37
- }
38
- }.freeze
39
-
40
- Parameter = Struct.new(:name, :value, :type_expr) do
41
- def captures_rest
42
- false
43
- end
44
- end
10
+ VAR_NAME_PATTERN = /\A[a-z_][a-z0-9_]*\z/.freeze
45
11
 
46
12
  attr_reader :name, :parameters, :steps, :return
47
13
 
@@ -51,14 +17,18 @@ module Bolt
51
17
  plan = Bolt::Util.walk_keys(plan) { |key| stringify(key) }
52
18
  @name = name.freeze
53
19
 
54
- # Nothing in parameters is allowed to be code, since no variables are defined yet
55
20
  params_hash = stringify(plan.fetch('parameters', {}))
56
-
57
21
  # Ensure params is a hash
58
22
  unless params_hash.is_a?(Hash)
59
23
  raise Bolt::Error.new("Plan parameters must be a Hash", "bolt/invalid-plan")
60
24
  end
61
25
 
26
+ # Munge parameters into an array of Parameter objects, which is what
27
+ # the Puppet API expects
28
+ @parameters = params_hash.map do |param, definition|
29
+ Parameter.new(param, definition)
30
+ end.freeze
31
+
62
32
  # Validate top level plan keys
63
33
  top_level_keys = plan.keys.to_set
64
34
  unless PLAN_KEYS.superset?(top_level_keys)
@@ -67,75 +37,31 @@ module Bolt
67
37
  "bolt/invalid-plan")
68
38
  end
69
39
 
70
- # Munge parameters into an array of Parameter objects, which is what
71
- # the Puppet API expects
72
- @parameters = params_hash.map do |param, definition|
73
- definition ||= {}
74
- definition_keys = definition.keys.to_set
75
- unless PARAMETER_KEYS.superset?(definition_keys)
76
- invalid_keys = definition_keys - PARAMETER_KEYS
77
- raise Bolt::Error.new("Plan parameter #{param.inspect} contains illegal key(s)" \
78
- " #{invalid_keys.to_a.inspect}",
79
- "bolt/invalid-plan")
80
- end
81
- type = Puppet::Pops::Types::TypeParser.singleton.parse(definition['type']) if definition.key?('type')
82
- Parameter.new(param, definition['default'], type)
83
- end.freeze
40
+ unless plan['steps'].is_a?(Array)
41
+ raise Bolt::Error.new("Plan must specify an array of steps", "bolt/invalid-plan")
42
+ end
84
43
 
85
- @steps = plan['steps']&.map do |step|
44
+ used_names = Set.new(@parameters.map(&:name))
45
+
46
+ @steps = plan['steps'].each_with_index.map do |step, index|
86
47
  # Step keys also aren't allowed to be code and neither is the value of "name"
87
48
  stringified_step = Bolt::Util.walk_keys(step) { |key| stringify(key) }
88
49
  stringified_step['name'] = stringify(stringified_step['name']) if stringified_step.key?('name')
89
- stringified_step
90
- end.freeze
91
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)
54
+ used_names << stringified_step['name'] if stringified_step['name']
55
+ step
56
+ end.freeze
92
57
  @return = plan['return']
93
-
94
- validate
95
58
  end
96
59
 
97
- VAR_NAME_PATTERN = /\A[a-z_][a-z0-9_]*\z/.freeze
98
-
99
- def validate
100
- unless @steps.is_a?(Array)
101
- raise Bolt::Error.new("Plan must specify an array of steps", "bolt/invalid-plan")
102
- end
103
-
104
- used_names = Set.new
105
- step_number = 1
106
-
107
- # Parameters come in a hash, so they must be unique
108
- @parameters.each do |param|
109
- unless param.name.is_a?(String) && param.name.match?(VAR_NAME_PATTERN)
110
- raise Bolt::Error.new("Invalid parameter name #{param.name.inspect}", "bolt/invalid-plan")
111
- end
112
-
113
- used_names << param.name
114
- end
115
-
116
- @steps.each do |step|
117
- validate_step_keys(step, step_number)
118
-
119
- begin
120
- step.each { |k, v| validate_puppet_code(k, v) }
121
- rescue Bolt::Error => e
122
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], e.msg), 'bolt/invalid-plan')
123
- end
124
-
125
- if step.key?('name')
126
- unless step['name'].is_a?(String) && step['name'].match?(VAR_NAME_PATTERN)
127
- error_message = "Invalid step name: #{step['name'].inspect}"
128
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
129
- end
130
-
131
- if used_names.include?(step['name'])
132
- error_message = "Duplicate step name or parameter detected: #{step['name'].inspect}"
133
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
134
- end
135
-
136
- used_names << step['name']
137
- end
138
- step_number += 1
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)
64
+ raise Bolt::Error.new(err, "bolt/invalid-plan")
139
65
  end
140
66
  end
141
67
 
@@ -143,6 +69,10 @@ module Bolt
143
69
  self
144
70
  end
145
71
 
72
+ def return_type
73
+ Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult')
74
+ end
75
+
146
76
  # Turn all "potential" strings in the object into actual strings.
147
77
  # Because we interpret bare strings as potential Puppet code, even in
148
78
  # places where Puppet code isn't allowed (like some hash keys), we need
@@ -163,88 +93,6 @@ module Bolt
163
93
  end
164
94
  end
165
95
 
166
- def return_type
167
- Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult')
168
- end
169
-
170
- def step_err_msg(step_number, step_name, message)
171
- if step_name
172
- "Parse error in step number #{step_number} with name #{step_name.inspect}: \n #{message}"
173
- else
174
- "Parse error in step number #{step_number}: \n #{message}"
175
- end
176
- end
177
-
178
- def validate_step_keys(step, step_number)
179
- step_keys = step.keys.to_set
180
- action = step_keys.intersection(STEP_KEYS.keys.to_set).to_a
181
- unless action.count == 1
182
- if action.count > 1
183
- # Upload step is special in that it is identified by both `source` and `destination`
184
- unless action.to_set == Set['source', 'destination']
185
- error_message = "Multiple action keys detected: #{action.inspect}"
186
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
187
- end
188
- else
189
- error_message = "No valid action detected"
190
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
191
- end
192
- end
193
-
194
- # For validated step action, ensure only valid keys
195
- unless STEP_KEYS[action.first]['allowed_keys'].superset?(step_keys)
196
- illegal_keys = step_keys - STEP_KEYS[action.first]['allowed_keys']
197
- error_message = "The #{action.first.inspect} step does not support: #{illegal_keys.to_a.inspect} key(s)"
198
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
199
- end
200
-
201
- # Ensure all required keys are present
202
- STEP_KEYS[action.first]['required_keys'].each do |k|
203
- next if step_keys.include?(k)
204
- missing_keys = STEP_KEYS[action.first]['required_keys'] - step_keys
205
- error_message = "The #{action.first.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
206
- raise Bolt::Error.new(step_err_msg(step_number, step['name'], error_message), "bolt/invalid-plan")
207
- end
208
- end
209
-
210
- # Recursively ensure all puppet code can be parsed
211
- def validate_puppet_code(step_key, value)
212
- case value
213
- when Array
214
- value.map { |element| validate_puppet_code(step_key, element) }
215
- when Hash
216
- value.each_with_object({}) do |(k, v), o|
217
- key = k.is_a?(EvaluableString) ? k.value : k
218
- o[key] = validate_puppet_code(key, v)
219
- end
220
- # CodeLiterals can be parsed directly
221
- when CodeLiteral
222
- parse_code_string(value.value)
223
- # BareString is parsed directly if it starts with '$'
224
- when BareString
225
- if value.value.start_with?('$')
226
- parse_code_string(value.value)
227
- else
228
- parse_code_string(value.value, true)
229
- end
230
- when EvaluableString
231
- # Must quote parsed strings to evaluate them
232
- parse_code_string(value.value, true)
233
- end
234
- rescue Puppet::Error => e
235
- raise Bolt::Error.new("Error parsing #{step_key.inspect}: #{e.basic_message}", "bolt/invalid-plan")
236
- end
237
-
238
- # Parses the an evaluable string, optionally quote it before parsing
239
- def parse_code_string(code, quote = false)
240
- if quote
241
- quoted = Puppet::Pops::Parser::EvaluatingParser.quote(code)
242
- Puppet::Pops::Parser::EvaluatingParser.new.parse_string(quoted)
243
- else
244
- Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code)
245
- end
246
- end
247
-
248
96
  # This class wraps a value parsed from YAML which may be Puppet code.
249
97
  # That includes double-quoted strings and string literals, each of which
250
98
  # subclasses this parent class in order to implement its own evaluation
@@ -12,26 +12,23 @@ module Bolt
12
12
  @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
13
  end
14
14
 
15
- STEP_KEYS = %w[task command eval script source plan].freeze
16
-
17
15
  def dispatch_step(scope, step)
18
- step = evaluate_code_blocks(scope, step)
19
-
20
- step_type = STEP_KEYS.find { |key| step.key?(key) }
16
+ step_type = step.type
17
+ step_body = evaluate_code_blocks(scope, step.body)
21
18
 
22
19
  case step_type
23
20
  when 'task'
24
- task_step(scope, step)
21
+ task_step(scope, step_body)
25
22
  when 'command'
26
- command_step(scope, step)
23
+ command_step(scope, step_body)
27
24
  when 'plan'
28
- plan_step(scope, step)
25
+ plan_step(scope, step_body)
29
26
  when 'script'
30
- script_step(scope, step)
27
+ script_step(scope, step_body)
31
28
  when 'source'
32
- upload_file_step(scope, step)
29
+ upload_file_step(scope, step_body)
33
30
  when 'eval'
34
- eval_step(scope, step)
31
+ eval_step(scope, step_body)
35
32
  end
36
33
  end
37
34
 
@@ -107,7 +104,7 @@ module Bolt
107
104
  plan.steps.each do |step|
108
105
  step_result = dispatch_step(scope, step)
109
106
 
110
- scope.setvar(step['name'], step_result) if step.key?('name')
107
+ scope.setvar(step.name, step_result) if step.name
111
108
  end
112
109
 
113
110
  evaluate_code_blocks(scope, plan.return)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Parameter
7
+ attr_reader :name, :value, :type_expr
8
+
9
+ PARAMETER_KEYS = Set['type', 'default', 'description']
10
+
11
+ def initialize(param, definition)
12
+ definition ||= {}
13
+ validate_param(param, definition)
14
+
15
+ @name = param
16
+ @value = definition['default']
17
+ @type_expr = Puppet::Pops::Types::TypeParser.singleton.parse(definition['type']) if definition['type']
18
+ end
19
+
20
+ def validate_param(param, definition)
21
+ unless param.is_a?(String) && param.match?(Bolt::PAL::YamlPlan::VAR_NAME_PATTERN)
22
+ raise Bolt::Error.new("Invalid parameter name #{param.inspect}", "bolt/invalid-plan")
23
+ end
24
+
25
+ definition_keys = definition.keys.to_set
26
+ unless PARAMETER_KEYS.superset?(definition_keys)
27
+ invalid_keys = definition_keys - PARAMETER_KEYS
28
+ raise Bolt::Error.new("Plan parameter #{param.inspect} contains illegal key(s)" \
29
+ " #{invalid_keys.to_a.inspect}",
30
+ "bolt/invalid-plan")
31
+ end
32
+ end
33
+
34
+ def captures_rest
35
+ false
36
+ end
37
+
38
+ def transpile
39
+ result = String.new
40
+ result << "\n\s\s"
41
+
42
+ # Param type
43
+ if @type_expr.respond_to?(:type_string)
44
+ result << @type_expr.type_string + " "
45
+ elsif !@type_expr.nil?
46
+ result << @type_expr.to_s + " "
47
+ end
48
+
49
+ # Param name
50
+ result << "$#{@name}"
51
+
52
+ # Param default
53
+ if @value
54
+ default = @type_expr.to_s =~ /String/ ? "'#{@value}'" : @value
55
+ result << " = #{default}"
56
+ end
57
+ result
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/util'
4
+
5
+ module Bolt
6
+ class PAL
7
+ class YamlPlan
8
+ class Step
9
+ attr_reader :name, :type, :body, :target
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']
52
+ end
53
+
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.include?("\n")
81
+ # A little indented niceness
82
+ indented = code.gsub(/\n/, "\n ").chomp(" ")
83
+ result << "with() || {\n #{indented}}"
84
+ else
85
+ result << code
86
+ 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
+ end
91
+ result << "\n"
92
+ result
93
+ end
94
+
95
+ def validate_step
96
+ validate_step_keys
97
+
98
+ begin
99
+ @body.each { |k, v| validate_puppet_code(k, v) }
100
+ rescue Bolt::Error => e
101
+ err = step_err_msg(e.msg)
102
+ raise Bolt::Error.new(err, 'bolt/invalid-plan')
103
+ end
104
+
105
+ unless body.fetch('parameters', {}).is_a?(Hash)
106
+ msg = "Parameters key must be a hash"
107
+ raise Bolt::Error.new(step_err_msg(msg), "bolt/invalid-plan")
108
+ end
109
+
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")
115
+ end
116
+ end
117
+ end
118
+
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
136
+
137
+ # 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)
142
+ raise Bolt::Error.new(err, "bolt/invalid-plan")
143
+ end
144
+
145
+ # 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)
151
+ raise Bolt::Error.new(err, "bolt/invalid-plan")
152
+ end
153
+ end
154
+
155
+ # Recursively ensure all puppet code can be parsed
156
+ def validate_puppet_code(step_key, value)
157
+ case value
158
+ when Array
159
+ value.map { |element| validate_puppet_code(step_key, element) }
160
+ when Hash
161
+ value.each_with_object({}) do |(k, v), o|
162
+ key = k.is_a?(Bolt::PAL::YamlPlan::EvaluableString) ? k.value : k
163
+ o[key] = validate_puppet_code(key, v)
164
+ end
165
+ # CodeLiterals can be parsed directly
166
+ when Bolt::PAL::YamlPlan::CodeLiteral
167
+ parse_code_string(value.value)
168
+ # BareString is parsed directly if it starts with '$'
169
+ when Bolt::PAL::YamlPlan::BareString
170
+ if value.value.start_with?('$')
171
+ parse_code_string(value.value)
172
+ else
173
+ parse_code_string(value.value, true)
174
+ end
175
+ when Bolt::PAL::YamlPlan::EvaluableString
176
+ # Must quote parsed strings to evaluate them
177
+ parse_code_string(value.value, true)
178
+ end
179
+ rescue Puppet::Error => e
180
+ raise Bolt::Error.new("Error parsing #{step_key.inspect}: #{e.basic_message}", "bolt/invalid-plan")
181
+ end
182
+
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
189
+ end
190
+
191
+ # Parses the an evaluable string, optionally quote it before parsing
192
+ def parse_code_string(code, quote = false)
193
+ if quote
194
+ quoted = Puppet::Pops::Parser::EvaluatingParser.quote(code)
195
+ Puppet::Pops::Parser::EvaluatingParser.new.parse_string(quoted)
196
+ else
197
+ Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/pal/yaml_plan/loader'
5
+ require 'bolt/util'
6
+
7
+ module Bolt
8
+ class PAL
9
+ class YamlPlan
10
+ class Transpiler
11
+ class ConvertError < Bolt::Error
12
+ def initialize(msg, plan_path)
13
+ super(msg, 'bolt/convert-error', { "plan_path" => plan_path })
14
+ end
15
+ end
16
+
17
+ def transpile(relative_path)
18
+ @plan_path = File.expand_path(relative_path)
19
+ @modulename = Bolt::Util.module_name(@plan_path)
20
+ @filename = @plan_path.split(File::SEPARATOR)[-1]
21
+ validate_path
22
+
23
+ plan_object = parse_plan
24
+
25
+ plan_string = String.new("# WARNING: This is an autogenerated plan. " \
26
+ "It may not behave as expected.\n" \
27
+ "plan #{plan_object.name}(")
28
+ # Parameters are Bolt::PAL::YamlPlan::Parameter
29
+ plan_object.parameters&.each_with_index do |param, i|
30
+ plan_string << param.transpile
31
+
32
+ # If it's the last parameter add a newline and no comma
33
+ last = i + 1 == plan_object.parameters.length ? "\n" : ","
34
+ # This encodes strangely if we << directly to plan_string
35
+ plan_string << last
36
+ end
37
+ plan_string << ") {\n"
38
+
39
+ plan_object.steps&.each do |step|
40
+ # This only needs the plan path for raising errors
41
+ plan_string << step.transpile(@plan_path)
42
+ end
43
+
44
+ plan_string << "\n return #{Bolt::Util.to_code(plan_object.return)}\n" if plan_object.return
45
+ plan_string << "}"
46
+ # We always print the plan, even if there's an error
47
+ puts plan_string
48
+ validate_plan(plan_string)
49
+ plan_string
50
+ end
51
+
52
+ # Save me from all these rescue statements...
53
+ def parse_plan
54
+ begin
55
+ file_contents = File.read(@plan_path)
56
+ rescue Errno::ENOENT
57
+ msg = "Could not read yaml plan file: #{@plan_path}"
58
+ raise Bolt::FileError.new(msg, @plan_path)
59
+ end
60
+
61
+ begin
62
+ result = Bolt::PAL::YamlPlan::Loader.parse_plan(file_contents, @plan_path)
63
+ rescue Error => e
64
+ raise ConvertError.new("Failed to convert yaml plan: #{e.message}", @plan_path)
65
+ end
66
+
67
+ unless result.is_a?(Hash)
68
+ type = result.class.name
69
+ raise ArgumentError, "The data loaded from #{source_ref} does not contain an object - its type is #{type}"
70
+ end
71
+
72
+ Bolt::PAL::YamlPlan.new(@modulename, result).freeze
73
+ end
74
+
75
+ def validate_path
76
+ unless File.extname(@filename) == ".yaml"
77
+ raise ConvertError.new("You can only convert plans written in yaml", @plan_path)
78
+ end
79
+ end
80
+
81
+ def validate_plan(plan)
82
+ Puppet::Pops::Parser::EvaluatingParser.new.parse_string(plan)
83
+ rescue Puppet::Error => e
84
+ $stderr.puts "The converted puppet plan contains invalid puppet code: #{e.message}"
85
+ exit 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -35,6 +35,70 @@ module Bolt
35
35
  raise Bolt::FileError.new("Could not read #{file_name} file: #{path}", path)
36
36
  end
37
37
 
38
+ # Accepts a path with either 'plans' or 'tasks' in it and determines
39
+ # the name of the module
40
+ def module_name(path)
41
+ # Remove extra dots and slashes
42
+ path = Pathname.new(path).cleanpath.to_s
43
+ fs = File::SEPARATOR
44
+ regex = Regexp.new("#{fs}plans#{fs}|#{fs}tasks#{fs}")
45
+
46
+ # Only accept paths with '/plans/' or '/tasks/'
47
+ unless path.match?(regex)
48
+ msg = "Could not determine module from #{path}. "\
49
+ "The path must include 'plans' or 'tasks' directory"
50
+ raise Bolt::Error.new(msg, 'bolt/modulepath-error')
51
+ end
52
+
53
+ # Split the path on the first instance of /plans/ or /tasks/
54
+ parts = path.split(regex, 2)
55
+ # Module name is the last entry before 'plans' or 'tasks'
56
+ modulename = parts[0].split(fs)[-1]
57
+ filename = File.basename(path).split('.')[0]
58
+ # Remove "/init.*" if filename is init or just remove the file
59
+ # extension
60
+ if filename == 'init'
61
+ parts[1].chomp!(File.basename(path))
62
+ else
63
+ parts[1].chomp!(File.extname(path))
64
+ end
65
+
66
+ # The plan or task name is the rest of the path
67
+ [modulename, parts[1].split(fs)].flatten.join('::')
68
+ end
69
+
70
+ def to_code(string)
71
+ case string
72
+ when Bolt::PAL::YamlPlan::DoubleQuotedString
73
+ string.value.inspect
74
+ when Bolt::PAL::YamlPlan::BareString
75
+ if string.value.start_with?('$')
76
+ string.value.to_s
77
+ else
78
+ "'#{string.value}'"
79
+ end
80
+ when Bolt::PAL::YamlPlan::EvaluableString, Bolt::PAL::YamlPlan::CodeLiteral
81
+ string.value.to_s
82
+ when String
83
+ "'#{string}'"
84
+ when Hash
85
+ formatted = String.new("{")
86
+ string.each do |k, v|
87
+ formatted << "#{to_code(k)} => #{to_code(v)}, "
88
+ end
89
+ formatted.chomp!(", ")
90
+ formatted << "}"
91
+ formatted
92
+ when Array
93
+ formatted = String.new("[")
94
+ formatted << string.map { |str| to_code(str) }.join(', ')
95
+ formatted << "]"
96
+ formatted
97
+ else
98
+ string
99
+ end
100
+ end
101
+
38
102
  def deep_merge(hash1, hash2)
39
103
  recursive_merge = proc do |_key, h1, h2|
40
104
  if h1.is_a?(Hash) && h2.is_a?(Hash)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.18.0'
4
+ VERSION = '1.19.0'
5
5
  end
@@ -92,9 +92,15 @@ module PlanExecutor
92
92
  error = validate_schema(@schema, body)
93
93
  return [400, error.to_json] unless error.nil?
94
94
  name = body['plan_name']
95
- # Errors if plan is not found
96
- @pal.get_plan_info(name)
97
-
95
+ # We need to wrap all calls to @pal (not just plan_run) in a future
96
+ # to ensure that the process always uses the SingleThreadExecutor
97
+ # worker and forces one call to @pal at a time regardless of the number
98
+ # of concurrent calls to POST /plan_run
99
+ result = Concurrent::Future.execute(executor: @worker) do
100
+ @pal.get_plan_info(name)
101
+ end
102
+ # .value! will fail if the internal process of the thread fails
103
+ result.value!
98
104
  executor = PlanExecutor::Executor.new(body['job_id'], @http_client)
99
105
  applicator = PlanExecutor::Applicator.new(@inventory, executor, nil)
100
106
  params = body['params']
@@ -104,7 +110,6 @@ module PlanExecutor
104
110
  executor.finish_plan(pal_result)
105
111
  pal_result
106
112
  end
107
-
108
113
  [200, { status: 'running' }.to_json]
109
114
  end
110
115
 
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.18.0
4
+ version: 1.19.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-04-25 00:00:00.000000000 Z
11
+ date: 2019-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -380,6 +380,9 @@ files:
380
380
  - lib/bolt/pal/yaml_plan.rb
381
381
  - lib/bolt/pal/yaml_plan/evaluator.rb
382
382
  - lib/bolt/pal/yaml_plan/loader.rb
383
+ - lib/bolt/pal/yaml_plan/parameter.rb
384
+ - lib/bolt/pal/yaml_plan/step.rb
385
+ - lib/bolt/pal/yaml_plan/transpiler.rb
383
386
  - lib/bolt/plan_result.rb
384
387
  - lib/bolt/plugin.rb
385
388
  - lib/bolt/plugin/puppetdb.rb