Skip to content

Commit fdbe45d

Browse files
author
Kevin Paulisse
committed
Merge branch 'kpaulisse-validate-references' into kpaulisse-environment
2 parents ec4b53c + 8517eeb commit fdbe45d

37 files changed

Lines changed: 3137 additions & 20 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

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: 63 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,69 @@ 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 = remove_existing_resources(x['parameters'][r])
194+
missing << missing_resources.map { |missing_target| { source: x, target_type: r, target_value: missing_target } }
195+
end
196+
end
197+
missing.flatten!
198+
missing.compact!
199+
return if missing.empty?
200+
201+
# At this point there is at least one broken/missing reference. Format an error message and
202+
# raise. Error message will look like this:
203+
# ---
204+
# Catalog has broken references: exec[subscribe caller 1] -> subscribe[Exec[subscribe target]];
205+
# exec[subscribe caller 2] -> subscribe[Exec[subscribe target]]; exec[subscribe caller 2] ->
206+
# subscribe[Exec[subscribe target 2]]
207+
# ---
208+
formatted_references = missing.map do |obj|
209+
# obj[:target_value] can be a string or an array. If it's an array, create a
210+
# separate error message per element of that array. This allows the total number
211+
# of errors to be correct.
212+
src = "#{obj[:source]['type'].downcase}[#{obj[:source]['title']}]"
213+
target_val = obj[:target_value].is_a?(Array) ? obj[:target_value] : [obj[:target_value]]
214+
target_val.map { |tv| "#{src} -> #{obj[:target_type].downcase}[#{tv}]" }
215+
end
216+
formatted_references.flatten!
217+
plural = formatted_references.size == 1 ? '' : 's'
218+
errors = formatted_references.join('; ')
219+
raise ReferenceValidationError, "Catalog has broken reference#{plural}: #{errors}"
220+
end
221+
176222
private
177223

224+
# Private method: Determine if a catalog contains resource or resources, which may
225+
# have been passed in as an array or a string. Return the references to resources
226+
# that are missing from the catalog. (An empty array would indicate all references
227+
# are present.)
228+
# @param resources_to_check [String / Array] Resources to check
229+
# @return [Array] References that are missing from catalog
230+
def remove_existing_resources(resources_to_check)
231+
rtc_array = resources_to_check.is_a?(Array) ? resources_to_check : [resources_to_check]
232+
rtc_array.map do |res|
233+
unless res =~ /\A([\w:]+)\[(.+)\]\z/
234+
raise ArgumentError, "Resource #{res} is not in the expected format"
235+
end
236+
resource(type: Regexp.last_match(1), title: Regexp.last_match(2)).nil? ? res : nil
237+
end.compact
238+
end
239+
178240
# Private method: Choose backend based on passed-in options
179241
# @param options [Hash] Options passed into constructor
180242
# @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

octocatalog-diff.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
66
s.required_ruby_version = '>= 2.0.0'
77

88
s.name = 'octocatalog-diff'
9-
s.version = OctocatalogDiff::Version::VERSION
9+
s.version = ENV['OCTOCATALOG_DIFF_VERSION'] || OctocatalogDiff::Version::VERSION
1010
s.license = 'MIT'
1111
s.authors = ['GitHub, Inc.', 'Kevin Paulisse']
1212
s.email = 'opensource+octocatalog-diff@github.com'

rake/gem.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66
module OctocatalogDiff
77
# A class to contain methods and constants for cleaner code
88
class Gem
9+
# Override version number from the environment
10+
def self.version
11+
version = ENV['OCTOCATALOG_DIFF_VERSION'] || OctocatalogDiff::Version::VERSION
12+
unless version == OctocatalogDiff::Version::VERSION
13+
warn "WARNING: Using version #{version}, not #{OctocatalogDiff::Version::VERSION}"
14+
end
15+
version
16+
end
17+
918
BASEDIR = File.expand_path('..', File.dirname(__FILE__)).freeze
10-
VERSION = OctocatalogDiff::Version::VERSION
19+
VERSION = version.freeze
1120
GEMFILE = "octocatalog-diff-#{VERSION}.gem".freeze
1221
PKGDIR = File.join(BASEDIR, 'pkg').freeze
1322
OUTFILE = File.join(BASEDIR, GEMFILE).freeze
@@ -77,8 +86,12 @@ def self.exec_command(command)
7786

7887
task 'force-build' do
7988
branch = OctocatalogDiff::Gem.branch
80-
warn "WARNING: Force-building from non-master branch #{branch}" unless branch == 'master'
81-
OctocatalogDiff::Gem.build("octocatalog-diff-#{OctocatalogDiff::Gem::VERSION}-#{branch}.gem")
89+
unless branch == 'master'
90+
warn "WARNING: Force-building from non-master branch #{branch}"
91+
end
92+
93+
version = OctocatalogDiff::Gem.version
94+
OctocatalogDiff::Gem.build("octocatalog-diff-#{version}-#{branch}.gem")
8295
end
8396

8497
task 'push' do
@@ -127,7 +140,7 @@ def self.exec_command(command)
127140

128141
# Make sure the gem has been built
129142
branch = OctocatalogDiff::Gem.branch
130-
version = OctocatalogDiff::Gem::VERSION
143+
version = OctocatalogDiff::Gem.version
131144
gemfile = if branch == 'master'
132145
OctocatalogDiff::Gem::FINAL_GEMFILE
133146
else

0 commit comments

Comments
 (0)