Skip to content

Commit 0a6af59

Browse files
(CAT-1731) improve handling of pinned nodes
Within the Puppet Enterprise UI console the concept of pinned nodes allows more complex rules to be expressed while including pinned nodes. Pinned nodes are formed by having a top level "or" clause, with expressions in the form `['=', 'name', 'hostname']` Prior to this change, the code would not properly maintain pinned nodes, nor correctly handle merging expressions like: ``` ['and', ['~', ['fact', 'pe_server_version'], '.+']] ``` with ``` ['or', ['~', ['trusted', 'extensions', '1.3.6.1.4.1.34380.1.1.9812'], '^puppet/']] ``` Incorrectly combining them into an `and` clause when logically they should be an "or" clause. With this change, pinned nodes are separated our from the other rules and then recombined later (if they are present) with a top-level "or" clause. Test are added to demonstrate the behaviors.
1 parent 537bf15 commit 0a6af59

File tree

2 files changed

+352
-161
lines changed

2 files changed

+352
-161
lines changed

lib/puppet/type/node_group.rb

Lines changed: 76 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,73 +37,89 @@
3737
desc 'Match conditions for this group'
3838
def should
3939
case @resource[:purge_behavior]
40-
when :rule, :all
41-
super
42-
else
40+
# only do something special when we are asked to merge, otherwise, fall back to the default
41+
when :none
42+
# rule grammar
43+
# condition : [ {bool} {condition}+ ] | [ "not" {condition} ] | {operation}
44+
# bool : "and" | "or"
45+
# operation : [ {operator} {fact-path} {value} ]
46+
# operator : "=" | "~" | ">" | ">=" | "<" | "<=" | "<:"
47+
# fact-path : {field-name} | [ {path-type} {field-name} {path-component}+ ]
48+
# path-type : "trusted" | "fact"
49+
# path-component : field-name | number
50+
# field-name : string
51+
# value : string
52+
4353
a = @resource.property(:rule).retrieve || {}
4454
b = shouldorig
45-
aorig = a.map(&:clone)
46-
atmp = a.map(&:clone)
47-
# check if the node classifer has any rules defined before attempting merge.
48-
if a.length >= 2
49-
if (a[0] == 'or') && (a[1][0] == 'or') || (a[1][0] == 'and')
50-
# Merging both rules and pinned nodes
51-
if (b[0] == 'or') && (b[1][0] == 'or') || (b[1][0] == 'and')
52-
# b has rules to merge
53-
rules = (atmp[1] + b[1].drop(1)).uniq
54-
atmp[1] = rules
55-
pinned = (b[2, b.length] + atmp[2, atmp.length]).uniq
56-
merged = (atmp + pinned).uniq
57-
elsif ((b[0] == 'and') || (b[0] == 'or')) && PuppetX::Node_manager::Common.factcheck(b)
58-
# b only has rules to merge
59-
rules = (atmp[1] + b.drop(1)).uniq
60-
atmp[1] = rules
61-
merged = atmp
62-
else
63-
pinned = (b[1, b.length] + atmp[2, atmp.length]).uniq
64-
merged = (atmp + pinned).uniq
65-
end
66-
elsif (b[0] == 'or') && (b[1][0] == 'or') || (b[1][0] == 'and')
67-
# Merging both rules and pinned nodes
68-
rules = b[1] # no rules to merge on a side
69-
pinned = (b[2, b.length] + a[1, a.length]).uniq
70-
merged = (b + pinned).uniq
71-
elsif ((a[0] == 'and') || (a[0] == 'or')) && PuppetX::Node_manager::Common.factcheck(a)
72-
# a only has fact rules
73-
if (b[0] == 'or') && !PuppetX::Node_manager::Common.factcheck(b)
74-
# b only has pinned nodes
75-
rules = atmp
76-
temp = ['or']
77-
temp[1] = atmp
78-
merged = (temp + b[1, b.length]).uniq
79-
else
80-
# b only has rules
81-
merged = (a + b.drop(1)).uniq
82-
end
83-
elsif (a[0] == 'or') && (a[1][1] == 'name')
84-
# a only has pinned nodes
85-
if (b[0] == 'or') && !PuppetX::Node_manager::Common.factcheck(b)
86-
# b only has pinned nodes
87-
merged = (b + a.drop(1)).uniq
88-
else
89-
# b only has rules
90-
temp = ['or']
91-
temp[1] = b
92-
merged = (temp + atmp[1, atmp.length]).uniq
55+
56+
# extract all pinned nodes if any
57+
# pinned nodes are in the form ['=', 'name', <hostname>]
58+
apinned = []
59+
a_without_pinned = a
60+
if (a[0] == 'or')
61+
apinned = a.select {|item| (item[0] == '=') && (item[1] == 'name')}
62+
a_without_pinned = a.select { |item| (item[0] != '=') || (item[1] != 'name') }
63+
end
64+
bpinned = []
65+
b_without_pinned = b
66+
merged = []
67+
if (a == [""])
68+
# ensure rules are unique at the top level and remove any empty rule sets
69+
return b.uniq.select { |item| (item != ['or'] && item != ['and'] )}
70+
elsif (b == [""])
71+
# ensure rules are unique at the top level and remove any empty rule sets
72+
return a.uniq.select { |item| (item != ['or'] && item != ['and'] )}
73+
end
74+
75+
if (b[0] == 'or')
76+
bpinned = b.select {|item| (item[0] == '=') && (item[1] == 'name')}
77+
b_without_pinned = b.select { |item| (item[0] != '=') || (item[1] != 'name') }
78+
end
79+
80+
if (((a[0] == 'and') || (a[0] == 'or')) && a[0] == b[0])
81+
# if a and b start with the same 'and' or 'or' clause, we can just combine them
82+
if (a[0] == 'or')
83+
merged = (['or'] + a_without_pinned.drop(1) + b_without_pinned.drop(1) + apinned + bpinned).uniq
84+
else
85+
# must both be 'and' clauses
86+
if (apinned.length > 0 || bpinned.length > 0)
87+
# we have pinned nodes
88+
merged = (['or'] + [a_without_pinned + b_without_pinned.drop(1)] + apinned + bpinned).uniq
89+
else
90+
# no pinned nodes and one top level 'and' clause, just combine them.
91+
merged = a_without_pinned + b_without_pinned.drop(1)
9392
end
94-
else
95-
# default fall back.
96-
merged = (b + a.drop(1)).uniq
9793
end
98-
if merged == aorig
99-
# values are the same, returning orginal value"
100-
aorig
94+
else
95+
# first clause of a and b aren't equal
96+
# a first clause is one of and/or/not/operator
97+
# b first clause is one of and/or/not/operator
98+
# if a starts with `and` and b starts with `or`, create a top level `or` clause, nest a under it and append the rest of b
99+
if (a_without_pinned[0] == 'and' && b_without_pinned[0] == 'or')
100+
# special case of a only having one subclause
101+
if (a_without_pinned.length == 2)
102+
merged = (['or'] + a_without_pinned[1] + b_without_pinned.drop(1) + apinned + bpinned)
103+
else
104+
merged = (['or'] + [a_without_pinned] + b_without_pinned.drop(1) + apinned + bpinned)
105+
end
101106
else
102-
merged
107+
if (a_without_pinned[0] == 'or')
108+
merged = (a_without_pinned + [b_without_pinned] + apinned + bpinned).uniq
109+
else
110+
# if b starts with 'or', we want to be sure to drop that.
111+
if (b_without_pinned[0] == 'or')
112+
merged = (['or'] + [a_without_pinned] + b_without_pinned.drop(1) + apinned + bpinned)
113+
else
114+
merged = (['or'] + [a_without_pinned] + [b_without_pinned] + apinned + bpinned)
115+
end
116+
end
103117
end
104-
else
105-
b
106118
end
119+
# ensure rules are unique at the top level and remove any empty rule sets
120+
merged.uniq.select { |item| (item != ['or'] && item != ['and'] )}
121+
else
122+
super
107123
end
108124
end
109125

0 commit comments

Comments
 (0)