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 +4 -4
- data/lib/bolt/bolt_option_parser.rb +2 -0
- data/lib/bolt/cli.rb +18 -2
- data/lib/bolt/pal.rb +9 -0
- data/lib/bolt/pal/yaml_plan.rb +31 -183
- data/lib/bolt/pal/yaml_plan/evaluator.rb +9 -12
- data/lib/bolt/pal/yaml_plan/parameter.rb +62 -0
- data/lib/bolt/pal/yaml_plan/step.rb +203 -0
- data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
- data/lib/bolt/util.rb +64 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/plan_executor/app.rb +9 -4
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d56ec91fb5d2a032a164f87f30ece4f103b93e3957dbff221ea775e5c52af0e1
|
4
|
+
data.tar.gz: cad130dc12fcba0d3fa39feb61603b9fe801373c04c62a9306958dff7bac13cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/bolt/cli.rb
CHANGED
@@ -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' ||
|
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"
|
data/lib/bolt/pal.rb
CHANGED
@@ -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}>}]
|
data/lib/bolt/pal/yaml_plan.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
raise Bolt::Error.new(
|
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
|
-
|
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,
|
21
|
+
task_step(scope, step_body)
|
25
22
|
when 'command'
|
26
|
-
command_step(scope,
|
23
|
+
command_step(scope, step_body)
|
27
24
|
when 'plan'
|
28
|
-
plan_step(scope,
|
25
|
+
plan_step(scope, step_body)
|
29
26
|
when 'script'
|
30
|
-
script_step(scope,
|
27
|
+
script_step(scope, step_body)
|
31
28
|
when 'source'
|
32
|
-
upload_file_step(scope,
|
29
|
+
upload_file_step(scope, step_body)
|
33
30
|
when 'eval'
|
34
|
-
eval_step(scope,
|
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
|
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
|
data/lib/bolt/util.rb
CHANGED
@@ -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)
|
data/lib/bolt/version.rb
CHANGED
data/lib/plan_executor/app.rb
CHANGED
@@ -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
|
-
#
|
96
|
-
|
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.
|
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-
|
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
|