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  }