Skip to content

Commit

Permalink
Node metadata matcher (#154)
Browse files Browse the repository at this point in the history
This PR adds a matcher for node metadata fields. This is for #125.

Similarly to #137, we encode the node metadata information required in the aggregation rules in a pair of protos: (1) NodeMetadataMatch, and (2) NodeMetadataAction, the former related to the matching process, whereas the latter has to do with the formation of the fragment.

Envoy's node metadata is an opaque struct of type google.protobuf.Struct, which basically represents a dictionary of strings to Value.

This PR adds support for 3 of the 6 values: string, bool, and a nested struct of Value. The support for these other types is going to be added in separate PRs.
  • Loading branch information
Eduardo Apolinario authored Nov 19, 2020
1 parent 1606087 commit 70181db
Show file tree
Hide file tree
Showing 5 changed files with 1,536 additions and 238 deletions.
49 changes: 46 additions & 3 deletions api/protos/aggregation/v1/aggregation.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ message KeyerConfiguration {
repeated Fragment fragments = 1 [(validate.rules).repeated.min_items = 1];
}

// [#next-free-field: 4]
message StringMatch {
oneof type {
option (validate.required) = true;
Expand All @@ -46,6 +47,12 @@ message StringMatch {
}
}

// [#next-free-field: 2]
message BoolMatch {
bool value_match = 1;
}

// [#next-free-field: 4]
message LocalityMatch {
StringMatch region = 1;

Expand All @@ -54,6 +61,31 @@ message LocalityMatch {
StringMatch sub_zone = 3;
}

// [#next-free-field: 2]
message PathSegment {
string key = 1 [(validate.rules).string.min_len = 1];
}

// [#next-free-field: 3]
message StructValueMatch {
// TODO: we have to match every single type described in
// https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Value.
oneof match {
option (validate.required) = true;

StringMatch string_match = 1;

BoolMatch bool_match = 2;
}
}

// [#next-free-field: 3]
message NodeMetadataMatch {
repeated PathSegment path = 1 [(validate.rules).repeated.min_items = 1];

StructValueMatch match = 2 [(validate.rules).message.required = true];
}

// This is a recursive structure which allows complex nested match
// configurations to be built using various logical operators.
// [#next-free-field: 7]
Expand All @@ -68,7 +100,7 @@ message MatchPredicate {
}

// Match on a field in Envoy's request node.
// [#next-free-field: 4]
// [#next-free-field: 5]
message RequestNodeMatch {
oneof type {
option (validate.required) = true;
Expand All @@ -78,6 +110,8 @@ message MatchPredicate {
StringMatch cluster_match = 2;

LocalityMatch locality_match = 3;

NodeMetadataMatch node_metadata_match = 4;
}
}

Expand Down Expand Up @@ -120,8 +154,8 @@ message MatchPredicate {
// [#next-free-field: 5]
message ResultPredicate {

// [#next-free-field: 3]
message ResultAction {

// TODO potentially use "safe regex"
// https://github.com/envoyproxy/envoy/blob/10f756efa17e56c8d4d1033be7b4286410db4e01/api/envoy/type/matcher/v3/regex.proto
// [#next-free-field: 3]
Expand Down Expand Up @@ -152,12 +186,20 @@ message ResultPredicate {
ResultAction subzone_action = 3;
}

// [#next-free-field: 3]
message NodeMetadataAction {
repeated PathSegment path = 1 [(validate.rules).repeated.min_items = 1];

ResultAction action = 2 [(validate.rules).message.required = true];
}

// [#next-free-field: 2]
message AndResult {
repeated ResultPredicate result_predicates = 1 [(validate.rules).repeated.min_items = 2];
}

// Rules for generating the resulting fragment from a Envoy request node.
// [#next-free-field: 4]
// [#next-free-field: 5]
message RequestNodeFragment {

oneof action {
Expand All @@ -166,6 +208,7 @@ message ResultPredicate {
ResultAction id_action = 1;
ResultAction cluster_action = 2;
LocalityResultAction locality_action = 3;
NodeMetadataAction node_metadata_action = 4;
}
}

Expand Down
69 changes: 69 additions & 0 deletions internal/app/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"github.com/envoyproxy/xds-relay/internal/app/metrics"
"github.com/envoyproxy/xds-relay/internal/app/transport"
"google.golang.org/protobuf/types/known/structpb"

"github.com/uber-go/tally"

Expand Down Expand Up @@ -155,6 +156,11 @@ func isNodeMatch(matchPredicate *matchPredicate, req transport.Request) (bool, e
return compareLocality(localityMatch, req.GetLocality())
}

nodeMetadataMatch := predicate.GetNodeMetadataMatch()
if nodeMetadataMatch != nil {
return compareNodeMetadata(nodeMetadataMatch, req.GetNodeMetadata())
}

return false, fmt.Errorf("RequestNodeMatch is invalid")
}

Expand Down Expand Up @@ -287,6 +293,9 @@ func getResultFromRequestNodePredicate(predicate *resultPredicate, req transport
resultFragment, err = getResultFragmentFromAction(req.GetCluster(), requestNodeFragment.GetClusterAction())
} else if requestNodeFragment.GetLocalityAction() != nil {
resultFragment, err = getFragmentFromLocalityAction(req.GetLocality(), requestNodeFragment.GetLocalityAction())
} else if requestNodeFragment.GetNodeMetadataAction() != nil {
resultFragment, err = getFragmentFromNodeMetadataAction(req.GetNodeMetadata(),
requestNodeFragment.GetNodeMetadataAction())
}

if err != nil {
Expand Down Expand Up @@ -439,6 +448,27 @@ func getFragmentFromLocalityAction(
return strings.Join(matches, "|"), nil
}

func getFragmentFromNodeMetadataAction(
nodeMetadata *structpb.Struct,
action *aggregationv1.ResultPredicate_NodeMetadataAction) (string, error) {
// Traverse to the right node
var value *structpb.Value = nil
var ok bool
for _, segment := range action.GetPath() {
fields := nodeMetadata.GetFields()
value, ok = fields[segment.Key]
if !ok {
// TODO what to do if the key doesn't map to a valid struct field?
return "", fmt.Errorf("Path to key is inexistent")
}
nodeMetadata = value.GetStructValue()
}

// TODO: We need to stringify values other than strings (bool, integers, etc) before
// extracting the fragment via a call to getResultFragmentFromAction.
return getResultFragmentFromAction(value.GetStringValue(), action.GetAction())
}

func compareString(stringMatch *aggregationv1.StringMatch, nodeValue string) (bool, error) {
if nodeValue == "" {
return false, fmt.Errorf("MatchPredicate Node field cannot be empty")
Expand All @@ -460,6 +490,10 @@ func compareString(stringMatch *aggregationv1.StringMatch, nodeValue string) (bo
return false, nil
}

func compareBool(boolMatch *aggregationv1.BoolMatch, boolValue bool) bool {
return boolMatch.ValueMatch == boolValue
}

func compareLocality(localityMatch *aggregationv1.LocalityMatch,
reqNodeLocality *transport.Locality) (bool, error) {
if reqNodeLocality == nil {
Expand Down Expand Up @@ -493,3 +527,38 @@ func compareLocality(localityMatch *aggregationv1.LocalityMatch,

return regionMatch && zoneMatch && subZoneMatch, nil
}

func compareNodeMetadata(nodeMetadataMatch *aggregationv1.NodeMetadataMatch,
nodeMetadata *structpb.Struct) (bool, error) {
if nodeMetadata == nil {
return false, fmt.Errorf("Metadata Node field cannot be empty")
}

var value *structpb.Value = nil
var ok bool
for _, segment := range nodeMetadataMatch.GetPath() {
// Starting from the second iteration, make sure that we're dealing with structs
if value != nil {
if value.GetStructValue() != nil {
nodeMetadata = value.GetStructValue()
} else {
// TODO: signal that the field is not a struct
return false, nil
}
}
fields := nodeMetadata.GetFields()
value, ok = fields[segment.Key]
if !ok {
return false, nil
}
}

// TODO: implement the other structpb.Value types.
if nodeMetadataMatch.Match.GetStringMatch() != nil {
return compareString(nodeMetadataMatch.Match.GetStringMatch(), value.GetStringValue())
} else if nodeMetadataMatch.Match.GetBoolMatch() != nil {
return compareBool(nodeMetadataMatch.Match.GetBoolMatch(), value.GetBoolValue()), nil
} else {
return false, fmt.Errorf("Invalid NodeMetadata Match")
}
}
Loading

0 comments on commit 70181db

Please sign in to comment.