Skip to content

Commit 30eb257

Browse files
authored
Merge pull request #36 from github/kpaulisse-validate-references
Validate references
2 parents 81a15a6 + be046f0 commit 30eb257

36 files changed

Lines changed: 3133 additions & 15 deletions

doc/advanced-catalog-validation.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Catalog validation
2+
3+
`octocatalog-diff` contains additional functionality to validate catalogs, based on configurable criteria.
4+
5+
Catalog validation features include:
6+
7+
- Validate references: Ensure resources targeted by `before`, `notify`, `require`, and/or `subscribe` exist in the catalog
8+
9+
## Validate references
10+
11+
`octocatalog-diff` includes the ability to validate references by ensuring resources targeted by `before`, `notify`, `require`, and/or `subscribe` parameters also exist in the catalog.
12+
13+
Consider the following Puppet code:
14+
15+
```
16+
file { '/usr/local/bin/some-script.sh':
17+
source => 'puppet:///modules/test/usr/local/bin/some-script.sh',
18+
notify => Exec['execute /usr/local/bin/some-script.sh'],
19+
}
20+
```
21+
22+
The catalog for this code would build, whether or not the `exec { 'execute /usr/local/bin/some-script.sh': ... }` resource was part of the catalog. However, when the catalog is applied on the Puppet agent, it would fail if this resource is missing.
23+
24+
With the `--validate-references` command line flag (or the `settings[:validate_references]` [configuration setting](/doc/configuration.md)), you can instruct `octocatalog-diff` to confirm that any resource targeted by a `before`, `notify`, `require`, and `subscribe` parameter actually exists. If the resource is missing from the catalog, an error will be raised, just as if the catalog failed to compile.
25+
26+
The command line argument is demonstrated here:
27+
28+
```
29+
# Validate all references: before,notify,require,subscribe
30+
octocatalog-diff ... --validate-references before,notify,require,subscribe
31+
32+
# Validate some references: only before and require
33+
octocatalog-diff ... --validate-references before,require
34+
35+
# Validate no references
36+
octocatalog-diff ... --no-validate-references
37+
```
38+
39+
By default, no references are validated.
40+
41+
Note as well, when using `octocatalog-diff` to compare two catalogs, the references in the "from" catalog are not checked. The reason for this design decision is as follows: the "from" catalog is generally what is considered to be stable and is perhaps already deployed, so it adds no value (and perhaps inhibits the ability to develop further) if `octocatalog-diff` fails just because references in the "from" catalog are broken.

doc/advanced.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ See also:
2121
- [Overriding facts](/doc/advanced-override-facts.md)
2222
- [Puppet Enterprise node classification service](/doc/advanced-pe-enc.md)
2323
- [Using `octocatalog-diff` without git](/doc/advanced-using-without-git.md)
24+
- [Catalog validation](/doc/advanced-catalog-validation.md)
2425

2526
### Controlling output
2627

