Skip to content

Commit cab4e5f

Browse files
committed
(#1952) Fix fact filter parsing
With the addition of GJson to describe fact values the old fact parser is no longer able to correctly identify facts vs operators in examples like `'storage.#(name=="vda").size'`. Here we rewrite the ParseFactFilterString function to no longer used complicated regular expressions. We will now correctly identify GJson facts and parse the filters correctly. In addition we're adding a wider range of tests for ParseFactFilterString so that we're covered if we were to add more fact filter functionality in the future.
1 parent f354c55 commit cab4e5f

File tree

2 files changed

+97
-11
lines changed

2 files changed

+97
-11
lines changed

filter/facts/facts.go

+45-11
Original file line numberDiff line numberDiff line change
@@ -173,18 +173,52 @@ func HasFact(fact string, operator string, value string, file string, log Logger
173173
}
174174

175175
// ParseFactFilterString parses a fact filter string as typically typed on the CLI
176-
func ParseFactFilterString(f string) (pf [3]string, err error) {
177-
if matched := regexp.MustCompile("^([^ ]+?)[ ]*=>[ ]*(.+)").FindStringSubmatch(f); len(matched) > 0 {
178-
return [3]string{matched[1], ">=", matched[2]}, nil
179-
} else if matched := regexp.MustCompile("^([^ ]+?)[ ]*=<[ ]*(.+)").FindStringSubmatch(f); len(matched) > 0 {
180-
return [3]string{matched[1], "<=", matched[2]}, nil
181-
} else if matched := regexp.MustCompile("^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)").FindStringSubmatch(f); len(matched) > 0 {
182-
return [3]string{matched[1], matched[2], matched[3]}, nil
183-
} else if matched := regexp.MustCompile("^(.+?)[ ]*=[ ]*/(.+)/$").FindStringSubmatch(f); len(matched) > 0 {
184-
return [3]string{matched[1], "=~", "/" + matched[2] + "/"}, nil
185-
} else if matched := regexp.MustCompile("^([^= ]+?)[ ]*=[ ]*(.+)").FindStringSubmatch(f); len(matched) > 0 {
186-
return [3]string{matched[1], "==", matched[2]}, nil
176+
func ParseFactFilterString(f string) ([3]string, error) {
177+
// strip whatspaces before processing
178+
f = strings.ReplaceAll(f, " ", "")
179+
validOperators := regexp.MustCompile(`<=|>=|=>|=<|<|>|!=|=~|={1,2}`)
180+
operatorIndexes := validOperators.FindAllStringIndex(f, -1)
181+
var mainOpIndex []int
182+
183+
if opCount := len(operatorIndexes); opCount > 1 {
184+
// This is a special case where the left operand contains a valid operator.
185+
// We skip over everything and use the right most operator.
186+
mainOpIndex = operatorIndexes[len(operatorIndexes)-1]
187+
} else if opCount == 1 {
188+
mainOpIndex = operatorIndexes[0]
187189
} else {
188190
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
189191
}
192+
193+
op := f[mainOpIndex[0]:mainOpIndex[1]]
194+
leftOp := f[:mainOpIndex[0]]
195+
rightOp := f[mainOpIndex[1]:]
196+
197+
// validate that the left and right operands are both valid
198+
if len(leftOp) == 0 || len(rightOp) == 0 {
199+
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
200+
}
201+
202+
lStartString := string(leftOp[0])
203+
rEndString := string(rightOp[len(rightOp)-1])
204+
if validOperators.MatchString(lStartString) || validOperators.Match([]byte(rEndString)) {
205+
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
206+
}
207+
208+
// transform op and value for processing
209+
switch op {
210+
case "=":
211+
op = "=="
212+
case "=<":
213+
op = "<="
214+
case "=>":
215+
op = ">="
216+
}
217+
218+
// finally check for old style regex fact matches
219+
if rightOp[0] == '/' && rightOp[len(rightOp)-1] == '/' {
220+
op = "=~"
221+
}
222+
223+
return [3]string{leftOp, op, rightOp}, nil
190224
}

filter/facts/facts_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package facts
66

77
import (
8+
"fmt"
89
"io"
910
"os"
1011
"strings"
@@ -26,6 +27,7 @@ func Test(t *testing.T) {
2627
var _ = Describe("Server/Discovery/Facts", func() {
2728
var (
2829
t func(fact, op, val string) (bool, error)
30+
p func(fact string) ([3]string, error)
2931
l *logrus.Entry
3032
)
3133

@@ -37,6 +39,10 @@ var _ = Describe("Server/Discovery/Facts", func() {
3739
return HasFact(fact, op, val, "testdata/fact.yaml", l)
3840
}
3941

42+
p = func(fact string) ([3]string, error) {
43+
return (ParseFactFilterString(fact))
44+
}
45+
4046
// fw.Config.FactSourceFile = "testdata/fact.yaml"
4147
})
4248

@@ -133,4 +139,50 @@ var _ = Describe("Server/Discovery/Facts", func() {
133139
Expect(t("fbool", "!=", "false")).To(BeTrue())
134140
})
135141
})
142+
143+
Describe("ParseFactFilterString", func() {
144+
It("Should transform = comparisons to ==", func() {
145+
result, err := p("storage.size=64")
146+
Expect(err).To(BeNil())
147+
Expect(result).To(BeEquivalentTo([3]string{"storage.size", "==", "64"}))
148+
})
149+
150+
It("Should transform *= comparisons to regex matches", func() {
151+
result, err := p("storage.size=/64/")
152+
Expect(err).To(BeNil())
153+
Expect(result).To(BeEquivalentTo([3]string{"storage.size", "=~", "/64/"}))
154+
})
155+
156+
It("Should transform =< comparisons to <=", func() {
157+
result, err := p("storage.size=<64")
158+
Expect(err).To(BeNil())
159+
Expect(result).To(BeEquivalentTo([3]string{"storage.size", "<=", "64"}))
160+
})
161+
162+
It("Should transform => comparisons to <=", func() {
163+
result, err := p("storage.size=>64")
164+
Expect(err).To(BeNil())
165+
Expect(result).To(BeEquivalentTo([3]string{"storage.size", ">=", "64"}))
166+
})
167+
168+
It("Should walk over array indexing from the external parser nested facts", func() {
169+
result, err := p("storage.#(name==\"vda\").size=64")
170+
Expect(err).To(BeNil())
171+
Expect(result).To(BeEquivalentTo([3]string{"storage.#(name==\"vda\").size", "==", "64"}))
172+
})
173+
174+
It("Should fail on invalid fact filters", func() {
175+
badFilters := []string{"foobarbaz", "=foo(bar=baz)", "foo=", "foo(bar=baz)>", "=><=="}
176+
for _, filterString := range badFilters {
177+
_, err := p(filterString)
178+
Expect(err).To(Equal(fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", filterString)))
179+
}
180+
})
181+
182+
It("Should handle spaces", func() {
183+
result, err := p("storage.size = 64")
184+
Expect(err).To(BeNil())
185+
Expect(result).To(BeEquivalentTo([3]string{"storage.size", "==", "64"}))
186+
})
187+
})
136188
})

0 commit comments

Comments
 (0)