Merge pull request 'Answer AST name queries for pdbmux_source on /facts' (#30) from benvin/pdbmux-source-fact-ast into main
Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
@@ -247,7 +247,13 @@ matters more.
|
||||
friends ask for specific facts, and the synthetic record is not one of them.
|
||||
Only the outer query is inspected: a `name` filter inside an `in`/`select_facts`
|
||||
subquery narrows which *nodes* match, not which facts come back, so injection
|
||||
still happens;
|
||||
still happens. The exception is a query whose `name` comparison (`=`, `~` or
|
||||
`in` an `array`) selects `pdbmux_source` itself, built only from
|
||||
`and`/`or`/`not` over `=`, `~` and `in`-`array` on `certname`, `environment`,
|
||||
`name` and `value`: it is still fanned out for the real facts it names, and
|
||||
the synthetic records are added after being filtered by the query locally.
|
||||
Their fetch carries only the query's top-level `and` conjuncts on
|
||||
`certname`, so a single-host lookup stays single-host;
|
||||
- the path is `/facts/<name>` for any other fact. The path segment is the same
|
||||
outer `name` constraint, so only the fact's own path carries the record;
|
||||
- injection is turned off (see `source_fact_enabled`).
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -703,3 +705,129 @@ func TestHandler_FactRoutesAreNotCached(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// node-lookup and puppetboard filter /facts by name with an AST rather than the
|
||||
// drilldown path; those queries select the synthetic records the path serves.
|
||||
func TestHandler_FactsQuerySelectsSourceFact(t *testing.T) {
|
||||
src := defaultSourceFact
|
||||
tests := []struct {
|
||||
query string
|
||||
want map[string]string
|
||||
}{
|
||||
{`["=","name","` + src + `"]`, map[string]string{"h1": "a", "h2": "b", "h3": "b"}},
|
||||
{`["and",["=","name","` + src + `"]]`, map[string]string{"h1": "a", "h2": "b", "h3": "b"}},
|
||||
{`["and",["=","name","` + src + `"],["=","value","b"]]`, map[string]string{"h2": "b", "h3": "b"}},
|
||||
{`["and",["=","name","` + src + `"],["not",["~","value","^b$"]]]`, map[string]string{"h1": "a"}},
|
||||
{`["and",["=","certname","h3"],["=","name","` + src + `"]]`, map[string]string{"h3": "b"}},
|
||||
{`["and",["or",["=","name","osfamily"],["=","name","` + src + `"]]]`, map[string]string{"h1": "a", "h2": "b", "h3": "b"}},
|
||||
{`["in","name",["array",["osfamily","` + src + `"]]]`, map[string]string{"h1": "a", "h2": "b", "h3": "b"}},
|
||||
{`["and",["or",["=","name","` + src + `"]],["=","environment","prod"]]`, map[string]string{"h1": "a"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.query, func(t *testing.T) {
|
||||
a, b := sourceFactBackends(t)
|
||||
a.bodies[factsPath] = `[` + factEnv("h1", "osfamily", "RedHat", "prod") + `,` + fact("h1", src, "stale", "") + `]`
|
||||
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeFreshness))
|
||||
|
||||
rec := doGet(t, srv.Handler(), factsPath, tc.query)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
got, n := sourceValues(t, rec.Body.Bytes(), src)
|
||||
if n != len(tc.want) || !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("got %v (%d records), want %v: %s", got, n, tc.want, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The query still reaches the backends, so the real facts it names come back
|
||||
// beside the synthetic ones.
|
||||
func TestHandler_FactsQueryMixedNamesKeepsRealFacts(t *testing.T) {
|
||||
a, b := sourceFactBackends(t)
|
||||
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeFreshness))
|
||||
|
||||
rec := doGet(t, srv.Handler(), factsPath, `["in","name",["array",["osfamily","`+defaultSourceFact+`"]]]`)
|
||||
if os, _ := sourceValues(t, rec.Body.Bytes(), "osfamily"); len(os) != 3 {
|
||||
t.Errorf("osfamily = %v, want all three nodes: %s", os, rec.Body.String())
|
||||
}
|
||||
if h := rec.Header().Get(backendsHeader); h != "2/2" {
|
||||
t.Errorf("%s = %q, want 2/2", backendsHeader, h)
|
||||
}
|
||||
}
|
||||
|
||||
// With the feature off the name is an ordinary fact, so the query is fanned out
|
||||
// untouched and nothing is synthesised.
|
||||
func TestHandler_FactsQuerySourceFactDisabled(t *testing.T) {
|
||||
a, b := sourceFactBackends(t)
|
||||
cfg := testConfig(a.srv.URL, b.srv.URL, mergeFreshness)
|
||||
cfg.SourceFactEnabled = false
|
||||
srv := newTestServer(cfg)
|
||||
|
||||
rec := doGet(t, srv.Handler(), factsPath, `["=","name","`+defaultSourceFact+`"]`)
|
||||
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 0 {
|
||||
t.Errorf("disabled feature synthesised %d records: %s", n, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// The synthetic set is fetched with the query's top-level certname conjuncts
|
||||
// only, so a single-host lookup never pulls the whole estate.
|
||||
func TestHandler_FactsQuerySourceFactNarrowsUpstream(t *testing.T) {
|
||||
a, b := sourceFactBackends(t)
|
||||
var mu sync.Mutex
|
||||
var got []string
|
||||
h3Only := map[*fakeBackend]string{
|
||||
a: `[` + factEnv("h3", "osfamily", "RedHat", "prod") + `]`,
|
||||
b: `[` + factEnv("h3", "osfamily", "Debian", "dev") + `]`,
|
||||
}
|
||||
for fb, body := range h3Only {
|
||||
fb.handle = func(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.URL.Path != factsPath {
|
||||
return false
|
||||
}
|
||||
q := r.URL.Query().Get("query")
|
||||
mu.Lock()
|
||||
got = append(got, q)
|
||||
mu.Unlock()
|
||||
if q == `["=","certname","h3"]` {
|
||||
_, _ = w.Write([]byte(body))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeFreshness))
|
||||
|
||||
query := `["and",["=","certname","h3"],["=","environment","dev"],["=","name","` + defaultSourceFact + `"]]`
|
||||
rec := doGet(t, srv.Handler(), factsPath, query)
|
||||
if vals, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 1 || vals["h3"] != "b" {
|
||||
t.Fatalf("got %v (%d records), want h3=b: %s", vals, n, rec.Body.String())
|
||||
}
|
||||
for _, q := range got {
|
||||
if q == "" {
|
||||
t.Fatalf("an upstream /facts fetch was unfiltered: %q", got)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(got, `["=","certname","h3"]`) {
|
||||
t.Fatalf("synthetic fetch was not narrowed to the certname: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A backend lost between the two fetches is reported as missing.
|
||||
func TestHandler_FactsQuerySourceFactCountsSecondFetch(t *testing.T) {
|
||||
a, b := sourceFactBackends(t)
|
||||
var calls atomic.Int32
|
||||
b.handle = func(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.URL.Path == factsPath && calls.Add(1) > 1 {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeFreshness))
|
||||
|
||||
rec := doGet(t, srv.Handler(), factsPath, `["=","name","`+defaultSourceFact+`"]`)
|
||||
if h := rec.Header().Get(backendsHeader); h != "1/2" {
|
||||
t.Errorf("%s = %q, want 1/2", backendsHeader, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -387,7 +388,65 @@ func (s *Server) serveUnion(w http.ResponseWriter, r *http.Request, path string,
|
||||
}
|
||||
|
||||
func (s *Server) serveFacts(w http.ResponseWriter, r *http.Request) {
|
||||
s.serveMerged(w, r, factsPath, s.mergeFactsResponse(r))
|
||||
query := r.URL.Query().Get("query")
|
||||
inject := s.newSourceInjector(query, true)
|
||||
if ast := inject.sourceQuery(query); ast != nil {
|
||||
s.serveSourceFactQuery(w, r, inject, ast)
|
||||
return
|
||||
}
|
||||
s.serveMerged(w, r, factsPath, s.mergeFactsWith(inject))
|
||||
}
|
||||
|
||||
// serveSourceFactQuery answers a /facts query that selects the owned fact by
|
||||
// name. The query still goes upstream for the real facts it selects; the
|
||||
// synthetic records come from the same merge serveSourceFact uses, fetched for
|
||||
// the query's certname scope, and are filtered by the query locally, since no
|
||||
// backend holds them.
|
||||
func (s *Server) serveSourceFactQuery(w http.ResponseWriter, r *http.Request, inject *sourceInjector, ast []json.RawMessage) {
|
||||
params := queryParams(r.URL.Query().Get("query"))
|
||||
s.serveCached(w, r, factsPath, params, func(ctx context.Context) (cachedResponse, error) {
|
||||
alive, err := s.aliveResults(ctx, factsPath, params)
|
||||
if err != nil {
|
||||
return cachedResponse{}, err
|
||||
}
|
||||
recs := s.mergeFactsWith(inject)(alive)
|
||||
synth, scoped, err := s.sourceFacts(ctx, s.newSourceInjector("", true), queryParams(certnameScope(ast)))
|
||||
if err != nil {
|
||||
return cachedResponse{}, err
|
||||
}
|
||||
for _, raw := range synth {
|
||||
var f factFields
|
||||
if json.Unmarshal(raw, &f) != nil {
|
||||
continue
|
||||
}
|
||||
if match, _ := matchFact(ast, f); match {
|
||||
recs = append(recs, raw)
|
||||
}
|
||||
}
|
||||
resp := cachedResponse{Body: encodeRecords(recs), Records: -1}
|
||||
s.countBackends(&resp, aliveInBoth(alive, scoped))
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
|
||||
func aliveInBoth(a, b []backendResult) []backendResult {
|
||||
var out []backendResult
|
||||
for _, x := range a {
|
||||
if slices.ContainsFunc(b, func(y backendResult) bool { return y.name == x.name }) {
|
||||
out = append(out, x)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sourceFacts is the owned fact's synthetic record set for the nodes params
|
||||
// selects, taken from the /facts merge so owner and environment match it.
|
||||
func (s *Server) sourceFacts(ctx context.Context, inject *sourceInjector, params url.Values) ([]json.RawMessage, []backendResult, error) {
|
||||
alive, err := s.aliveResults(ctx, factsPath, params)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sourceFactRecords(s.mergeFactsWith(inject)(alive), inject.name, "", false), alive, nil
|
||||
}
|
||||
|
||||
// Both path forms are the facts entity with a name (and value) constraint ANDed
|
||||
@@ -427,7 +486,6 @@ func (s *Server) serveSourceFact(w http.ResponseWriter, r *http.Request, inject
|
||||
}
|
||||
|
||||
params := queryParams(r.URL.Query().Get("query"))
|
||||
merge := s.mergeFactsWith(inject)
|
||||
var filter recordFilter
|
||||
if valued {
|
||||
filter = func(recs []json.RawMessage) []json.RawMessage {
|
||||
@@ -437,11 +495,10 @@ func (s *Server) serveSourceFact(w http.ResponseWriter, r *http.Request, inject
|
||||
// The stored set is the whole owned-fact record set, so every value of it
|
||||
// keys, and waits on, the same fetch.
|
||||
s.serveFiltered(w, r, factsPath+"/"+inject.name, params, filter, func(ctx context.Context) (cachedResponse, error) {
|
||||
alive, err := s.aliveResults(ctx, factsPath, params)
|
||||
recs, alive, err := s.sourceFacts(ctx, inject, params)
|
||||
if err != nil {
|
||||
return cachedResponse{}, err
|
||||
}
|
||||
recs := sourceFactRecords(merge(alive), inject.name, "", false)
|
||||
resp := cachedResponse{Body: encodeRecords(recs), Records: -1}
|
||||
s.countBackends(&resp, alive)
|
||||
return resp, nil
|
||||
@@ -766,10 +823,6 @@ func (s *Server) mergeNodesResponse(r *http.Request) func([]backendResult) []jso
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) mergeFactsResponse(r *http.Request) func([]backendResult) []json.RawMessage {
|
||||
return s.mergeFactsWith(s.newSourceInjector(r.URL.Query().Get("query"), true))
|
||||
}
|
||||
|
||||
// The path segment of /facts/<name> is the same outer `name` constraint the
|
||||
// query gate already rules injection out on, so nothing is synthesised here:
|
||||
// the source fact's own path is diverted to serveSourceFact before this.
|
||||
|
||||
@@ -44,6 +44,8 @@ type fakeBackend struct {
|
||||
// rawPaths records the still-escaped request paths, so tests can assert an
|
||||
// MBean name's percent-encoding survived the proxy.
|
||||
rawPaths []string
|
||||
// handle, when set, runs first and answers the request itself by returning true.
|
||||
handle func(w http.ResponseWriter, r *http.Request) bool
|
||||
}
|
||||
|
||||
func newFakeBackend(t *testing.T, nodesBody, factsBody string) *fakeBackend {
|
||||
@@ -63,6 +65,9 @@ func newFakeBackend(t *testing.T, nodesBody, factsBody string) *fakeBackend {
|
||||
fb.gotParams[r.URL.Path] = r.URL.Query()
|
||||
fb.rawPaths = append(fb.rawPaths, r.URL.EscapedPath())
|
||||
fb.mu.Unlock()
|
||||
if fb.handle != nil && fb.handle(w, r) {
|
||||
return
|
||||
}
|
||||
if fb.dead {
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -204,3 +206,174 @@ func constrainsField(parts []json.RawMessage, field string) bool {
|
||||
var name string
|
||||
return json.Unmarshal(parts[1], &name) == nil && name == field
|
||||
}
|
||||
|
||||
// sourceQuery returns the parsed /facts query when one of its name comparisons
|
||||
// selects the owned fact and the whole query can be evaluated locally against a
|
||||
// fact record; nil otherwise, which leaves the query on the gated path.
|
||||
// ponytail: only a name match that is true for the owned fact selects it, so
|
||||
// ["not",["=","name","osfamily"]] stays on the gated path and synthesises
|
||||
// nothing; widen namesFact if a client needs negated selections.
|
||||
func (si *sourceInjector) sourceQuery(query string) []json.RawMessage {
|
||||
if si == nil {
|
||||
return nil
|
||||
}
|
||||
var ast []json.RawMessage
|
||||
if json.Unmarshal([]byte(query), &ast) != nil {
|
||||
return nil
|
||||
}
|
||||
probe := factFields{Name: si.name}
|
||||
if _, ok := matchFact(ast, probe); !ok || !namesFact(ast, probe) {
|
||||
return nil
|
||||
}
|
||||
return ast
|
||||
}
|
||||
|
||||
// factFields are the queryable columns of the facts entity (openvoxdb
|
||||
// engine.clj facts-query). value is a string here: the synthetic value always is.
|
||||
type factFields struct {
|
||||
Certname string `json:"certname"`
|
||||
Environment string `json:"environment"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (f factFields) column(name string) (string, bool) {
|
||||
switch name {
|
||||
case "certname":
|
||||
return f.Certname, true
|
||||
case "environment":
|
||||
return f.Environment, true
|
||||
case "name":
|
||||
return f.Name, true
|
||||
case "value":
|
||||
return f.Value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// namesFact reports whether a name comparison in the boolean skeleton matches f.
|
||||
func namesFact(parts []json.RawMessage, f factFields) bool {
|
||||
var op, field string
|
||||
if len(parts) < 2 || json.Unmarshal(parts[0], &op) != nil {
|
||||
return false
|
||||
}
|
||||
switch op {
|
||||
case "and", "or", "not":
|
||||
for _, p := range parts[1:] {
|
||||
var sub []json.RawMessage
|
||||
if json.Unmarshal(p, &sub) == nil && namesFact(sub, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if json.Unmarshal(parts[1], &field) != nil || field != "name" {
|
||||
return false
|
||||
}
|
||||
match, _ := matchFact(parts, f)
|
||||
return match
|
||||
}
|
||||
|
||||
// matchFact evaluates a facts query against one record with openvoxdb's
|
||||
// semantics for and/or/not, =, ~ and in-array. ok is false for anything else,
|
||||
// including subqueries, so a caller can refuse rather than guess.
|
||||
func matchFact(parts []json.RawMessage, f factFields) (match, ok bool) {
|
||||
var op string
|
||||
if len(parts) == 0 || json.Unmarshal(parts[0], &op) != nil {
|
||||
return false, false
|
||||
}
|
||||
switch op {
|
||||
case "and", "or":
|
||||
match = op == "and"
|
||||
for _, p := range parts[1:] {
|
||||
var sub []json.RawMessage
|
||||
if json.Unmarshal(p, &sub) != nil {
|
||||
return false, false
|
||||
}
|
||||
m, ok := matchFact(sub, f)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if op == "and" {
|
||||
match = match && m
|
||||
} else {
|
||||
match = match || m
|
||||
}
|
||||
}
|
||||
return match, true
|
||||
case "not":
|
||||
var sub []json.RawMessage
|
||||
if len(parts) != 2 || json.Unmarshal(parts[1], &sub) != nil {
|
||||
return false, false
|
||||
}
|
||||
m, ok := matchFact(sub, f)
|
||||
return !m, ok
|
||||
}
|
||||
var field string
|
||||
if len(parts) != 3 || json.Unmarshal(parts[1], &field) != nil {
|
||||
return false, false
|
||||
}
|
||||
got, known := f.column(field)
|
||||
if !known {
|
||||
return false, false
|
||||
}
|
||||
switch op {
|
||||
case "=":
|
||||
var want any
|
||||
if json.Unmarshal(parts[2], &want) != nil {
|
||||
return false, false
|
||||
}
|
||||
return want == any(got), true
|
||||
case "~":
|
||||
var pattern string
|
||||
if json.Unmarshal(parts[2], &pattern) != nil {
|
||||
return false, false
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return re.MatchString(got), true
|
||||
case "in":
|
||||
var arr []json.RawMessage
|
||||
var tag string
|
||||
var values []any
|
||||
if json.Unmarshal(parts[2], &arr) != nil || len(arr) != 2 ||
|
||||
json.Unmarshal(arr[0], &tag) != nil || tag != "array" ||
|
||||
json.Unmarshal(arr[1], &values) != nil {
|
||||
return false, false
|
||||
}
|
||||
return slices.Contains(values, any(got)), true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// certnameScope returns the top-level and's certname comparisons as a query,
|
||||
// or "" when there are none. Pushing them upstream keeps every record of a
|
||||
// selected node on every backend, so the merge's ownership is unchanged; an
|
||||
// environment comparison is not pushed because it can hide the owner's records
|
||||
// when backends disagree on a node's environment.
|
||||
func certnameScope(ast []json.RawMessage) string {
|
||||
var op string
|
||||
if len(ast) < 2 || json.Unmarshal(ast[0], &op) != nil || op != "and" {
|
||||
return ""
|
||||
}
|
||||
var keep []string
|
||||
for _, p := range ast[1:] {
|
||||
var sub []json.RawMessage
|
||||
var field string
|
||||
if json.Unmarshal(p, &sub) != nil || len(sub) != 3 || json.Unmarshal(sub[1], &field) != nil || field != "certname" {
|
||||
continue
|
||||
}
|
||||
if _, ok := matchFact(sub, factFields{}); ok {
|
||||
keep = append(keep, string(p))
|
||||
}
|
||||
}
|
||||
switch len(keep) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return keep[0]
|
||||
}
|
||||
return `["and",` + strings.Join(keep, ",") + `]`
|
||||
}
|
||||
|
||||
+93
-2
@@ -234,7 +234,8 @@ func TestHandler_UpstreamSourceFactSuppressedOnEveryGateState(t *testing.T) {
|
||||
}{
|
||||
{"injection on", "", "a"},
|
||||
{"gated by extract", `["extract",["certname","name","value"],["=","certname","h1"]]`, ""},
|
||||
{"gated by name filter", `["=","name","` + defaultSourceFact + `"]`, ""},
|
||||
{"gated by name filter", `["or",["=","name","role"],["=","name","os"]]`, ""},
|
||||
{"selected by name filter", `["or",["=","name","role"],["=","name","` + defaultSourceFact + `"]]`, "a"},
|
||||
{"gated by nested extract", `["and",["=","certname","h1"],["extract",["certname"]]]`, ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
@@ -391,7 +392,6 @@ func TestHandler_SourceNotInjectedWhenNameFiltered(t *testing.T) {
|
||||
for _, q := range []string{
|
||||
`["=","name","role"]`,
|
||||
`["and",["=","certname","h1"],["=","name","role"]]`,
|
||||
`["=","name","` + defaultSourceFact + `"]`,
|
||||
} {
|
||||
rec := doGet(t, srv.Handler(), factsPath, q)
|
||||
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 0 {
|
||||
@@ -568,3 +568,94 @@ func TestMergeFacts_SourceOrderedAfterOwnersFacts(t *testing.T) {
|
||||
t.Errorf("merged = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchFact(t *testing.T) {
|
||||
f := factFields{Certname: "h1", Environment: "prod", Name: defaultSourceFact, Value: "a"}
|
||||
tests := []struct {
|
||||
query string
|
||||
match, ok bool
|
||||
}{
|
||||
{`["=","name","` + defaultSourceFact + `"]`, true, true},
|
||||
{`["=","name","osfamily"]`, false, true},
|
||||
{`["=","value","a"]`, true, true},
|
||||
{`["=","value",1]`, false, true},
|
||||
{`["=","value",{"a":1}]`, false, true},
|
||||
{`["~","value","^a$"]`, true, true},
|
||||
{`["~","certname","^h"]`, true, true},
|
||||
{`["not",["=","value","a"]]`, false, true},
|
||||
{`["in","name",["array",["osfamily","` + defaultSourceFact + `"]]]`, true, true},
|
||||
{`["in","name",["array",["osfamily"]]]`, false, true},
|
||||
{`["and",["=","certname","h1"],["=","environment","prod"]]`, true, true},
|
||||
{`["and",["=","certname","h1"],["=","environment","dev"]]`, false, true},
|
||||
{`["or",["=","certname","h2"],["=","environment","prod"]]`, true, true},
|
||||
{`["and"]`, true, true},
|
||||
{`["or"]`, false, true},
|
||||
{`["and",["=","name","` + defaultSourceFact + `"],["null?","value",false]]`, false, false},
|
||||
{`["in","certname",["extract",["certname"],["select_nodes",["=","certname","h1"]]]]`, false, false},
|
||||
{`["=","fact_contents","x"]`, false, false},
|
||||
{`["~","value","("]`, false, false},
|
||||
{`["<","value","b"]`, false, false},
|
||||
{`["extract",["certname"],["=","name","x"]]`, false, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
var ast []json.RawMessage
|
||||
if err := json.Unmarshal([]byte(tc.query), &ast); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m, ok := matchFact(ast, f); m != tc.match || ok != tc.ok {
|
||||
t.Errorf("matchFact(%s) = %v, %v; want %v, %v", tc.query, m, ok, tc.match, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceInjector_SourceQuery(t *testing.T) {
|
||||
si := &sourceInjector{name: defaultSourceFact}
|
||||
tests := []struct {
|
||||
query string
|
||||
want bool
|
||||
}{
|
||||
{`["=","name","` + defaultSourceFact + `"]`, true},
|
||||
{`["and",["=","name","` + defaultSourceFact + `"]]`, true},
|
||||
{`["and",["or",["=","name","ipaddress"],["=","name","` + defaultSourceFact + `"]]]`, true},
|
||||
{`["and",["=","name","` + defaultSourceFact + `"],["not",["~","value","new"]]]`, true},
|
||||
{`["in","name",["array",["ipaddress","` + defaultSourceFact + `"]]]`, true},
|
||||
{`["~","name","^pdbmux"]`, true},
|
||||
{`["=","name","ipaddress"]`, false},
|
||||
{`["=","certname","h1"]`, false},
|
||||
{`["=","value","` + defaultSourceFact + `"]`, false},
|
||||
{`["and",["=","name","` + defaultSourceFact + `"],["in","certname",["extract",["certname"],["select_nodes",["=","certname","h1"]]]]]`, false},
|
||||
{``, false},
|
||||
{`not json`, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := si.sourceQuery(tc.query) != nil; got != tc.want {
|
||||
t.Errorf("sourceQuery(%s) = %v, want %v", tc.query, got, tc.want)
|
||||
}
|
||||
}
|
||||
var off *sourceInjector
|
||||
if off.sourceQuery(`["=","name","`+defaultSourceFact+`"]`) != nil {
|
||||
t.Error("disabled injector selected a query")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertnameScope(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`["=","name","x"]`: ``,
|
||||
`["and",["=","name","x"],["=","environment","prod"]]`: ``,
|
||||
`["or",["=","certname","h1"],["=","name","x"]]`: ``,
|
||||
`["and",["not",["=","certname","h1"]],["=","name","x"]]`: ``,
|
||||
`["and",["=","certname","h1"],["=","name","x"]]`: `["=","certname","h1"]`,
|
||||
`["and",["~","certname","["],["=","name","x"]]`: ``,
|
||||
`["and",["in","certname",["extract","certname",["=","a","b"]]],["=","name","x"]]`: ``,
|
||||
`["and",["in","certname",["array",["h1","h2"]]],["~","certname","^h"],["=","name","x"]]`: `["and",["in","certname",["array",["h1","h2"]]],["~","certname","^h"]]`,
|
||||
}
|
||||
for in, want := range tests {
|
||||
var ast []json.RawMessage
|
||||
if err := json.Unmarshal([]byte(in), &ast); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := certnameScope(ast); got != want {
|
||||
t.Errorf("certnameScope(%s) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user