/* * * Copyright 2021 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 e2e import ( "context" "fmt" "regexp" "strings" "testing" "github.com/cespare/xxhash/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" channelzpb "google.golang.org/grpc/channelz/grpc_channelz_v1" "google.golang.org/grpc/internal/xds/clients" "google.golang.org/grpc/internal/xds/xdsclient/xdsresource" "google.golang.org/grpc/resolver" ) func verifySubConnStates(t *testing.T, scs []*channelzpb.Subchannel, want map[channelzpb.ChannelConnectivityState_State]int) { t.Helper() var scStatsCount = map[channelzpb.ChannelConnectivityState_State]int{} for _, sc := range scs { scStatsCount[sc.Data.State.State]++ } if diff := cmp.Diff(scStatsCount, want); diff != "" { t.Fatalf("got unexpected number of subchannels in state Ready, %v, scs: %v", diff, scs) } } // This function determines the stable, canonical order for any two // resolver.Endpoint structs. func lessEndpoint(a, b resolver.Endpoint) bool { return getHash(a) < getHash(b) } func getHash(e resolver.Endpoint) uint64 { h := xxhash.New() // We iterate through all addresses to ensure the hash represents // the full endpoint identity. for _, addr := range e.Addresses { h.Write([]byte(addr.Addr)) h.Write([]byte(addr.ServerName)) } return h.Sum64() } // VerifyXDSConfig waits for an update on the xdsCh channel or an error on the // errCh channel, and verifies that the received XDSConfig matches the expected // XDS configuration. func VerifyXDSConfig(ctx context.Context, xdsCh chan *xdsresource.XDSConfig, errCh chan error, want *xdsresource.XDSConfig) error { select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for update from dependency manager") case update := <-xdsCh: if err := CompareXDSConfig(update, want); err != nil { return err } case err := <-errCh: return fmt.Errorf("received unexpected error from dependency manager: %v", err) } return nil } // CompareXDSConfig compares two XDSConfig structs and returns an error if they // do not match. func CompareXDSConfig(gotXDSConfig, wantXDSConfig *xdsresource.XDSConfig) error { cmpOpts := []cmp.Option{ cmpopts.EquateEmpty(), cmpopts.IgnoreFields(xdsresource.HTTPFilter{}, "Filter", "Config"), cmpopts.IgnoreFields(xdsresource.ListenerUpdate{}, "Raw"), cmpopts.IgnoreFields(xdsresource.RouteConfigUpdate{}, "Raw"), cmpopts.IgnoreFields(xdsresource.ClusterUpdate{}, "Raw", "LBPolicy", "TelemetryLabels"), cmpopts.IgnoreFields(xdsresource.EndpointsUpdate{}, "Raw"), // Used for EndpointConfig.ResolutionNote and ClusterResult.Err fields. cmp.Transformer("ErrorsToString", func(in error) string { if in == nil { return "" // Treat nil as an empty string } s := in.Error() // Replace all sequences of whitespace (including newlines and // tabs) with a single standard space. s = regexp.MustCompile(`\s+`).ReplaceAllString(s, " ") // Trim any leading/trailing space that might be left over and // return error as string. return strings.TrimSpace(s) }), cmpopts.SortSlices(lessEndpoint), } if diff := cmp.Diff(gotXDSConfig, wantXDSConfig, cmpOpts...); diff != "" { return fmt.Errorf("received unexpected update from dependency manager. Diff (-got +want):\n%v", diff) } return nil } // MakeXDSConfig creates a mock XDSConfig with a standard structure containing // a Listener, RouteConfig, VirtualHost, and a single EDS Cluster, populated // with the provided parameters. func MakeXDSConfig(servicename, routeConfigName, clusterName, edsServiceName, addr string) *xdsresource.XDSConfig { prefix := "/" return &xdsresource.XDSConfig{ Listener: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: routeConfigName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, RouteConfig: &xdsresource.RouteConfigUpdate{ VirtualHosts: []*xdsresource.VirtualHost{ { Domains: []string{servicename}, Routes: []*xdsresource.Route{{ Prefix: &prefix, WeightedClusters: []xdsresource.WeightedCluster{{Name: clusterName, Weight: 100}}, ActionType: xdsresource.RouteActionRoute, }}, }, }, }, VirtualHost: &xdsresource.VirtualHost{ Domains: []string{servicename}, Routes: []*xdsresource.Route{{ Prefix: &prefix, WeightedClusters: []xdsresource.WeightedCluster{{Name: clusterName, Weight: 100}}, ActionType: xdsresource.RouteActionRoute}, }, }, Clusters: map[string]*xdsresource.ClusterResult{ clusterName: { Config: xdsresource.ClusterConfig{Cluster: &xdsresource.ClusterUpdate{ ClusterType: xdsresource.ClusterTypeEDS, ClusterName: clusterName, EDSServiceName: edsServiceName, }, EndpointConfig: &xdsresource.EndpointConfig{ EDSUpdate: &xdsresource.EndpointsUpdate{ Localities: []xdsresource.Locality{ {ID: clients.Locality{ Region: "region-1", Zone: "zone-1", SubZone: "subzone-1", }, Endpoints: []xdsresource.Endpoint{ { ResolverEndpoint: resolver.Endpoint{Addresses: []resolver.Address{{Addr: addr}}}, HealthStatus: xdsresource.EndpointHealthStatusUnknown, Weight: 1, }, }, Weight: 1, }, }, }, }, }, }, }, } }