bolt 1.17.0 → 1.18.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 +21 -11
- data/lib/bolt/cli.rb +27 -28
- data/lib/bolt/inventory.rb +4 -4
- data/lib/bolt/inventory/group2.rb +85 -50
- data/lib/bolt/inventory/inventory2.rb +2 -1
- data/lib/bolt/pal/yaml_plan.rb +151 -9
- data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -18
- data/lib/bolt/plugin.rb +29 -0
- data/lib/bolt/plugin/puppetdb.rb +24 -0
- data/lib/bolt/rerun.rb +1 -0
- data/lib/bolt/transport/sudoable.rb +1 -1
- data/lib/bolt/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa6e4eb68e71946db22097bb174c21cfd49be4541980b8e0690a52e72a7a9330
|
4
|
+
data.tar.gz: 6e8cc701adcc4c3351e907e9d8550844bb014e3ab96d92afdca2115ed1b9ca41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f7a793488438e0fb03700c536e1dbd680604eed52ea51ae03d1d7aa4da55203be5f4bb19bd481aceef92d0cbd30d531adf3e139a8cb36b9706e33afc249fe4f
|
7
|
+
data.tar.gz: a16943eac6c51d4e06df6bba47b81786053c3c1127ce26c3bbb5308ac88459c789ff3a327c24b192794f7e0c42222e5354b25faa4b8564d52e8bd1a7605b1abb
|
@@ -135,17 +135,23 @@ Usage: bolt apply <manifest.pp> [options]
|
|
135
135
|
@options = options
|
136
136
|
|
137
137
|
@nodes = define('-n', '--nodes NODES',
|
138
|
-
'
|
139
|
-
|
140
|
-
"Or read a node list from an input file '@<file>' or stdin '-'.",
|
141
|
-
'Example: --nodes localhost,node_group,ssh://nix.com:23,winrm://windows.puppet.com',
|
142
|
-
'URI format is [protocol://]host[:port]',
|
143
|
-
"SSH is the default protocol; may be #{TRANSPORTS.keys.join(', ')}",
|
144
|
-
'For Windows nodes, specify the winrm:// protocol if it has not be configured',
|
145
|
-
'For SSH, port defaults to `22`',
|
146
|
-
'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |nodes|
|
138
|
+
'Alias for --targets') do |nodes|
|
139
|
+
@options [:nodes] ||= []
|
147
140
|
@options[:nodes] << get_arg_input(nodes)
|
148
141
|
end.extend(SwitchHider)
|
142
|
+
@targets = define('-t', '--targets TARGETS',
|
143
|
+
'Identifies the targets of command.',
|
144
|
+
'Enter a comma-separated list of target URIs or group names.',
|
145
|
+
"Or read a target list from an input file '@<file>' or stdin '-'.",
|
146
|
+
'Example: --targets localhost,node_group,ssh://nix.com:23,winrm://windows.puppet.com',
|
147
|
+
'URI format is [protocol://]host[:port]',
|
148
|
+
"SSH is the default protocol; may be #{TRANSPORTS.keys.join(', ')}",
|
149
|
+
'For Windows targets, specify the winrm:// protocol if it has not be configured',
|
150
|
+
'For SSH, port defaults to `22`',
|
151
|
+
'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
|
152
|
+
@options[:targets] ||= []
|
153
|
+
@options[:targets] << get_arg_input(targets)
|
154
|
+
end.extend(SwitchHider)
|
149
155
|
@query = define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query|
|
150
156
|
@options[:query] = query
|
151
157
|
end.extend(SwitchHider)
|
@@ -290,9 +296,13 @@ Usage: bolt apply <manifest.pp> [options]
|
|
290
296
|
update
|
291
297
|
end
|
292
298
|
|
299
|
+
def hide_target_opts(toggle = true)
|
300
|
+
@nodes.hide = @query.hide = @rerun.hide = @targets.hide = toggle
|
301
|
+
end
|
302
|
+
|
293
303
|
def update
|
294
304
|
# show the --nodes, --query, and --rerun switches by default
|
295
|
-
|
305
|
+
hide_target_opts(false)
|
296
306
|
# Don't show the --execute switch except for `apply`
|
297
307
|
@execute.hide = true
|
298
308
|
|
@@ -310,7 +320,7 @@ Usage: bolt apply <manifest.pp> [options]
|
|
310
320
|
FILE_HELP
|
311
321
|
when 'puppetfile'
|
312
322
|
# Don't show targeting options for puppetfile
|
313
|
-
|
323
|
+
hide_target_opts
|
314
324
|
PUPPETFILE_HELP
|
315
325
|
when 'apply'
|
316
326
|
@execute.hide = false
|
data/lib/bolt/cli.rb
CHANGED
@@ -19,6 +19,7 @@ require 'bolt/rerun'
|
|
19
19
|
require 'bolt/logger'
|
20
20
|
require 'bolt/outputter'
|
21
21
|
require 'bolt/puppetdb'
|
22
|
+
require 'bolt/plugin'
|
22
23
|
require 'bolt/pal'
|
23
24
|
require 'bolt/target'
|
24
25
|
require 'bolt/version'
|
@@ -41,14 +42,12 @@ module Bolt
|
|
41
42
|
@logger = Logging.logger[self]
|
42
43
|
@argv = argv
|
43
44
|
@config = Bolt::Config.default
|
44
|
-
@options = {
|
45
|
-
nodes: []
|
46
|
-
}
|
45
|
+
@options = {}
|
47
46
|
end
|
48
47
|
|
49
48
|
# Only call after @config has been initialized.
|
50
49
|
def inventory
|
51
|
-
@inventory ||= Bolt::Inventory.from_config(config)
|
50
|
+
@inventory ||= Bolt::Inventory.from_config(config, plugins)
|
52
51
|
end
|
53
52
|
private :inventory
|
54
53
|
|
@@ -117,8 +116,11 @@ module Bolt
|
|
117
116
|
Bolt::Logger.configure(config.log, config.color)
|
118
117
|
|
119
118
|
# After validation, initialize inventory and targets. Errors here are better to catch early.
|
119
|
+
# After this step
|
120
|
+
# options[:target_args] will contain a string/array version of the targetting options this is passed to plans
|
121
|
+
# options[:targets] will contain a resolved set of Target objects
|
120
122
|
unless options[:subcommand] == 'puppetfile' || options[:action] == 'show'
|
121
|
-
|
123
|
+
update_targets(options)
|
122
124
|
end
|
123
125
|
|
124
126
|
options
|
@@ -127,23 +129,24 @@ module Bolt
|
|
127
129
|
raise e
|
128
130
|
end
|
129
131
|
|
130
|
-
def
|
131
|
-
target_opts =
|
132
|
-
|
132
|
+
def update_targets(options)
|
133
|
+
target_opts = options.keys.select { |opt| %i[query rerun nodes targets].include?(opt) }
|
134
|
+
target_string = "'--nodes', '--targets', '--rerun', or '--query'"
|
133
135
|
if target_opts.length > 1
|
134
|
-
raise Bolt::CLIError, "Only one
|
136
|
+
raise Bolt::CLIError, "Only one targeting option #{target_string} may be specified"
|
137
|
+
elsif target_opts.empty? && options[:subcommand] != 'plan'
|
138
|
+
raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
|
135
139
|
end
|
136
140
|
|
137
|
-
if options[:query]
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
inventory.get_targets(nodes)
|
141
|
+
nodes = if options[:query]
|
142
|
+
query_puppetdb_nodes(options[:query])
|
143
|
+
elsif options[:rerun]
|
144
|
+
rerun.get_targets(options[:rerun])
|
145
|
+
else
|
146
|
+
options[:targets] || options[:nodes] || []
|
147
|
+
end
|
148
|
+
options[:target_args] = nodes
|
149
|
+
options[:targets] = inventory.get_targets(nodes)
|
147
150
|
end
|
148
151
|
|
149
152
|
def validate(options)
|
@@ -184,14 +187,6 @@ module Bolt
|
|
184
187
|
end
|
185
188
|
end
|
186
189
|
|
187
|
-
if !%w[plan puppetfile].include?(options[:subcommand]) && options[:action] != 'show'
|
188
|
-
if options[:nodes].empty? && options[:query].nil? && options[:rerun].nil?
|
189
|
-
raise Bolt::CLIError, "Targets must be specified with '--nodes', '--query' or '--rerun'"
|
190
|
-
elsif options[:nodes].any? && options[:query]
|
191
|
-
raise Bolt::CLIError, "Only one of '--nodes', '--query', or '--rerun' may be specified"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
190
|
if options[:boltdir] && options[:configfile]
|
196
191
|
raise Bolt::CLIError, "Only one of '--boltdir' or '--configfile' may be specified"
|
197
192
|
end
|
@@ -227,6 +222,10 @@ module Bolt
|
|
227
222
|
@puppetdb_client = Bolt::PuppetDB::Client.new(puppetdb_config)
|
228
223
|
end
|
229
224
|
|
225
|
+
def plugins
|
226
|
+
@plugins ||= Bolt::Plugin.setup(config, puppetdb_client)
|
227
|
+
end
|
228
|
+
|
230
229
|
def query_puppetdb_nodes(query)
|
231
230
|
puppetdb_client.query_certnames(query)
|
232
231
|
end
|
@@ -284,7 +283,7 @@ module Bolt
|
|
284
283
|
|
285
284
|
case options[:subcommand]
|
286
285
|
when 'plan'
|
287
|
-
code = run_plan(options[:object], options[:task_options], options[:
|
286
|
+
code = run_plan(options[:object], options[:task_options], options[:target_args], options)
|
288
287
|
when 'puppetfile'
|
289
288
|
code = install_puppetfile(@config.puppetfile, @config.modulepath)
|
290
289
|
when 'apply'
|
data/lib/bolt/inventory.rb
CHANGED
@@ -43,7 +43,7 @@ module Bolt
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
def self.from_config(config)
|
46
|
+
def self.from_config(config, plugins = nil)
|
47
47
|
if ENV.include?(ENVIRONMENT_VAR)
|
48
48
|
begin
|
49
49
|
data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
|
@@ -54,18 +54,18 @@ module Bolt
|
|
54
54
|
data = Bolt::Util.read_config_file(config.inventoryfile, config.default_inventoryfile, 'inventory')
|
55
55
|
end
|
56
56
|
|
57
|
-
inventory = create_version(data, config)
|
57
|
+
inventory = create_version(data, config, plugins)
|
58
58
|
inventory.validate
|
59
59
|
inventory
|
60
60
|
end
|
61
61
|
|
62
|
-
def self.create_version(data, config)
|
62
|
+
def self.create_version(data, config, plugins)
|
63
63
|
version = (data || {}).delete('version') { 1 }
|
64
64
|
case version
|
65
65
|
when 1
|
66
66
|
new(data, config)
|
67
67
|
when 2
|
68
|
-
Bolt::Inventory::Inventory2.new(data, config)
|
68
|
+
Bolt::Inventory::Inventory2.new(data, config, plugins: plugins)
|
69
69
|
else
|
70
70
|
raise ValidationError, "Unsupported version #{version} specified in inventory"
|
71
71
|
end
|
@@ -13,7 +13,7 @@ module Bolt
|
|
13
13
|
|
14
14
|
DATA_KEYS = %w[name config facts vars features].freeze
|
15
15
|
NODE_KEYS = DATA_KEYS + %w[alias uri]
|
16
|
-
GROUP_KEYS = DATA_KEYS + %w[groups targets]
|
16
|
+
GROUP_KEYS = DATA_KEYS + %w[groups targets target-lookups]
|
17
17
|
CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
|
18
18
|
|
19
19
|
def initialize(data)
|
@@ -36,6 +36,8 @@ module Bolt
|
|
36
36
|
@features = fetch_value(data, 'features', Array)
|
37
37
|
@config = fetch_value(data, 'config', Hash)
|
38
38
|
|
39
|
+
@target_lookups = fetch_value(data, 'target-lookups', Array)
|
40
|
+
|
39
41
|
unless (unexpected_keys = @config.keys - CONFIG_KEYS).empty?
|
40
42
|
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for group #{@name}"
|
41
43
|
@logger.warn(msg)
|
@@ -46,58 +48,18 @@ module Bolt
|
|
46
48
|
|
47
49
|
@targets = {}
|
48
50
|
@aliases = {}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
if @targets.include?(target['name'])
|
61
|
-
@logger.warn("Ignoring duplicate target in #{@name}: #{target}")
|
62
|
-
next
|
63
|
-
end
|
64
|
-
|
65
|
-
raise ValidationError.new("Node #{target} does not have a name", @name) unless target['name']
|
66
|
-
@targets[target['name']] = target
|
67
|
-
|
68
|
-
unless (unexpected_keys = target.keys - NODE_KEYS).empty?
|
69
|
-
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{target['name']}"
|
70
|
-
@logger.warn(msg)
|
71
|
-
end
|
72
|
-
config_keys = target['config']&.keys || []
|
73
|
-
unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
|
74
|
-
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for target #{target['name']}"
|
75
|
-
@logger.warn(msg)
|
76
|
-
end
|
77
|
-
|
78
|
-
next unless target.include?('alias')
|
79
|
-
|
80
|
-
aliases = target['alias']
|
81
|
-
aliases = [aliases] if aliases.is_a?(String)
|
82
|
-
unless aliases.is_a?(Array)
|
83
|
-
msg = "Alias entry on #{target['name']} must be a String or Array, not #{aliases.class}"
|
84
|
-
raise ValidationError.new(msg, @name)
|
85
|
-
end
|
86
|
-
|
87
|
-
aliases.each do |alia|
|
88
|
-
raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
|
89
|
-
|
90
|
-
if (found = @aliases[alia])
|
91
|
-
raise ValidationError.new(alias_conflict(alia, found, target['name']), @name)
|
92
|
-
end
|
93
|
-
@aliases[alia] = target['name']
|
51
|
+
@name_or_alias = []
|
52
|
+
targets.each do |target|
|
53
|
+
# If target is a string, it can refer to either a target name or
|
54
|
+
# alias. Which can't be determined until all groups have been
|
55
|
+
# resolved, and requires a depth-first traversal to categorize them.
|
56
|
+
if target.is_a?(String)
|
57
|
+
@name_or_alias << target
|
58
|
+
else
|
59
|
+
add_target(target)
|
94
60
|
end
|
95
61
|
end
|
96
62
|
|
97
|
-
# If target is a string, it can refer to either a target name or alias. Which can't be determined
|
98
|
-
# until all groups have been resolved, and requires a depth-first traversal to categorize them.
|
99
|
-
@name_or_alias = targets.select { |target| target.is_a?(String) }
|
100
|
-
|
101
63
|
@groups = groups.map { |g| Group2.new(g) }
|
102
64
|
end
|
103
65
|
|
@@ -115,6 +77,79 @@ module Bolt
|
|
115
77
|
end
|
116
78
|
end
|
117
79
|
|
80
|
+
def add_target(target)
|
81
|
+
# TODO: Do we want to accept strings from lookup_targets plugins? How should
|
82
|
+
# they be handled?
|
83
|
+
unless target.is_a?(Hash)
|
84
|
+
raise ValidationError.new("Node entry must be a String or Hash, not #{target.class}", @name)
|
85
|
+
end
|
86
|
+
|
87
|
+
target['name'] ||= target['uri']
|
88
|
+
|
89
|
+
if target['name'].nil? || target['name'].empty?
|
90
|
+
raise ValidationError.new("No name or uri for target: #{target}", @name)
|
91
|
+
end
|
92
|
+
|
93
|
+
if @targets.include?(target['name'])
|
94
|
+
@logger.warn("Ignoring duplicate target in #{@name}: #{target}")
|
95
|
+
return
|
96
|
+
end
|
97
|
+
|
98
|
+
raise ValidationError.new("Node #{target} does not have a name", @name) unless target['name']
|
99
|
+
@targets[target['name']] = target
|
100
|
+
|
101
|
+
unless (unexpected_keys = target.keys - NODE_KEYS).empty?
|
102
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in target #{target['name']}"
|
103
|
+
@logger.warn(msg)
|
104
|
+
end
|
105
|
+
config_keys = target['config']&.keys || []
|
106
|
+
unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
|
107
|
+
msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for target #{target['name']}"
|
108
|
+
@logger.warn(msg)
|
109
|
+
end
|
110
|
+
|
111
|
+
unless target.include?('alias')
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
aliases = target['alias']
|
116
|
+
aliases = [aliases] if aliases.is_a?(String)
|
117
|
+
unless aliases.is_a?(Array)
|
118
|
+
msg = "Alias entry on #{target['name']} must be a String or Array, not #{aliases.class}"
|
119
|
+
raise ValidationError.new(msg, @name)
|
120
|
+
end
|
121
|
+
|
122
|
+
aliases.each do |alia|
|
123
|
+
raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
|
124
|
+
|
125
|
+
if (found = @aliases[alia])
|
126
|
+
raise ValidationError.new(alias_conflict(alia, found, target['name']), @name)
|
127
|
+
end
|
128
|
+
@aliases[alia] = target['name']
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def lookup_targets(plugins)
|
133
|
+
@target_lookups.each do |lookup|
|
134
|
+
unless lookup.is_a?(Hash)
|
135
|
+
raise ValidationError.new("target-lookup is not a hash: #{lookup}", @name)
|
136
|
+
end
|
137
|
+
unless lookup['plugin']
|
138
|
+
raise ValidationError.new("target-lookup does not specify a plugin: #{lookup}", @name)
|
139
|
+
end
|
140
|
+
|
141
|
+
unless (plugin = plugins.by_name(lookup['plugin']))
|
142
|
+
raise ValidationError.new("target-lookup specifies an unkown plugin: '#{lookup['plugin']}'", @name)
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
targets = plugin.lookup_targets(lookup)
|
147
|
+
targets.each { |target| add_target(target) }
|
148
|
+
end
|
149
|
+
|
150
|
+
@groups.each { |g| g.lookup_targets(plugins) }
|
151
|
+
end
|
152
|
+
|
118
153
|
def data_merge(data1, data2)
|
119
154
|
if data2.nil? || data1.nil?
|
120
155
|
return data2 || data1
|
@@ -12,7 +12,7 @@ module Bolt
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def initialize(data, config = nil, target_vars: {}, target_facts: {}, target_features: {})
|
15
|
+
def initialize(data, config = nil, plugins: nil, target_vars: {}, target_facts: {}, target_features: {})
|
16
16
|
@logger = Logging.logger[self]
|
17
17
|
# Config is saved to add config options to targets
|
18
18
|
@config = config || Bolt::Config.default
|
@@ -23,6 +23,7 @@ module Bolt
|
|
23
23
|
@target_facts = target_facts
|
24
24
|
@target_features = target_features
|
25
25
|
|
26
|
+
@groups.lookup_targets(plugins)
|
26
27
|
@groups.resolve_aliases(@groups.target_aliases, @groups.target_names)
|
27
28
|
collect_groups
|
28
29
|
end
|
data/lib/bolt/pal/yaml_plan.rb
CHANGED
@@ -3,6 +3,40 @@
|
|
3
3
|
module Bolt
|
4
4
|
class PAL
|
5
5
|
class YamlPlan
|
6
|
+
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
|
+
|
6
40
|
Parameter = Struct.new(:name, :value, :type_expr) do
|
7
41
|
def captures_rest
|
8
42
|
false
|
@@ -15,16 +49,35 @@ module Bolt
|
|
15
49
|
# Top-level plan keys aren't allowed to be Puppet code, so force them
|
16
50
|
# all to strings.
|
17
51
|
plan = Bolt::Util.walk_keys(plan) { |key| stringify(key) }
|
18
|
-
|
19
52
|
@name = name.freeze
|
20
53
|
|
21
54
|
# Nothing in parameters is allowed to be code, since no variables are defined yet
|
22
55
|
params_hash = stringify(plan.fetch('parameters', {}))
|
23
56
|
|
57
|
+
# Ensure params is a hash
|
58
|
+
unless params_hash.is_a?(Hash)
|
59
|
+
raise Bolt::Error.new("Plan parameters must be a Hash", "bolt/invalid-plan")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Validate top level plan keys
|
63
|
+
top_level_keys = plan.keys.to_set
|
64
|
+
unless PLAN_KEYS.superset?(top_level_keys)
|
65
|
+
invalid_keys = top_level_keys - PLAN_KEYS
|
66
|
+
raise Bolt::Error.new("Plan contains illegal key(s) #{invalid_keys.to_a.inspect}",
|
67
|
+
"bolt/invalid-plan")
|
68
|
+
end
|
69
|
+
|
24
70
|
# Munge parameters into an array of Parameter objects, which is what
|
25
71
|
# the Puppet API expects
|
26
72
|
@parameters = params_hash.map do |param, definition|
|
27
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
|
28
81
|
type = Puppet::Pops::Types::TypeParser.singleton.parse(definition['type']) if definition.key?('type')
|
29
82
|
Parameter.new(param, definition['default'], type)
|
30
83
|
end.freeze
|
@@ -49,6 +102,7 @@ module Bolt
|
|
49
102
|
end
|
50
103
|
|
51
104
|
used_names = Set.new
|
105
|
+
step_number = 1
|
52
106
|
|
53
107
|
# Parameters come in a hash, so they must be unique
|
54
108
|
@parameters.each do |param|
|
@@ -60,18 +114,28 @@ module Bolt
|
|
60
114
|
end
|
61
115
|
|
62
116
|
@steps.each do |step|
|
63
|
-
|
117
|
+
validate_step_keys(step, step_number)
|
64
118
|
|
65
|
-
|
66
|
-
|
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')
|
67
123
|
end
|
68
124
|
|
69
|
-
if
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
73
135
|
|
74
|
-
|
136
|
+
used_names << step['name']
|
137
|
+
end
|
138
|
+
step_number += 1
|
75
139
|
end
|
76
140
|
end
|
77
141
|
|
@@ -103,6 +167,84 @@ module Bolt
|
|
103
167
|
Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult')
|
104
168
|
end
|
105
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
|
+
|
106
248
|
# This class wraps a value parsed from YAML which may be Puppet code.
|
107
249
|
# That includes double-quoted strings and string literals, each of which
|
108
250
|
# subclasses this parent class in order to implement its own evaluation
|
@@ -17,10 +17,7 @@ module Bolt
|
|
17
17
|
def dispatch_step(scope, step)
|
18
18
|
step = evaluate_code_blocks(scope, step)
|
19
19
|
|
20
|
-
step_type
|
21
|
-
if !step_type || extra_keys.any?
|
22
|
-
unsupported_step(scope, step)
|
23
|
-
end
|
20
|
+
step_type = STEP_KEYS.find { |key| step.key?(key) }
|
24
21
|
|
25
22
|
case step_type
|
26
23
|
when 'task'
|
@@ -35,11 +32,6 @@ module Bolt
|
|
35
32
|
upload_file_step(scope, step)
|
36
33
|
when 'eval'
|
37
34
|
eval_step(scope, step)
|
38
|
-
else
|
39
|
-
# This shouldn't be able to happen since this case statement should
|
40
|
-
# match the STEP_KEYS list, but raise an error *just in case*,
|
41
|
-
# instead of silently skipping the step.
|
42
|
-
unsupported_step(scope, step)
|
43
35
|
end
|
44
36
|
end
|
45
37
|
|
@@ -48,7 +40,6 @@ module Bolt
|
|
48
40
|
target = step['target']
|
49
41
|
description = step['description']
|
50
42
|
params = step['parameters'] || {}
|
51
|
-
raise "Can't run a task without specifying a target" unless target
|
52
43
|
|
53
44
|
args = if description
|
54
45
|
[task, target, description, params]
|
@@ -73,7 +64,6 @@ module Bolt
|
|
73
64
|
target = step['target']
|
74
65
|
description = step['description']
|
75
66
|
arguments = step['arguments'] || []
|
76
|
-
raise "Can't run a script without specifying a target" unless target
|
77
67
|
|
78
68
|
options = { 'arguments' => arguments }
|
79
69
|
args = if description
|
@@ -89,7 +79,6 @@ module Bolt
|
|
89
79
|
command = step['command']
|
90
80
|
target = step['target']
|
91
81
|
description = step['description']
|
92
|
-
raise "Can't run a command without specifying a target" unless target
|
93
82
|
|
94
83
|
args = [command, target]
|
95
84
|
args << description if description
|
@@ -101,8 +90,6 @@ module Bolt
|
|
101
90
|
destination = step['destination']
|
102
91
|
target = step['target']
|
103
92
|
description = step['description']
|
104
|
-
raise "Can't upload a file without specifying a target" unless target
|
105
|
-
raise "Can't upload a file without specifying a destination" unless destination
|
106
93
|
|
107
94
|
args = [source, destination, target]
|
108
95
|
args << description if description
|
@@ -113,10 +100,6 @@ module Bolt
|
|
113
100
|
step['eval']
|
114
101
|
end
|
115
102
|
|
116
|
-
def unsupported_step(_scope, step)
|
117
|
-
raise Bolt::Error.new("Unsupported plan step", "bolt/unsupported-step", step: step)
|
118
|
-
end
|
119
|
-
|
120
103
|
# This is the method that Puppet calls to evaluate the plan. The name
|
121
104
|
# makes more sense for .pp plans.
|
122
105
|
def evaluate_block_with_bindings(closure_scope, args_hash, plan)
|
data/lib/bolt/plugin.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/plugin/puppetdb'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class Plugin
|
7
|
+
def self.setup(config, pdb_client)
|
8
|
+
plugins = new(config)
|
9
|
+
plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
|
10
|
+
plugins
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(_config)
|
14
|
+
@plugins = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_plugin(plugin)
|
18
|
+
@plugins[plugin.name] = plugin
|
19
|
+
end
|
20
|
+
|
21
|
+
def for_hook(hook)
|
22
|
+
@plugins.filter { |_n, plug| plug.hooks.include? hook }
|
23
|
+
end
|
24
|
+
|
25
|
+
def by_name(plugin_name)
|
26
|
+
@plugins[plugin_name]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bolt
|
4
|
+
class Plugin
|
5
|
+
class Puppetdb
|
6
|
+
def initialize(pdb_client)
|
7
|
+
@puppetdb_client = pdb_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def name
|
11
|
+
'puppetdb'
|
12
|
+
end
|
13
|
+
|
14
|
+
def hooks
|
15
|
+
['lookup_targets']
|
16
|
+
end
|
17
|
+
|
18
|
+
def lookup_targets(opts)
|
19
|
+
nodes = @puppetdb_client.query_certnames(opts['query'])
|
20
|
+
nodes.map { |certname| { 'uri' => certname } }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/bolt/rerun.rb
CHANGED
@@ -46,6 +46,7 @@ module Bolt
|
|
46
46
|
|
47
47
|
if result_set.is_a?(Bolt::ResultSet)
|
48
48
|
data = result_set.map { |res| res.status_hash.select { |k, _| %i[target status].include? k } }
|
49
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
49
50
|
File.write(@path, data.to_json)
|
50
51
|
elsif File.exist?(@path)
|
51
52
|
FileUtils.rm(@path)
|
@@ -44,7 +44,7 @@ module Bolt
|
|
44
44
|
conn.copy_file(source, tmpfile)
|
45
45
|
# pass over file ownership if we're using run-as to be a different user
|
46
46
|
dir.chown(conn.run_as)
|
47
|
-
result = conn.execute(['mv', tmpfile, destination], sudoable: true)
|
47
|
+
result = conn.execute(['mv', '-f', tmpfile, destination], sudoable: true)
|
48
48
|
if result.exit_code != 0
|
49
49
|
message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
|
50
50
|
raise Bolt::Node::FileError.new(message, 'MV_ERROR')
|
data/lib/bolt/version.rb
CHANGED
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.18.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-
|
11
|
+
date: 2019-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -381,6 +381,8 @@ files:
|
|
381
381
|
- lib/bolt/pal/yaml_plan/evaluator.rb
|
382
382
|
- lib/bolt/pal/yaml_plan/loader.rb
|
383
383
|
- lib/bolt/plan_result.rb
|
384
|
+
- lib/bolt/plugin.rb
|
385
|
+
- lib/bolt/plugin/puppetdb.rb
|
384
386
|
- lib/bolt/puppetdb.rb
|
385
387
|
- lib/bolt/puppetdb/client.rb
|
386
388
|
- lib/bolt/puppetdb/config.rb
|