Skip to content

Commit b657d53

Browse files
authored
Merge pull request #1954 from ploubser/issue_1952
(#1952) Fix fact filter parsing
2 parents 45c3407 + 31c535d commit b657d53

File tree

2 files changed

+60
-15
lines changed

2 files changed

+60
-15
lines changed

filter/facts/facts.go

+44-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/choria-io/go-choria/internal/util"
1919
)
2020

21+
var validOperators = regexp.MustCompile(`<=|>=|=>|=<|<|>|!=|=~|={1,2}`)
22+
2123
// Logger provides logging facilities
2224
type Logger interface {
2325
Warnf(format string, args ...any)
@@ -173,18 +175,49 @@ func HasFact(fact string, operator string, value string, file string, log Logger
173175
}
174176

175177
// 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
178+
func ParseFactFilterString(f string) ([3]string, error) {
179+
operatorIndexes := validOperators.FindAllStringIndex(f, -1)
180+
var mainOpIndex []int
181+
182+
if opCount := len(operatorIndexes); opCount > 1 {
183+
// This is a special case where the left operand contains a valid operator.
184+
// We skip over everything and use the right most operator.
185+
mainOpIndex = operatorIndexes[len(operatorIndexes)-1]
186+
} else if opCount == 1 {
187+
mainOpIndex = operatorIndexes[0]
187188
} else {
188189
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
189190
}
191+
192+
op := f[mainOpIndex[0]:mainOpIndex[1]]
193+
leftOp := strings.TrimSpace(f[:mainOpIndex[0]])
194+
rightOp := strings.TrimSpace(f[mainOpIndex[1]:])
195+
196+
// validate that the left and right operands are both valid
197+
if len(leftOp) == 0 || len(rightOp) == 0 {
198+
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
199+
}
200+
201+
lStartString := string(leftOp[0])
202+
rEndString := string(rightOp[len(rightOp)-1])
203+
if validOperators.MatchString(lStartString) || validOperators.Match([]byte(rEndString)) {
204+
return [3]string{}, fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", f)
205+
}
206+
207+
// transform op and value for processing
208+
switch op {
209+
case "=":
210+
op = "=="
211+
case "=<":
212+
op = "<="
213+
case "=>":
214+
op = ">="
215+
}
216+
217+
// finally check for old style regex fact matches
218+
if rightOp[0] == '/' && rightOp[len(rightOp)-1] == '/' {
219+
op = "=~"
220+
}
221+
222+
return [3]string{leftOp, op, rightOp}, nil
190223
}

filter/filter_test.go

+16-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package filter
66

77
import (
8+
"fmt"
89
"testing"
910

1011
"github.com/choria-io/go-choria/protocol"
@@ -160,10 +161,21 @@ var _ = Describe("Filter", func() {
160161
t("foo == bar", "foo", "==", "bar")
161162
})
162163

163-
It("Should fail for facts in the wrong format", func() {
164-
pf, err := ParseFactFilterString("foo")
165-
Expect(err).To(MatchError("could not parse fact foo it does not appear to be in a valid format"))
166-
Expect(pf).To(BeNil())
164+
It("Should parse gjson facts", func() {
165+
t("storage.#(name=\"nvme0n1\").size==64", "storage.#(name=\"nvme0n1\").size", "==", "64")
166+
t("storage.#(name='nvme0n1').size==64", "storage.#(name='nvme0n1').size", "==", "64")
167+
t("storage.#(name==\"nvme0n1\").size=64", "storage.#(name==\"nvme0n1\").size", "==", "64")
168+
t("storage.#(name=\"nvme0n1\").size == 64", "storage.#(name=\"nvme0n1\").size", "==", "64")
169+
t("storage.#(name<= \"nvme0n1\" ).size==64", "storage.#(name<= \"nvme0n1\" ).size", "==", "64")
170+
t("storage.#(name=\"foo bar\").size=>baz bar", "storage.#(name=\"foo bar\").size", ">=", "baz bar")
171+
})
172+
173+
It("Should fail on invalid fact filters", func() {
174+
badFilters := []string{"foobarbaz", "=foo(bar=baz)", "foo=", "foo(bar=baz)>", "=><=="}
175+
for _, filterString := range badFilters {
176+
_, err := ParseFactFilterString(filterString)
177+
Expect(err).To(MatchError(fmt.Errorf("could not parse fact %s it does not appear to be in a valid format", filterString)))
178+
}
167179
})
168180
})
169181
})

0 commit comments

Comments
 (0)