google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/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 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "strconv" 26 "testing" 27 "time" 28 29 "google.golang.org/grpc/internal/grpctest" 30 "google.golang.org/grpc/xds/internal/clients/internal/pretty" 31 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 32 "google.golang.org/protobuf/proto" 33 "google.golang.org/protobuf/types/known/anypb" 34 35 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 36 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 37 ) 38 39 type s struct { 40 grpctest.Tester 41 } 42 43 func Test(t *testing.T) { 44 grpctest.RunSubTests(t, s{}) 45 } 46 47 const ( 48 defaultTestWatchExpiryTimeout = 100 * time.Millisecond 49 defaultTestTimeout = 5 * time.Second 50 defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen. 51 // listenerResourceTypeName represents the transport agnostic name for the 52 // listener resource. 53 listenerResourceTypeName = "ListenerResource" 54 ) 55 56 var ( 57 // Singleton instantiation of the resource type implementation. 58 listenerType = ResourceType{ 59 TypeURL: xdsresource.V3ListenerURL, 60 TypeName: listenerResourceTypeName, 61 AllResourcesRequiredInSotW: true, 62 Decoder: listenerDecoder{}, 63 } 64 ) 65 66 func unmarshalListenerResource(rProto *anypb.Any) (string, listenerUpdate, error) { 67 rProto, err := xdsresource.UnwrapResource(rProto) 68 if err != nil { 69 return "", listenerUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err) 70 } 71 if !xdsresource.IsListenerResource(rProto.GetTypeUrl()) { 72 return "", listenerUpdate{}, fmt.Errorf("unexpected listener resource type: %q", rProto.GetTypeUrl()) 73 } 74 lis := &v3listenerpb.Listener{} 75 if err := proto.Unmarshal(rProto.GetValue(), lis); err != nil { 76 return "", listenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) 77 } 78 79 lu, err := processListener(lis) 80 if err != nil { 81 return lis.GetName(), listenerUpdate{}, err 82 } 83 lu.Raw = rProto.GetValue() 84 return lis.GetName(), *lu, nil 85 } 86 87 func processListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 88 if lis.GetApiListener() != nil { 89 return processClientSideListener(lis) 90 } 91 return processServerSideListener(lis) 92 } 93 94 // processClientSideListener checks if the provided Listener proto meets 95 // the expected criteria. If so, it returns a non-empty routeConfigName. 96 func processClientSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 97 update := &listenerUpdate{} 98 99 apiLisAny := lis.GetApiListener().GetApiListener() 100 if !xdsresource.IsHTTPConnManagerResource(apiLisAny.GetTypeUrl()) { 101 return nil, fmt.Errorf("unexpected http connection manager resource type: %q", apiLisAny.GetTypeUrl()) 102 } 103 apiLis := &v3httppb.HttpConnectionManager{} 104 if err := proto.Unmarshal(apiLisAny.GetValue(), apiLis); err != nil { 105 return nil, fmt.Errorf("failed to unmarshal api_listener: %v", err) 106 } 107 108 switch apiLis.RouteSpecifier.(type) { 109 case *v3httppb.HttpConnectionManager_Rds: 110 if configsource := apiLis.GetRds().GetConfigSource(); configsource.GetAds() == nil && configsource.GetSelf() == nil { 111 return nil, fmt.Errorf("LDS's RDS configSource is not ADS or Self: %+v", lis) 112 } 113 name := apiLis.GetRds().GetRouteConfigName() 114 if name == "" { 115 return nil, fmt.Errorf("empty route_config_name: %+v", lis) 116 } 117 update.RouteConfigName = name 118 case *v3httppb.HttpConnectionManager_RouteConfig: 119 routeU := apiLis.GetRouteConfig() 120 if routeU == nil { 121 return nil, fmt.Errorf("empty inline RDS resp:: %+v", lis) 122 } 123 if routeU.Name == "" { 124 return nil, fmt.Errorf("empty route_config_name in inline RDS resp: %+v", lis) 125 } 126 update.RouteConfigName = routeU.Name 127 case nil: 128 return nil, fmt.Errorf("no RouteSpecifier: %+v", apiLis) 129 default: 130 return nil, fmt.Errorf("unsupported type %T for RouteSpecifier", apiLis.RouteSpecifier) 131 } 132 133 return update, nil 134 } 135 136 func processServerSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 137 if n := len(lis.ListenerFilters); n != 0 { 138 return nil, fmt.Errorf("unsupported field 'listener_filters' contains %d entries", n) 139 } 140 if lis.GetUseOriginalDst().GetValue() { 141 return nil, errors.New("unsupported field 'use_original_dst' is present and set to true") 142 } 143 addr := lis.GetAddress() 144 if addr == nil { 145 return nil, fmt.Errorf("no address field in LDS response: %+v", lis) 146 } 147 sockAddr := addr.GetSocketAddress() 148 if sockAddr == nil { 149 return nil, fmt.Errorf("no socket_address field in LDS response: %+v", lis) 150 } 151 lu := &listenerUpdate{ 152 InboundListenerCfg: &inboundListenerConfig{ 153 Address: sockAddr.GetAddress(), 154 Port: strconv.Itoa(int(sockAddr.GetPortValue())), 155 }, 156 } 157 158 return lu, nil 159 } 160 161 type listenerDecoder struct{} 162 163 // Decode deserializes and validates an xDS resource serialized inside the 164 // provided `Any` proto, as received from the xDS management server. 165 func (listenerDecoder) Decode(resource AnyProto, _ DecodeOptions) (*DecodeResult, error) { 166 rProto := &anypb.Any{ 167 TypeUrl: resource.TypeURL, 168 Value: resource.Value, 169 } 170 name, listener, err := unmarshalListenerResource(rProto) 171 switch { 172 case name == "": 173 // Name is unset only when protobuf deserialization fails. 174 return nil, err 175 case err != nil: 176 // Protobuf deserialization succeeded, but resource validation failed. 177 return &DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listenerUpdate{}}}, err 178 } 179 180 return &DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listener}}, nil 181 182 } 183 184 // listenerResourceData wraps the configuration of a Listener resource as 185 // received from the management server. 186 // 187 // Implements the ResourceData interface. 188 type listenerResourceData struct { 189 ResourceData 190 191 Resource listenerUpdate 192 } 193 194 // Equal returns true if other is equal to l. 195 func (l *listenerResourceData) Equal(other ResourceData) bool { 196 if l == nil && other == nil { 197 return true 198 } 199 if (l == nil) != (other == nil) { 200 return false 201 } 202 return bytes.Equal(l.Resource.Raw, other.Bytes()) 203 } 204 205 // ToJSON returns a JSON string representation of the resource data. 206 func (l *listenerResourceData) ToJSON() string { 207 return pretty.ToJSON(l.Resource) 208 } 209 210 // Bytes returns the underlying raw protobuf form of the listener resource. 211 func (l *listenerResourceData) Bytes() []byte { 212 return l.Resource.Raw 213 } 214 215 // ListenerUpdate contains information received in an LDS response, which is of 216 // interest to the registered LDS watcher. 217 type listenerUpdate struct { 218 // RouteConfigName is the route configuration name corresponding to the 219 // target which is being watched through LDS. 220 RouteConfigName string 221 222 // InboundListenerCfg contains inbound listener configuration. 223 InboundListenerCfg *inboundListenerConfig 224 225 // Raw is the resource from the xds response. 226 Raw []byte 227 } 228 229 // InboundListenerConfig contains information about the inbound listener, i.e 230 // the server-side listener. 231 type inboundListenerConfig struct { 232 // Address is the local address on which the inbound listener is expected to 233 // accept incoming connections. 234 Address string 235 // Port is the local port on which the inbound listener is expected to 236 // accept incoming connections. 237 Port string 238 }