/* * * Copyright 2020 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package rls import ( "encoding/json" "fmt" "strings" "testing" "time" _ "google.golang.org/grpc/balancer/grpclb" // grpclb for config parsing. _ "google.golang.org/grpc/internal/resolver/passthrough" // passthrough resolver. ) // testEqual reports whether the lbCfgs a and b are equal. This is to be used // only from tests. This ignores the keyBuilderMap field because its internals // are not exported, and hence not possible to specify in the want section of // the test. This is fine because we already have tests to make sure that the // keyBuilder is parsed properly from the service config. func testEqual(a, b *lbConfig) bool { return a.lookupService == b.lookupService && a.lookupServiceTimeout == b.lookupServiceTimeout && a.maxAge == b.maxAge && a.staleAge == b.staleAge && a.cacheSizeBytes == b.cacheSizeBytes && a.defaultTarget == b.defaultTarget && a.controlChannelServiceConfig == b.controlChannelServiceConfig && a.childPolicyName == b.childPolicyName && a.childPolicyTargetField == b.childPolicyTargetField && childPolicyConfigEqual(a.childPolicyConfig, b.childPolicyConfig) } // TestParseConfig verifies successful config parsing scenarios. func (s) TestParseConfig(t *testing.T) { childPolicyTargetFieldVal, _ := json.Marshal(dummyChildPolicyTarget) tests := []struct { desc string input []byte wantCfg *lbConfig }{ { // This input validates a few cases: // - A top-level unknown field should not fail. // - An unknown field in routeLookupConfig proto should not fail. // - lookupServiceTimeout is set to its default value, since it is not specified in the input. // - maxAge is set to maxMaxAge since the value is too large in the input. // - staleAge is ignore because it is higher than maxAge in the input. // - cacheSizeBytes is greater than the hard upper limit of 5MB desc: "with transformations 1", input: []byte(`{ "top-level-unknown-field": "unknown-value", "routeLookupConfig": { "unknown-field": "unknown-value", "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": ":///target", "maxAge" : "500s", "staleAge": "600s", "cacheSizeBytes": 100000000, "defaultTarget": "passthrough:///default" }, "childPolicy": [ {"cds_experimental": {"Cluster": "my-fav-cluster"}}, {"unknown-policy": {"unknown-field": "unknown-value"}}, {"grpclb": {"childPolicy": [{"pickfirst": {}}]}} ], "childPolicyConfigTargetFieldName": "serviceName" }`), wantCfg: &lbConfig{ lookupService: ":///target", lookupServiceTimeout: 10 * time.Second, // This is the default value. maxAge: 5 * time.Minute, // This is max maxAge. staleAge: time.Duration(0), // StaleAge is ignore because it was higher than maxAge. cacheSizeBytes: maxCacheSize, defaultTarget: "passthrough:///default", childPolicyName: "grpclb", childPolicyTargetField: "serviceName", childPolicyConfig: map[string]json.RawMessage{ "childPolicy": json.RawMessage(`[{"pickfirst": {}}]`), "serviceName": json.RawMessage(childPolicyTargetFieldVal), }, }, }, { desc: "without transformations", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "target", "lookupServiceTimeout" : "100s", "maxAge": "60s", "staleAge" : "50s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "routeLookupChannelServiceConfig": {"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]}, "childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}], "childPolicyConfigTargetFieldName": "serviceName" }`), wantCfg: &lbConfig{ lookupService: "target", lookupServiceTimeout: 100 * time.Second, maxAge: 60 * time.Second, staleAge: 50 * time.Second, cacheSizeBytes: 1000, defaultTarget: "passthrough:///default", controlChannelServiceConfig: `{"loadBalancingConfig": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}]}`, childPolicyName: "grpclb", childPolicyTargetField: "serviceName", childPolicyConfig: map[string]json.RawMessage{ "childPolicy": json.RawMessage(`[{"pickfirst": {}}]`), "serviceName": json.RawMessage(childPolicyTargetFieldVal), }, }, }, } builder := rlsBB{} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { lbCfg, err := builder.ParseConfig(test.input) if err != nil || !testEqual(lbCfg.(*lbConfig), test.wantCfg) { t.Errorf("ParseConfig(%s) = {%+v, %v}, want {%+v, nil}", string(test.input), lbCfg, err, test.wantCfg) } }) } } // TestParseConfigErrors verifies config parsing failure scenarios. func (s) TestParseConfigErrors(t *testing.T) { tests := []struct { desc string input []byte wantErr string }{ { desc: "empty input", input: nil, wantErr: "rls: json unmarshal failed for service config", }, { desc: "bad json", input: []byte(`bad bad json`), wantErr: "rls: json unmarshal failed for service config", }, { desc: "bad grpcKeyBuilder", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "requiredMatch": true, "names": ["v1"]}] }] } }`), wantErr: "rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set", }, { desc: "empty lookup service", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }] } }`), wantErr: "rls: empty lookup_service in route lookup config", }, { desc: "unregistered scheme in lookup service URI", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "badScheme:///target" } }`), wantErr: "rls: unregistered scheme in lookup_service", }, { desc: "invalid lookup service timeout", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "315576000001s" } }`), wantErr: "google.protobuf.Duration value out of range", }, { desc: "invalid max age", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge" : "315576000001s" } }`), wantErr: "google.protobuf.Duration value out of range", }, { desc: "invalid stale age", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge" : "10s", "staleAge" : "315576000001s" } }`), wantErr: "google.protobuf.Duration value out of range", }, { desc: "invalid max age stale age combo", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "staleAge" : "10s" } }`), wantErr: "rls: stale_age is set, but max_age is not in route lookup config", }, { desc: "cache_size_bytes field is not set", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "defaultTarget": "passthrough:///default" }, "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "rls: cache_size_bytes must be set to a non-zero value", }, { desc: "routeLookupChannelServiceConfig is not in service config format", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "target", "lookupServiceTimeout" : "100s", "maxAge": "60s", "staleAge" : "50s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "routeLookupChannelServiceConfig": "unknown", "childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}], "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "cannot unmarshal string into Go value of type grpc.jsonSC", }, { desc: "routeLookupChannelServiceConfig contains unknown LB policy", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "target", "lookupServiceTimeout" : "100s", "maxAge": "60s", "staleAge" : "50s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "routeLookupChannelServiceConfig": { "loadBalancingConfig": [{"not_a_balancer1": {} }, {"not_a_balancer2": {}}] }, "childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}], "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "invalid loadBalancingConfig: no supported policies found", }, { desc: "no child policy", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "rls: invalid childPolicy config: no supported policies found", }, { desc: "no known child policy", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "childPolicy": [ {"cds_experimental": {"Cluster": "my-fav-cluster"}}, {"unknown-policy": {"unknown-field": "unknown-value"}} ], "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "rls: invalid childPolicy config: no supported policies found", }, { desc: "invalid child policy config - more than one entry in map", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "childPolicy": [ { "cds_experimental": {"Cluster": "my-fav-cluster"}, "unknown-policy": {"unknown-field": "unknown-value"} } ], "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "does not contain exactly 1 policy/config pair", }, { desc: "no childPolicyConfigTargetFieldName", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "childPolicy": [ {"cds_experimental": {"Cluster": "my-fav-cluster"}}, {"unknown-policy": {"unknown-field": "unknown-value"}}, {"grpclb": {}} ] }`), wantErr: "rls: childPolicyConfigTargetFieldName field is not set in service config", }, { desc: "child policy config validation failure", input: []byte(`{ "routeLookupConfig": { "grpcKeybuilders": [{ "names": [{"service": "service", "method": "method"}], "headers": [{"key": "k1", "names": ["v1"]}] }], "lookupService": "passthrough:///target", "lookupServiceTimeout" : "10s", "maxAge": "30s", "staleAge" : "25s", "cacheSizeBytes": 1000, "defaultTarget": "passthrough:///default" }, "childPolicy": [ {"cds_experimental": {"Cluster": "my-fav-cluster"}}, {"unknown-policy": {"unknown-field": "unknown-value"}}, {"grpclb": {"childPolicy": "not-an-array"}} ], "childPolicyConfigTargetFieldName": "serviceName" }`), wantErr: "rls: childPolicy config validation failed", }, } builder := rlsBB{} for _, test := range tests { t.Run(test.desc, func(t *testing.T) { lbCfg, err := builder.ParseConfig(test.input) if lbCfg != nil || !strings.Contains(fmt.Sprint(err), test.wantErr) { t.Errorf("ParseConfig(%s) = {%+v, %v}, want {nil, %s}", string(test.input), lbCfg, err, test.wantErr) } }) } }