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