github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/filter/compound/compound.go (about) 1 // Copyright (c) 2021-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package compound 6 7 import ( 8 "encoding/json" 9 "fmt" 10 11 "github.com/Masterminds/semver" 12 "github.com/expr-lang/expr" 13 "github.com/expr-lang/expr/vm" 14 "github.com/google/go-cmp/cmp" 15 "github.com/tidwall/gjson" 16 17 "github.com/choria-io/go-choria/filter/agents" 18 "github.com/choria-io/go-choria/filter/classes" 19 "github.com/choria-io/go-choria/filter/facts" 20 "github.com/choria-io/go-choria/providers/data/ddl" 21 ) 22 23 // Logger provides logging facilities 24 type Logger interface { 25 Warnf(format string, args ...any) 26 Debugf(format string, args ...any) 27 Errorf(format string, args ...any) 28 } 29 30 func MatchExprStringFiles(queries [][]map[string]string, factFile string, classesFile string, knownAgents []string, log Logger) bool { 31 c, err := classes.ReadClasses(classesFile) 32 if err != nil { 33 log.Errorf("cannot read classes file: %s", err) 34 return false 35 } 36 37 f, err := facts.JSON(factFile, log) 38 if err != nil { 39 log.Errorf("cannot read facts file: %s", err) 40 return false 41 } 42 43 return MatchExprString(queries, f, c, knownAgents, nil, log) 44 } 45 46 func CompileExprQuery(query string, df ddl.FuncMap) (*vm.Program, error) { 47 return expr.Compile(query, expr.Env(EmptyEnv(df)), expr.AsBool(), expr.AllowUndefinedVariables()) 48 } 49 50 func MatchExprProgram(prog *vm.Program, facts json.RawMessage, classes []string, knownAgents []string, df ddl.FuncMap, log Logger) (bool, error) { 51 env := EmptyEnv(df) 52 env["classes"] = classes 53 env["agents"] = knownAgents 54 env["facts"] = facts 55 env["with"] = matchFunc(facts, classes, knownAgents, log) 56 env["fact"] = factFunc(facts) 57 env["include"] = includeFunc 58 env["semver"] = semverFunc 59 60 res, err := expr.Run(prog, env) 61 if err != nil { 62 return false, fmt.Errorf("could not execute compound query: %s", err) 63 } 64 65 b, ok := res.(bool) 66 if !ok { 67 return false, fmt.Errorf("compound query returned non boolean") 68 } 69 70 return b, nil 71 } 72 73 func MatchExprString(queries [][]map[string]string, facts json.RawMessage, classes []string, knownAgents []string, df ddl.FuncMap, log Logger) bool { 74 matched := 0 75 failed := 0 76 77 for _, cf := range queries { 78 if len(cf) != 1 { 79 return false 80 } 81 82 query, ok := cf[0]["expr"] 83 if !ok { 84 return false 85 } 86 87 prog, err := CompileExprQuery(query, df) 88 if err != nil { 89 log.Errorf("Could not compile compound query '%s': %s", query, err) 90 failed++ 91 continue 92 } 93 94 res, err := MatchExprProgram(prog, facts, classes, knownAgents, df, log) 95 if err != nil { 96 log.Errorf("Could not match compound query '%s': %s", query, err) 97 failed++ 98 continue 99 } 100 101 if res { 102 matched++ 103 } else { 104 matched-- 105 } 106 } 107 108 return failed == 0 && matched > 0 109 } 110 111 func matchFunc(f json.RawMessage, c []string, a []string, log Logger) func(string) bool { 112 return func(query string) bool { 113 pf, err := facts.ParseFactFilterString(query) 114 if err == nil { 115 return facts.MatchFacts([][3]string{pf}, f, log) 116 } 117 118 if classes.Match([]string{query}, c) { 119 return true 120 } 121 122 return agents.Match([]string{query}, a) 123 } 124 } 125 126 func factFunc(facts json.RawMessage) func(string) any { 127 return func(query string) any { 128 return gjson.GetBytes(facts, query).Value() 129 } 130 } 131 132 func semverFunc(value string, cmp string) (bool, error) { 133 cons, err := semver.NewConstraint(cmp) 134 if err != nil { 135 return false, err 136 } 137 138 v, err := semver.NewVersion(value) 139 if err != nil { 140 return false, err 141 } 142 143 return cons.Check(v), nil 144 } 145 146 func includeFunc(hay []any, needle any) bool { 147 // gjson always turns numbers into float64 148 i, ok := needle.(int) 149 if ok { 150 needle = float64(i) 151 } 152 153 for _, i := range hay { 154 if cmp.Equal(i, needle) { 155 return true 156 } 157 } 158 159 return false 160 } 161 162 func EmptyEnv(df ddl.FuncMap) map[string]any { 163 env := map[string]any{ 164 "agents": []string{}, 165 "classes": []string{}, 166 "facts": json.RawMessage{}, 167 "with": func(_ string) bool { return false }, 168 "fact": func(_ string) any { return nil }, 169 "include": func(_ []any, _ any) bool { return false }, 170 "semver": func(_ string, _ string) (bool, error) { return false, nil }, 171 } 172 173 for k, v := range df { 174 _, ok := env[k] 175 if ok { 176 continue 177 } 178 179 env[k] = v.F 180 } 181 182 return env 183 }