google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/helpers_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_test 20 21 import ( 22 "bytes" 23 "context" 24 "errors" 25 "fmt" 26 "strconv" 27 "strings" 28 "testing" 29 "time" 30 31 "google.golang.org/grpc/internal/grpctest" 32 "google.golang.org/grpc/xds/internal/clients/internal/pretty" 33 "google.golang.org/grpc/xds/internal/clients/internal/testutils" 34 "google.golang.org/grpc/xds/internal/clients/xdsclient" 35 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 36 "google.golang.org/protobuf/proto" 37 "google.golang.org/protobuf/types/known/anypb" 38 39 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 40 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 41 "github.com/google/go-cmp/cmp" 42 ) 43 44 type s struct { 45 grpctest.Tester 46 } 47 48 func Test(t *testing.T) { 49 grpctest.RunSubTests(t, s{}) 50 } 51 52 const ( 53 defaultTestWatchExpiryTimeout = 500 * time.Millisecond 54 defaultTestTimeout = 10 * time.Second 55 defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen. 56 57 // ListenerResourceTypeName represents the transport agnostic name for the 58 // listener resource. 59 listenerResourceTypeName = "ListenerResource" 60 61 ldsName = "xdsclient-test-lds-resource" 62 rdsName = "xdsclient-test-rds-resource" 63 ldsNameNewStyle = "xdstp:///envoy.config.listener.v3.Listener/xdsclient-test-lds-resource" 64 rdsNameNewStyle = "xdstp:///envoy.config.route.v3.RouteConfiguration/xdsclient-test-rds-resource" 65 ) 66 67 var ( 68 // Singleton instantiation of the resource type implementation. 69 listenerType = xdsclient.ResourceType{ 70 TypeURL: xdsresource.V3ListenerURL, 71 TypeName: listenerResourceTypeName, 72 AllResourcesRequiredInSotW: true, 73 Decoder: listenerDecoder{}, 74 } 75 ) 76 77 func unmarshalListenerResource(rProto *anypb.Any) (string, listenerUpdate, error) { 78 rProto, err := xdsresource.UnwrapResource(rProto) 79 if err != nil { 80 return "", listenerUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err) 81 } 82 if !xdsresource.IsListenerResource(rProto.GetTypeUrl()) { 83 return "", listenerUpdate{}, fmt.Errorf("unexpected listener resource type: %q", rProto.GetTypeUrl()) 84 } 85 lis := &v3listenerpb.Listener{} 86 if err := proto.Unmarshal(rProto.GetValue(), lis); err != nil { 87 return "", listenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) 88 } 89 90 lu, err := processListener(lis) 91 if err != nil { 92 return lis.GetName(), listenerUpdate{}, err 93 } 94 lu.Raw = rProto.GetValue() 95 return lis.GetName(), *lu, nil 96 } 97 98 func processListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 99 if lis.GetApiListener() != nil { 100 return processClientSideListener(lis) 101 } 102 return processServerSideListener(lis) 103 } 104 105 // processClientSideListener checks if the provided Listener proto meets 106 // the expected criteria. If so, it returns a non-empty routeConfigName. 107 func processClientSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 108 update := &listenerUpdate{} 109 110 apiLisAny := lis.GetApiListener().GetApiListener() 111 if !xdsresource.IsHTTPConnManagerResource(apiLisAny.GetTypeUrl()) { 112 return nil, fmt.Errorf("unexpected http connection manager resource type: %q", apiLisAny.GetTypeUrl()) 113 } 114 apiLis := &v3httppb.HttpConnectionManager{} 115 if err := proto.Unmarshal(apiLisAny.GetValue(), apiLis); err != nil { 116 return nil, fmt.Errorf("failed to unmarshal api_listener: %v", err) 117 } 118 119 switch apiLis.RouteSpecifier.(type) { 120 case *v3httppb.HttpConnectionManager_Rds: 121 if configsource := apiLis.GetRds().GetConfigSource(); configsource.GetAds() == nil && configsource.GetSelf() == nil { 122 return nil, fmt.Errorf("LDS's RDS configSource is not ADS or Self: %+v", lis) 123 } 124 name := apiLis.GetRds().GetRouteConfigName() 125 if name == "" { 126 return nil, fmt.Errorf("empty route_config_name: %+v", lis) 127 } 128 update.RouteConfigName = name 129 case *v3httppb.HttpConnectionManager_RouteConfig: 130 routeU := apiLis.GetRouteConfig() 131 if routeU == nil { 132 return nil, fmt.Errorf("empty inline RDS resp:: %+v", lis) 133 } 134 if routeU.Name == "" { 135 return nil, fmt.Errorf("empty route_config_name in inline RDS resp: %+v", lis) 136 } 137 update.RouteConfigName = routeU.Name 138 case nil: 139 return nil, fmt.Errorf("no RouteSpecifier: %+v", apiLis) 140 default: 141 return nil, fmt.Errorf("unsupported type %T for RouteSpecifier", apiLis.RouteSpecifier) 142 } 143 144 return update, nil 145 } 146 147 func processServerSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 148 if n := len(lis.ListenerFilters); n != 0 { 149 return nil, fmt.Errorf("unsupported field 'listener_filters' contains %d entries", n) 150 } 151 if lis.GetUseOriginalDst().GetValue() { 152 return nil, errors.New("unsupported field 'use_original_dst' is present and set to true") 153 } 154 addr := lis.GetAddress() 155 if addr == nil { 156 return nil, fmt.Errorf("no address field in LDS response: %+v", lis) 157 } 158 sockAddr := addr.GetSocketAddress() 159 if sockAddr == nil { 160 return nil, fmt.Errorf("no socket_address field in LDS response: %+v", lis) 161 } 162 lu := &listenerUpdate{ 163 InboundListenerCfg: &inboundListenerConfig{ 164 Address: sockAddr.GetAddress(), 165 Port: strconv.Itoa(int(sockAddr.GetPortValue())), 166 }, 167 } 168 169 return lu, nil 170 } 171 172 type listenerDecoder struct{} 173 174 // Decode deserializes and validates an xDS resource serialized inside the 175 // provided `Any` proto, as received from the xDS management server. 176 func (listenerDecoder) Decode(resource xdsclient.AnyProto, _ xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) { 177 rProto := &anypb.Any{ 178 TypeUrl: resource.TypeURL, 179 Value: resource.Value, 180 } 181 name, listener, err := unmarshalListenerResource(rProto) 182 switch { 183 case name == "": 184 // Name is unset only when protobuf deserialization fails. 185 return nil, err 186 case err != nil: 187 // Protobuf deserialization succeeded, but resource validation failed. 188 return &xdsclient.DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listenerUpdate{}}}, err 189 } 190 191 return &xdsclient.DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listener}}, nil 192 193 } 194 195 // listenerResourceData wraps the configuration of a Listener resource as 196 // received from the management server. 197 // 198 // Implements the ResourceData interface. 199 type listenerResourceData struct { 200 xdsclient.ResourceData 201 202 Resource listenerUpdate 203 } 204 205 // Equal returns true if other is equal to l. 206 func (l *listenerResourceData) Equal(other xdsclient.ResourceData) bool { 207 if l == nil && other == nil { 208 return true 209 } 210 if (l == nil) != (other == nil) { 211 return false 212 } 213 return bytes.Equal(l.Resource.Raw, other.Bytes()) 214 } 215 216 // ToJSON returns a JSON string representation of the resource data. 217 func (l *listenerResourceData) ToJSON() string { 218 return pretty.ToJSON(l.Resource) 219 } 220 221 // Bytes returns the underlying raw protobuf form of the listener resource. 222 func (l *listenerResourceData) Bytes() []byte { 223 return l.Resource.Raw 224 } 225 226 // ListenerUpdate contains information received in an LDS response, which is of 227 // interest to the registered LDS watcher. 228 type listenerUpdate struct { 229 // RouteConfigName is the route configuration name corresponding to the 230 // target which is being watched through LDS. 231 RouteConfigName string 232 233 // InboundListenerCfg contains inbound listener configuration. 234 InboundListenerCfg *inboundListenerConfig 235 236 // Raw is the resource from the xds response. 237 Raw []byte 238 } 239 240 // InboundListenerConfig contains information about the inbound listener, i.e 241 // the server-side listener. 242 type inboundListenerConfig struct { 243 // Address is the local address on which the inbound listener is expected to 244 // accept incoming connections. 245 Address string 246 // Port is the local port on which the inbound listener is expected to 247 // accept incoming connections. 248 Port string 249 } 250 251 func makeAuthorityName(name string) string { 252 segs := strings.Split(name, "/") 253 return strings.Join(segs, "") 254 } 255 256 func makeNewStyleLDSName(authority string) string { 257 return fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/xdsclient-test-lds-resource", authority) 258 } 259 260 // buildResourceName returns the resource name in the format of an xdstp:// 261 // resource. 262 func buildResourceName(typeName, auth, id string, ctxParams map[string]string) string { 263 var typS string 264 switch typeName { 265 case listenerResourceTypeName: 266 typS = "envoy.config.listener.v3.Listener" 267 default: 268 // If the name doesn't match any of the standard resources fallback 269 // to the type name. 270 typS = typeName 271 } 272 return (&xdsresource.Name{ 273 Scheme: "xdstp", 274 Authority: auth, 275 Type: typS, 276 ID: id, 277 ContextParams: ctxParams, 278 }).String() 279 } 280 281 // testMetricsReporter is a MetricsReporter to be used in tests. It sends 282 // recording events on channels and provides helpers to check if certain events 283 // have taken place. 284 type testMetricsReporter struct { 285 metricsCh *testutils.Channel 286 } 287 288 // newTestMetricsReporter returns a new testMetricsReporter. 289 func newTestMetricsReporter() *testMetricsReporter { 290 return &testMetricsReporter{ 291 metricsCh: testutils.NewChannelWithSize(1), 292 } 293 } 294 295 // waitForMetric waits for a metric to be recorded and verifies that the 296 // recorded metrics data matches the expected metricsDataWant. Returns 297 // an error if failed to wait or received wrong data. 298 func (r *testMetricsReporter) waitForMetric(ctx context.Context, metricsDataWant any) error { 299 got, err := r.metricsCh.Receive(ctx) 300 if err != nil { 301 return fmt.Errorf("timeout waiting for int64Count") 302 } 303 if diff := cmp.Diff(got, metricsDataWant); diff != "" { 304 return fmt.Errorf("received unexpected metrics value (-got, +want): %v", diff) 305 } 306 return nil 307 } 308 309 // ReportMetric sends the metrics data to the metricsCh channel. 310 func (r *testMetricsReporter) ReportMetric(m any) { 311 r.metricsCh.Replace(m) 312 }