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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9fe11fc4842de35277e098cfe7740eb274df8aa5a6e1049cab373c31d196612
4
- data.tar.gz: 7884fb6bbc0292e0545a9429ba134dc3bbcb5045d86b924391cdafb523eba1f9
3
+ metadata.gz: aa6e4eb68e71946db22097bb174c21cfd49be4541980b8e0690a52e72a7a9330
4
+ data.tar.gz: 6e8cc701adcc4c3351e907e9d8550844bb014e3ab96d92afdca2115ed1b9ca41
5
5
  SHA512:
6
- metadata.gz: 8a9a4298796b099efb223ae4a9277a2a6b589f1f8d27267af0de768494579aa820bc3191338cb6eaa73c0f8a23112c48bef2e2037d5dea72d3b3d5204a0bb4ce
7
- data.tar.gz: 89c4a471a4b4c23e71cc1fb25df7fcd29e5b76374eb63564deca6b6a113c7c5c0c052d461c0277f668c1b5c8be644d3796c3de2f5078ce0127e32d4090591ec4
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
- 'Identifies the nodes to target.',
139
- 'Enter a comma-separated list of node URIs or group names.',
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
- @nodes.hide = @query.hide = @rerun.hide = false
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
- @nodes.hide = @query.hide = @rerun.hide = true
323
+ hide_target_opts
314
324
  PUPPETFILE_HELP
315
325
  when 'apply'
316
326
  @execute.hide = false
@@ -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
- options[:targets] = get_targets(options)
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 get_targets(options)
131
- target_opts = options.keys.select { |opt| %i[query rerun].include?(opt) }
132
- target_opts << :nodes unless options[:nodes].empty?
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 of '--nodes', '--rerun', or '--query' may be specified"
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
- nodes = query_puppetdb_nodes(options[:query])
139
- options[:nodes] = nodes if options[:subcommand] == 'plan'
140
- elsif options[:rerun]
141
- nodes = rerun.get_targets(options[:rerun])
142
- options[:nodes] = nodes if options[:subcommand] == 'plan'
143
- else
144
- nodes = options[:nodes]
145
- end
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[:nodes], 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'
@@ -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
- targets.reject { |target| target.is_a?(String) }.each do |target|
50
- unless target.is_a?(Hash)
51
- raise ValidationError.new("Node entry must be a String or Hash, not #{target.class}", @name)
52
- end
53
-
54
- target['name'] ||= target['uri']
55
-
56
- if target['name'].nil? || target['name'].empty?
57
- raise ValidationError.new("No name or uri for target: #{target}", @name)
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
@@ -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
- next unless step.key?('name')
117
+ validate_step_keys(step, step_number)
64
118
 
65
- unless step['name'].is_a?(String) && step['name'].match?(VAR_NAME_PATTERN)
66
- raise Bolt::Error.new("Invalid step name #{step['name'].inspect}", "bolt/invalid-plan")
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 used_names.include?(step['name'])
70
- msg = "Step name #{step['name'].inspect} matches an existing parameter or step name"
71
- raise Bolt::Error.new(msg, "bolt/invalid-plan")
72
- end
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
- used_names << step['name']
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, *extra_keys = STEP_KEYS.select { |key| step.key?(key) }
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)
@@ -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
@@ -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')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.17.0'
4
+ VERSION = '1.18.0'
5
5
  end
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.17.0
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-19 00:00:00.000000000 Z
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