/* * * 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 resolver import ( "testing" "github.com/google/go-cmp/cmp" "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/grpcrand" "google.golang.org/grpc/serviceconfig" _ "google.golang.org/grpc/xds/internal/balancer/weightedtarget" _ "google.golang.org/grpc/xds/internal/balancer/xdsrouting" xdsclient "google.golang.org/grpc/xds/internal/client" ) const ( testCluster1 = "test-cluster-1" testOneClusterOnlyJSON = `{"loadBalancingConfig":[{ "xds_routing_experimental":{ "action":{ "test-cluster-1_0":{ "childPolicy":[{ "weighted_target_experimental":{ "targets":{ "test-cluster-1":{ "weight":1, "childPolicy":[{"cds_experimental":{"cluster":"test-cluster-1"}}] } }}}] } }, "route":[{"prefix":"","action":"test-cluster-1_0"}] }}]}` testWeightedCDSJSON = `{"loadBalancingConfig":[{ "xds_routing_experimental":{ "action":{ "cluster_1_cluster_2_1":{ "childPolicy":[{ "weighted_target_experimental":{ "targets":{ "cluster_1":{ "weight":75, "childPolicy":[{"cds_experimental":{"cluster":"cluster_1"}}] }, "cluster_2":{ "weight":25, "childPolicy":[{"cds_experimental":{"cluster":"cluster_2"}}] } }}}] } }, "route":[{"prefix":"","action":"cluster_1_cluster_2_1"}] }}]}` testRoutingJSON = `{"loadBalancingConfig":[{ "xds_routing_experimental": { "action":{ "cluster_1_cluster_2_0":{ "childPolicy":[{ "weighted_target_experimental": { "targets": { "cluster_1" : { "weight":75, "childPolicy":[{"cds_experimental":{"cluster":"cluster_1"}}] }, "cluster_2" : { "weight":25, "childPolicy":[{"cds_experimental":{"cluster":"cluster_2"}}] } } } }] } }, "route":[{ "path":"/service_1/method_1", "action":"cluster_1_cluster_2_0" }] } }]} ` testRoutingAllMatchersJSON = `{"loadBalancingConfig":[{ "xds_routing_experimental": { "action":{ "cluster_1_0":{ "childPolicy":[{ "weighted_target_experimental": { "targets": { "cluster_1" : { "weight":1, "childPolicy":[{"cds_experimental":{"cluster":"cluster_1"}}] } } } }] }, "cluster_2_0":{ "childPolicy":[{ "weighted_target_experimental": { "targets": { "cluster_2" : { "weight":1, "childPolicy":[{"cds_experimental":{"cluster":"cluster_2"}}] } } } }] }, "cluster_3_0":{ "childPolicy":[{ "weighted_target_experimental": { "targets": { "cluster_3" : { "weight":1, "childPolicy":[{"cds_experimental":{"cluster":"cluster_3"}}] } } } }] } }, "route":[{ "path":"/service_1/method_1", "action":"cluster_1_0" }, { "prefix":"/service_2/method_1", "action":"cluster_1_0" }, { "regex":"^/service_2/method_3$", "action":"cluster_1_0" }, { "prefix":"", "headers":[{"name":"header-1", "exactMatch":"value-1", "invertMatch":true}], "action":"cluster_2_0" }, { "prefix":"", "headers":[{"name":"header-1", "regexMatch":"^value-1$"}], "action":"cluster_2_0" }, { "prefix":"", "headers":[{"name":"header-1", "rangeMatch":{"start":-1, "end":7}}], "action":"cluster_3_0" }, { "prefix":"", "headers":[{"name":"header-1", "presentMatch":true}], "action":"cluster_3_0" }, { "prefix":"", "headers":[{"name":"header-1", "prefixMatch":"value-1"}], "action":"cluster_2_0" }, { "prefix":"", "headers":[{"name":"header-1", "suffixMatch":"value-1"}], "action":"cluster_2_0" }, { "prefix":"", "matchFraction":{"value": 31415}, "action":"cluster_3_0" }] } }]} ` ) func TestRoutesToJSON(t *testing.T) { tests := []struct { name string routes []*xdsclient.Route wantJSON string wantErr bool }{ { name: "one route", routes: []*xdsclient.Route{{ Path: newStringP("/service_1/method_1"), Action: map[string]uint32{"cluster_1": 75, "cluster_2": 25}, }}, wantJSON: testRoutingJSON, wantErr: false, }, { name: "all matchers", routes: []*xdsclient.Route{ { Path: newStringP("/service_1/method_1"), Action: map[string]uint32{"cluster_1": 1}, }, { Prefix: newStringP("/service_2/method_1"), Action: map[string]uint32{"cluster_1": 1}, }, { Regex: newStringP("^/service_2/method_3$"), Action: map[string]uint32{"cluster_1": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", InvertMatch: newBoolP(true), ExactMatch: newStringP("value-1"), }}, Action: map[string]uint32{"cluster_2": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", RegexMatch: newStringP("^value-1$"), }}, Action: map[string]uint32{"cluster_2": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", RangeMatch: &xdsclient.Int64Range{Start: -1, End: 7}, }}, Action: map[string]uint32{"cluster_3": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", PresentMatch: newBoolP(true), }}, Action: map[string]uint32{"cluster_3": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", PrefixMatch: newStringP("value-1"), }}, Action: map[string]uint32{"cluster_2": 1}, }, { Prefix: newStringP(""), Headers: []*xdsclient.HeaderMatcher{{ Name: "header-1", SuffixMatch: newStringP("value-1"), }}, Action: map[string]uint32{"cluster_2": 1}, }, { Prefix: newStringP(""), Fraction: newUint32P(31415), Action: map[string]uint32{"cluster_3": 1}, }, }, wantJSON: testRoutingAllMatchersJSON, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Note this random number function only generates 0. This is // because the test doesn't handle action update, and there's only // one action for each cluster bundle. // // This is necessary so the output is deterministic. grpcrandInt63n = func(int64) int64 { return 0 } defer func() { grpcrandInt63n = grpcrand.Int63n }() gotJSON, err := (&xdsResolver{}).routesToJSON(tt.routes) if err != nil { t.Errorf("routesToJSON returned error: %v", err) return } gotParsed := internal.ParseServiceConfigForTesting.(func(string) *serviceconfig.ParseResult)(gotJSON) wantParsed := internal.ParseServiceConfigForTesting.(func(string) *serviceconfig.ParseResult)(tt.wantJSON) if !internal.EqualServiceConfigForTesting(gotParsed.Config, wantParsed.Config) { t.Errorf("serviceUpdateToJSON() = %v, want %v", gotJSON, tt.wantJSON) t.Error("gotParsed: ", cmp.Diff(nil, gotParsed)) t.Error("wantParsed: ", cmp.Diff(nil, wantParsed)) } }) } } func TestServiceUpdateToJSON(t *testing.T) { tests := []struct { name string su xdsclient.ServiceUpdate wantJSON string wantErr bool }{ { name: "routing", su: xdsclient.ServiceUpdate{ Routes: []*xdsclient.Route{{ Path: newStringP("/service_1/method_1"), Action: map[string]uint32{"cluster_1": 75, "cluster_2": 25}, }}, }, wantJSON: testRoutingJSON, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer replaceRandNumGenerator(0)() gotJSON, err := (&xdsResolver{}).serviceUpdateToJSON(tt.su) if err != nil { t.Errorf("serviceUpdateToJSON returned error: %v", err) return } gotParsed := internal.ParseServiceConfigForTesting.(func(string) *serviceconfig.ParseResult)(gotJSON) wantParsed := internal.ParseServiceConfigForTesting.(func(string) *serviceconfig.ParseResult)(tt.wantJSON) if !internal.EqualServiceConfigForTesting(gotParsed.Config, wantParsed.Config) { t.Errorf("serviceUpdateToJSON() = %v, want %v", gotJSON, tt.wantJSON) t.Error("gotParsed: ", cmp.Diff(nil, gotParsed)) t.Error("wantParsed: ", cmp.Diff(nil, wantParsed)) } }) } } // Two updates to the same resolver, test that action names are reused. func TestServiceUpdateToJSON_TwoConfig_UpdateActions(t *testing.T) { } func newStringP(s string) *string { return &s } func newBoolP(b bool) *bool { return &b } func newUint32P(i uint32) *uint32 { return &i }