open-consul/vendor/github.com/hashicorp/go-bexpr/evaluate.go
Matt Keeler ac78c23021
Implement data filtering of some endpoints (#5579)
Fixes: #4222 

# Data Filtering

This PR will implement filtering for the following endpoints:

## Supported HTTP Endpoints

- `/agent/checks`
- `/agent/services`
- `/catalog/nodes`
- `/catalog/service/:service`
- `/catalog/connect/:service`
- `/catalog/node/:node`
- `/health/node/:node`
- `/health/checks/:service`
- `/health/service/:service`
- `/health/connect/:service`
- `/health/state/:state`
- `/internal/ui/nodes`
- `/internal/ui/services`

More can be added going forward and any endpoint which is used to list some data is a good candidate.

## Usage

When using the HTTP API a `filter` query parameter can be used to pass a filter expression to Consul. Filter Expressions take the general form of:

```
<selector> == <value>
<selector> != <value>
<value> in <selector>
<value> not in <selector>
<selector> contains <value>
<selector> not contains <value>
<selector> is empty
<selector> is not empty
not <other expression>
<expression 1> and <expression 2>
<expression 1> or <expression 2>
```

Normal boolean logic and precedence is supported. All of the actual filtering and evaluation logic is coming from the [go-bexpr](https://github.com/hashicorp/go-bexpr) library

## Other changes

Adding the `Internal.ServiceDump` RPC endpoint. This will allow the UI to filter services better.
2019-04-16 12:00:15 -04:00

301 lines
9.7 KiB
Go

package bexpr
import (
"fmt"
"reflect"
"strings"
)
var primitiveEqualityFns = map[reflect.Kind]func(first interface{}, second reflect.Value) bool{
reflect.Bool: doEqualBool,
reflect.Int: doEqualInt,
reflect.Int8: doEqualInt8,
reflect.Int16: doEqualInt16,
reflect.Int32: doEqualInt32,
reflect.Int64: doEqualInt64,
reflect.Uint: doEqualUint,
reflect.Uint8: doEqualUint8,
reflect.Uint16: doEqualUint16,
reflect.Uint32: doEqualUint32,
reflect.Uint64: doEqualUint64,
reflect.Float32: doEqualFloat32,
reflect.Float64: doEqualFloat64,
reflect.String: doEqualString,
}
func doEqualBool(first interface{}, second reflect.Value) bool {
return first.(bool) == second.Bool()
}
func doEqualInt(first interface{}, second reflect.Value) bool {
return first.(int) == int(second.Int())
}
func doEqualInt8(first interface{}, second reflect.Value) bool {
return first.(int8) == int8(second.Int())
}
func doEqualInt16(first interface{}, second reflect.Value) bool {
return first.(int16) == int16(second.Int())
}
func doEqualInt32(first interface{}, second reflect.Value) bool {
return first.(int32) == int32(second.Int())
}
func doEqualInt64(first interface{}, second reflect.Value) bool {
return first.(int64) == second.Int()
}
func doEqualUint(first interface{}, second reflect.Value) bool {
return first.(uint) == uint(second.Uint())
}
func doEqualUint8(first interface{}, second reflect.Value) bool {
return first.(uint8) == uint8(second.Uint())
}
func doEqualUint16(first interface{}, second reflect.Value) bool {
return first.(uint16) == uint16(second.Uint())
}
func doEqualUint32(first interface{}, second reflect.Value) bool {
return first.(uint32) == uint32(second.Uint())
}
func doEqualUint64(first interface{}, second reflect.Value) bool {
return first.(uint64) == second.Uint()
}
func doEqualFloat32(first interface{}, second reflect.Value) bool {
return first.(float32) == float32(second.Float())
}
func doEqualFloat64(first interface{}, second reflect.Value) bool {
return first.(float64) == second.Float()
}
func doEqualString(first interface{}, second reflect.Value) bool {
return first.(string) == second.String()
}
// Get rid of 0 to many levels of pointers to get at the real type
func derefType(rtype reflect.Type) reflect.Type {
for rtype.Kind() == reflect.Ptr {
rtype = rtype.Elem()
}
return rtype
}
func doMatchEqual(expression *MatchExpression, value reflect.Value) (bool, error) {
// NOTE: see preconditions in evaluateMatchExpressionRecurse
eqFn := primitiveEqualityFns[value.Kind()]
matchValue := getMatchExprValue(expression)
return eqFn(matchValue, value), nil
}
func doMatchIn(expression *MatchExpression, value reflect.Value) (bool, error) {
// NOTE: see preconditions in evaluateMatchExpressionRecurse
matchValue := getMatchExprValue(expression)
switch kind := value.Kind(); kind {
case reflect.Map:
found := value.MapIndex(reflect.ValueOf(matchValue))
return found.IsValid(), nil
case reflect.Slice, reflect.Array:
itemType := derefType(value.Type().Elem())
eqFn := primitiveEqualityFns[itemType.Kind()]
for i := 0; i < value.Len(); i++ {
item := value.Index(i)
// the value will be the correct type as we verified the itemType
if eqFn(matchValue, reflect.Indirect(item)) {
return true, nil
}
}
return false, nil
case reflect.String:
return strings.Contains(value.String(), matchValue.(string)), nil
default:
// this shouldn't be possible but we have to have something to return to keep the compiler happy
return false, fmt.Errorf("Cannot perform in/contains operations on type %s for selector: %q", kind, expression.Selector)
}
}
func doMatchIsEmpty(matcher *MatchExpression, value reflect.Value) (bool, error) {
// NOTE: see preconditions in evaluateMatchExpressionRecurse
return value.Len() == 0, nil
}
func getMatchExprValue(expression *MatchExpression) interface{} {
// NOTE: see preconditions in evaluateMatchExpressionRecurse
if expression.Value == nil {
return nil
}
if expression.Value.Converted != nil {
return expression.Value.Converted
}
return expression.Value.Raw
}
func evaluateMatchExpressionRecurse(expression *MatchExpression, depth int, rvalue reflect.Value, fields FieldConfigurations) (bool, error) {
// NOTE: Some information about preconditions is probably good to have here. Parsing
// as well as the extra validation pass that MUST occur before executing the
// expression evaluation allow us to make some assumptions here.
//
// 1. Selectors MUST be valid. Therefore we don't need to test if they should
// be valid. This means that we can index in the FieldConfigurations map
// and a configuration MUST be present.
// 2. If expression.Value could be converted it will already have been. No need to try
// and convert again. There is also no need to check that the types match as they MUST
// in order to have passed validation.
// 3. If we are presented with a map and we have more selectors to go through then its key
// type MUST be a string
// 4. We already have validated that the operations can be performed on the target data.
// So calls to the doMatch* functions don't need to do any checking to ensure that
// calling various fns on them will work and not panic - because they wont.
if depth >= len(expression.Selector) {
// we have reached the end of the selector - execute the match operations
switch expression.Operator {
case MatchEqual:
return doMatchEqual(expression, rvalue)
case MatchNotEqual:
result, err := doMatchEqual(expression, rvalue)
if err == nil {
return !result, nil
}
return false, err
case MatchIn:
return doMatchIn(expression, rvalue)
case MatchNotIn:
result, err := doMatchIn(expression, rvalue)
if err == nil {
return !result, nil
}
return false, err
case MatchIsEmpty:
return doMatchIsEmpty(expression, rvalue)
case MatchIsNotEmpty:
result, err := doMatchIsEmpty(expression, rvalue)
if err == nil {
return !result, nil
}
return false, err
default:
return false, fmt.Errorf("Invalid match operation: %d", expression.Operator)
}
}
switch rvalue.Kind() {
case reflect.Struct:
fieldName := expression.Selector[depth]
fieldConfig := fields[FieldName(fieldName)]
if fieldConfig.StructFieldName != "" {
fieldName = fieldConfig.StructFieldName
}
value := reflect.Indirect(rvalue.FieldByName(fieldName))
if matcher, ok := value.Interface().(MatchExpressionEvaluator); ok {
return matcher.EvaluateMatch(expression.Selector[depth+1:], expression.Operator, getMatchExprValue(expression))
}
return evaluateMatchExpressionRecurse(expression, depth+1, value, fieldConfig.SubFields)
case reflect.Slice, reflect.Array:
// TODO (mkeeler) - Should we support implementing the MatchExpressionEvaluator interface for slice/array types?
// Punting on that for now.
for i := 0; i < rvalue.Len(); i++ {
item := reflect.Indirect(rvalue.Index(i))
// we use the same depth because right now we are not allowing
// selection of individual slice/array elements
result, err := evaluateMatchExpressionRecurse(expression, depth, item, fields)
if err != nil {
return false, err
}
// operations on slices are implicity ANY operations currently so the first truthy evaluation we find we can stop
if result {
return true, nil
}
}
return false, nil
case reflect.Map:
// TODO (mkeeler) - Should we support implementing the MatchExpressionEvaluator interface for map types
// such as the FieldConfigurations type? Maybe later
//
value := reflect.Indirect(rvalue.MapIndex(reflect.ValueOf(expression.Selector[depth])))
if !value.IsValid() {
// when the key doesn't exist in the map
switch expression.Operator {
case MatchEqual, MatchIsNotEmpty, MatchIn:
return false, nil
default:
// MatchNotEqual, MatchIsEmpty, MatchNotIn
// Whatever you were looking for cannot be equal because it doesn't exist
// Similarly it cannot be in some other container and every other container
// is always empty.
return true, nil
}
}
if matcher, ok := value.Interface().(MatchExpressionEvaluator); ok {
return matcher.EvaluateMatch(expression.Selector[depth+1:], expression.Operator, getMatchExprValue(expression))
}
return evaluateMatchExpressionRecurse(expression, depth+1, value, fields[FieldNameAny].SubFields)
default:
return false, fmt.Errorf("Value at selector %q with type %s does not support nested field selection", expression.Selector[:depth], rvalue.Kind())
}
}
func evaluateMatchExpression(expression *MatchExpression, datum interface{}, fields FieldConfigurations) (bool, error) {
if matcher, ok := datum.(MatchExpressionEvaluator); ok {
return matcher.EvaluateMatch(expression.Selector, expression.Operator, getMatchExprValue(expression))
}
rvalue := reflect.Indirect(reflect.ValueOf(datum))
return evaluateMatchExpressionRecurse(expression, 0, rvalue, fields)
}
func evaluate(ast Expression, datum interface{}, fields FieldConfigurations) (bool, error) {
switch node := ast.(type) {
case *UnaryExpression:
switch node.Operator {
case UnaryOpNot:
result, err := evaluate(node.Operand, datum, fields)
return !result, err
}
case *BinaryExpression:
switch node.Operator {
case BinaryOpAnd:
result, err := evaluate(node.Left, datum, fields)
if err != nil || result == false {
return result, err
}
return evaluate(node.Right, datum, fields)
case BinaryOpOr:
result, err := evaluate(node.Left, datum, fields)
if err != nil || result == true {
return result, err
}
return evaluate(node.Right, datum, fields)
}
case *MatchExpression:
return evaluateMatchExpression(node, datum, fields)
}
return false, fmt.Errorf("Invalid AST node")
}