github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/grpc/xds/internal/xdsclient/xdsresource/unmarshal_rds.go (about) 1 /* 2 * 3 * Copyright 2021 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 package xdsresource 19 20 import ( 21 "fmt" 22 "regexp" 23 "strings" 24 "time" 25 26 "github.com/golang/protobuf/proto" 27 v3routepb "github.com/hxx258456/ccgo/go-control-plane/envoy/config/route/v3" 28 v3typepb "github.com/hxx258456/ccgo/go-control-plane/envoy/type/v3" 29 "github.com/hxx258456/ccgo/grpc/codes" 30 "github.com/hxx258456/ccgo/grpc/internal/envconfig" 31 "github.com/hxx258456/ccgo/grpc/internal/grpclog" 32 "github.com/hxx258456/ccgo/grpc/internal/pretty" 33 "github.com/hxx258456/ccgo/grpc/xds/internal/clusterspecifier" 34 "github.com/hxx258456/ccgo/grpc/xds/internal/xdsclient/xdsresource/version" 35 "google.golang.org/protobuf/types/known/anypb" 36 ) 37 38 // UnmarshalRouteConfig processes resources received in an RDS response, 39 // validates them, and transforms them into a native struct which contains only 40 // fields we are interested in. The provided hostname determines the route 41 // configuration resources of interest. 42 func UnmarshalRouteConfig(opts *UnmarshalOptions) (map[string]RouteConfigUpdateErrTuple, UpdateMetadata, error) { 43 update := make(map[string]RouteConfigUpdateErrTuple) 44 md, err := processAllResources(opts, update) 45 return update, md, err 46 } 47 48 func unmarshalRouteConfigResource(r *anypb.Any, logger *grpclog.PrefixLogger) (string, RouteConfigUpdate, error) { 49 if !IsRouteConfigResource(r.GetTypeUrl()) { 50 return "", RouteConfigUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl()) 51 } 52 rc := &v3routepb.RouteConfiguration{} 53 if err := proto.Unmarshal(r.GetValue(), rc); err != nil { 54 return "", RouteConfigUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) 55 } 56 logger.Infof("Resource with name: %v, type: %T, contains: %v.", rc.GetName(), rc, pretty.ToJSON(rc)) 57 58 // TODO: Pass version.TransportAPI instead of relying upon the type URL 59 v2 := r.GetTypeUrl() == version.V2RouteConfigURL 60 u, err := generateRDSUpdateFromRouteConfiguration(rc, logger, v2) 61 if err != nil { 62 return rc.GetName(), RouteConfigUpdate{}, err 63 } 64 u.Raw = r 65 return rc.GetName(), u, nil 66 } 67 68 // generateRDSUpdateFromRouteConfiguration checks if the provided 69 // RouteConfiguration meets the expected criteria. If so, it returns a 70 // RouteConfigUpdate with nil error. 71 // 72 // A RouteConfiguration resource is considered valid when only if it contains a 73 // VirtualHost whose domain field matches the server name from the URI passed 74 // to the gRPC channel, and it contains a clusterName or a weighted cluster. 75 // 76 // The RouteConfiguration includes a list of virtualHosts, which may have zero 77 // or more elements. We are interested in the element whose domains field 78 // matches the server name specified in the "xds:" URI. The only field in the 79 // VirtualHost proto that the we are interested in is the list of routes. We 80 // only look at the last route in the list (the default route), whose match 81 // field must be empty and whose route field must be set. Inside that route 82 // message, the cluster field will contain the clusterName or weighted clusters 83 // we are looking for. 84 func generateRDSUpdateFromRouteConfiguration(rc *v3routepb.RouteConfiguration, logger *grpclog.PrefixLogger, v2 bool) (RouteConfigUpdate, error) { 85 vhs := make([]*VirtualHost, 0, len(rc.GetVirtualHosts())) 86 csps := make(map[string]clusterspecifier.BalancerConfig) 87 if envconfig.XDSRLS { 88 var err error 89 csps, err = processClusterSpecifierPlugins(rc.ClusterSpecifierPlugins) 90 if err != nil { 91 return RouteConfigUpdate{}, fmt.Errorf("received route is invalid %v", err) 92 } 93 } 94 // cspNames represents all the cluster specifiers referenced by Route 95 // Actions - any cluster specifiers not referenced by a Route Action can be 96 // ignored and not emitted by the xdsclient. 97 var cspNames = make(map[string]bool) 98 for _, vh := range rc.GetVirtualHosts() { 99 routes, cspNs, err := routesProtoToSlice(vh.Routes, csps, logger, v2) 100 if err != nil { 101 return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err) 102 } 103 for n := range cspNs { 104 cspNames[n] = true 105 } 106 rc, err := generateRetryConfig(vh.GetRetryPolicy()) 107 if err != nil { 108 return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err) 109 } 110 vhOut := &VirtualHost{ 111 Domains: vh.GetDomains(), 112 Routes: routes, 113 RetryConfig: rc, 114 } 115 if !v2 { 116 cfgs, err := processHTTPFilterOverrides(vh.GetTypedPerFilterConfig()) 117 if err != nil { 118 return RouteConfigUpdate{}, fmt.Errorf("virtual host %+v: %v", vh, err) 119 } 120 vhOut.HTTPFilterConfigOverride = cfgs 121 } 122 vhs = append(vhs, vhOut) 123 } 124 125 // "For any entry in the RouteConfiguration.cluster_specifier_plugins not 126 // referenced by an enclosed ActionType's cluster_specifier_plugin, the xDS 127 // client should not provide it to its consumers." - RLS in xDS Design 128 for name := range csps { 129 if !cspNames[name] { 130 delete(csps, name) 131 } 132 } 133 134 return RouteConfigUpdate{VirtualHosts: vhs, ClusterSpecifierPlugins: csps}, nil 135 } 136 137 func processClusterSpecifierPlugins(csps []*v3routepb.ClusterSpecifierPlugin) (map[string]clusterspecifier.BalancerConfig, error) { 138 cspCfgs := make(map[string]clusterspecifier.BalancerConfig) 139 // "The xDS client will inspect all elements of the 140 // cluster_specifier_plugins field looking up a plugin based on the 141 // extension.typed_config of each." - RLS in xDS design 142 for _, csp := range csps { 143 cs := clusterspecifier.Get(csp.GetExtension().GetTypedConfig().GetTypeUrl()) 144 if cs == nil { 145 // "If no plugin is registered for it, the resource will be NACKed." 146 // - RLS in xDS design 147 return nil, fmt.Errorf("cluster specifier %q of type %q was not found", csp.GetExtension().GetName(), csp.GetExtension().GetTypedConfig().GetTypeUrl()) 148 } 149 lbCfg, err := cs.ParseClusterSpecifierConfig(csp.GetExtension().GetTypedConfig()) 150 if err != nil { 151 // "If a plugin is found, the value of the typed_config field will 152 // be passed to it's conversion method, and if an error is 153 // encountered, the resource will be NACKED." - RLS in xDS design 154 return nil, fmt.Errorf("error: %q parsing config %q for cluster specifier %q of type %q", err, csp.GetExtension().GetTypedConfig(), csp.GetExtension().GetName(), csp.GetExtension().GetTypedConfig().GetTypeUrl()) 155 } 156 // "If all cluster specifiers are valid, the xDS client will store the 157 // configurations in a map keyed by the name of the extension instance." - 158 // RLS in xDS Design 159 cspCfgs[csp.GetExtension().GetName()] = lbCfg 160 } 161 return cspCfgs, nil 162 } 163 164 func generateRetryConfig(rp *v3routepb.RetryPolicy) (*RetryConfig, error) { 165 if rp == nil { 166 return nil, nil 167 } 168 169 cfg := &RetryConfig{RetryOn: make(map[codes.Code]bool)} 170 for _, s := range strings.Split(rp.GetRetryOn(), ",") { 171 switch strings.TrimSpace(strings.ToLower(s)) { 172 case "cancelled": 173 cfg.RetryOn[codes.Canceled] = true 174 case "deadline-exceeded": 175 cfg.RetryOn[codes.DeadlineExceeded] = true 176 case "internal": 177 cfg.RetryOn[codes.Internal] = true 178 case "resource-exhausted": 179 cfg.RetryOn[codes.ResourceExhausted] = true 180 case "unavailable": 181 cfg.RetryOn[codes.Unavailable] = true 182 } 183 } 184 185 if rp.NumRetries == nil { 186 cfg.NumRetries = 1 187 } else { 188 cfg.NumRetries = rp.GetNumRetries().Value 189 if cfg.NumRetries < 1 { 190 return nil, fmt.Errorf("retry_policy.num_retries = %v; must be >= 1", cfg.NumRetries) 191 } 192 } 193 194 backoff := rp.GetRetryBackOff() 195 if backoff == nil { 196 cfg.RetryBackoff.BaseInterval = 25 * time.Millisecond 197 } else { 198 cfg.RetryBackoff.BaseInterval = backoff.GetBaseInterval().AsDuration() 199 if cfg.RetryBackoff.BaseInterval <= 0 { 200 return nil, fmt.Errorf("retry_policy.base_interval = %v; must be > 0", cfg.RetryBackoff.BaseInterval) 201 } 202 } 203 if max := backoff.GetMaxInterval(); max == nil { 204 cfg.RetryBackoff.MaxInterval = 10 * cfg.RetryBackoff.BaseInterval 205 } else { 206 cfg.RetryBackoff.MaxInterval = max.AsDuration() 207 if cfg.RetryBackoff.MaxInterval <= 0 { 208 return nil, fmt.Errorf("retry_policy.max_interval = %v; must be > 0", cfg.RetryBackoff.MaxInterval) 209 } 210 } 211 212 if len(cfg.RetryOn) == 0 { 213 return &RetryConfig{}, nil 214 } 215 return cfg, nil 216 } 217 218 func routesProtoToSlice(routes []*v3routepb.Route, csps map[string]clusterspecifier.BalancerConfig, logger *grpclog.PrefixLogger, v2 bool) ([]*Route, map[string]bool, error) { 219 var routesRet []*Route 220 var cspNames = make(map[string]bool) 221 for _, r := range routes { 222 match := r.GetMatch() 223 if match == nil { 224 return nil, nil, fmt.Errorf("route %+v doesn't have a match", r) 225 } 226 227 if len(match.GetQueryParameters()) != 0 { 228 // Ignore route with query parameters. 229 logger.Warningf("route %+v has query parameter matchers, the route will be ignored", r) 230 continue 231 } 232 233 pathSp := match.GetPathSpecifier() 234 if pathSp == nil { 235 return nil, nil, fmt.Errorf("route %+v doesn't have a path specifier", r) 236 } 237 238 var route Route 239 switch pt := pathSp.(type) { 240 case *v3routepb.RouteMatch_Prefix: 241 route.Prefix = &pt.Prefix 242 case *v3routepb.RouteMatch_Path: 243 route.Path = &pt.Path 244 case *v3routepb.RouteMatch_SafeRegex: 245 regex := pt.SafeRegex.GetRegex() 246 re, err := regexp.Compile(regex) 247 if err != nil { 248 return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex) 249 } 250 route.Regex = re 251 default: 252 return nil, nil, fmt.Errorf("route %+v has an unrecognized path specifier: %+v", r, pt) 253 } 254 255 if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil { 256 route.CaseInsensitive = !caseSensitive.Value 257 } 258 259 for _, h := range match.GetHeaders() { 260 var header HeaderMatcher 261 switch ht := h.GetHeaderMatchSpecifier().(type) { 262 case *v3routepb.HeaderMatcher_ExactMatch: 263 header.ExactMatch = &ht.ExactMatch 264 case *v3routepb.HeaderMatcher_SafeRegexMatch: 265 regex := ht.SafeRegexMatch.GetRegex() 266 re, err := regexp.Compile(regex) 267 if err != nil { 268 return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex) 269 } 270 header.RegexMatch = re 271 case *v3routepb.HeaderMatcher_RangeMatch: 272 header.RangeMatch = &Int64Range{ 273 Start: ht.RangeMatch.Start, 274 End: ht.RangeMatch.End, 275 } 276 case *v3routepb.HeaderMatcher_PresentMatch: 277 header.PresentMatch = &ht.PresentMatch 278 case *v3routepb.HeaderMatcher_PrefixMatch: 279 header.PrefixMatch = &ht.PrefixMatch 280 case *v3routepb.HeaderMatcher_SuffixMatch: 281 header.SuffixMatch = &ht.SuffixMatch 282 default: 283 return nil, nil, fmt.Errorf("route %+v has an unrecognized header matcher: %+v", r, ht) 284 } 285 header.Name = h.GetName() 286 invert := h.GetInvertMatch() 287 header.InvertMatch = &invert 288 route.Headers = append(route.Headers, &header) 289 } 290 291 if fr := match.GetRuntimeFraction(); fr != nil { 292 d := fr.GetDefaultValue() 293 n := d.GetNumerator() 294 switch d.GetDenominator() { 295 case v3typepb.FractionalPercent_HUNDRED: 296 n *= 10000 297 case v3typepb.FractionalPercent_TEN_THOUSAND: 298 n *= 100 299 case v3typepb.FractionalPercent_MILLION: 300 } 301 route.Fraction = &n 302 } 303 304 switch r.GetAction().(type) { 305 case *v3routepb.Route_Route: 306 route.WeightedClusters = make(map[string]WeightedCluster) 307 action := r.GetRoute() 308 309 // Hash Policies are only applicable for a Ring Hash LB. 310 if envconfig.XDSRingHash { 311 hp, err := hashPoliciesProtoToSlice(action.HashPolicy, logger) 312 if err != nil { 313 return nil, nil, err 314 } 315 route.HashPolicies = hp 316 } 317 318 switch a := action.GetClusterSpecifier().(type) { 319 case *v3routepb.RouteAction_Cluster: 320 route.WeightedClusters[a.Cluster] = WeightedCluster{Weight: 1} 321 case *v3routepb.RouteAction_WeightedClusters: 322 wcs := a.WeightedClusters 323 var totalWeight uint32 324 for _, c := range wcs.Clusters { 325 w := c.GetWeight().GetValue() 326 if w == 0 { 327 continue 328 } 329 wc := WeightedCluster{Weight: w} 330 if !v2 { 331 cfgs, err := processHTTPFilterOverrides(c.GetTypedPerFilterConfig()) 332 if err != nil { 333 return nil, nil, fmt.Errorf("route %+v, action %+v: %v", r, a, err) 334 } 335 wc.HTTPFilterConfigOverride = cfgs 336 } 337 route.WeightedClusters[c.GetName()] = wc 338 totalWeight += w 339 } 340 // envoy xds doc 341 // default TotalWeight https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto.html#envoy-v3-api-field-config-route-v3-weightedcluster-total-weight 342 wantTotalWeight := uint32(100) 343 if tw := wcs.GetTotalWeight(); tw != nil { 344 wantTotalWeight = tw.GetValue() 345 } 346 if totalWeight != wantTotalWeight { 347 return nil, nil, fmt.Errorf("route %+v, action %+v, weights of clusters do not add up to total total weight, got: %v, expected total weight from response: %v", r, a, totalWeight, wantTotalWeight) 348 } 349 if totalWeight == 0 { 350 return nil, nil, fmt.Errorf("route %+v, action %+v, has no valid cluster in WeightedCluster action", r, a) 351 } 352 case *v3routepb.RouteAction_ClusterHeader: 353 continue 354 case *v3routepb.RouteAction_ClusterSpecifierPlugin: 355 if !envconfig.XDSRLS { 356 return nil, nil, fmt.Errorf("route %+v, has an unknown ClusterSpecifier: %+v", r, a) 357 } 358 if _, ok := csps[a.ClusterSpecifierPlugin]; !ok { 359 // "When processing RouteActions, if any action includes a 360 // cluster_specifier_plugin value that is not in 361 // RouteConfiguration.cluster_specifier_plugins, the 362 // resource will be NACKed." - RLS in xDS design 363 return nil, nil, fmt.Errorf("route %+v, action %+v, specifies a cluster specifier plugin %+v that is not in Route Configuration", r, a, a.ClusterSpecifierPlugin) 364 } 365 cspNames[a.ClusterSpecifierPlugin] = true 366 route.ClusterSpecifierPlugin = a.ClusterSpecifierPlugin 367 default: 368 return nil, nil, fmt.Errorf("route %+v, has an unknown ClusterSpecifier: %+v", r, a) 369 } 370 371 msd := action.GetMaxStreamDuration() 372 // Prefer grpc_timeout_header_max, if set. 373 dur := msd.GetGrpcTimeoutHeaderMax() 374 if dur == nil { 375 dur = msd.GetMaxStreamDuration() 376 } 377 if dur != nil { 378 d := dur.AsDuration() 379 route.MaxStreamDuration = &d 380 } 381 382 var err error 383 route.RetryConfig, err = generateRetryConfig(action.GetRetryPolicy()) 384 if err != nil { 385 return nil, nil, fmt.Errorf("route %+v, action %+v: %v", r, action, err) 386 } 387 388 route.ActionType = RouteActionRoute 389 390 case *v3routepb.Route_NonForwardingAction: 391 // Expected to be used on server side. 392 route.ActionType = RouteActionNonForwardingAction 393 default: 394 route.ActionType = RouteActionUnsupported 395 } 396 397 if !v2 { 398 cfgs, err := processHTTPFilterOverrides(r.GetTypedPerFilterConfig()) 399 if err != nil { 400 return nil, nil, fmt.Errorf("route %+v: %v", r, err) 401 } 402 route.HTTPFilterConfigOverride = cfgs 403 } 404 routesRet = append(routesRet, &route) 405 } 406 return routesRet, cspNames, nil 407 } 408 409 func hashPoliciesProtoToSlice(policies []*v3routepb.RouteAction_HashPolicy, logger *grpclog.PrefixLogger) ([]*HashPolicy, error) { 410 var hashPoliciesRet []*HashPolicy 411 for _, p := range policies { 412 policy := HashPolicy{Terminal: p.Terminal} 413 switch p.GetPolicySpecifier().(type) { 414 case *v3routepb.RouteAction_HashPolicy_Header_: 415 policy.HashPolicyType = HashPolicyTypeHeader 416 policy.HeaderName = p.GetHeader().GetHeaderName() 417 if rr := p.GetHeader().GetRegexRewrite(); rr != nil { 418 regex := rr.GetPattern().GetRegex() 419 re, err := regexp.Compile(regex) 420 if err != nil { 421 return nil, fmt.Errorf("hash policy %+v contains an invalid regex %q", p, regex) 422 } 423 policy.Regex = re 424 policy.RegexSubstitution = rr.GetSubstitution() 425 } 426 case *v3routepb.RouteAction_HashPolicy_FilterState_: 427 if p.GetFilterState().GetKey() != "io.grpc.channel_id" { 428 logger.Infof("hash policy %+v contains an invalid key for filter state policy %q", p, p.GetFilterState().GetKey()) 429 continue 430 } 431 policy.HashPolicyType = HashPolicyTypeChannelID 432 default: 433 logger.Infof("hash policy %T is an unsupported hash policy", p.GetPolicySpecifier()) 434 continue 435 } 436 437 hashPoliciesRet = append(hashPoliciesRet, &policy) 438 } 439 return hashPoliciesRet, nil 440 }