dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/client/resource/unmarshal_cds.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  	"errors"
    28  	"fmt"
    29  	"net"
    30  	"strconv"
    31  )
    32  
    33  import (
    34  	dubbogoLogger "github.com/dubbogo/gost/log/logger"
    35  
    36  	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    37  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    38  	v3aggregateclusterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
    39  	v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
    40  
    41  	"github.com/golang/protobuf/proto"
    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/utils/envconfig"
    49  	"dubbo.apache.org/dubbo-go/v3/xds/utils/matcher"
    50  	"dubbo.apache.org/dubbo-go/v3/xds/utils/pretty"
    51  )
    52  
    53  // TransportSocket proto message has a `name` field which is expected to be set
    54  // to this value by the management server.
    55  const transportSocketName = "envoy.transport_sockets.tls"
    56  
    57  // UnmarshalCluster processes resources received in an CDS response, validates
    58  // them, and transforms them into a native struct which contains only fields we
    59  // are interested in.
    60  func UnmarshalCluster(opts *UnmarshalOptions) (map[string]ClusterUpdateErrTuple, UpdateMetadata, error) {
    61  	update := make(map[string]ClusterUpdateErrTuple)
    62  	md, err := processAllResources(opts, update)
    63  	return update, md, err
    64  }
    65  
    66  func unmarshalClusterResource(r *anypb.Any, f UpdateValidatorFunc, logger dubbogoLogger.Logger) (string, ClusterUpdate, error) {
    67  	if !IsClusterResource(r.GetTypeUrl()) {
    68  		return "", ClusterUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl())
    69  	}
    70  
    71  	cluster := &v3clusterpb.Cluster{}
    72  	if err := proto.Unmarshal(r.GetValue(), cluster); err != nil {
    73  		return "", ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
    74  	}
    75  	dubbogoLogger.Debugf("Resource with name: %v, type: %T, contains: %v", cluster.GetName(), cluster, pretty.ToJSON(cluster))
    76  	cu, err := validateClusterAndConstructClusterUpdate(cluster)
    77  	if err != nil {
    78  		return cluster.GetName(), ClusterUpdate{}, err
    79  	}
    80  	cu.Raw = r
    81  	if f != nil {
    82  		if err := f(cu); err != nil {
    83  			return "", ClusterUpdate{}, err
    84  		}
    85  	}
    86  
    87  	return cluster.GetName(), cu, nil
    88  }
    89  
    90  const (
    91  	defaultRingHashMinSize = 1024
    92  	defaultRingHashMaxSize = 8 * 1024 * 1024 // 8M
    93  	ringHashSizeUpperBound = 8 * 1024 * 1024 // 8M
    94  )
    95  
    96  func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster) (ClusterUpdate, error) {
    97  	var lbPolicy *ClusterLBPolicyRingHash
    98  	// todo @(laurence) this direct set
    99  	cluster.LbPolicy = v3clusterpb.Cluster_ROUND_ROBIN
   100  	switch cluster.GetLbPolicy() {
   101  	case v3clusterpb.Cluster_ROUND_ROBIN:
   102  		lbPolicy = nil // The default is round_robin, and there's no config to set.
   103  	case v3clusterpb.Cluster_RING_HASH:
   104  		if !envconfig.XDSRingHash {
   105  			return ClusterUpdate{}, fmt.Errorf("unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster)
   106  		}
   107  		rhc := cluster.GetRingHashLbConfig()
   108  		if rhc.GetHashFunction() != v3clusterpb.Cluster_RingHashLbConfig_XX_HASH {
   109  			return ClusterUpdate{}, fmt.Errorf("unsupported ring_hash hash function %v in response: %+v", rhc.GetHashFunction(), cluster)
   110  		}
   111  		// Minimum defaults to 1024 entries, and limited to 8M entries Maximum
   112  		// defaults to 8M entries, and limited to 8M entries
   113  		var minSize, maxSize uint64 = defaultRingHashMinSize, defaultRingHashMaxSize
   114  		if min := rhc.GetMinimumRingSize(); min != nil {
   115  			if min.GetValue() > ringHashSizeUpperBound {
   116  				return ClusterUpdate{}, fmt.Errorf("unexpected ring_hash mininum ring size %v in response: %+v", min.GetValue(), cluster)
   117  			}
   118  			minSize = min.GetValue()
   119  		}
   120  		if max := rhc.GetMaximumRingSize(); max != nil {
   121  			if max.GetValue() > ringHashSizeUpperBound {
   122  				return ClusterUpdate{}, fmt.Errorf("unexpected ring_hash maxinum ring size %v in response: %+v", max.GetValue(), cluster)
   123  			}
   124  			maxSize = max.GetValue()
   125  		}
   126  		if minSize > maxSize {
   127  			return ClusterUpdate{}, fmt.Errorf("ring_hash config min size %v is greater than max %v", minSize, maxSize)
   128  		}
   129  		lbPolicy = &ClusterLBPolicyRingHash{MinimumRingSize: minSize, MaximumRingSize: maxSize}
   130  	default:
   131  		return ClusterUpdate{}, fmt.Errorf("unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster)
   132  	}
   133  
   134  	// Process security configuration received from the control plane iff the
   135  	// corresponding environment variable is set.
   136  	var sc *SecurityConfig
   137  	if envconfig.XDSClientSideSecurity {
   138  		var err error
   139  		if sc, err = securityConfigFromCluster(cluster); err != nil {
   140  			return ClusterUpdate{}, err
   141  		}
   142  	}
   143  
   144  	ret := ClusterUpdate{
   145  		ClusterName: cluster.GetName(),
   146  		EnableLRS:   cluster.GetLrsServer().GetSelf() != nil,
   147  		SecurityCfg: sc,
   148  		MaxRequests: circuitBreakersFromCluster(cluster),
   149  		LBPolicy:    lbPolicy,
   150  	}
   151  
   152  	// Validate and set cluster type from the response.
   153  	// todo @laurence this set cluster
   154  	if x, ok := cluster.GetClusterDiscoveryType().(*v3clusterpb.Cluster_Type); ok {
   155  		x.Type = v3clusterpb.Cluster_EDS
   156  	}
   157  	switch {
   158  	case cluster.GetType() == v3clusterpb.Cluster_EDS:
   159  		if cluster.GetEdsClusterConfig().GetEdsConfig().GetAds() == nil {
   160  			return ClusterUpdate{}, fmt.Errorf("unexpected edsConfig in response: %+v", cluster)
   161  		}
   162  		ret.ClusterType = ClusterTypeEDS
   163  		ret.EDSServiceName = cluster.GetEdsClusterConfig().GetServiceName()
   164  		return ret, nil
   165  	case cluster.GetType() == v3clusterpb.Cluster_LOGICAL_DNS:
   166  		if !envconfig.XDSAggregateAndDNS {
   167  			return ClusterUpdate{}, fmt.Errorf("unsupported cluster type (%v, %v) in response: %+v", cluster.GetType(), cluster.GetClusterType(), cluster)
   168  		}
   169  		ret.ClusterType = ClusterTypeLogicalDNS
   170  		dnsHN, err := dnsHostNameFromCluster(cluster)
   171  		if err != nil {
   172  			return ClusterUpdate{}, err
   173  		}
   174  		ret.DNSHostName = dnsHN
   175  		return ret, nil
   176  	case cluster.GetClusterType() != nil && cluster.GetClusterType().Name == "envoy.clusters.aggregate":
   177  		if !envconfig.XDSAggregateAndDNS {
   178  			return ClusterUpdate{}, fmt.Errorf("unsupported cluster type (%v, %v) in response: %+v", cluster.GetType(), cluster.GetClusterType(), cluster)
   179  		}
   180  		clusters := &v3aggregateclusterpb.ClusterConfig{}
   181  		if err := proto.Unmarshal(cluster.GetClusterType().GetTypedConfig().GetValue(), clusters); err != nil {
   182  			return ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
   183  		}
   184  		ret.ClusterType = ClusterTypeAggregate
   185  		ret.PrioritizedClusterNames = clusters.Clusters
   186  		return ret, nil
   187  	default:
   188  		return ClusterUpdate{}, fmt.Errorf("unsupported cluster type (%v, %v) in response: %+v", cluster.GetType(), cluster.GetClusterType(), cluster)
   189  	}
   190  }
   191  
   192  // dnsHostNameFromCluster extracts the DNS host name from the cluster's load
   193  // assignment.
   194  //
   195  // There should be exactly one locality, with one endpoint, whose address
   196  // contains the address and port.
   197  func dnsHostNameFromCluster(cluster *v3clusterpb.Cluster) (string, error) {
   198  	loadAssignment := cluster.GetLoadAssignment()
   199  	if loadAssignment == nil {
   200  		return "", fmt.Errorf("load_assignment not present for LOGICAL_DNS cluster")
   201  	}
   202  	if len(loadAssignment.GetEndpoints()) != 1 {
   203  		return "", fmt.Errorf("load_assignment for LOGICAL_DNS cluster must have exactly one locality, got: %+v", loadAssignment)
   204  	}
   205  	endpoints := loadAssignment.GetEndpoints()[0].GetLbEndpoints()
   206  	if len(endpoints) != 1 {
   207  		return "", fmt.Errorf("locality for LOGICAL_DNS cluster must have exactly one endpoint, got: %+v", endpoints)
   208  	}
   209  	endpoint := endpoints[0].GetEndpoint()
   210  	if endpoint == nil {
   211  		return "", fmt.Errorf("endpoint for LOGICAL_DNS cluster not set")
   212  	}
   213  	socketAddr := endpoint.GetAddress().GetSocketAddress()
   214  	if socketAddr == nil {
   215  		return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set")
   216  	}
   217  	if socketAddr.GetResolverName() != "" {
   218  		return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set has unexpected custom resolver name: %v", socketAddr.GetResolverName())
   219  	}
   220  	host := socketAddr.GetAddress()
   221  	if host == "" {
   222  		return "", fmt.Errorf("host for endpoint for LOGICAL_DNS cluster not set")
   223  	}
   224  	port := socketAddr.GetPortValue()
   225  	if port == 0 {
   226  		return "", fmt.Errorf("port for endpoint for LOGICAL_DNS cluster not set")
   227  	}
   228  	return net.JoinHostPort(host, strconv.Itoa(int(port))), nil
   229  }
   230  
   231  // securityConfigFromCluster extracts the relevant security configuration from
   232  // the received Cluster resource.
   233  func securityConfigFromCluster(cluster *v3clusterpb.Cluster) (*SecurityConfig, error) {
   234  	if tsm := cluster.GetTransportSocketMatches(); len(tsm) != 0 {
   235  		return nil, fmt.Errorf("unsupport transport_socket_matches field is non-empty: %+v", tsm)
   236  	}
   237  	// The Cluster resource contains a `transport_socket` field, which contains
   238  	// a oneof `typed_config` field of type `protobuf.Any`. The any proto
   239  	// contains a marshaled representation of an `UpstreamTlsContext` message.
   240  	ts := cluster.GetTransportSocket()
   241  	if ts == nil {
   242  		return nil, nil
   243  	}
   244  	if name := ts.GetName(); name != transportSocketName {
   245  		return nil, fmt.Errorf("transport_socket field has unexpected name: %s", name)
   246  	}
   247  	any := ts.GetTypedConfig()
   248  	if any == nil || any.TypeUrl != version.V3UpstreamTLSContextURL {
   249  		return nil, fmt.Errorf("transport_socket field has unexpected typeURL: %s", any.TypeUrl)
   250  	}
   251  	upstreamCtx := &v3tlspb.UpstreamTlsContext{}
   252  	if err := proto.Unmarshal(any.GetValue(), upstreamCtx); err != nil {
   253  		return nil, fmt.Errorf("failed to unmarshal UpstreamTlsContext in CDS response: %v", err)
   254  	}
   255  	// The following fields from `UpstreamTlsContext` are ignored:
   256  	// - sni
   257  	// - allow_renegotiation
   258  	// - max_session_keys
   259  	if upstreamCtx.GetCommonTlsContext() == nil {
   260  		return nil, errors.New("UpstreamTlsContext in CDS response does not contain a CommonTlsContext")
   261  	}
   262  
   263  	return securityConfigFromCommonTLSContext(upstreamCtx.GetCommonTlsContext(), false)
   264  }
   265  
   266  // common is expected to be not nil.
   267  // The `alpn_protocols` field is ignored.
   268  func securityConfigFromCommonTLSContext(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
   269  	if common.GetTlsParams() != nil {
   270  		return nil, fmt.Errorf("unsupported tls_params field in CommonTlsContext message: %+v", common)
   271  	}
   272  	if common.GetCustomHandshaker() != nil {
   273  		return nil, fmt.Errorf("unsupported custom_handshaker field in CommonTlsContext message: %+v", common)
   274  	}
   275  
   276  	// For now, if we can't get a valid security config from the new fields, we
   277  	// fallback to the old deprecated fields.
   278  	// TODO: Drop support for deprecated fields. NACK if err != nil here.
   279  	sc, _ := securityConfigFromCommonTLSContextUsingNewFields(common, server)
   280  	if sc == nil || sc.Equal(&SecurityConfig{}) {
   281  		var err error
   282  		sc, err = securityConfigFromCommonTLSContextWithDeprecatedFields(common, server)
   283  		if err != nil {
   284  			return nil, err
   285  		}
   286  	}
   287  	if sc != nil {
   288  		// sc == nil is a valid case where the control plane has not sent us any
   289  		// security configuration. xDS creds will use fallback creds.
   290  		if server {
   291  			if sc.IdentityInstanceName == "" {
   292  				return nil, errors.New("security configuration on the server-side does not contain identity certificate provider instance name")
   293  			}
   294  		} else {
   295  			if sc.RootInstanceName == "" {
   296  				return nil, errors.New("security configuration on the client-side does not contain root certificate provider instance name")
   297  			}
   298  		}
   299  	}
   300  	return sc, nil
   301  }
   302  
   303  func securityConfigFromCommonTLSContextWithDeprecatedFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
   304  	// The `CommonTlsContext` contains a
   305  	// `tls_certificate_certificate_provider_instance` field of type
   306  	// `CertificateProviderInstance`, which contains the provider instance name
   307  	// and the certificate name to fetch identity certs.
   308  	sc := &SecurityConfig{}
   309  	if identity := common.GetTlsCertificateCertificateProviderInstance(); identity != nil {
   310  		sc.IdentityInstanceName = identity.GetInstanceName()
   311  		sc.IdentityCertName = identity.GetCertificateName()
   312  	}
   313  
   314  	// The `CommonTlsContext` contains a `validation_context_type` field which
   315  	// is a oneof. We can get the values that we are interested in from two of
   316  	// those possible values:
   317  	//  - combined validation context:
   318  	//    - contains a default validation context which holds the list of
   319  	//      matchers for accepted SANs.
   320  	//    - contains certificate provider instance configuration
   321  	//  - certificate provider instance configuration
   322  	//    - in this case, we do not get a list of accepted SANs.
   323  	switch t := common.GetValidationContextType().(type) {
   324  	case *v3tlspb.CommonTlsContext_CombinedValidationContext:
   325  		combined := common.GetCombinedValidationContext()
   326  		var matchers []matcher.StringMatcher
   327  		if def := combined.GetDefaultValidationContext(); def != nil {
   328  			for _, m := range def.GetMatchSubjectAltNames() {
   329  				matcher, err := matcher.StringMatcherFromProto(m)
   330  				if err != nil {
   331  					return nil, err
   332  				}
   333  				matchers = append(matchers, matcher)
   334  			}
   335  		}
   336  		if server && len(matchers) != 0 {
   337  			return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common)
   338  		}
   339  		sc.SubjectAltNameMatchers = matchers
   340  		if pi := combined.GetValidationContextCertificateProviderInstance(); pi != nil {
   341  			sc.RootInstanceName = pi.GetInstanceName()
   342  			sc.RootCertName = pi.GetCertificateName()
   343  		}
   344  	case *v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance:
   345  		pi := common.GetValidationContextCertificateProviderInstance()
   346  		sc.RootInstanceName = pi.GetInstanceName()
   347  		sc.RootCertName = pi.GetCertificateName()
   348  	case nil:
   349  		// It is valid for the validation context to be nil on the server side.
   350  	default:
   351  		return nil, fmt.Errorf("validation context contains unexpected type: %T", t)
   352  	}
   353  	return sc, nil
   354  }
   355  
   356  // gRFC A29 https://github.com/grpc/proposal/blob/master/A29-xds-tls-security.md
   357  // specifies the new way to fetch security configuration and says the following:
   358  //
   359  // Although there are various ways to obtain certificates as per this proto
   360  // (which are supported by Envoy), gRPC supports only one of them and that is
   361  // the `CertificateProviderPluginInstance` proto.
   362  //
   363  // This helper function attempts to fetch security configuration from the
   364  // `CertificateProviderPluginInstance` message, given a CommonTlsContext.
   365  func securityConfigFromCommonTLSContextUsingNewFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
   366  	// The `tls_certificate_provider_instance` field of type
   367  	// `CertificateProviderPluginInstance` is used to fetch the identity
   368  	// certificate provider.
   369  	sc := &SecurityConfig{}
   370  	identity := common.GetTlsCertificateProviderInstance()
   371  	if identity == nil && len(common.GetTlsCertificates()) != 0 {
   372  		return nil, fmt.Errorf("expected field tls_certificate_provider_instance is not set, while unsupported field tls_certificates is set in CommonTlsContext message: %+v", common)
   373  	}
   374  	if identity == nil && common.GetTlsCertificateSdsSecretConfigs() != nil {
   375  		return nil, fmt.Errorf("expected field tls_certificate_provider_instance is not set, while unsupported field tls_certificate_sds_secret_configs is set in CommonTlsContext message: %+v", common)
   376  	}
   377  	sc.IdentityInstanceName = identity.GetInstanceName()
   378  	sc.IdentityCertName = identity.GetCertificateName()
   379  
   380  	// The `CommonTlsContext` contains a oneof field `validation_context_type`,
   381  	// which contains the `CertificateValidationContext` message in one of the
   382  	// following ways:
   383  	//  - `validation_context` field
   384  	//    - this is directly of type `CertificateValidationContext`
   385  	//  - `combined_validation_context` field
   386  	//    - this is of type `CombinedCertificateValidationContext` and contains
   387  	//      a `default validation context` field of type
   388  	//      `CertificateValidationContext`
   389  	//
   390  	// The `CertificateValidationContext` message has the following fields that
   391  	// we are interested in:
   392  	//  - `ca_certificate_provider_instance`
   393  	//    - this is of type `CertificateProviderPluginInstance`
   394  	//  - `match_subject_alt_names`
   395  	//    - this is a list of string matchers
   396  	//
   397  	// The `CertificateProviderPluginInstance` message contains two fields
   398  	//  - instance_name
   399  	//    - this is the certificate provider instance name to be looked up in
   400  	//      the bootstrap configuration
   401  	//  - certificate_name
   402  	//    -  this is an opaque name passed to the certificate provider
   403  	var validationCtx *v3tlspb.CertificateValidationContext
   404  	switch typ := common.GetValidationContextType().(type) {
   405  	case *v3tlspb.CommonTlsContext_ValidationContext:
   406  		validationCtx = common.GetValidationContext()
   407  	case *v3tlspb.CommonTlsContext_CombinedValidationContext:
   408  		validationCtx = common.GetCombinedValidationContext().GetDefaultValidationContext()
   409  	case nil:
   410  		// It is valid for the validation context to be nil on the server side.
   411  		return sc, nil
   412  	default:
   413  		return nil, fmt.Errorf("validation context contains unexpected type: %T", typ)
   414  	}
   415  	// If we get here, it means that the `CertificateValidationContext` message
   416  	// was found through one of the supported ways. It is an error if the
   417  	// validation context is specified, but it does not contain the
   418  	// ca_certificate_provider_instance field which contains information about
   419  	// the certificate provider to be used for the root certificates.
   420  	if validationCtx.GetCaCertificateProviderInstance() == nil {
   421  		return nil, fmt.Errorf("expected field ca_certificate_provider_instance is missing in CommonTlsContext message: %+v", common)
   422  	}
   423  	// The following fields are ignored:
   424  	// - trusted_ca
   425  	// - watched_directory
   426  	// - allow_expired_certificate
   427  	// - trust_chain_verification
   428  	switch {
   429  	case len(validationCtx.GetVerifyCertificateSpki()) != 0:
   430  		return nil, fmt.Errorf("unsupported verify_certificate_spki field in CommonTlsContext message: %+v", common)
   431  	case len(validationCtx.GetVerifyCertificateHash()) != 0:
   432  		return nil, fmt.Errorf("unsupported verify_certificate_hash field in CommonTlsContext message: %+v", common)
   433  	case validationCtx.GetRequireSignedCertificateTimestamp().GetValue():
   434  		return nil, fmt.Errorf("unsupported require_sugned_ceritificate_timestamp field in CommonTlsContext message: %+v", common)
   435  	case validationCtx.GetCrl() != nil:
   436  		return nil, fmt.Errorf("unsupported crl field in CommonTlsContext message: %+v", common)
   437  	case validationCtx.GetCustomValidatorConfig() != nil:
   438  		return nil, fmt.Errorf("unsupported custom_validator_config field in CommonTlsContext message: %+v", common)
   439  	}
   440  
   441  	if rootProvider := validationCtx.GetCaCertificateProviderInstance(); rootProvider != nil {
   442  		sc.RootInstanceName = rootProvider.GetInstanceName()
   443  		sc.RootCertName = rootProvider.GetCertificateName()
   444  	}
   445  	var matchers []matcher.StringMatcher
   446  	for _, m := range validationCtx.GetMatchSubjectAltNames() {
   447  		matcher, err := matcher.StringMatcherFromProto(m)
   448  		if err != nil {
   449  			return nil, err
   450  		}
   451  		matchers = append(matchers, matcher)
   452  	}
   453  	if server && len(matchers) != 0 {
   454  		return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common)
   455  	}
   456  	sc.SubjectAltNameMatchers = matchers
   457  	return sc, nil
   458  }
   459  
   460  // circuitBreakersFromCluster extracts the circuit breakers configuration from
   461  // the received cluster resource. Returns nil if no CircuitBreakers or no
   462  // Thresholds in CircuitBreakers.
   463  func circuitBreakersFromCluster(cluster *v3clusterpb.Cluster) *uint32 {
   464  	for _, threshold := range cluster.GetCircuitBreakers().GetThresholds() {
   465  		if threshold.GetPriority() != v3corepb.RoutingPriority_DEFAULT {
   466  			continue
   467  		}
   468  		maxRequestsPb := threshold.GetMaxRequests()
   469  		if maxRequestsPb == nil {
   470  			return nil
   471  		}
   472  		maxRequests := maxRequestsPb.GetValue()
   473  		return &maxRequests
   474  	}
   475  	return nil
   476  }