mirror of
https://github.com/status-im/consul.git
synced 2025-01-16 08:45:37 +00:00
afa1cc98d1
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.
163 lines
4.5 KiB
Go
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)
|
|
}
|