diff --git a/doc/advanced-ignores.md b/doc/advanced-ignores.md index 56051590..1f6d1608 100644 --- a/doc/advanced-ignores.md +++ b/doc/advanced-ignores.md @@ -222,3 +222,13 @@ File[/tmp/foo] => In this case, the very important line was removed from the catalog, and you want to know about this. Ignoring `File[/tmp/foo]::parameters::content` would have suppressed this (because all changes to that attribute are ignored). Also ignoring `File[/tmp/foo]::parameters::content=~>This is the line in the new catalog that I do not care about$` would have also suppressed this (because the regular expression was matched for *one* of the lines). However, the two examples with `=&>` in this section would *not* have suppressed this change, because it is no longer the case that *all* changes in the file matched the regular expression. :warning: All lines are stripped of leading and trailing spaces before the regular expression match is tried. This stripping of whitespace is done *only* for this comparison stage, and does not affect the display of any results. + +#### Ignoring attributes which have identical elements but in arbitrary order + +You can ignore attributes where both the values in both the old and new catalogs are arrays and the arrays +contain identical elements but in arbitrary order. Basically, you can ignore a parameter where the values +have set equality. + +To ignore any parameters named `foo` with values having set equality, you would use: + + --ignore 'My::Custom::Resource[*]::parameters::foo=s>=' diff --git a/lib/octocatalog-diff/catalog-diff/differ.rb b/lib/octocatalog-diff/catalog-diff/differ.rb index 07e19e89..879e8999 100644 --- a/lib/octocatalog-diff/catalog-diff/differ.rb +++ b/lib/octocatalog-diff/catalog-diff/differ.rb @@ -340,7 +340,8 @@ def attr_match_rule?(rule, attrib, old_val, new_val) # =-> Attribute must have been removed and equal this # =~> Change must match regexp (one line of change matching is sufficient) # =&> Change must match regexp (all lines of change MUST match regexp) - if rule_attr =~ /\A(.+?)(=[\-\+~&]?>)(.+)/m + # =s> Change must be array and contain identical elements, ignoring order + if rule_attr =~ /\A(.+?)(=[\-\+~&s]?>)(.+)/m rule_attr = Regexp.last_match(1) operator = Regexp.last_match(2) value = Regexp.last_match(3) @@ -361,6 +362,9 @@ def attr_match_rule?(rule, attrib, old_val, new_val) raise RegexpError, "Invalid ignore regexp for #{key}: #{exc.message}" end matcher = ->(x, y) { regexp_operator_match?(operator, my_regex, x, y) } + elsif operator == '=s>' + raise ArgumentError, "Invalid ignore option for =s>, must be '='" unless value == '=' + matcher = ->(x, y) { x.is_a?(Array) && y.is_a?(Array) && Set.new(x) == Set.new(y) } end end diff --git a/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-1.json b/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-1.json new file mode 100644 index 00000000..9ecf02ce --- /dev/null +++ b/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-1.json @@ -0,0 +1,28 @@ +{ + "document_type": "Catalog", + "data": { + "tags": ["settings"], + "name": "my.rspec.node", + "version": "production", + "environment": "production", + "resources": [ + { + "type": "Myres", + "title": "res1", + "file": "/environments/production/modules/foo/manifests/init.pp", + "line": 10, + "exported": false, + "parameters": { + "set1": ["one", "two", "three"], + "set2": ["a", "b"] + } + } + ], + "classes": [ + "settings" + ] + }, + "metadata": { + "api_version": 1 + } +} diff --git a/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-2.json b/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-2.json new file mode 100644 index 00000000..eb7c87ac --- /dev/null +++ b/spec/octocatalog-diff/fixtures/catalogs/ignore-parameter-set-2.json @@ -0,0 +1,29 @@ +{ + "document_type": "Catalog", + "data": { + "tags": ["settings"], + "name": "my.rspec.node", + "version": "production", + "environment": "production", + "resources": [ + { + "type": "Myres", + "title": "res1", + "file": "/environments/production/modules/foo/manifests/init.pp", + "line": 10, + "exported": false, + "parameters": { + "set1": ["three", "two", "one"], + "set2": ["a", "b", "c"], + "set3": [1, 2, 3] + } + } + ], + "classes": [ + "settings" + ] + }, + "metadata": { + "api_version": 1 + } +} diff --git a/spec/octocatalog-diff/tests/catalog-diff/differ_spec.rb b/spec/octocatalog-diff/tests/catalog-diff/differ_spec.rb index 32bde785..522ec39b 100644 --- a/spec/octocatalog-diff/tests/catalog-diff/differ_spec.rb +++ b/spec/octocatalog-diff/tests/catalog-diff/differ_spec.rb @@ -1234,6 +1234,73 @@ end end + context 'ignoring changes in sets' do + describe '#ignore' do + before(:all) do + @c1 = OctocatalogDiff::Catalog.create(json: OctocatalogDiff::Spec.fixture_read('catalogs/ignore-parameter-set-1.json')) + @c2 = OctocatalogDiff::Catalog.create(json: OctocatalogDiff::Spec.fixture_read('catalogs/ignore-parameter-set-2.json')) + @set1 = [ + '!', + "Myres\fres1\fparameters\fset1", + %w(one two three), + %w(three two one) + ] + @set2 = [ + '!', + "Myres\fres1\fparameters\fset2", + %w(a b), + %w(a b c) + ] + @set3 = [ + '!', + "Myres\fres1\fparameters\fset3", + nil, + [1, 2, 3] + ] + end + + it 'should not filter out a change when attribute does not match' do + opts = {} + testobj = OctocatalogDiff::CatalogDiff::Differ.new(opts, @c1, @c2) + testobj.ignore(type: 'Myres', title: 'res1', attr: "parameters\fmode") + result = testobj.diff + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set1)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set2)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set3)).to eq(true) + end + + it 'should filter out a change when two arrays have set equality' do + opts = {} + testobj = OctocatalogDiff::CatalogDiff::Differ.new(opts, @c1, @c2) + testobj.ignore(type: 'Myres', title: 'res1', attr: "parameters\fset1=s>=") + result = testobj.diff + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set1)).to eq(false) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set2)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set3)).to eq(true) + end + + it 'should not filter out a change when two arrays are not equivalent sets' do + opts = {} + testobj = OctocatalogDiff::CatalogDiff::Differ.new(opts, @c1, @c2) + testobj.ignore(type: 'Myres', title: 'res1', attr: "parameters\fset2=s>=") + result = testobj.diff + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set1)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set2)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set3)).to eq(true) + end + + it 'should not filter out a change when one array is not specified' do + opts = {} + testobj = OctocatalogDiff::CatalogDiff::Differ.new(opts, @c1, @c2) + testobj.ignore(type: 'Myres', title: 'res1', attr: "parameters\fset3=s>=") + result = testobj.diff + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set1)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set2)).to eq(true) + expect(OctocatalogDiff::Spec.array_contains_partial_array?(result, @set3)).to eq(true) + end + end + end + describe '#ignore_match?' do let(:resource) { { type: 'Apple', title: 'delicious', attr: "parameters\fcolor" } } let(:testobj) { described_class.allocate }