| /* |
| * |
| * 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" |
| |
| "github.com/google/go-cmp/cmp" |
| |
| "google.golang.org/grpc/balancer" |
| _ "google.golang.org/grpc/balancer/grpclb" // grpclb for config parsing. |
| _ "google.golang.org/grpc/internal/resolver/passthrough" // passthrough resolver. |
| ) |
| |
| const balancerWithoutConfigParserName = "dummy_balancer" |
| |
| type dummyBB struct { |
| balancer.Builder |
| } |
| |
| func (*dummyBB) Name() string { |
| return balancerWithoutConfigParserName |
| } |
| |
| func init() { |
| balancer.Register(&dummyBB{}) |
| } |
| |
| // 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.cpName == b.cpName && |
| a.cpTargetField == b.cpTargetField && |
| cmp.Equal(a.cpConfig, b.cpConfig) |
| } |
| |
| func TestParseConfig(t *testing.T) { |
| 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. |
| { |
| desc: "with transformations", |
| 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": "passthrough:///target", |
| "maxAge" : "500s", |
| "staleAge": "600s", |
| "cacheSizeBytes": 1000, |
| "defaultTarget": "passthrough:///default" |
| }, |
| "childPolicy": [ |
| {"cds_experimental": {"Cluster": "my-fav-cluster"}}, |
| {"unknown-policy": {"unknown-field": "unknown-value"}}, |
| {"grpclb": {"childPolicy": [{"pickfirst": {}}]}} |
| ], |
| "childPolicyConfigTargetFieldName": "service_name" |
| }`), |
| wantCfg: &lbConfig{ |
| lookupService: "passthrough:///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: 1000, |
| defaultTarget: "passthrough:///default", |
| cpName: "grpclb", |
| cpTargetField: "service_name", |
| cpConfig: map[string]json.RawMessage{"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`)}, |
| }, |
| }, |
| { |
| desc: "without transformations", |
| input: []byte(`{ |
| "routeLookupConfig": { |
| "grpcKeybuilders": [{ |
| "names": [{"service": "service", "method": "method"}], |
| "headers": [{"key": "k1", "names": ["v1"]}] |
| }], |
| "lookupService": "passthrough:///target", |
| "lookupServiceTimeout" : "100s", |
| "maxAge": "60s", |
| "staleAge" : "50s", |
| "cacheSizeBytes": 1000, |
| "defaultTarget": "passthrough:///default" |
| }, |
| "childPolicy": [{"grpclb": {"childPolicy": [{"pickfirst": {}}]}}], |
| "childPolicyConfigTargetFieldName": "service_name" |
| }`), |
| wantCfg: &lbConfig{ |
| lookupService: "passthrough:///target", |
| lookupServiceTimeout: 100 * time.Second, |
| maxAge: 60 * time.Second, |
| staleAge: 50 * time.Second, |
| cacheSizeBytes: 1000, |
| defaultTarget: "passthrough:///default", |
| cpName: "grpclb", |
| cpTargetField: "service_name", |
| cpConfig: map[string]json.RawMessage{"childPolicy": json.RawMessage(`[{"pickfirst": {}}]`)}, |
| }, |
| }, |
| } |
| |
| 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) |
| } |
| }) |
| } |
| } |
| |
| func 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 service config", |
| }, |
| { |
| desc: "invalid lookup service URI", |
| input: []byte(`{ |
| "routeLookupConfig": { |
| "grpcKeybuilders": [{ |
| "names": [{"service": "service", "method": "method"}], |
| "headers": [{"key": "k1", "names": ["v1"]}] |
| }], |
| "lookupService": "badScheme:///target" |
| } |
| }`), |
| wantErr: "rls: invalid target URI 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: "bad Duration: time: invalid duration", |
| }, |
| { |
| 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: "bad Duration: time: invalid duration", |
| }, |
| { |
| 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: "bad Duration: time: invalid duration", |
| }, |
| { |
| 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 service config", |
| }, |
| { |
| desc: "invalid cache size", |
| input: []byte(`{ |
| "routeLookupConfig": { |
| "grpcKeybuilders": [{ |
| "names": [{"service": "service", "method": "method"}], |
| "headers": [{"key": "k1", "names": ["v1"]}] |
| }], |
| "lookupService": "passthrough:///target", |
| "lookupServiceTimeout" : "10s", |
| "maxAge": "30s", |
| "staleAge" : "25s" |
| } |
| }`), |
| wantErr: "rls: cache_size_bytes must be greater than 0 in service config", |
| }, |
| { |
| 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" |
| } |
| }`), |
| wantErr: "rls: childPolicy is invalid in service config", |
| }, |
| { |
| 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"}} |
| ] |
| }`), |
| wantErr: "rls: childPolicy is invalid in service config", |
| }, |
| { |
| 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": "service_name" |
| }`), |
| 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) |
| } |
| }) |
| } |
| } |
| |
| func TestValidateChildPolicyConfig(t *testing.T) { |
| jsonCfg := json.RawMessage(`[{"round_robin" : {}}, {"pick_first" : {}}]`) |
| wantChildConfig := map[string]json.RawMessage{"childPolicy": jsonCfg} |
| cp := &loadBalancingConfig{ |
| Name: "grpclb", |
| Config: []byte(`{"childPolicy": [{"round_robin" : {}}, {"pick_first" : {}}]}`), |
| } |
| cpTargetField := "serviceName" |
| |
| gotChildConfig, err := validateChildPolicyConfig(cp, cpTargetField) |
| if err != nil || !cmp.Equal(gotChildConfig, wantChildConfig) { |
| t.Errorf("validateChildPolicyConfig(%v, %v) = {%v, %v}, want {%v, nil}", cp, cpTargetField, gotChildConfig, err, wantChildConfig) |
| } |
| } |
| |
| func TestValidateChildPolicyConfigErrors(t *testing.T) { |
| tests := []struct { |
| desc string |
| cp *loadBalancingConfig |
| wantErrPrefix string |
| }{ |
| { |
| desc: "unknown child policy", |
| cp: &loadBalancingConfig{ |
| Name: "unknown", |
| Config: []byte(`{}`), |
| }, |
| wantErrPrefix: "rls: balancer builder not found for child_policy", |
| }, |
| { |
| desc: "balancer builder does not implement ConfigParser", |
| cp: &loadBalancingConfig{ |
| Name: balancerWithoutConfigParserName, |
| Config: []byte(`{}`), |
| }, |
| wantErrPrefix: "rls: balancer builder for child_policy does not implement balancer.ConfigParser", |
| }, |
| { |
| desc: "child policy config parsing failure", |
| cp: &loadBalancingConfig{ |
| Name: "grpclb", |
| Config: []byte(`{"childPolicy": "not-an-array"}`), |
| }, |
| wantErrPrefix: "rls: childPolicy config validation failed", |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| gotChildConfig, gotErr := validateChildPolicyConfig(test.cp, "") |
| if gotChildConfig != nil || !strings.HasPrefix(fmt.Sprint(gotErr), test.wantErrPrefix) { |
| t.Errorf("validateChildPolicyConfig(%v) = {%v, %v}, want {nil, %v}", test.cp, gotChildConfig, gotErr, test.wantErrPrefix) |
| } |
| }) |
| } |
| } |