Skip to content

Commit 51294c3

Browse files
authored
Merge pull request #244 from kpaulisse/kpaulisse-ignore-comparison-tag
Update capability and docs of --compare-file-text
2 parents 6701b4f + 62da538 commit 51294c3

File tree

8 files changed

+361
-22
lines changed

8 files changed

+361
-22
lines changed

doc/advanced-compare-file-text.md

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Compare file text
2+
3+
`octocatalog-diff` contains functionality to detect changes in file content for file resources that use the `source` attribute like this:
4+
5+
```
6+
file { '/etc/logrotate.d/my-service.conf':
7+
source => 'puppet:///modules/logrotate/etc/logrotate.d/my-service.conf',
8+
}
9+
```
10+
11+
When the `source` attribute is used, the catalog contains the value of the attribute (in the example above, `source` = `puppet:///modules/logrotate/etc/logrotate.d/my-service.conf`). However, the catalog does not contain the content of the file. When an agent applies the catalog, the file found on the server or in the code base at `modules/logrotate/file/etc/logrotate.d/my-service.conf` will be installed into `/etc/logrotate.d/my-service.conf`.
12+
13+
If a developer creates a change to this file, the catalog will not indicate this change. This means that so long as the `source` location does not change, any tool analyzing just the catalog would not detect a "diff" resulting from changes in the underlying file itself.
14+
15+
However, since applying the catalog could change the content of a file on the target node, `octocatalog-diff` has a feature that will do the following for any `source => 'puppet:///modules/...'` files:
16+
17+
- Locate the source file in the Puppet code
18+
- Substitute the content of the file into the generated catalog (remove `source` and populate `content`)
19+
- Display the "diff" in the now-populated `content` field
20+
21+
This feature is available only when the catalogs are being compiled from local code. This feature is not available, and will be automatically disabled, when pulling catalogs from PuppetDB or a Puppet server.
22+
23+
Note: In Puppet >= 4.4 there is an option in Puppet itself called "static catalogs" which if enabled will cause the checksum of the file to be included in the catalog. However, the `octocatalog-diff` feature described here is still useful because it can be used to display a "diff" of the change rather than just displaying a "diff" of a checksum.
24+
## Command line options
25+
26+
### `--compare-file-text` and `--no-compare-file-text`
27+
28+
The feature described above is enabled by default, and no special setup is required.
29+
30+
To disable this feature for a specific run, add `--no-compare-file-text` to the command line.
31+
32+
To disable this feature by default, add the following to a [configuration](/doc/configuration.md) file:
33+
34+
```ruby
35+
settings[:compare_file_text] = false
36+
```
37+
38+
If this feature is disabled by default in a configuration file, add `--compare-file-text` to enable the feature for this specific run.
39+
40+
Note that the feature will be automatically disabled, regardless of configuration or command line options, if catalogs are being pulled from PuppetDB or a Puppet server.
41+
42+
### `--compare-file-text-ignore-tags`
43+
44+
To disable this feature for specific `file` resources, set a tag on the resources for which the comparison is undesired. For example:
45+
46+
```
47+
file { '/etc/logrotate.d/my-service.conf':
48+
source => 'puppet:///modules/logrotate/etc/logrotate.d/my-service.conf',
49+
}
50+
51+
file { '/etc/logrotate.d/other-service.conf':
52+
tag => ['compare-file-text-disable'],
53+
source => 'puppet:///modules/logrotate/etc/logrotate.d/other-service.conf',
54+
}
55+
```
56+
57+
Inform `octocatalog-diff` of the name of the tag either via the command line (`--compare-file-text-ignore-tags "compare-file-text-disable"`) or via a [configuration](/doc/configuration.md) file:
58+
59+
```ruby
60+
settings[:compare_file_text_ignore_tags] = %w(compare-file-text-disable)
61+
```
62+
63+
With this example setup, the file text would be compared for `/etc/logrotate.d/my-service.conf` but would NOT be compared for `/etc/logrotate.d/other-service.conf`.
64+
65+
Notes:
66+
67+
1. `--compare-file-text-ignore-tags` can take comma-separated arguments if there are multiple tags, e.g.: `--compare-file-text-ignore-tags tag1,tag2`.
68+
69+
1. When defining values in the configuration file, `settings[:compare_file_text_ignore_tags]` must be an array. (The default value is an empty array, `[]`.)
70+
71+
1. "compare-file-text-disable" is used as the name of the tag in the example above, but it is not a magical value. Any valid tag name can be used.
72+
73+
## Notes
74+
75+
1. If the underlying file source cannot be found when building the "to" catalog, an exception will be raised. This will display the resource type and title, the value of `source` that is invalid, and the file name and line number of the Puppet manifest that declared the resource.
76+
77+
1. If the underlying file source cannot be found when building the "from" catalog, no exception will be raised. This behavior allows `octocatalog-diff` to work correctly even if there is a problem in the "from" catalog that is being corrected in the "to" catalog. (Of course, if the underlying file source can't be found in the "to" catalog either, an exception is raised -- see #1.)
78+
79+
1. No processing takes place for file `source` whose values do not start with `puppet:///modules/`.

doc/optionsref.md

+30
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ Usage: octocatalog-diff [command line options]
185185
--no-ignore-tags Disable ignoring based on tags
186186
--ignore-tags STRING1[,STRING2[,...]]
187187
Specify tags to ignore
188+
--compare-file-text-ignore-tags STRING1[,STRING2[,...]]
189+
Tags that exclude a file resource from text comparison
188190
--[no-]preserve-environments Enable or disable environment preservation
189191
--environment STRING Environment for catalog compilation globally
190192
--to-environment STRING Environment for catalog compilation for the to branch
@@ -374,6 +376,21 @@ what is most often desired. (<a href="../lib/octocatalog-diff/cli/options/compar
374376
</td>
375377
</tr>
376378

379+
<tr>
380+
<td valign=top>
381+
<pre><code>--compare-file-text-ignore-tags STRING1[,STRING2[,...]]</code></pre>
382+
</td>
383+
<td valign=top>
384+
Tags that exclude a file resource from text comparison
385+
</td>
386+
<td valign=top>
387+
When a file is specified with `source => 'puppet:///modules/something/foo.txt'`, remove
388+
the 'source' attribute and populate the 'content' attribute with the text of the file.
389+
This allows for a diff of the content, rather than a diff of the location, which is
390+
what is most often desired. (<a href="../lib/octocatalog-diff/cli/options/compare_file_text.rb">compare_file_text.rb</a>)
391+
</td>
392+
</tr>
393+
377394
<tr>
378395
<td valign=top>
379396
<pre><code>--create-symlinks STRING1[,STRING2[,...]]</code></pre>
@@ -1897,6 +1914,19 @@ has no effect when `--display-detail-add` is not used. (<a href="../lib/octocata
18971914
</td>
18981915
</tr>
18991916

1917+
<tr>
1918+
<td valign=top>
1919+
<pre><code>--use-lcs
1920+
--no-use-lcs </code></pre>
1921+
</td>
1922+
<td valign=top>
1923+
Use the LCS algorithm to determine differences in arrays
1924+
</td>
1925+
<td valign=top>
1926+
Configures using the Longest common subsequence (LCS) algorithm to determine differences in arrays (<a href="../lib/octocatalog-diff/cli/options/use_lcs.rb">use_lcs.rb</a>)
1927+
</td>
1928+
</tr>
1929+
19001930
<tr>
19011931
<td valign=top>
19021932
<pre><code>--validate-references

lib/octocatalog-diff/catalog-util/fileresources.rb

+38-15
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@ class FileResources
1313
# Public method: Convert file resources to text. See the description of the class
1414
# just above for details.
1515
# @param obj [OctocatalogDiff::Catalog] Catalog object (will be modified)
16+
# @param environment [String] Environment (defaults to production)
1617
def self.convert_file_resources(obj, environment = 'production')
1718
return unless obj.valid? && obj.compilation_dir.is_a?(String) && !obj.compilation_dir.empty?
18-
_convert_file_resources(obj.resources, obj.compilation_dir, environment)
19+
_convert_file_resources(
20+
obj.resources,
21+
obj.compilation_dir,
22+
environment,
23+
obj.options[:compare_file_text_ignore_tags],
24+
obj.options[:tag]
25+
)
1926
begin
2027
obj.catalog_json = ::JSON.generate(obj.catalog)
2128
rescue ::JSON::GeneratorError => exc
@@ -70,8 +77,11 @@ def self.module_path(dir)
7077
# Internal method: Static method to convert file resources. The compilation directory is
7178
# required, or else this is a no-op. The passed-in array of resources is modified by this method.
7279
# @param resources [Array<Hash>] Array of catalog resources
73-
# @param compilation_dir [String] Compilation directory (so files can be looked up)
74-
def self._convert_file_resources(resources, compilation_dir, environment = 'production')
80+
# @param compilation_dir [String] Compilation directory
81+
# @param environment [String] Environment
82+
# @param ignore_tags [Array<String>] Tags that exempt a resource from conversion
83+
# @param to_from_tag [String] Either "to" or "from" for catalog type
84+
def self._convert_file_resources(resources, compilation_dir, environment, ignore_tags, to_from_tag)
7585
# Calculate compilation directory. There is not explicit error checking here because
7686
# there is on-demand, explicit error checking for each file within the modification loop.
7787
return unless compilation_dir.is_a?(String) && compilation_dir != ''
@@ -88,38 +98,51 @@ def self._convert_file_resources(resources, compilation_dir, environment = 'prod
8898
resources.map! do |resource|
8999
if resource_convertible?(resource)
90100
path = file_path(resource['parameters']['source'], modulepaths)
91-
if path.nil?
92-
# Pass this through as a wrapped exception, because it's more likely to be something wrong
93-
# in the catalog itself than it is to be a broken setup of octocatalog-diff.
94-
message = "Errno::ENOENT: Unable to resolve '#{resource['parameters']['source']}'!"
95-
raise OctocatalogDiff::Errors::CatalogError, message
96-
end
97101

98-
if File.file?(path)
102+
if resource['tags'] && ignore_tags && (resource['tags'] & ignore_tags).any?
103+
# Resource tagged not to be converted -- do nothing.
104+
elsif path && File.file?(path)
99105
# If the file is found, read its content. If the content is all ASCII, substitute it into
100106
# the 'content' parameter for easier comparison. If not, instead populate the md5sum.
101107
# Delete the 'source' attribute as well.
102108
content = File.read(path)
103109
is_ascii = content.force_encoding('UTF-8').ascii_only?
104110
resource['parameters']['content'] = is_ascii ? content : '{md5}' + Digest::MD5.hexdigest(content)
105111
resource['parameters'].delete('source')
106-
elsif File.exist?(path)
112+
elsif path && File.exist?(path)
107113
# We are not handling recursive file installs from a directory or anything else.
108114
# However, the fact that we found *something* at this location indicates that the catalog
109115
# is probably correct. Hence, the very general .exist? check.
116+
elsif to_from_tag == 'from'
117+
# Don't raise an exception for an invalid source in the "from"
118+
# catalog, because the developer may be fixing this in the "to"
119+
# catalog. If it's broken in the "to" catalog as well, the
120+
# exception will be raised when this code runs on that catalog.
110121
else
111-
# This is probably a bug
112-
# :nocov:
113-
raise "Unable to find '#{resource['parameters']['source']}' at #{path}!"
114-
# :nocov:
122+
# Pass this through as a wrapped exception, because it's more likely to be something wrong
123+
# in the catalog itself than it is to be a broken setup of octocatalog-diff.
124+
#
125+
# Example error: <OctocatalogDiff::Errors::CatalogError: Unable to resolve
126+
# source=>'puppet:///modules/test/tmp/bar' in File[/tmp/bar]
127+
# (/x/modules/test/manifests/init.pp:46)>
128+
source = resource['parameters']['source']
129+
type = resource['type']
130+
title = resource['title']
131+
file = resource['file'].sub(Regexp.new('^' + Regexp.escape(env_dir) + '/'), '')
132+
line = resource['line']
133+
message = "Unable to resolve source=>'#{source}' in #{type}[#{title}] (#{file}:#{line})"
134+
raise OctocatalogDiff::Errors::CatalogError, message
115135
end
116136
end
137+
117138
resource
118139
end
119140
end
120141

121142
# Internal method: Determine if a resource is convertible. It is convertible if it
122143
# is a file resource with no declared 'content' and with a declared and parseable 'source'.
144+
# It is not convertible if the resource is tagged with one of the tags declared by
145+
# the option `--compare-file-text-ignore-tags`.
123146
# @param resource [Hash] Resource to check
124147
# @return [Boolean] True of resource is convertible, false if not
125148
def self.resource_convertible?(resource)

lib/octocatalog-diff/cli/options/compare_file_text.rb

+18
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ def parse(parser, options)
1515
end
1616
end
1717
end
18+
19+
# Sometimes there is a particular file resource for which the file text
20+
# comparison is not desired, while not disabling that option globally. Similar
21+
# to --ignore_tags, it's possible to tag the file resource and exempt it from
22+
# the --compare_file_text checks.
23+
# @param parser [OptionParser object] The OptionParser argument
24+
# @param options [Hash] Options hash being constructed; this is modified in this method.
25+
OctocatalogDiff::Cli::Options::Option.newoption(:compare_file_text_ignore_tags) do
26+
has_weight 415
27+
28+
def parse(parser, options)
29+
description = 'Tags that exclude a file resource from text comparison'
30+
parser.on('--compare-file-text-ignore-tags STRING1[,STRING2[,...]]', Array, description) do |x|
31+
options[:compare_file_text_ignore_tags] ||= []
32+
options[:compare_file_text_ignore_tags].concat x
33+
end
34+
end
35+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"document_type": "Catalog",
3+
"tags": ["settings","test"],
4+
"name": "my.rspec.node",
5+
"version": "production",
6+
"environment": "production",
7+
"resources": [
8+
{
9+
"type": "Stage",
10+
"title": "main",
11+
"tags": ["stage"],
12+
"exported": false,
13+
"parameters": {
14+
"name": "main"
15+
}
16+
},
17+
{
18+
"type": "Class",
19+
"title": "Settings",
20+
"tags": ["class","settings"],
21+
"exported": false
22+
},
23+
{
24+
"type": "File",
25+
"title": "/tmp/foo",
26+
"tags": ["file","class"],
27+
"file": "/x/modules/test/manifests/init.pp",
28+
"line": 37,
29+
"exported": false,
30+
"parameters": {
31+
"backup": false,
32+
"mode": "0440",
33+
"owner": "root",
34+
"group": "root",
35+
"source": "puppet:///modules/test/tmp/foo"
36+
}
37+
},
38+
{
39+
"type": "File",
40+
"title": "/tmp/bar",
41+
"tags": ["file","class","no_compare_tag"],
42+
"file": "/x/modules/test/manifests/init.pp",
43+
"line": 46,
44+
"exported": false,
45+
"parameters": {
46+
"backup": false,
47+
"mode": "0440",
48+
"owner": "root",
49+
"group": "root",
50+
"source": "puppet:///modules/test/tmp/bar"
51+
}
52+
}
53+
],
54+
"classes": [
55+
"test"
56+
]
57+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
class test {
22
file { '/tmp/foo1':
3+
tag => ['_convert_file_resources_foo1_'],
34
source => 'puppet:///modules/test/foo-new',
45
}
56

67
file { '/tmp/foo2':
8+
tag => ['_convert_file_resources_foo2_'],
79
source => 'puppet:///modules/test/foo-old',
810
}
911
}

0 commit comments

Comments
 (0)