google.golang.org/grpc@v1.74.2/xds/internal/xdsclient/clientimpl_test.go (about) 1 /* 2 * 3 * Copyright 2025 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package xdsclient 20 21 import ( 22 "encoding/json" 23 "fmt" 24 "reflect" 25 "sync" 26 "testing" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 "google.golang.org/grpc" 31 "google.golang.org/grpc/credentials/insecure" 32 "google.golang.org/grpc/internal/testutils/stats" 33 "google.golang.org/grpc/internal/xds/bootstrap" 34 "google.golang.org/grpc/xds/internal/clients" 35 "google.golang.org/grpc/xds/internal/clients/grpctransport" 36 "google.golang.org/grpc/xds/internal/clients/xdsclient" 37 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 38 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 39 "google.golang.org/protobuf/testing/protocmp" 40 ) 41 42 const ( 43 testXDSServerURL = "xds.example.com:8080" 44 testXDSServerURL2 = "xds.example.com:8081" 45 testNodeID = "test-node-id" 46 testClusterName = "test-cluster" 47 testUserAgentName = "test-ua-name" 48 testUserAgentVer = "test-ua-ver" 49 testLocalityRegion = "test-region" 50 testLocalityZone = "test-zone" 51 testLocalitySubZone = "test-sub-zone" 52 testTargetName = "test-target" 53 ) 54 55 var ( 56 testMetadataJSON, _ = json.Marshal(map[string]any{"foo": "bar", "baz": float64(1)}) 57 ) 58 59 func (s) TestBuildXDSClientConfig_Success(t *testing.T) { 60 tests := []struct { 61 name string 62 bootstrapContents []byte 63 wantXDSClientConfig func(bootstrapCfg *bootstrap.Config) xdsclient.Config 64 }{ 65 { 66 name: "without authorities", 67 bootstrapContents: []byte(fmt.Sprintf(`{ 68 "xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}], 69 "node": { 70 "id": "%s", "cluster": "%s", "metadata": %s, 71 "locality": {"region": "%s", "zone": "%s", "sub_zone": "%s"}, 72 "user_agent_name": "%s", "user_agent_version": "%s" 73 } 74 }`, testXDSServerURL, testNodeID, testClusterName, testMetadataJSON, testLocalityRegion, testLocalityZone, testLocalitySubZone, testUserAgentName, testUserAgentVer)), 75 wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config { 76 node, serverCfg := c.Node(), c.XDSServers()[0] 77 expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}} 78 gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg} 79 return xdsclient.Config{ 80 Servers: []xdsclient.ServerConfig{expectedServer}, 81 Node: clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, Locality: clients.Locality{Region: node.Locality.Region, Zone: node.Locality.Zone, SubZone: node.Locality.SubZone}, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()}, 82 Authorities: map[string]xdsclient.Authority{}, 83 ResourceTypes: map[string]xdsclient.ResourceType{ 84 version.V3ListenerURL: {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)}, 85 version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()}, 86 version.V3ClusterURL: {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)}, 87 version.V3EndpointsURL: {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()}, 88 }, 89 MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName}, 90 TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{ 91 "insecure": { 92 Credentials: insecure.NewBundle(), 93 GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { 94 opts = append(opts, serverCfg.DialOptions()...) 95 return grpc.NewClient(target, opts...) 96 }}, 97 }), 98 } 99 }, 100 }, 101 { 102 name: "with authorities", 103 bootstrapContents: []byte(fmt.Sprintf(`{ 104 "xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}], 105 "node": {"id": "%s"}, 106 "authorities": { 107 "auth1": {}, 108 "auth2": {"xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}]}]} 109 } 110 }`, testXDSServerURL, testNodeID, testXDSServerURL2)), 111 wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config { 112 node := c.Node() 113 topLevelSCfg, auth2SCfg := c.XDSServers()[0], c.Authorities()["auth2"].XDSServers[0] 114 expTopLevelS := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: topLevelSCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}} 115 expAuth2S := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: auth2SCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}} 116 gSCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expTopLevelS: topLevelSCfg, expAuth2S: auth2SCfg} 117 return xdsclient.Config{ 118 Servers: []xdsclient.ServerConfig{expTopLevelS}, 119 Node: clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()}, 120 Authorities: map[string]xdsclient.Authority{"auth1": {XDSServers: []xdsclient.ServerConfig{expTopLevelS}}, "auth2": {XDSServers: []xdsclient.ServerConfig{expAuth2S}}}, 121 ResourceTypes: map[string]xdsclient.ResourceType{ 122 version.V3ListenerURL: {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)}, 123 version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()}, 124 version.V3ClusterURL: {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gSCfgMap)}, 125 version.V3EndpointsURL: {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()}, 126 }, 127 MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName}, 128 TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{ 129 "insecure": { 130 Credentials: insecure.NewBundle(), 131 GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { 132 opts = append(opts, topLevelSCfg.DialOptions()...) 133 return grpc.NewClient(target, opts...) 134 }}, 135 }), 136 } 137 }, 138 }, 139 { 140 name: "server features with ignore_resource_deletion", 141 bootstrapContents: []byte(fmt.Sprintf(`{ 142 "xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "insecure"}], "server_features": ["ignore_resource_deletion"]}], 143 "node": {"id": "%s"} 144 }`, testXDSServerURL, testNodeID)), 145 wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config { 146 node, serverCfg := c.Node(), c.XDSServers()[0] 147 expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}, IgnoreResourceDeletion: true} 148 gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg} 149 return xdsclient.Config{ 150 Servers: []xdsclient.ServerConfig{expectedServer}, 151 Node: clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()}, 152 Authorities: map[string]xdsclient.Authority{}, 153 ResourceTypes: map[string]xdsclient.ResourceType{ 154 version.V3ListenerURL: {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)}, 155 version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()}, 156 version.V3ClusterURL: {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)}, 157 version.V3EndpointsURL: {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()}, 158 }, 159 MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName}, 160 TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{ 161 "insecure": { 162 Credentials: insecure.NewBundle(), 163 GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { 164 opts = append(opts, serverCfg.DialOptions()...) 165 return grpc.NewClient(target, opts...) 166 }}, 167 }), 168 } 169 }, 170 }, 171 { 172 name: "channel creds - unknown type skipped", 173 bootstrapContents: []byte(fmt.Sprintf(`{ 174 "xds_servers": [{"server_uri": "%s", "channel_creds": [{"type": "unknown-type"}, {"type": "insecure"}]}], 175 "node": {"id": "%s"} 176 }`, testXDSServerURL, testNodeID)), // "insecure" is selected 177 wantXDSClientConfig: func(c *bootstrap.Config) xdsclient.Config { 178 node, serverCfg := c.Node(), c.XDSServers()[0] // SelectedCreds will be "insecure" 179 expectedServer := xdsclient.ServerConfig{ServerIdentifier: clients.ServerIdentifier{ServerURI: serverCfg.ServerURI(), Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}}} 180 gServerCfgMap := map[xdsclient.ServerConfig]*bootstrap.ServerConfig{expectedServer: serverCfg} 181 return xdsclient.Config{ 182 Servers: []xdsclient.ServerConfig{expectedServer}, 183 Node: clients.Node{ID: node.GetId(), Cluster: node.GetCluster(), Metadata: node.Metadata, UserAgentName: node.UserAgentName, UserAgentVersion: node.GetUserAgentVersion()}, 184 Authorities: map[string]xdsclient.Authority{}, 185 ResourceTypes: map[string]xdsclient.ResourceType{ 186 version.V3ListenerURL: {TypeURL: version.V3ListenerURL, TypeName: xdsresource.ListenerResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericListenerResourceTypeDecoder(c)}, 187 version.V3RouteConfigURL: {TypeURL: version.V3RouteConfigURL, TypeName: xdsresource.RouteConfigTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericRouteConfigResourceTypeDecoder()}, 188 version.V3ClusterURL: {TypeURL: version.V3ClusterURL, TypeName: xdsresource.ClusterResourceTypeName, AllResourcesRequiredInSotW: true, Decoder: xdsresource.NewGenericClusterResourceTypeDecoder(c, gServerCfgMap)}, 189 version.V3EndpointsURL: {TypeURL: version.V3EndpointsURL, TypeName: xdsresource.EndpointsResourceTypeName, AllResourcesRequiredInSotW: false, Decoder: xdsresource.NewGenericEndpointsResourceTypeDecoder()}, 190 }, 191 MetricsReporter: &metricsReporter{recorder: stats.NewTestMetricsRecorder(), target: testTargetName}, 192 TransportBuilder: grpctransport.NewBuilder(map[string]grpctransport.Config{ 193 "insecure": { 194 Credentials: insecure.NewBundle(), 195 GRPCNewClient: func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { 196 opts = append(opts, serverCfg.DialOptions()...) 197 return grpc.NewClient(target, opts...) 198 }}, 199 }), 200 } 201 }, 202 }, 203 } 204 205 for _, tt := range tests { 206 t.Run(tt.name, func(t *testing.T) { 207 bootstrapConfig, err := bootstrap.NewConfigFromContents(tt.bootstrapContents) 208 if err != nil { 209 t.Fatalf("Failed to create bootstrap config: %v", err) 210 } 211 gotCfg, err := buildXDSClientConfig(bootstrapConfig, stats.NewTestMetricsRecorder(), testTargetName) 212 if err != nil { 213 t.Fatalf("Failed to build XDSClientConfig: %v", err) 214 } 215 216 wantCfg := tt.wantXDSClientConfig(bootstrapConfig) 217 218 unexportedTypeOpts := cmpopts.IgnoreUnexported(clients.Node{}, grpctransport.Builder{}) 219 ignoreTypeOpts := cmpopts.IgnoreTypes(sync.Mutex{}) 220 resourceTypeCmpOpts := cmp.Comparer(func(a, b xdsclient.ResourceType) bool { 221 return a.TypeURL == b.TypeURL && a.TypeName == b.TypeName && a.AllResourcesRequiredInSotW == b.AllResourcesRequiredInSotW && reflect.TypeOf(a.Decoder) == reflect.TypeOf(b.Decoder) 222 }) 223 metricsReporterCmpOpts := cmp.Comparer(func(a, b clients.MetricsReporter) bool { 224 if (a == nil) != (b == nil) { 225 return false 226 } 227 if a == nil { // Both are nil 228 return true 229 } 230 // Both are non-nil, compare type and target. 231 aConcrete, aOK := a.(*metricsReporter) 232 bConcrete, bOK := b.(*metricsReporter) 233 if !(aOK && bOK && aConcrete.target == bConcrete.target) { 234 return false 235 } 236 // Compare recorder by type. 237 if (aConcrete.recorder == nil) != (bConcrete.recorder == nil) { 238 return false 239 } 240 // If both are nil, recorder check passes. If both non-nil, check types. 241 return aConcrete.recorder == nil || reflect.TypeOf(aConcrete.recorder) == reflect.TypeOf(bConcrete.recorder) 242 }) 243 transportBuilderCmpOpts := cmp.Comparer(func(a, b grpctransport.Config) bool { 244 // Compare Credentials by type 245 credsEqual := true 246 if (a.Credentials == nil) != (b.Credentials == nil) { 247 credsEqual = false 248 } else if a.Credentials != nil && reflect.TypeOf(a.Credentials) != reflect.TypeOf(b.Credentials) { 249 credsEqual = false 250 } 251 // Compare GRPCNewClient by nil-ness 252 newClientEqual := (a.GRPCNewClient == nil) == (b.GRPCNewClient == nil) 253 return credsEqual && newClientEqual 254 }) 255 256 if diff := cmp.Diff(wantCfg, gotCfg, protocmp.Transform(), unexportedTypeOpts, ignoreTypeOpts, resourceTypeCmpOpts, metricsReporterCmpOpts, transportBuilderCmpOpts); diff != "" { 257 t.Errorf("buildXDSClientConfig() mismatch (-want +got):\n%s", diff) 258 } 259 }) 260 } 261 }