Matt Keeler afa1cc98d1
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

163 lines
4.5 KiB
Go

// bexpr is an implementation of a generic boolean expression evaluator.
// The general goal is to be able to evaluate some expression against some
// arbitrary data and get back a boolean of whether or not the data
// was matched by the expression
package bexpr
import (
"fmt"
"reflect"
)
const (
defaultMaxMatches = 32
defaultMaxRawValueLength = 512
)
// MatchExpressionEvaluator is the interface to implement to provide custom evaluation
// logic for a selector. This could be used to enable synthetic fields or other
// more complex logic that the default behavior does not support
type MatchExpressionEvaluator interface {
// FieldConfigurations returns the configuration for this field and any subfields
// it may have. It must be valid to call this method on nil.
FieldConfigurations() FieldConfigurations
// EvaluateMatch returns whether there was a match or not. We are not also
// expecting any errors because all the validation bits are handled
// during parsing and cross checking against the output of FieldConfigurations.
EvaluateMatch(sel Selector, op MatchOperator, value interface{}) (bool, error)
}
type Evaluator struct {
// The syntax tree
ast Expression
// A few configurations for extra validation of the AST
config EvaluatorConfig
// Once an expression has been run against a particular data type it cannot be executed
// against a different data type. Some coerced value memoization occurs which would
// be invalid against other data types.
boundType reflect.Type
// The field configuration of the boundType
fields FieldConfigurations
}
// Extra configuration used to perform further validation on a parsed
// expression and to aid in the evaluation process
type EvaluatorConfig struct {
// Maximum number of matching expressions allowed. 0 means unlimited
// This does not include and, or and not expressions within the AST
MaxMatches int
// Maximum length of raw values. 0 means unlimited
MaxRawValueLength int
// The Registry to use for validating expressions for a data type
// If nil the `DefaultRegistry` will be used. To disable using a
// registry all together you can set this to `NilRegistry`
Registry Registry
}
func CreateEvaluator(expression string, config *EvaluatorConfig) (*Evaluator, error) {
return CreateEvaluatorForType(expression, config, nil)
}
func CreateEvaluatorForType(expression string, config *EvaluatorConfig, dataType interface{}) (*Evaluator, error) {
ast, err := Parse("", []byte(expression))
if err != nil {
return nil, err
}
eval := &Evaluator{ast: ast.(Expression)}
if config == nil {
config = &eval.config
}
err = eval.validate(config, dataType, true)
if err != nil {
return nil, err
}
return eval, nil
}
func (eval *Evaluator) Evaluate(datum interface{}) (bool, error) {
if eval.fields == nil {
err := eval.validate(&eval.config, datum, true)
if err != nil {
return false, err
}
} else if reflect.TypeOf(datum) != eval.boundType {
return false, fmt.Errorf("This evaluator can only be used to evaluate matches against %s", eval.boundType)
}
return evaluate(eval.ast, datum, eval.fields)
}
func (eval *Evaluator) validate(config *EvaluatorConfig, dataType interface{}, updateEvaluator bool) error {
if config == nil {
return fmt.Errorf("Invalid config")
}
var fields FieldConfigurations
var err error
var rtype reflect.Type
if dataType != nil {
registry := DefaultRegistry
if config.Registry != nil {
registry = config.Registry
}
switch t := dataType.(type) {
case reflect.Type:
rtype = t
case *reflect.Type:
rtype = *t
case reflect.Value:
rtype = t.Type()
case *reflect.Value:
rtype = t.Type()
default:
rtype = reflect.TypeOf(dataType)
}
fields, err = registry.GetFieldConfigurations(rtype)
if err != nil {
return err
}
if len(fields) < 1 {
return fmt.Errorf("Data type %s has no evaluatable fields", rtype.String())
}
}
maxMatches := config.MaxMatches
if maxMatches == 0 {
maxMatches = defaultMaxMatches
}
maxRawValueLength := config.MaxRawValueLength
if maxRawValueLength == 0 {
maxRawValueLength = defaultMaxRawValueLength
}
err = validate(eval.ast, fields, config.MaxMatches, config.MaxRawValueLength)
if err != nil {
return err
}
if updateEvaluator {
eval.config = *config
eval.fields = fields
eval.boundType = rtype
}
return nil
}
// Validates an existing expression against a possibly different configuration
func (eval *Evaluator) Validate(config *EvaluatorConfig, dataType interface{}) error {
return eval.validate(config, dataType, false)
}