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  }