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
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
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
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
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
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
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
Stale bot / stale (push) Failing after 1s
This commit is contained in:
479
internal/xds/resolver/cluster_specifier_plugin_test.go
Normal file
479
internal/xds/resolver/cluster_specifier_plugin_test.go
Normal file
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/balancer"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
|
||||
"google.golang.org/grpc/internal/xds/clusterspecifier"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
balancer.Register(cspBalancerBuilder{})
|
||||
clusterspecifier.Register(testClusterSpecifierPlugin{})
|
||||
}
|
||||
|
||||
// cspBalancerBuilder is a no-op LB policy which is referenced by the
|
||||
// testClusterSpecifierPlugin.
|
||||
type cspBalancerBuilder struct{}
|
||||
|
||||
func (cspBalancerBuilder) Build(balancer.ClientConn, balancer.BuildOptions) balancer.Balancer {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cspBalancerBuilder) Name() string {
|
||||
return "csp_experimental"
|
||||
}
|
||||
|
||||
type cspBalancerConfig struct {
|
||||
serviceconfig.LoadBalancingConfig
|
||||
ArbitraryField string `json:"arbitrary_field"`
|
||||
}
|
||||
|
||||
func (cspBalancerBuilder) ParseConfig(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
cfg := &cspBalancerConfig{}
|
||||
if err := json.Unmarshal(lbCfg, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
|
||||
}
|
||||
|
||||
// testClusterSpecifierPlugin is a test cluster specifier plugin which returns
|
||||
// an LB policy configuration specifying the cspBalancer.
|
||||
type testClusterSpecifierPlugin struct {
|
||||
}
|
||||
|
||||
func (testClusterSpecifierPlugin) TypeURLs() []string {
|
||||
// The config for this plugin contains a wrapperspb.StringValue, and since
|
||||
// we marshal that proto as an Any proto, the type URL on the latter gets
|
||||
// set to "type.googleapis.com/google.protobuf.StringValue". If we wanted a
|
||||
// more descriptive type URL for this test plugin, we would have to define a
|
||||
// proto package with a message for the configuration. That would be
|
||||
// overkill for a test. Therefore, this seems to be an acceptable tradeoff.
|
||||
return []string{"type.googleapis.com/google.protobuf.StringValue"}
|
||||
}
|
||||
|
||||
func (testClusterSpecifierPlugin) ParseClusterSpecifierConfig(cfg proto.Message) (clusterspecifier.BalancerConfig, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("testClusterSpecifierPlugin: nil configuration message provided")
|
||||
}
|
||||
anyp, ok := cfg.(*anypb.Any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("testClusterSpecifierPlugin: error parsing config %v: got type %T, want *anypb.Any", cfg, cfg)
|
||||
}
|
||||
lbCfg := new(wrapperspb.StringValue)
|
||||
if err := anypb.UnmarshalTo(anyp, lbCfg, proto.UnmarshalOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("testClusterSpecifierPlugin: error parsing config %v: %v", cfg, err)
|
||||
}
|
||||
return []map[string]any{{"csp_experimental": cspBalancerConfig{ArbitraryField: lbCfg.GetValue()}}}, nil
|
||||
}
|
||||
|
||||
// TestResolverClusterSpecifierPlugin tests the case where a route configuration
|
||||
// containing cluster specifier plugins is sent by the management server. The
|
||||
// test verifies that the service config output by the resolver contains the LB
|
||||
// policy specified by the cluster specifier plugin, and the config selector
|
||||
// returns the cluster associated with the cluster specifier plugin.
|
||||
//
|
||||
// The test also verifies that a change in the cluster specifier plugin config
|
||||
// result in appropriate change in the service config pushed by the resolver.
|
||||
func (s) TestResolverClusterSpecifierPlugin(t *testing.T) {
|
||||
// Spin up an xDS management server for the test.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
nodeID := uuid.New().String()
|
||||
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
|
||||
|
||||
// Configure resources on the management server.
|
||||
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)}
|
||||
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
ListenerName: defaultTestServiceName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
|
||||
ClusterSpecifierPluginName: "cspA",
|
||||
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anything"}),
|
||||
})}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
|
||||
|
||||
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
|
||||
|
||||
// Wait for an update from the resolver, and verify the service config.
|
||||
wantSC := `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspA": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anything"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
|
||||
if err != nil {
|
||||
t.Fatalf("cs.SelectConfig(): %v", err)
|
||||
}
|
||||
|
||||
gotCluster := clustermanager.PickedCluster(res.Context)
|
||||
wantCluster := "cluster_specifier_plugin:cspA"
|
||||
if gotCluster != wantCluster {
|
||||
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
|
||||
}
|
||||
|
||||
// Change the cluster specifier plugin configuration.
|
||||
routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
ListenerName: defaultTestServiceName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
|
||||
ClusterSpecifierPluginName: "cspA",
|
||||
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "changed"}),
|
||||
})}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
|
||||
|
||||
// Wait for an update from the resolver, and verify the service config.
|
||||
wantSC = `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspA": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "changed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
}
|
||||
|
||||
// TestXDSResolverDelayedOnCommittedCSP tests that cluster specifier plugins and
|
||||
// their corresponding configurations remain in service config if RPCs are in
|
||||
// flight.
|
||||
func (s) TestXDSResolverDelayedOnCommittedCSP(t *testing.T) {
|
||||
// Spin up an xDS management server for the test.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
nodeID := uuid.New().String()
|
||||
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
|
||||
|
||||
// Configure resources on the management server.
|
||||
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)}
|
||||
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
ListenerName: defaultTestServiceName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
|
||||
ClusterSpecifierPluginName: "cspA",
|
||||
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anythingA"}),
|
||||
})}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
|
||||
|
||||
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
|
||||
|
||||
// Wait for an update from the resolver, and verify the service config.
|
||||
wantSC := `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspA": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anythingA"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
|
||||
resOld, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
|
||||
if err != nil {
|
||||
t.Fatalf("cs.SelectConfig(): %v", err)
|
||||
}
|
||||
|
||||
gotCluster := clustermanager.PickedCluster(resOld.Context)
|
||||
wantCluster := "cluster_specifier_plugin:cspA"
|
||||
if gotCluster != wantCluster {
|
||||
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
|
||||
}
|
||||
|
||||
// Delay resOld.OnCommitted(). As long as there are pending RPCs to removed
|
||||
// clusters, they still appear in the service config.
|
||||
|
||||
// Change the cluster specifier plugin configuration.
|
||||
routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
ListenerName: defaultTestServiceName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
|
||||
ClusterSpecifierPluginName: "cspB",
|
||||
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anythingB"}),
|
||||
})}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
|
||||
|
||||
// Wait for an update from the resolver, and verify the service config.
|
||||
wantSC = `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspA": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anythingA"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cluster_specifier_plugin:cspB": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anythingB"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
cs = verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
|
||||
// Perform an RPC and ensure that it is routed to the new cluster.
|
||||
resNew, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
|
||||
if err != nil {
|
||||
t.Fatalf("cs.SelectConfig(): %v", err)
|
||||
}
|
||||
|
||||
gotCluster = clustermanager.PickedCluster(resNew.Context)
|
||||
wantCluster = "cluster_specifier_plugin:cspB"
|
||||
if gotCluster != wantCluster {
|
||||
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
|
||||
}
|
||||
|
||||
// Invoke resOld.OnCommitted; should lead to a service config update that deletes
|
||||
// cspA.
|
||||
resOld.OnCommitted()
|
||||
|
||||
wantSC = `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspB": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anythingB"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
}
|
||||
|
||||
// TestResolverClusterSpecifierPlugin_WithFilters tests the case where a route
|
||||
// configuration containing cluster specifier plugins is sent by the management
|
||||
// server, and HTTP filters are configured. The test verifies that the
|
||||
// interceptor chain is built for routes matching cluster specifier plugins.
|
||||
func (s) TestResolverClusterSpecifierPlugin_WithFilters(t *testing.T) {
|
||||
// Register custom httpFilter builders for the test.
|
||||
testFilterTypeURL1 := "test-filter-type-url-1" + uuid.New().String()
|
||||
testFilterTypeURL2 := "test-filter-type-url-2" + uuid.New().String()
|
||||
newStreamChan := testutils.NewChannelWithSize(2)
|
||||
fb1 := &testHTTPFilterWithRPCMetadata{
|
||||
logger: t,
|
||||
typeURL: testFilterTypeURL1,
|
||||
newStreamChan: newStreamChan,
|
||||
}
|
||||
fb2 := &testHTTPFilterWithRPCMetadata{
|
||||
logger: t,
|
||||
typeURL: testFilterTypeURL2,
|
||||
newStreamChan: newStreamChan,
|
||||
}
|
||||
httpfilter.Register(fb1)
|
||||
httpfilter.Register(fb2)
|
||||
defer httpfilter.UnregisterForTesting(fb1.typeURL)
|
||||
defer httpfilter.UnregisterForTesting(fb2.typeURL)
|
||||
|
||||
// Spin up an xDS management server.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
nodeID := uuid.New().String()
|
||||
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
|
||||
|
||||
// Configure resources on the management server.
|
||||
// We need a listener with the filter, and a route with ClusterSpecifierPlugin.
|
||||
listeners := []*v3listenerpb.Listener{{
|
||||
Name: defaultTestServiceName,
|
||||
ApiListener: &v3listenerpb.ApiListener{
|
||||
ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{
|
||||
ConfigSource: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
|
||||
},
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
}},
|
||||
HttpFilters: []*v3httppb.HttpFilter{
|
||||
newHTTPFilter(t, "test-filter-1", testFilterTypeURL1, "filter-path-1", ""),
|
||||
newHTTPFilter(t, "test-filter-2", testFilterTypeURL2, "filter-path-2", ""),
|
||||
e2e.RouterHTTPFilter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}}
|
||||
|
||||
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: defaultTestRouteConfigName,
|
||||
ListenerName: defaultTestServiceName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
|
||||
ClusterSpecifierPluginName: "cspA",
|
||||
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anything"}),
|
||||
})}
|
||||
// Override the configuration for "test-filter-1" in the route.
|
||||
routes[0].VirtualHosts[0].Routes[0].TypedPerFilterConfig = map[string]*anypb.Any{
|
||||
"test-filter-1": newHTTPFilter(t, "test-filter-1", testFilterTypeURL1, "override-path-1", "").GetTypedConfig(),
|
||||
}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
|
||||
|
||||
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
|
||||
|
||||
// Wait for an update from the resolver, and verify the service config.
|
||||
wantSC := `
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster_specifier_plugin:cspA": {
|
||||
"childPolicy": [
|
||||
{
|
||||
"csp_experimental": {
|
||||
"arbitrary_field": "anything"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
|
||||
res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
|
||||
if err != nil {
|
||||
t.Fatalf("cs.SelectConfig(): %v", err)
|
||||
}
|
||||
|
||||
// Verify that the interceptor is not nil.
|
||||
if res.Interceptor == nil {
|
||||
t.Fatal("RPCInfo does not contain interceptors list")
|
||||
}
|
||||
newStream := func(context.Context, ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if interceptor, ok := res.Interceptor.(httpfilter.ClientInterceptor); ok {
|
||||
if _, err = interceptor.NewStream(ctx, iresolver.RPCInfo{Method: "/service/method", Context: ctx}, newStream); err != nil {
|
||||
t.Fatalf("NewStream() failed with error: %v", err)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("res.Interceptor is type %T, want httpfilter.ClientInterceptor", res.Interceptor)
|
||||
}
|
||||
|
||||
// Verify that first filter receives the config.
|
||||
cfg, err := newStreamChan.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timeout waiting for first filter to receive config: %v", err)
|
||||
}
|
||||
ofc := cfg.(overallFilterConfig)
|
||||
if ofc.BasePath != "filter-path-1" {
|
||||
t.Fatalf("Unexpected base path for first filter, got: %q, want: %q", ofc.BasePath, "filter-path-1")
|
||||
}
|
||||
if ofc.OverridePath != "override-path-1" {
|
||||
t.Fatalf("Unexpected override path for first filter, got: %q, want: %q", ofc.OverridePath, "override-path-1")
|
||||
}
|
||||
|
||||
// Verify that second filter receives the base path.
|
||||
cfg, err = newStreamChan.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timeout waiting for second filter to receive config: %v", err)
|
||||
}
|
||||
ofc = cfg.(overallFilterConfig)
|
||||
if ofc.BasePath != "filter-path-2" {
|
||||
t.Fatalf("Unexpected base path for second filter, got: %q, want: %q", ofc.BasePath, "filter-path-2")
|
||||
}
|
||||
if ofc.OverridePath != "" {
|
||||
t.Fatalf("Unexpected override path for second filter, got: %q, want: %q", ofc.OverridePath, "")
|
||||
}
|
||||
}
|
||||
288
internal/xds/resolver/helpers_test.go
Normal file
288
internal/xds/resolver/helpers_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
xdsresolver "google.golang.org/grpc/internal/xds/resolver"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 100 * time.Microsecond
|
||||
|
||||
defaultTestServiceName = "service-name"
|
||||
defaultTestRouteConfigName = "route-config-name"
|
||||
defaultTestClusterName = "cluster-name"
|
||||
defaultTestEndpointName = "endpoint-name"
|
||||
defaultTestHostname = "test-host"
|
||||
)
|
||||
|
||||
var defaultTestPort = []uint32{8080}
|
||||
|
||||
// wantServiceConfig returns a JSON representation of a service config with
|
||||
// xds_cluster_manager_experimental LB policy with a child policy of
|
||||
// cds_experimental for the provided cluster name.
|
||||
func wantServiceConfig(clusterName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"loadBalancingConfig": [{
|
||||
"xds_cluster_manager_experimental": {
|
||||
"children": {
|
||||
"cluster:%s": {
|
||||
"childPolicy": [{
|
||||
"cds_experimental": {
|
||||
"cluster": "%s"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}`, clusterName, clusterName)
|
||||
}
|
||||
|
||||
// buildResolverForTarget builds an xDS resolver for the given target. If
|
||||
// the bootstrap contents are provided, it build the xDS resolver using them
|
||||
// otherwise, it uses the default xDS resolver.
|
||||
//
|
||||
// It returns the following:
|
||||
// - a channel to read updates from the resolver
|
||||
// - a channel to read errors from the resolver
|
||||
// - the newly created xDS resolver
|
||||
func buildResolverForTarget(t *testing.T, target resolver.Target, bootstrapContents []byte) (chan resolver.State, chan error, resolver.Resolver) {
|
||||
t.Helper()
|
||||
|
||||
var builder resolver.Builder
|
||||
if bootstrapContents != nil {
|
||||
// Create an xDS resolver with the provided bootstrap configuration.
|
||||
var err error
|
||||
builder, err = internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
|
||||
}
|
||||
} else {
|
||||
builder = resolver.Get(xdsresolver.Scheme)
|
||||
if builder == nil {
|
||||
t.Fatalf("Scheme %q is not registered", xdsresolver.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
stateCh := make(chan resolver.State, 1)
|
||||
updateStateF := func(s resolver.State) error {
|
||||
stateCh <- s
|
||||
return nil
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
reportErrorF := func(err error) {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF}
|
||||
r, err := builder.Build(target, tcc, resolver.BuildOptions{
|
||||
Authority: url.PathEscape(target.Endpoint()),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build xDS resolver for target %q: %v", target, err)
|
||||
}
|
||||
t.Cleanup(r.Close)
|
||||
return stateCh, errCh, r
|
||||
}
|
||||
|
||||
// verifyUpdateFromResolver waits for the resolver to push an update to the fake
|
||||
// resolver.ClientConn and verifies that update matches the provided service
|
||||
// config.
|
||||
//
|
||||
// Tests that want to skip verifying the contents of the service config can pass
|
||||
// an empty string.
|
||||
//
|
||||
// Returns the config selector from the state update pushed by the resolver.
|
||||
// Tests that don't need the config selector can ignore the return value.
|
||||
func verifyUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State, wantSC string) iresolver.ConfigSelector {
|
||||
t.Helper()
|
||||
|
||||
var state resolver.State
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err())
|
||||
case state = <-stateCh:
|
||||
if err := state.ServiceConfig.Err; err != nil {
|
||||
t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err)
|
||||
}
|
||||
if wantSC == "" {
|
||||
break
|
||||
}
|
||||
wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantSC)
|
||||
if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) {
|
||||
t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config))
|
||||
}
|
||||
}
|
||||
cs := iresolver.GetConfigSelector(state)
|
||||
if cs == nil {
|
||||
t.Fatal("Received nil config selector in update from resolver")
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
// verifyNoUpdateFromResolver verifies that no update is pushed on stateCh.
|
||||
// Calls t.Fatal() if an update is received before defaultTestShortTimeout
|
||||
// expires.
|
||||
func verifyNoUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State) {
|
||||
t.Helper()
|
||||
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
select {
|
||||
case <-sCtx.Done():
|
||||
case u := <-stateCh:
|
||||
t.Fatalf("Received update from resolver %v when none expected", u)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyResolverError(gotErr error, wantCode codes.Code, wantErr, wantNodeID string) error {
|
||||
if gotErr == nil {
|
||||
return fmt.Errorf("got nil error from resolver, want error with code %v", wantCode)
|
||||
}
|
||||
if !strings.Contains(gotErr.Error(), wantErr) {
|
||||
return fmt.Errorf("got error from resolver %q, want %q", gotErr, wantErr)
|
||||
}
|
||||
if gotCode := status.Code(gotErr); gotCode != wantCode {
|
||||
return fmt.Errorf("got error from resolver with code %v, want %v", gotCode, wantCode)
|
||||
}
|
||||
if !strings.Contains(gotErr.Error(), wantNodeID) {
|
||||
return fmt.Errorf("got error from resolver %q, want nodeID %q", gotErr, wantNodeID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Spins up an xDS management server and sets up an xDS bootstrap configuration
|
||||
// file that points to it.
|
||||
//
|
||||
// Returns the following:
|
||||
// - A reference to the xDS management server
|
||||
// - A channel to read requested Listener resource names
|
||||
// - A channel to read requested RouteConfiguration resource names
|
||||
// - Contents of the bootstrap configuration pointing to xDS management
|
||||
// server
|
||||
func setupManagementServerForTest(t *testing.T, nodeID string) (*e2e.ManagementServer, chan []string, chan []string, []byte) {
|
||||
t.Helper()
|
||||
|
||||
listenerResourceNamesCh := make(chan []string, 1)
|
||||
routeConfigResourceNamesCh := make(chan []string, 1)
|
||||
|
||||
// Setup the management server to push the requested listener and route
|
||||
// configuration resource names on to separate channels for the test to
|
||||
// inspect.
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
switch req.GetTypeUrl() {
|
||||
case version.V3ListenerURL:
|
||||
select {
|
||||
case <-listenerResourceNamesCh:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case listenerResourceNamesCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
case version.V3RouteConfigURL:
|
||||
select {
|
||||
case <-routeConfigResourceNamesCh:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case routeConfigResourceNamesCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create a bootstrap configuration specifying the above management server.
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
return mgmtServer, listenerResourceNamesCh, routeConfigResourceNamesCh, bootstrapContents
|
||||
}
|
||||
|
||||
// Updates all resources on the given management server.
|
||||
func configureResources(ctx context.Context, t *testing.T, mgmtServer *e2e.ManagementServer, nodeID string, listeners []*v3listenerpb.Listener, routes []*v3routepb.RouteConfiguration, clusters []*v3clusterpb.Cluster, endpoints []*v3endpointpb.ClusterLoadAssignment) {
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: listeners,
|
||||
Routes: routes,
|
||||
Clusters: clusters,
|
||||
Endpoints: endpoints,
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// waitForResourceNames waits for the wantNames to be pushed on to namesCh.
|
||||
// Fails the test by calling t.Fatal if the context expires before that.
|
||||
func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) {
|
||||
t.Helper()
|
||||
|
||||
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case gotNames := <-namesCh:
|
||||
if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
|
||||
return
|
||||
}
|
||||
t.Logf("Received resource names %v, want %v", gotNames, wantNames)
|
||||
}
|
||||
}
|
||||
t.Fatalf("Timeout waiting for resource to be requested from the management server")
|
||||
}
|
||||
30
internal/xds/resolver/internal/internal.go
Normal file
30
internal/xds/resolver/internal/internal.go
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal contains functionality internal to the xDS resolver.
|
||||
package internal
|
||||
|
||||
// The following variables are overridden in tests.
|
||||
var (
|
||||
// NewWRR is the function used to create a new weighted round robin
|
||||
// implementation.
|
||||
NewWRR any // func() wrr.WRR
|
||||
|
||||
// NewXDSClient is the function used to create a new xDS client.
|
||||
NewXDSClient any // func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error)
|
||||
)
|
||||
34
internal/xds/resolver/logging.go
Normal file
34
internal/xds/resolver/logging.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
const prefix = "[xds-resolver %p] "
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
func prefixLogger(p *xdsResolver) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
|
||||
}
|
||||
381
internal/xds/resolver/serviceconfig.go
Normal file
381
internal/xds/resolver/serviceconfig.go
Normal file
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
rand "math/rand/v2"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/grpcutil"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/internal/wrr"
|
||||
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
|
||||
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
cdsName = "cds_experimental"
|
||||
xdsClusterManagerName = "xds_cluster_manager_experimental"
|
||||
clusterPrefix = "cluster:"
|
||||
clusterSpecifierPluginPrefix = "cluster_specifier_plugin:"
|
||||
)
|
||||
|
||||
type serviceConfig struct {
|
||||
LoadBalancingConfig balancerConfig `json:"loadBalancingConfig"`
|
||||
}
|
||||
|
||||
type balancerConfig []map[string]any
|
||||
|
||||
func newBalancerConfig(name string, config any) balancerConfig {
|
||||
return []map[string]any{{name: config}}
|
||||
}
|
||||
|
||||
type cdsBalancerConfig struct {
|
||||
Cluster string `json:"cluster"`
|
||||
}
|
||||
|
||||
type xdsChildConfig struct {
|
||||
ChildPolicy balancerConfig `json:"childPolicy"`
|
||||
}
|
||||
|
||||
type xdsClusterManagerConfig struct {
|
||||
Children map[string]xdsChildConfig `json:"children"`
|
||||
}
|
||||
|
||||
// serviceConfigJSON produces a service config in JSON format that contains LB
|
||||
// policy config for the "xds_cluster_manager" LB policy, with entries in the
|
||||
// children map for all active clusters.
|
||||
func serviceConfigJSON(activeClusters map[string]*clusterInfo, activePlugins map[string]*clusterInfo) []byte {
|
||||
// Generate children (all entries in activeClusters).
|
||||
children := make(map[string]xdsChildConfig)
|
||||
for cluster, ci := range activeClusters {
|
||||
children[cluster] = ci.cfg
|
||||
}
|
||||
for plugin, ci := range activePlugins {
|
||||
children[plugin] = ci.cfg
|
||||
}
|
||||
|
||||
sc := serviceConfig{
|
||||
LoadBalancingConfig: newBalancerConfig(
|
||||
xdsClusterManagerName, xdsClusterManagerConfig{Children: children},
|
||||
),
|
||||
}
|
||||
|
||||
// This is not expected to fail as we have constructed the service config by
|
||||
// hand right above, and therefore ok to panic.
|
||||
bs, err := json.Marshal(sc)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to marshal service config %+v: %v", sc, err))
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
type virtualHost struct {
|
||||
// retry policy present in virtual host
|
||||
retryConfig *xdsresource.RetryConfig
|
||||
}
|
||||
|
||||
// routeCluster holds information about a cluster as referenced by a route.
|
||||
type routeCluster struct {
|
||||
name string // Name of the cluster.
|
||||
interceptor httpfilter.ClientInterceptor // HTTP filters to run for RPCs matching this route.
|
||||
}
|
||||
|
||||
type route struct {
|
||||
m *xdsresource.CompositeMatcher // converted from route matchers
|
||||
actionType xdsresource.RouteActionType // holds route action type
|
||||
clusters wrr.WRR // holds *routeCluster entries
|
||||
routeClusters []*grpcsync.RefCounted[*routeCluster] // Route clusters belonging to this route
|
||||
maxStreamDuration time.Duration
|
||||
retryConfig *xdsresource.RetryConfig
|
||||
hashPolicies []*xdsresource.HashPolicy
|
||||
autoHostRewrite bool
|
||||
}
|
||||
|
||||
func (r route) String() string {
|
||||
return fmt.Sprintf("%s -> { clusters: %v, maxStreamDuration: %v }", r.m.String(), r.clusters, r.maxStreamDuration)
|
||||
}
|
||||
|
||||
// stoppableConfigSelector extends the iresolver.ConfigSelector interface with a
|
||||
// stop() method. This makes it possible to swap the current config selector
|
||||
// with an erroring config selector when the LDS or RDS resource is not found on
|
||||
// the management server.
|
||||
type stoppableConfigSelector interface {
|
||||
iresolver.ConfigSelector
|
||||
stop()
|
||||
}
|
||||
|
||||
// erroringConfigSelector always returns an error, with the xDS node ID included
|
||||
// in the error message. It is used to swap out the current config selector
|
||||
// when the LDS or RDS resource is not found on the management server.
|
||||
type erroringConfigSelector struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newErroringConfigSelector(err error, xdsNodeID string) *erroringConfigSelector {
|
||||
return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Error(codes.Unavailable, err.Error()), xdsNodeID)}
|
||||
}
|
||||
|
||||
func (cs *erroringConfigSelector) SelectConfig(iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
|
||||
return nil, cs.err
|
||||
}
|
||||
func (cs *erroringConfigSelector) stop() {}
|
||||
|
||||
type configSelector struct {
|
||||
channelID uint64 // Static hash when hash policy is HashPolicyTypeChannelID
|
||||
xdsNodeID string // xDS node ID, for annotating errors.
|
||||
sendNewServiceConfig func() // Function to send a new service config to gRPC.
|
||||
|
||||
// Configuration received from the xDS management server.
|
||||
virtualHost virtualHost
|
||||
routes []route
|
||||
clusters map[string]*clusterInfo
|
||||
plugins map[string]*clusterInfo
|
||||
httpFilterConfig []xdsresource.HTTPFilter
|
||||
xdsConfig *xdsresource.XDSConfig
|
||||
}
|
||||
|
||||
var errNoMatchedRouteFound = status.Errorf(codes.Unavailable, "no matched route was found")
|
||||
var errUnsupportedClientRouteAction = status.Errorf(codes.Unavailable, "matched route does not have a supported route action type")
|
||||
|
||||
// annotateErrorWithNodeID annotates the given error with the provided xDS node
|
||||
// ID. This is used by the real config selector when it runs into errors, and
|
||||
// also by the erroring config selector.
|
||||
func annotateErrorWithNodeID(err error, nodeID string) error {
|
||||
return fmt.Errorf("[xDS node id: %s]: %w", nodeID, err)
|
||||
}
|
||||
|
||||
func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
|
||||
var rt *route
|
||||
md, _ := metadata.FromOutgoingContext(rpcInfo.Context)
|
||||
if extraMD, ok := grpcutil.ExtraMetadata(rpcInfo.Context); ok {
|
||||
md = metadata.Join(md, extraMD)
|
||||
// Remove all binary headers. They are hard to match with. May need
|
||||
// to add back if asked by users.
|
||||
for k := range md {
|
||||
if strings.HasSuffix(k, "-bin") {
|
||||
delete(md, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through routes in order and select first match.
|
||||
for _, r := range cs.routes {
|
||||
if r.m.Match(rpcInfo.Method, md) {
|
||||
rt = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rt == nil || rt.clusters == nil {
|
||||
return nil, annotateErrorWithNodeID(errNoMatchedRouteFound, cs.xdsNodeID)
|
||||
}
|
||||
|
||||
if rt.actionType != xdsresource.RouteActionRoute {
|
||||
return nil, annotateErrorWithNodeID(errUnsupportedClientRouteAction, cs.xdsNodeID)
|
||||
}
|
||||
|
||||
rc, ok := rt.clusters.Next().(*grpcsync.RefCounted[*routeCluster])
|
||||
if !ok {
|
||||
return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", rc, rc), cs.xdsNodeID)
|
||||
}
|
||||
cluster := rc.Value()
|
||||
lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
|
||||
lbCtx = xdsresource.NewContextWithXDSConfig(lbCtx, cs.xdsConfig)
|
||||
lbCtx = iringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
|
||||
if rt.autoHostRewrite {
|
||||
lbCtx = clusterimpl.EnableAutoHostRewrite(lbCtx)
|
||||
}
|
||||
|
||||
config := &iresolver.RPCConfig{
|
||||
Context: lbCtx,
|
||||
Interceptor: cluster.interceptor,
|
||||
}
|
||||
// Add a ref to the selected cluster to keep the interceptors alive until RPC
|
||||
// is committed.
|
||||
rc.Increment()
|
||||
if info, ok := cs.clusters[cluster.name]; ok {
|
||||
// Add a ref to the selected cluster, as this RPC needs this
|
||||
// cluster until it is committed.
|
||||
info.refCount.Add(1)
|
||||
config.OnCommitted = sync.OnceFunc(func() {
|
||||
if v := info.refCount.Add(-1); v == 0 {
|
||||
// We call unsubscribe rather than sendNewServiceConfig to
|
||||
// prevent redundant updates. If the reference count in the
|
||||
// dependency manager drops to zero, it will automatically
|
||||
// trigger a service config update with this cluster
|
||||
// removed. Calling unsubscribe allows the dependency
|
||||
// manager to handle the update flow once and for all.
|
||||
info.unsubscribe()
|
||||
}
|
||||
// Decrement the refcount of the route cluster and close the interceptor
|
||||
// if refcount goes to zero.
|
||||
rc.Decrement()
|
||||
})
|
||||
} else if info, ok := cs.plugins[cluster.name]; ok {
|
||||
// Add a ref to the selected plugin, as this RPC needs this
|
||||
// plugin until it is committed.
|
||||
info.refCount.Add(1)
|
||||
config.OnCommitted = sync.OnceFunc(func() {
|
||||
if v := info.refCount.Add(-1); v == 0 {
|
||||
// This entry will be removed from activePlugins when
|
||||
// producing a new service config update.
|
||||
cs.sendNewServiceConfig()
|
||||
}
|
||||
// Decrement the refcount of the route cluster and close the interceptor
|
||||
// if refcount goes to zero.
|
||||
rc.Decrement()
|
||||
})
|
||||
} else {
|
||||
// This should be unreachable because all route clusters are normalized
|
||||
// into cs.clusters or cs.plugins during config selector creation.
|
||||
panic(fmt.Sprintf("matched cluster %q not found in ConfigSelector", cluster.name))
|
||||
}
|
||||
|
||||
if rt.maxStreamDuration != 0 {
|
||||
config.MethodConfig.Timeout = &rt.maxStreamDuration
|
||||
}
|
||||
if rt.retryConfig != nil {
|
||||
config.MethodConfig.RetryPolicy = retryConfigToPolicy(rt.retryConfig)
|
||||
} else if cs.virtualHost.retryConfig != nil {
|
||||
config.MethodConfig.RetryPolicy = retryConfigToPolicy(cs.virtualHost.retryConfig)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func retryConfigToPolicy(config *xdsresource.RetryConfig) *serviceconfig.RetryPolicy {
|
||||
return &serviceconfig.RetryPolicy{
|
||||
MaxAttempts: int(config.NumRetries) + 1,
|
||||
InitialBackoff: config.RetryBackoff.BaseInterval,
|
||||
MaxBackoff: config.RetryBackoff.MaxInterval,
|
||||
BackoffMultiplier: 2,
|
||||
RetryableStatusCodes: config.RetryOn,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *configSelector) generateHash(rpcInfo iresolver.RPCInfo, hashPolicies []*xdsresource.HashPolicy) uint64 {
|
||||
var hash uint64
|
||||
var generatedHash bool
|
||||
var md, emd metadata.MD
|
||||
var mdRead bool
|
||||
for _, policy := range hashPolicies {
|
||||
var policyHash uint64
|
||||
var generatedPolicyHash bool
|
||||
switch policy.HashPolicyType {
|
||||
case xdsresource.HashPolicyTypeHeader:
|
||||
if strings.HasSuffix(policy.HeaderName, "-bin") {
|
||||
continue
|
||||
}
|
||||
if !mdRead {
|
||||
md, _ = metadata.FromOutgoingContext(rpcInfo.Context)
|
||||
emd, _ = grpcutil.ExtraMetadata(rpcInfo.Context)
|
||||
mdRead = true
|
||||
}
|
||||
values := emd.Get(policy.HeaderName)
|
||||
if len(values) == 0 {
|
||||
// Extra metadata (e.g. the "content-type" header) takes
|
||||
// precedence over the user's metadata.
|
||||
values = md.Get(policy.HeaderName)
|
||||
if len(values) == 0 {
|
||||
// If the header isn't present at all, this policy is a no-op.
|
||||
continue
|
||||
}
|
||||
}
|
||||
joinedValues := strings.Join(values, ",")
|
||||
if policy.Regex != nil {
|
||||
joinedValues = policy.Regex.ReplaceAllString(joinedValues, policy.RegexSubstitution)
|
||||
}
|
||||
policyHash = xxhash.Sum64String(joinedValues)
|
||||
generatedHash = true
|
||||
generatedPolicyHash = true
|
||||
case xdsresource.HashPolicyTypeChannelID:
|
||||
// Use the static channel ID as the hash for this policy.
|
||||
policyHash = cs.channelID
|
||||
generatedHash = true
|
||||
generatedPolicyHash = true
|
||||
}
|
||||
|
||||
// Deterministically combine the hash policies. Rotating prevents
|
||||
// duplicate hash policies from cancelling each other out and preserves
|
||||
// the 64 bits of entropy.
|
||||
if generatedPolicyHash {
|
||||
hash = bits.RotateLeft64(hash, 1)
|
||||
hash = hash ^ policyHash
|
||||
}
|
||||
|
||||
// If terminal policy and a hash has already been generated, ignore the
|
||||
// rest of the policies and use that hash already generated.
|
||||
if policy.Terminal && generatedHash {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if generatedHash {
|
||||
return hash
|
||||
}
|
||||
// If no generated hash return a random long. In the grand scheme of things
|
||||
// this logically will map to choosing a random backend to route request to.
|
||||
return rand.Uint64()
|
||||
}
|
||||
|
||||
// stop decrements refs of all clusters referenced by this config selector.
|
||||
func (cs *configSelector) stop() {
|
||||
// The resolver's old configSelector may be nil. Handle that here.
|
||||
if cs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Decrement the refcount of all the route clusters associated with this
|
||||
// config selector and close the interceptors of the route cluster if it's
|
||||
// refcount goes to zero.
|
||||
for _, r := range cs.routes {
|
||||
for _, rc := range r.routeClusters {
|
||||
rc.Decrement()
|
||||
}
|
||||
}
|
||||
|
||||
// If any reference counts drop to zero, a service config update is required
|
||||
// to remove the clusters. Since the old config selector is stopped
|
||||
// after a new one is active, we must trigger a subsequent update to delete
|
||||
// the now-unused clusters.
|
||||
for _, ci := range cs.clusters {
|
||||
if v := ci.refCount.Add(-1); v == 0 {
|
||||
ci.unsubscribe()
|
||||
}
|
||||
}
|
||||
for _, ci := range cs.plugins {
|
||||
if v := ci.refCount.Add(-1); v == 0 {
|
||||
cs.sendNewServiceConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
187
internal/xds/resolver/serviceconfig_test.go
Normal file
187
internal/xds/resolver/serviceconfig_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/grpcutil"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
_ "google.golang.org/grpc/internal/xds/balancer/cdsbalancer" // To parse LB config
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var defaultTestTimeout = 10 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestPruneActiveClusters(t *testing.T) {
|
||||
newClusterInfo := func(ref int32, unsubscribe func()) *clusterInfo {
|
||||
ci := &clusterInfo{unsubscribe: unsubscribe}
|
||||
ci.refCount.Store(ref)
|
||||
return ci
|
||||
}
|
||||
r := &xdsResolver{
|
||||
activeClusters: map[string]*clusterInfo{
|
||||
"zero": newClusterInfo(0, func() {}),
|
||||
"one": newClusterInfo(1, func() {}),
|
||||
"two": newClusterInfo(2, func() {}),
|
||||
"anotherzero": newClusterInfo(0, func() {}),
|
||||
},
|
||||
activePlugins: map[string]*clusterInfo{
|
||||
"zero": newClusterInfo(0, nil),
|
||||
"one": newClusterInfo(1, nil),
|
||||
"two": newClusterInfo(2, nil),
|
||||
"anotherzero": newClusterInfo(0, nil),
|
||||
},
|
||||
}
|
||||
wantActiveClusters := map[string]int32{
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
}
|
||||
wantActivePlugins := map[string]int32{
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
}
|
||||
r.pruneActiveClustersAndPlugins()
|
||||
|
||||
getRefCounts := func(m map[string]*clusterInfo) map[string]int32 {
|
||||
res := make(map[string]int32)
|
||||
for k, v := range m {
|
||||
res[k] = v.refCount.Load()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if d := cmp.Diff(getRefCounts(r.activeClusters), wantActiveClusters); d != "" {
|
||||
t.Fatalf("r.activeClusters refCounts mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
if d := cmp.Diff(getRefCounts(r.activePlugins), wantActivePlugins); d != "" {
|
||||
t.Fatalf("r.activePlugins refCounts mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestGenerateRequestHash(t *testing.T) {
|
||||
const channelID = 12378921
|
||||
cs := &configSelector{channelID: channelID}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
tests := []struct {
|
||||
name string
|
||||
hashPolicies []*xdsresource.HashPolicy
|
||||
requestHashWant uint64
|
||||
rpcInfo iresolver.RPCInfo
|
||||
}{
|
||||
// TestGenerateRequestHashHeaders tests generating request hashes for
|
||||
// hash policies that specify to hash headers.
|
||||
{
|
||||
name: "test-generate-request-hash-headers",
|
||||
hashPolicies: []*xdsresource.HashPolicy{{
|
||||
HashPolicyType: xdsresource.HashPolicyTypeHeader,
|
||||
HeaderName: ":path",
|
||||
Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(), // Will replace /products with /new-products, to test find and replace functionality.
|
||||
RegexSubstitution: "/new-products",
|
||||
}},
|
||||
requestHashWant: xxhash.Sum64String("/new-products"),
|
||||
rpcInfo: iresolver.RPCInfo{
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "/products")),
|
||||
Method: "/some-method",
|
||||
},
|
||||
},
|
||||
// TestGenerateHashChannelID tests generating request hashes for hash
|
||||
// policies that specify to hash something that uniquely identifies the
|
||||
// ClientConn (the pointer).
|
||||
{
|
||||
name: "test-generate-request-hash-channel-id",
|
||||
hashPolicies: []*xdsresource.HashPolicy{{
|
||||
HashPolicyType: xdsresource.HashPolicyTypeChannelID,
|
||||
}},
|
||||
requestHashWant: channelID,
|
||||
rpcInfo: iresolver.RPCInfo{},
|
||||
},
|
||||
// TestGenerateRequestHashEmptyString tests generating request hashes
|
||||
// for hash policies that specify to hash headers and replace empty
|
||||
// strings in the headers.
|
||||
{
|
||||
name: "test-generate-request-hash-empty-string",
|
||||
hashPolicies: []*xdsresource.HashPolicy{{
|
||||
HashPolicyType: xdsresource.HashPolicyTypeHeader,
|
||||
HeaderName: ":path",
|
||||
Regex: func() *regexp.Regexp { return regexp.MustCompile("") }(),
|
||||
RegexSubstitution: "e",
|
||||
}},
|
||||
requestHashWant: xxhash.Sum64String("eaebece"),
|
||||
rpcInfo: iresolver.RPCInfo{
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "abc")),
|
||||
Method: "/some-method",
|
||||
},
|
||||
},
|
||||
// Tests that bin headers are skipped.
|
||||
{
|
||||
name: "skip-bin",
|
||||
hashPolicies: []*xdsresource.HashPolicy{{
|
||||
HashPolicyType: xdsresource.HashPolicyTypeHeader,
|
||||
HeaderName: "something-bin",
|
||||
}, {
|
||||
HashPolicyType: xdsresource.HashPolicyTypeChannelID,
|
||||
}},
|
||||
requestHashWant: channelID,
|
||||
rpcInfo: iresolver.RPCInfo{
|
||||
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("something-bin", "xyz")),
|
||||
},
|
||||
},
|
||||
// Tests that extra metadata takes precedence over the user's metadata.
|
||||
{
|
||||
name: "extra-metadata",
|
||||
hashPolicies: []*xdsresource.HashPolicy{{
|
||||
HashPolicyType: xdsresource.HashPolicyTypeHeader,
|
||||
HeaderName: "content-type",
|
||||
}},
|
||||
requestHashWant: xxhash.Sum64String("grpc value"),
|
||||
rpcInfo: iresolver.RPCInfo{
|
||||
Context: grpcutil.WithExtraMetadata(
|
||||
metadata.NewOutgoingContext(ctx, metadata.Pairs("content-type", "user value")),
|
||||
metadata.Pairs("content-type", "grpc value"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
requestHashGot := cs.generateHash(test.rpcInfo, test.hashPolicies)
|
||||
if requestHashGot != test.requestHashWant {
|
||||
t.Fatalf("requestHashGot = %v, requestHashWant = %v", requestHashGot, test.requestHashWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
187
internal/xds/resolver/watch_service_test.go
Normal file
187
internal/xds/resolver/watch_service_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
*
|
||||
* 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 resolver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// Tests the case where the listener resource starts pointing to a new route
|
||||
// configuration resource after the xDS resolver has successfully resolved the
|
||||
// service name and pushed an update on the channel. The test verifies that the
|
||||
// resolver stops requesting the old route configuration resource and requests
|
||||
// the new resource, and once successfully resolved, verifies that the update
|
||||
// from the resolver matches expected service config.
|
||||
func (s) TestServiceWatch_ListenerPointsToNewRouteConfiguration(t *testing.T) {
|
||||
// Spin up an xDS management server for the test.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
nodeID := uuid.New().String()
|
||||
mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)
|
||||
|
||||
// Configure resources on the management server.
|
||||
resources := e2e.DefaultClientResources(e2e.ResourceParams{
|
||||
DialTarget: defaultTestServiceName,
|
||||
NodeID: nodeID,
|
||||
Host: defaultTestHostname,
|
||||
Port: defaultTestPort[0],
|
||||
SecLevel: e2e.SecurityLevelNone,
|
||||
})
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
|
||||
|
||||
// Verify initial update from the resolver.
|
||||
waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
|
||||
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
|
||||
|
||||
// Update the listener resource to point to a new route configuration name.
|
||||
// The old route configuration resource is left unchanged to prevent a race:
|
||||
// if it were removed immediately, the resolver might encounter a
|
||||
// "resource-not-found" error for the old route before the listener update
|
||||
// successfully transitions the client to the new route.
|
||||
newTestRouteConfigName := defaultTestRouteConfigName + "-new"
|
||||
resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, newTestRouteConfigName)}
|
||||
resources.SkipValidation = true
|
||||
mgmtServer.Update(ctx, resources)
|
||||
|
||||
// Verify that the new route configuration resource is requested.
|
||||
waitForResourceNames(ctx, t, routeCfgCh, []string{newTestRouteConfigName})
|
||||
|
||||
// Update the old route configuration resource by adding a new route.
|
||||
resources.Routes[0].VirtualHosts[0].Routes = append(resources.Routes[0].VirtualHosts[0].Routes, &v3routepb.Route{
|
||||
Match: &v3routepb.RouteMatch{
|
||||
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/foo/bar"},
|
||||
CaseSensitive: &wrapperspb.BoolValue{Value: false},
|
||||
},
|
||||
Action: &v3routepb.Route_Route{
|
||||
Route: &v3routepb.RouteAction{
|
||||
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "some-random-cluster"},
|
||||
},
|
||||
},
|
||||
})
|
||||
mgmtServer.Update(ctx, resources)
|
||||
|
||||
// Wait for no update from the resolver since the listener resource no
|
||||
// longer points to the old route resource and new route resource hasn't
|
||||
// been sent yet.
|
||||
verifyNoUpdateFromResolver(ctx, t, stateCh)
|
||||
|
||||
// Update the management server with the new route configuration resource.
|
||||
resources.Routes = append(resources.Routes, e2e.DefaultRouteConfig(newTestRouteConfigName, defaultTestServiceName, resources.Clusters[0].Name))
|
||||
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes, nil, nil)
|
||||
|
||||
// Ensure update from the resolver.
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
|
||||
}
|
||||
|
||||
// Tests the case where the listener resource changes to contain an inline route
|
||||
// configuration and changes back to having a route configuration resource name.
|
||||
// Verifies that the expected xDS resource names are requested by the resolver
|
||||
// and that the update from the resolver matches expected service config.
|
||||
func (s) TestServiceWatch_ListenerPointsToInlineRouteConfiguration(t *testing.T) {
|
||||
// Spin up an xDS management server for the test.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
nodeID := uuid.New().String()
|
||||
mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)
|
||||
|
||||
// Configure resources on the management server.
|
||||
resources := e2e.DefaultClientResources(e2e.ResourceParams{
|
||||
DialTarget: defaultTestServiceName,
|
||||
NodeID: nodeID,
|
||||
Host: defaultTestHostname,
|
||||
Port: defaultTestPort[0],
|
||||
SecLevel: e2e.SecurityLevelNone,
|
||||
})
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
|
||||
|
||||
// Verify initial update from the resolver.
|
||||
waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
|
||||
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
|
||||
|
||||
// Update listener to contain an inline route configuration.
|
||||
hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: &v3routepb.RouteConfiguration{
|
||||
Name: resources.Routes[0].Name,
|
||||
VirtualHosts: []*v3routepb.VirtualHost{{
|
||||
Domains: []string{defaultTestServiceName},
|
||||
Routes: []*v3routepb.Route{{
|
||||
Match: &v3routepb.RouteMatch{
|
||||
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
|
||||
},
|
||||
Action: &v3routepb.Route_Route{
|
||||
Route: &v3routepb.RouteAction{
|
||||
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: resources.Clusters[0].Name},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
|
||||
})
|
||||
resources.Listeners = []*v3listenerpb.Listener{{
|
||||
Name: defaultTestServiceName,
|
||||
ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
|
||||
FilterChains: []*v3listenerpb.FilterChain{{
|
||||
Name: "filter-chain-name",
|
||||
Filters: []*v3listenerpb.Filter{{
|
||||
Name: wellknown.HTTPConnectionManager,
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, nil, nil, nil)
|
||||
|
||||
// Verify that the old route configuration is not requested anymore.
|
||||
waitForResourceNames(ctx, t, routeCfgCh, []string{})
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
|
||||
|
||||
// Update listener back to contain a route configuration name.
|
||||
resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, resources.Routes[0].Name)}
|
||||
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes, nil, nil)
|
||||
|
||||
// Verify that that route configuration resource is requested.
|
||||
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
|
||||
|
||||
// Verify that appropriate SC is pushed on the channel.
|
||||
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
|
||||
}
|
||||
1462
internal/xds/resolver/xds_http_filters_test.go
Normal file
1462
internal/xds/resolver/xds_http_filters_test.go
Normal file
File diff suppressed because it is too large
Load Diff
731
internal/xds/resolver/xds_resolver.go
Normal file
731
internal/xds/resolver/xds_resolver.go
Normal file
@@ -0,0 +1,731 @@
|
||||
/*
|
||||
* Copyright 2019 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 resolver implements the xds resolver, that does LDS and RDS to find
|
||||
// the cluster to use.
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
rand "math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
estats "google.golang.org/grpc/experimental/stats"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
iresolver "google.golang.org/grpc/internal/resolver"
|
||||
"google.golang.org/grpc/internal/wrr"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
rinternal "google.golang.org/grpc/internal/xds/resolver/internal"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/internal/xds/xdsdepmgr"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// Scheme is the xDS resolver's scheme.
|
||||
//
|
||||
// TODO(easwars): Rename this package as xdsresolver so that this is accessed as
|
||||
// xdsresolver.Scheme
|
||||
const Scheme = "xds"
|
||||
|
||||
// newBuilderWithConfigForTesting creates a new xds resolver builder using a
|
||||
// specific xds bootstrap config, so tests can use multiple xDS clients in
|
||||
// different ClientConns at the same time. The builder creates a new pool with
|
||||
// the provided config and a new xDS client in that pool.
|
||||
func newBuilderWithConfigForTesting(config []byte) (resolver.Builder, error) {
|
||||
return &xdsResolverBuilder{
|
||||
newXDSClient: func(name string, mr estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
|
||||
config, err := bootstrap.NewConfigFromContents(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
return pool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: name,
|
||||
MetricsRecorder: mr,
|
||||
})
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newBuilderWithPoolForTesting creates a new xds resolver builder using the
|
||||
// specific xds client pool, so that tests have complete control over the exact
|
||||
// specific xds client pool being used.
|
||||
func newBuilderWithPoolForTesting(pool *xdsclient.Pool) (resolver.Builder, error) {
|
||||
return &xdsResolverBuilder{
|
||||
newXDSClient: func(name string, mr estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
|
||||
return pool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: name,
|
||||
MetricsRecorder: mr,
|
||||
})
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newBuilderWithClientForTesting creates a new xds resolver builder using the
|
||||
// specific xDS client, so that tests have complete control over the exact
|
||||
// specific xDS client being used.
|
||||
func newBuilderWithClientForTesting(client xdsclient.XDSClient) (resolver.Builder, error) {
|
||||
return &xdsResolverBuilder{
|
||||
newXDSClient: func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
|
||||
// Returning an empty close func here means that the responsibility
|
||||
// of closing the client lies with the caller.
|
||||
return client, func() {}, nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
resolver.Register(&xdsResolverBuilder{})
|
||||
internal.NewXDSResolverWithConfigForTesting = newBuilderWithConfigForTesting
|
||||
internal.NewXDSResolverWithPoolForTesting = newBuilderWithPoolForTesting
|
||||
internal.NewXDSResolverWithClientForTesting = newBuilderWithClientForTesting
|
||||
|
||||
rinternal.NewWRR = wrr.NewRandom
|
||||
rinternal.NewXDSClient = xdsclient.DefaultPool.NewClient
|
||||
}
|
||||
|
||||
type xdsResolverBuilder struct {
|
||||
newXDSClient func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error)
|
||||
}
|
||||
|
||||
// Build helps implement the resolver.Builder interface.
|
||||
//
|
||||
// The xds bootstrap process is performed (and a new xDS client is built) every
|
||||
// time an xds resolver is built.
|
||||
func (b *xdsResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (_ resolver.Resolver, retErr error) {
|
||||
// Initialize the xDS client.
|
||||
newXDSClient := rinternal.NewXDSClient.(func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error))
|
||||
if b.newXDSClient != nil {
|
||||
newXDSClient = b.newXDSClient
|
||||
}
|
||||
client, xdsClientClose, err := newXDSClient(target.String(), opts.MetricsRecorder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xds: failed to create xds-client: %v", err)
|
||||
}
|
||||
|
||||
template, err := sanityChecksOnBootstrapConfig(target, client)
|
||||
if err != nil {
|
||||
xdsClientClose()
|
||||
return nil, err
|
||||
}
|
||||
ldsResourceName := bootstrap.PopulateResourceTemplate(template, target.Endpoint())
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
r := &xdsResolver{
|
||||
cc: cc,
|
||||
xdsClient: client,
|
||||
xdsClientClose: xdsClientClose,
|
||||
activeClusters: make(map[string]*clusterInfo),
|
||||
activePlugins: make(map[string]*clusterInfo),
|
||||
httpFilters: make(map[clientFilterKey]httpfilter.ClientFilter),
|
||||
channelID: rand.Uint64(),
|
||||
ldsResourceName: ldsResourceName,
|
||||
target: target.String(),
|
||||
metricsRecorder: opts.MetricsRecorder,
|
||||
|
||||
// serializer used to synchronize the following:
|
||||
// - updates from the dependency manager. This could lead to generation
|
||||
// of new service config if resolution is complete.
|
||||
// - completion of an RPC to a removed cluster causing the associated
|
||||
// ref count to become zero, resulting in generation of new service
|
||||
// config.
|
||||
// - stopping of a config selector that results in generation of new
|
||||
// service config.
|
||||
serializer: grpcsync.NewCallbackSerializer(ctx),
|
||||
serializerCancel: cancel,
|
||||
}
|
||||
r.logger = prefixLogger(r)
|
||||
r.logger.Infof("Creating resolver for target: %+v", target)
|
||||
|
||||
dmSet := make(chan struct{})
|
||||
// Schedule a callback that blocks until r.dm is set i.e xdsdepmgr.New()
|
||||
// returns. This acts as a gatekeeper: even if dependency manager sends the
|
||||
// updates before the xdsdepmgr.New() has a chance to return, they will be
|
||||
// queued behind this blocker and processed only after initialization is
|
||||
// complete.
|
||||
r.serializer.TrySchedule(func(ctx context.Context) {
|
||||
select {
|
||||
case <-dmSet:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
r.dm = xdsdepmgr.New(r.ldsResourceName, opts.Authority, r.xdsClient, r)
|
||||
close(dmSet)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Performs the following sanity checks:
|
||||
// - Verifies that the bootstrap configuration is not empty.
|
||||
// - Verifies that if xDS credentials are specified by the user, the
|
||||
// bootstrap configuration contains certificate providers.
|
||||
// - Verifies that if the provided dial target contains an authority, the
|
||||
// bootstrap configuration contains server config for that authority.
|
||||
//
|
||||
// Returns the listener resource name template to use. If any of the above
|
||||
// validations fail, a non-nil error is returned.
|
||||
func sanityChecksOnBootstrapConfig(target resolver.Target, client xdsclient.XDSClient) (string, error) {
|
||||
bootstrapConfig := client.BootstrapConfig()
|
||||
if bootstrapConfig == nil {
|
||||
// This is never expected to happen after a successful xDS client
|
||||
// creation. Defensive programming.
|
||||
return "", fmt.Errorf("xds: bootstrap configuration is empty")
|
||||
}
|
||||
|
||||
// Find the client listener template to use from the bootstrap config:
|
||||
// - If authority is not set in the target, use the top level template
|
||||
// - If authority is set, use the template from the authority map.
|
||||
template := bootstrapConfig.ClientDefaultListenerResourceNameTemplate()
|
||||
if authority := target.URL.Host; authority != "" {
|
||||
authorities := bootstrapConfig.Authorities()
|
||||
if authorities == nil {
|
||||
return "", fmt.Errorf("xds: authority %q specified in dial target %q is not found in the bootstrap file", authority, target)
|
||||
}
|
||||
a := authorities[authority]
|
||||
if a == nil {
|
||||
return "", fmt.Errorf("xds: authority %q specified in dial target %q is not found in the bootstrap file", authority, target)
|
||||
}
|
||||
if a.ClientListenerResourceNameTemplate != "" {
|
||||
// This check will never be false, because
|
||||
// ClientListenerResourceNameTemplate is required to start with
|
||||
// xdstp://, and has a default value (not an empty string) if unset.
|
||||
template = a.ClientListenerResourceNameTemplate
|
||||
}
|
||||
}
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// Name helps implement the resolver.Builder interface.
|
||||
func (*xdsResolverBuilder) Scheme() string {
|
||||
return Scheme
|
||||
}
|
||||
|
||||
// xdsResolver implements the resolver.Resolver interface.
|
||||
//
|
||||
// It manages the dependency manager which in turn manages all xDS resource
|
||||
// watches. It receives the xDS resource config and passes them to ClientConn.
|
||||
type xdsResolver struct {
|
||||
// The following fields are initialized at creation time and are read-only
|
||||
// after that.
|
||||
cc resolver.ClientConn
|
||||
logger *grpclog.PrefixLogger
|
||||
ldsResourceName string
|
||||
dm *xdsdepmgr.DependencyManager
|
||||
xdsClient xdsclient.XDSClient
|
||||
xdsClientClose func()
|
||||
channelID uint64 // Unique random ID for the channel owning this resolver.
|
||||
target string
|
||||
metricsRecorder estats.MetricsRecorder
|
||||
|
||||
// All methods on the xdsResolver type except for the ones invoked by gRPC,
|
||||
// i.e ResolveNow() and Close(), are guaranteed to execute in the context of
|
||||
// this serializer's callback. We use the serializer because these shared
|
||||
// states are accessed by each RPC when it is committed, and so
|
||||
// serializer is preffered over a mutex.
|
||||
serializer *grpcsync.CallbackSerializer
|
||||
serializerCancel context.CancelFunc
|
||||
|
||||
// The following fields are accessed only from within the serializer
|
||||
// callbacks.
|
||||
xdsConfig *xdsresource.XDSConfig
|
||||
// activeClusters is a map from cluster name to information about the
|
||||
// weighted cluster that includes a reference count and load balancing
|
||||
// configuration. These counts are used only by the resolver. The current
|
||||
// configSelector holds one reference, and each ongoing RPC holds an
|
||||
// additional reference. When the count hits zero, the resolver removes the
|
||||
// cluster from this map and calls unsubscribe. This signals the dependency
|
||||
// manager to stop the xDS watch once its own reference count reaches zero.
|
||||
activeClusters map[string]*clusterInfo
|
||||
// activePlugins is a map from cluster specifier plugin name to information
|
||||
// about the cluster specifier plugin that includes a ref count and load
|
||||
// balancing configuration. These counts are used only by the resolver. The
|
||||
// current configSelector holds one reference, and each ongoing RPC holds an
|
||||
// additional reference. When the count hits zero, the resolver removes the
|
||||
// plugin name from this map.
|
||||
activePlugins map[string]*clusterInfo
|
||||
curConfigSelector stoppableConfigSelector
|
||||
// httpFilters is a map from client filter key to client filter instance. It
|
||||
// lives here so that the resolver can reuse filter instances across config
|
||||
// updates when the same filter is specified, and to be able to clean up
|
||||
// filter instances that are no longer used.
|
||||
httpFilters map[clientFilterKey]httpfilter.ClientFilter
|
||||
}
|
||||
|
||||
// ResolveNow calls RequestDNSReresolution on the dependency manager.
|
||||
func (r *xdsResolver) ResolveNow(opts resolver.ResolveNowOptions) {
|
||||
if r.dm != nil {
|
||||
r.dm.RequestDNSReresolution(opts)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *xdsResolver) Close() {
|
||||
// Cancel the context passed to the serializer and wait for any scheduled
|
||||
// callbacks to complete. Canceling the context ensures that no new
|
||||
// callbacks will be scheduled.
|
||||
r.serializerCancel()
|
||||
<-r.serializer.Done()
|
||||
|
||||
if r.dm != nil {
|
||||
r.dm.Close()
|
||||
}
|
||||
if r.curConfigSelector != nil {
|
||||
r.curConfigSelector.stop()
|
||||
}
|
||||
for _, cf := range r.httpFilters {
|
||||
cf.Close()
|
||||
}
|
||||
// Release the xDS client only after the filters are torn down: filters
|
||||
// may hold side channels whose credentials are owned by the client's
|
||||
// bootstrap config and are released when the last reference to the
|
||||
// client is dropped.
|
||||
if r.xdsClientClose != nil {
|
||||
r.xdsClientClose()
|
||||
}
|
||||
// Drop field references so the resolver isn't retained across ClientConn
|
||||
// close via any cross-object retention chain (e.g. ClientConn holding the
|
||||
// ServiceConfig that captures the configSelector, shared xDS-client
|
||||
// watcher metadata, channelz trace entries). Each nil individually looks
|
||||
// harmless; the combination breaks the multi-path retention observed when
|
||||
// callers repeatedly Close and redial DirectPath channels.
|
||||
r.cc = nil
|
||||
r.curConfigSelector = nil
|
||||
r.dm = nil
|
||||
r.xdsClient = nil
|
||||
r.xdsClientClose = nil
|
||||
r.httpFilters = nil
|
||||
r.activeClusters = nil
|
||||
r.activePlugins = nil
|
||||
r.xdsConfig = nil
|
||||
r.logger.Infof("Shutdown")
|
||||
}
|
||||
|
||||
// Update is called when there is a new xDS config available from the dependency
|
||||
// manager and does the following:
|
||||
// - creates a new config selector (this involves incrementing references to
|
||||
// clusters owned by this config selector).
|
||||
// - stops the old config selector (this involves decrementing references to
|
||||
// clusters owned by this config selector).
|
||||
// - prunes active clusters and pushes a new service config to the channel.
|
||||
// - updates the current config selector used by the resolver.
|
||||
func (r *xdsResolver) Update(config *xdsresource.XDSConfig) {
|
||||
r.serializer.TrySchedule(func(context.Context) {
|
||||
r.xdsConfig = config
|
||||
cs, err := r.newConfigSelector()
|
||||
if err != nil {
|
||||
r.onResourceError(err)
|
||||
return
|
||||
}
|
||||
r.sendNewServiceConfig(cs)
|
||||
|
||||
if r.curConfigSelector != nil {
|
||||
r.curConfigSelector.stop()
|
||||
}
|
||||
r.curConfigSelector = cs
|
||||
})
|
||||
}
|
||||
|
||||
func (r *xdsResolver) Error(err error) {
|
||||
r.serializer.TrySchedule(func(context.Context) {
|
||||
r.onResourceError(err)
|
||||
})
|
||||
}
|
||||
|
||||
// sendNewServiceConfig prunes active clusters, generates a new service config
|
||||
// based on the current set of active clusters, and sends an update to the
|
||||
// channel with that service config and the provided config selector. It is safe
|
||||
// to ignore the error from `cc.UpdateState` because the clientconn uses the
|
||||
// given configSelector even if it returns an error.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) sendNewServiceConfig(cs stoppableConfigSelector) {
|
||||
// Delete entries from r.activeClusters with zero references;
|
||||
// otherwise serviceConfigJSON will generate a config including
|
||||
// them.
|
||||
r.pruneActiveClustersAndPlugins()
|
||||
|
||||
if errCS, ok := cs.(*erroringConfigSelector); ok {
|
||||
// Send an empty config, which picks pick-first, with no address, and
|
||||
// puts the ClientConn into transient failure.
|
||||
//
|
||||
// This call to UpdateState is expected to return ErrBadResolverState
|
||||
// since pick_first doesn't like an update with no addresses.
|
||||
r.cc.UpdateState(resolver.State{ServiceConfig: r.cc.ParseServiceConfig("{}")})
|
||||
|
||||
// Send a resolver error to pick_first so that RPCs will fail with a
|
||||
// more meaningful error, as opposed to one that says that pick_first
|
||||
// received no addresses.
|
||||
r.cc.ReportError(errCS.err)
|
||||
return
|
||||
}
|
||||
|
||||
sc := serviceConfigJSON(r.activeClusters, r.activePlugins)
|
||||
if r.logger.V(2) {
|
||||
r.logger.Infof("For Listener resource %q and RouteConfiguration resource %q, generated service config: %s", r.ldsResourceName, r.xdsConfig.Listener.APIListener.RouteConfigName, string(sc))
|
||||
}
|
||||
|
||||
// Send the update to the ClientConn.
|
||||
state := iresolver.SetConfigSelector(resolver.State{
|
||||
ServiceConfig: r.cc.ParseServiceConfig(string(sc)),
|
||||
}, cs)
|
||||
state = xdsresource.SetXDSConfig(state, r.xdsConfig)
|
||||
state = xdsdepmgr.SetXDSClusterSubscriber(state, r.dm)
|
||||
r.cc.UpdateState(xdsclient.SetClient(state, r.xdsClient))
|
||||
}
|
||||
|
||||
// newConfigSelector creates a new config selector using the most recently
|
||||
// received listener and route config updates. May add entries to
|
||||
// r.activeClusters for previously-unseen clusters.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) newConfigSelector() (_ *configSelector, err error) {
|
||||
cs := &configSelector{
|
||||
channelID: r.channelID,
|
||||
xdsNodeID: r.xdsClient.BootstrapConfig().Node().GetId(),
|
||||
sendNewServiceConfig: func() {
|
||||
r.serializer.TrySchedule(func(context.Context) {
|
||||
r.sendNewServiceConfig(r.curConfigSelector)
|
||||
})
|
||||
},
|
||||
virtualHost: virtualHost{
|
||||
retryConfig: r.xdsConfig.VirtualHost.RetryConfig,
|
||||
},
|
||||
routes: make([]route, len(r.xdsConfig.VirtualHost.Routes)),
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
plugins: make(map[string]*clusterInfo),
|
||||
httpFilterConfig: r.xdsConfig.Listener.APIListener.HTTPFilters,
|
||||
xdsConfig: r.xdsConfig,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Stop the config selector if an error occurs during construction
|
||||
// to ensure that interceptors that were created successfully before
|
||||
// the error are cleaned up.
|
||||
cs.stop()
|
||||
}
|
||||
}()
|
||||
|
||||
for i, rt := range r.xdsConfig.VirtualHost.Routes {
|
||||
clusters := rinternal.NewWRR.(func() wrr.WRR)()
|
||||
interceptors := []httpfilter.ClientInterceptor{}
|
||||
// TODO: Carve out the common logic between the ClusterSpecifierPlugin
|
||||
// and WeightedClusters.
|
||||
if rt.ClusterSpecifierPlugin != "" {
|
||||
clusterName := clusterSpecifierPluginPrefix + rt.ClusterSpecifierPlugin
|
||||
interceptor, err := r.newInterceptor(r.xdsConfig.Listener.APIListener.HTTPFilters, nil, rt.HTTPFilterConfigOverride, r.xdsConfig.VirtualHost.HTTPFilterConfigOverride)
|
||||
if err != nil {
|
||||
// Clean up any interceptors that were successfully built
|
||||
// for the current route before this error occurred. Note
|
||||
// that this is not handled by the call to cs.stop() in the
|
||||
// deferred function.
|
||||
for _, i := range interceptors {
|
||||
i.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
routeCluster := &routeCluster{
|
||||
name: clusterName,
|
||||
interceptor: interceptor,
|
||||
}
|
||||
rc := grpcsync.NewRefCounted(routeCluster, func() { interceptor.Close() })
|
||||
cs.routes[i].routeClusters = append(cs.routes[i].routeClusters, rc)
|
||||
clusters.Add(rc, 1)
|
||||
ci := r.addOrGetActiveClusterInfo(clusterName, "")
|
||||
ci.cfg = xdsChildConfig{ChildPolicy: balancerConfig(r.xdsConfig.RouteConfig.ClusterSpecifierPlugins[rt.ClusterSpecifierPlugin])}
|
||||
cs.plugins[clusterName] = ci
|
||||
} else {
|
||||
for _, wc := range rt.WeightedClusters {
|
||||
clusterName := clusterPrefix + wc.Name
|
||||
interceptor, err := r.newInterceptor(r.xdsConfig.Listener.APIListener.HTTPFilters, wc.HTTPFilterConfigOverride, rt.HTTPFilterConfigOverride, r.xdsConfig.VirtualHost.HTTPFilterConfigOverride)
|
||||
if err != nil {
|
||||
// Clean up any interceptors that were successfully built
|
||||
// for the current route before this error occurred. Note
|
||||
// that this is not handled by the call to cs.stop() in the
|
||||
// deferred function.
|
||||
for _, i := range interceptors {
|
||||
i.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
routeCluster := &routeCluster{
|
||||
name: clusterName,
|
||||
interceptor: interceptor,
|
||||
}
|
||||
rc := grpcsync.NewRefCounted(routeCluster, func() { interceptor.Close() })
|
||||
cs.routes[i].routeClusters = append(cs.routes[i].routeClusters, rc)
|
||||
clusters.Add(rc, int64(wc.Weight))
|
||||
ci := r.addOrGetActiveClusterInfo(clusterName, wc.Name)
|
||||
ci.cfg = xdsChildConfig{ChildPolicy: newBalancerConfig(cdsName, cdsBalancerConfig{Cluster: wc.Name})}
|
||||
cs.clusters[clusterName] = ci
|
||||
}
|
||||
}
|
||||
cs.routes[i].clusters = clusters
|
||||
cs.routes[i].m = xdsresource.RouteToMatcher(rt)
|
||||
cs.routes[i].actionType = rt.ActionType
|
||||
if rt.MaxStreamDuration == nil {
|
||||
cs.routes[i].maxStreamDuration = r.xdsConfig.Listener.APIListener.MaxStreamDuration
|
||||
} else {
|
||||
cs.routes[i].maxStreamDuration = *rt.MaxStreamDuration
|
||||
}
|
||||
|
||||
cs.routes[i].retryConfig = rt.RetryConfig
|
||||
cs.routes[i].hashPolicies = rt.HashPolicies
|
||||
cs.routes[i].autoHostRewrite = rt.AutoHostRewrite
|
||||
}
|
||||
|
||||
// Account for this config selector's clusters. Do this after no further
|
||||
// errors may occur. Note: cs.clusters are pointers to entries in
|
||||
// activeClusters.
|
||||
for _, ci := range cs.clusters {
|
||||
ci.refCount.Add(1)
|
||||
}
|
||||
for _, ci := range cs.plugins {
|
||||
ci.refCount.Add(1)
|
||||
}
|
||||
|
||||
// Cleanup filter instances that are no longer specified in the current
|
||||
// listener resource.
|
||||
filtersInNewConfig := make(map[clientFilterKey]bool)
|
||||
for _, filter := range r.xdsConfig.Listener.APIListener.HTTPFilters {
|
||||
filtersInNewConfig[newClientFilterKey(&filter)] = true
|
||||
}
|
||||
for key, cf := range r.httpFilters {
|
||||
if _, ok := filtersInNewConfig[key]; ok {
|
||||
continue
|
||||
}
|
||||
cf.Close()
|
||||
delete(r.httpFilters, key)
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// pruneActiveClustersAndPlugins removes entries from activeClusters and
|
||||
// activePlugins that have a reference count of zero. For clusters, it also
|
||||
// invokes the unsubscribe function to signal the dependency manager to stop the
|
||||
// xDS watch. Because cluster specifier plugins do not have their own watches,
|
||||
// they are simply removed from the map without an unsubscribe call.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) pruneActiveClustersAndPlugins() {
|
||||
for cluster, ci := range r.activeClusters {
|
||||
if ci.refCount.Load() == 0 {
|
||||
ci.unsubscribe()
|
||||
delete(r.activeClusters, cluster)
|
||||
}
|
||||
}
|
||||
for cluster, ci := range r.activePlugins {
|
||||
if ci.refCount.Load() == 0 {
|
||||
delete(r.activePlugins, cluster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addOrGetActiveClusterInfo returns the clusterInfo for the provided key,
|
||||
// creating it if it does not exist. It accepts the following parameters:
|
||||
// - key: Formatted as "cluster:<name>" or "cluster_specifier_plugin:<name>",
|
||||
// this is the lookup key for the activeClusters or activePlugins maps.
|
||||
// - name: The actual xDS resource name used to initiate a CDS watch.
|
||||
// If empty (e.g., for plugins), no resource watch is triggered.
|
||||
//
|
||||
// This function manages entry creation and xDS subscriptions but does not
|
||||
// increment the reference count of the returned clusterInfo.
|
||||
func (r *xdsResolver) addOrGetActiveClusterInfo(key string, name string) *clusterInfo {
|
||||
if name == "" {
|
||||
ci, ok := r.activePlugins[key]
|
||||
if !ok {
|
||||
ci = &clusterInfo{}
|
||||
r.activePlugins[key] = ci
|
||||
}
|
||||
return ci
|
||||
}
|
||||
ci, ok := r.activeClusters[key]
|
||||
if !ok {
|
||||
ci = &clusterInfo{unsubscribe: r.dm.SubscribeToCluster(name)}
|
||||
r.activeClusters[key] = ci
|
||||
}
|
||||
return ci
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
// refCount is the number of references to this cluster.
|
||||
refCount atomic.Int32
|
||||
// cfg is the child configuration for this cluster, containing either the
|
||||
// csp config or the cds cluster config.
|
||||
cfg xdsChildConfig
|
||||
// unsubscribe is the function to call to unsubscribe from this cluster's
|
||||
// CDS resource. It is populated only for clusters in activeClusters and not
|
||||
// for cluster specifier plugins. When invoked, it decrements the reference
|
||||
// count in the dependency manager; once that count reaches zero, the
|
||||
// underlying CDS watch is terminated. Plugins do not have associated
|
||||
// watches and therefore do not require an unsubscribe function.
|
||||
unsubscribe func()
|
||||
}
|
||||
|
||||
// Contains common functionality to be executed when resources of either type
|
||||
// are removed.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) onResourceError(err error) {
|
||||
// We cannot remove clusters from the service config that have ongoing RPCs.
|
||||
// Instead, what we can do is to send an erroring config selector
|
||||
// along with normal service config. This will ensure that new RPCs will
|
||||
// fail, and once the active RPCs complete, the reference counts on the
|
||||
// clusters will come down to zero. At that point, we will send an empty
|
||||
// service config with no addresses. This results in the pick-first
|
||||
// LB policy being configured on the channel, and since there are no
|
||||
// address, pick-first will put the channel in TRANSIENT_FAILURE.
|
||||
cs := newErroringConfigSelector(err, r.xdsClient.BootstrapConfig().Node().GetId())
|
||||
r.sendNewServiceConfig(cs)
|
||||
|
||||
// Stop and dereference the active config selector, if one exists.
|
||||
if r.curConfigSelector != nil {
|
||||
r.curConfigSelector.stop()
|
||||
}
|
||||
r.curConfigSelector = cs
|
||||
}
|
||||
|
||||
// newInterceptor builds a chain of client interceptors for the given filters
|
||||
// and override configuration. The cluster override has the highest priority,
|
||||
// followed by the route override, and finally the virtual host override.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) newInterceptor(filters []xdsresource.HTTPFilter, clusterOverride, routeOverride, virtualHostOverride map[string]httpfilter.FilterConfig) (_ httpfilter.ClientInterceptor, err error) {
|
||||
interceptors := make([]httpfilter.ClientInterceptor, 0, len(filters))
|
||||
defer func() {
|
||||
// Clean up any interceptors that were successfully built before the
|
||||
// error occurred, to avoid leaking resources.
|
||||
if err != nil {
|
||||
for _, i := range interceptors {
|
||||
i.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
for _, filter := range filters {
|
||||
override := clusterOverride[filter.Name]
|
||||
if override == nil {
|
||||
override = routeOverride[filter.Name]
|
||||
}
|
||||
if override == nil {
|
||||
override = virtualHostOverride[filter.Name]
|
||||
}
|
||||
|
||||
// Determine the effective disabled state of the filter. The base
|
||||
// configuration's disabled state is used unless an override is present.
|
||||
// If an override is present, the filter is disabled if the override is
|
||||
// a DisabledFilterConfig.
|
||||
disabled := filter.Disabled
|
||||
if override != nil {
|
||||
_, disabled = override.(httpfilter.DisabledFilterConfig)
|
||||
}
|
||||
|
||||
if disabled {
|
||||
if r.logger.V(2) {
|
||||
r.logger.Infof("Filter %q has been disabled.", filter.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
builder, ok := filter.Filter.(httpfilter.ClientFilterBuilder)
|
||||
if !ok {
|
||||
// Should not happen if it passed xdsClient validation.
|
||||
return nil, fmt.Errorf("filter %q does not support use in client", filter.Name)
|
||||
}
|
||||
|
||||
clientFilter := r.getOrCreateClientFilter(builder, newClientFilterKey(&filter))
|
||||
i, err := clientFilter.BuildClientInterceptor(filter.Config, override)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build client interceptor for filter %q: %v", filter.Name, err)
|
||||
}
|
||||
if i != nil {
|
||||
interceptors = append(interceptors, i)
|
||||
}
|
||||
}
|
||||
|
||||
return &interceptorList{interceptors: interceptors}, nil
|
||||
}
|
||||
|
||||
// interceptorList is a client interceptor that contains a list of client
|
||||
// interceptors to execute in order.
|
||||
type interceptorList struct {
|
||||
interceptors []httpfilter.ClientInterceptor
|
||||
}
|
||||
|
||||
func (il *interceptorList) NewStream(ctx context.Context, ri iresolver.RPCInfo, newStream func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error), opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
for idx := len(il.interceptors) - 1; idx >= 0; idx-- {
|
||||
ns := newStream
|
||||
i := il.interceptors[idx]
|
||||
newStream = func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return i.NewStream(ctx, ri, ns, opts...)
|
||||
}
|
||||
}
|
||||
return newStream(ctx, opts...)
|
||||
}
|
||||
|
||||
func (il *interceptorList) Close() {
|
||||
for _, i := range il.interceptors {
|
||||
i.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateClientFilter retrieves an existing client filter from the
|
||||
// httpFilters map or creates a new one if it doesn't exist. It uses the filter
|
||||
// builder to create a new client filter and stores it in the httpFilters map
|
||||
// for future use.
|
||||
//
|
||||
// Only executed in the context of a serializer callback.
|
||||
func (r *xdsResolver) getOrCreateClientFilter(builder httpfilter.ClientFilterBuilder, key clientFilterKey) httpfilter.ClientFilter {
|
||||
clientFilter, ok := r.httpFilters[key]
|
||||
if ok {
|
||||
return clientFilter
|
||||
}
|
||||
|
||||
cf := builder.BuildClientFilter(httpfilter.ClientFilterOptions{
|
||||
FilterName: key.name,
|
||||
MetricsRecorder: r.metricsRecorder,
|
||||
Target: r.target,
|
||||
})
|
||||
r.httpFilters[key] = cf
|
||||
return cf
|
||||
}
|
||||
|
||||
// newClientFilterKey generates a key for the given filter using the filter name
|
||||
// and type URLs. This is used for storing ClientFilters in a map.
|
||||
func newClientFilterKey(f *xdsresource.HTTPFilter) clientFilterKey {
|
||||
typeURLs := slices.Clone(f.Filter.TypeURLs())
|
||||
slices.Sort(typeURLs)
|
||||
return clientFilterKey{
|
||||
name: f.Name,
|
||||
typeURLs: strings.Join(typeURLs, ":"),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *clientFilterKey) String() string {
|
||||
return f.name + ":" + f.typeURLs
|
||||
}
|
||||
|
||||
type clientFilterKey struct {
|
||||
name string
|
||||
typeURLs string
|
||||
}
|
||||
1511
internal/xds/resolver/xds_resolver_test.go
Normal file
1511
internal/xds/resolver/xds_resolver_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user