google.golang.org/grpc@v1.74.2/xds/internal/resolver/helpers_test.go (about) 1 /* 2 * 3 * Copyright 2023 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 resolver_test 20 21 import ( 22 "context" 23 "fmt" 24 "net/url" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 "google.golang.org/grpc/codes" 32 "google.golang.org/grpc/internal" 33 "google.golang.org/grpc/internal/grpctest" 34 iresolver "google.golang.org/grpc/internal/resolver" 35 "google.golang.org/grpc/internal/testutils" 36 "google.golang.org/grpc/internal/testutils/xds/e2e" 37 "google.golang.org/grpc/resolver" 38 "google.golang.org/grpc/serviceconfig" 39 "google.golang.org/grpc/status" 40 xdsresolver "google.golang.org/grpc/xds/internal/resolver" 41 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 42 43 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 44 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 45 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 46 ) 47 48 type s struct { 49 grpctest.Tester 50 } 51 52 func Test(t *testing.T) { 53 grpctest.RunSubTests(t, s{}) 54 } 55 56 const ( 57 defaultTestTimeout = 10 * time.Second 58 defaultTestShortTimeout = 100 * time.Microsecond 59 60 defaultTestServiceName = "service-name" 61 defaultTestRouteConfigName = "route-config-name" 62 defaultTestClusterName = "cluster-name" 63 ) 64 65 // This is the expected service config when using default listener and route 66 // configuration resources from the e2e package using the above resource names. 67 var wantDefaultServiceConfig = fmt.Sprintf(`{ 68 "loadBalancingConfig": [{ 69 "xds_cluster_manager_experimental": { 70 "children": { 71 "cluster:%s": { 72 "childPolicy": [{ 73 "cds_experimental": { 74 "cluster": "%s" 75 } 76 }] 77 } 78 } 79 } 80 }] 81 }`, defaultTestClusterName, defaultTestClusterName) 82 83 // buildResolverForTarget builds an xDS resolver for the given target. If 84 // the bootstrap contents are provided, it build the xDS resolver using them 85 // otherwise, it uses the default xDS resolver. 86 // 87 // It returns the following: 88 // - a channel to read updates from the resolver 89 // - a channel to read errors from the resolver 90 // - the newly created xDS resolver 91 func buildResolverForTarget(t *testing.T, target resolver.Target, bootstrapContents []byte) (chan resolver.State, chan error, resolver.Resolver) { 92 t.Helper() 93 94 var builder resolver.Builder 95 if bootstrapContents != nil { 96 // Create an xDS resolver with the provided bootstrap configuration. 97 if internal.NewXDSResolverWithConfigForTesting == nil { 98 t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil") 99 } 100 var err error 101 builder, err = internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents) 102 if err != nil { 103 t.Fatalf("Failed to create xDS resolver for testing: %v", err) 104 } 105 } else { 106 builder = resolver.Get(xdsresolver.Scheme) 107 if builder == nil { 108 t.Fatalf("Scheme %q is not registered", xdsresolver.Scheme) 109 } 110 } 111 112 stateCh := make(chan resolver.State, 1) 113 updateStateF := func(s resolver.State) error { 114 stateCh <- s 115 return nil 116 } 117 errCh := make(chan error, 1) 118 reportErrorF := func(err error) { 119 select { 120 case errCh <- err: 121 default: 122 } 123 } 124 tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF} 125 r, err := builder.Build(target, tcc, resolver.BuildOptions{ 126 Authority: url.PathEscape(target.Endpoint()), 127 }) 128 if err != nil { 129 t.Fatalf("Failed to build xDS resolver for target %q: %v", target, err) 130 } 131 t.Cleanup(r.Close) 132 return stateCh, errCh, r 133 } 134 135 // verifyUpdateFromResolver waits for the resolver to push an update to the fake 136 // resolver.ClientConn and verifies that update matches the provided service 137 // config. 138 // 139 // Tests that want to skip verifying the contents of the service config can pass 140 // an empty string. 141 // 142 // Returns the config selector from the state update pushed by the resolver. 143 // Tests that don't need the config selector can ignore the return value. 144 func verifyUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State, wantSC string) iresolver.ConfigSelector { 145 t.Helper() 146 147 var state resolver.State 148 select { 149 case <-ctx.Done(): 150 t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err()) 151 case state = <-stateCh: 152 if err := state.ServiceConfig.Err; err != nil { 153 t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err) 154 } 155 if wantSC == "" { 156 break 157 } 158 wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantSC) 159 if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) { 160 t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config)) 161 } 162 } 163 cs := iresolver.GetConfigSelector(state) 164 if cs == nil { 165 t.Fatal("Received nil config selector in update from resolver") 166 } 167 return cs 168 } 169 170 // verifyNoUpdateFromResolver verifies that no update is pushed on stateCh. 171 // Calls t.Fatal() if an update is received before defaultTestShortTimeout 172 // expires. 173 func verifyNoUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State) { 174 t.Helper() 175 176 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 177 defer sCancel() 178 select { 179 case <-sCtx.Done(): 180 case u := <-stateCh: 181 t.Fatalf("Received update from resolver %v when none expected", u) 182 } 183 } 184 185 // waitForErrorFromResolver waits for the resolver to push an error and verifies 186 // that it matches the expected error and contains the expected node ID. 187 func waitForErrorFromResolver(ctx context.Context, errCh chan error, wantErr, wantNodeID string) error { 188 select { 189 case <-ctx.Done(): 190 return fmt.Errorf("timeout when waiting for error to be propagated to the ClientConn") 191 case gotErr := <-errCh: 192 if gotErr == nil { 193 return fmt.Errorf("got nil error from resolver, want %q", wantErr) 194 } 195 if !strings.Contains(gotErr.Error(), wantErr) { 196 return fmt.Errorf("got error from resolver %q, want %q", gotErr, wantErr) 197 } 198 if !strings.Contains(gotErr.Error(), wantNodeID) { 199 return fmt.Errorf("got error from resolver %q, want nodeID %q", gotErr, wantNodeID) 200 } 201 } 202 return nil 203 } 204 205 func verifyResolverError(gotErr error, wantCode codes.Code, wantErr, wantNodeID string) error { 206 if gotErr == nil { 207 return fmt.Errorf("got nil error from resolver, want error with code %v", wantCode) 208 } 209 if !strings.Contains(gotErr.Error(), wantErr) { 210 return fmt.Errorf("got error from resolver %q, want %q", gotErr, wantErr) 211 } 212 if gotCode := status.Code(gotErr); gotCode != wantCode { 213 return fmt.Errorf("got error from resolver with code %v, want %v", gotCode, wantCode) 214 } 215 if !strings.Contains(gotErr.Error(), wantNodeID) { 216 return fmt.Errorf("got error from resolver %q, want nodeID %q", gotErr, wantNodeID) 217 } 218 return nil 219 } 220 221 // Spins up an xDS management server and sets up an xDS bootstrap configuration 222 // file that points to it. 223 // 224 // Returns the following: 225 // - A reference to the xDS management server 226 // - A channel to read requested Listener resource names 227 // - A channel to read requested RouteConfiguration resource names 228 // - Contents of the bootstrap configuration pointing to xDS management 229 // server 230 func setupManagementServerForTest(t *testing.T, nodeID string) (*e2e.ManagementServer, chan []string, chan []string, []byte) { 231 t.Helper() 232 233 listenerResourceNamesCh := make(chan []string, 1) 234 routeConfigResourceNamesCh := make(chan []string, 1) 235 236 // Setup the management server to push the requested listener and route 237 // configuration resource names on to separate channels for the test to 238 // inspect. 239 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 240 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 241 switch req.GetTypeUrl() { 242 case version.V3ListenerURL: 243 select { 244 case <-listenerResourceNamesCh: 245 default: 246 } 247 select { 248 case listenerResourceNamesCh <- req.GetResourceNames(): 249 default: 250 } 251 case version.V3RouteConfigURL: 252 select { 253 case <-routeConfigResourceNamesCh: 254 default: 255 } 256 select { 257 case routeConfigResourceNamesCh <- req.GetResourceNames(): 258 default: 259 } 260 } 261 return nil 262 }, 263 AllowResourceSubset: true, 264 }) 265 266 // Create a bootstrap configuration specifying the above management server. 267 bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 268 return mgmtServer, listenerResourceNamesCh, routeConfigResourceNamesCh, bootstrapContents 269 } 270 271 // Spins up an xDS management server and configures it with a default listener 272 // and route configuration resource. It also sets up an xDS bootstrap 273 // configuration file that points to the above management server. 274 func configureResourcesOnManagementServer(ctx context.Context, t *testing.T, mgmtServer *e2e.ManagementServer, nodeID string, listeners []*v3listenerpb.Listener, routes []*v3routepb.RouteConfiguration) { 275 resources := e2e.UpdateOptions{ 276 NodeID: nodeID, 277 Listeners: listeners, 278 Routes: routes, 279 SkipValidation: true, 280 } 281 if err := mgmtServer.Update(ctx, resources); err != nil { 282 t.Fatal(err) 283 } 284 } 285 286 // waitForResourceNames waits for the wantNames to be pushed on to namesCh. 287 // Fails the test by calling t.Fatal if the context expires before that. 288 func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) { 289 t.Helper() 290 291 for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { 292 select { 293 case <-ctx.Done(): 294 case gotNames := <-namesCh: 295 if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty()) { 296 return 297 } 298 t.Logf("Received resource names %v, want %v", gotNames, wantNames) 299 } 300 } 301 t.Fatalf("Timeout waiting for resource to be requested from the management server") 302 }