doc/optionsref.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Usage: octocatalog-diff [command line options]
4747
--ignore-attr "attr1,attr2,..."
4848
Attributes to ignore
4949
--[no-]display-source Show source file and line for each difference
50+
--[no-]validate-references "before,require,subscribe,notify"
51+
References to validate
5052
--[no-]compare-file-text Compare text, not source location, of file resources
5153
--[no-]storeconfigs Enable integration with puppetdb for collected resources
5254
--retry-failed-catalog N Retry building a failed catalog N times
@@ -1028,4 +1030,19 @@ These files must exist and be in Puppet catalog format. (<a href="../lib/octocat
10281030
</td>
10291031
</tr>
10301032

1033+
<tr>
1034+
<td valign=top>
1035+
<pre><code>--validate-references
1036+
--no-validate-references </code></pre>
1037+
</td>
1038+
<td valign=top>
1039+
References to validate
1040+
</td>
1041+
<td valign=top>
1042+
Confirm that each `before`, `require`, `subscribe`, and/or `notify` points to a valid
1043+
resource in the catalog. This value should be specified as an array of which of these
1044+
parameters are to be checked. (<a href="../lib/octocatalog-diff/catalog-diff/cli/options/validate_references.rb">validate_references.rb</a>)
1045+
</td>
1046+
</tr>
1047+
10311048
</table>

examples/octocatalog-diff.cfg.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ def self.config
200200

201201
settings[:from_env] = 'origin/master'
202202

203+
##############################################################################################
204+
# validate_references
205+
# Set this to an array containing 1 or more of: `before`, `notify`, `require`, `subscribe`
206+
# to validate references in the "to" catalog. This will cause the catalog to fail if it
207+
# contains references to resources that aren't in the catalog. If you don't want to validate
208+
# any references, set this to an empty array, or just comment out the line.
209+
##############################################################################################
210+
211+
settings[:validate_references] = %w(before notify require subscribe)
212+
203213
##############################################################################################
204214
# Less commonly changed settings
205215
##############################################################################################

lib/octocatalog-diff/catalog-diff/cli/catalogs.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def build_catalog_tasks
157157
task = OctocatalogDiff::Util::Parallel::Task.new(
158158
method: method(:build_catalog),
159159
validator: method(:catalog_validator),
160+
validator_args: { task: key },
160161
description: "build_catalog for #{@options["#{key}_env".to_sym]}",
161162
args: args
162163
)
@@ -231,9 +232,12 @@ def build_catalog(opts, logger = @logger)
231232

232233
# Validate a catalog in the parallel execution
233234
# @param catalog [OctocatalogDiff::Catalog] Catalog object
235+
# @param logger [Logger] Logger object (presently unused)
236+
# @param args [Hash] Additional arguments set specifically for validator
234237
# @return [Boolean] true if catalog is valid, false otherwise
235-
def catalog_validator(catalog = nil, _logger = @logger)
238+
def catalog_validator(catalog = nil, _logger = @logger, args = {})
236239
return false unless catalog.is_a?(OctocatalogDiff::Catalog)
240+
catalog.validate_references if args[:task] == :to
237241
catalog.valid?
238242
end
239243
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
# Confirm that each `before`, `require`, `subscribe`, and/or `notify` points to a valid
4+
# resource in the catalog. This value should be specified as an array of which of these
5+
# parameters are to be checked.
6+
# @param parser [OptionParser object] The OptionParser argument
7+
# @param options [Hash] Options hash being constructed; this is modified in this method.
8+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:validate_references) do
9+
has_weight 205
10+
11+
def parse(parser, options)
12+
parser.on('--[no-]validate-references "before,require,subscribe,notify"', Array, 'References to validate') do |res|
13+
if res == false
14+
options[:validate_references] = []
15+
else
16+
options[:validate_references] ||= []
17+
res.each do |item|
18+
unless %w(before require subscribe notify).include?(item)
19+
raise ArgumentError, "Invalid reference validation #{item}"
20+
end
21+
22+
options[:validate_references] << item
23+
end
24+
end
25+
end
26+
end
27+
end

lib/octocatalog-diff/catalog.rb

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Catalog
2121
# Error classes that we can throw
2222
class PuppetVersionError < RuntimeError; end
2323
class CatalogError < RuntimeError; end
24+
class ReferenceValidationError < RuntimeError; end
2425

2526
# Constructor
2627
# @param :backend [Symbol] If set, this will force a backend
@@ -157,7 +158,7 @@ def resources
157158
# This is a bug condition
158159
# :nocov:
159160
raise "BUG: catalog has no data::resources or ::resources array. Please report this. #{@catalog.inspect}"
160-
# :nocov
161+
# :nocov:
161162
end
162163

163164
# This retrieves the number of retries necessary to compile the catalog. If the underlying catalog
@@ -173,8 +174,66 @@ def valid?
173174
!@catalog.nil?
174175
end
175176

