snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
140
internal/xds/xdsclient/xdsresource/cluster_resource_type.go
Normal file
140
internal/xds/xdsclient/xdsresource/cluster_resource_type.go
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdsclient "google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// ClusterResourceTypeName represents the transport agnostic name for the
|
||||
// cluster resource.
|
||||
ClusterResourceTypeName = "ClusterResource"
|
||||
)
|
||||
|
||||
// clusterResourceDecoder is an implementation of the xdsclient.Decoder
|
||||
// interface for listener resources.
|
||||
type clusterResourceDecoder struct {
|
||||
bootstrapConfig *bootstrap.Config
|
||||
serverConfigs map[xdsclient.ServerConfig]*bootstrap.ServerConfig
|
||||
}
|
||||
|
||||
func (d *clusterResourceDecoder) Decode(resource *xdsclient.AnyProto, opts xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
|
||||
serverCfg, ok := d.serverConfigs[*opts.ServerConfig]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no server config found for {%+v}", opts.ServerConfig)
|
||||
}
|
||||
name, cluster, err := unmarshalClusterResource(resource.ToAny(), serverCfg)
|
||||
if name == "" {
|
||||
// Name is unset only when protobuf deserialization fails.
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
// Protobuf deserialization succeeded, but resource validation failed.
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ClusterResourceData{Resource: ClusterUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
// Perform extra validation here.
|
||||
if err := securityConfigValidator(d.bootstrapConfig, cluster.SecurityCfg); err != nil {
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ClusterResourceData{Resource: ClusterUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ClusterResourceData{Resource: cluster},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClusterResourceData wraps the configuration of a Cluster resource as received
|
||||
// from the management server.
|
||||
type ClusterResourceData struct {
|
||||
Resource ClusterUpdate
|
||||
}
|
||||
|
||||
// Equal returns true if other is equal to c.
|
||||
func (c *ClusterResourceData) Equal(other xdsclient.ResourceData) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(c.Bytes(), other.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the protobuf serialized bytes of the cluster resource proto.
|
||||
func (c *ClusterResourceData) Bytes() []byte {
|
||||
return c.Resource.Raw.GetValue()
|
||||
}
|
||||
|
||||
// ClusterWatcher wraps the callbacks to be invoked for different events
|
||||
// corresponding to the cluster resource being watched. gRFC A88 contains an
|
||||
// exhaustive list of what method is invoked under what conditions.
|
||||
type ClusterWatcher interface {
|
||||
// ResourceChanged indicates a new version of the resource is available.
|
||||
ResourceChanged(resource *ClusterUpdate, done func())
|
||||
|
||||
// ResourceError indicates an error occurred while trying to fetch or
|
||||
// decode the associated resource. The previous version of the resource
|
||||
// should be considered invalid.
|
||||
ResourceError(err error, done func())
|
||||
|
||||
// AmbientError indicates an error occurred after a resource has been
|
||||
// received that should not modify the use of that resource but may provide
|
||||
// useful information about the state of the XDSClient for debugging
|
||||
// purposes. The previous version of the resource should still be
|
||||
// considered valid.
|
||||
AmbientError(err error, done func())
|
||||
}
|
||||
|
||||
type delegatingClusterWatcher struct {
|
||||
watcher ClusterWatcher
|
||||
}
|
||||
|
||||
func (d *delegatingClusterWatcher) ResourceChanged(data xdsclient.ResourceData, onDone func()) {
|
||||
c := data.(*ClusterResourceData)
|
||||
d.watcher.ResourceChanged(&c.Resource, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingClusterWatcher) ResourceError(err error, onDone func()) {
|
||||
d.watcher.ResourceError(err, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingClusterWatcher) AmbientError(err error, onDone func()) {
|
||||
d.watcher.AmbientError(err, onDone)
|
||||
}
|
||||
|
||||
// WatchCluster uses xDS to discover the configuration associated with the
|
||||
// provided cluster resource name.
|
||||
func WatchCluster(p Producer, name string, w ClusterWatcher) (cancel func()) {
|
||||
return p.WatchResource(version.V3ClusterURL, name, &delegatingClusterWatcher{watcher: w})
|
||||
}
|
||||
|
||||
// NewClusterResourceTypeDecoder returns a xdsclient.Decoder that wraps
|
||||
// the xdsresource.clusterType.
|
||||
func NewClusterResourceTypeDecoder(bc *bootstrap.Config, gServerCfgMap map[xdsclient.ServerConfig]*bootstrap.ServerConfig) xdsclient.Decoder {
|
||||
return &clusterResourceDecoder{bootstrapConfig: bc, serverConfigs: gServerCfgMap}
|
||||
}
|
||||
126
internal/xds/xdsclient/xdsresource/endpoints_resource_type.go
Normal file
126
internal/xds/xdsclient/xdsresource/endpoints_resource_type.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdsclient "google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// EndpointsResourceTypeName represents the transport agnostic name for the
|
||||
// endpoint resource.
|
||||
EndpointsResourceTypeName = "EndpointsResource"
|
||||
)
|
||||
|
||||
// endpointsResourceDecoder is an implementation of the xdsclient.Decoder
|
||||
// interface for endpoints resources.
|
||||
type endpointsResourceDecoder struct {
|
||||
bootstrapConfig *bootstrap.Config
|
||||
}
|
||||
|
||||
func (d *endpointsResourceDecoder) Decode(resource *xdsclient.AnyProto, _ xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
|
||||
name, endpoints, err := unmarshalEndpointsResource(resource.ToAny())
|
||||
if name == "" {
|
||||
// Name is unset only when protobuf deserialization fails.
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
// Protobuf deserialization succeeded, but resource validation failed.
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ListenerResourceData{Resource: ListenerUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &EndpointsResourceData{Resource: endpoints},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EndpointsResourceData is an implementation of the xdsclient.ResourceData
|
||||
// interface for endpoints resources.
|
||||
type EndpointsResourceData struct {
|
||||
Resource EndpointsUpdate
|
||||
}
|
||||
|
||||
// Equal returns true if other is equal to e.
|
||||
func (e *EndpointsResourceData) Equal(other xdsclient.ResourceData) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(e.Bytes(), other.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the protobuf serialized bytes of the listener resource proto.
|
||||
func (e *EndpointsResourceData) Bytes() []byte {
|
||||
return e.Resource.Raw.GetValue()
|
||||
}
|
||||
|
||||
// EndpointsWatcher wraps the callbacks to be invoked for different
|
||||
// events corresponding to the endpoints resource being watched. gRFC A88
|
||||
// contains an exhaustive list of what method is invoked under what conditions.
|
||||
type EndpointsWatcher interface {
|
||||
// ResourceChanged indicates a new version of the resource is available.
|
||||
ResourceChanged(resource *EndpointsUpdate, done func())
|
||||
|
||||
// ResourceError indicates an error occurred while trying to fetch or
|
||||
// decode the associated resource. The previous version of the resource
|
||||
// should be considered invalid.
|
||||
ResourceError(err error, done func())
|
||||
|
||||
// AmbientError indicates an error occurred after a resource has been
|
||||
// received that should not modify the use of that resource but may provide
|
||||
// useful information about the state of the XDSClient for debugging
|
||||
// purposes. The previous version of the resource should still be
|
||||
// considered valid.
|
||||
AmbientError(err error, done func())
|
||||
}
|
||||
|
||||
type delegatingEndpointsWatcher struct {
|
||||
watcher EndpointsWatcher
|
||||
}
|
||||
|
||||
func (d *delegatingEndpointsWatcher) ResourceChanged(data xdsclient.ResourceData, onDone func()) {
|
||||
e := data.(*EndpointsResourceData)
|
||||
d.watcher.ResourceChanged(&e.Resource, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingEndpointsWatcher) ResourceError(err error, onDone func()) {
|
||||
d.watcher.ResourceError(err, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingEndpointsWatcher) AmbientError(err error, onDone func()) {
|
||||
d.watcher.AmbientError(err, onDone)
|
||||
}
|
||||
|
||||
// WatchEndpoints uses xDS to discover the configuration associated with the
|
||||
// provided endpoints resource name.
|
||||
func WatchEndpoints(p Producer, name string, w EndpointsWatcher) (cancel func()) {
|
||||
return p.WatchResource(version.V3EndpointsURL, name, &delegatingEndpointsWatcher{watcher: w})
|
||||
}
|
||||
|
||||
// NewEndpointsResourceTypeDecoder returns a xdsclient.Decoder that wraps
|
||||
// the xdsresource.endpointsType.
|
||||
func NewEndpointsResourceTypeDecoder(bc *bootstrap.Config) xdsclient.Decoder {
|
||||
return &endpointsResourceDecoder{bootstrapConfig: bc}
|
||||
}
|
||||
79
internal/xds/xdsclient/xdsresource/errors.go
Normal file
79
internal/xds/xdsclient/xdsresource/errors.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrorType is the type of the error that the watcher will receive from the xds
|
||||
// client.
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
// ErrorTypeUnknown indicates the error doesn't have a specific type. It is
|
||||
// the default value, and is returned if the error is not an xds error.
|
||||
ErrorTypeUnknown ErrorType = iota
|
||||
// ErrorTypeConnection indicates a connection error from the gRPC client.
|
||||
ErrorTypeConnection
|
||||
// ErrorTypeResourceNotFound indicates a resource is not found from the xds
|
||||
// response. It's typically returned if the resource is removed in the xds
|
||||
// server.
|
||||
ErrorTypeResourceNotFound
|
||||
// ErrorTypeResourceTypeUnsupported indicates the receipt of a message from
|
||||
// the management server with resources of an unsupported resource type.
|
||||
ErrorTypeResourceTypeUnsupported
|
||||
// ErrTypeStreamFailedAfterRecv indicates an ADS stream error, after
|
||||
// successful receipt of at least one message from the server.
|
||||
ErrTypeStreamFailedAfterRecv
|
||||
// ErrorTypeNACKed indicates that configuration provided by the xDS management
|
||||
// server was NACKed.
|
||||
ErrorTypeNACKed
|
||||
)
|
||||
|
||||
type xdsClientError struct {
|
||||
t ErrorType
|
||||
desc string
|
||||
}
|
||||
|
||||
func (e *xdsClientError) Error() string {
|
||||
return e.desc
|
||||
}
|
||||
|
||||
// NewErrorf creates an xDS client error. The callbacks are called with this
|
||||
// error, to pass additional information about the error.
|
||||
func NewErrorf(t ErrorType, format string, args ...any) error {
|
||||
return &xdsClientError{t: t, desc: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// NewError creates an xDS client error. The callbacks are called with this
|
||||
// error, to pass additional information about the error.
|
||||
func NewError(t ErrorType, message string) error {
|
||||
return NewErrorf(t, "%s", message)
|
||||
}
|
||||
|
||||
// ErrType returns the error's type.
|
||||
func ErrType(err error) ErrorType {
|
||||
var xe *xdsClientError
|
||||
if errors.As(err, &xe) {
|
||||
return xe.t
|
||||
}
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
180
internal/xds/xdsclient/xdsresource/filter_chain.go
Normal file
180
internal/xds/xdsclient/xdsresource/filter_chain.go
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
)
|
||||
|
||||
// NetworkFilterChainMap contains the match configuration for network filter
|
||||
// chains on the server side. It is a multi-level map structure to facilitate
|
||||
// efficient matching of incoming connections based on destination IP, source
|
||||
// {type, IP and port}.
|
||||
type NetworkFilterChainMap struct {
|
||||
// DstPrefixes is the list of destination prefix entries to match on.
|
||||
DstPrefixes []DestinationPrefixEntry
|
||||
}
|
||||
|
||||
// DestinationPrefixEntry contains a destination prefix entry and the associated
|
||||
// source type matchers.
|
||||
type DestinationPrefixEntry struct {
|
||||
// Prefix is the destination IP prefix.
|
||||
Prefix netip.Prefix
|
||||
// SourceTypeArr contains the source type matchers. The supported source
|
||||
// types and their associated indices in the array are:
|
||||
// - 0: Any: matches connection attempts from any source.
|
||||
// - 1: SameOrLoopback: matches connection attempts from the same host.
|
||||
// - 2: External: matches connection attempts from a different host.
|
||||
SourceTypeArr [3]SourcePrefixes
|
||||
}
|
||||
|
||||
// SourcePrefixes contains a list of source prefix entries to match on.
|
||||
type SourcePrefixes struct {
|
||||
// Entries is the list of source prefix entries.
|
||||
Entries []SourcePrefixEntry
|
||||
}
|
||||
|
||||
// SourcePrefixEntry contains a source prefix entry and the associated source
|
||||
// port matchers.
|
||||
type SourcePrefixEntry struct {
|
||||
// Prefix is the source IP prefix.
|
||||
Prefix netip.Prefix
|
||||
// PortMap contains the matchers for source ports.
|
||||
PortMap map[int]NetworkFilterChainConfig
|
||||
}
|
||||
|
||||
// NetworkFilterChainConfig contains the configuration for a network filter
|
||||
// chain on the server side. The only support network filter is the HTTP
|
||||
// connection manager.
|
||||
type NetworkFilterChainConfig struct {
|
||||
// SecurityCfg contains transport socket security configuration.
|
||||
SecurityCfg *SecurityConfig
|
||||
// HTTPConnMgr contains the HTTP connection manager configuration.
|
||||
HTTPConnMgr *HTTPConnectionManagerConfig
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the NetworkFilterChainConfig contains no
|
||||
// configuration.
|
||||
func (n NetworkFilterChainConfig) IsEmpty() bool {
|
||||
return n.SecurityCfg == nil && n.HTTPConnMgr == nil
|
||||
}
|
||||
|
||||
func processNetworkFilters(filters []*v3listenerpb.Filter, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (*HTTPConnectionManagerConfig, error) {
|
||||
hcmConfig := &HTTPConnectionManagerConfig{}
|
||||
seenNames := make(map[string]bool, len(filters))
|
||||
seenHCM := false
|
||||
for _, filter := range filters {
|
||||
name := filter.GetName()
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("network filters {%+v} is missing name field in filter: {%+v}", filters, filter)
|
||||
}
|
||||
if seenNames[name] {
|
||||
return nil, fmt.Errorf("network filters {%+v} has duplicate filter name %q", filters, name)
|
||||
}
|
||||
seenNames[name] = true
|
||||
|
||||
// Network filters have a oneof field named `config_type` where we
|
||||
// only support `TypedConfig` variant.
|
||||
switch typ := filter.GetConfigType().(type) {
|
||||
case *v3listenerpb.Filter_TypedConfig:
|
||||
// The typed_config field has an `anypb.Any` proto which could
|
||||
// directly contain the serialized bytes of the actual filter
|
||||
// configuration, or it could be encoded as a `TypedStruct`.
|
||||
// TODO: Add support for `TypedStruct`.
|
||||
tc := filter.GetTypedConfig()
|
||||
|
||||
// The only network filter that we currently support is the v3
|
||||
// HttpConnectionManager. So, we can directly check the type_url
|
||||
// and unmarshal the config.
|
||||
// TODO: Implement a registry of supported network filters (like
|
||||
// we have for HTTP filters), when we have to support network
|
||||
// filters other than HttpConnectionManager.
|
||||
if tc.GetTypeUrl() != version.V3HTTPConnManagerURL {
|
||||
return nil, fmt.Errorf("network filters {%+v} has unsupported network filter %q in filter {%+v}", filters, tc.GetTypeUrl(), filter)
|
||||
}
|
||||
hcm := &v3httppb.HttpConnectionManager{}
|
||||
if err := tc.UnmarshalTo(hcm); err != nil {
|
||||
return nil, fmt.Errorf("network filters {%+v} failed unmarshalling of network filter {%+v}: %v", filters, filter, err)
|
||||
}
|
||||
// "Any filters after HttpConnectionManager should be ignored during
|
||||
// connection processing but still be considered for validity.
|
||||
// HTTPConnectionManager must have valid http_filters." - A36
|
||||
filters, err := processHTTPFilters(hcm.GetHttpFilters(), true, bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network filters {%+v} had invalid server side HTTP Filters {%+v}: %v", filters, hcm.GetHttpFilters(), err)
|
||||
}
|
||||
if !seenHCM {
|
||||
// Validate for RBAC in only the HCM that will be used, since this isn't a logical validation failure,
|
||||
// it's simply a validation to support RBAC HTTP Filter.
|
||||
// "HttpConnectionManager.xff_num_trusted_hops must be unset or zero and
|
||||
// HttpConnectionManager.original_ip_detection_extensions must be empty. If
|
||||
// either field has an incorrect value, the Listener must be NACKed." - A41
|
||||
if hcm.XffNumTrustedHops != 0 {
|
||||
return nil, fmt.Errorf("xff_num_trusted_hops must be unset or zero %+v", hcm)
|
||||
}
|
||||
if len(hcm.OriginalIpDetectionExtensions) != 0 {
|
||||
return nil, fmt.Errorf("original_ip_detection_extensions must be empty %+v", hcm)
|
||||
}
|
||||
|
||||
// TODO: Implement terminal filter logic, as per A36.
|
||||
hcmConfig.HTTPFilters = filters
|
||||
seenHCM = true
|
||||
switch hcm.RouteSpecifier.(type) {
|
||||
case *v3httppb.HttpConnectionManager_Rds:
|
||||
if hcm.GetRds().GetConfigSource().GetAds() == nil {
|
||||
return nil, fmt.Errorf("ConfigSource is not ADS: %+v", hcm)
|
||||
}
|
||||
name := hcm.GetRds().GetRouteConfigName()
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("empty route_config_name: %+v", hcm)
|
||||
}
|
||||
hcmConfig.RouteConfigName = name
|
||||
case *v3httppb.HttpConnectionManager_RouteConfig:
|
||||
// "RouteConfiguration validation logic inherits all
|
||||
// previous validations made for client-side usage as RDS
|
||||
// does not distinguish between client-side and
|
||||
// server-side." - A36
|
||||
// Can specify v3 here, as will never get to this function
|
||||
// if v2.
|
||||
routeU, err := generateRDSUpdateFromRouteConfiguration(hcm.GetRouteConfig(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse inline RDS resp: %v", err)
|
||||
}
|
||||
hcmConfig.InlineRouteConfig = &routeU
|
||||
case nil:
|
||||
return nil, fmt.Errorf("no RouteSpecifier: %+v", hcm)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %T for RouteSpecifier", hcm.RouteSpecifier)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("network filters {%+v} has unsupported config_type %T in filter %s", filters, typ, filter.GetName())
|
||||
}
|
||||
}
|
||||
if !seenHCM {
|
||||
return nil, fmt.Errorf("network filters {%+v} missing HttpConnectionManager filter", filters)
|
||||
}
|
||||
return hcmConfig, nil
|
||||
}
|
||||
2568
internal/xds/xdsclient/xdsresource/filter_chain_test.go
Normal file
2568
internal/xds/xdsclient/xdsresource/filter_chain_test.go
Normal file
File diff suppressed because it is too large
Load Diff
185
internal/xds/xdsclient/xdsresource/listener_resource_type.go
Normal file
185
internal/xds/xdsclient/xdsresource/listener_resource_type.go
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
)
|
||||
|
||||
// ListenerResourceTypeName is a human friendly name for the listener resource.
|
||||
const ListenerResourceTypeName = "ListenerResource"
|
||||
|
||||
// listenerResourceDecoder is an implementation of the xdsclient.Decoder
|
||||
// interface for listener resources.
|
||||
type listenerResourceDecoder struct {
|
||||
bootstrapConfig *bootstrap.Config
|
||||
serverConfigs map[xdsclient.ServerConfig]*bootstrap.ServerConfig
|
||||
}
|
||||
|
||||
func (d *listenerResourceDecoder) Decode(resource *xdsclient.AnyProto, opts xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
|
||||
var serverCfg *bootstrap.ServerConfig
|
||||
if opts.ServerConfig != nil {
|
||||
serverCfg = d.serverConfigs[*opts.ServerConfig]
|
||||
}
|
||||
name, listener, err := unmarshalListenerResource(resource.ToAny(), d.bootstrapConfig, serverCfg)
|
||||
if name == "" {
|
||||
// Name is unset only when protobuf deserialization fails.
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
// Protobuf deserialization succeeded, but resource validation failed.
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ListenerResourceData{Resource: ListenerUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
// Perform extra validation here.
|
||||
if err := listenerValidator(d.bootstrapConfig, listener); err != nil {
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ListenerResourceData{Resource: ListenerUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &ListenerResourceData{Resource: listener},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func securityConfigValidator(bc *bootstrap.Config, sc *SecurityConfig) error {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
if sc.IdentityInstanceName != "" {
|
||||
if _, ok := bc.CertProviderConfigs()[sc.IdentityInstanceName]; !ok {
|
||||
return fmt.Errorf("identity certificate provider instance name %q missing in bootstrap configuration", sc.IdentityInstanceName)
|
||||
}
|
||||
}
|
||||
if sc.RootInstanceName != "" {
|
||||
if _, ok := bc.CertProviderConfigs()[sc.RootInstanceName]; !ok {
|
||||
return fmt.Errorf("root certificate provider instance name %q missing in bootstrap configuration", sc.RootInstanceName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listenerValidator(bc *bootstrap.Config, lis ListenerUpdate) error {
|
||||
// Validate Filter Chains.
|
||||
validateFC := func(fc *NetworkFilterChainConfig) error {
|
||||
if fc == nil {
|
||||
return nil
|
||||
}
|
||||
return securityConfigValidator(bc, fc.SecurityCfg)
|
||||
}
|
||||
|
||||
if lis.TCPListener == nil {
|
||||
return nil
|
||||
}
|
||||
if err := validateFC(&lis.TCPListener.DefaultFilterChain); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dst := range lis.TCPListener.FilterChains.DstPrefixes {
|
||||
for _, srcType := range dst.SourceTypeArr {
|
||||
if len(srcType.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, src := range srcType.Entries {
|
||||
for _, fc := range src.PortMap {
|
||||
if err := validateFC(&fc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenerResourceData is an implementation of the xdsclient.ResourceData
|
||||
// interface for listener resources.
|
||||
type ListenerResourceData struct {
|
||||
Resource ListenerUpdate
|
||||
}
|
||||
|
||||
// Equal returns true if other is equal to l.
|
||||
func (l *ListenerResourceData) Equal(other xdsclient.ResourceData) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(l.Bytes(), other.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the protobuf serialized bytes of the listener resource proto.
|
||||
func (l *ListenerResourceData) Bytes() []byte {
|
||||
return l.Resource.Raw.GetValue()
|
||||
}
|
||||
|
||||
// ListenerWatcher wraps the callbacks to be invoked for different
|
||||
// events corresponding to the listener resource being watched. gRFC A88
|
||||
// contains an exhaustive list of what method is invoked under what conditions.
|
||||
type ListenerWatcher interface {
|
||||
// ResourceChanged indicates a new version of the resource is available.
|
||||
ResourceChanged(resource *ListenerUpdate, done func())
|
||||
|
||||
// ResourceError indicates an error occurred while trying to fetch or
|
||||
// decode the associated resource. The previous version of the resource
|
||||
// should be considered invalid.
|
||||
ResourceError(err error, done func())
|
||||
|
||||
// AmbientError indicates an error occurred after a resource has been
|
||||
// received that should not modify the use of that resource but may provide
|
||||
// useful information about the state of the XDSClient for debugging
|
||||
// purposes. The previous version of the resource should still be
|
||||
// considered valid.
|
||||
AmbientError(err error, done func())
|
||||
}
|
||||
|
||||
type delegatingListenerWatcher struct {
|
||||
watcher ListenerWatcher
|
||||
}
|
||||
|
||||
func (d *delegatingListenerWatcher) ResourceChanged(data xdsclient.ResourceData, onDone func()) {
|
||||
l := data.(*ListenerResourceData)
|
||||
d.watcher.ResourceChanged(&l.Resource, onDone)
|
||||
}
|
||||
func (d *delegatingListenerWatcher) ResourceError(err error, onDone func()) {
|
||||
d.watcher.ResourceError(err, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingListenerWatcher) AmbientError(err error, onDone func()) {
|
||||
d.watcher.AmbientError(err, onDone)
|
||||
}
|
||||
|
||||
// WatchListener uses xDS to discover the configuration associated with the
|
||||
// provided listener resource name.
|
||||
func WatchListener(p Producer, name string, w ListenerWatcher) (cancel func()) {
|
||||
return p.WatchResource(version.V3ListenerURL, name, &delegatingListenerWatcher{watcher: w})
|
||||
}
|
||||
|
||||
// NewListenerResourceTypeDecoder returns a xdsclient.Decoder that wraps
|
||||
// the xdsresource.listenerType.
|
||||
func NewListenerResourceTypeDecoder(bc *bootstrap.Config, gServerCfgMap map[xdsclient.ServerConfig]*bootstrap.ServerConfig) xdsclient.Decoder {
|
||||
return &listenerResourceDecoder{bootstrapConfig: bc, serverConfigs: gServerCfgMap}
|
||||
}
|
||||
28
internal/xds/xdsclient/xdsresource/logging.go
Normal file
28
internal/xds/xdsclient/xdsresource/logging.go
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
const prefix = "[xds-resource] "
|
||||
|
||||
var logger = internalgrpclog.NewPrefixLogger(grpclog.Component("xds"), prefix)
|
||||
231
internal/xds/xdsclient/xdsresource/matcher.go
Normal file
231
internal/xds/xdsclient/xdsresource/matcher.go
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
rand "math/rand/v2"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// RouteToMatcher converts a route to a Matcher to match incoming RPC's against.
|
||||
//
|
||||
// Only expected to be called on a Route that passed validation checks by the
|
||||
// xDS client.
|
||||
func RouteToMatcher(r *Route) *CompositeMatcher {
|
||||
var pm pathMatcher
|
||||
switch {
|
||||
case r.Regex != nil:
|
||||
pm = newPathRegexMatcher(r.Regex)
|
||||
case r.Path != nil:
|
||||
pm = newPathExactMatcher(*r.Path, r.CaseInsensitive)
|
||||
case r.Prefix != nil:
|
||||
pm = newPathPrefixMatcher(*r.Prefix, r.CaseInsensitive)
|
||||
default:
|
||||
panic("illegal route: missing path_matcher")
|
||||
}
|
||||
|
||||
headerMatchers := make([]matcher.HeaderMatcher, 0, len(r.Headers))
|
||||
for _, h := range r.Headers {
|
||||
var matcherT matcher.HeaderMatcher
|
||||
invert := h.InvertMatch != nil && *h.InvertMatch
|
||||
switch {
|
||||
case h.RegexMatch != nil:
|
||||
matcherT = matcher.NewHeaderRegexMatcher(h.Name, h.RegexMatch, invert)
|
||||
case h.RangeMatch != nil:
|
||||
matcherT = matcher.NewHeaderRangeMatcher(h.Name, h.RangeMatch.Start, h.RangeMatch.End, invert)
|
||||
case h.PresentMatch != nil:
|
||||
matcherT = matcher.NewHeaderPresentMatcher(h.Name, *h.PresentMatch, invert)
|
||||
case h.StringMatch != nil:
|
||||
matcherT = matcher.NewHeaderStringMatcher(h.Name, *h.StringMatch, invert)
|
||||
default:
|
||||
panic("illegal route: missing header_match_specifier")
|
||||
}
|
||||
headerMatchers = append(headerMatchers, matcherT)
|
||||
}
|
||||
|
||||
var fractionMatcher *fractionMatcher
|
||||
if r.Fraction != nil {
|
||||
fractionMatcher = newFractionMatcher(*r.Fraction)
|
||||
}
|
||||
return newCompositeMatcher(pm, headerMatchers, fractionMatcher)
|
||||
}
|
||||
|
||||
// CompositeMatcher is a matcher that holds onto many matchers and aggregates
|
||||
// the matching results.
|
||||
type CompositeMatcher struct {
|
||||
pm pathMatcher
|
||||
hms []matcher.HeaderMatcher
|
||||
fm *fractionMatcher
|
||||
}
|
||||
|
||||
func newCompositeMatcher(pm pathMatcher, hms []matcher.HeaderMatcher, fm *fractionMatcher) *CompositeMatcher {
|
||||
return &CompositeMatcher{pm: pm, hms: hms, fm: fm}
|
||||
}
|
||||
|
||||
// Match returns true if all matchers return true.
|
||||
func (a *CompositeMatcher) Match(method string, md metadata.MD) bool {
|
||||
if a.pm != nil && !a.pm.match(method) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range a.hms {
|
||||
if !m.Match(md) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if a.fm != nil && !a.fm.match() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *CompositeMatcher) String() string {
|
||||
var ret string
|
||||
if a.pm != nil {
|
||||
ret += a.pm.String()
|
||||
}
|
||||
for _, m := range a.hms {
|
||||
ret += m.String()
|
||||
}
|
||||
if a.fm != nil {
|
||||
ret += a.fm.String()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type fractionMatcher struct {
|
||||
fraction int64 // real fraction is fraction/1,000,000.
|
||||
}
|
||||
|
||||
func newFractionMatcher(fraction uint32) *fractionMatcher {
|
||||
return &fractionMatcher{fraction: int64(fraction)}
|
||||
}
|
||||
|
||||
// RandInt64n overwrites rand for control in tests.
|
||||
var RandInt64n = rand.Int64N
|
||||
|
||||
func (fm *fractionMatcher) match() bool {
|
||||
t := RandInt64n(1000000)
|
||||
return t <= fm.fraction
|
||||
}
|
||||
|
||||
func (fm *fractionMatcher) String() string {
|
||||
return fmt.Sprintf("fraction:%v", fm.fraction)
|
||||
}
|
||||
|
||||
type domainMatchType int
|
||||
|
||||
const (
|
||||
domainMatchTypeInvalid domainMatchType = iota
|
||||
domainMatchTypeUniversal
|
||||
domainMatchTypePrefix
|
||||
domainMatchTypeSuffix
|
||||
domainMatchTypeExact
|
||||
)
|
||||
|
||||
// Exact > Suffix > Prefix > Universal > Invalid.
|
||||
func (t domainMatchType) betterThan(b domainMatchType) bool {
|
||||
return t > b
|
||||
}
|
||||
|
||||
func matchTypeForDomain(d string) domainMatchType {
|
||||
if d == "" {
|
||||
return domainMatchTypeInvalid
|
||||
}
|
||||
if d == "*" {
|
||||
return domainMatchTypeUniversal
|
||||
}
|
||||
if strings.HasPrefix(d, "*") {
|
||||
return domainMatchTypeSuffix
|
||||
}
|
||||
if strings.HasSuffix(d, "*") {
|
||||
return domainMatchTypePrefix
|
||||
}
|
||||
if strings.Contains(d, "*") {
|
||||
return domainMatchTypeInvalid
|
||||
}
|
||||
return domainMatchTypeExact
|
||||
}
|
||||
|
||||
func match(domain, host string) (domainMatchType, bool) {
|
||||
switch typ := matchTypeForDomain(domain); typ {
|
||||
case domainMatchTypeInvalid:
|
||||
return typ, false
|
||||
case domainMatchTypeUniversal:
|
||||
return typ, true
|
||||
case domainMatchTypePrefix:
|
||||
// abc.*
|
||||
return typ, strings.HasPrefix(host, strings.TrimSuffix(domain, "*"))
|
||||
case domainMatchTypeSuffix:
|
||||
// *.123
|
||||
return typ, strings.HasSuffix(host, strings.TrimPrefix(domain, "*"))
|
||||
case domainMatchTypeExact:
|
||||
return typ, domain == host
|
||||
default:
|
||||
return domainMatchTypeInvalid, false
|
||||
}
|
||||
}
|
||||
|
||||
// FindBestMatchingVirtualHost returns the virtual host whose domains field best
|
||||
// matches host
|
||||
//
|
||||
// The domains field support 4 different matching pattern types:
|
||||
//
|
||||
// - Exact match
|
||||
// - Suffix match (e.g. “*ABC”)
|
||||
// - Prefix match (e.g. “ABC*)
|
||||
// - Universal match (e.g. “*”)
|
||||
//
|
||||
// The best match is defined as:
|
||||
// - A match is better if it’s matching pattern type is better.
|
||||
// * Exact match > suffix match > prefix match > universal match.
|
||||
//
|
||||
// - If two matches are of the same pattern type, the longer match is
|
||||
// better.
|
||||
// * This is to compare the length of the matching pattern, e.g. “*ABCDE” >
|
||||
// “*ABC”
|
||||
func FindBestMatchingVirtualHost(host string, vHosts []*VirtualHost) *VirtualHost { // Maybe move this crap to client
|
||||
var (
|
||||
matchVh *VirtualHost
|
||||
matchType = domainMatchTypeInvalid
|
||||
matchLen int
|
||||
)
|
||||
for _, vh := range vHosts {
|
||||
for _, domain := range vh.Domains {
|
||||
typ, matched := match(domain, host)
|
||||
if typ == domainMatchTypeInvalid {
|
||||
// The rds response is invalid.
|
||||
return nil
|
||||
}
|
||||
if matchType.betterThan(typ) || matchType == typ && matchLen >= len(domain) || !matched {
|
||||
// The previous match has better type, or the previous match has
|
||||
// better length, or this domain isn't a match.
|
||||
continue
|
||||
}
|
||||
matchVh = vh
|
||||
matchType = typ
|
||||
matchLen = len(domain)
|
||||
}
|
||||
}
|
||||
return matchVh
|
||||
}
|
||||
100
internal/xds/xdsclient/xdsresource/matcher_path.go
Normal file
100
internal/xds/xdsclient/xdsresource/matcher_path.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type pathMatcher interface {
|
||||
match(path string) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type pathExactMatcher struct {
|
||||
// fullPath is all upper case if caseInsensitive is true.
|
||||
fullPath string
|
||||
caseInsensitive bool
|
||||
}
|
||||
|
||||
func newPathExactMatcher(p string, caseInsensitive bool) *pathExactMatcher {
|
||||
ret := &pathExactMatcher{
|
||||
fullPath: p,
|
||||
caseInsensitive: caseInsensitive,
|
||||
}
|
||||
if caseInsensitive {
|
||||
ret.fullPath = strings.ToUpper(p)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pem *pathExactMatcher) match(path string) bool {
|
||||
if pem.caseInsensitive {
|
||||
return pem.fullPath == strings.ToUpper(path)
|
||||
}
|
||||
return pem.fullPath == path
|
||||
}
|
||||
|
||||
func (pem *pathExactMatcher) String() string {
|
||||
return "pathExact:" + pem.fullPath
|
||||
}
|
||||
|
||||
type pathPrefixMatcher struct {
|
||||
// prefix is all upper case if caseInsensitive is true.
|
||||
prefix string
|
||||
caseInsensitive bool
|
||||
}
|
||||
|
||||
func newPathPrefixMatcher(p string, caseInsensitive bool) *pathPrefixMatcher {
|
||||
ret := &pathPrefixMatcher{
|
||||
prefix: p,
|
||||
caseInsensitive: caseInsensitive,
|
||||
}
|
||||
if caseInsensitive {
|
||||
ret.prefix = strings.ToUpper(p)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ppm *pathPrefixMatcher) match(path string) bool {
|
||||
if ppm.caseInsensitive {
|
||||
return strings.HasPrefix(strings.ToUpper(path), ppm.prefix)
|
||||
}
|
||||
return strings.HasPrefix(path, ppm.prefix)
|
||||
}
|
||||
|
||||
func (ppm *pathPrefixMatcher) String() string {
|
||||
return "pathPrefix:" + ppm.prefix
|
||||
}
|
||||
|
||||
type pathRegexMatcher struct {
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func newPathRegexMatcher(re *regexp.Regexp) *pathRegexMatcher {
|
||||
return &pathRegexMatcher{re: re}
|
||||
}
|
||||
|
||||
func (prm *pathRegexMatcher) match(path string) bool {
|
||||
return prm.re.MatchString(path)
|
||||
}
|
||||
|
||||
func (prm *pathRegexMatcher) String() string {
|
||||
return "pathRegex:" + prm.re.String()
|
||||
}
|
||||
93
internal/xds/xdsclient/xdsresource/matcher_path_test.go
Normal file
93
internal/xds/xdsclient/xdsresource/matcher_path_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (s) TestPathFullMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fullPath string
|
||||
caseInsensitive bool
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", fullPath: "/s/m", path: "/s/m", want: true},
|
||||
{name: "case insensitive match", fullPath: "/s/m", caseInsensitive: true, path: "/S/m", want: true},
|
||||
{name: "case insensitive match 2", fullPath: "/s/M", caseInsensitive: true, path: "/S/m", want: true},
|
||||
{name: "not match", fullPath: "/s/m", path: "/a/b", want: false},
|
||||
{name: "case insensitive not match", fullPath: "/s/m", caseInsensitive: true, path: "/a/b", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathExactMatcher(tt.fullPath, tt.caseInsensitive)
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.fullPath, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestPathPrefixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
caseInsensitive bool
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", prefix: "/s/", path: "/s/m", want: true},
|
||||
{name: "case insensitive match", prefix: "/s/", caseInsensitive: true, path: "/S/m", want: true},
|
||||
{name: "case insensitive match 2", prefix: "/S/", caseInsensitive: true, path: "/s/m", want: true},
|
||||
{name: "not match", prefix: "/s/", path: "/a/b", want: false},
|
||||
{name: "case insensitive not match", prefix: "/s/", caseInsensitive: true, path: "/a/b", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathPrefixMatcher(tt.prefix, tt.caseInsensitive)
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.prefix, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestPathRegexMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
regexPath string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", regexPath: "^/s+/m.*$", path: "/sss/me", want: true},
|
||||
{name: "not match", regexPath: "^/s+/m*$", path: "/sss/b", want: false},
|
||||
{name: "no match because only part of path matches with regex", regexPath: "^a+$", path: "ab", want: false},
|
||||
{name: "match because full path matches with regex", regexPath: "^a+$", path: "aa", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathRegexMatcher(regexp.MustCompile(tt.regexPath))
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.regexPath, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
244
internal/xds/xdsclient/xdsresource/matcher_test.go
Normal file
244
internal/xds/xdsclient/xdsresource/matcher_test.go
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
rand "math/rand/v2"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpcutil"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func (s) TestAndMatcherMatch(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
tests := []struct {
|
||||
name string
|
||||
pm pathMatcher
|
||||
hm matcher.HeaderMatcher
|
||||
info iresolver.RPCInfo
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "both_match",
|
||||
pm: newPathExactMatcher("/a/b", false),
|
||||
hm: matcher.NewHeaderExactMatcher("th", "tv", false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/a/b",
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both_match_with_path_case_insensitive",
|
||||
pm: newPathExactMatcher("/A/B", true),
|
||||
hm: matcher.NewHeaderExactMatcher("th", "tv", false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/a/b",
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only_one_match",
|
||||
pm: newPathExactMatcher("/a/b", false),
|
||||
hm: matcher.NewHeaderExactMatcher("th", "tv", false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/z/y",
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "both_not match",
|
||||
pm: newPathExactMatcher("/z/y", false),
|
||||
hm: matcher.NewHeaderExactMatcher("th", "abc", false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/a/b",
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "fake_header",
|
||||
pm: newPathPrefixMatcher("/", false),
|
||||
hm: matcher.NewHeaderExactMatcher("content-type", "fake", false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/a/b",
|
||||
Context: grpcutil.WithExtraMetadata(ctx, metadata.Pairs(
|
||||
"content-type", "fake",
|
||||
)),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "binary_header",
|
||||
pm: newPathPrefixMatcher("/", false),
|
||||
hm: matcher.NewHeaderPresentMatcher("t-bin", true, false),
|
||||
info: iresolver.RPCInfo{
|
||||
Method: "/a/b",
|
||||
Context: grpcutil.WithExtraMetadata(
|
||||
metadata.NewOutgoingContext(ctx, metadata.Pairs("t-bin", "123")), metadata.Pairs(
|
||||
"content-type", "fake",
|
||||
)),
|
||||
},
|
||||
// Shouldn't match binary header, even though it's in metadata.
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := newCompositeMatcher(tt.pm, []matcher.HeaderMatcher{tt.hm}, nil)
|
||||
md, _ := metadata.FromOutgoingContext(tt.info.Context)
|
||||
if extraMD, ok := grpcutil.ExtraMetadata(tt.info.Context); ok {
|
||||
md = metadata.Join(md, extraMD)
|
||||
for k := range md {
|
||||
if strings.HasSuffix(k, "-bin") {
|
||||
delete(md, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if got := a.Match(tt.info.Method, md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFractionMatcherMatch(t *testing.T) {
|
||||
const fraction = 500000
|
||||
fm := newFractionMatcher(fraction)
|
||||
defer func() {
|
||||
RandInt64n = rand.Int64N
|
||||
}()
|
||||
|
||||
// rand > fraction, should return false.
|
||||
RandInt64n = func(int64) int64 {
|
||||
return fraction + 1
|
||||
}
|
||||
if matched := fm.match(); matched {
|
||||
t.Errorf("match() = %v, want not match", matched)
|
||||
}
|
||||
|
||||
// rand == fraction, should return true.
|
||||
RandInt64n = func(int64) int64 {
|
||||
return fraction
|
||||
}
|
||||
if matched := fm.match(); !matched {
|
||||
t.Errorf("match() = %v, want match", matched)
|
||||
}
|
||||
|
||||
// rand < fraction, should return true.
|
||||
RandInt64n = func(int64) int64 {
|
||||
return fraction - 1
|
||||
}
|
||||
if matched := fm.match(); !matched {
|
||||
t.Errorf("match() = %v, want match", matched)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestMatchTypeForDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
d string
|
||||
want domainMatchType
|
||||
}{
|
||||
{d: "", want: domainMatchTypeInvalid},
|
||||
{d: "*", want: domainMatchTypeUniversal},
|
||||
{d: "bar.*", want: domainMatchTypePrefix},
|
||||
{d: "*.abc.com", want: domainMatchTypeSuffix},
|
||||
{d: "foo.bar.com", want: domainMatchTypeExact},
|
||||
{d: "foo.*.com", want: domainMatchTypeInvalid},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := matchTypeForDomain(tt.d); got != tt.want {
|
||||
t.Errorf("matchTypeForDomain(%q) = %v, want %v", tt.d, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
host string
|
||||
wantTyp domainMatchType
|
||||
wantMatched bool
|
||||
}{
|
||||
{name: "invalid-empty", domain: "", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
|
||||
{name: "invalid", domain: "a.*.b", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
|
||||
{name: "universal", domain: "*", host: "abc.com", wantTyp: domainMatchTypeUniversal, wantMatched: true},
|
||||
{name: "prefix-match", domain: "abc.*", host: "abc.123", wantTyp: domainMatchTypePrefix, wantMatched: true},
|
||||
{name: "prefix-no-match", domain: "abc.*", host: "abcd.123", wantTyp: domainMatchTypePrefix, wantMatched: false},
|
||||
{name: "suffix-match", domain: "*.123", host: "abc.123", wantTyp: domainMatchTypeSuffix, wantMatched: true},
|
||||
{name: "suffix-no-match", domain: "*.123", host: "abc.1234", wantTyp: domainMatchTypeSuffix, wantMatched: false},
|
||||
{name: "exact-match", domain: "foo.bar", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: true},
|
||||
{name: "exact-no-match", domain: "foo.bar.com", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotTyp, gotMatched := match(tt.domain, tt.host); gotTyp != tt.wantTyp || gotMatched != tt.wantMatched {
|
||||
t.Errorf("match() = %v, %v, want %v, %v", gotTyp, gotMatched, tt.wantTyp, tt.wantMatched)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFindBestMatchingVirtualHost(t *testing.T) {
|
||||
var (
|
||||
oneExactMatch = &VirtualHost{Domains: []string{"foo.bar.com"}}
|
||||
oneSuffixMatch = &VirtualHost{Domains: []string{"*.bar.com"}}
|
||||
onePrefixMatch = &VirtualHost{Domains: []string{"foo.bar.*"}}
|
||||
oneUniversalMatch = &VirtualHost{Domains: []string{"*"}}
|
||||
longExactMatch = &VirtualHost{Domains: []string{"v2.foo.bar.com"}}
|
||||
multipleMatch = &VirtualHost{Domains: []string{"pi.foo.bar.com", "314.*", "*.159"}}
|
||||
vhs = []*VirtualHost{oneExactMatch, oneSuffixMatch, onePrefixMatch, oneUniversalMatch, longExactMatch, multipleMatch}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
vHosts []*VirtualHost
|
||||
want *VirtualHost
|
||||
}{
|
||||
{name: "exact-match", host: "foo.bar.com", vHosts: vhs, want: oneExactMatch},
|
||||
{name: "suffix-match", host: "123.bar.com", vHosts: vhs, want: oneSuffixMatch},
|
||||
{name: "prefix-match", host: "foo.bar.org", vHosts: vhs, want: onePrefixMatch},
|
||||
{name: "universal-match", host: "abc.123", vHosts: vhs, want: oneUniversalMatch},
|
||||
{name: "long-exact-match", host: "v2.foo.bar.com", vHosts: vhs, want: longExactMatch},
|
||||
// Matches suffix "*.bar.com" and exact "pi.foo.bar.com". Takes exact.
|
||||
{name: "multiple-match-exact", host: "pi.foo.bar.com", vHosts: vhs, want: multipleMatch},
|
||||
// Matches suffix "*.159" and prefix "foo.bar.*". Takes suffix.
|
||||
{name: "multiple-match-suffix", host: "foo.bar.159", vHosts: vhs, want: multipleMatch},
|
||||
// Matches suffix "*.bar.com" and prefix "314.*". Takes suffix.
|
||||
{name: "multiple-match-prefix", host: "314.bar.com", vHosts: vhs, want: oneSuffixMatch},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := FindBestMatchingVirtualHost(tt.host, tt.vHosts); !cmp.Equal(got, tt.want, cmp.Comparer(proto.Equal)) {
|
||||
t.Errorf("FindBestMatchingxdsclient.VirtualHost() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
164
internal/xds/xdsclient/xdsresource/metadata.go
Normal file
164
internal/xds/xdsclient/xdsresource/metadata.go
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if envconfig.XDSHTTPConnectEnabled {
|
||||
registerMetadataConverter(version.V3AddressURL, proxyAddressConverter{})
|
||||
}
|
||||
if envconfig.GCPAuthenticationFilterEnabled {
|
||||
registerMetadataConverter(version.V3AudienceURL, audienceConverter{})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// metadataRegistry is a map from proto type to metadataConverter.
|
||||
metadataRegistry = make(map[string]metadataConverter)
|
||||
)
|
||||
|
||||
// metadataConverter converts xds metadata entries in
|
||||
// Metadata.typed_filter_metadata into an internal form with the fields relevant
|
||||
// to gRPC.
|
||||
type metadataConverter interface {
|
||||
// convert parses the Any proto into a concrete struct.
|
||||
convert(*anypb.Any) (any, error)
|
||||
}
|
||||
|
||||
// registerMetadataConverter registers the converter to the map keyed on a proto
|
||||
// type_url. Must be called at init time. Not thread safe.
|
||||
func registerMetadataConverter(protoType string, c metadataConverter) {
|
||||
metadataRegistry[protoType] = c
|
||||
}
|
||||
|
||||
// metadataConverterForType retrieves a converter based on key given.
|
||||
func metadataConverterForType(typeURL string) metadataConverter {
|
||||
return metadataRegistry[typeURL]
|
||||
}
|
||||
|
||||
// RegisterMetadataConverterForTesting registers the converter for testing
|
||||
// purposes and returns a cleanup function to restore the registry to its
|
||||
// previous state.
|
||||
func RegisterMetadataConverterForTesting(typeURL string) (func(), error) {
|
||||
var conv metadataConverter
|
||||
switch typeURL {
|
||||
case version.V3AddressURL:
|
||||
conv = proxyAddressConverter{}
|
||||
case version.V3AudienceURL:
|
||||
conv = audienceConverter{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown typeURL for testing: %s", typeURL)
|
||||
}
|
||||
curConverter, found := metadataRegistry[typeURL]
|
||||
registerMetadataConverter(typeURL, conv)
|
||||
return func() {
|
||||
if found {
|
||||
metadataRegistry[typeURL] = curConverter
|
||||
return
|
||||
}
|
||||
delete(metadataRegistry, typeURL)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UnregisterMetadataConverterForTesting unregisters the converter for testing
|
||||
// purposes and returns a cleanup function to restore the registry to its
|
||||
// previous state.
|
||||
func UnregisterMetadataConverterForTesting(typeURL string) func() {
|
||||
curConverter, found := metadataRegistry[typeURL]
|
||||
delete(metadataRegistry, typeURL)
|
||||
return func() {
|
||||
if found {
|
||||
metadataRegistry[typeURL] = curConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StructMetadataValue stores the values in a google.protobuf.Struct from
|
||||
// FilterMetadata.
|
||||
type StructMetadataValue struct {
|
||||
// Data stores the parsed JSON representation of a google.protobuf.Struct.
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
// ProxyAddressMetadataValue holds the address parsed from the
|
||||
// envoy.config.core.v3.Address proto message, as specified in gRFC A86.
|
||||
type ProxyAddressMetadataValue struct {
|
||||
// Address stores the proxy address configured (A86). It will be in the form
|
||||
// of host:port. It has to be either IPv6 or IPv4.
|
||||
Address string
|
||||
}
|
||||
|
||||
// proxyAddressConvertor implements the metadataConverter interface to handle
|
||||
// the conversion of envoy.config.core.v3.Address protobuf messages into an
|
||||
// internal representation.
|
||||
type proxyAddressConverter struct{}
|
||||
|
||||
func (proxyAddressConverter) convert(anyProto *anypb.Any) (any, error) {
|
||||
addressProto := &v3corepb.Address{}
|
||||
if err := anyProto.UnmarshalTo(addressProto); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal resource from Any proto: %v", err)
|
||||
}
|
||||
socketaddress := addressProto.GetSocketAddress()
|
||||
if socketaddress == nil {
|
||||
return nil, fmt.Errorf("no socket_address field in metadata")
|
||||
}
|
||||
if _, err := netip.ParseAddr(socketaddress.GetAddress()); err != nil {
|
||||
return nil, fmt.Errorf("address field is not a valid IPv4 or IPv6 address: %q", socketaddress.GetAddress())
|
||||
}
|
||||
portvalue := socketaddress.GetPortValue()
|
||||
if portvalue == 0 {
|
||||
return nil, fmt.Errorf("port value not set in socket_address")
|
||||
}
|
||||
return ProxyAddressMetadataValue{Address: parseAddress(socketaddress)}, nil
|
||||
}
|
||||
|
||||
// AudienceMetadataValue holds the audience parsed from the
|
||||
// envoy.extensions.filters.http.gcp_authn.v3.Audience proto message, as
|
||||
// specified in gRFC A83.
|
||||
type AudienceMetadataValue struct {
|
||||
// Audience is the URL of the receiving service that performs token
|
||||
// authentication.
|
||||
Audience string
|
||||
}
|
||||
|
||||
// audienceConverter implements the metadataConverter interface to
|
||||
// handle the conversion of envoy.extensions.filters.http.gcp_authn.v3.Audience
|
||||
// protobuf messages into an internal representation.
|
||||
type audienceConverter struct{}
|
||||
|
||||
func (audienceConverter) convert(anyProto *anypb.Any) (any, error) {
|
||||
audienceProto := &v3gcpauthnpb.Audience{}
|
||||
if err := anyProto.UnmarshalTo(audienceProto); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal the envoy.extensions.filters.http.gcp_authn.v3.Audience resource from Any proto: %v", err)
|
||||
}
|
||||
if audienceProto.GetUrl() == "" {
|
||||
return nil, fmt.Errorf("empty url field in audience metadata")
|
||||
}
|
||||
return AudienceMetadataValue{Audience: audienceProto.GetUrl()}, nil
|
||||
}
|
||||
259
internal/xds/xdsclient/xdsresource/metadata_test.go
Normal file
259
internal/xds/xdsclient/xdsresource/metadata_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3"
|
||||
)
|
||||
|
||||
func (s) TestProxyAddressConverterSuccess(t *testing.T) {
|
||||
cleanup, err := RegisterMetadataConverterForTesting(version.V3AddressURL)
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterMetadataConverterForTesting(%q) failed: %v", version.V3AddressURL, err)
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
converter := metadataConverterForType(version.V3AddressURL)
|
||||
if converter == nil {
|
||||
t.Fatalf("Converter for %q not found in registry", version.V3AddressURL)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
addr *v3corepb.Address
|
||||
want ProxyAddressMetadataValue
|
||||
}{
|
||||
{
|
||||
name: "valid_IPv4_address_and_port",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "192.168.1.1",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ProxyAddressMetadataValue{
|
||||
Address: "192.168.1.1:8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid_full_IPv6_address_and_port",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 9090,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ProxyAddressMetadataValue{
|
||||
Address: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9090",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid_shortened_IPv6_address",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "2001:db8::1",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 9090,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ProxyAddressMetadataValue{
|
||||
Address: "[2001:db8::1]:9090",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid_link-local_IPv6_address",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "fe80::1ff:fe23:4567:890a",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 8888,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ProxyAddressMetadataValue{
|
||||
Address: "[fe80::1ff:fe23:4567:890a]:8888",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid_IPv4-mapped_IPv6_address",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "::ffff:192.0.2.128",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 1234,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ProxyAddressMetadataValue{
|
||||
Address: "[::ffff:192.0.2.128]:1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
anyProto := testutils.MarshalAny(t, tt.addr)
|
||||
got, err := converter.convert(anyProto)
|
||||
if err != nil {
|
||||
t.Fatalf("convert() failed with error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(ProxyAddressMetadataValue{})); diff != "" {
|
||||
t.Errorf("convert() returned unexpected value (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestProxyAddressConverterFailure(t *testing.T) {
|
||||
cleanup, err := RegisterMetadataConverterForTesting(version.V3AddressURL)
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterMetadataConverterForTesting(%q) failed: %v", version.V3AddressURL, err)
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
converter := metadataConverterForType(version.V3AddressURL)
|
||||
if converter == nil {
|
||||
t.Fatalf("Converter for %q not found in registry", version.V3AddressURL)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
addr *v3corepb.Address
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid_address",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "invalid-ip",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "address field is not a valid IPv4 or IPv6 address: \"invalid-ip\"",
|
||||
},
|
||||
{
|
||||
name: "missing_socket_address",
|
||||
addr: &v3corepb.Address{
|
||||
// No SocketAddress field set.
|
||||
},
|
||||
wantErr: "no socket_address field in metadata",
|
||||
},
|
||||
{
|
||||
name: "address_is_not_a_socket_address",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_EnvoyInternalAddress{
|
||||
EnvoyInternalAddress: &v3corepb.EnvoyInternalAddress{
|
||||
AddressNameSpecifier: &v3corepb.EnvoyInternalAddress_ServerListenerName{
|
||||
ServerListenerName: "some-internal-listener",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "no socket_address field in metadata",
|
||||
},
|
||||
{
|
||||
name: "port_value_not_set",
|
||||
addr: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "127.0.0.1",
|
||||
PortSpecifier: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "port value not set in socket_address",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
anyProto := testutils.MarshalAny(t, tt.addr)
|
||||
_, err := converter.convert(anyProto)
|
||||
if err == nil || err.Error() != tt.wantErr {
|
||||
t.Errorf("convert() got error = %v, wantErr = %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestAudienceConverterSuccess(t *testing.T) {
|
||||
cleanup, err := RegisterMetadataConverterForTesting(version.V3AudienceURL)
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterMetadataConverterForTesting(%q) failed: %v", version.V3AudienceURL, err)
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
converter := metadataConverterForType(version.V3AudienceURL)
|
||||
if converter == nil {
|
||||
t.Fatalf("Converter for %q not found in registry", version.V3AudienceURL)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
audience *anypb.Any
|
||||
want AudienceMetadataValue
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid_audience",
|
||||
audience: testutils.MarshalAny(t, &v3gcpauthnpb.Audience{Url: "https://example.com"}),
|
||||
want: AudienceMetadataValue{Audience: "https://example.com"},
|
||||
},
|
||||
{
|
||||
name: "empty_audience",
|
||||
audience: testutils.MarshalAny(t, &v3gcpauthnpb.Audience{Url: ""}),
|
||||
want: AudienceMetadataValue{Audience: ""},
|
||||
wantErr: "empty url field in audience metadata",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := converter.convert(tt.audience)
|
||||
if tt.wantErr != "" {
|
||||
if err == nil || err.Error() != tt.wantErr {
|
||||
t.Errorf("convert() got error = %v, wantErr = %q", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("convert() failed with error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("convert(%s) returned unexpected diff (-want +got):\n%s", tt.audience.GetTypeUrl(), diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
internal/xds/xdsclient/xdsresource/name.go
Normal file
127
internal/xds/xdsclient/xdsresource/name.go
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FederationScheme is the scheme of a federation resource name.
|
||||
const FederationScheme = "xdstp"
|
||||
|
||||
// Name contains the parsed component of an xDS resource name.
|
||||
//
|
||||
// An xDS resource name is in the format of
|
||||
// xdstp://[{authority}]/{resource type}/{id/*}?{context parameters}{#processing directive,*}
|
||||
//
|
||||
// See
|
||||
// https://github.com/cncf/xds/blob/main/proposals/TP1-xds-transport-next.md#uri-based-xds-resource-names
|
||||
// for details, and examples.
|
||||
type Name struct {
|
||||
Scheme string
|
||||
Authority string
|
||||
Type string
|
||||
ID string
|
||||
|
||||
ContextParams map[string]string
|
||||
|
||||
processingDirective string
|
||||
}
|
||||
|
||||
// ParseName splits the name and returns a struct representation of the Name.
|
||||
//
|
||||
// If the name isn't a valid new-style xDS name, field ID is set to the input.
|
||||
// Note that this is not an error, because we still support the old-style
|
||||
// resource names (those not starting with "xdstp:").
|
||||
//
|
||||
// The caller can tell if the parsing is successful by checking the returned
|
||||
// Scheme.
|
||||
func ParseName(name string) *Name {
|
||||
if !strings.Contains(name, "://") {
|
||||
// Only the long form URL, with ://, is valid.
|
||||
return &Name{ID: name}
|
||||
}
|
||||
parsed, err := url.Parse(name)
|
||||
if err != nil {
|
||||
return &Name{ID: name}
|
||||
}
|
||||
|
||||
ret := &Name{
|
||||
Scheme: parsed.Scheme,
|
||||
Authority: parsed.Host,
|
||||
}
|
||||
split := strings.SplitN(parsed.Path, "/", 3)
|
||||
if len(split) < 3 {
|
||||
// Path is in the format of "/type/id". There must be at least 3
|
||||
// segments after splitting.
|
||||
return &Name{ID: name}
|
||||
}
|
||||
ret.Type = split[1]
|
||||
ret.ID = split[2]
|
||||
if len(parsed.Query()) != 0 {
|
||||
ret.ContextParams = make(map[string]string)
|
||||
for k, vs := range parsed.Query() {
|
||||
if len(vs) > 0 {
|
||||
// We only keep one value of each key. Behavior for multiple values
|
||||
// is undefined.
|
||||
ret.ContextParams[k] = vs[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: processing directive (the part comes after "#" in the URL, stored
|
||||
// in parsed.RawFragment) is kept but not processed. Add support for that
|
||||
// when it's needed.
|
||||
ret.processingDirective = parsed.RawFragment
|
||||
return ret
|
||||
}
|
||||
|
||||
// String returns a canonicalized string of name. The context parameters are
|
||||
// sorted by the keys.
|
||||
func (n *Name) String() string {
|
||||
if n.Scheme == "" {
|
||||
return n.ID
|
||||
}
|
||||
|
||||
// Sort and build query.
|
||||
keys := make([]string, 0, len(n.ContextParams))
|
||||
for k := range n.ContextParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var pairs []string
|
||||
for _, k := range keys {
|
||||
pairs = append(pairs, strings.Join([]string{k, n.ContextParams[k]}, "="))
|
||||
}
|
||||
rawQuery := strings.Join(pairs, "&")
|
||||
|
||||
path := n.Type
|
||||
if n.ID != "" {
|
||||
path = "/" + path + "/" + n.ID
|
||||
}
|
||||
|
||||
tempURL := &url.URL{
|
||||
Scheme: n.Scheme,
|
||||
Host: n.Authority,
|
||||
Path: path,
|
||||
RawQuery: rawQuery,
|
||||
RawFragment: n.processingDirective,
|
||||
}
|
||||
return tempURL.String()
|
||||
}
|
||||
103
internal/xds/xdsclient/xdsresource/name_test.go
Normal file
103
internal/xds/xdsclient/xdsresource/name_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func TestParseName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want *Name
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
name: "old style name",
|
||||
in: "test-resource",
|
||||
want: &Name{ID: "test-resource"},
|
||||
wantStr: "test-resource",
|
||||
},
|
||||
{
|
||||
name: "invalid not url",
|
||||
in: "a:/b/c",
|
||||
want: &Name{ID: "a:/b/c"},
|
||||
wantStr: "a:/b/c",
|
||||
},
|
||||
{
|
||||
name: "invalid no resource type",
|
||||
in: "xdstp://auth/id",
|
||||
want: &Name{ID: "xdstp://auth/id"},
|
||||
wantStr: "xdstp://auth/id",
|
||||
},
|
||||
{
|
||||
name: "valid with no authority",
|
||||
in: "xdstp:///type/id",
|
||||
want: &Name{Scheme: "xdstp", Authority: "", Type: "type", ID: "id"},
|
||||
wantStr: "xdstp:///type/id",
|
||||
},
|
||||
{
|
||||
name: "valid no ctx params",
|
||||
in: "xdstp://auth/type/id",
|
||||
want: &Name{Scheme: "xdstp", Authority: "auth", Type: "type", ID: "id"},
|
||||
wantStr: "xdstp://auth/type/id",
|
||||
},
|
||||
{
|
||||
name: "valid with ctx params",
|
||||
in: "xdstp://auth/type/id?a=1&b=2",
|
||||
want: &Name{Scheme: "xdstp", Authority: "auth", Type: "type", ID: "id", ContextParams: map[string]string{"a": "1", "b": "2"}},
|
||||
wantStr: "xdstp://auth/type/id?a=1&b=2",
|
||||
},
|
||||
{
|
||||
name: "valid with ctx params sorted by keys",
|
||||
in: "xdstp://auth/type/id?b=2&a=1",
|
||||
want: &Name{Scheme: "xdstp", Authority: "auth", Type: "type", ID: "id", ContextParams: map[string]string{"a": "1", "b": "2"}},
|
||||
wantStr: "xdstp://auth/type/id?a=1&b=2",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ParseName(tt.in)
|
||||
if !cmp.Equal(got, tt.want, cmpopts.IgnoreFields(Name{}, "processingDirective")) {
|
||||
t.Errorf("ParseName() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
if gotStr := got.String(); gotStr != tt.wantStr {
|
||||
t.Errorf("Name.String() = %s, want %s", gotStr, tt.wantStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNameStringCtxParamsOrder covers the case that if two names differ only in
|
||||
// context parameter __order__, the parsed name.String() has the same value.
|
||||
func TestNameStringCtxParamsOrder(t *testing.T) {
|
||||
const (
|
||||
a = "xdstp://auth/type/id?a=1&b=2"
|
||||
b = "xdstp://auth/type/id?b=2&a=1"
|
||||
)
|
||||
aParsed := ParseName(a).String()
|
||||
bParsed := ParseName(b).String()
|
||||
|
||||
if aParsed != bParsed {
|
||||
t.Fatalf("aParsed.String() = %q, bParsed.String() = %q, want them to be the same", aParsed, bParsed)
|
||||
}
|
||||
}
|
||||
139
internal/xds/xdsclient/xdsresource/resource_type.go
Normal file
139
internal/xds/xdsclient/xdsresource/resource_type.go
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Package xdsresource implements the xDS data model layer.
|
||||
//
|
||||
// Provides resource-type specific functionality to unmarshal xDS protos into
|
||||
// internal data structures that contain only fields gRPC is interested in.
|
||||
// These internal data structures are passed to components in the xDS stack
|
||||
// (resolver/balancers/server) that have expressed interest in receiving
|
||||
// updates to specific resources.
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// Producer contains a single method to discover resource configuration from a
|
||||
// remote management server using xDS APIs.
|
||||
//
|
||||
// The xdsclient package provides a concrete implementation of this interface.
|
||||
type Producer interface {
|
||||
// WatchResource uses xDS to discover the resource associated with the
|
||||
// provided resource name. The resource type implementation determines how
|
||||
// xDS responses are are deserialized and validated, as received from the
|
||||
// xDS management server. Upon receipt of a response from the management
|
||||
// server, an appropriate callback on the watcher is invoked.
|
||||
WatchResource(typeURL, resourceName string, watcher xdsclient.ResourceWatcher) (cancel func())
|
||||
}
|
||||
|
||||
// ResourceWatcher is notified of the resource updates and errors that are
|
||||
// received by the xDS client from the management server.
|
||||
//
|
||||
// All methods contain a done parameter which should be called when processing
|
||||
// of the update has completed. For example, if processing a resource requires
|
||||
// watching new resources, registration of those new watchers should be
|
||||
// completed before done is called, which can happen after the ResourceWatcher
|
||||
// method has returned. Failure to call done will prevent the xDS client from
|
||||
// providing future ResourceWatcher notifications.
|
||||
type ResourceWatcher interface {
|
||||
// ResourceChanged indicates a new version of the resource is available.
|
||||
ResourceChanged(resourceData ResourceData, done func())
|
||||
|
||||
// ResourceError indicates an error occurred while trying to fetch or
|
||||
// decode the associated resource. The previous version of the resource
|
||||
// should be considered invalid.
|
||||
ResourceError(err error, done func())
|
||||
|
||||
// AmbientError indicates an error occurred after a resource has been
|
||||
// received that should not modify the use of that resource but may provide
|
||||
// useful information about the state of the XDSClient for debugging
|
||||
// purposes. The previous version of the resource should still be
|
||||
// considered valid.
|
||||
AmbientError(err error, done func())
|
||||
}
|
||||
|
||||
// TODO: Once the implementation is complete, rename this interface as
|
||||
// ResourceType and get rid of the existing ResourceType enum.
|
||||
|
||||
// Type wraps all resource-type specific functionality. Each supported resource
|
||||
// type will provide an implementation of this interface.
|
||||
type Type interface {
|
||||
// TypeURL is the xDS type URL of this resource type for v3 transport.
|
||||
TypeURL() string
|
||||
|
||||
// TypeName identifies resources in a transport protocol agnostic way. This
|
||||
// can be used for logging/debugging purposes, as well in cases where the
|
||||
// resource type name is to be uniquely identified but the actual
|
||||
// functionality provided by the resource type is not required.
|
||||
//
|
||||
// TODO: once Type is renamed to ResourceType, rename TypeName to
|
||||
// ResourceTypeName.
|
||||
TypeName() string
|
||||
|
||||
// AllResourcesRequiredInSotW indicates whether this resource type requires
|
||||
// that all resources be present in every SotW response from the server. If
|
||||
// true, a response that does not include a previously seen resource will be
|
||||
// interpreted as a deletion of that resource.
|
||||
AllResourcesRequiredInSotW() bool
|
||||
|
||||
// Decode deserializes and validates an xDS resource serialized inside the
|
||||
// provided `Any` proto, as received from the xDS management server.
|
||||
//
|
||||
// If protobuf deserialization fails or resource validation fails,
|
||||
// returns a non-nil error. Otherwise, returns a fully populated
|
||||
// DecodeResult.
|
||||
Decode(*DecodeOptions, *anypb.Any) (*DecodeResult, error)
|
||||
}
|
||||
|
||||
// ResourceData contains the configuration data sent by the xDS management
|
||||
// server, associated with the resource being watched. Every resource type must
|
||||
// provide an implementation of this interface to represent the configuration
|
||||
// received from the xDS management server.
|
||||
type ResourceData interface {
|
||||
// RawEqual returns true if the passed in resource data is equal to that of
|
||||
// the receiver, based on the underlying raw protobuf message.
|
||||
RawEqual(ResourceData) bool
|
||||
|
||||
// ToJSON returns a JSON string representation of the resource data.
|
||||
ToJSON() string
|
||||
|
||||
Raw() *anypb.Any
|
||||
}
|
||||
|
||||
// DecodeOptions wraps the options required by ResourceType implementation for
|
||||
// decoding configuration received from the xDS management server.
|
||||
type DecodeOptions struct {
|
||||
// BootstrapConfig contains the complete bootstrap configuration passed to
|
||||
// the xDS client. This contains useful data for resource validation.
|
||||
BootstrapConfig *bootstrap.Config
|
||||
// ServerConfig contains the server config (from the above bootstrap
|
||||
// configuration) of the xDS server from which the current resource, for
|
||||
// which Decode() is being invoked, was received.
|
||||
ServerConfig *bootstrap.ServerConfig
|
||||
}
|
||||
|
||||
// DecodeResult is the result of a decode operation.
|
||||
type DecodeResult struct {
|
||||
// Name is the name of the resource being watched.
|
||||
Name string
|
||||
// Resource contains the configuration associated with the resource being
|
||||
// watched.
|
||||
Resource ResourceData
|
||||
}
|
||||
132
internal/xds/xdsclient/xdsresource/route_config_resource_type.go
Normal file
132
internal/xds/xdsclient/xdsresource/route_config_resource_type.go
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdsclient "google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// RouteConfigTypeName represents the transport agnostic name for the
|
||||
// route config resource.
|
||||
RouteConfigTypeName = "RouteConfigResource"
|
||||
)
|
||||
|
||||
// routeConfigResourceDecoder is an implementation of the xdsclient.Decoder
|
||||
// interface for route configuration resources.
|
||||
type routeConfigResourceDecoder struct {
|
||||
bootstrapConfig *bootstrap.Config
|
||||
serverConfigs map[xdsclient.ServerConfig]*bootstrap.ServerConfig
|
||||
}
|
||||
|
||||
func (d *routeConfigResourceDecoder) Decode(resource *xdsclient.AnyProto, opts xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
|
||||
var serverCfg *bootstrap.ServerConfig
|
||||
if opts.ServerConfig != nil {
|
||||
serverCfg = d.serverConfigs[*opts.ServerConfig]
|
||||
}
|
||||
name, rc, err := unmarshalRouteConfigResource(resource.ToAny(), d.bootstrapConfig, serverCfg)
|
||||
if name == "" {
|
||||
// Name is unset only when protobuf deserialization fails.
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
// Protobuf deserialization succeeded, but resource validation failed.
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &RouteConfigResourceData{Resource: RouteConfigUpdate{}},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &xdsclient.DecodeResult{
|
||||
Name: name,
|
||||
Resource: &RouteConfigResourceData{Resource: rc},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RouteConfigResourceData is an implementation of the xdsclient.ResourceData
|
||||
// interface for route configuration resources.
|
||||
type RouteConfigResourceData struct {
|
||||
Resource RouteConfigUpdate
|
||||
}
|
||||
|
||||
// Equal returns true if other is equal to er.
|
||||
func (r *RouteConfigResourceData) Equal(other xdsclient.ResourceData) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(r.Bytes(), other.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the protobuf serialized bytes of the route config resource proto.
|
||||
func (r *RouteConfigResourceData) Bytes() []byte {
|
||||
return r.Resource.Raw.GetValue()
|
||||
}
|
||||
|
||||
// RouteConfigWatcher wraps the callbacks to be invoked for different
|
||||
// events corresponding to the route configuration resource being watched. gRFC
|
||||
// A88 contains an exhaustive list of what method is invoked under what
|
||||
// conditions.
|
||||
type RouteConfigWatcher interface {
|
||||
// ResourceChanged indicates a new version of the resource is available.
|
||||
ResourceChanged(resource *RouteConfigUpdate, done func())
|
||||
|
||||
// ResourceError indicates an error occurred while trying to fetch or
|
||||
// decode the associated resource. The previous version of the resource
|
||||
// should be considered invalid.
|
||||
ResourceError(err error, done func())
|
||||
|
||||
// AmbientError indicates an error occurred after a resource has been
|
||||
// received that should not modify the use of that resource but may provide
|
||||
// useful information about the state of the XDSClient for debugging
|
||||
// purposes. The previous version of the resource should still be
|
||||
// considered valid.
|
||||
AmbientError(err error, done func())
|
||||
}
|
||||
|
||||
type delegatingRouteConfigWatcher struct {
|
||||
watcher RouteConfigWatcher
|
||||
}
|
||||
|
||||
func (d *delegatingRouteConfigWatcher) ResourceChanged(data xdsclient.ResourceData, onDone func()) {
|
||||
rc := data.(*RouteConfigResourceData)
|
||||
d.watcher.ResourceChanged(&rc.Resource, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingRouteConfigWatcher) ResourceError(err error, onDone func()) {
|
||||
d.watcher.ResourceError(err, onDone)
|
||||
}
|
||||
|
||||
func (d *delegatingRouteConfigWatcher) AmbientError(err error, onDone func()) {
|
||||
d.watcher.AmbientError(err, onDone)
|
||||
}
|
||||
|
||||
// WatchRouteConfig uses xDS to discover the configuration associated with the
|
||||
// provided route configuration resource name.
|
||||
func WatchRouteConfig(p Producer, name string, w RouteConfigWatcher) (cancel func()) {
|
||||
return p.WatchResource(version.V3RouteConfigURL, name, &delegatingRouteConfigWatcher{watcher: w})
|
||||
}
|
||||
|
||||
// NewRouteConfigResourceTypeDecoder returns a xdsclient.Decoder that wraps
|
||||
// the xdsresource.routeConfigType.
|
||||
func NewRouteConfigResourceTypeDecoder(bc *bootstrap.Config, gServerCfgMap map[xdsclient.ServerConfig]*bootstrap.ServerConfig) xdsclient.Decoder {
|
||||
return &routeConfigResourceDecoder{bootstrapConfig: bc, serverConfigs: gServerCfgMap}
|
||||
}
|
||||
124
internal/xds/xdsclient/xdsresource/test_utils_test.go
Normal file
124
internal/xds/xdsclient/xdsresource/test_utils_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter/router"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
|
||||
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
var (
|
||||
cmpOpts = cmp.Options{
|
||||
cmpopts.EquateEmpty(),
|
||||
cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}),
|
||||
cmp.FilterValues(func(_, _ error) bool { return true }, cmpopts.EquateErrors()),
|
||||
cmp.Comparer(func(_, _ time.Time) bool { return true }),
|
||||
protocmp.Transform(),
|
||||
}
|
||||
routeConfig = &v3routepb.RouteConfiguration{
|
||||
Name: "routeName",
|
||||
VirtualHosts: []*v3routepb.VirtualHost{{
|
||||
Domains: []string{"lds.target.good:3333"},
|
||||
Routes: []*v3routepb.Route{{
|
||||
Match: &v3routepb.RouteMatch{
|
||||
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
|
||||
},
|
||||
Action: &v3routepb.Route_NonForwardingAction{},
|
||||
}}}}}
|
||||
inlineRouteConfig = &RouteConfigUpdate{
|
||||
VirtualHosts: []*VirtualHost{{
|
||||
Domains: []string{"lds.target.good:3333"},
|
||||
Routes: []*Route{{Prefix: newStringP("/"), ActionType: RouteActionNonForwardingAction}},
|
||||
}}}
|
||||
|
||||
validServerSideHTTPFilter1 = &v3httppb.HttpFilter{
|
||||
Name: "serverOnlyCustomFilter",
|
||||
ConfigType: &v3httppb.HttpFilter_TypedConfig{TypedConfig: serverOnlyCustomFilterConfig},
|
||||
}
|
||||
validServerSideHTTPFilter2 = &v3httppb.HttpFilter{
|
||||
Name: "serverOnlyCustomFilter2",
|
||||
ConfigType: &v3httppb.HttpFilter_TypedConfig{TypedConfig: serverOnlyCustomFilterConfig},
|
||||
}
|
||||
emptyRouterFilter = e2e.RouterHTTPFilter
|
||||
localSocketAddress = &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "0.0.0.0",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 9999,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func makeRouterFilter(t *testing.T) HTTPFilter {
|
||||
t.Helper()
|
||||
routerBuilder := httpfilter.Get(router.TypeURL)
|
||||
routerConfig, err := routerBuilder.ParseFilterConfig(testutils.MarshalAny(t, &v3routerpb.Router{}), httpfilter.ParseOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse Router filter configuration: %v", err)
|
||||
}
|
||||
return HTTPFilter{Name: "router", Filter: routerBuilder, Config: routerConfig}
|
||||
}
|
||||
|
||||
func makeRouterFilterList(t *testing.T) []HTTPFilter {
|
||||
return []HTTPFilter{makeRouterFilter(t)}
|
||||
}
|
||||
|
||||
func emptyValidNetworkFilters(t *testing.T) []*v3listenerpb.Filter {
|
||||
return []*v3listenerpb.Filter{
|
||||
{
|
||||
Name: "filter-1",
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: routeConfig,
|
||||
},
|
||||
HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
575
internal/xds/xdsclient/xdsresource/tests/unmarshal_cds_test.go
Normal file
575
internal/xds/xdsclient/xdsresource/tests/unmarshal_cds_test.go
Normal file
@@ -0,0 +1,575 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package tests_test contains test cases for unmarshalling of CDS resources.
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"google.golang.org/grpc/balancer/leastrequest"
|
||||
"google.golang.org/grpc/internal/balancer/stub"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
xdsinternal "google.golang.org/grpc/internal/xds"
|
||||
"google.golang.org/grpc/internal/xds/balancer/wrrlocality"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3"
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||
v3aggregateclusterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
|
||||
v3ringhashpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/ring_hash/v3"
|
||||
v3roundrobinpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3"
|
||||
v3wrrlocalitypb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3"
|
||||
|
||||
_ "google.golang.org/grpc/balancer/roundrobin" // To register round_robin load balancer.
|
||||
_ "google.golang.org/grpc/xds" // Register the xDS LB Registry Converters.
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
clusterName = "clusterName"
|
||||
serviceName = "service"
|
||||
)
|
||||
|
||||
func wrrLocality(t *testing.T, m proto.Message) *v3wrrlocalitypb.WrrLocality {
|
||||
return &v3wrrlocalitypb.WrrLocality{
|
||||
EndpointPickingPolicy: &v3clusterpb.LoadBalancingPolicy{
|
||||
Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{
|
||||
{
|
||||
TypedExtensionConfig: &v3corepb.TypedExtensionConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, m),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func wrrLocalityAny(t *testing.T, m proto.Message) *anypb.Any {
|
||||
return testutils.MarshalAny(t, wrrLocality(t, m))
|
||||
}
|
||||
|
||||
type customLBConfig struct {
|
||||
serviceconfig.LoadBalancingConfig
|
||||
}
|
||||
|
||||
// We have this test in a separate test package in order to not take a
|
||||
// dependency on the internal xDS balancer packages within the xDS Client.
|
||||
func (s) TestValidateCluster_Success(t *testing.T) {
|
||||
const customLBPolicyName = "myorg.MyCustomLeastRequestPolicy"
|
||||
stub.Register(customLBPolicyName, stub.BalancerFuncs{
|
||||
ParseConfig: func(json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
return customLBConfig{}, nil
|
||||
},
|
||||
})
|
||||
serverCfg, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{URI: "test-server"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server config for testing: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cluster *v3clusterpb.Cluster
|
||||
serverCfg *bootstrap.ServerConfig
|
||||
wantUpdate xdsresource.ClusterUpdate
|
||||
wantLBConfig *iserviceconfig.BalancerConfig
|
||||
}{
|
||||
{
|
||||
name: "happy-case-logical-dns",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_LOGICAL_DNS},
|
||||
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
|
||||
LoadAssignment: &v3endpointpb.ClusterLoadAssignment{
|
||||
Endpoints: []*v3endpointpb.LocalityLbEndpoints{{
|
||||
LbEndpoints: []*v3endpointpb.LbEndpoint{{
|
||||
HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
|
||||
Endpoint: &v3endpointpb.Endpoint{
|
||||
Address: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "dns_host",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
ClusterType: xdsresource.ClusterTypeLogicalDNS,
|
||||
DNSHostName: "dns_host:8080",
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy-case-aggregate-v3",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_ClusterType{
|
||||
ClusterType: &v3clusterpb.Cluster_CustomClusterType{
|
||||
Name: "envoy.clusters.aggregate",
|
||||
TypedConfig: testutils.MarshalAny(t, &v3aggregateclusterpb.ClusterConfig{
|
||||
Clusters: []string{"a", "b", "c"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
ClusterType: xdsresource.ClusterTypeAggregate,
|
||||
PrioritizedClusterNames: []string{"a", "b", "c"},
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy-case-no-service-name-no-lrs",
|
||||
cluster: e2e.DefaultCluster(clusterName, "", e2e.SecurityLevelNone),
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy-case-no-lrs",
|
||||
cluster: e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone),
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-lrs",
|
||||
cluster: e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
||||
ClusterName: clusterName,
|
||||
ServiceName: serviceName,
|
||||
EnableLRS: true,
|
||||
}),
|
||||
serverCfg: serverCfg,
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
LRSServerConfig: serverCfg,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-circuitbreakers",
|
||||
cluster: func() *v3clusterpb.Cluster {
|
||||
c := e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
||||
ClusterName: clusterName,
|
||||
ServiceName: serviceName,
|
||||
EnableLRS: true,
|
||||
})
|
||||
c.CircuitBreakers = &v3clusterpb.CircuitBreakers{
|
||||
Thresholds: []*v3clusterpb.CircuitBreakers_Thresholds{
|
||||
{
|
||||
Priority: v3corepb.RoutingPriority_DEFAULT,
|
||||
MaxRequests: wrapperspb.UInt32(512),
|
||||
},
|
||||
{
|
||||
Priority: v3corepb.RoutingPriority_HIGH,
|
||||
MaxRequests: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
serverCfg: serverCfg,
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
LRSServerConfig: serverCfg,
|
||||
MaxRequests: func() *uint32 { i := uint32(512); return &i }(),
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-ring-hash-lb-policy-with-default-config",
|
||||
cluster: func() *v3clusterpb.Cluster {
|
||||
c := e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)
|
||||
c.LbPolicy = v3clusterpb.Cluster_RING_HASH
|
||||
return c
|
||||
}(),
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "ring_hash_experimental",
|
||||
Config: &iringhash.LBConfig{
|
||||
MinRingSize: 1024,
|
||||
MaxRingSize: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-least-request-lb-policy-with-default-config",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "least_request_experimental",
|
||||
Config: &leastrequest.LBConfig{
|
||||
ChoiceCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-ring-hash-lb-policy-with-none-default-config",
|
||||
cluster: func() *v3clusterpb.Cluster {
|
||||
c := e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)
|
||||
c.LbPolicy = v3clusterpb.Cluster_RING_HASH
|
||||
c.LbConfig = &v3clusterpb.Cluster_RingHashLbConfig_{
|
||||
RingHashLbConfig: &v3clusterpb.Cluster_RingHashLbConfig{
|
||||
MinimumRingSize: wrapperspb.UInt64(10),
|
||||
MaximumRingSize: wrapperspb.UInt64(100),
|
||||
},
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "ring_hash_experimental",
|
||||
Config: &iringhash.LBConfig{
|
||||
MinRingSize: 10,
|
||||
MaxRingSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-least-request-lb-policy-with-none-default-config",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
|
||||
LbConfig: &v3clusterpb.Cluster_LeastRequestLbConfig_{
|
||||
LeastRequestLbConfig: &v3clusterpb.Cluster_LeastRequestLbConfig{
|
||||
ChoiceCount: wrapperspb.UInt32(3),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "least_request_experimental",
|
||||
Config: &leastrequest.LBConfig{
|
||||
ChoiceCount: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-ring-hash-lb-policy-configured-through-LoadBalancingPolicy",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LoadBalancingPolicy: &v3clusterpb.LoadBalancingPolicy{
|
||||
Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{
|
||||
{
|
||||
TypedExtensionConfig: &v3corepb.TypedExtensionConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3ringhashpb.RingHash{
|
||||
HashFunction: v3ringhashpb.RingHash_XX_HASH,
|
||||
MinimumRingSize: wrapperspb.UInt64(10),
|
||||
MaximumRingSize: wrapperspb.UInt64(100),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "ring_hash_experimental",
|
||||
Config: &iringhash.LBConfig{
|
||||
MinRingSize: 10,
|
||||
MaxRingSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-wrrlocality-rr-child-configured-through-LoadBalancingPolicy",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LoadBalancingPolicy: &v3clusterpb.LoadBalancingPolicy{
|
||||
Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{
|
||||
{
|
||||
TypedExtensionConfig: &v3corepb.TypedExtensionConfig{
|
||||
TypedConfig: wrrLocalityAny(t, &v3roundrobinpb.RoundRobin{}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "round_robin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happiest-case-with-custom-lb-configured-through-LoadBalancingPolicy",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LoadBalancingPolicy: &v3clusterpb.LoadBalancingPolicy{
|
||||
Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{
|
||||
{
|
||||
TypedExtensionConfig: &v3corepb.TypedExtensionConfig{
|
||||
TypedConfig: wrrLocalityAny(t, &v3xdsxdstypepb.TypedStruct{
|
||||
TypeUrl: "type.googleapis.com/myorg.MyCustomLeastRequestPolicy",
|
||||
Value: &structpb.Struct{},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: "myorg.MyCustomLeastRequestPolicy",
|
||||
Config: customLBConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "load-balancing-policy-takes-precedence-over-lb-policy-and-enum",
|
||||
cluster: &v3clusterpb.Cluster{
|
||||
Name: clusterName,
|
||||
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
|
||||
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
|
||||
EdsConfig: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
|
||||
Ads: &v3corepb.AggregatedConfigSource{},
|
||||
},
|
||||
},
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
LbPolicy: v3clusterpb.Cluster_RING_HASH,
|
||||
LbConfig: &v3clusterpb.Cluster_RingHashLbConfig_{
|
||||
RingHashLbConfig: &v3clusterpb.Cluster_RingHashLbConfig{
|
||||
MinimumRingSize: wrapperspb.UInt64(20),
|
||||
MaximumRingSize: wrapperspb.UInt64(200),
|
||||
},
|
||||
},
|
||||
LoadBalancingPolicy: &v3clusterpb.LoadBalancingPolicy{
|
||||
Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{
|
||||
{
|
||||
TypedExtensionConfig: &v3corepb.TypedExtensionConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3ringhashpb.RingHash{
|
||||
HashFunction: v3ringhashpb.RingHash_XX_HASH,
|
||||
MinimumRingSize: wrapperspb.UInt64(10),
|
||||
MaximumRingSize: wrapperspb.UInt64(100),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantUpdate: xdsresource.ClusterUpdate{
|
||||
ClusterName: clusterName,
|
||||
EDSServiceName: serviceName,
|
||||
TelemetryLabels: xdsinternal.UnknownCSMLabels,
|
||||
},
|
||||
wantLBConfig: &iserviceconfig.BalancerConfig{
|
||||
Name: "ring_hash_experimental",
|
||||
Config: &iringhash.LBConfig{
|
||||
MinRingSize: 10,
|
||||
MaxRingSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
update, err := xdsresource.ValidateClusterAndConstructClusterUpdateForTesting(test.cluster, test.serverCfg)
|
||||
if err != nil {
|
||||
t.Errorf("validateClusterAndConstructClusterUpdate(%+v) failed: %v", test.cluster, err)
|
||||
}
|
||||
// Ignore the raw JSON string into the cluster update. JSON bytes
|
||||
// are nondeterministic (whitespace etc.) so we cannot reliably
|
||||
// compare JSON bytes in a test. Thus, marshal into a Balancer
|
||||
// Config struct and compare on that. Only need to test this JSON
|
||||
// emission here, as this covers the possible output space.
|
||||
if diff := cmp.Diff(update, test.wantUpdate, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(xdsresource.ClusterUpdate{}, "LBPolicy")); diff != "" {
|
||||
t.Errorf("validateClusterAndConstructClusterUpdate(%+v) got diff: %v (-got, +want)", test.cluster, diff)
|
||||
}
|
||||
bc := &iserviceconfig.BalancerConfig{}
|
||||
if err := json.Unmarshal(update.LBPolicy, bc); err != nil {
|
||||
t.Fatalf("failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(bc, test.wantLBConfig); diff != "" {
|
||||
t.Fatalf("update.LBConfig got unexpected output, diff (-got +want): %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
135
internal/xds/xdsclient/xdsresource/type.go
Normal file
135
internal/xds/xdsclient/xdsresource/type.go
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// UpdateValidatorFunc performs validations on update structs using
|
||||
// context/logic available at the xdsClient layer. Since these validation are
|
||||
// performed on internal update structs, they can be shared between different
|
||||
// API clients.
|
||||
type UpdateValidatorFunc func(any) error
|
||||
|
||||
// UpdateMetadata contains the metadata for each update, including timestamp,
|
||||
// raw message, and so on.
|
||||
type UpdateMetadata struct {
|
||||
// Status is the status of this resource, e.g. ACKed, NACKed, or
|
||||
// Not_exist(removed).
|
||||
Status ServiceStatus
|
||||
// Version is the version of the xds response. Note that this is the version
|
||||
// of the resource in use (previous ACKed). If a response is NACKed, the
|
||||
// NACKed version is in ErrState.
|
||||
Version string
|
||||
// Timestamp is when the response is received.
|
||||
Timestamp time.Time
|
||||
// ErrState is set when the update is NACKed.
|
||||
ErrState *UpdateErrorMetadata
|
||||
}
|
||||
|
||||
// IsListenerResource returns true if the provider URL corresponds to an xDS
|
||||
// Listener resource.
|
||||
func IsListenerResource(url string) bool {
|
||||
return url == version.V3ListenerURL
|
||||
}
|
||||
|
||||
// IsHTTPConnManagerResource returns true if the provider URL corresponds to an xDS
|
||||
// HTTPConnManager resource.
|
||||
func IsHTTPConnManagerResource(url string) bool {
|
||||
return url == version.V3HTTPConnManagerURL
|
||||
}
|
||||
|
||||
// IsRouteConfigResource returns true if the provider URL corresponds to an xDS
|
||||
// RouteConfig resource.
|
||||
func IsRouteConfigResource(url string) bool {
|
||||
return url == version.V3RouteConfigURL
|
||||
}
|
||||
|
||||
// IsClusterResource returns true if the provider URL corresponds to an xDS
|
||||
// Cluster resource.
|
||||
func IsClusterResource(url string) bool {
|
||||
return url == version.V3ClusterURL
|
||||
}
|
||||
|
||||
// IsEndpointsResource returns true if the provider URL corresponds to an xDS
|
||||
// Endpoints resource.
|
||||
func IsEndpointsResource(url string) bool {
|
||||
return url == version.V3EndpointsURL
|
||||
}
|
||||
|
||||
// UnwrapResource unwraps and returns the inner resource if it's in a resource
|
||||
// wrapper. The original resource is returned if it's not wrapped.
|
||||
func UnwrapResource(r *anypb.Any) (*anypb.Any, error) {
|
||||
url := r.GetTypeUrl()
|
||||
if url != version.V3ResourceWrapperURL {
|
||||
// Not wrapped.
|
||||
return r, nil
|
||||
}
|
||||
inner := &v3discoverypb.Resource{}
|
||||
if err := proto.Unmarshal(r.GetValue(), inner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inner.Resource, nil
|
||||
}
|
||||
|
||||
// ServiceStatus is the status of the update.
|
||||
type ServiceStatus int
|
||||
|
||||
const (
|
||||
// ServiceStatusUnknown is the default state, before a watch is started for
|
||||
// the resource.
|
||||
ServiceStatusUnknown ServiceStatus = iota
|
||||
// ServiceStatusRequested is when the watch is started, but before and
|
||||
// response is received.
|
||||
ServiceStatusRequested
|
||||
// ServiceStatusNotExist is when the resource doesn't exist in
|
||||
// state-of-the-world responses (e.g. LDS and CDS), which means the resource
|
||||
// is removed by the management server.
|
||||
ServiceStatusNotExist // Resource is removed in the server, in LDS/CDS.
|
||||
// ServiceStatusACKed is when the resource is ACKed.
|
||||
ServiceStatusACKed
|
||||
// ServiceStatusNACKed is when the resource is NACKed.
|
||||
ServiceStatusNACKed
|
||||
)
|
||||
|
||||
// UpdateErrorMetadata is part of UpdateMetadata. It contains the error state
|
||||
// when a response is NACKed.
|
||||
type UpdateErrorMetadata struct {
|
||||
// Version is the version of the NACKed response.
|
||||
Version string
|
||||
// Err contains why the response was NACKed.
|
||||
Err error
|
||||
// Timestamp is when the NACKed response was received.
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// UpdateWithMD contains the raw message of the update and the metadata,
|
||||
// including version, raw message, timestamp.
|
||||
//
|
||||
// This is to be used for config dump and CSDS, not directly by users (like
|
||||
// resolvers/balancers).
|
||||
type UpdateWithMD struct {
|
||||
MD UpdateMetadata
|
||||
Raw *anypb.Any
|
||||
}
|
||||
228
internal/xds/xdsclient/xdsresource/type_cds.go
Normal file
228
internal/xds/xdsclient/xdsresource/type_cds.go
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// ClusterType is the type of cluster from a received CDS response.
|
||||
type ClusterType int
|
||||
|
||||
const (
|
||||
// ClusterTypeEDS represents the EDS cluster type, which will delegate endpoint
|
||||
// discovery to the management server.
|
||||
ClusterTypeEDS ClusterType = iota
|
||||
// ClusterTypeLogicalDNS represents the Logical DNS cluster type, which essentially
|
||||
// maps to the gRPC behavior of using the DNS resolver with pick_first LB policy.
|
||||
ClusterTypeLogicalDNS
|
||||
// ClusterTypeAggregate represents the Aggregate Cluster type, which provides a
|
||||
// prioritized list of clusters to use. It is used for failover between clusters
|
||||
// with a different configuration.
|
||||
ClusterTypeAggregate
|
||||
)
|
||||
|
||||
// ClusterUpdate contains information from a received CDS response, which is of
|
||||
// interest to the registered CDS watcher.
|
||||
type ClusterUpdate struct {
|
||||
ClusterType ClusterType
|
||||
// ClusterName is the clusterName being watched for through CDS.
|
||||
ClusterName string
|
||||
// EDSServiceName is an optional name for EDS. If it's not set, the balancer
|
||||
// should watch ClusterName for the EDS resources.
|
||||
EDSServiceName string
|
||||
// LRSServerConfig contains configuration about the xDS server that sent
|
||||
// this cluster resource. This is also the server where load reports are to
|
||||
// be sent, for this cluster.
|
||||
LRSServerConfig *bootstrap.ServerConfig
|
||||
// SecurityCfg contains security configuration sent by the control plane.
|
||||
SecurityCfg *SecurityConfig
|
||||
// MaxRequests for circuit breaking, if any (otherwise nil).
|
||||
MaxRequests *uint32
|
||||
// DNSHostName is used only for cluster type DNS. It's the DNS name to
|
||||
// resolve in "host:port" form
|
||||
DNSHostName string
|
||||
// PrioritizedClusterNames is used only for cluster type aggregate. It represents
|
||||
// a prioritized list of cluster names.
|
||||
PrioritizedClusterNames []string
|
||||
|
||||
// LBPolicy represents the locality and endpoint picking policy in JSON,
|
||||
// which will be the child policy of xds_cluster_impl.
|
||||
LBPolicy json.RawMessage
|
||||
|
||||
// OutlierDetection is the outlier detection configuration for this cluster.
|
||||
// If nil, it means this cluster does not use the outlier detection feature.
|
||||
OutlierDetection json.RawMessage
|
||||
|
||||
// Raw is the resource from the xds response.
|
||||
Raw *anypb.Any
|
||||
// TelemetryLabels are the string valued metadata of filter_metadata type
|
||||
// "com.google.csm.telemetry_labels" with keys "service_name" or
|
||||
// "service_namespace".
|
||||
TelemetryLabels map[string]string
|
||||
|
||||
// LRSReportEndpointMetrics specifies the subset of ORCA metrics that
|
||||
// should be propagated to the LRS server.
|
||||
LRSReportEndpointMetrics *LRSReportEndpointMetricsConfig
|
||||
|
||||
// Metadata contains the metadata from the cluster resource.
|
||||
Metadata map[string]any
|
||||
|
||||
// IsHTTP11ProxyEnabled is true if the cluster transport socket is wrapped
|
||||
// in Http11ProxyUpstreamTransport.
|
||||
IsHTTP11ProxyEnabled bool
|
||||
}
|
||||
|
||||
// LRSReportEndpointMetricsConfig holds the configuration for propagating ORCA
|
||||
// metrics to the LRS server.
|
||||
type LRSReportEndpointMetricsConfig struct {
|
||||
CPUUtilization bool
|
||||
MemUtilization bool
|
||||
ApplicationUtilization bool
|
||||
// NamedMetricsAll specifies whether all named metrics should be propagated.
|
||||
// If true, NamedMetrics is ignored.
|
||||
NamedMetricsAll bool
|
||||
// NamedMetrics specifies the set of named metrics to propagate when
|
||||
// NamedMetricsAll is false.
|
||||
NamedMetrics map[string]struct{}
|
||||
}
|
||||
|
||||
// Equal returns whether the two LRSReportEndpointMetricsConfig configurations
|
||||
// are identical.
|
||||
func (bmp *LRSReportEndpointMetricsConfig) Equal(other *LRSReportEndpointMetricsConfig) bool {
|
||||
switch {
|
||||
case bmp == nil && other == nil:
|
||||
return true
|
||||
case (bmp != nil) != (other != nil):
|
||||
return false
|
||||
}
|
||||
if bmp.CPUUtilization != other.CPUUtilization ||
|
||||
bmp.MemUtilization != other.MemUtilization ||
|
||||
bmp.ApplicationUtilization != other.ApplicationUtilization ||
|
||||
bmp.NamedMetricsAll != other.NamedMetricsAll {
|
||||
return false
|
||||
}
|
||||
if len(bmp.NamedMetrics) != len(other.NamedMetrics) {
|
||||
return false
|
||||
}
|
||||
for k := range bmp.NamedMetrics {
|
||||
if _, ok := other.NamedMetrics[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SecurityConfig contains the security configuration received as part of the
|
||||
// Cluster resource on the client-side, and as part of the Listener resource on
|
||||
// the server-side.
|
||||
type SecurityConfig struct {
|
||||
// RootInstanceName identifies the certProvider plugin to be used to fetch
|
||||
// root certificates. This instance name will be resolved to the plugin name
|
||||
// and its associated configuration from the certificate_providers field of
|
||||
// the bootstrap file.
|
||||
RootInstanceName string
|
||||
// RootCertName is the certificate name to be passed to the plugin (looked
|
||||
// up from the bootstrap file) while fetching root certificates.
|
||||
RootCertName string
|
||||
// IdentityInstanceName identifies the certProvider plugin to be used to
|
||||
// fetch identity certificates. This instance name will be resolved to the
|
||||
// plugin name and its associated configuration from the
|
||||
// certificate_providers field of the bootstrap file.
|
||||
IdentityInstanceName string
|
||||
// IdentityCertName is the certificate name to be passed to the plugin
|
||||
// (looked up from the bootstrap file) while fetching identity certificates.
|
||||
IdentityCertName string
|
||||
// SubjectAltNameMatchers is an optional list of match criteria for SANs
|
||||
// specified on the peer certificate. Used only on the client-side.
|
||||
//
|
||||
// Some intricacies:
|
||||
// - If this field is empty, then any peer certificate is accepted.
|
||||
// - If the peer certificate contains a wildcard DNS SAN, and an `exact`
|
||||
// matcher is configured, a wildcard DNS match is performed instead of a
|
||||
// regular string comparison.
|
||||
SubjectAltNameMatchers []matcher.StringMatcher
|
||||
// RequireClientCert indicates if the server handshake process expects the
|
||||
// client to present a certificate. Set to true when performing mTLS. Used
|
||||
// only on the server-side.
|
||||
RequireClientCert bool
|
||||
// UseSystemRootCerts indicates that the client should use system root
|
||||
// certificates to validate the server certificate. This field is mutually
|
||||
// exclusive with RootCertName and RootInstanceName. Validation performed
|
||||
// after unmarshalling xDS resources ensures that this field is set only
|
||||
// when both RootCertName and RootInstanceName are empty.
|
||||
UseSystemRootCerts bool
|
||||
// SNI is the string to be used as the Server Name when creating TLS
|
||||
// configurations for the handshake. An empty string for SNI value will be
|
||||
// treated as SNI not specified.
|
||||
SNI string
|
||||
// UseAutoHostSNI indicates whether to set the ServerName for the TLS handshake
|
||||
// configuration to the hostname (if available). The host is the DNS
|
||||
// hostname for DNS clusters, or Endpoint.hostname for EDS clusters. The
|
||||
// port will not be included in the SNI value.
|
||||
UseAutoHostSNI bool
|
||||
// AutoSNISANValidation indicates whether to replace any Subject Alternative
|
||||
// Name (SAN) matchers with a validation for a DNS SAN matching the SNI
|
||||
// value sent. This validation uses the SNI being set in the TLS
|
||||
// configuration, regardless of how the SNI is determined.
|
||||
AutoSNISANValidation bool
|
||||
}
|
||||
|
||||
// Equal returns true if sc is equal to other.
|
||||
func (sc *SecurityConfig) Equal(other *SecurityConfig) bool {
|
||||
switch {
|
||||
case sc == nil && other == nil:
|
||||
return true
|
||||
case (sc != nil) != (other != nil):
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case sc.RootInstanceName != other.RootInstanceName:
|
||||
return false
|
||||
case sc.RootCertName != other.RootCertName:
|
||||
return false
|
||||
case sc.IdentityInstanceName != other.IdentityInstanceName:
|
||||
return false
|
||||
case sc.IdentityCertName != other.IdentityCertName:
|
||||
return false
|
||||
case sc.RequireClientCert != other.RequireClientCert:
|
||||
return false
|
||||
case sc.UseSystemRootCerts != other.UseSystemRootCerts:
|
||||
return false
|
||||
case sc.SNI != other.SNI:
|
||||
return false
|
||||
case sc.UseAutoHostSNI != other.UseAutoHostSNI:
|
||||
return false
|
||||
case sc.AutoSNISANValidation != other.AutoSNISANValidation:
|
||||
return false
|
||||
default:
|
||||
if len(sc.SubjectAltNameMatchers) != len(other.SubjectAltNameMatchers) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(sc.SubjectAltNameMatchers); i++ {
|
||||
if !sc.SubjectAltNameMatchers[i].Equal(other.SubjectAltNameMatchers[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
78
internal/xds/xdsclient/xdsresource/type_eds.go
Normal file
78
internal/xds/xdsclient/xdsresource/type_eds.go
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// OverloadDropConfig contains the config to drop overloads.
|
||||
type OverloadDropConfig struct {
|
||||
Category string
|
||||
Numerator uint32
|
||||
Denominator uint32
|
||||
}
|
||||
|
||||
// EndpointHealthStatus represents the health status of an endpoint.
|
||||
type EndpointHealthStatus int32
|
||||
|
||||
const (
|
||||
// EndpointHealthStatusUnknown represents HealthStatus UNKNOWN.
|
||||
EndpointHealthStatusUnknown EndpointHealthStatus = iota
|
||||
// EndpointHealthStatusHealthy represents HealthStatus HEALTHY.
|
||||
EndpointHealthStatusHealthy
|
||||
// EndpointHealthStatusUnhealthy represents HealthStatus UNHEALTHY.
|
||||
EndpointHealthStatusUnhealthy
|
||||
// EndpointHealthStatusDraining represents HealthStatus DRAINING.
|
||||
EndpointHealthStatusDraining
|
||||
// EndpointHealthStatusTimeout represents HealthStatus TIMEOUT.
|
||||
EndpointHealthStatusTimeout
|
||||
// EndpointHealthStatusDegraded represents HealthStatus DEGRADED.
|
||||
EndpointHealthStatusDegraded
|
||||
)
|
||||
|
||||
// Endpoint contains information of an endpoint.
|
||||
type Endpoint struct {
|
||||
ResolverEndpoint resolver.Endpoint
|
||||
HealthStatus EndpointHealthStatus
|
||||
Weight uint32
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
// Locality contains information of a locality.
|
||||
type Locality struct {
|
||||
Endpoints []Endpoint
|
||||
ID clients.Locality
|
||||
Priority uint32
|
||||
Weight uint32
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
// EndpointsUpdate contains an EDS update.
|
||||
type EndpointsUpdate struct {
|
||||
Drops []OverloadDropConfig
|
||||
// Localities in the EDS response with `load_balancing_weight` field not set
|
||||
// or explicitly set to 0 are ignored while parsing the resource, and
|
||||
// therefore do not show up here.
|
||||
Localities []Locality
|
||||
|
||||
// Raw is the resource from the xds response.
|
||||
Raw *anypb.Any
|
||||
}
|
||||
92
internal/xds/xdsclient/xdsresource/type_lds.go
Normal file
92
internal/xds/xdsclient/xdsresource/type_lds.go
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// ListenerUpdate contains information received in an LDS response, which is of
|
||||
// interest to the registered LDS watcher.
|
||||
type ListenerUpdate struct {
|
||||
// APIListener contains the HTTP connection manager configuration.
|
||||
APIListener *HTTPConnectionManagerConfig
|
||||
// TCPListener contains inbound listener configuration.
|
||||
TCPListener *InboundListenerConfig
|
||||
|
||||
// Raw is the resource from the xds response.
|
||||
Raw *anypb.Any
|
||||
}
|
||||
|
||||
// HTTPConnectionManagerConfig contains the HTTP connection manager configuration.
|
||||
type HTTPConnectionManagerConfig struct {
|
||||
// RouteConfigName is the route configuration name corresponding to the
|
||||
// target which is being watched through LDS.
|
||||
//
|
||||
// Exactly one of RouteConfigName and InlineRouteConfig is set.
|
||||
RouteConfigName string
|
||||
// InlineRouteConfig is the inline route configuration (RDS response)
|
||||
// returned inside LDS.
|
||||
//
|
||||
// Exactly one of RouteConfigName and InlineRouteConfig is set.
|
||||
InlineRouteConfig *RouteConfigUpdate
|
||||
|
||||
// MaxStreamDuration contains the HTTP connection manager's
|
||||
// common_http_protocol_options.max_stream_duration field, or zero if
|
||||
// unset.
|
||||
MaxStreamDuration time.Duration
|
||||
// HTTPFilters is a list of HTTP filters (name, config) from the LDS
|
||||
// response.
|
||||
HTTPFilters []HTTPFilter
|
||||
}
|
||||
|
||||
// HTTPFilter represents one HTTP filter from an LDS response's HTTP connection
|
||||
// manager field.
|
||||
type HTTPFilter struct {
|
||||
// Name is an arbitrary name of the filter. Used for applying override
|
||||
// settings in virtual host / route / weighted cluster configuration (not
|
||||
// yet supported).
|
||||
Name string
|
||||
// Filter is the HTTP filter found in the registry for the config type.
|
||||
Filter httpfilter.Builder
|
||||
// Config contains the filter's configuration
|
||||
Config httpfilter.FilterConfig
|
||||
// Disabled specifies if the filter is disabled. For more information, see
|
||||
// envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_filters#route-based-filter-chain
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// InboundListenerConfig contains information about the inbound listener, i.e
|
||||
// the server-side listener.
|
||||
type InboundListenerConfig struct {
|
||||
// Address is the local address on which the inbound listener is expected to
|
||||
// accept incoming connections.
|
||||
Address string
|
||||
// Port is the local port on which the inbound listener is expected to
|
||||
// accept incoming connections.
|
||||
Port string
|
||||
|
||||
// DefaultFilterChain is the default filter chain to use if no other filter
|
||||
// chain matches.
|
||||
DefaultFilterChain NetworkFilterChainConfig
|
||||
// FilterChains contains the filter chains associated with this listener.
|
||||
FilterChains NetworkFilterChainMap
|
||||
}
|
||||
184
internal/xds/xdsclient/xdsresource/type_rds.go
Normal file
184
internal/xds/xdsclient/xdsresource/type_rds.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/xds/clusterspecifier"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// RouteConfigUpdate contains information received in an RDS response, which is
|
||||
// of interest to the registered RDS watcher.
|
||||
type RouteConfigUpdate struct {
|
||||
VirtualHosts []*VirtualHost
|
||||
// ClusterSpecifierPlugins are the LB Configurations for any
|
||||
// ClusterSpecifierPlugins referenced by the Route Table.
|
||||
ClusterSpecifierPlugins map[string]clusterspecifier.BalancerConfig
|
||||
// Raw is the resource from the xds response.
|
||||
Raw *anypb.Any
|
||||
}
|
||||
|
||||
// VirtualHost contains the routes for a list of Domains.
|
||||
//
|
||||
// Note that the domains in this slice can be a wildcard, not an exact string.
|
||||
// The consumer of this struct needs to find the best match for its hostname.
|
||||
type VirtualHost struct {
|
||||
Domains []string
|
||||
// Routes contains a list of routes, each containing matchers and
|
||||
// corresponding action.
|
||||
Routes []*Route
|
||||
// HTTPFilterConfigOverride contains any HTTP filter config overrides for
|
||||
// the virtual host which may be present. An individual filter's override
|
||||
// may be unused if the matching Route contains an override for that
|
||||
// filter.
|
||||
HTTPFilterConfigOverride map[string]httpfilter.FilterConfig
|
||||
RetryConfig *RetryConfig
|
||||
}
|
||||
|
||||
// RetryConfig contains all retry-related configuration in either a VirtualHost
|
||||
// or Route.
|
||||
type RetryConfig struct {
|
||||
// RetryOn is a set of status codes on which to retry. Only Canceled,
|
||||
// DeadlineExceeded, Internal, ResourceExhausted, and Unavailable are
|
||||
// supported; any other values will be omitted.
|
||||
RetryOn map[codes.Code]bool
|
||||
NumRetries uint32 // maximum number of retry attempts
|
||||
RetryBackoff RetryBackoff // retry backoff policy
|
||||
}
|
||||
|
||||
// RetryBackoff describes the backoff policy for retries.
|
||||
type RetryBackoff struct {
|
||||
BaseInterval time.Duration // initial backoff duration between attempts
|
||||
MaxInterval time.Duration // maximum backoff duration
|
||||
}
|
||||
|
||||
// HashPolicyType specifies the type of HashPolicy from a received RDS Response.
|
||||
type HashPolicyType int
|
||||
|
||||
const (
|
||||
// HashPolicyTypeHeader specifies to hash a Header in the incoming request.
|
||||
HashPolicyTypeHeader HashPolicyType = iota
|
||||
// HashPolicyTypeChannelID specifies to hash a unique Identifier of the
|
||||
// Channel. This is a 64-bit random int computed at initialization time.
|
||||
HashPolicyTypeChannelID
|
||||
)
|
||||
|
||||
// HashPolicy specifies the HashPolicy if the upstream cluster uses a hashing
|
||||
// load balancer.
|
||||
type HashPolicy struct {
|
||||
HashPolicyType HashPolicyType
|
||||
Terminal bool
|
||||
// Fields used for type HEADER.
|
||||
HeaderName string
|
||||
Regex *regexp.Regexp
|
||||
RegexSubstitution string
|
||||
}
|
||||
|
||||
// RouteActionType is the action of the route from a received RDS response.
|
||||
type RouteActionType int
|
||||
|
||||
const (
|
||||
// RouteActionUnsupported are routing types currently unsupported by grpc.
|
||||
// According to A36, "A Route with an inappropriate action causes RPCs
|
||||
// matching that route to fail."
|
||||
RouteActionUnsupported RouteActionType = iota
|
||||
// RouteActionRoute is the expected route type on the client side. Route
|
||||
// represents routing a request to some upstream cluster. On the client
|
||||
// side, if an RPC matches to a route that is not RouteActionRoute, the RPC
|
||||
// will fail according to A36.
|
||||
RouteActionRoute
|
||||
// RouteActionNonForwardingAction is the expected route type on the server
|
||||
// side. NonForwardingAction represents when a route will generate a
|
||||
// response directly, without forwarding to an upstream host.
|
||||
RouteActionNonForwardingAction
|
||||
)
|
||||
|
||||
// Route is both a specification of how to match a request as well as an
|
||||
// indication of the action to take upon match.
|
||||
type Route struct {
|
||||
Path *string
|
||||
Prefix *string
|
||||
Regex *regexp.Regexp
|
||||
// Indicates if prefix/path matching should be case insensitive. The default
|
||||
// is false (case sensitive).
|
||||
CaseInsensitive bool
|
||||
Headers []*HeaderMatcher
|
||||
Fraction *uint32
|
||||
|
||||
HashPolicies []*HashPolicy
|
||||
|
||||
// If the matchers above indicate a match, the below configuration is used.
|
||||
// If MaxStreamDuration is nil, it indicates neither of the route action's
|
||||
// max_stream_duration fields (grpc_timeout_header_max nor
|
||||
// max_stream_duration) were set. In this case, the ListenerUpdate's
|
||||
// MaxStreamDuration field should be used. If MaxStreamDuration is set to
|
||||
// an explicit zero duration, the application's deadline should be used.
|
||||
MaxStreamDuration *time.Duration
|
||||
// HTTPFilterConfigOverride contains any HTTP filter config overrides for
|
||||
// the route which may be present. An individual filter's override may be
|
||||
// unused if the matching WeightedCluster contains an override for that
|
||||
// filter.
|
||||
HTTPFilterConfigOverride map[string]httpfilter.FilterConfig
|
||||
RetryConfig *RetryConfig
|
||||
|
||||
ActionType RouteActionType
|
||||
|
||||
// Only one of the following fields (WeightedClusters or
|
||||
// ClusterSpecifierPlugin) will be set for a route.
|
||||
WeightedClusters []WeightedCluster
|
||||
// ClusterSpecifierPlugin is the name of the Cluster Specifier Plugin that
|
||||
// this Route is linked to, if specified by xDS.
|
||||
ClusterSpecifierPlugin string
|
||||
// AutoHostRewrite indicates that the ":authority" header can be rewritten
|
||||
// to the hostname of the upstream endpoint.
|
||||
AutoHostRewrite bool
|
||||
}
|
||||
|
||||
// WeightedCluster contains settings for an xds ActionType.WeightedCluster.
|
||||
type WeightedCluster struct {
|
||||
// Name is the name of the cluster.
|
||||
Name string
|
||||
// Weight is the relative weight of the cluster. It will never be zero.
|
||||
Weight uint32
|
||||
// HTTPFilterConfigOverride contains any HTTP filter config overrides for
|
||||
// the weighted cluster which may be present.
|
||||
HTTPFilterConfigOverride map[string]httpfilter.FilterConfig
|
||||
}
|
||||
|
||||
// HeaderMatcher represents header matchers. Exact, prefix, suffix and contains
|
||||
// matches are represented as a StringMatch.
|
||||
type HeaderMatcher struct {
|
||||
Name string
|
||||
InvertMatch *bool
|
||||
RegexMatch *regexp.Regexp
|
||||
RangeMatch *Int64Range
|
||||
PresentMatch *bool
|
||||
StringMatch *matcher.StringMatcher
|
||||
}
|
||||
|
||||
// Int64Range is a range for header range match.
|
||||
type Int64Range struct {
|
||||
Start int64
|
||||
End int64
|
||||
}
|
||||
805
internal/xds/xdsclient/xdsresource/unmarshal_cds.go
Normal file
805
internal/xds/xdsclient/xdsresource/unmarshal_cds.go
Normal file
@@ -0,0 +1,805 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3aggregateclusterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3"
|
||||
v3http11proxypb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/http_11_proxy/v3"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdslbregistry"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// ValidateClusterAndConstructClusterUpdateForTesting exports the
|
||||
// validateClusterAndConstructClusterUpdate function for testing purposes.
|
||||
var ValidateClusterAndConstructClusterUpdateForTesting = validateClusterAndConstructClusterUpdate
|
||||
|
||||
const (
|
||||
maxSNILength = 255
|
||||
// TransportSocket proto message has a `name` field which is expected to be set
|
||||
// to this value by the management server.
|
||||
transportSocketName = "envoy.transport_sockets.tls"
|
||||
)
|
||||
|
||||
func unmarshalClusterResource(r *anypb.Any, serverCfg *bootstrap.ServerConfig) (string, ClusterUpdate, error) {
|
||||
r, err := UnwrapResource(r)
|
||||
if err != nil {
|
||||
return "", ClusterUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
|
||||
}
|
||||
|
||||
if !IsClusterResource(r.GetTypeUrl()) {
|
||||
return "", ClusterUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl())
|
||||
}
|
||||
|
||||
cluster := &v3clusterpb.Cluster{}
|
||||
if err := proto.Unmarshal(r.GetValue(), cluster); err != nil {
|
||||
return "", ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||
}
|
||||
|
||||
if cluster.GetName() == "" {
|
||||
return "", ClusterUpdate{}, fmt.Errorf("empty resource name in Cluster resource")
|
||||
}
|
||||
cu, err := validateClusterAndConstructClusterUpdate(cluster, serverCfg)
|
||||
if err != nil {
|
||||
return cluster.GetName(), ClusterUpdate{}, err
|
||||
}
|
||||
cu.Raw = r
|
||||
|
||||
return cluster.GetName(), cu, nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRingHashMinSize = 1024
|
||||
defaultRingHashMaxSize = 8 * 1024 * 1024 // 8M
|
||||
ringHashSizeUpperBound = 8 * 1024 * 1024 // 8M
|
||||
|
||||
defaultLeastRequestChoiceCount = 2
|
||||
)
|
||||
|
||||
func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serverCfg *bootstrap.ServerConfig) (ClusterUpdate, error) {
|
||||
telemetryLabels := make(map[string]string)
|
||||
if fmd := cluster.GetMetadata().GetFilterMetadata(); fmd != nil {
|
||||
if val, ok := fmd["com.google.csm.telemetry_labels"]; ok {
|
||||
if fields := val.GetFields(); fields != nil {
|
||||
if val, ok := fields["service_name"]; ok {
|
||||
if _, ok := val.GetKind().(*structpb.Value_StringValue); ok {
|
||||
telemetryLabels["csm.service_name"] = val.GetStringValue()
|
||||
}
|
||||
}
|
||||
if val, ok := fields["service_namespace"]; ok {
|
||||
if _, ok := val.GetKind().(*structpb.Value_StringValue); ok {
|
||||
telemetryLabels["csm.service_namespace_name"] = val.GetStringValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// "The values for the service labels csm.service_name and
|
||||
// csm.service_namespace_name come from xDS, “unknown” if not present." -
|
||||
// CSM Design.
|
||||
if _, ok := telemetryLabels["csm.service_name"]; !ok {
|
||||
telemetryLabels["csm.service_name"] = "unknown"
|
||||
}
|
||||
if _, ok := telemetryLabels["csm.service_namespace_name"]; !ok {
|
||||
telemetryLabels["csm.service_namespace_name"] = "unknown"
|
||||
}
|
||||
|
||||
var lbPolicy json.RawMessage
|
||||
var err error
|
||||
switch cluster.GetLbPolicy() {
|
||||
case v3clusterpb.Cluster_ROUND_ROBIN:
|
||||
lbPolicy = []byte(`[{"xds_wrr_locality_experimental": {"childPolicy": [{"round_robin": {}}]}}]`)
|
||||
case v3clusterpb.Cluster_RING_HASH:
|
||||
rhc := cluster.GetRingHashLbConfig()
|
||||
if rhc.GetHashFunction() != v3clusterpb.Cluster_RingHashLbConfig_XX_HASH {
|
||||
return ClusterUpdate{}, fmt.Errorf("unsupported ring_hash hash function %v in response: %+v", rhc.GetHashFunction(), cluster)
|
||||
}
|
||||
// Minimum defaults to 1024 entries, and limited to 8M entries Maximum
|
||||
// defaults to 8M entries, and limited to 8M entries
|
||||
var minSize, maxSize uint64 = defaultRingHashMinSize, defaultRingHashMaxSize
|
||||
if min := rhc.GetMinimumRingSize(); min != nil {
|
||||
minSize = min.GetValue()
|
||||
}
|
||||
if max := rhc.GetMaximumRingSize(); max != nil {
|
||||
maxSize = max.GetValue()
|
||||
}
|
||||
|
||||
rhLBCfg := []byte(fmt.Sprintf("{\"minRingSize\": %d, \"maxRingSize\": %d}", minSize, maxSize))
|
||||
lbPolicy = []byte(fmt.Sprintf(`[{"ring_hash_experimental": %s}]`, rhLBCfg))
|
||||
case v3clusterpb.Cluster_LEAST_REQUEST:
|
||||
// "The configuration for the Least Request LB policy is the
|
||||
// least_request_lb_config field. The field is optional; if not present,
|
||||
// defaults will be assumed for all of its values." - A48
|
||||
lr := cluster.GetLeastRequestLbConfig()
|
||||
var choiceCount uint32 = defaultLeastRequestChoiceCount
|
||||
if cc := lr.GetChoiceCount(); cc != nil {
|
||||
choiceCount = cc.GetValue()
|
||||
}
|
||||
// "If choice_count < 2, the config will be rejected." - A48
|
||||
if choiceCount < 2 {
|
||||
return ClusterUpdate{}, fmt.Errorf("Cluster_LeastRequestLbConfig.ChoiceCount must be >= 2, got: %v", choiceCount)
|
||||
}
|
||||
|
||||
lrLBCfg := []byte(fmt.Sprintf("{\"choiceCount\": %d}", choiceCount))
|
||||
lbPolicy = []byte(fmt.Sprintf(`[{"least_request_experimental": %s}]`, lrLBCfg))
|
||||
default:
|
||||
return ClusterUpdate{}, fmt.Errorf("unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster)
|
||||
}
|
||||
// Process security configuration received from the control plane iff the
|
||||
// corresponding environment variable is set.
|
||||
var sc *SecurityConfig
|
||||
var isHTTP11ProxyEnabled bool
|
||||
if sc, isHTTP11ProxyEnabled, err = securityConfigFromCluster(cluster); err != nil {
|
||||
return ClusterUpdate{}, err
|
||||
}
|
||||
|
||||
// Process outlier detection received from the control plane iff the
|
||||
// corresponding environment variable is set.
|
||||
var od json.RawMessage
|
||||
if od, err = outlierConfigFromCluster(cluster); err != nil {
|
||||
return ClusterUpdate{}, err
|
||||
}
|
||||
|
||||
if cluster.GetLoadBalancingPolicy() != nil {
|
||||
lbPolicy, err = xdslbregistry.ConvertToServiceConfig(cluster.GetLoadBalancingPolicy(), 0)
|
||||
if err != nil {
|
||||
return ClusterUpdate{}, fmt.Errorf("error converting LoadBalancingPolicy %v in response: %+v: %v", cluster.GetLoadBalancingPolicy(), cluster, err)
|
||||
}
|
||||
// "It will be the responsibility of the XdsClient to validate the
|
||||
// converted configuration. It will do this by having the gRPC LB policy
|
||||
// registry parse the configuration." - A52
|
||||
bc := &iserviceconfig.BalancerConfig{}
|
||||
if err := json.Unmarshal(lbPolicy, bc); err != nil {
|
||||
return ClusterUpdate{}, fmt.Errorf("JSON generated from xDS LB policy registry: %s is invalid: %v", pretty.FormatJSON(lbPolicy), err)
|
||||
}
|
||||
}
|
||||
var lrsReportEndpointMetrics *LRSReportEndpointMetricsConfig
|
||||
if envconfig.XDSORCAToLRSPropEnabled && len(cluster.GetLrsReportEndpointMetrics()) > 0 {
|
||||
lrsReportEndpointMetrics = &LRSReportEndpointMetricsConfig{
|
||||
NamedMetrics: make(map[string]struct{}),
|
||||
}
|
||||
for _, m := range cluster.GetLrsReportEndpointMetrics() {
|
||||
switch m {
|
||||
case "cpu_utilization":
|
||||
lrsReportEndpointMetrics.CPUUtilization = true
|
||||
case "mem_utilization":
|
||||
lrsReportEndpointMetrics.MemUtilization = true
|
||||
case "application_utilization":
|
||||
lrsReportEndpointMetrics.ApplicationUtilization = true
|
||||
case "named_metrics.*":
|
||||
// If "named_metrics.*" is present, it takes precedence over any specific
|
||||
// "named_metrics.foo" fields. Per gRFC A85, specific named metrics are ignored
|
||||
// if NamedMetricsAll is true. We clear the map to save memory.
|
||||
lrsReportEndpointMetrics.NamedMetricsAll = true
|
||||
lrsReportEndpointMetrics.NamedMetrics = nil
|
||||
default:
|
||||
if lrsReportEndpointMetrics.NamedMetricsAll {
|
||||
continue
|
||||
}
|
||||
if name, found := strings.CutPrefix(m, "named_metrics."); found && name != "" {
|
||||
lrsReportEndpointMetrics.NamedMetrics[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadata map[string]any
|
||||
if envconfig.GCPAuthenticationFilterEnabled {
|
||||
var err error
|
||||
if metadata, err = validateAndConstructMetadata(cluster.GetMetadata()); err != nil {
|
||||
return ClusterUpdate{}, err
|
||||
}
|
||||
}
|
||||
|
||||
ret := ClusterUpdate{
|
||||
ClusterName: cluster.GetName(),
|
||||
SecurityCfg: sc,
|
||||
MaxRequests: circuitBreakersFromCluster(cluster),
|
||||
LBPolicy: lbPolicy,
|
||||
OutlierDetection: od,
|
||||
TelemetryLabels: telemetryLabels,
|
||||
LRSReportEndpointMetrics: lrsReportEndpointMetrics,
|
||||
Metadata: metadata,
|
||||
IsHTTP11ProxyEnabled: isHTTP11ProxyEnabled,
|
||||
}
|
||||
|
||||
if lrs := cluster.GetLrsServer(); lrs != nil {
|
||||
if lrs.GetSelf() == nil {
|
||||
return ClusterUpdate{}, fmt.Errorf("unsupported config_source_specifier %T in lrs_server field", lrs.ConfigSourceSpecifier)
|
||||
}
|
||||
ret.LRSServerConfig = serverCfg
|
||||
}
|
||||
|
||||
// Validate and set cluster type from the response.
|
||||
switch {
|
||||
case cluster.GetType() == v3clusterpb.Cluster_EDS:
|
||||
if configsource := cluster.GetEdsClusterConfig().GetEdsConfig(); configsource.GetAds() == nil && configsource.GetSelf() == nil {
|
||||
return ClusterUpdate{}, fmt.Errorf("CDS's EDS config source is not ADS or Self: %+v", cluster)
|
||||
}
|
||||
ret.ClusterType = ClusterTypeEDS
|
||||
ret.EDSServiceName = cluster.GetEdsClusterConfig().GetServiceName()
|
||||
if strings.HasPrefix(ret.ClusterName, "xdstp:") && ret.EDSServiceName == "" {
|
||||
return ClusterUpdate{}, fmt.Errorf("CDS's EDS service name is not set with a new-style cluster name: %+v", cluster)
|
||||
}
|
||||
return ret, nil
|
||||
case cluster.GetType() == v3clusterpb.Cluster_LOGICAL_DNS:
|
||||
ret.ClusterType = ClusterTypeLogicalDNS
|
||||
dnsHN, err := dnsHostNameFromCluster(cluster)
|
||||
if err != nil {
|
||||
return ClusterUpdate{}, err
|
||||
}
|
||||
ret.DNSHostName = dnsHN
|
||||
return ret, nil
|
||||
case cluster.GetClusterType() != nil && cluster.GetClusterType().Name == "envoy.clusters.aggregate":
|
||||
clusters := &v3aggregateclusterpb.ClusterConfig{}
|
||||
if err := proto.Unmarshal(cluster.GetClusterType().GetTypedConfig().GetValue(), clusters); err != nil {
|
||||
return ClusterUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||
}
|
||||
if len(clusters.Clusters) == 0 {
|
||||
return ClusterUpdate{}, fmt.Errorf("xds: aggregate cluster has empty clusters field in response: %+v", cluster)
|
||||
}
|
||||
ret.ClusterType = ClusterTypeAggregate
|
||||
ret.PrioritizedClusterNames = clusters.Clusters
|
||||
return ret, nil
|
||||
default:
|
||||
return ClusterUpdate{}, fmt.Errorf("unsupported cluster type (%v, %v) in response: %+v", cluster.GetType(), cluster.GetClusterType(), cluster)
|
||||
}
|
||||
}
|
||||
|
||||
// dnsHostNameFromCluster extracts the DNS host name from the cluster's load
|
||||
// assignment.
|
||||
//
|
||||
// There should be exactly one locality, with one endpoint, whose address
|
||||
// contains the address and port.
|
||||
func dnsHostNameFromCluster(cluster *v3clusterpb.Cluster) (string, error) {
|
||||
loadAssignment := cluster.GetLoadAssignment()
|
||||
if loadAssignment == nil {
|
||||
return "", fmt.Errorf("load_assignment not present for LOGICAL_DNS cluster")
|
||||
}
|
||||
if len(loadAssignment.GetEndpoints()) != 1 {
|
||||
return "", fmt.Errorf("load_assignment for LOGICAL_DNS cluster must have exactly one locality, got: %+v", loadAssignment)
|
||||
}
|
||||
endpoints := loadAssignment.GetEndpoints()[0].GetLbEndpoints()
|
||||
if len(endpoints) != 1 {
|
||||
return "", fmt.Errorf("locality for LOGICAL_DNS cluster must have exactly one endpoint, got: %+v", endpoints)
|
||||
}
|
||||
endpoint := endpoints[0].GetEndpoint()
|
||||
if endpoint == nil {
|
||||
return "", fmt.Errorf("endpoint for LOGICAL_DNS cluster not set")
|
||||
}
|
||||
socketAddr := endpoint.GetAddress().GetSocketAddress()
|
||||
if socketAddr == nil {
|
||||
return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set")
|
||||
}
|
||||
if socketAddr.GetResolverName() != "" {
|
||||
return "", fmt.Errorf("socket address for endpoint for LOGICAL_DNS cluster not set has unexpected custom resolver name: %v", socketAddr.GetResolverName())
|
||||
}
|
||||
host := socketAddr.GetAddress()
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("host for endpoint for LOGICAL_DNS cluster not set")
|
||||
}
|
||||
port := socketAddr.GetPortValue()
|
||||
if port == 0 {
|
||||
return "", fmt.Errorf("port for endpoint for LOGICAL_DNS cluster not set")
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(int(port))), nil
|
||||
}
|
||||
|
||||
// securityConfigFromCluster extracts the relevant security configuration from
|
||||
// the received Cluster resource.
|
||||
func securityConfigFromCluster(cluster *v3clusterpb.Cluster) (*SecurityConfig, bool, error) {
|
||||
if tsm := cluster.GetTransportSocketMatches(); len(tsm) != 0 {
|
||||
return nil, false, fmt.Errorf("unsupported transport_socket_matches field is non-empty: %+v", tsm)
|
||||
}
|
||||
// The Cluster resource contains a `transport_socket` field, which contains
|
||||
// a oneof `typed_config` field of type `protobuf.Any`. The any proto
|
||||
// contains a marshaled representation of an `UpstreamTlsContext` message.
|
||||
ts := cluster.GetTransportSocket()
|
||||
if ts == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
var isHTTP11ProxyEnabled bool
|
||||
tc := ts.GetTypedConfig()
|
||||
typeURL := tc.GetTypeUrl()
|
||||
|
||||
if envconfig.XDSHTTPConnectEnabled && typeURL == version.V3HTTP11ProxyUpstreamTransportURL {
|
||||
isHTTP11ProxyEnabled = true
|
||||
http11Proxy := &v3http11proxypb.Http11ProxyUpstreamTransport{}
|
||||
if err := proto.Unmarshal(tc.GetValue(), http11Proxy); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal Http11ProxyUpstreamTransport in CDS response: %v", err)
|
||||
}
|
||||
ts = http11Proxy.GetTransportSocket()
|
||||
if ts == nil {
|
||||
return nil, true, nil
|
||||
}
|
||||
tc = ts.GetTypedConfig()
|
||||
typeURL = tc.GetTypeUrl()
|
||||
}
|
||||
|
||||
if name := ts.GetName(); name != transportSocketName {
|
||||
return nil, false, fmt.Errorf("transport_socket field has unexpected name: %s", name)
|
||||
}
|
||||
if typeURL != version.V3UpstreamTLSContextURL {
|
||||
return nil, false, fmt.Errorf("transport_socket missing typed_config or wrong type_url: %q", typeURL)
|
||||
}
|
||||
upstreamCtx := &v3tlspb.UpstreamTlsContext{}
|
||||
if err := proto.Unmarshal(tc.GetValue(), upstreamCtx); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal UpstreamTlsContext in CDS response: %v", err)
|
||||
}
|
||||
// The following fields from `UpstreamTlsContext` are ignored:
|
||||
// - allow_renegotiation
|
||||
// - max_session_keys
|
||||
if upstreamCtx.GetCommonTlsContext() == nil {
|
||||
return nil, false, errors.New("UpstreamTlsContext in CDS response does not contain a CommonTlsContext")
|
||||
}
|
||||
|
||||
sc, err := securityConfigFromCommonTLSContext(upstreamCtx.GetCommonTlsContext(), false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
// Set SNI related fields in SecurityConfig from UpstreamTlsContext if
|
||||
// `GRPC_EXPERIMENTAL_XDS_SNI` is enabled.
|
||||
if envconfig.XDSSNIEnabled {
|
||||
sc.SNI = upstreamCtx.GetSni()
|
||||
if len(sc.SNI) > maxSNILength {
|
||||
return nil, false, fmt.Errorf("SNI value %q in UpstreamTlsContext in CDS response exceeds max length of %d", sc.SNI, maxSNILength)
|
||||
}
|
||||
sc.UseAutoHostSNI = upstreamCtx.GetAutoHostSni()
|
||||
sc.AutoSNISANValidation = upstreamCtx.GetAutoSniSanValidation()
|
||||
}
|
||||
return sc, isHTTP11ProxyEnabled, nil
|
||||
}
|
||||
|
||||
// common is expected to be not nil.
|
||||
// The `alpn_protocols` field is ignored.
|
||||
func securityConfigFromCommonTLSContext(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
|
||||
if common.GetTlsParams() != nil {
|
||||
return nil, fmt.Errorf("unsupported tls_params field in CommonTlsContext message: %+v", common)
|
||||
}
|
||||
if common.GetCustomHandshaker() != nil {
|
||||
return nil, fmt.Errorf("unsupported custom_handshaker field in CommonTlsContext message: %+v", common)
|
||||
}
|
||||
|
||||
// For now, if we can't get a valid security config from the new fields, we
|
||||
// fallback to the old deprecated fields.
|
||||
// TODO: Drop support for deprecated fields. NACK if err != nil here.
|
||||
sc, err1 := securityConfigFromCommonTLSContextUsingNewFields(common, server)
|
||||
if sc == nil || sc.Equal(&SecurityConfig{}) {
|
||||
var err error
|
||||
sc, err = securityConfigFromCommonTLSContextWithDeprecatedFields(common, server)
|
||||
if err != nil {
|
||||
// Retain the validation error from using the new fields.
|
||||
return nil, errors.Join(err1, fmt.Errorf("failed to parse config using deprecated fields: %v", err))
|
||||
}
|
||||
}
|
||||
if sc != nil {
|
||||
// sc == nil is a valid case where the control plane has not sent us any
|
||||
// security configuration. xDS creds will use fallback creds.
|
||||
if server {
|
||||
if sc.IdentityInstanceName == "" {
|
||||
return nil, errors.New("security configuration on the server-side does not contain identity certificate provider instance name")
|
||||
}
|
||||
} else {
|
||||
if !sc.UseSystemRootCerts && sc.RootInstanceName == "" {
|
||||
return nil, errors.New("security configuration on the client-side does not contain root certificate provider instance name")
|
||||
}
|
||||
}
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func securityConfigFromCommonTLSContextWithDeprecatedFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
|
||||
// The `CommonTlsContext` contains a
|
||||
// `tls_certificate_certificate_provider_instance` field of type
|
||||
// `CertificateProviderInstance`, which contains the provider instance name
|
||||
// and the certificate name to fetch identity certs.
|
||||
sc := &SecurityConfig{}
|
||||
if identity := common.GetTlsCertificateCertificateProviderInstance(); identity != nil {
|
||||
sc.IdentityInstanceName = identity.GetInstanceName()
|
||||
sc.IdentityCertName = identity.GetCertificateName()
|
||||
}
|
||||
|
||||
// The `CommonTlsContext` contains a `validation_context_type` field which
|
||||
// is a oneof. We can get the values that we are interested in from two of
|
||||
// those possible values:
|
||||
// - combined validation context:
|
||||
// - contains a default validation context which holds the list of
|
||||
// matchers for accepted SANs.
|
||||
// - contains certificate provider instance configuration
|
||||
// - certificate provider instance configuration
|
||||
// - in this case, we do not get a list of accepted SANs.
|
||||
switch t := common.GetValidationContextType().(type) {
|
||||
case *v3tlspb.CommonTlsContext_CombinedValidationContext:
|
||||
combined := common.GetCombinedValidationContext()
|
||||
var matchers []matcher.StringMatcher
|
||||
if def := combined.GetDefaultValidationContext(); def != nil {
|
||||
for _, m := range def.GetMatchSubjectAltNames() {
|
||||
matcher, err := matcher.StringMatcherFromProto(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
}
|
||||
if server && len(matchers) != 0 {
|
||||
return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common)
|
||||
}
|
||||
sc.SubjectAltNameMatchers = matchers
|
||||
if pi := combined.GetValidationContextCertificateProviderInstance(); pi != nil {
|
||||
sc.RootInstanceName = pi.GetInstanceName()
|
||||
sc.RootCertName = pi.GetCertificateName()
|
||||
}
|
||||
case *v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance:
|
||||
pi := common.GetValidationContextCertificateProviderInstance()
|
||||
sc.RootInstanceName = pi.GetInstanceName()
|
||||
sc.RootCertName = pi.GetCertificateName()
|
||||
case nil:
|
||||
// It is valid for the validation context to be nil on the server side.
|
||||
default:
|
||||
return nil, fmt.Errorf("validation context contains unexpected type: %T", t)
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// gRFC A29 https://github.com/grpc/proposal/blob/master/A29-xds-tls-security.md
|
||||
// specifies the new way to fetch security configuration and says the following:
|
||||
//
|
||||
// Although there are various ways to obtain certificates as per this proto
|
||||
// (which are supported by Envoy), gRPC supports only one of them and that is
|
||||
// the `CertificateProviderPluginInstance` proto.
|
||||
//
|
||||
// This helper function attempts to fetch security configuration from the
|
||||
// `CertificateProviderPluginInstance` message, given a CommonTlsContext.
|
||||
func securityConfigFromCommonTLSContextUsingNewFields(common *v3tlspb.CommonTlsContext, server bool) (*SecurityConfig, error) {
|
||||
// The `tls_certificate_provider_instance` field of type
|
||||
// `CertificateProviderPluginInstance` is used to fetch the identity
|
||||
// certificate provider.
|
||||
sc := &SecurityConfig{}
|
||||
identity := common.GetTlsCertificateProviderInstance()
|
||||
if identity == nil && len(common.GetTlsCertificates()) != 0 {
|
||||
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)
|
||||
}
|
||||
if identity == nil && common.GetTlsCertificateSdsSecretConfigs() != nil {
|
||||
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)
|
||||
}
|
||||
sc.IdentityInstanceName = identity.GetInstanceName()
|
||||
sc.IdentityCertName = identity.GetCertificateName()
|
||||
|
||||
// The `CommonTlsContext` contains a oneof field `validation_context_type`,
|
||||
// which contains the `CertificateValidationContext` message in one of the
|
||||
// following ways:
|
||||
// - `validation_context` field
|
||||
// - this is directly of type `CertificateValidationContext`
|
||||
// - `combined_validation_context` field
|
||||
// - this is of type `CombinedCertificateValidationContext` and contains
|
||||
// a `default validation context` field of type
|
||||
// `CertificateValidationContext`
|
||||
//
|
||||
// The `CertificateValidationContext` message has the following fields that
|
||||
// we are interested in:
|
||||
// - `ca_certificate_provider_instance`
|
||||
// - this is of type `CertificateProviderPluginInstance`
|
||||
// - `system_root_certs`:
|
||||
// - This indicates the usage of system root certs for validation.
|
||||
// - `match_subject_alt_names`
|
||||
// - this is a list of string matchers
|
||||
//
|
||||
// The `CertificateProviderPluginInstance` message contains two fields
|
||||
// - instance_name
|
||||
// - this is the certificate provider instance name to be looked up in
|
||||
// the bootstrap configuration
|
||||
// - certificate_name
|
||||
// - this is an opaque name passed to the certificate provider
|
||||
var validationCtx *v3tlspb.CertificateValidationContext
|
||||
switch typ := common.GetValidationContextType().(type) {
|
||||
case *v3tlspb.CommonTlsContext_ValidationContext:
|
||||
validationCtx = common.GetValidationContext()
|
||||
case *v3tlspb.CommonTlsContext_CombinedValidationContext:
|
||||
validationCtx = common.GetCombinedValidationContext().GetDefaultValidationContext()
|
||||
case nil:
|
||||
// It is valid for the validation context to be nil on the server side.
|
||||
return sc, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("validation context contains unexpected type: %T", typ)
|
||||
}
|
||||
// If we get here, it means that the `CertificateValidationContext` message
|
||||
// was found through one of the supported ways. It is an error if the
|
||||
// validation context is specified, but it does not specify a way to
|
||||
// validate TLS certificates. Peer TLS certs can be verified in the
|
||||
// following ways:
|
||||
// 1. If the ca_certificate_provider_instance field is set, it contains
|
||||
// information about the certificate provider to be used for the root
|
||||
// certificates, else
|
||||
// 2. If the system_root_certs field is set, and the config is for a client,
|
||||
// use the system default root certs.
|
||||
useSystemRootCerts := false
|
||||
if validationCtx.GetCaCertificateProviderInstance() == nil && envconfig.XDSSystemRootCertsEnabled {
|
||||
if server {
|
||||
if validationCtx.GetSystemRootCerts() != nil {
|
||||
// The `system_root_certs` field will not be supported on the
|
||||
// gRPC server side. If `ca_certificate_provider_instance` is
|
||||
// unset and `system_root_certs` is set, the LDS resource will
|
||||
// be NACKed.
|
||||
// - A82
|
||||
return nil, fmt.Errorf("expected field ca_certificate_provider_instance is missing and unexpected field system_root_certs is set for server in CommonTlsContext message: %+v", common)
|
||||
}
|
||||
} else {
|
||||
if validationCtx.GetSystemRootCerts() != nil {
|
||||
useSystemRootCerts = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The following fields are ignored:
|
||||
// - trusted_ca
|
||||
// - watched_directory
|
||||
// - allow_expired_certificate
|
||||
// - trust_chain_verification
|
||||
switch {
|
||||
case len(validationCtx.GetVerifyCertificateSpki()) != 0:
|
||||
return nil, fmt.Errorf("unsupported verify_certificate_spki field in CommonTlsContext message: %+v", common)
|
||||
case len(validationCtx.GetVerifyCertificateHash()) != 0:
|
||||
return nil, fmt.Errorf("unsupported verify_certificate_hash field in CommonTlsContext message: %+v", common)
|
||||
case validationCtx.GetRequireSignedCertificateTimestamp().GetValue():
|
||||
return nil, fmt.Errorf("unsupported require_signed_certificate_timestamp field in CommonTlsContext message: %+v", common)
|
||||
case validationCtx.GetCrl() != nil:
|
||||
return nil, fmt.Errorf("unsupported crl field in CommonTlsContext message: %+v", common)
|
||||
case validationCtx.GetCustomValidatorConfig() != nil:
|
||||
return nil, fmt.Errorf("unsupported custom_validator_config field in CommonTlsContext message: %+v", common)
|
||||
}
|
||||
|
||||
if rootProvider := validationCtx.GetCaCertificateProviderInstance(); rootProvider != nil {
|
||||
sc.RootInstanceName = rootProvider.GetInstanceName()
|
||||
sc.RootCertName = rootProvider.GetCertificateName()
|
||||
} else if useSystemRootCerts {
|
||||
sc.UseSystemRootCerts = true
|
||||
} else if !server && envconfig.XDSSystemRootCertsEnabled {
|
||||
return nil, fmt.Errorf("expected fields ca_certificate_provider_instance and system_root_certs are missing in CommonTlsContext message: %+v", common)
|
||||
} else {
|
||||
// Don't mention the system_root_certs field if it was not checked.
|
||||
return nil, fmt.Errorf("expected field ca_certificate_provider_instance is missing in CommonTlsContext message: %+v", common)
|
||||
}
|
||||
|
||||
var matchers []matcher.StringMatcher
|
||||
for _, m := range validationCtx.GetMatchSubjectAltNames() {
|
||||
matcher, err := matcher.StringMatcherFromProto(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
if server && len(matchers) != 0 {
|
||||
return nil, fmt.Errorf("match_subject_alt_names field in validation context is not supported on the server: %v", common)
|
||||
}
|
||||
sc.SubjectAltNameMatchers = matchers
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// circuitBreakersFromCluster extracts the circuit breakers configuration from
|
||||
// the received cluster resource. Returns nil if no CircuitBreakers or no
|
||||
// Thresholds in CircuitBreakers.
|
||||
func circuitBreakersFromCluster(cluster *v3clusterpb.Cluster) *uint32 {
|
||||
for _, threshold := range cluster.GetCircuitBreakers().GetThresholds() {
|
||||
if threshold.GetPriority() != v3corepb.RoutingPriority_DEFAULT {
|
||||
continue
|
||||
}
|
||||
maxRequestsPb := threshold.GetMaxRequests()
|
||||
if maxRequestsPb == nil {
|
||||
return nil
|
||||
}
|
||||
maxRequests := maxRequestsPb.GetValue()
|
||||
return &maxRequests
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// idurationp takes a time.Duration and converts it to an internal duration, and
|
||||
// returns a pointer to that internal duration.
|
||||
func idurationp(d time.Duration) *iserviceconfig.Duration {
|
||||
id := iserviceconfig.Duration(d)
|
||||
return &id
|
||||
}
|
||||
|
||||
func uint32p(i uint32) *uint32 {
|
||||
return &i
|
||||
}
|
||||
|
||||
// Helper types to prepare Outlier Detection JSON. Pointer types to distinguish
|
||||
// between unset and a zero value.
|
||||
type successRateEjection struct {
|
||||
StdevFactor *uint32 `json:"stdevFactor,omitempty"`
|
||||
EnforcementPercentage *uint32 `json:"enforcementPercentage,omitempty"`
|
||||
MinimumHosts *uint32 `json:"minimumHosts,omitempty"`
|
||||
RequestVolume *uint32 `json:"requestVolume,omitempty"`
|
||||
}
|
||||
|
||||
type failurePercentageEjection struct {
|
||||
Threshold *uint32 `json:"threshold,omitempty"`
|
||||
EnforcementPercentage *uint32 `json:"enforcementPercentage,omitempty"`
|
||||
MinimumHosts *uint32 `json:"minimumHosts,omitempty"`
|
||||
RequestVolume *uint32 `json:"requestVolume,omitempty"`
|
||||
}
|
||||
|
||||
type odLBConfig struct {
|
||||
Interval *iserviceconfig.Duration `json:"interval,omitempty"`
|
||||
BaseEjectionTime *iserviceconfig.Duration `json:"baseEjectionTime,omitempty"`
|
||||
MaxEjectionTime *iserviceconfig.Duration `json:"maxEjectionTime,omitempty"`
|
||||
MaxEjectionPercent *uint32 `json:"maxEjectionPercent,omitempty"`
|
||||
SuccessRateEjection *successRateEjection `json:"successRateEjection,omitempty"`
|
||||
FailurePercentageEjection *failurePercentageEjection `json:"failurePercentageEjection,omitempty"`
|
||||
}
|
||||
|
||||
// outlierConfigFromCluster converts the received Outlier Detection
|
||||
// configuration into JSON configuration for Outlier Detection, taking into
|
||||
// account xDS Defaults. Returns nil if no OutlierDetection field set in the
|
||||
// cluster resource.
|
||||
func outlierConfigFromCluster(cluster *v3clusterpb.Cluster) (json.RawMessage, error) {
|
||||
od := cluster.GetOutlierDetection()
|
||||
if od == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// "The outlier_detection field of the Cluster resource should have its fields
|
||||
// validated according to the rules for the corresponding LB policy config
|
||||
// fields in the above "Validation" section. If any of these requirements is
|
||||
// violated, the Cluster resource should be NACKed." - A50
|
||||
// "The google.protobuf.Duration fields interval, base_ejection_time, and
|
||||
// max_ejection_time must obey the restrictions in the
|
||||
// google.protobuf.Duration documentation and they must have non-negative
|
||||
// values." - A50
|
||||
var interval *iserviceconfig.Duration
|
||||
if i := od.GetInterval(); i != nil {
|
||||
if err := i.CheckValid(); err != nil {
|
||||
return nil, fmt.Errorf("outlier_detection.interval is invalid with error: %v", err)
|
||||
}
|
||||
if interval = idurationp(i.AsDuration()); *interval < 0 {
|
||||
return nil, fmt.Errorf("outlier_detection.interval = %v; must be a valid duration and >= 0", *interval)
|
||||
}
|
||||
}
|
||||
|
||||
var baseEjectionTime *iserviceconfig.Duration
|
||||
if bet := od.GetBaseEjectionTime(); bet != nil {
|
||||
if err := bet.CheckValid(); err != nil {
|
||||
return nil, fmt.Errorf("outlier_detection.base_ejection_time is invalid with error: %v", err)
|
||||
}
|
||||
if baseEjectionTime = idurationp(bet.AsDuration()); *baseEjectionTime < 0 {
|
||||
return nil, fmt.Errorf("outlier_detection.base_ejection_time = %v; must be >= 0", *baseEjectionTime)
|
||||
}
|
||||
}
|
||||
|
||||
var maxEjectionTime *iserviceconfig.Duration
|
||||
if met := od.GetMaxEjectionTime(); met != nil {
|
||||
if err := met.CheckValid(); err != nil {
|
||||
return nil, fmt.Errorf("outlier_detection.max_ejection_time is invalid: %v", err)
|
||||
}
|
||||
if maxEjectionTime = idurationp(met.AsDuration()); *maxEjectionTime < 0 {
|
||||
return nil, fmt.Errorf("outlier_detection.max_ejection_time = %v; must be >= 0", *maxEjectionTime)
|
||||
}
|
||||
}
|
||||
|
||||
// "The fields max_ejection_percent, enforcing_success_rate,
|
||||
// failure_percentage_threshold, and enforcing_failure_percentage must have
|
||||
// values less than or equal to 100. If any of these requirements is
|
||||
// violated, the Cluster resource should be NACKed." - A50
|
||||
var maxEjectionPercent *uint32
|
||||
if mep := od.GetMaxEjectionPercent(); mep != nil {
|
||||
if maxEjectionPercent = uint32p(mep.GetValue()); *maxEjectionPercent > 100 {
|
||||
return nil, fmt.Errorf("outlier_detection.max_ejection_percent = %v; must be <= 100", *maxEjectionPercent)
|
||||
}
|
||||
}
|
||||
// "if the enforcing_success_rate field is set to 0, the config
|
||||
// success_rate_ejection field will be null and all success_rate_* fields
|
||||
// will be ignored." - A50
|
||||
var enforcingSuccessRate *uint32
|
||||
if esr := od.GetEnforcingSuccessRate(); esr != nil {
|
||||
if enforcingSuccessRate = uint32p(esr.GetValue()); *enforcingSuccessRate > 100 {
|
||||
return nil, fmt.Errorf("outlier_detection.enforcing_success_rate = %v; must be <= 100", *enforcingSuccessRate)
|
||||
}
|
||||
}
|
||||
var failurePercentageThreshold *uint32
|
||||
if fpt := od.GetFailurePercentageThreshold(); fpt != nil {
|
||||
if failurePercentageThreshold = uint32p(fpt.GetValue()); *failurePercentageThreshold > 100 {
|
||||
return nil, fmt.Errorf("outlier_detection.failure_percentage_threshold = %v; must be <= 100", *failurePercentageThreshold)
|
||||
}
|
||||
}
|
||||
// "If the enforcing_failure_percent field is set to 0 or null, the config
|
||||
// failure_percent_ejection field will be null and all failure_percent_*
|
||||
// fields will be ignored." - A50
|
||||
var enforcingFailurePercentage *uint32
|
||||
if efp := od.GetEnforcingFailurePercentage(); efp != nil {
|
||||
if enforcingFailurePercentage = uint32p(efp.GetValue()); *enforcingFailurePercentage > 100 {
|
||||
return nil, fmt.Errorf("outlier_detection.enforcing_failure_percentage = %v; must be <= 100", *enforcingFailurePercentage)
|
||||
}
|
||||
}
|
||||
|
||||
var successRateStdevFactor *uint32
|
||||
if srsf := od.GetSuccessRateStdevFactor(); srsf != nil {
|
||||
successRateStdevFactor = uint32p(srsf.GetValue())
|
||||
}
|
||||
var successRateMinimumHosts *uint32
|
||||
if srmh := od.GetSuccessRateMinimumHosts(); srmh != nil {
|
||||
successRateMinimumHosts = uint32p(srmh.GetValue())
|
||||
}
|
||||
var successRateRequestVolume *uint32
|
||||
if srrv := od.GetSuccessRateRequestVolume(); srrv != nil {
|
||||
successRateRequestVolume = uint32p(srrv.GetValue())
|
||||
}
|
||||
var failurePercentageMinimumHosts *uint32
|
||||
if fpmh := od.GetFailurePercentageMinimumHosts(); fpmh != nil {
|
||||
failurePercentageMinimumHosts = uint32p(fpmh.GetValue())
|
||||
}
|
||||
var failurePercentageRequestVolume *uint32
|
||||
if fprv := od.GetFailurePercentageRequestVolume(); fprv != nil {
|
||||
failurePercentageRequestVolume = uint32p(fprv.GetValue())
|
||||
}
|
||||
|
||||
// "if the enforcing_success_rate field is set to 0, the config
|
||||
// success_rate_ejection field will be null and all success_rate_* fields
|
||||
// will be ignored." - A50
|
||||
var sre *successRateEjection
|
||||
if enforcingSuccessRate == nil || *enforcingSuccessRate != 0 {
|
||||
sre = &successRateEjection{
|
||||
StdevFactor: successRateStdevFactor,
|
||||
EnforcementPercentage: enforcingSuccessRate,
|
||||
MinimumHosts: successRateMinimumHosts,
|
||||
RequestVolume: successRateRequestVolume,
|
||||
}
|
||||
}
|
||||
|
||||
// "If the enforcing_failure_percent field is set to 0 or null, the config
|
||||
// failure_percent_ejection field will be null and all failure_percent_*
|
||||
// fields will be ignored." - A50
|
||||
var fpe *failurePercentageEjection
|
||||
if enforcingFailurePercentage != nil && *enforcingFailurePercentage != 0 {
|
||||
fpe = &failurePercentageEjection{
|
||||
Threshold: failurePercentageThreshold,
|
||||
EnforcementPercentage: enforcingFailurePercentage,
|
||||
MinimumHosts: failurePercentageMinimumHosts,
|
||||
RequestVolume: failurePercentageRequestVolume,
|
||||
}
|
||||
}
|
||||
|
||||
odLBCfg := &odLBConfig{
|
||||
Interval: interval,
|
||||
BaseEjectionTime: baseEjectionTime,
|
||||
MaxEjectionTime: maxEjectionTime,
|
||||
MaxEjectionPercent: maxEjectionPercent,
|
||||
SuccessRateEjection: sre,
|
||||
FailurePercentageEjection: fpe,
|
||||
}
|
||||
return json.Marshal(odLBCfg)
|
||||
}
|
||||
2600
internal/xds/xdsclient/xdsresource/unmarshal_cds_test.go
Normal file
2600
internal/xds/xdsclient/xdsresource/unmarshal_cds_test.go
Normal file
File diff suppressed because it is too large
Load Diff
294
internal/xds/xdsclient/xdsresource/unmarshal_eds.go
Normal file
294
internal/xds/xdsclient/xdsresource/unmarshal_eds.go
Normal file
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
||||
"google.golang.org/grpc/experimental/balancer/hostname"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
xdsinternal "google.golang.org/grpc/internal/xds"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/ringhash"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// Hostname returns the hostname from the BalancerAttributes of the
|
||||
// given Address. If this attribute is not set, it returns the empty
|
||||
// string.
|
||||
func Hostname(addr resolver.Address) string {
|
||||
ep := resolver.Endpoint{Attributes: addr.BalancerAttributes}
|
||||
return hostname.FromEndpoint(ep)
|
||||
}
|
||||
|
||||
func unmarshalEndpointsResource(r *anypb.Any) (string, EndpointsUpdate, error) {
|
||||
r, err := UnwrapResource(r)
|
||||
if err != nil {
|
||||
return "", EndpointsUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
|
||||
}
|
||||
|
||||
if !IsEndpointsResource(r.GetTypeUrl()) {
|
||||
return "", EndpointsUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl())
|
||||
}
|
||||
|
||||
cla := &v3endpointpb.ClusterLoadAssignment{}
|
||||
if err := proto.Unmarshal(r.GetValue(), cla); err != nil {
|
||||
return "", EndpointsUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||
}
|
||||
|
||||
if cla.GetClusterName() == "" {
|
||||
return "", EndpointsUpdate{}, fmt.Errorf("empty resource name in endpoints resource")
|
||||
}
|
||||
|
||||
u, err := parseEDSRespProto(cla)
|
||||
if err != nil {
|
||||
return cla.GetClusterName(), EndpointsUpdate{}, err
|
||||
}
|
||||
u.Raw = r
|
||||
|
||||
return cla.GetClusterName(), u, nil
|
||||
}
|
||||
|
||||
func parseAddress(socketAddress *v3corepb.SocketAddress) string {
|
||||
return net.JoinHostPort(socketAddress.GetAddress(), strconv.Itoa(int(socketAddress.GetPortValue())))
|
||||
}
|
||||
|
||||
func parseDropPolicy(dropPolicy *v3endpointpb.ClusterLoadAssignment_Policy_DropOverload) (OverloadDropConfig, error) {
|
||||
percentage := dropPolicy.GetDropPercentage()
|
||||
var (
|
||||
numerator = percentage.GetNumerator()
|
||||
denominator uint32
|
||||
)
|
||||
switch percentage.GetDenominator() {
|
||||
case v3typepb.FractionalPercent_HUNDRED:
|
||||
denominator = 100
|
||||
case v3typepb.FractionalPercent_TEN_THOUSAND:
|
||||
denominator = 10000
|
||||
case v3typepb.FractionalPercent_MILLION:
|
||||
denominator = 1000000
|
||||
default:
|
||||
return OverloadDropConfig{}, fmt.Errorf("EDS response contains a drop policy with unsupported denominator: %v", percentage.GetDenominator())
|
||||
}
|
||||
return OverloadDropConfig{
|
||||
Category: dropPolicy.GetCategory(),
|
||||
Numerator: numerator,
|
||||
Denominator: denominator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseEndpoints(lbEndpoints []*v3endpointpb.LbEndpoint, uniqueEndpointAddrs map[string]bool) ([]Endpoint, error) {
|
||||
endpoints := make([]Endpoint, 0, len(lbEndpoints))
|
||||
var totalWeight uint64
|
||||
for _, lbEndpoint := range lbEndpoints {
|
||||
// If the load_balancing_weight field is specified, it must be set to a
|
||||
// value of at least 1. If unspecified, each host is presumed to have
|
||||
// equal weight in a locality.
|
||||
weight := uint32(1)
|
||||
if w := lbEndpoint.GetLoadBalancingWeight(); w != nil {
|
||||
if w.GetValue() == 0 {
|
||||
return nil, fmt.Errorf("EDS response contains an endpoint with zero weight: %+v", lbEndpoint)
|
||||
}
|
||||
weight = w.GetValue()
|
||||
}
|
||||
|
||||
totalWeight += uint64(weight)
|
||||
if totalWeight > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("sum of weights of endpoints in the same locality exceeds maximum value %d", uint64(math.MaxUint32))
|
||||
}
|
||||
|
||||
addrs := []string{parseAddress(lbEndpoint.GetEndpoint().GetAddress().GetSocketAddress())}
|
||||
if envconfig.XDSDualstackEndpointsEnabled {
|
||||
for _, sa := range lbEndpoint.GetEndpoint().GetAdditionalAddresses() {
|
||||
addrs = append(addrs, parseAddress(sa.GetAddress().GetSocketAddress()))
|
||||
}
|
||||
}
|
||||
|
||||
address := []resolver.Address{}
|
||||
for _, a := range addrs {
|
||||
address = append(address, resolver.Address{Addr: a})
|
||||
if uniqueEndpointAddrs[a] {
|
||||
return nil, fmt.Errorf("duplicate endpoint with the same address %s", a)
|
||||
}
|
||||
uniqueEndpointAddrs[a] = true
|
||||
}
|
||||
|
||||
var endpointMetadata map[string]any
|
||||
var hashKey string
|
||||
if envconfig.XDSHTTPConnectEnabled || !envconfig.XDSEndpointHashKeyBackwardCompat {
|
||||
var err error
|
||||
endpointMetadata, err = validateAndConstructMetadata(lbEndpoint.GetMetadata())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// "The xDS resolver, described in A74, will be changed to set the hash_key
|
||||
// endpoint attribute to the value of LbEndpoint.Metadata envoy.lb hash_key
|
||||
// field, as described in Envoy's documentation for the ring hash load
|
||||
// balancer." - A76
|
||||
if !envconfig.XDSEndpointHashKeyBackwardCompat {
|
||||
hashKey = hashKeyFromMetadata(endpointMetadata)
|
||||
}
|
||||
}
|
||||
endpoint := resolver.Endpoint{Addresses: address}
|
||||
endpoint = hostname.Set(endpoint, lbEndpoint.GetEndpoint().GetHostname())
|
||||
endpoint = ringhash.SetHashKey(endpoint, hashKey)
|
||||
endpoints = append(endpoints, Endpoint{
|
||||
ResolverEndpoint: endpoint,
|
||||
HealthStatus: EndpointHealthStatus(lbEndpoint.GetHealthStatus()),
|
||||
Weight: weight,
|
||||
Metadata: endpointMetadata,
|
||||
})
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// hashKey extracts and returns the hash key from the given endpoint metadata.
|
||||
// If no hash key is found, it returns an empty string.
|
||||
func hashKeyFromMetadata(metadata map[string]any) string {
|
||||
envoyLB, ok := metadata["envoy.lb"].(StructMetadataValue)
|
||||
if ok {
|
||||
if h, ok := envoyLB.Data["hash_key"].(string); ok {
|
||||
return h
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseEDSRespProto(m *v3endpointpb.ClusterLoadAssignment) (EndpointsUpdate, error) {
|
||||
ret := EndpointsUpdate{}
|
||||
for _, dropPolicy := range m.GetPolicy().GetDropOverloads() {
|
||||
drop, err := parseDropPolicy(dropPolicy)
|
||||
if err != nil {
|
||||
return EndpointsUpdate{}, err
|
||||
}
|
||||
ret.Drops = append(ret.Drops, drop)
|
||||
}
|
||||
priorities := make(map[uint32]map[string]bool)
|
||||
sumOfWeights := make(map[uint32]uint64)
|
||||
uniqueEndpointAddrs := make(map[string]bool)
|
||||
for _, locality := range m.Endpoints {
|
||||
l := locality.GetLocality()
|
||||
if l == nil {
|
||||
return EndpointsUpdate{}, fmt.Errorf("EDS response contains a locality without ID, locality: %+v", locality)
|
||||
}
|
||||
weight := locality.GetLoadBalancingWeight().GetValue()
|
||||
if weight == 0 {
|
||||
logger.Warningf("Ignoring locality %s with weight 0", pretty.ToJSON(l))
|
||||
continue
|
||||
}
|
||||
priority := locality.GetPriority()
|
||||
sumOfWeights[priority] += uint64(weight)
|
||||
if sumOfWeights[priority] > math.MaxUint32 {
|
||||
return EndpointsUpdate{}, fmt.Errorf("sum of weights of localities at the same priority %d exceeded maximal value", priority)
|
||||
}
|
||||
localitiesWithPriority := priorities[priority]
|
||||
if localitiesWithPriority == nil {
|
||||
localitiesWithPriority = make(map[string]bool)
|
||||
priorities[priority] = localitiesWithPriority
|
||||
}
|
||||
lid := clients.Locality{
|
||||
Region: l.Region,
|
||||
Zone: l.Zone,
|
||||
SubZone: l.SubZone,
|
||||
}
|
||||
lidStr := xdsinternal.LocalityString(lid)
|
||||
|
||||
// "Since an xDS configuration can place a given locality under multiple
|
||||
// priorities, it is possible to see locality weight attributes with
|
||||
// different values for the same locality." - A52
|
||||
//
|
||||
// This is handled in the client by emitting the locality weight
|
||||
// specified for the priority it is specified in. If the same locality
|
||||
// has a different weight in two priorities, each priority will specify
|
||||
// a locality with the locality weight specified for that priority, and
|
||||
// thus the subsequent tree of balancers linked to that priority will
|
||||
// use that locality weight as well.
|
||||
if localitiesWithPriority[lidStr] {
|
||||
return EndpointsUpdate{}, fmt.Errorf("duplicate locality %s with the same priority %v", lidStr, priority)
|
||||
}
|
||||
localitiesWithPriority[lidStr] = true
|
||||
endpoints, err := parseEndpoints(locality.GetLbEndpoints(), uniqueEndpointAddrs)
|
||||
if err != nil {
|
||||
return EndpointsUpdate{}, err
|
||||
}
|
||||
var localityMetadata map[string]any
|
||||
if envconfig.XDSHTTPConnectEnabled {
|
||||
var err error
|
||||
localityMetadata, err = validateAndConstructMetadata(locality.GetMetadata())
|
||||
if err != nil {
|
||||
return EndpointsUpdate{}, err
|
||||
}
|
||||
}
|
||||
|
||||
ret.Localities = append(ret.Localities, Locality{
|
||||
ID: lid,
|
||||
Endpoints: endpoints,
|
||||
Weight: weight,
|
||||
Priority: priority,
|
||||
Metadata: localityMetadata,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(priorities); i++ {
|
||||
if _, ok := priorities[uint32(i)]; !ok {
|
||||
return EndpointsUpdate{}, fmt.Errorf("priority %v missing (with different priorities %v received)", i, priorities)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// validateAndConstructMetadata processes the metadata from the xDS resource
|
||||
// and returns a map of parsed metadata values.
|
||||
func validateAndConstructMetadata(metadataProto *v3corepb.Metadata) (map[string]any, error) {
|
||||
if metadataProto == nil {
|
||||
return nil, nil
|
||||
}
|
||||
metadata := make(map[string]any)
|
||||
// First go through TypedFilterMetadata.
|
||||
for key, anyProto := range metadataProto.GetTypedFilterMetadata() {
|
||||
converter := metadataConverterForType(anyProto.GetTypeUrl())
|
||||
// Ignore types we don't have a converter for.
|
||||
if converter == nil {
|
||||
continue
|
||||
}
|
||||
val, err := converter.convert(anyProto)
|
||||
if err != nil {
|
||||
// If the converter fails, nack the whole resource.
|
||||
return nil, fmt.Errorf("metadata conversion for key %q and type %q failed: %v", key, anyProto.GetTypeUrl(), err)
|
||||
}
|
||||
metadata[key] = val
|
||||
}
|
||||
|
||||
// Process FilterMetadata for any keys not already handled.
|
||||
for key, structProto := range metadataProto.GetFilterMetadata() {
|
||||
// Skip keys already added from TypedFilterMetadata.
|
||||
if metadata[key] != nil {
|
||||
continue
|
||||
}
|
||||
metadata[key] = StructMetadataValue{Data: structProto.AsMap()}
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
1721
internal/xds/xdsclient/xdsresource/unmarshal_eds_test.go
Normal file
1721
internal/xds/xdsclient/xdsresource/unmarshal_eds_test.go
Normal file
File diff suppressed because it is too large
Load Diff
618
internal/xds/xdsclient/xdsresource/unmarshal_lds.go
Normal file
618
internal/xds/xdsclient/xdsresource/unmarshal_lds.go
Normal file
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1"
|
||||
v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
func unmarshalListenerResource(r *anypb.Any, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (string, ListenerUpdate, error) {
|
||||
r, err := UnwrapResource(r)
|
||||
if err != nil {
|
||||
return "", ListenerUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
|
||||
}
|
||||
|
||||
if !IsListenerResource(r.GetTypeUrl()) {
|
||||
return "", ListenerUpdate{}, fmt.Errorf("unexpected listener resource type: %q ", r.GetTypeUrl())
|
||||
}
|
||||
lis := &v3listenerpb.Listener{}
|
||||
if err := proto.Unmarshal(r.GetValue(), lis); err != nil {
|
||||
return "", ListenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||
}
|
||||
|
||||
if lis.GetName() == "" {
|
||||
return "", ListenerUpdate{}, fmt.Errorf("empty resource name in listener resource")
|
||||
}
|
||||
|
||||
lu, err := processListener(lis, bc, sc)
|
||||
if err != nil {
|
||||
return lis.GetName(), ListenerUpdate{}, err
|
||||
}
|
||||
lu.Raw = r
|
||||
|
||||
return lis.GetName(), *lu, nil
|
||||
}
|
||||
|
||||
func processListener(lis *v3listenerpb.Listener, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (*ListenerUpdate, error) {
|
||||
if lis.GetApiListener() != nil {
|
||||
return processClientSideListener(lis, bc, sc)
|
||||
}
|
||||
return processServerSideListener(lis, bc, sc)
|
||||
}
|
||||
|
||||
// processClientSideListener checks if the provided Listener proto meets
|
||||
// the expected criteria. If so, it returns a non-empty routeConfigName.
|
||||
func processClientSideListener(lis *v3listenerpb.Listener, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (*ListenerUpdate, error) {
|
||||
apiLisAny := lis.GetApiListener().GetApiListener()
|
||||
if !IsHTTPConnManagerResource(apiLisAny.GetTypeUrl()) {
|
||||
return nil, fmt.Errorf("unexpected http connection manager resource type: %q", apiLisAny.GetTypeUrl())
|
||||
}
|
||||
apiLis := &v3httppb.HttpConnectionManager{}
|
||||
if err := proto.Unmarshal(apiLisAny.GetValue(), apiLis); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal api_listener: %v", err)
|
||||
}
|
||||
// "HttpConnectionManager.xff_num_trusted_hops must be unset or zero and
|
||||
// HttpConnectionManager.original_ip_detection_extensions must be empty. If
|
||||
// either field has an incorrect value, the Listener must be NACKed." - A41
|
||||
if apiLis.XffNumTrustedHops != 0 {
|
||||
return nil, fmt.Errorf("xff_num_trusted_hops must be unset or zero %+v", apiLis)
|
||||
}
|
||||
if len(apiLis.OriginalIpDetectionExtensions) != 0 {
|
||||
return nil, fmt.Errorf("original_ip_detection_extensions must be empty %+v", apiLis)
|
||||
}
|
||||
|
||||
hcm := &HTTPConnectionManagerConfig{}
|
||||
switch apiLis.RouteSpecifier.(type) {
|
||||
case *v3httppb.HttpConnectionManager_Rds:
|
||||
if configsource := apiLis.GetRds().GetConfigSource(); configsource.GetAds() == nil && configsource.GetSelf() == nil {
|
||||
return nil, fmt.Errorf("LDS's RDS configSource is not ADS or Self: %+v", lis)
|
||||
}
|
||||
name := apiLis.GetRds().GetRouteConfigName()
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("empty route_config_name: %+v", lis)
|
||||
}
|
||||
hcm.RouteConfigName = name
|
||||
case *v3httppb.HttpConnectionManager_RouteConfig:
|
||||
routeU, err := generateRDSUpdateFromRouteConfiguration(apiLis.GetRouteConfig(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse inline RDS resp: %v", err)
|
||||
}
|
||||
hcm.InlineRouteConfig = &routeU
|
||||
case nil:
|
||||
return nil, fmt.Errorf("no RouteSpecifier: %+v", apiLis)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %T for RouteSpecifier", apiLis.RouteSpecifier)
|
||||
}
|
||||
|
||||
// The following checks and fields only apply to xDS protocol versions v3+.
|
||||
|
||||
hcm.MaxStreamDuration = apiLis.GetCommonHttpProtocolOptions().GetMaxStreamDuration().AsDuration()
|
||||
|
||||
var err error
|
||||
if hcm.HTTPFilters, err = processHTTPFilters(apiLis.GetHttpFilters(), false, bc, sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ListenerUpdate{APIListener: hcm}, nil
|
||||
}
|
||||
|
||||
func unwrapHTTPFilterConfig(config *anypb.Any) (proto.Message, string, error) {
|
||||
switch {
|
||||
case config.MessageIs(&v3xdsxdstypepb.TypedStruct{}):
|
||||
// The real type name is inside the new TypedStruct message.
|
||||
s := new(v3xdsxdstypepb.TypedStruct)
|
||||
if err := config.UnmarshalTo(s); err != nil {
|
||||
return nil, "", fmt.Errorf("error unmarshalling TypedStruct filter config: %v", err)
|
||||
}
|
||||
return s, s.GetTypeUrl(), nil
|
||||
case config.MessageIs(&v1xdsudpatypepb.TypedStruct{}):
|
||||
// The real type name is inside the old TypedStruct message.
|
||||
s := new(v1xdsudpatypepb.TypedStruct)
|
||||
if err := config.UnmarshalTo(s); err != nil {
|
||||
return nil, "", fmt.Errorf("error unmarshalling TypedStruct filter config: %v", err)
|
||||
}
|
||||
return s, s.GetTypeUrl(), nil
|
||||
default:
|
||||
return config, config.GetTypeUrl(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateHTTPFilterConfig(cfg *anypb.Any, lds, optional bool, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (httpfilter.Builder, httpfilter.FilterConfig, error) {
|
||||
config, typeURL, err := unwrapHTTPFilterConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
filterBuilder := httpfilter.Get(typeURL)
|
||||
if filterBuilder == nil {
|
||||
if optional {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("no filter implementation found for %q", typeURL)
|
||||
}
|
||||
parseFunc := filterBuilder.ParseFilterConfig
|
||||
if !lds {
|
||||
parseFunc = filterBuilder.ParseFilterConfigOverride
|
||||
}
|
||||
filterConfig, err := parseFunc(config, httpfilter.ParseOptions{BootstrapConfig: bc, ServerConfig: sc})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error parsing config for filter %q: %v", typeURL, err)
|
||||
}
|
||||
return filterBuilder, filterConfig, nil
|
||||
}
|
||||
|
||||
func processHTTPFilterOverrides(cfgs map[string]*anypb.Any, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (map[string]httpfilter.FilterConfig, error) {
|
||||
if len(cfgs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
m := make(map[string]httpfilter.FilterConfig)
|
||||
for name, cfg := range cfgs {
|
||||
optional := false
|
||||
disabled := false
|
||||
s := new(v3routepb.FilterConfig)
|
||||
if cfg.MessageIs(s) {
|
||||
if err := cfg.UnmarshalTo(s); err != nil {
|
||||
return nil, fmt.Errorf("filter override %q: error unmarshalling FilterConfig: %v", name, err)
|
||||
}
|
||||
cfg = s.GetConfig()
|
||||
optional = s.GetIsOptional()
|
||||
if envconfig.XDSClientExtProcEnabled || envconfig.XDSClientExtAuthzEnabled {
|
||||
disabled = s.GetDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
if disabled {
|
||||
m[name] = httpfilter.DisabledFilterConfig{}
|
||||
continue
|
||||
}
|
||||
|
||||
httpFilter, config, err := validateHTTPFilterConfig(cfg, false, optional, bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("filter override %q: %v", name, err)
|
||||
}
|
||||
if httpFilter == nil {
|
||||
// Optional configs are ignored.
|
||||
continue
|
||||
}
|
||||
m[name] = config
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func processHTTPFilters(filters []*v3httppb.HttpFilter, server bool, bc *bootstrap.Config, sc *bootstrap.ServerConfig) ([]HTTPFilter, error) {
|
||||
ret := make([]HTTPFilter, 0, len(filters))
|
||||
seenNames := make(map[string]bool, len(filters))
|
||||
for _, filter := range filters {
|
||||
name := filter.GetName()
|
||||
if name == "" {
|
||||
return nil, errors.New("filter missing name field")
|
||||
}
|
||||
if seenNames[name] {
|
||||
return nil, fmt.Errorf("duplicate filter name %q", name)
|
||||
}
|
||||
seenNames[name] = true
|
||||
|
||||
httpFilter, config, err := validateHTTPFilterConfig(filter.GetTypedConfig(), true, filter.GetIsOptional(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpFilter == nil {
|
||||
// Optional configs are ignored.
|
||||
continue
|
||||
}
|
||||
if server {
|
||||
if _, ok := httpFilter.(httpfilter.ServerFilterBuilder); !ok {
|
||||
if filter.GetIsOptional() {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("HTTP filter %q not supported server-side", name)
|
||||
}
|
||||
} else if _, ok := httpFilter.(httpfilter.ClientFilterBuilder); !ok {
|
||||
if filter.GetIsOptional() {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("HTTP filter %q not supported client-side", name)
|
||||
}
|
||||
|
||||
disabled := false
|
||||
if envconfig.XDSClientExtProcEnabled || envconfig.XDSClientExtAuthzEnabled {
|
||||
disabled = filter.GetDisabled()
|
||||
}
|
||||
// Save name/config
|
||||
ret = append(ret, HTTPFilter{Name: name, Filter: httpFilter, Config: config, Disabled: disabled})
|
||||
}
|
||||
// "Validation will fail if a terminal filter is not the last filter in the
|
||||
// chain or if a non-terminal filter is the last filter in the chain." - A39
|
||||
if len(ret) == 0 {
|
||||
return nil, fmt.Errorf("http filters list is empty")
|
||||
}
|
||||
var i int
|
||||
for ; i < len(ret)-1; i++ {
|
||||
if ret[i].Filter.IsTerminal() {
|
||||
return nil, fmt.Errorf("http filter %q is a terminal filter but it is not last in the filter chain", ret[i].Name)
|
||||
}
|
||||
}
|
||||
if !ret[i].Filter.IsTerminal() {
|
||||
return nil, fmt.Errorf("http filter %q is not a terminal filter", ret[len(ret)-1].Name)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func processServerSideListener(lis *v3listenerpb.Listener, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (*ListenerUpdate, error) {
|
||||
if n := len(lis.ListenerFilters); n != 0 {
|
||||
return nil, fmt.Errorf("unsupported field 'listener_filters' contains %d entries", n)
|
||||
}
|
||||
if lis.GetUseOriginalDst().GetValue() {
|
||||
return nil, errors.New("unsupported field 'use_original_dst' is present and set to true")
|
||||
}
|
||||
addr := lis.GetAddress()
|
||||
if addr == nil {
|
||||
return nil, fmt.Errorf("no address field in LDS response: %+v", lis)
|
||||
}
|
||||
sockAddr := addr.GetSocketAddress()
|
||||
if sockAddr == nil {
|
||||
return nil, fmt.Errorf("no socket_address field in LDS response: %+v", lis)
|
||||
}
|
||||
lu := &ListenerUpdate{
|
||||
TCPListener: &InboundListenerConfig{
|
||||
Address: sockAddr.GetAddress(),
|
||||
Port: strconv.Itoa(int(sockAddr.GetPortValue())),
|
||||
},
|
||||
}
|
||||
|
||||
// Populate the default filter chain.
|
||||
if dfc := lis.GetDefaultFilterChain(); dfc != nil {
|
||||
fc, err := filterChainFromProto(dfc, bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal default filter chain: %v", err)
|
||||
}
|
||||
lu.TCPListener.DefaultFilterChain = fc
|
||||
}
|
||||
|
||||
// Populated the filter chain map.
|
||||
fcm, err := buildFilterChainMap(lis.GetFilterChains(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal filter chains: %v", err)
|
||||
}
|
||||
lu.TCPListener.FilterChains = fcm
|
||||
|
||||
// If there are no supported filter chains and no default filter chain, we
|
||||
// fail here. This will cause the Listener resource to be NACK'ed.
|
||||
if len(lu.TCPListener.FilterChains.DstPrefixes) == 0 && lu.TCPListener.DefaultFilterChain.IsEmpty() {
|
||||
return nil, fmt.Errorf("no supported filter chains and no default filter chain")
|
||||
}
|
||||
|
||||
return lu, nil
|
||||
}
|
||||
|
||||
func filterChainFromProto(fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (NetworkFilterChainConfig, error) {
|
||||
var emptyFilterChain NetworkFilterChainConfig
|
||||
|
||||
hcmConfig, err := processNetworkFilters(fc.GetFilters(), bc, sc)
|
||||
if err != nil {
|
||||
return emptyFilterChain, err
|
||||
}
|
||||
|
||||
fcc := NetworkFilterChainConfig{HTTPConnMgr: hcmConfig}
|
||||
// If the transport_socket field is not specified, it means that the control
|
||||
// plane has not sent us any security config. This is fine and the server
|
||||
// will use the fallback credentials configured as part of the
|
||||
// xdsCredentials.
|
||||
ts := fc.GetTransportSocket()
|
||||
if ts == nil {
|
||||
return fcc, nil
|
||||
}
|
||||
if name := ts.GetName(); name != transportSocketName {
|
||||
return emptyFilterChain, fmt.Errorf("transport_socket field has unexpected name: %s", name)
|
||||
}
|
||||
tc := ts.GetTypedConfig()
|
||||
if typeURL := tc.GetTypeUrl(); typeURL != version.V3DownstreamTLSContextURL {
|
||||
return emptyFilterChain, fmt.Errorf("transport_socket missing typed_config or wrong type_url: %q", typeURL)
|
||||
}
|
||||
downstreamCtx := &v3tlspb.DownstreamTlsContext{}
|
||||
if err := proto.Unmarshal(tc.GetValue(), downstreamCtx); err != nil {
|
||||
return emptyFilterChain, fmt.Errorf("failed to unmarshal DownstreamTlsContext in LDS response: %v", err)
|
||||
}
|
||||
if downstreamCtx.GetRequireSni().GetValue() {
|
||||
return emptyFilterChain, fmt.Errorf("require_sni field set to true in DownstreamTlsContext message: %v", downstreamCtx)
|
||||
}
|
||||
if downstreamCtx.GetOcspStaplePolicy() != v3tlspb.DownstreamTlsContext_LENIENT_STAPLING {
|
||||
return emptyFilterChain, fmt.Errorf("ocsp_staple_policy field set to unsupported value in DownstreamTlsContext message: %v", downstreamCtx)
|
||||
}
|
||||
if downstreamCtx.GetCommonTlsContext() == nil {
|
||||
return emptyFilterChain, errors.New("DownstreamTlsContext in LDS response does not contain a CommonTlsContext")
|
||||
}
|
||||
secCfg, err := securityConfigFromCommonTLSContext(downstreamCtx.GetCommonTlsContext(), true)
|
||||
if err != nil {
|
||||
return emptyFilterChain, err
|
||||
}
|
||||
if secCfg != nil {
|
||||
secCfg.RequireClientCert = downstreamCtx.GetRequireClientCertificate().GetValue()
|
||||
if secCfg.RequireClientCert && secCfg.RootInstanceName == "" {
|
||||
return emptyFilterChain, errors.New("security configuration on the server-side does not contain root certificate provider instance name, but require_client_cert field is set")
|
||||
}
|
||||
fcc.SecurityCfg = secCfg
|
||||
}
|
||||
return fcc, nil
|
||||
}
|
||||
|
||||
// dstPrefixEntry wraps DestinationPrefixEntry to track build state.
|
||||
type dstPrefixEntry struct {
|
||||
entry DestinationPrefixEntry
|
||||
rawBufferSeen bool
|
||||
}
|
||||
|
||||
func buildFilterChainMap(fcs []*v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (NetworkFilterChainMap, error) {
|
||||
dstPrefixEntries := []*dstPrefixEntry{}
|
||||
for _, fc := range fcs {
|
||||
fcMatch := fc.GetFilterChainMatch()
|
||||
if fcMatch.GetDestinationPort().GetValue() != 0 {
|
||||
// Destination port is the first match criteria and we do not
|
||||
// support filter chains that contain this match criteria.
|
||||
logger.Warningf("Dropping filter chain %q since it contains unsupported destination_port match field", fc.GetName())
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
dstPrefixEntries, err = addFilterChainsForDestPrefixes(dstPrefixEntries, fc, bc, sc)
|
||||
if err != nil {
|
||||
return NetworkFilterChainMap{}, err
|
||||
}
|
||||
}
|
||||
|
||||
entries := []DestinationPrefixEntry{}
|
||||
for _, bEntry := range dstPrefixEntries {
|
||||
fcSeen := false
|
||||
for _, srcPrefixes := range bEntry.entry.SourceTypeArr {
|
||||
if len(srcPrefixes.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, srcPrefix := range srcPrefixes.Entries {
|
||||
for _, fc := range srcPrefix.PortMap {
|
||||
if !fc.IsEmpty() {
|
||||
fcSeen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fcSeen {
|
||||
entries = append(entries, bEntry.entry)
|
||||
}
|
||||
}
|
||||
return NetworkFilterChainMap{DstPrefixes: entries}, nil
|
||||
}
|
||||
|
||||
func addFilterChainsForDestPrefixes(dstPrefixEntries []*dstPrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) ([]*dstPrefixEntry, error) {
|
||||
ranges := fc.GetFilterChainMatch().GetPrefixRanges()
|
||||
dstPrefixes, err := parsePrefixRanges(ranges)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse destination prefix ranges: %v", err)
|
||||
}
|
||||
|
||||
var entry *dstPrefixEntry
|
||||
if len(dstPrefixes) == 0 {
|
||||
// Use the unspecified entry when destination prefix is unspecified, and
|
||||
// set the `prefix` field to nil.
|
||||
dstPrefixEntries, entry = getOrCreateDestPrefixEntry(dstPrefixEntries, netip.Prefix{})
|
||||
if err := addFilterChainsForServerNames(entry, fc, bc, sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dstPrefixEntries, nil
|
||||
}
|
||||
for _, prefix := range dstPrefixes {
|
||||
dstPrefixEntries, entry = getOrCreateDestPrefixEntry(dstPrefixEntries, prefix)
|
||||
if err := addFilterChainsForServerNames(entry, fc, bc, sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return dstPrefixEntries, nil
|
||||
}
|
||||
|
||||
// getOrCreateDestPrefixEntry looks for an existing dstPrefixEntry in the
|
||||
// provided slice with the same destination prefix as the provided prefix. If
|
||||
// such an entry is found, it is returned. Otherwise, a new entry is created and
|
||||
// appended to the slice, and the new entry is returned.
|
||||
func getOrCreateDestPrefixEntry(dstPrefixEntries []*dstPrefixEntry, prefix netip.Prefix) ([]*dstPrefixEntry, *dstPrefixEntry) {
|
||||
for _, e := range dstPrefixEntries {
|
||||
if e.entry.Prefix == prefix {
|
||||
return dstPrefixEntries, e
|
||||
}
|
||||
}
|
||||
entry := &dstPrefixEntry{entry: DestinationPrefixEntry{Prefix: prefix}}
|
||||
dstPrefixEntries = append(dstPrefixEntries, entry)
|
||||
return dstPrefixEntries, entry
|
||||
}
|
||||
|
||||
func addFilterChainsForServerNames(dstEntry *dstPrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
// Filter chains specifying server names in their match criteria always fail
|
||||
// a match at connection time. So, these filter chains can be dropped now.
|
||||
if len(fc.GetFilterChainMatch().GetServerNames()) != 0 {
|
||||
logger.Warningf("Dropping filter chain %q since it contains unsupported server_names match field", fc.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
return addFilterChainsForTransportProtocols(dstEntry, fc, bc, sc)
|
||||
}
|
||||
|
||||
func addFilterChainsForTransportProtocols(dstEntry *dstPrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
tp := fc.GetFilterChainMatch().GetTransportProtocol()
|
||||
switch {
|
||||
case tp != "" && tp != "raw_buffer":
|
||||
// Only allow filter chains with transport protocol set to empty string
|
||||
// or "raw_buffer".
|
||||
logger.Warningf("Dropping filter chain %q since it contains unsupported value for transport_protocol match field", fc.GetName())
|
||||
return nil
|
||||
case tp == "" && dstEntry.rawBufferSeen:
|
||||
// If we have already seen filter chains with transport protocol set to
|
||||
// "raw_buffer", we can drop filter chains with transport protocol set
|
||||
// to empty string, since the former takes precedence.
|
||||
logger.Warningf("Dropping filter chain %q since it contains empty string for transport_protocol match field, but one with raw_buffer already exists", fc.GetName())
|
||||
return nil
|
||||
case tp != "" && !dstEntry.rawBufferSeen:
|
||||
// This is the first "raw_buffer" that we are seeing. Set the bit and
|
||||
// reset the source types array which might contain entries for filter
|
||||
// chains with transport protocol set to empty string.
|
||||
dstEntry.rawBufferSeen = true
|
||||
dstEntry.entry.SourceTypeArr = [3]SourcePrefixes{}
|
||||
}
|
||||
return addFilterChainsForApplicationProtocols(dstEntry, fc, bc, sc)
|
||||
}
|
||||
|
||||
func addFilterChainsForApplicationProtocols(dstEntry *dstPrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
if len(fc.GetFilterChainMatch().GetApplicationProtocols()) != 0 {
|
||||
logger.Warningf("Dropping filter chain %q since it contains unsupported application_protocols match field", fc.GetName())
|
||||
return nil
|
||||
}
|
||||
return addFilterChainsForSourceType(&dstEntry.entry, fc, bc, sc)
|
||||
}
|
||||
|
||||
// sourceType specifies the connection source IP match type.
|
||||
type sourceType int
|
||||
|
||||
const (
|
||||
// sourceTypeAny matches connection attempts from any source.
|
||||
sourceTypeAny sourceType = iota
|
||||
// sourceTypeSameOrLoopback matches connection attempts from the same host.
|
||||
sourceTypeSameOrLoopback
|
||||
// sourceTypeExternal matches connection attempts from a different host.
|
||||
sourceTypeExternal
|
||||
)
|
||||
|
||||
func addFilterChainsForSourceType(entry *DestinationPrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
var srcType sourceType
|
||||
switch st := fc.GetFilterChainMatch().GetSourceType(); st {
|
||||
case v3listenerpb.FilterChainMatch_ANY:
|
||||
srcType = sourceTypeAny
|
||||
case v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK:
|
||||
srcType = sourceTypeSameOrLoopback
|
||||
case v3listenerpb.FilterChainMatch_EXTERNAL:
|
||||
srcType = sourceTypeExternal
|
||||
default:
|
||||
return fmt.Errorf("unsupported source type: %v", st)
|
||||
}
|
||||
|
||||
return addFilterChainsForSourcePrefixes(&entry.SourceTypeArr[srcType], fc, bc, sc)
|
||||
}
|
||||
|
||||
func addFilterChainsForSourcePrefixes(srcPrefixes *SourcePrefixes, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
ranges := fc.GetFilterChainMatch().GetSourcePrefixRanges()
|
||||
prefixes, err := parsePrefixRanges(ranges)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse source prefix ranges: %v", err)
|
||||
}
|
||||
|
||||
if len(prefixes) == 0 {
|
||||
return getOrCreateSourcePrefixEntry(srcPrefixes, netip.Prefix{}, fc, bc, sc)
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
if err := getOrCreateSourcePrefixEntry(srcPrefixes, prefix, fc, bc, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePrefixRanges(ranges []*v3corepb.CidrRange) ([]netip.Prefix, error) {
|
||||
prefixes := make([]netip.Prefix, 0, len(ranges))
|
||||
for _, pr := range ranges {
|
||||
addrStr := pr.GetAddressPrefix()
|
||||
bits := int(pr.GetPrefixLen().GetValue())
|
||||
|
||||
addr, err := netip.ParseAddr(addrStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address: %q", addrStr)
|
||||
}
|
||||
prefix := netip.PrefixFrom(addr.Unmap(), bits).Masked()
|
||||
|
||||
if !prefix.IsValid() {
|
||||
return nil, fmt.Errorf(`length %d is invalid for "%s" (max %d)`, bits, addrStr, addr.BitLen())
|
||||
}
|
||||
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
return prefixes, nil
|
||||
}
|
||||
|
||||
// getOrCreateSourcePrefixEntry looks for an existing SourcePrefixEntry in the
|
||||
// provided SourcePrefixes with the same source prefix as the provided prefix. If
|
||||
// such an entry is found, the provided filter chain is added to the entry and
|
||||
// nil is returned. Otherwise, a new entry is created and appended to the
|
||||
// SourcePrefixes, the provided filter chain is added to the new entry, and nil
|
||||
// is returned. If there are multiple filter chains with overlapping matching
|
||||
// rules, an error is returned.
|
||||
func getOrCreateSourcePrefixEntry(srcPrefixes *SourcePrefixes, prefix netip.Prefix, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
for i := range srcPrefixes.Entries {
|
||||
if srcPrefixes.Entries[i].Prefix == prefix {
|
||||
return addFilterChainsForSourcePorts(&srcPrefixes.Entries[i], fc, bc, sc)
|
||||
}
|
||||
}
|
||||
|
||||
// Not found, create a new entry.
|
||||
srcPrefixes.Entries = append(srcPrefixes.Entries, SourcePrefixEntry{
|
||||
Prefix: prefix,
|
||||
PortMap: make(map[int]NetworkFilterChainConfig),
|
||||
})
|
||||
return addFilterChainsForSourcePorts(&srcPrefixes.Entries[len(srcPrefixes.Entries)-1], fc, bc, sc)
|
||||
}
|
||||
|
||||
func addFilterChainsForSourcePorts(entry *SourcePrefixEntry, fc *v3listenerpb.FilterChain, bc *bootstrap.Config, sc *bootstrap.ServerConfig) error {
|
||||
ports := fc.GetFilterChainMatch().GetSourcePorts()
|
||||
srcPorts := make([]int, 0, len(ports))
|
||||
for _, port := range ports {
|
||||
srcPorts = append(srcPorts, int(port))
|
||||
}
|
||||
|
||||
if len(srcPorts) == 0 {
|
||||
if !entry.PortMap[0].IsEmpty() {
|
||||
return errors.New("multiple filter chains with overlapping matching rules are defined")
|
||||
}
|
||||
fcc, err := filterChainFromProto(fc, bc, sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.PortMap[0] = fcc
|
||||
return nil
|
||||
}
|
||||
for _, port := range srcPorts {
|
||||
if !entry.PortMap[port].IsEmpty() {
|
||||
return errors.New("multiple filter chains with overlapping matching rules are defined")
|
||||
}
|
||||
fcc, err := filterChainFromProto(fc, bc, sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.PortMap[port] = fcc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
2066
internal/xds/xdsclient/xdsresource/unmarshal_lds_test.go
Normal file
2066
internal/xds/xdsclient/xdsresource/unmarshal_lds_test.go
Normal file
File diff suppressed because it is too large
Load Diff
464
internal/xds/xdsclient/xdsresource/unmarshal_rds.go
Normal file
464
internal/xds/xdsclient/xdsresource/unmarshal_rds.go
Normal file
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/clusterspecifier"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
||||
)
|
||||
|
||||
func unmarshalRouteConfigResource(r *anypb.Any, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (string, RouteConfigUpdate, error) {
|
||||
r, err := UnwrapResource(r)
|
||||
if err != nil {
|
||||
return "", RouteConfigUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
|
||||
}
|
||||
|
||||
if !IsRouteConfigResource(r.GetTypeUrl()) {
|
||||
return "", RouteConfigUpdate{}, fmt.Errorf("unexpected resource type: %q ", r.GetTypeUrl())
|
||||
}
|
||||
rc := &v3routepb.RouteConfiguration{}
|
||||
if err := proto.Unmarshal(r.GetValue(), rc); err != nil {
|
||||
return "", RouteConfigUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||
}
|
||||
|
||||
if rc.GetName() == "" {
|
||||
return "", RouteConfigUpdate{}, fmt.Errorf("empty resource name in route config resource")
|
||||
}
|
||||
|
||||
u, err := generateRDSUpdateFromRouteConfiguration(rc, bc, sc)
|
||||
if err != nil {
|
||||
return rc.GetName(), RouteConfigUpdate{}, err
|
||||
}
|
||||
u.Raw = r
|
||||
|
||||
return rc.GetName(), u, nil
|
||||
}
|
||||
|
||||
// generateRDSUpdateFromRouteConfiguration checks if the provided
|
||||
// RouteConfiguration meets the expected criteria. If so, it returns a
|
||||
// RouteConfigUpdate with nil error.
|
||||
//
|
||||
// A RouteConfiguration resource is considered valid when only if it contains a
|
||||
// VirtualHost whose domain field matches the server name from the URI passed
|
||||
// to the gRPC channel, and it contains a clusterName or a weighted cluster.
|
||||
//
|
||||
// The RouteConfiguration includes a list of virtualHosts, which may have zero
|
||||
// or more elements. We are interested in the element whose domains field
|
||||
// matches the server name specified in the "xds:" URI. The only field in the
|
||||
// VirtualHost proto that we are interested in is the list of routes. We
|
||||
// only look at the last route in the list (the default route), whose match
|
||||
// field must be empty and whose route field must be set. Inside that route
|
||||
// message, the cluster field will contain the clusterName or weighted clusters
|
||||
// we are looking for.
|
||||
func generateRDSUpdateFromRouteConfiguration(rc *v3routepb.RouteConfiguration, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (RouteConfigUpdate, error) {
|
||||
vhs := make([]*VirtualHost, 0, len(rc.GetVirtualHosts()))
|
||||
csps, err := processClusterSpecifierPlugins(rc.ClusterSpecifierPlugins)
|
||||
if err != nil {
|
||||
return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err)
|
||||
}
|
||||
// cspNames represents all the cluster specifiers referenced by Route
|
||||
// Actions - any cluster specifiers not referenced by a Route Action can be
|
||||
// ignored and not emitted by the xdsclient.
|
||||
var cspNames = make(map[string]bool)
|
||||
for _, vh := range rc.GetVirtualHosts() {
|
||||
routes, cspNs, err := routesProtoToSlice(vh.Routes, csps, bc, sc)
|
||||
if err != nil {
|
||||
return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err)
|
||||
}
|
||||
for n := range cspNs {
|
||||
cspNames[n] = true
|
||||
}
|
||||
rc, err := generateRetryConfig(vh.GetRetryPolicy())
|
||||
if err != nil {
|
||||
return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err)
|
||||
}
|
||||
vhOut := &VirtualHost{
|
||||
Domains: vh.GetDomains(),
|
||||
Routes: routes,
|
||||
RetryConfig: rc,
|
||||
}
|
||||
cfgs, err := processHTTPFilterOverrides(vh.GetTypedPerFilterConfig(), bc, sc)
|
||||
if err != nil {
|
||||
return RouteConfigUpdate{}, fmt.Errorf("virtual host %+v: %v", vh, err)
|
||||
}
|
||||
vhOut.HTTPFilterConfigOverride = cfgs
|
||||
vhs = append(vhs, vhOut)
|
||||
}
|
||||
|
||||
// "For any entry in the RouteConfiguration.cluster_specifier_plugins not
|
||||
// referenced by an enclosed ActionType's cluster_specifier_plugin, the xDS
|
||||
// client should not provide it to its consumers." - RLS in xDS Design
|
||||
for name := range csps {
|
||||
if !cspNames[name] {
|
||||
delete(csps, name)
|
||||
}
|
||||
}
|
||||
|
||||
return RouteConfigUpdate{VirtualHosts: vhs, ClusterSpecifierPlugins: csps}, nil
|
||||
}
|
||||
|
||||
func processClusterSpecifierPlugins(csps []*v3routepb.ClusterSpecifierPlugin) (map[string]clusterspecifier.BalancerConfig, error) {
|
||||
cspCfgs := make(map[string]clusterspecifier.BalancerConfig)
|
||||
// "The xDS client will inspect all elements of the
|
||||
// cluster_specifier_plugins field looking up a plugin based on the
|
||||
// extension.typed_config of each." - RLS in xDS design
|
||||
for _, csp := range csps {
|
||||
cs := clusterspecifier.Get(csp.GetExtension().GetTypedConfig().GetTypeUrl())
|
||||
if cs == nil {
|
||||
if csp.GetIsOptional() {
|
||||
// "If a plugin is not supported but has is_optional set, then
|
||||
// we will ignore any routes that point to that plugin"
|
||||
cspCfgs[csp.GetExtension().GetName()] = nil
|
||||
continue
|
||||
}
|
||||
// "If no plugin is registered for it, the resource will be NACKed."
|
||||
// - RLS in xDS design
|
||||
return nil, fmt.Errorf("cluster specifier %q of type %q was not found", csp.GetExtension().GetName(), csp.GetExtension().GetTypedConfig().GetTypeUrl())
|
||||
}
|
||||
lbCfg, err := cs.ParseClusterSpecifierConfig(csp.GetExtension().GetTypedConfig())
|
||||
if err != nil {
|
||||
// "If a plugin is found, the value of the typed_config field will
|
||||
// be passed to it's conversion method, and if an error is
|
||||
// encountered, the resource will be NACKED." - RLS in xDS design
|
||||
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())
|
||||
}
|
||||
// "If all cluster specifiers are valid, the xDS client will store the
|
||||
// configurations in a map keyed by the name of the extension instance." -
|
||||
// RLS in xDS Design
|
||||
cspCfgs[csp.GetExtension().GetName()] = lbCfg
|
||||
}
|
||||
return cspCfgs, nil
|
||||
}
|
||||
|
||||
func generateRetryConfig(rp *v3routepb.RetryPolicy) (*RetryConfig, error) {
|
||||
if rp == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cfg := &RetryConfig{RetryOn: make(map[codes.Code]bool)}
|
||||
for _, s := range strings.Split(rp.GetRetryOn(), ",") {
|
||||
switch strings.TrimSpace(strings.ToLower(s)) {
|
||||
case "cancelled":
|
||||
cfg.RetryOn[codes.Canceled] = true
|
||||
case "deadline-exceeded":
|
||||
cfg.RetryOn[codes.DeadlineExceeded] = true
|
||||
case "internal":
|
||||
cfg.RetryOn[codes.Internal] = true
|
||||
case "resource-exhausted":
|
||||
cfg.RetryOn[codes.ResourceExhausted] = true
|
||||
case "unavailable":
|
||||
cfg.RetryOn[codes.Unavailable] = true
|
||||
}
|
||||
}
|
||||
|
||||
if rp.NumRetries == nil {
|
||||
cfg.NumRetries = 1
|
||||
} else {
|
||||
cfg.NumRetries = rp.GetNumRetries().Value
|
||||
if cfg.NumRetries < 1 {
|
||||
return nil, fmt.Errorf("retry_policy.num_retries = %v; must be >= 1", cfg.NumRetries)
|
||||
}
|
||||
}
|
||||
|
||||
backoff := rp.GetRetryBackOff()
|
||||
if backoff == nil {
|
||||
cfg.RetryBackoff.BaseInterval = 25 * time.Millisecond
|
||||
} else {
|
||||
cfg.RetryBackoff.BaseInterval = backoff.GetBaseInterval().AsDuration()
|
||||
if cfg.RetryBackoff.BaseInterval <= 0 {
|
||||
return nil, fmt.Errorf("retry_policy.base_interval = %v; must be > 0", cfg.RetryBackoff.BaseInterval)
|
||||
}
|
||||
}
|
||||
if max := backoff.GetMaxInterval(); max == nil {
|
||||
cfg.RetryBackoff.MaxInterval = 10 * cfg.RetryBackoff.BaseInterval
|
||||
} else {
|
||||
cfg.RetryBackoff.MaxInterval = max.AsDuration()
|
||||
if cfg.RetryBackoff.MaxInterval <= 0 {
|
||||
return nil, fmt.Errorf("retry_policy.max_interval = %v; must be > 0", cfg.RetryBackoff.MaxInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.RetryOn) == 0 {
|
||||
return &RetryConfig{}, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func routesProtoToSlice(routes []*v3routepb.Route, csps map[string]clusterspecifier.BalancerConfig, bc *bootstrap.Config, sc *bootstrap.ServerConfig) ([]*Route, map[string]bool, error) {
|
||||
var routesRet []*Route
|
||||
var cspNames = make(map[string]bool)
|
||||
for _, r := range routes {
|
||||
match := r.GetMatch()
|
||||
if match == nil {
|
||||
return nil, nil, fmt.Errorf("route %+v doesn't have a match", r)
|
||||
}
|
||||
|
||||
if len(match.GetQueryParameters()) != 0 {
|
||||
// Ignore route with query parameters.
|
||||
logger.Warningf("Ignoring route %+v with query parameter matchers", r)
|
||||
continue
|
||||
}
|
||||
|
||||
pathSp := match.GetPathSpecifier()
|
||||
if pathSp == nil {
|
||||
return nil, nil, fmt.Errorf("route %+v doesn't have a path specifier", r)
|
||||
}
|
||||
|
||||
var route Route
|
||||
switch pt := pathSp.(type) {
|
||||
case *v3routepb.RouteMatch_Prefix:
|
||||
route.Prefix = &pt.Prefix
|
||||
case *v3routepb.RouteMatch_Path:
|
||||
route.Path = &pt.Path
|
||||
case *v3routepb.RouteMatch_SafeRegex:
|
||||
regex := pt.SafeRegex.GetRegex()
|
||||
re, err := matcher.CompileSafeRegex(regex)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex)
|
||||
}
|
||||
route.Regex = re
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("route %+v has an unrecognized path specifier: %+v", r, pt)
|
||||
}
|
||||
|
||||
if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil {
|
||||
route.CaseInsensitive = !caseSensitive.Value
|
||||
}
|
||||
|
||||
for _, h := range match.GetHeaders() {
|
||||
var header HeaderMatcher
|
||||
// The deprecated exact/prefix/suffix/contains match fields are
|
||||
// converted to the equivalent StringMatcher so that
|
||||
// StringMatcherFromProto owns validation for all of them.
|
||||
var smProto *v3matcherpb.StringMatcher
|
||||
switch ht := h.GetHeaderMatchSpecifier().(type) {
|
||||
case *v3routepb.HeaderMatcher_ExactMatch:
|
||||
smProto = &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: ht.ExactMatch}}
|
||||
case *v3routepb.HeaderMatcher_SafeRegexMatch:
|
||||
regex := ht.SafeRegexMatch.GetRegex()
|
||||
re, err := matcher.CompileSafeRegex(regex)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v contains an invalid regex %q", r, regex)
|
||||
}
|
||||
header.RegexMatch = re
|
||||
case *v3routepb.HeaderMatcher_RangeMatch:
|
||||
header.RangeMatch = &Int64Range{
|
||||
Start: ht.RangeMatch.Start,
|
||||
End: ht.RangeMatch.End,
|
||||
}
|
||||
case *v3routepb.HeaderMatcher_PresentMatch:
|
||||
header.PresentMatch = &ht.PresentMatch
|
||||
case *v3routepb.HeaderMatcher_PrefixMatch:
|
||||
smProto = &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: ht.PrefixMatch}}
|
||||
case *v3routepb.HeaderMatcher_SuffixMatch:
|
||||
smProto = &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: ht.SuffixMatch}}
|
||||
case *v3routepb.HeaderMatcher_ContainsMatch:
|
||||
smProto = &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: ht.ContainsMatch}}
|
||||
case *v3routepb.HeaderMatcher_StringMatch:
|
||||
if ht.StringMatch == nil {
|
||||
return nil, nil, fmt.Errorf("route %+v has an empty string matcher", r)
|
||||
}
|
||||
smProto = ht.StringMatch
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("route %+v has an unrecognized header matcher: %+v", r, ht)
|
||||
}
|
||||
if smProto != nil {
|
||||
sm, err := matcher.StringMatcherFromProto(smProto)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v has an invalid header matcher: %v", r, err)
|
||||
}
|
||||
header.StringMatch = &sm
|
||||
}
|
||||
// The metadata the matchers run against always has lowercase keys,
|
||||
// so a name that contains an uppercase character matches no header
|
||||
// and the route holding it never fires.
|
||||
header.Name = strings.ToLower(h.GetName())
|
||||
invert := h.GetInvertMatch()
|
||||
header.InvertMatch = &invert
|
||||
route.Headers = append(route.Headers, &header)
|
||||
}
|
||||
|
||||
if fr := match.GetRuntimeFraction(); fr != nil {
|
||||
d := fr.GetDefaultValue()
|
||||
n := d.GetNumerator()
|
||||
switch d.GetDenominator() {
|
||||
case v3typepb.FractionalPercent_HUNDRED:
|
||||
n *= 10000
|
||||
case v3typepb.FractionalPercent_TEN_THOUSAND:
|
||||
n *= 100
|
||||
case v3typepb.FractionalPercent_MILLION:
|
||||
}
|
||||
route.Fraction = &n
|
||||
}
|
||||
|
||||
switch r.GetAction().(type) {
|
||||
case *v3routepb.Route_Route:
|
||||
action := r.GetRoute()
|
||||
|
||||
if envconfig.XDSAuthorityRewrite {
|
||||
if sc != nil && sc.ServerFeaturesTrustedXDSServer() {
|
||||
route.AutoHostRewrite = action.GetAutoHostRewrite().GetValue()
|
||||
}
|
||||
}
|
||||
|
||||
// Hash Policies are only applicable for a Ring Hash LB.
|
||||
hp, err := hashPoliciesProtoToSlice(action.HashPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
route.HashPolicies = hp
|
||||
|
||||
switch a := action.GetClusterSpecifier().(type) {
|
||||
case *v3routepb.RouteAction_Cluster:
|
||||
route.WeightedClusters = append(route.WeightedClusters, WeightedCluster{Name: a.Cluster, Weight: 1})
|
||||
case *v3routepb.RouteAction_WeightedClusters:
|
||||
wcs := a.WeightedClusters
|
||||
var totalWeight uint64
|
||||
for _, c := range wcs.Clusters {
|
||||
w := c.GetWeight().GetValue()
|
||||
if w == 0 {
|
||||
continue
|
||||
}
|
||||
totalWeight += uint64(w)
|
||||
if totalWeight > math.MaxUint32 {
|
||||
return nil, nil, fmt.Errorf("xds: total weight of clusters exceeds MaxUint32")
|
||||
}
|
||||
wc := WeightedCluster{Name: c.GetName(), Weight: w}
|
||||
cfgs, err := processHTTPFilterOverrides(c.GetTypedPerFilterConfig(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v, action %+v: %v", r, a, err)
|
||||
}
|
||||
wc.HTTPFilterConfigOverride = cfgs
|
||||
route.WeightedClusters = append(route.WeightedClusters, wc)
|
||||
}
|
||||
if totalWeight == 0 {
|
||||
return nil, nil, fmt.Errorf("route %+v, action %+v, has no valid cluster in WeightedCluster action", r, a)
|
||||
}
|
||||
case *v3routepb.RouteAction_ClusterSpecifierPlugin:
|
||||
// gRFC A28 was updated to say the following:
|
||||
//
|
||||
// The route’s action field must be route, and its
|
||||
// cluster_specifier:
|
||||
// - Can be Cluster
|
||||
// - Can be Weighted_clusters
|
||||
// - Can be unset or an unsupported field. The route containing
|
||||
// this action will be ignored.
|
||||
//
|
||||
// This means that if this env var is not set, we should treat
|
||||
// it as if it we didn't know about the cluster_specifier_plugin
|
||||
// at all.
|
||||
if _, ok := csps[a.ClusterSpecifierPlugin]; !ok {
|
||||
// "When processing RouteActions, if any action includes a
|
||||
// cluster_specifier_plugin value that is not in
|
||||
// RouteConfiguration.cluster_specifier_plugins, the
|
||||
// resource will be NACKed." - RLS in xDS design
|
||||
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)
|
||||
}
|
||||
if csps[a.ClusterSpecifierPlugin] == nil {
|
||||
logger.Warningf("Ignoring route %+v with optional and unsupported cluster specifier plugin %+v", r, a.ClusterSpecifierPlugin)
|
||||
continue
|
||||
}
|
||||
cspNames[a.ClusterSpecifierPlugin] = true
|
||||
route.ClusterSpecifierPlugin = a.ClusterSpecifierPlugin
|
||||
default:
|
||||
logger.Warningf("Ignoring route %+v with unknown ClusterSpecifier %+v", r, a)
|
||||
continue
|
||||
}
|
||||
|
||||
msd := action.GetMaxStreamDuration()
|
||||
// Prefer grpc_timeout_header_max, if set.
|
||||
dur := msd.GetGrpcTimeoutHeaderMax()
|
||||
if dur == nil {
|
||||
dur = msd.GetMaxStreamDuration()
|
||||
}
|
||||
if dur != nil {
|
||||
d := dur.AsDuration()
|
||||
route.MaxStreamDuration = &d
|
||||
}
|
||||
|
||||
route.RetryConfig, err = generateRetryConfig(action.GetRetryPolicy())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v, action %+v: %v", r, action, err)
|
||||
}
|
||||
|
||||
route.ActionType = RouteActionRoute
|
||||
|
||||
case *v3routepb.Route_NonForwardingAction:
|
||||
// Expected to be used on server side.
|
||||
route.ActionType = RouteActionNonForwardingAction
|
||||
default:
|
||||
route.ActionType = RouteActionUnsupported
|
||||
}
|
||||
|
||||
cfgs, err := processHTTPFilterOverrides(r.GetTypedPerFilterConfig(), bc, sc)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("route %+v: %v", r, err)
|
||||
}
|
||||
route.HTTPFilterConfigOverride = cfgs
|
||||
routesRet = append(routesRet, &route)
|
||||
}
|
||||
return routesRet, cspNames, nil
|
||||
}
|
||||
|
||||
func hashPoliciesProtoToSlice(policies []*v3routepb.RouteAction_HashPolicy) ([]*HashPolicy, error) {
|
||||
var hashPoliciesRet []*HashPolicy
|
||||
for _, p := range policies {
|
||||
policy := HashPolicy{Terminal: p.Terminal}
|
||||
switch p.GetPolicySpecifier().(type) {
|
||||
case *v3routepb.RouteAction_HashPolicy_Header_:
|
||||
policy.HashPolicyType = HashPolicyTypeHeader
|
||||
policy.HeaderName = p.GetHeader().GetHeaderName()
|
||||
if rr := p.GetHeader().GetRegexRewrite(); rr != nil {
|
||||
regex := rr.GetPattern().GetRegex()
|
||||
re, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash policy %+v contains an invalid regex %q", p, regex)
|
||||
}
|
||||
policy.Regex = re
|
||||
policy.RegexSubstitution = rr.GetSubstitution()
|
||||
}
|
||||
case *v3routepb.RouteAction_HashPolicy_FilterState_:
|
||||
if p.GetFilterState().GetKey() != "io.grpc.channel_id" {
|
||||
logger.Warningf("Ignoring hash policy %+v with invalid key for filter state policy %q", p, p.GetFilterState().GetKey())
|
||||
continue
|
||||
}
|
||||
policy.HashPolicyType = HashPolicyTypeChannelID
|
||||
default:
|
||||
logger.Warningf("Ignoring unsupported hash policy %T", p.GetPolicySpecifier())
|
||||
continue
|
||||
}
|
||||
|
||||
hashPoliciesRet = append(hashPoliciesRet, &policy)
|
||||
}
|
||||
return hashPoliciesRet, nil
|
||||
}
|
||||
1910
internal/xds/xdsclient/xdsresource/unmarshal_rds_test.go
Normal file
1910
internal/xds/xdsclient/xdsresource/unmarshal_rds_test.go
Normal file
File diff suppressed because it is too large
Load Diff
44
internal/xds/xdsclient/xdsresource/version/version.go
Normal file
44
internal/xds/xdsclient/xdsresource/version/version.go
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package version defines constants to distinguish between supported xDS API
|
||||
// versions.
|
||||
package version
|
||||
|
||||
// Resource URLs. We need to be able to accept either version of the resource
|
||||
// regardless of the version of the transport protocol in use.
|
||||
const (
|
||||
googleapiPrefix = "type.googleapis.com/"
|
||||
|
||||
V3ListenerType = "envoy.config.listener.v3.Listener"
|
||||
V3RouteConfigType = "envoy.config.route.v3.RouteConfiguration"
|
||||
V3ClusterType = "envoy.config.cluster.v3.Cluster"
|
||||
V3EndpointsType = "envoy.config.endpoint.v3.ClusterLoadAssignment"
|
||||
|
||||
V3ResourceWrapperURL = googleapiPrefix + "envoy.service.discovery.v3.Resource"
|
||||
V3ListenerURL = googleapiPrefix + V3ListenerType
|
||||
V3RouteConfigURL = googleapiPrefix + V3RouteConfigType
|
||||
V3ClusterURL = googleapiPrefix + V3ClusterType
|
||||
V3EndpointsURL = googleapiPrefix + V3EndpointsType
|
||||
V3HTTPConnManagerURL = googleapiPrefix + "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
|
||||
V3UpstreamTLSContextURL = googleapiPrefix + "envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext"
|
||||
V3DownstreamTLSContextURL = googleapiPrefix + "envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext"
|
||||
V3HTTP11ProxyUpstreamTransportURL = googleapiPrefix + "envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport"
|
||||
V3AddressURL = googleapiPrefix + "envoy.config.core.v3.Address"
|
||||
V3AudienceURL = googleapiPrefix + "envoy.extensions.filters.http.gcp_authn.v3.Audience"
|
||||
)
|
||||
133
internal/xds/xdsclient/xdsresource/xdsconfig.go
Normal file
133
internal/xds/xdsclient/xdsresource/xdsconfig.go
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xdsresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// XDSConfig holds the complete gRPC client-side xDS configuration containing
|
||||
// all necessary resources.
|
||||
type XDSConfig struct {
|
||||
// Listener holds the listener configuration. It is guaranteed to be
|
||||
// non-nil.
|
||||
Listener *ListenerUpdate
|
||||
|
||||
// RouteConfig holds the route configuration. It will be populated even if
|
||||
// the route configuration was inlined into the Listener resource. It is
|
||||
// guaranteed to be non-nil.
|
||||
RouteConfig *RouteConfigUpdate
|
||||
|
||||
// VirtualHost is selected from the route configuration whose domain field
|
||||
// offers the best match against the provided dataplane authority. It is
|
||||
// guaranteed to be non-nil.
|
||||
VirtualHost *VirtualHost
|
||||
|
||||
// Clusters is a map from cluster name to its configuration.
|
||||
Clusters map[string]*ClusterResult
|
||||
}
|
||||
|
||||
// ClusterResult contains a cluster's configuration when a valid resource is
|
||||
// received from the management server. It contains an error when:
|
||||
// - an invalid resource is received from the management server and
|
||||
// a valid resource was not already present or
|
||||
// - the cluster resource does not exist on the management server
|
||||
type ClusterResult struct {
|
||||
Config ClusterConfig
|
||||
Err error
|
||||
}
|
||||
|
||||
// ClusterConfig contains configuration for a single cluster.
|
||||
type ClusterConfig struct {
|
||||
// Cluster configuration for the cluster. This field is always set to a
|
||||
// non-nil value.
|
||||
Cluster *ClusterUpdate
|
||||
// EndpointConfig contains endpoint configuration for a leaf cluster. This
|
||||
// field is only set for EDS and LOGICAL_DNS clusters.
|
||||
EndpointConfig *EndpointConfig
|
||||
// AggregateConfig contains configuration for an aggregate cluster. This
|
||||
// field is only set for AGGREGATE clusters.
|
||||
AggregateConfig *AggregateConfig
|
||||
}
|
||||
|
||||
// AggregateConfig holds the configuration for an aggregate cluster.
|
||||
type AggregateConfig struct {
|
||||
// LeafClusters contains a prioritized list of names of the leaf clusters
|
||||
// for the cluster.
|
||||
LeafClusters []string
|
||||
}
|
||||
|
||||
// EndpointConfig contains configuration corresponding to the endpoints in a
|
||||
// cluster. Only one of EDSUpdate or DNSEndpoints will be populated based on the
|
||||
// cluster type.
|
||||
type EndpointConfig struct {
|
||||
// Endpoint configurartion for the EDS clusters.
|
||||
EDSUpdate *EndpointsUpdate
|
||||
// Endpoint configuration for the LOGICAL_DNS clusters.
|
||||
DNSEndpoints *DNSUpdate
|
||||
// ResolutionNote stores error encountered while obtaining endpoints data
|
||||
// for the cluster. It will contain a nil value when a valid endpoint data is
|
||||
// received. It contains an error when:
|
||||
// - an invalid resource is received from the management server or
|
||||
// - the endpoint resource does not exist on the management server
|
||||
ResolutionNote error
|
||||
}
|
||||
|
||||
// DNSUpdate represents the result of a DNS resolution, containing a list of
|
||||
// discovered endpoints.
|
||||
type DNSUpdate struct {
|
||||
// Endpoints is the complete list of endpoints returned by the DNS resolver.
|
||||
Endpoints []resolver.Endpoint
|
||||
}
|
||||
|
||||
// xdsConfigkey is the type used as the key to store XDSConfig in the Attributes
|
||||
// field of resolver.State.
|
||||
type xdsConfigkey struct{}
|
||||
|
||||
// SetXDSConfig returns a copy of state in which the Attributes field is updated
|
||||
// with the XDSConfig.
|
||||
func SetXDSConfig(state resolver.State, config *XDSConfig) resolver.State {
|
||||
state.Attributes = state.Attributes.WithValue(xdsConfigkey{}, config)
|
||||
return state
|
||||
}
|
||||
|
||||
// XDSConfigFromResolverState returns XDSConfig stored as an attribute in the
|
||||
// resolver state.
|
||||
func XDSConfigFromResolverState(state resolver.State) *XDSConfig {
|
||||
if v := state.Attributes.Value(xdsConfigkey{}); v != nil {
|
||||
return v.(*XDSConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type xdsConfigKey struct{}
|
||||
|
||||
// NewContextWithXDSConfig returns a new context with xDSConfig added to it.
|
||||
func NewContextWithXDSConfig(ctx context.Context, config *XDSConfig) context.Context {
|
||||
return context.WithValue(ctx, xdsConfigKey{}, config)
|
||||
}
|
||||
|
||||
// XDSConfigFromContext returns the XDSConfig stored in ctx.
|
||||
func XDSConfigFromContext(ctx context.Context) *XDSConfig {
|
||||
if v := ctx.Value(xdsConfigKey{}); v != nil {
|
||||
return v.(*XDSConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user