Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node metadata matcher #154

Merged
merged 17 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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