177+
# Determine if all of the (before, notify, require, subscribe) targets are actually in the catalog.
178+
# Raise a ReferenceValidationError for any found to be missing.
179+
# Uses @options[:validate_references] to influence which references are checked.
180+
def validate_references
181+
# Skip out early if no reference validation has been requested.
182+
unless @options[:validate_references].is_a?(Array) && @options[:validate_references].any?
183+
return
184+
end
185+
186+
# Iterate over all the resources and check each one that has one of the attributes being checked.
187+
# Keep track of all references that are missing for ultimate inclusion in the error message.
188+
missing = []
189+
resources.each do |x|
190+
@options[:validate_references].each do |r|
191+
next unless x.key?('parameters')
192+
next unless x['parameters'].key?(r)
193+
missing_resources = resources_missing_from_catalog(x['parameters'][r])
194+
next unless missing_resources.any?
195+
missing.concat missing_resources.map { |missing_target| { source: x, target_type: r, target_value: missing_target } }
196+
end
197+
end
198+
return if missing.empty?
199+
200+
# At this point there is at least one broken/missing reference. Format an error message and
201+
# raise. Error message will look like this:
202+
# ---
203+
# Catalog has broken references: exec[subscribe caller 1] -> subscribe[Exec[subscribe target]];
204+
# exec[subscribe caller 2] -> subscribe[Exec[subscribe target]]; exec[subscribe caller 2] ->
205+
# subscribe[Exec[subscribe target 2]]
206+
# ---
207+
formatted_references = missing.map do |obj|
208+
# obj[:target_value] can be a string or an array. If it's an array, create a
209+
# separate error message per element of that array. This allows the total number
210+
# of errors to be correct.
211+
src = "#{obj[:source]['type'].downcase}[#{obj[:source]['title']}]"
212+
target_val = obj[:target_value].is_a?(Array) ? obj[:target_value] : [obj[:target_value]]
213+
target_val.map { |tv| "#{src} -> #{obj[:target_type].downcase}[#{tv}]" }
214+
end
215+
formatted_references.flatten!
216+
plural = formatted_references.size == 1 ? '' : 's'
217+
errors = formatted_references.join('; ')
218+
raise ReferenceValidationError, "Catalog has broken reference#{plural}: #{errors}"
219+
end
220+
176221
private
177222

223+
# Private method: Given a list of resources to check, return the references from
224+
# that list that are missing from the catalog. (An empty array returned would indicate
225+
# all references are present in the catalog.)
226+
# @param resources_to_check [String / Array] Resources to check
227+
# @return [Array] References that are missing from catalog
228+
def resources_missing_from_catalog(resources_to_check)
229+
[resources_to_check].flatten.select do |res|
230+
unless res =~ /\A([\w:]+)\[(.+)\]\z/
231+
raise ArgumentError, "Resource #{res} is not in the expected format"
232+
end
233+
resource(type: Regexp.last_match(1), title: Regexp.last_match(2)).nil?
234+
end
235+
end
236+
178237
# Private method: Choose backend based on passed-in options
179238
# @param options [Hash] Options passed into constructor
180239
# @return [Object] OctocatalogDiff::Catalog::<whatever> object

lib/octocatalog-diff/util/parallel.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def initialize(opts = {})
2222
@args = opts.fetch(:args, {})
2323
@description = opts[:description] || @method.name
2424
@validator = opts[:validator]
25+
@validator_args = opts[:validator_args] || {}
2526
end
2627

2728
def execute(logger = Logger.new(StringIO.new))
@@ -30,7 +31,7 @@ def execute(logger = Logger.new(StringIO.new))
3031

3132
def validate(result, logger = Logger.new(StringIO.new))
3233
return true if @validator.nil?
33-
@validator.call(result, logger)
34+
@validator.call(result, logger, @validator_args)
3435
end
3536
end
3637

0 commit comments

Comments
 (0)