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:
102
xds/bootstrap/bootstrap.go
Normal file
102
xds/bootstrap/bootstrap.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package bootstrap provides the functionality to register possible options
|
||||
// for aspects of the xDS client through the bootstrap file.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This package is EXPERIMENTAL and may be changed or removed
|
||||
// in a later release.
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// channelCredsRegistry is a map from channel credential type name to
|
||||
// ChannelCredential builder.
|
||||
var channelCredsRegistry = make(map[string]ChannelCredentials)
|
||||
|
||||
// callCredsRegistry is a map from call credential type name to
|
||||
// ChannelCredential builder.
|
||||
var callCredsRegistry = make(map[string]CallCredentials)
|
||||
|
||||
// ChannelCredentials interface encapsulates a credentials.Bundle builder
|
||||
// that can be used for communicating with the xDS Management server.
|
||||
type ChannelCredentials interface {
|
||||
// Build returns a credential bundle associated with this credential, and a
|
||||
// function to clean up any additional resources associated with this bundle
|
||||
// when it is no longer needed.
|
||||
Build(config json.RawMessage) (credentials.Bundle, func(), error)
|
||||
// Name returns the credential name associated with this credential.
|
||||
Name() string
|
||||
}
|
||||
|
||||
// RegisterChannelCredentials registers ChannelCredentials used for connecting
|
||||
// to the xDS management server.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple credentials are
|
||||
// registered with the same name, the one registered last will take effect.
|
||||
func RegisterChannelCredentials(c ChannelCredentials) {
|
||||
channelCredsRegistry[c.Name()] = c
|
||||
}
|
||||
|
||||
// GetChannelCredentials returns the credentials associated with a given name.
|
||||
// If no credentials are registered with the name, nil will be returned.
|
||||
func GetChannelCredentials(name string) ChannelCredentials {
|
||||
if c, ok := channelCredsRegistry[name]; ok {
|
||||
return c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallCredentials interface encapsulates a credentials.PerRPCCredentials
|
||||
// builder that can be used for communicating with the xDS Management server.
|
||||
type CallCredentials interface {
|
||||
// Build returns a PerRPCCredentials created from the provided
|
||||
// configuration, and a function to clean up any additional resources
|
||||
// associated with them when they are no longer needed.
|
||||
Build(config json.RawMessage) (credentials.PerRPCCredentials, func(), error)
|
||||
// Name returns the credential name associated with this credential.
|
||||
Name() string
|
||||
}
|
||||
|
||||
// RegisterCallCredentials registers CallCredentials used for connecting
|
||||
// to the xDS management server.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple credentials are
|
||||
// registered with the same name, the one registered last will take effect.
|
||||
func RegisterCallCredentials(c CallCredentials) {
|
||||
callCredsRegistry[c.Name()] = c
|
||||
}
|
||||
|
||||
// GetCallCredentials returns the credentials associated with a given name.
|
||||
// If no credentials are registered with the name, nil will be returned.
|
||||
func GetCallCredentials(name string) CallCredentials {
|
||||
if c, ok := callCredsRegistry[name]; ok {
|
||||
return c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
xds/bootstrap/bootstrap_test.go
Normal file
119
xds/bootstrap/bootstrap_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const testCredsBuilderName = "test_creds"
|
||||
|
||||
var builder = &testCredsBuilder{}
|
||||
|
||||
func init() {
|
||||
RegisterChannelCredentials(builder)
|
||||
}
|
||||
|
||||
type testCredsBuilder struct {
|
||||
config json.RawMessage
|
||||
}
|
||||
|
||||
func (t *testCredsBuilder) Build(config json.RawMessage) (credentials.Bundle, func(), error) {
|
||||
t.config = config
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (t *testCredsBuilder) Name() string {
|
||||
return testCredsBuilderName
|
||||
}
|
||||
|
||||
func TestRegisterNew(t *testing.T) {
|
||||
c := GetChannelCredentials(testCredsBuilderName)
|
||||
if c == nil {
|
||||
t.Fatalf("GetCredentials(%q) credential = nil", testCredsBuilderName)
|
||||
}
|
||||
|
||||
const sampleConfig = "sample_config"
|
||||
rawMessage := json.RawMessage(sampleConfig)
|
||||
if _, _, err := c.Build(rawMessage); err != nil {
|
||||
t.Errorf("Build(%v) error = %v, want nil", rawMessage, err)
|
||||
}
|
||||
|
||||
if got, want := string(builder.config), sampleConfig; got != want {
|
||||
t.Errorf("Build config = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelCredsBuilders(t *testing.T) {
|
||||
tests := []struct {
|
||||
typename string
|
||||
builder ChannelCredentials
|
||||
}{
|
||||
{"google_default", &googleDefaultCredsBuilder{}},
|
||||
{"insecure", &insecureCredsBuilder{}},
|
||||
{"tls", &tlsCredsBuilder{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.typename, func(t *testing.T) {
|
||||
if got, want := test.builder.Name(), test.typename; got != want {
|
||||
t.Errorf("%T.Name = %v, want %v", test.builder, got, want)
|
||||
}
|
||||
|
||||
bundle, stop, err := test.builder.Build(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%T.Build failed: %v", test.builder, err)
|
||||
}
|
||||
if bundle == nil {
|
||||
t.Errorf("%T.Build returned nil bundle, expected non-nil", test.builder)
|
||||
}
|
||||
stop()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTCallCredsBuilder(t *testing.T) {
|
||||
builder := &jwtCallCredsBuilder{}
|
||||
config := json.RawMessage(`{"jwt_token_file":"/path/to/token.jwt"}`)
|
||||
|
||||
creds, stop, err := builder.Build(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Build(%s) failed: %v", config, err)
|
||||
}
|
||||
defer stop()
|
||||
if creds == nil {
|
||||
t.Errorf("Build(%s) returned nil creds, expected non-nil", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTlsCredsBuilder(t *testing.T) {
|
||||
tls := &tlsCredsBuilder{}
|
||||
_, stop, err := tls.Build(json.RawMessage(`{}`))
|
||||
if err != nil {
|
||||
t.Fatalf("tls.Build() failed with error %s when expected to succeed", err)
|
||||
}
|
||||
defer stop()
|
||||
|
||||
if _, stop, err := tls.Build(json.RawMessage(`{"ca_certificate_file":"/ca_certificates.pem","refresh_interval": "asdf"}`)); err == nil {
|
||||
defer stop()
|
||||
t.Errorf("tls.Build() succeeded with an invalid refresh interval, when expected to fail")
|
||||
}
|
||||
}
|
||||
85
xds/bootstrap/credentials.go
Normal file
85
xds/bootstrap/credentials.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/google"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap/jwtcreds"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap/tlscreds"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterChannelCredentials(&insecureCredsBuilder{})
|
||||
RegisterChannelCredentials(&googleDefaultCredsBuilder{})
|
||||
RegisterChannelCredentials(&tlsCredsBuilder{})
|
||||
|
||||
RegisterCallCredentials(&jwtCallCredsBuilder{})
|
||||
}
|
||||
|
||||
// insecureCredsBuilder implements the `ChannelCredentials` interface defined in
|
||||
// package `xds/bootstrap` and encapsulates an insecure credential.
|
||||
type insecureCredsBuilder struct{}
|
||||
|
||||
func (i *insecureCredsBuilder) Build(json.RawMessage) (credentials.Bundle, func(), error) {
|
||||
return insecure.NewBundle(), func() {}, nil
|
||||
}
|
||||
|
||||
func (i *insecureCredsBuilder) Name() string {
|
||||
return "insecure"
|
||||
}
|
||||
|
||||
// tlsCredsBuilder implements the `ChannelCredentials` interface defined in
|
||||
// package `xds/bootstrap` and encapsulates a TLS credential.
|
||||
type tlsCredsBuilder struct{}
|
||||
|
||||
func (t *tlsCredsBuilder) Build(config json.RawMessage) (credentials.Bundle, func(), error) {
|
||||
return tlscreds.NewBundle(config)
|
||||
}
|
||||
|
||||
func (t *tlsCredsBuilder) Name() string {
|
||||
return "tls"
|
||||
}
|
||||
|
||||
// googleDefaultCredsBuilder implements the `ChannelCredentials` interface defined in
|
||||
// package `xds/bootstrap` and encapsulates a Google Default credential.
|
||||
type googleDefaultCredsBuilder struct{}
|
||||
|
||||
func (d *googleDefaultCredsBuilder) Build(json.RawMessage) (credentials.Bundle, func(), error) {
|
||||
return google.NewDefaultCredentials(), func() {}, nil
|
||||
}
|
||||
|
||||
func (d *googleDefaultCredsBuilder) Name() string {
|
||||
return "google_default"
|
||||
}
|
||||
|
||||
// jwtCallCredsBuilder implements the `CallCredentials` interface defined in
|
||||
// package `xds/bootstrap` and encapsulates JWT call credentials.
|
||||
type jwtCallCredsBuilder struct{}
|
||||
|
||||
func (j *jwtCallCredsBuilder) Build(configJSON json.RawMessage) (credentials.PerRPCCredentials, func(), error) {
|
||||
return jwtcreds.NewCallCredentials(configJSON)
|
||||
}
|
||||
|
||||
func (j *jwtCallCredsBuilder) Name() string {
|
||||
return "jwt_token_file"
|
||||
}
|
||||
108
xds/csds/csds.go
Normal file
108
xds/csds/csds.go
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
*
|
||||
* 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 csds implements features to dump the status (xDS responses) the
|
||||
// xds_client is using.
|
||||
//
|
||||
// Notice: This package is EXPERIMENTAL and may be changed or removed in a later
|
||||
// release.
|
||||
package csds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v3statusgrpc "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
|
||||
v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
const prefix = "[csds-server %p] "
|
||||
|
||||
func prefixLogger(s *ClientStatusDiscoveryServer) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, s))
|
||||
}
|
||||
|
||||
// ClientStatusDiscoveryServer provides an implementation of the Client Status
|
||||
// Discovery Service (CSDS) for exposing the xDS config of a given client. See
|
||||
// https://github.com/envoyproxy/envoy/blob/main/api/envoy/service/status/v3/csds.proto.
|
||||
//
|
||||
// For more details about the gRPC implementation of CSDS, refer to gRPC A40 at:
|
||||
// https://github.com/grpc/proposal/blob/master/A40-csds-support.md.
|
||||
type ClientStatusDiscoveryServer struct {
|
||||
logger *internalgrpclog.PrefixLogger
|
||||
}
|
||||
|
||||
// NewClientStatusDiscoveryServer returns an implementation of the CSDS server
|
||||
// that can be registered on a gRPC server.
|
||||
func NewClientStatusDiscoveryServer() (*ClientStatusDiscoveryServer, error) {
|
||||
s := &ClientStatusDiscoveryServer{}
|
||||
s.logger = prefixLogger(s)
|
||||
s.logger.Infof("Created CSDS server")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// StreamClientStatus implements interface ClientStatusDiscoveryServiceServer.
|
||||
func (s *ClientStatusDiscoveryServer) StreamClientStatus(stream v3statusgrpc.ClientStatusDiscoveryService_StreamClientStatusServer) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := s.buildClientStatusRespForReq(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FetchClientStatus implements interface ClientStatusDiscoveryServiceServer.
|
||||
func (s *ClientStatusDiscoveryServer) FetchClientStatus(_ context.Context, req *v3statuspb.ClientStatusRequest) (*v3statuspb.ClientStatusResponse, error) {
|
||||
return s.buildClientStatusRespForReq(req)
|
||||
}
|
||||
|
||||
// buildClientStatusRespForReq fetches the status of xDS resources from the
|
||||
// xdsclient, and returns the response to be sent back to the csds client.
|
||||
//
|
||||
// If it returns an error, the error is a status error.
|
||||
func (s *ClientStatusDiscoveryServer) buildClientStatusRespForReq(req *v3statuspb.ClientStatusRequest) (*v3statuspb.ClientStatusResponse, error) {
|
||||
// Field NodeMatchers is unsupported, by design
|
||||
// https://github.com/grpc/proposal/blob/master/A40-csds-support.md#detail-node-matching.
|
||||
if len(req.NodeMatchers) != 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "node_matchers are not supported, request contains node_matchers: %v", req.NodeMatchers)
|
||||
}
|
||||
|
||||
return xdsclient.DumpResources(), nil
|
||||
}
|
||||
|
||||
// Close cleans up the resources.
|
||||
func (s *ClientStatusDiscoveryServer) Close() {}
|
||||
699
xds/csds/csds_e2e_test.go
Normal file
699
xds/csds/csds_e2e_test.go
Normal file
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
*
|
||||
* 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 csds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/xds/csds"
|
||||
"google.golang.org/protobuf/encoding/prototext"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
v3adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
|
||||
v3statuspbgrpc "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
|
||||
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 5 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
// The following watcher implementations are no-ops since we don't really care
|
||||
// about the callback received by these watchers in the test. We only care
|
||||
// whether CSDS reports the expected state.
|
||||
|
||||
type nopListenerWatcher struct{}
|
||||
|
||||
func (nopListenerWatcher) ResourceChanged(_ *xdsresource.ListenerUpdate, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopListenerWatcher) ResourceError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopListenerWatcher) AmbientError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
|
||||
type nopRouteConfigWatcher struct{}
|
||||
|
||||
func (nopRouteConfigWatcher) ResourceChanged(_ *xdsresource.RouteConfigUpdate, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopRouteConfigWatcher) ResourceError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopRouteConfigWatcher) AmbientError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
|
||||
type nopClusterWatcher struct{}
|
||||
|
||||
func (nopClusterWatcher) ResourceChanged(_ *xdsresource.ClusterUpdate, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopClusterWatcher) ResourceError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopClusterWatcher) AmbientError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
|
||||
type nopEndpointsWatcher struct{}
|
||||
|
||||
func (nopEndpointsWatcher) ResourceChanged(_ *xdsresource.EndpointsUpdate, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopEndpointsWatcher) ResourceError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
func (nopEndpointsWatcher) AmbientError(_ error, onDone func()) {
|
||||
onDone()
|
||||
}
|
||||
|
||||
// This watcher writes the onDone callback on to a channel for the test to
|
||||
// invoke it when it wants to unblock the next read on the ADS stream in the xDS
|
||||
// client. This is particularly useful when a resource is NACKed, because the
|
||||
// go-control-plane management server continuously resends the same resource in
|
||||
// this case, and applying flow control from these watchers ensures that xDS
|
||||
// client does not spend all of its time receiving and NACKing updates from the
|
||||
// management server. This was indeed the case on arm64 (before we had support
|
||||
// for ADS stream level flow control), and was causing CSDS to not receive any
|
||||
// updates from the xDS client.
|
||||
type blockingListenerWatcher struct {
|
||||
testCtxDone <-chan struct{} // Closed when the test is done.
|
||||
onDoneCh chan func() // Channel to write the onDone callback to.
|
||||
}
|
||||
|
||||
func newBlockingListenerWatcher(testCtxDone <-chan struct{}) *blockingListenerWatcher {
|
||||
return &blockingListenerWatcher{
|
||||
testCtxDone: testCtxDone,
|
||||
onDoneCh: make(chan func(), 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *blockingListenerWatcher) ResourceChanged(_ *xdsresource.ListenerUpdate, onDone func()) {
|
||||
writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
|
||||
}
|
||||
func (w *blockingListenerWatcher) ResourceError(_ error, onDone func()) {
|
||||
writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
|
||||
}
|
||||
func (w *blockingListenerWatcher) AmbientError(_ error, onDone func()) {
|
||||
writeOnDone(w.testCtxDone, w.onDoneCh, onDone)
|
||||
}
|
||||
|
||||
// writeOnDone attempts to write the onDone callback on the onDone channel. It
|
||||
// returns when it can successfully write to the channel or when the test is
|
||||
// done, which is signalled by testCtxDone being closed.
|
||||
func writeOnDone(testCtxDone <-chan struct{}, onDoneCh chan func(), onDone func()) {
|
||||
select {
|
||||
case <-testCtxDone:
|
||||
case onDoneCh <- onDone:
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a gRPC server and starts serving a CSDS service implementation on it.
|
||||
// Returns the address of the newly created gRPC server.
|
||||
//
|
||||
// Registers cleanup functions on t to stop the gRPC server and the CSDS
|
||||
// implementation.
|
||||
func startCSDSServer(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
server := grpc.NewServer()
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
csdss, err := csds.NewClientStatusDiscoveryServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create CSDS service implementation: %v", err)
|
||||
}
|
||||
v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss)
|
||||
t.Cleanup(csdss.Close)
|
||||
|
||||
// Create a local listener and pass it to Serve().
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
go func() {
|
||||
if err := server.Serve(lis); err != nil {
|
||||
t.Errorf("Serve() failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return lis.Addr().String()
|
||||
}
|
||||
|
||||
func startCSDSClientStream(ctx context.Context, t *testing.T, serverAddr string) v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient {
|
||||
conn, err := grpc.NewClient(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial CSDS server %q: %v", serverAddr, err)
|
||||
}
|
||||
|
||||
client := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn)
|
||||
stream, err := client.StreamClientStatus(ctx, grpc.WaitForReady(true))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a stream for CSDS: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return stream
|
||||
}
|
||||
|
||||
// Tests CSDS functionality. The test performs the following:
|
||||
// - Spins up a management server and creates two xDS clients talking to it.
|
||||
// - Registers a set of watches on the xDS clients, and verifies that the CSDS
|
||||
// response reports resources in REQUESTED state.
|
||||
// - Configures resources on the management server corresponding to the ones
|
||||
// being watched by the clients, and verifies that the CSDS response reports
|
||||
// resources in ACKED state.
|
||||
//
|
||||
// For the above operations, the test also verifies that the client_scope field
|
||||
// in the CSDS response is populated appropriately.
|
||||
func (s) TestCSDS(t *testing.T) {
|
||||
// Spin up a xDS management server on a local port.
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create a bootstrap contents pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
// We use the default xDS client pool here because the CSDS service reports
|
||||
// on the state of the default xDS client which is implicitly managed
|
||||
// within the xdsclient.DefaultPool.
|
||||
xdsclient.DefaultPool.SetFallbackBootstrapConfig(config)
|
||||
defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }()
|
||||
// Create two xDS clients, with different names. These should end up
|
||||
// creating two different xDS clients.
|
||||
const xdsClient1Name = "xds-csds-client-1"
|
||||
xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsClient1Name,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer xdsClose1()
|
||||
const xdsClient2Name = "xds-csds-client-2"
|
||||
xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsClient2Name,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer xdsClose2()
|
||||
|
||||
// Start a CSDS server and create a client stream to it.
|
||||
addr := startCSDSServer(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream := startCSDSClientStream(ctx, t, addr)
|
||||
|
||||
// Verify that the xDS client reports an empty config.
|
||||
wantNode := &v3corepb.Node{
|
||||
Id: nodeID,
|
||||
UserAgentName: "gRPC Go",
|
||||
UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
|
||||
ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
|
||||
}
|
||||
wantResp := &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize the xDS resources to be used in this test.
|
||||
ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"}
|
||||
rdsTargets := []string{"route-config-0", "route-config-1"}
|
||||
cdsTargets := []string{"cluster-0", "cluster-1"}
|
||||
edsTargets := []string{"endpoints-0", "endpoints-1"}
|
||||
listeners := make([]*v3listenerpb.Listener, len(ldsTargets))
|
||||
listenerAnys := make([]*anypb.Any, len(ldsTargets))
|
||||
for i := range ldsTargets {
|
||||
listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i])
|
||||
listenerAnys[i] = testutils.MarshalAny(t, listeners[i])
|
||||
}
|
||||
routes := make([]*v3routepb.RouteConfiguration, len(rdsTargets))
|
||||
routeAnys := make([]*anypb.Any, len(rdsTargets))
|
||||
for i := range rdsTargets {
|
||||
routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i])
|
||||
routeAnys[i] = testutils.MarshalAny(t, routes[i])
|
||||
}
|
||||
clusters := make([]*v3clusterpb.Cluster, len(cdsTargets))
|
||||
clusterAnys := make([]*anypb.Any, len(cdsTargets))
|
||||
for i := range cdsTargets {
|
||||
clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone)
|
||||
clusterAnys[i] = testutils.MarshalAny(t, clusters[i])
|
||||
}
|
||||
endpoints := make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets))
|
||||
endpointAnys := make([]*anypb.Any, len(edsTargets))
|
||||
ips := []string{"0.0.0.0", "1.1.1.1"}
|
||||
ports := []uint32{123, 456}
|
||||
for i := range edsTargets {
|
||||
endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1])
|
||||
endpointAnys[i] = testutils.MarshalAny(t, endpoints[i])
|
||||
}
|
||||
|
||||
// Register watches on the xDS clients for two resources of each type.
|
||||
for _, xdsC := range []xdsclient.XDSClient{xdsClient1, xdsClient2} {
|
||||
for _, target := range ldsTargets {
|
||||
xdsresource.WatchListener(xdsC, target, nopListenerWatcher{})
|
||||
}
|
||||
for _, target := range rdsTargets {
|
||||
xdsresource.WatchRouteConfig(xdsC, target, nopRouteConfigWatcher{})
|
||||
}
|
||||
for _, target := range cdsTargets {
|
||||
xdsresource.WatchCluster(xdsC, target, nopClusterWatcher{})
|
||||
}
|
||||
for _, target := range edsTargets {
|
||||
xdsresource.WatchEndpoints(xdsC, target, nopEndpointsWatcher{})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the xDS client reports the resources as being in "Requested"
|
||||
// state, and in version "0".
|
||||
wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
}
|
||||
wantResp = &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: wantConfigs,
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: wantConfigs,
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Configure the management server with two resources of each type,
|
||||
// corresponding to the watches registered above.
|
||||
if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: listeners,
|
||||
Routes: routes,
|
||||
Clusters: clusters,
|
||||
Endpoints: endpoints,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that the xDS client reports the resources as being in "ACKed"
|
||||
// state, and in version "1".
|
||||
wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[0], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[0], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[0], nil),
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil),
|
||||
}
|
||||
wantResp = &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: wantConfigs,
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: wantConfigs,
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests CSDS functionality. The test performs the following:
|
||||
// - Spins up a management server and creates two xDS clients talking to it.
|
||||
// - Registers one watch on each xDS client, and verifies that the CSDS
|
||||
// response reports resources in REQUESTED state.
|
||||
// - Configures two resources on the management server and verifies that the
|
||||
// CSDS response reports the resources as being in ACKED state.
|
||||
// - Updates one of two resources on the management server such that it is
|
||||
// expected to be NACKed by the client. Verifies that the CSDS response
|
||||
// contains one resource in ACKED state and one in NACKED state.
|
||||
//
|
||||
// For the above operations, the test also verifies that the client_scope field
|
||||
// in the CSDS response is populated appropriately.
|
||||
//
|
||||
// This test does a bunch of similar things to the previous test, but has
|
||||
// reduced complexity because of having to deal with a single resource type.
|
||||
// This makes it possible to test the NACKing a resource (which results in
|
||||
// continuous resending of the resource by the go-control-plane management
|
||||
// server), in an easier and less flaky way.
|
||||
func (s) TestCSDS_NACK(t *testing.T) {
|
||||
// Spin up a xDS management server on a local port.
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create a bootstrap contents pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
// We use the default xDS client pool here because the CSDS service reports
|
||||
// on the state of the default xDS client which is implicitly managed
|
||||
// within the xdsclient.DefaultPool.
|
||||
xdsclient.DefaultPool.SetFallbackBootstrapConfig(config)
|
||||
defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }()
|
||||
// Create two xDS clients, with different names. These should end up
|
||||
// creating two different xDS clients.
|
||||
const xdsClient1Name = "xds-csds-client-1"
|
||||
xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsClient1Name,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer xdsClose1()
|
||||
const xdsClient2Name = "xds-csds-client-2"
|
||||
xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsClient2Name,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer xdsClose2()
|
||||
|
||||
// Start a CSDS server and create a client stream to it.
|
||||
addr := startCSDSServer(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream := startCSDSClientStream(ctx, t, addr)
|
||||
|
||||
// Verify that the xDS client reports an empty config.
|
||||
wantNode := &v3corepb.Node{
|
||||
Id: nodeID,
|
||||
UserAgentName: "gRPC Go",
|
||||
UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
|
||||
ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
|
||||
}
|
||||
wantResp := &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize the xDS resources to be used in this test.
|
||||
const ldsTarget0, ldsTarget1 = "lds.target.good:0000", "lds.target.good:1111"
|
||||
listener0 := e2e.DefaultClientListener(ldsTarget0, "rds-name")
|
||||
listener1 := e2e.DefaultClientListener(ldsTarget1, "rds-name")
|
||||
listenerAny0 := testutils.MarshalAny(t, listener0)
|
||||
listenerAny1 := testutils.MarshalAny(t, listener1)
|
||||
|
||||
// Register the watchers, one for each xDS client.
|
||||
watcher1 := nopListenerWatcher{}
|
||||
watcher2 := newBlockingListenerWatcher(ctx.Done())
|
||||
xdsresource.WatchListener(xdsClient1, ldsTarget0, watcher1)
|
||||
xdsresource.WatchListener(xdsClient2, ldsTarget1, watcher2)
|
||||
|
||||
// Verify that the xDS client reports the resources as being in "Requested"
|
||||
// state, and in version "0".
|
||||
wantResp = &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
},
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil),
|
||||
},
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Configure the management server with two listener resources corresponding
|
||||
// to the watches registered above.
|
||||
if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener0, listener1},
|
||||
SkipValidation: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that the xDS client reports the resources as being in "ACKed"
|
||||
// state, and in version "1".
|
||||
wantResp = &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil),
|
||||
},
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny1, nil),
|
||||
},
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unblock reads on the ADS stream by calling the onDone callback sent to
|
||||
// the watcher.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timed out waiting for watch callback")
|
||||
case onDone := <-watcher2.onDoneCh:
|
||||
onDone()
|
||||
}
|
||||
|
||||
// Update the second resource with an empty ApiListener field which is
|
||||
// expected to be NACK'ed by the xDS client.
|
||||
listener1.ApiListener = nil
|
||||
if err := mgmtServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener0, listener1},
|
||||
SkipValidation: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the update to reach the watchers.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timed out waiting for watch callback")
|
||||
case onDone := <-watcher2.onDoneCh:
|
||||
onDone()
|
||||
}
|
||||
|
||||
// Verify that the xDS client reports the first listener resource as being
|
||||
// ACKed and the second listener resource as being NACKed. The version for
|
||||
// the ACKed resource would be "2", while that for the NACKed resource would
|
||||
// be "1". In the NACKed resource, the version which is NACKed is stored in
|
||||
// the ErrorState field.
|
||||
wantResp = &v3statuspb.ClientStatusResponse{
|
||||
Config: []*v3statuspb.ClientConfig{
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "2", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil),
|
||||
},
|
||||
ClientScope: xdsClient1Name,
|
||||
},
|
||||
{
|
||||
Node: wantNode,
|
||||
GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_NACKED, listenerAny1, &v3adminpb.UpdateFailureState{VersionInfo: "2"}),
|
||||
},
|
||||
ClientScope: xdsClient2Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig {
|
||||
return &v3statuspb.ClientConfig_GenericXdsConfig{
|
||||
TypeUrl: typeURL,
|
||||
Name: name,
|
||||
VersionInfo: version,
|
||||
ClientStatus: status,
|
||||
XdsConfig: config,
|
||||
ErrorState: failure,
|
||||
}
|
||||
}
|
||||
|
||||
// Repeatedly sends CSDS requests and receives CSDS responses on the provided
|
||||
// stream and verifies that the response matches `want`. Returns an error if
|
||||
// sending or receiving on the stream fails, or if the context expires before a
|
||||
// response matching `want` is received.
|
||||
//
|
||||
// Expects client configs in `want` to be sorted on `client_scope` and the
|
||||
// resource dump to be sorted on type_url and resource name.
|
||||
func checkClientStatusResponse(ctx context.Context, stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient, want *v3statuspb.ClientStatusResponse) error {
|
||||
var cmpOpts = cmp.Options{
|
||||
protocmp.Transform(),
|
||||
protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"),
|
||||
protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"),
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
|
||||
if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
|
||||
if err != io.EOF {
|
||||
return fmt.Errorf("failed to send ClientStatusRequest: %v", err)
|
||||
}
|
||||
// If the stream has closed, we call Recv() until it returns a non-nil
|
||||
// error to get the actual error on the stream.
|
||||
for {
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
return fmt.Errorf("failed to recv ClientStatusResponse: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
got, err := stream.Recv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to recv ClientStatusResponse: %v", err)
|
||||
}
|
||||
// Sort the client configs based on the `client_scope` field.
|
||||
slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int {
|
||||
return strings.Compare(a.ClientScope, b.ClientScope)
|
||||
})
|
||||
// Sort the resource configs based on the type_url and name fields.
|
||||
for _, cc := range got.GetConfig() {
|
||||
slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int {
|
||||
if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
return strings.Compare(a.TypeUrl, b.TypeUrl)
|
||||
})
|
||||
}
|
||||
diff := cmp.Diff(want, got, cmpOpts)
|
||||
if diff == "" {
|
||||
return nil
|
||||
}
|
||||
lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want))
|
||||
}
|
||||
return fmt.Errorf("timeout when waiting for resource dump to reach expected state: ctxErr: %v, otherErr: %v", ctx.Err(), lastErr)
|
||||
}
|
||||
|
||||
func (s) TestCSDSNoXDSClient(t *testing.T) {
|
||||
// Create a bootstrap file in a temporary directory. Since we pass an empty
|
||||
// bootstrap configuration, it will fail xDS client creation because the
|
||||
// `server_uri` field is unset.
|
||||
testutils.CreateBootstrapFileForTesting(t, []byte(``))
|
||||
|
||||
// Start a CSDS server and create a client stream to it.
|
||||
addr := startCSDSServer(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream := startCSDSClientStream(ctx, t, addr)
|
||||
|
||||
if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil {
|
||||
t.Fatalf("Failed to send ClientStatusRequest: %v", err)
|
||||
}
|
||||
r, err := stream.Recv()
|
||||
if err != nil {
|
||||
// io.EOF is not ok.
|
||||
t.Fatalf("Failed to recv ClientStatusResponse: %v", err)
|
||||
}
|
||||
if n := len(r.Config); n != 0 {
|
||||
t.Fatalf("got %d configs, want 0: %v", n, prototext.Format(r))
|
||||
}
|
||||
}
|
||||
250
xds/googledirectpath/googlec2p.go
Normal file
250
xds/googledirectpath/googlec2p.go
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
*
|
||||
* 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 googledirectpath implements a resolver that configures xds to make
|
||||
// cloud to prod directpath connection.
|
||||
//
|
||||
// It's a combo of DNS and xDS resolvers. It delegates to DNS if
|
||||
// - not on GCE, or
|
||||
// - xDS bootstrap env var is set (so this client needs to do normal xDS, not
|
||||
// direct path, and clients with this scheme is not part of the xDS mesh).
|
||||
package googledirectpath
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
rand "math/rand/v2"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/googlecloud"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/resolver"
|
||||
|
||||
_ "google.golang.org/grpc/xds" // To register xds resolvers and balancers.
|
||||
)
|
||||
|
||||
const (
|
||||
c2pScheme = "google-c2p"
|
||||
c2pAuthority = "traffic-director-c2p.xds.googleapis.com"
|
||||
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
zoneURL = "http://metadata.google.internal/computeMetadata/v1/instance/zone"
|
||||
ipv6URL = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"
|
||||
ipv6CapableMetadataName = "TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE"
|
||||
httpReqTimeout = 10 * time.Second
|
||||
|
||||
logPrefix = "[google-c2p-resolver]"
|
||||
dnsName, xdsName = "dns", "xds"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = internalgrpclog.NewPrefixLogger(grpclog.Component("directpath"), logPrefix)
|
||||
universeDomainMu sync.Mutex
|
||||
universeDomain = ""
|
||||
// For overriding in unittests.
|
||||
onGCE = googlecloud.OnGCE
|
||||
randInt = rand.Int
|
||||
xdsClientPool = xdsclient.DefaultPool
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver.Register(c2pResolverBuilder{})
|
||||
}
|
||||
|
||||
// SetUniverseDomain informs the gRPC library of the universe domain
|
||||
// in which the process is running (for example, "googleapis.com").
|
||||
// It is the caller's responsibility to ensure that the domain is correct.
|
||||
//
|
||||
// This setting is used by the "google-c2p" resolver (the resolver used
|
||||
// for URIs with the "google-c2p" scheme) to configure its dependencies.
|
||||
//
|
||||
// If a gRPC channel is created with the "google-c2p" URI scheme and this
|
||||
// function has NOT been called, then gRPC configures the universe domain as
|
||||
// "googleapis.com".
|
||||
//
|
||||
// Returns nil if either:
|
||||
//
|
||||
// a) The universe domain has not yet been configured.
|
||||
// b) The universe domain has been configured and matches the provided value.
|
||||
//
|
||||
// Otherwise, returns an error.
|
||||
func SetUniverseDomain(domain string) error {
|
||||
universeDomainMu.Lock()
|
||||
defer universeDomainMu.Unlock()
|
||||
if domain == "" {
|
||||
return fmt.Errorf("universe domain cannot be empty")
|
||||
}
|
||||
if universeDomain == "" {
|
||||
universeDomain = domain
|
||||
return nil
|
||||
}
|
||||
if universeDomain != domain {
|
||||
return fmt.Errorf("universe domain cannot be set to %s, already set to different value: %s", domain, universeDomain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getXdsServerURI() string {
|
||||
universeDomainMu.Lock()
|
||||
defer universeDomainMu.Unlock()
|
||||
if universeDomain == "" {
|
||||
universeDomain = defaultUniverseDomain
|
||||
}
|
||||
// Put env var override logic after default value logic so
|
||||
// that tests still run the default value logic.
|
||||
if envconfig.C2PResolverTestOnlyTrafficDirectorURI != "" {
|
||||
return envconfig.C2PResolverTestOnlyTrafficDirectorURI
|
||||
}
|
||||
return fmt.Sprintf("dns:///directpath-pa.%s", universeDomain)
|
||||
}
|
||||
|
||||
type c2pResolverWrapper struct {
|
||||
resolver.Resolver
|
||||
cancel func() // Release the reference to the xDS client that was created in Build().
|
||||
}
|
||||
|
||||
func (r *c2pResolverWrapper) Close() {
|
||||
r.Resolver.Close()
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
type c2pResolverBuilder struct{}
|
||||
|
||||
func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
if t.URL.Host != "" {
|
||||
return nil, fmt.Errorf("google-c2p URI scheme does not support authorities")
|
||||
}
|
||||
|
||||
isGCE := onGCE()
|
||||
// We check only for the presence of the "force-xds" query parameter key,
|
||||
// and do not evaluate its value.
|
||||
_, forceXDS := t.URL.Query()["force-xds"]
|
||||
if !forceXDS && !isGCE {
|
||||
// If not xDS, fallback to DNS.
|
||||
t.URL.Scheme = dnsName
|
||||
return resolver.Get(dnsName).Build(t, cc, opts)
|
||||
}
|
||||
|
||||
var zone string
|
||||
var ipv6Capable bool
|
||||
if forceXDS {
|
||||
// If the force-xds query parameter is present, GCE metadata server
|
||||
// queries are bypassed.
|
||||
zone = ""
|
||||
ipv6Capable = true
|
||||
} else {
|
||||
// Note that the following calls to getZone() and getIPv6Capable() does I/O,
|
||||
// and has 10 seconds timeout each.
|
||||
//
|
||||
// This should be fine in most of the cases. In certain error cases, this
|
||||
// could block Dial() for up to 10 seconds (each blocking call has its own
|
||||
// goroutine).
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() { zone = getZone(httpReqTimeout) })
|
||||
wg.Go(func() { ipv6Capable = getIPv6Capable(httpReqTimeout) })
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
xdsServerURI := getXdsServerURI()
|
||||
nodeCfg := newNodeConfig(zone, ipv6Capable, forceXDS)
|
||||
xdsServerCfg := newXdsServerConfig(xdsServerURI)
|
||||
authoritiesCfg := newAuthoritiesConfig(xdsServerCfg)
|
||||
|
||||
cfg := map[string]any{
|
||||
"xds_servers": []any{xdsServerCfg},
|
||||
"client_default_listener_resource_name_template": "%s",
|
||||
"authorities": authoritiesCfg,
|
||||
"node": nodeCfg,
|
||||
}
|
||||
cfgJSON, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal bootstrap configuration: %v", err)
|
||||
}
|
||||
config, err := bootstrap.NewConfigFromContents(cfgJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse bootstrap contents: %s, %v", string(cfgJSON), err)
|
||||
}
|
||||
|
||||
t = resolver.Target{
|
||||
URL: url.URL{
|
||||
Scheme: xdsName,
|
||||
Host: c2pAuthority,
|
||||
Path: t.URL.Path,
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new xDS client for this target using the provided bootstrap
|
||||
// configuration. This client is stored in the xdsclient pool’s internal
|
||||
// cache, keeping it alive and associated with this resolver until Closed().
|
||||
// While the c2p resolver itself does not directly use the client, creating
|
||||
// it ensures that when the xDS resolver later requests a client for the
|
||||
// same target, the existing instance will be reused.
|
||||
_, cancel, err := xdsClientPool.NewClientWithConfig(t.String(), opts.MetricsRecorder, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create xds client: %v", err)
|
||||
}
|
||||
|
||||
r, err := resolver.Get(xdsName).Build(t, cc, opts)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
return &c2pResolverWrapper{Resolver: r, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
func (b c2pResolverBuilder) Scheme() string {
|
||||
return c2pScheme
|
||||
}
|
||||
|
||||
func newNodeConfig(zone string, ipv6Capable bool, forceXDS bool) map[string]any {
|
||||
prefix := "C2P"
|
||||
if forceXDS {
|
||||
prefix = "C2P-non-gcp"
|
||||
}
|
||||
node := map[string]any{
|
||||
"id": fmt.Sprintf("%s-%d", prefix, randInt()),
|
||||
}
|
||||
if zone != "" {
|
||||
node["locality"] = map[string]any{"zone": zone}
|
||||
}
|
||||
// Enable dualstack endpoints in TD.
|
||||
if ipv6Capable {
|
||||
node["metadata"] = map[string]any{ipv6CapableMetadataName: true}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func newAuthoritiesConfig(serverCfg map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
c2pAuthority: map[string]any{"xds_servers": []any{serverCfg}},
|
||||
}
|
||||
}
|
||||
|
||||
func newXdsServerConfig(uri string) map[string]any {
|
||||
return map[string]any{
|
||||
"server_uri": uri,
|
||||
"channel_creds": []map[string]any{{"type": "google_default"}},
|
||||
"server_features": []any{"ignore_resource_deletion"},
|
||||
}
|
||||
}
|
||||
768
xds/googledirectpath/googlec2p_test.go
Normal file
768
xds/googledirectpath/googlec2p_test.go
Normal file
@@ -0,0 +1,768 @@
|
||||
/*
|
||||
*
|
||||
* 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 googledirectpath
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 5 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
type emptyResolver struct {
|
||||
resolver.Resolver
|
||||
scheme string
|
||||
}
|
||||
|
||||
func (er *emptyResolver) Build(_ resolver.Target, _ resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
return er, nil
|
||||
}
|
||||
|
||||
func (er *emptyResolver) Scheme() string {
|
||||
return er.scheme
|
||||
}
|
||||
|
||||
func (er *emptyResolver) Close() {}
|
||||
|
||||
var (
|
||||
testDNSResolver = &emptyResolver{scheme: "dns"}
|
||||
testXDSResolver = &emptyResolver{scheme: "xds"}
|
||||
)
|
||||
|
||||
// replaceResolvers unregisters the real resolvers for schemes `dns` and `xds`
|
||||
// and registers test resolvers instead. This allows the test to verify that
|
||||
// expected resolvers are built.
|
||||
func replaceResolvers(t *testing.T) {
|
||||
oldDNS := resolver.Get("dns")
|
||||
resolver.Register(testDNSResolver)
|
||||
oldXDS := resolver.Get("xds")
|
||||
resolver.Register(testXDSResolver)
|
||||
t.Cleanup(func() {
|
||||
resolver.Register(oldDNS)
|
||||
resolver.Register(oldXDS)
|
||||
})
|
||||
}
|
||||
|
||||
func simulateRunningOnGCE(t *testing.T, gce bool) {
|
||||
oldOnGCE := onGCE
|
||||
onGCE = func() bool { return gce }
|
||||
t.Cleanup(func() { onGCE = oldOnGCE })
|
||||
}
|
||||
|
||||
// ensure universeDomain is set to the expected default,
|
||||
// and clean it up again after the test.
|
||||
func useCleanUniverseDomain(t *testing.T) {
|
||||
universeDomainMu.Lock()
|
||||
defer universeDomainMu.Unlock()
|
||||
if universeDomain != "" {
|
||||
t.Fatalf("universe domain unexpectedly initialized: %v", universeDomain)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
universeDomainMu.Lock()
|
||||
universeDomain = ""
|
||||
universeDomainMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
// verifyXDSClientBootstrapConfig checks that an xDS client with the given name
|
||||
// exists in the pool and that its bootstrap config matches the expected config.
|
||||
func verifyXDSClientBootstrapConfig(t *testing.T, pool *xdsclient.Pool, name string, wantConfig *bootstrap.Config) {
|
||||
t.Helper()
|
||||
client, close, err := pool.GetClientForTesting(name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetClientForTesting(%q) failed to get xds client: %v", name, err)
|
||||
}
|
||||
defer close()
|
||||
|
||||
gotConfig := client.BootstrapConfig()
|
||||
if gotConfig == nil {
|
||||
t.Fatalf("Failed to get bootstrap config: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(wantConfig, gotConfig); diff != "" {
|
||||
t.Fatalf("xdsClient.BootstrapConfig() for client %q returned unexpected diff (-want +got):\n%s", name, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where the bootstrap env vars are set and we're running on
|
||||
// GCE. The test builds a google-c2p resolver and verifies that an xDS resolver
|
||||
// is built and that we don't fallback to DNS (because federation is enabled by
|
||||
// default).
|
||||
func (s) TestBuildWithBootstrapEnvSet(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
|
||||
builder := resolver.Get(c2pScheme)
|
||||
for i, envP := range []*string{&envconfig.XDSBootstrapFileName, &envconfig.XDSBootstrapFileContent} {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
// Set bootstrap config env var.
|
||||
oldEnv := *envP
|
||||
*envP = "does not matter"
|
||||
defer func() { *envP = oldEnv }()
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
r, err := builder.Build(resolver.Target{}, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build resolver: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Build should return wrapped xDS resolver, not DNS.
|
||||
if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
|
||||
t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where we are not running on GCE. The test builds a
|
||||
// google-c2p resolver and verifies that we fallback to DNS.
|
||||
func (s) TestBuildNotOnGCE(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, false)
|
||||
useCleanUniverseDomain(t)
|
||||
builder := resolver.Get(c2pScheme)
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
r, err := builder.Build(resolver.Target{}, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build resolver: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Build should return DNS, not xDS.
|
||||
if r != testDNSResolver {
|
||||
t.Fatalf("Build() returned %#v, want dns resolver", r)
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapConfig(t *testing.T, opts bootstrap.ConfigOptionsForTesting) *bootstrap.Config {
|
||||
t.Helper()
|
||||
|
||||
contents, err := bootstrap.NewContentsForTesting(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap contents: %v", err)
|
||||
}
|
||||
cfg, err := bootstrap.NewConfigFromContents(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap config: %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Test that when a google-c2p resolver is built, the xDS client is built with
|
||||
// the expected config.
|
||||
func (s) TestBuildXDS(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
builder := resolver.Get(c2pScheme)
|
||||
|
||||
// Override the zone returned by the metadata server.
|
||||
oldGetZone := getZone
|
||||
getZone = func(time.Duration) string { return "test-zone" }
|
||||
defer func() { getZone = oldGetZone }()
|
||||
|
||||
// Override the random func used in the node ID.
|
||||
origRandInd := randInt
|
||||
randInt = func() int { return 666 }
|
||||
defer func() { randInt = origRandInd }()
|
||||
|
||||
for _, tt := range []struct {
|
||||
desc string
|
||||
ipv6Capable bool
|
||||
tdURIOverride string
|
||||
wantBootstrapConfig *bootstrap.Config
|
||||
}{
|
||||
{
|
||||
desc: "ipv6 false",
|
||||
wantBootstrapConfig: bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
}
|
||||
}`),
|
||||
}),
|
||||
},
|
||||
{
|
||||
desc: "ipv6 true",
|
||||
ipv6Capable: true,
|
||||
wantBootstrapConfig: bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
},
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE": true
|
||||
}
|
||||
}`),
|
||||
}),
|
||||
},
|
||||
{
|
||||
desc: "override TD URI",
|
||||
ipv6Capable: true,
|
||||
tdURIOverride: "test-uri",
|
||||
wantBootstrapConfig: bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "test-uri",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "test-uri",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
},
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE": true
|
||||
}
|
||||
}`),
|
||||
}),
|
||||
},
|
||||
} {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
// Override IPv6 capability returned by the metadata server.
|
||||
oldGetIPv6Capability := getIPv6Capable
|
||||
getIPv6Capable = func(time.Duration) bool { return tt.ipv6Capable }
|
||||
defer func() { getIPv6Capable = oldGetIPv6Capability }()
|
||||
|
||||
// Override TD URI test only env var.
|
||||
if tt.tdURIOverride != "" {
|
||||
oldURI := envconfig.C2PResolverTestOnlyTrafficDirectorURI
|
||||
envconfig.C2PResolverTestOnlyTrafficDirectorURI = tt.tdURIOverride
|
||||
defer func() { envconfig.C2PResolverTestOnlyTrafficDirectorURI = oldURI }()
|
||||
}
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
getIPv6Capable = func(time.Duration) bool { return tt.ipv6Capable }
|
||||
defer func() { getIPv6Capable = oldGetIPv6Capability }()
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
|
||||
r, err := builder.Build(target, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build resolver: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Build should return wrapped xDS resolver, not DNS.
|
||||
if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
|
||||
t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
|
||||
}
|
||||
|
||||
xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), tt.wantBootstrapConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDialFailsWhenTargetContainsAuthority attempts to Dial a target URI of
|
||||
// google-c2p scheme with a non-empty authority and verifies that it fails with
|
||||
// an expected error.
|
||||
func (s) TestBuildFailsWhenCalledWithAuthority(t *testing.T) {
|
||||
useCleanUniverseDomain(t)
|
||||
uri := "google-c2p://an-authority/resource"
|
||||
cc, err := grpc.NewClient(uri, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a client for server: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if cc != nil {
|
||||
cc.Close()
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err = client.EmptyCall(ctx, &testpb.Empty{})
|
||||
wantErr := "google-c2p URI scheme does not support authorities"
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("client.EmptyCall(%s) returned error: %v, want: %v", uri, err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestSetUniverseDomainNonDefault(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
builder := resolver.Get(c2pScheme)
|
||||
|
||||
// Override the zone returned by the metadata server.
|
||||
oldGetZone := getZone
|
||||
getZone = func(time.Duration) string { return "test-zone" }
|
||||
defer func() { getZone = oldGetZone }()
|
||||
|
||||
// Override IPv6 capability returned by the metadata server.
|
||||
oldGetIPv6Capability := getIPv6Capable
|
||||
getIPv6Capable = func(time.Duration) bool { return false }
|
||||
defer func() { getIPv6Capable = oldGetIPv6Capability }()
|
||||
|
||||
// Override the random func used in the node ID.
|
||||
origRandInd := randInt
|
||||
randInt = func() int { return 666 }
|
||||
defer func() { randInt = origRandInd }()
|
||||
|
||||
// Set the universe domain
|
||||
testUniverseDomain := "test-universe-domain.test"
|
||||
if err := SetUniverseDomain(testUniverseDomain); err != nil {
|
||||
t.Fatalf("SetUniverseDomain(%s) failed: %v", testUniverseDomain, err)
|
||||
}
|
||||
|
||||
// Now set universe domain to something different, it should fail
|
||||
domain := "test-universe-domain-2.test"
|
||||
err := SetUniverseDomain(domain)
|
||||
wantErr := "already set"
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(%s) returned error: %v, want: %v", domain, err, wantErr)
|
||||
}
|
||||
|
||||
// Now explicitly set universe domain to the default, it should also fail
|
||||
domain = "googleapis.com"
|
||||
err = SetUniverseDomain(domain)
|
||||
wantErr = "already set"
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(%s) returned error: %v, want: %v", domain, err, wantErr)
|
||||
}
|
||||
|
||||
// Now set universe domain to the original value, it should work
|
||||
if err := SetUniverseDomain(testUniverseDomain); err != nil {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(%s) failed: %v", testUniverseDomain, err)
|
||||
}
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
|
||||
r, err := builder.Build(target, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build resolver: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Build should return wrapped xDS resolver, not DNS.
|
||||
if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
|
||||
t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
|
||||
}
|
||||
|
||||
// Check that we use directpath-pa.test-universe-domain.test in the
|
||||
// bootstrap config.
|
||||
wantBootstrapConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.test-universe-domain.test",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "dns:///directpath-pa.test-universe-domain.test",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
}
|
||||
}`),
|
||||
})
|
||||
|
||||
xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), wantBootstrapConfig)
|
||||
}
|
||||
|
||||
func (s) TestDefaultUniverseDomain(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
builder := resolver.Get(c2pScheme)
|
||||
|
||||
// Override the zone returned by the metadata server.
|
||||
oldGetZone := getZone
|
||||
getZone = func(time.Duration) string { return "test-zone" }
|
||||
defer func() { getZone = oldGetZone }()
|
||||
|
||||
// Override IPv6 capability returned by the metadata server.
|
||||
oldGetIPv6Capability := getIPv6Capable
|
||||
getIPv6Capable = func(time.Duration) bool { return false }
|
||||
defer func() { getIPv6Capable = oldGetIPv6Capability }()
|
||||
|
||||
// Override the random func used in the node ID.
|
||||
origRandInd := randInt
|
||||
randInt = func() int { return 666 }
|
||||
defer func() { randInt = origRandInd }()
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
|
||||
r, err := builder.Build(target, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build resolver: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Build should return xDS, not DNS.
|
||||
if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
|
||||
t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
|
||||
}
|
||||
|
||||
// Check that we use directpath-pa.googleapis.com in the bootstrap config
|
||||
wantBootstrapConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
}
|
||||
}`),
|
||||
})
|
||||
xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), wantBootstrapConfig)
|
||||
|
||||
// Now set universe domain to something different than the default, it should fail
|
||||
domain := "test-universe-domain.test"
|
||||
err = SetUniverseDomain(domain)
|
||||
wantErr := "already set"
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(%s) returned error: %v, want: %v", domain, err, wantErr)
|
||||
}
|
||||
|
||||
// Now explicitly set universe domain to the default, it should work
|
||||
domain = "googleapis.com"
|
||||
if err := SetUniverseDomain(domain); err != nil {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(%s) failed: %v", domain, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestSetUniverseDomainEmptyString(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
wantErr := "cannot be empty"
|
||||
err := SetUniverseDomain("")
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("googlec2p.SetUniverseDomain(\"\") returned error: %v, want: %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateMultipleXDSClients validates that multiple xds clients with
|
||||
// different bootstrap config coexist in the same pool. It confirms that
|
||||
// a client created by the google-c2p resolver does not interfere with an
|
||||
// explicitly created client using a different bootstrap configuration.
|
||||
func (s) TestCreateMultipleXDSClients(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, true)
|
||||
useCleanUniverseDomain(t)
|
||||
|
||||
// Override the zone returned by the metadata server.
|
||||
oldGetZone := getZone
|
||||
getZone = func(time.Duration) string { return "test-zone" }
|
||||
defer func() { getZone = oldGetZone }()
|
||||
|
||||
// Override the random func used in the node ID.
|
||||
origRandInd := randInt
|
||||
randInt = func() int { return 666 }
|
||||
defer func() { randInt = origRandInd }()
|
||||
|
||||
// Override IPv6 capability returned by the metadata server.
|
||||
oldGetIPv6Capability := getIPv6Capable
|
||||
getIPv6Capable = func(time.Duration) bool { return false }
|
||||
defer func() { getIPv6Capable = oldGetIPv6Capability }()
|
||||
|
||||
// Define bootstrap config for generic xds resolver
|
||||
genericXDSConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///regular-xds.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": []
|
||||
}]`),
|
||||
Node: []byte(`{"id": "regular-xds-node", "locality": {"zone": "test-zone"}}`),
|
||||
})
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(genericXDSConfig)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
// Create generic xds client.
|
||||
xdsTarget := resolver.Target{URL: *testutils.MustParseURL("xds:///target")}
|
||||
_, closeGeneric, err := xdsClientPool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsTarget.String(),
|
||||
Config: genericXDSConfig,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("xdsClientPool.NewClientForTesting(%q) failed: %v", xdsTarget.String(), err)
|
||||
}
|
||||
defer closeGeneric()
|
||||
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), genericXDSConfig)
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
c2pBuilder := resolver.Get(c2pScheme)
|
||||
c2pTarget := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
|
||||
tcc := &testutils.ResolverClientConn{Logger: t}
|
||||
c2pRes, err := c2pBuilder.Build(c2pTarget, tcc, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build resolver: %v", err)
|
||||
}
|
||||
defer c2pRes.Close()
|
||||
|
||||
// Bootstrap config for c2p resolver.
|
||||
c2pConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-666",
|
||||
"locality": {
|
||||
"zone": "test-zone"
|
||||
}
|
||||
}`),
|
||||
})
|
||||
|
||||
c2pXDSTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: c2pTarget.URL.Path}}
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, c2pXDSTarget.String(), c2pConfig)
|
||||
}
|
||||
|
||||
// Test validates that the C2P resolver handles non-GCP (Google Cloud
|
||||
// Interconnect) clients correctly when the force-xds query parameter is
|
||||
// provided in various valid formats.
|
||||
// It verifies that:
|
||||
// - GCE metadata server queries for zone and ipv6 capability are bypassed.
|
||||
// - The node ID is formatted using the prefix ("C2P-non-gcp").
|
||||
// - Zone information is completely omitted from the node config.
|
||||
// - Dualstack IPv6 capability (TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE)
|
||||
// is set to true.
|
||||
func (s) TestBuild_NotOnGCE_WithForceXDS(t *testing.T) {
|
||||
wantBootstrapConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(`[{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}]`),
|
||||
Authorities: map[string]json.RawMessage{
|
||||
"traffic-director-c2p.xds.googleapis.com": []byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "dns:///directpath-pa.googleapis.com",
|
||||
"channel_creds": [{"type": "google_default"}],
|
||||
"server_features": ["ignore_resource_deletion"]
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
Node: []byte(`{
|
||||
"id": "C2P-non-gcp-666",
|
||||
"metadata": {
|
||||
"TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE": true
|
||||
}
|
||||
}`),
|
||||
})
|
||||
tests := []struct {
|
||||
desc string
|
||||
rawQuery string
|
||||
onGCE bool
|
||||
}{
|
||||
{
|
||||
desc: "query_param_key_only",
|
||||
rawQuery: "force-xds",
|
||||
},
|
||||
{
|
||||
desc: "query_param_trailing_equals_value_empty",
|
||||
rawQuery: "force-xds=",
|
||||
},
|
||||
{
|
||||
desc: "query_param_value_true",
|
||||
rawQuery: "force-xds=true",
|
||||
},
|
||||
{
|
||||
desc: "query_param_value_false",
|
||||
rawQuery: "force-xds=false",
|
||||
},
|
||||
{
|
||||
desc: "query_param_multiple_query_params",
|
||||
rawQuery: "foo=bar&force-xds",
|
||||
},
|
||||
{
|
||||
desc: "onGCE_with_force-xds",
|
||||
rawQuery: "force-xds",
|
||||
onGCE: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
replaceResolvers(t)
|
||||
simulateRunningOnGCE(t, tt.onGCE)
|
||||
useCleanUniverseDomain(t)
|
||||
|
||||
// Override the random func used in the node ID.
|
||||
origRandInd := randInt
|
||||
randInt = func() int { return 666 }
|
||||
defer func() { randInt = origRandInd }()
|
||||
|
||||
// Override xDS client pool.
|
||||
oldXdsClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = oldXdsClientPool }()
|
||||
|
||||
// Target URI with force-xds query parameter.
|
||||
builder := resolver.Get(c2pScheme)
|
||||
target := resolver.Target{URL: url.URL{
|
||||
Scheme: c2pScheme,
|
||||
Path: "test-path",
|
||||
RawQuery: tt.rawQuery,
|
||||
}}
|
||||
|
||||
// Build the google-c2p resolver.
|
||||
res, err := builder.Build(target, nil, resolver.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build resolver: %v", err)
|
||||
}
|
||||
defer res.Close()
|
||||
|
||||
// Query parameters are omitted since the downstream xDS target is
|
||||
// configured using only active fields.
|
||||
xdsTarget := resolver.Target{
|
||||
URL: url.URL{
|
||||
Scheme: xdsName,
|
||||
Host: c2pAuthority,
|
||||
Path: target.URL.Path,
|
||||
},
|
||||
}
|
||||
verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), wantBootstrapConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
101
xds/googledirectpath/utils.go
Normal file
101
xds/googledirectpath/utils.go
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
*
|
||||
* 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 googledirectpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getFromMetadata(timeout time.Duration, urlStr string) ([]byte, error) {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
req := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: parsedURL,
|
||||
Header: http.Header{"Metadata-Flavor": {"Google"}},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed communicating with metadata server: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("metadata server returned resp with non-OK: %v", resp)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed reading from metadata server: %v", err)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
var (
|
||||
zone string
|
||||
zoneOnce sync.Once
|
||||
)
|
||||
|
||||
// Defined as var to be overridden in tests.
|
||||
var getZone = func(timeout time.Duration) string {
|
||||
zoneOnce.Do(func() {
|
||||
qualifiedZone, err := getFromMetadata(timeout, zoneURL)
|
||||
if err != nil {
|
||||
logger.Warningf("could not discover instance zone: %v", err)
|
||||
return
|
||||
}
|
||||
i := bytes.LastIndexByte(qualifiedZone, '/')
|
||||
if i == -1 {
|
||||
logger.Warningf("could not parse zone from metadata server: %s", qualifiedZone)
|
||||
return
|
||||
}
|
||||
zone = string(qualifiedZone[i+1:])
|
||||
})
|
||||
return zone
|
||||
}
|
||||
|
||||
var (
|
||||
ipv6Capable bool
|
||||
ipv6CapableOnce sync.Once
|
||||
)
|
||||
|
||||
// Defined as var to be overridden in tests.
|
||||
var getIPv6Capable = func(timeout time.Duration) bool {
|
||||
ipv6CapableOnce.Do(func() {
|
||||
addr, err := getFromMetadata(timeout, ipv6URL)
|
||||
if err != nil {
|
||||
logger.Warningf("could not discover ipv6 capability: %v", err)
|
||||
return
|
||||
}
|
||||
if trimmedAddr := strings.TrimSpace(string(addr)); trimmedAddr == "" {
|
||||
logger.Warningf("metadata server returned empty ipv6 address")
|
||||
return
|
||||
}
|
||||
ipv6Capable = true
|
||||
})
|
||||
return ipv6Capable
|
||||
}
|
||||
235
xds/server.go
Normal file
235
xds/server.go
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
*
|
||||
* 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 xds
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
estats "google.golang.org/grpc/experimental/stats"
|
||||
"google.golang.org/grpc/internal"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
istats "google.golang.org/grpc/internal/stats"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/server"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
)
|
||||
|
||||
const serverPrefix = "[xds-server %p] "
|
||||
|
||||
var (
|
||||
// These will be overridden in unit tests.
|
||||
xdsClientPool = xdsclient.DefaultPool
|
||||
newGRPCServer = func(opts ...grpc.ServerOption) grpcServer {
|
||||
return grpc.NewServer(opts...)
|
||||
}
|
||||
)
|
||||
|
||||
// grpcServer contains methods from grpc.Server which are used by the
|
||||
// GRPCServer type here. This is useful for overriding in unit tests.
|
||||
type grpcServer interface {
|
||||
RegisterService(*grpc.ServiceDesc, any)
|
||||
Serve(net.Listener) error
|
||||
Stop()
|
||||
GracefulStop()
|
||||
GetServiceInfo() map[string]grpc.ServiceInfo
|
||||
}
|
||||
|
||||
// GRPCServer wraps a gRPC server and provides server-side xDS functionality, by
|
||||
// communication with a management server using xDS APIs. It implements the
|
||||
// grpc.ServiceRegistrar interface and can be passed to service registration
|
||||
// functions in IDL generated code.
|
||||
type GRPCServer struct {
|
||||
gs grpcServer
|
||||
quit *grpcsync.Event
|
||||
logger *internalgrpclog.PrefixLogger
|
||||
opts *server.Options
|
||||
xdsC xdsclient.XDSClient
|
||||
xdsClientClose func()
|
||||
}
|
||||
|
||||
// NewGRPCServer creates an xDS-enabled gRPC server using the passed in opts.
|
||||
// The underlying gRPC server has no service registered and has not started to
|
||||
// accept requests yet.
|
||||
func NewGRPCServer(opts ...grpc.ServerOption) (*GRPCServer, error) {
|
||||
// xdsFilterWrapper is a callback passed to the underlying gRPC server.
|
||||
// It adapts the generic stream parameter and delegates to RouteAndProcess
|
||||
// to execute xDS HTTP filter interceptors for each incoming RPC.
|
||||
xDSFilterWrapper := func(ss grpc.ServerStream) (grpc.ServerStream, error) {
|
||||
return server.RouteAndProcess(ss)
|
||||
}
|
||||
// Construct a grpc.ServerOption that registers xDSFilterWrapper on
|
||||
// the server.
|
||||
xDSFilterWrapperOption := internal.XDSFilterWrapperOption.(func(func(grpc.ServerStream) (grpc.ServerStream, error)) grpc.ServerOption)
|
||||
newOpts := append([]grpc.ServerOption{xDSFilterWrapperOption(xDSFilterWrapper)}, opts...)
|
||||
s := &GRPCServer{
|
||||
gs: newGRPCServer(newOpts...),
|
||||
quit: grpcsync.NewEvent(),
|
||||
}
|
||||
s.handleServerOptions(opts)
|
||||
|
||||
var mrl estats.MetricsRecorder
|
||||
mrl = istats.NewMetricsRecorderList(nil)
|
||||
if srv, ok := s.gs.(*grpc.Server); ok { // Will hit in prod but not for testing.
|
||||
mrl = internal.MetricsRecorderForServer.(func(*grpc.Server) estats.MetricsRecorder)(srv)
|
||||
}
|
||||
|
||||
// Initializing the xDS client upfront (instead of at serving time)
|
||||
// simplifies the code by eliminating the need for a mutex to protect the
|
||||
// xdsC and xdsClientClose fields.
|
||||
pool := xdsClientPool
|
||||
if s.opts.ClientPoolForTesting != nil {
|
||||
pool = s.opts.ClientPoolForTesting
|
||||
}
|
||||
xdsClient, xdsClientClose, err := pool.NewClient(xdsclient.NameForServer, mrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xDS client creation failed: %v", err)
|
||||
}
|
||||
|
||||
// Validate the bootstrap configuration for server specific fields.
|
||||
|
||||
// Listener resource name template is mandatory on the server side unless a
|
||||
// listener resource name override is provided.
|
||||
if s.opts.OverrideListenerResourceName == nil {
|
||||
cfg := xdsClient.BootstrapConfig()
|
||||
if cfg.ServerListenerResourceNameTemplate() == "" {
|
||||
xdsClientClose()
|
||||
return nil, errors.New("missing server_listener_resource_name_template in the bootstrap configuration")
|
||||
}
|
||||
}
|
||||
|
||||
s.xdsC = xdsClient
|
||||
s.xdsClientClose = xdsClientClose
|
||||
|
||||
s.logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(serverPrefix, s))
|
||||
s.logger.Infof("Created xds.GRPCServer")
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// handleServerOptions iterates through the list of server options passed in by
|
||||
// the user, and handles the xDS server specific options.
|
||||
func (s *GRPCServer) handleServerOptions(opts []grpc.ServerOption) {
|
||||
so := s.defaultServerOptions()
|
||||
server.ApplyServerOptions(opts, so)
|
||||
s.opts = so
|
||||
}
|
||||
|
||||
func (s *GRPCServer) defaultServerOptions() *server.Options {
|
||||
return &server.Options{
|
||||
// A default serving mode change callback which simply logs at the
|
||||
// default-visible log level. This will be used if the application does not
|
||||
// register a mode change callback.
|
||||
//
|
||||
// Note that this means that `s.opts.ModeCallback` will never be nil and can
|
||||
// safely be invoked directly from `handleServingModeChanges`.
|
||||
ModeCallback: s.loggingServerModeChangeCallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GRPCServer) loggingServerModeChangeCallback(addr net.Addr, mode connectivity.ServingMode, err error) {
|
||||
switch mode {
|
||||
case connectivity.ServingModeServing:
|
||||
s.logger.Errorf("Listener %q entering mode: %q", addr.String(), mode)
|
||||
case connectivity.ServingModeNotServing:
|
||||
s.logger.Errorf("Listener %q entering mode: %q due to error: %v", addr.String(), mode, err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterService registers a service and its implementation to the underlying
|
||||
// gRPC server. It is called from the IDL generated code. This must be called
|
||||
// before invoking Serve.
|
||||
func (s *GRPCServer) RegisterService(sd *grpc.ServiceDesc, ss any) {
|
||||
s.gs.RegisterService(sd, ss)
|
||||
}
|
||||
|
||||
// GetServiceInfo returns a map from service names to ServiceInfo.
|
||||
// Service names include the package names, in the form of <package>.<service>.
|
||||
func (s *GRPCServer) GetServiceInfo() map[string]grpc.ServiceInfo {
|
||||
return s.gs.GetServiceInfo()
|
||||
}
|
||||
|
||||
// Serve gets the underlying gRPC server to accept incoming connections on the
|
||||
// listener lis, which is expected to be listening on a TCP port.
|
||||
//
|
||||
// A connection to the management server, to receive xDS configuration, is
|
||||
// initiated here.
|
||||
//
|
||||
// Serve will return a non-nil error unless Stop or GracefulStop is called.
|
||||
func (s *GRPCServer) Serve(lis net.Listener) error {
|
||||
s.logger.Infof("Serve() passed a net.Listener on %s", lis.Addr().String())
|
||||
if _, ok := lis.Addr().(*net.TCPAddr); !ok {
|
||||
return fmt.Errorf("xds: GRPCServer expects listener to return a net.TCPAddr. Got %T", lis.Addr())
|
||||
}
|
||||
|
||||
if s.quit.HasFired() {
|
||||
return grpc.ErrServerStopped
|
||||
}
|
||||
|
||||
// The server listener resource name template from the bootstrap
|
||||
// configuration contains a template for the name of the Listener resource
|
||||
// to subscribe to for a gRPC server. If the token `%s` is present in the
|
||||
// string, it will be replaced with the server's listening "IP:port" (e.g.,
|
||||
// "0.0.0.0:8080", "[::]:8080").
|
||||
var name string
|
||||
if s.opts.OverrideListenerResourceName != nil {
|
||||
name = s.opts.OverrideListenerResourceName(lis.Addr())
|
||||
} else {
|
||||
cfg := s.xdsC.BootstrapConfig()
|
||||
name = cfg.ServerListenerResourceNameTemplate()
|
||||
name = bootstrap.PopulateResourceTemplate(name, lis.Addr().String())
|
||||
}
|
||||
|
||||
// Create a listenerWrapper which handles all functionality required by
|
||||
// this particular instance of Serve().
|
||||
lw := server.NewListenerWrapper(server.ListenerWrapperParams{
|
||||
Listener: lis,
|
||||
ListenerResourceName: name,
|
||||
XDSClient: s.xdsC,
|
||||
ModeCallback: s.opts.ModeCallback,
|
||||
})
|
||||
return s.gs.Serve(lw)
|
||||
}
|
||||
|
||||
// Stop stops the underlying gRPC server. It immediately closes all open
|
||||
// connections. It cancels all active RPCs on the server side and the
|
||||
// corresponding pending RPCs on the client side will get notified by connection
|
||||
// errors.
|
||||
func (s *GRPCServer) Stop() {
|
||||
s.quit.Fire()
|
||||
s.gs.Stop()
|
||||
if s.xdsC != nil {
|
||||
s.xdsClientClose()
|
||||
}
|
||||
}
|
||||
|
||||
// GracefulStop stops the underlying gRPC server gracefully. It stops the server
|
||||
// from accepting new connections and RPCs and blocks until all the pending RPCs
|
||||
// are finished.
|
||||
func (s *GRPCServer) GracefulStop() {
|
||||
s.quit.Fire()
|
||||
s.gs.GracefulStop()
|
||||
if s.xdsC != nil {
|
||||
s.xdsClientClose()
|
||||
}
|
||||
}
|
||||
532
xds/server_ext_test.go
Normal file
532
xds/server_ext_test.go
Normal file
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 xds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/xds"
|
||||
|
||||
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"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen.
|
||||
)
|
||||
|
||||
func hostPortFromListener(lis net.Listener) (string, uint32, error) {
|
||||
host, p, err := net.SplitHostPort(lis.Addr().String())
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("net.SplitHostPort(%s) failed: %v", lis.Addr().String(), err)
|
||||
}
|
||||
port, err := strconv.ParseInt(p, 10, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("strconv.ParseInt(%s, 10, 32) failed: %v", p, err)
|
||||
}
|
||||
return host, uint32(port), nil
|
||||
}
|
||||
|
||||
// servingModeChangeHandler handles changes to the serving mode of an
|
||||
// xDS-enabled gRPC server. It logs the changes and sends the new mode and any
|
||||
// errors on appropriate channels for the test to consume.
|
||||
type servingModeChangeHandler struct {
|
||||
logger interface {
|
||||
Logf(format string, args ...any)
|
||||
}
|
||||
// Access to the below fields are guarded by this mutex.
|
||||
mu sync.Mutex
|
||||
modeCh chan connectivity.ServingMode
|
||||
errCh chan error
|
||||
currentMode connectivity.ServingMode
|
||||
currentErr error
|
||||
}
|
||||
|
||||
func newServingModeChangeHandler(t *testing.T) *servingModeChangeHandler {
|
||||
return &servingModeChangeHandler{
|
||||
logger: t,
|
||||
modeCh: make(chan connectivity.ServingMode, 1),
|
||||
errCh: make(chan error, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *servingModeChangeHandler) modeChangeCallback(addr net.Addr, args xds.ServingModeChangeArgs) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// Suppress pushing duplicate mode change and error if the mode is staying
|
||||
// in NOT_SERVING and the error is the same.
|
||||
//
|
||||
// TODO(purnesh42h): Should we move this check to listener wrapper? This
|
||||
// shouldn't happen in practice a lot. But we never know what kind of
|
||||
// management servers users run.
|
||||
if m.currentMode == args.Mode && m.currentMode == connectivity.ServingModeNotServing && m.currentErr.Error() == args.Err.Error() {
|
||||
return
|
||||
}
|
||||
m.logger.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
|
||||
m.modeCh <- args.Mode
|
||||
m.currentMode = args.Mode
|
||||
if args.Err != nil {
|
||||
m.errCh <- args.Err
|
||||
}
|
||||
m.currentErr = args.Err
|
||||
}
|
||||
|
||||
// createStubServer creates a new xDS-enabled gRPC server and returns a
|
||||
// stubserver.StubServer that can be used for testing. The server is configured
|
||||
// with the provided modeChangeOpt and xdsclient.Pool.
|
||||
func createStubServer(t *testing.T, lis net.Listener, opts ...grpc.ServerOption) *stubserver.StubServer {
|
||||
stub := &stubserver.StubServer{
|
||||
Listener: lis,
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
|
||||
for {
|
||||
if _, err := stream.Recv(); err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
server, err := xds.NewGRPCServer(opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
stub.S = server
|
||||
stubserver.StartTestService(t, stub)
|
||||
t.Cleanup(stub.Stop)
|
||||
return stub
|
||||
}
|
||||
|
||||
// waitForSuccessfulRPC waits for an RPC to succeed, repeatedly calling the
|
||||
// EmptyCall RPC on the provided client connection until the call succeeds.
|
||||
// If the context is canceled or the expected error is not before the context
|
||||
// timeout expires, the test will fail.
|
||||
func waitForSuccessfulRPC(ctx context.Context, t *testing.T, cc *grpc.ClientConn, opts ...grpc.CallOption) {
|
||||
t.Helper()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for RPCs to succeed")
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, opts...); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForFailedRPCWithStatus waits for an RPC to fail with the expected status
|
||||
// code, error message, and node ID. It repeatedly calls the EmptyCall RPC on
|
||||
// the provided client connection until the error matches the expected values.
|
||||
// If the context is canceled or the expected error is not before the context
|
||||
// timeout expires, the test will fail.
|
||||
func waitForFailedRPCWithStatus(ctx context.Context, t *testing.T, cc *grpc.ClientConn, wantCode codes.Code, wantErr, wantNodeID string) {
|
||||
t.Helper()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
var err error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("RPCs failed with most recent error: %v. Want status code %v, error: %s, node id: %s", err, wantCode, wantErr, wantNodeID)
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
_, err = client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if gotCode := status.Code(err); gotCode != wantCode {
|
||||
continue
|
||||
}
|
||||
if gotErr := err.Error(); !strings.Contains(gotErr, wantErr) {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), wantNodeID) {
|
||||
continue
|
||||
}
|
||||
t.Logf("Most recent error happy case: %v", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the basic scenario for an xDS enabled gRPC server.
|
||||
//
|
||||
// - Verifies that the xDS enabled gRPC server requests for the expected
|
||||
// listener resource.
|
||||
// - Once the listener resource is received from the management server, it
|
||||
// verifies that the xDS enabled gRPC server requests for the appropriate
|
||||
// route configuration name. Also verifies that at this point, the server has
|
||||
// not yet started serving RPCs
|
||||
// - Once the route configuration is received from the management server, it
|
||||
// verifies that the server can serve RPCs successfully.
|
||||
func (s) TestServer_Basic(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
listenerNamesCh := make(chan []string, 1)
|
||||
routeNamesCh := make(chan []string, 1)
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
// The test needs to only verify the names of resources being
|
||||
// subscribed to, we can skip ACKs.
|
||||
if req.GetVersionInfo() != "" {
|
||||
return nil
|
||||
}
|
||||
switch req.GetTypeUrl() {
|
||||
case "type.googleapis.com/envoy.config.listener.v3.Listener":
|
||||
select {
|
||||
case listenerNamesCh <- req.GetResourceNames():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
case "type.googleapis.com/envoy.config.route.v3.RouteConfiguration":
|
||||
select {
|
||||
case routeNamesCh <- req.GetResourceNames():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure the managegement server with a listener resource for the above
|
||||
// xDS enabled gRPC server.
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Wait for the expected listener resource to be requested.
|
||||
wantLisResourceNames := []string{fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port))))}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the expected listener resource to be requested")
|
||||
case gotLisResourceName := <-listenerNamesCh:
|
||||
if !cmp.Equal(gotLisResourceName, wantLisResourceNames) {
|
||||
t.Fatalf("Got unexpected listener resource names: %v, want %v", gotLisResourceName, wantLisResourceNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the expected route config resource to be requested.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the expected route config resource to be requested")
|
||||
case gotRouteNames := <-routeNamesCh:
|
||||
if !cmp.Equal(gotRouteNames, []string{routeConfigName}) {
|
||||
t.Fatalf("Got unexpected route config resource names: %v, want %v", gotRouteNames, []string{routeConfigName})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the server is not serving RPCs yet.
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
select {
|
||||
case <-sCtx.Done():
|
||||
case <-modeChangeHandler.modeCh:
|
||||
t.Fatal("Server started serving RPCs before the route config was received")
|
||||
}
|
||||
|
||||
// Create a gRPC channel to the xDS enabled server.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the server isnt't serving RPCs successfully.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err == nil || status.Code(err) != codes.Unavailable {
|
||||
t.Fatalf("EmptyCall() returned %v, want %v", err, codes.Unavailable)
|
||||
}
|
||||
|
||||
// Configure the management server with the expected route config resource,
|
||||
// and expext RPCs to succeed.
|
||||
resources.Routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the server to start serving RPCs")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
}
|
||||
|
||||
// Tests that the xDS-enabled gRPC server cleans up all its resources when all
|
||||
// connections to it are closed.
|
||||
func (s) TestServer_ConnectionCleanup(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure the managegement server with a listener and route configuration
|
||||
// resource for the above xDS enabled gRPC server.
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeOpt := xds.ServingModeCallback(func(addr net.Addr, args xds.ServingModeChangeArgs) {
|
||||
t.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
|
||||
})
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create a gRPC channel and verify that RPCs succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Create multiple channels to the server, and make an RPC on each one. When
|
||||
// everything is closed, the server should have cleaned up all its resources
|
||||
// as well (and this will be verified by the leakchecker).
|
||||
const numConns = 100
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numConns)
|
||||
for range numConns {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Errorf("grpc.NewClient failed with err: %v", err)
|
||||
}
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Errorf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
cc.Close()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Tests that multiple xDS-enabled gRPC servers can be created with different
|
||||
// bootstrap configurations, and that they correctly request different LDS
|
||||
// resources from the management server based on their respective listening
|
||||
// ports. It also ensures that gRPC clients can connect to the intended server
|
||||
// and that RPCs function correctly. The test uses the grpc.Peer() call option
|
||||
// to validate that the client is connected to the correct server.
|
||||
func (s) TestServer_MultipleServers_DifferentBootstrapConfigurations(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create two bootstrap configurations pointing to the above management server.
|
||||
nodeID1 := uuid.New().String()
|
||||
bootstrapContents1 := e2e.DefaultBootstrapContents(t, nodeID1, mgmtServer.Address)
|
||||
nodeID2 := uuid.New().String()
|
||||
bootstrapContents2 := e2e.DefaultBootstrapContents(t, nodeID2, mgmtServer.Address)
|
||||
|
||||
// Create two xDS-enabled gRPC servers using the above bootstrap configs.
|
||||
lis1, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
lis2, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
|
||||
modeChangeHandler1 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt1 := xds.ServingModeCallback(modeChangeHandler1.modeChangeCallback)
|
||||
modeChangeHandler2 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt2 := xds.ServingModeCallback(modeChangeHandler2.modeChangeCallback)
|
||||
config1, err := bootstrap.NewConfigFromContents(bootstrapContents1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents1), err)
|
||||
}
|
||||
pool1 := xdsclient.NewPool(config1)
|
||||
config2, err := bootstrap.NewConfigFromContents(bootstrapContents2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents2), err)
|
||||
}
|
||||
pool2 := xdsclient.NewPool(config2)
|
||||
createStubServer(t, lis1, modeChangeOpt1, xds.ClientPoolForTesting(pool1))
|
||||
createStubServer(t, lis2, modeChangeOpt2, xds.ClientPoolForTesting(pool2))
|
||||
|
||||
// Update the management server with the listener resources pointing to the
|
||||
// corresponding gRPC servers.
|
||||
host1, port1, err := hostPortFromListener(lis1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
host2, port2, err := hostPortFromListener(lis2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
resources1 := e2e.UpdateOptions{
|
||||
NodeID: nodeID1,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host1, port1, e2e.SecurityLevelNone, "routeName")},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resources2 := e2e.UpdateOptions{
|
||||
NodeID: nodeID2,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host2, port2, e2e.SecurityLevelNone, "routeName")},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler1.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler2.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create two gRPC clients, one for each server.
|
||||
cc1, err := grpc.NewClient(lis1.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client for test server 1: %s, %v", lis1.Addr().String(), err)
|
||||
}
|
||||
defer cc1.Close()
|
||||
|
||||
cc2, err := grpc.NewClient(lis2.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client for test server 2: %s, %v", lis2.Addr().String(), err)
|
||||
}
|
||||
defer cc2.Close()
|
||||
|
||||
// Both unary RPCs should work once the servers transitions into serving.
|
||||
var peer1 peer.Peer
|
||||
waitForSuccessfulRPC(ctx, t, cc1, grpc.Peer(&peer1))
|
||||
if peer1.Addr.String() != lis1.Addr().String() {
|
||||
t.Errorf("Connected to wrong peer: %s, want %s", peer1.Addr, lis1.Addr())
|
||||
}
|
||||
|
||||
var peer2 peer.Peer
|
||||
waitForSuccessfulRPC(ctx, t, cc2, grpc.Peer(&peer2))
|
||||
if peer2.Addr.String() != lis2.Addr().String() {
|
||||
t.Errorf("Connected to wrong peer: %s, want %s", peer2.Addr, lis2.Addr())
|
||||
}
|
||||
}
|
||||
102
xds/server_options.go
Normal file
102
xds/server_options.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
*
|
||||
* 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 xds
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
internalserver "google.golang.org/grpc/internal/xds/server"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
)
|
||||
|
||||
// ServingModeCallback returns a grpc.ServerOption which allows users to
|
||||
// register a callback to get notified about serving mode changes.
|
||||
func ServingModeCallback(cb ServingModeCallbackFunc) grpc.ServerOption {
|
||||
return internalserver.NewServerOption(func(o *internalserver.Options) {
|
||||
o.ModeCallback = func(addr net.Addr, mode connectivity.ServingMode, err error) {
|
||||
cb(addr, ServingModeChangeArgs{
|
||||
Mode: mode,
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ServingModeCallbackFunc is the callback that users can register to get
|
||||
// notified about the server's serving mode changes. The callback is invoked
|
||||
// with the address of the listener and its new mode.
|
||||
//
|
||||
// Users must not perform any blocking operations in this callback.
|
||||
type ServingModeCallbackFunc func(addr net.Addr, args ServingModeChangeArgs)
|
||||
|
||||
// ServingModeChangeArgs wraps the arguments passed to the serving mode callback
|
||||
// function.
|
||||
type ServingModeChangeArgs struct {
|
||||
// Mode is the new serving mode of the server listener.
|
||||
Mode connectivity.ServingMode
|
||||
// Err is set to a non-nil error if the server has transitioned into
|
||||
// not-serving mode.
|
||||
Err error
|
||||
}
|
||||
|
||||
// BootstrapContentsForTesting returns a grpc.ServerOption which allows users
|
||||
// to inject a bootstrap configuration used by only this server, instead of the
|
||||
// global configuration from the environment variables.
|
||||
//
|
||||
// # Testing Only
|
||||
//
|
||||
// This function should ONLY be used for testing and may not work with some
|
||||
// other features, including the CSDS service.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func BootstrapContentsForTesting(bootstrapContents []byte) grpc.ServerOption {
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to parse bootstrap contents %s for server options: %v", string(bootstrapContents), err)
|
||||
return internalserver.NewServerOption(func(o *internalserver.Options) {
|
||||
o.ClientPoolForTesting = nil
|
||||
})
|
||||
}
|
||||
return ClientPoolForTesting(xdsclient.NewPool(config))
|
||||
}
|
||||
|
||||
// ClientPoolForTesting returns a grpc.ServerOption with the pool for xds
|
||||
// clients. It allows users to set a pool for xDS clients sharing the bootstrap
|
||||
// contents for this server.
|
||||
//
|
||||
// # Testing Only
|
||||
//
|
||||
// This function should ONLY be used for testing and may not work with some
|
||||
// other features, including the CSDS service.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func ClientPoolForTesting(pool *xdsclient.Pool) grpc.ServerOption {
|
||||
return internalserver.NewServerOption(func(o *internalserver.Options) {
|
||||
o.ClientPoolForTesting = pool
|
||||
})
|
||||
}
|
||||
654
xds/server_resource_ext_test.go
Normal file
654
xds/server_resource_ext_test.go
Normal file
@@ -0,0 +1,654 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/grpc/xds"
|
||||
"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"
|
||||
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"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
// Tests the case where an LDS points to an RDS which returns resource not
|
||||
// found. Before getting the resource not found, the xDS Server has not received
|
||||
// all configuration needed, so it should Accept and Close any new connections.
|
||||
// After it has received the resource not found error (due to short watch
|
||||
// expiry), the server should move to serving, successfully Accept Connections,
|
||||
// and fail at the L7 level with resource not found specified.
|
||||
func (s) TestServer_RouteConfiguration_ResourceNotFound(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
routeConfigNamesCh := make(chan []string, 1)
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.TypeUrl == version.V3RouteConfigURL {
|
||||
select {
|
||||
case routeConfigNamesCh <- req.GetResourceNames():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Setup the management server to respond with a listener resource that
|
||||
// specifies a route name to watch, and no RDS resource corresponding to
|
||||
// this route name.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
const routeConfigResourceName = "routeName"
|
||||
listener := e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, routeConfigResourceName)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
// Create a specific xDS client instance within that pool for the server,
|
||||
// configuring it with a short WatchExpiryTimeout.
|
||||
pool := xdsclient.NewPool(config)
|
||||
_, serverXDSClientClose, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: xdsclient.NameForServer,
|
||||
WatchExpiryTimeout: 500 * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client for server: %v", err)
|
||||
}
|
||||
defer serverXDSClientClose()
|
||||
// Start an xDS-enabled gRPC server using the above client from the pool.
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Wait for the route configuration resource to be requested from the
|
||||
// management server.
|
||||
select {
|
||||
case gotNames := <-routeConfigNamesCh:
|
||||
if !cmp.Equal(gotNames, []string{routeConfigResourceName}) {
|
||||
t.Fatalf("Requested route config resource names: %v, want %v", gotNames, []string{routeConfigResourceName})
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for route config resource to be requested")
|
||||
}
|
||||
|
||||
// Do NOT send the RDS resource. The xDS client's watch expiry timer will
|
||||
// fire. After the RDS resource is deemed "not found" (due to the short
|
||||
// watch expiry), the server will transition to SERVING mode.
|
||||
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
// Before the watch expiry, the server is NOT_SERVING, RPCs should fail with UNAVAILABLE.
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "", "")
|
||||
|
||||
// Wait for the xDS-enabled gRPC server to go SERVING. This should happen
|
||||
// after the RDS watch expiry timer fires.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
// After watch expiry, the server should be SERVING, but RPCs should fail
|
||||
// at the L7 level with resource not found.
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "error from xDS configuration for matched route configuration", nodeID)
|
||||
}
|
||||
|
||||
// Tests the scenario where the control plane sends the same resource update. It
|
||||
// verifies that the mode change callback is not invoked and client connections
|
||||
// to the server are not recycled.
|
||||
func (s) TestServer_RedundantUpdateSuppression(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Setup the management server to respond with the listener resources.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
listener := e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler.modeCh:
|
||||
if mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Listener received new mode %v, want %v", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ClientConn and make a successful RPCs.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Start a goroutine to make sure that we do not see any connectivity state
|
||||
// changes on the client connection. If redundant updates are not
|
||||
// suppressed, server will recycle client connections.
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Ready)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
prev := connectivity.Ready
|
||||
for {
|
||||
curr := cc.GetState()
|
||||
if !(curr == connectivity.Ready || curr == connectivity.Idle) {
|
||||
errCh <- fmt.Errorf("unexpected connectivity state change {%s --> %s} on the client connection", prev, curr)
|
||||
return
|
||||
}
|
||||
if !cc.WaitForStateChange(ctx, curr) {
|
||||
// Break out of the for loop when the context has been cancelled.
|
||||
break
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
// Update the management server with the same listener resource. This will
|
||||
// update the resource version though, and should result in a the management
|
||||
// server sending the same resource to the xDS-enabled gRPC server.
|
||||
if err := managementServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Since redundant resource updates are suppressed, we should not see the
|
||||
// mode change callback being invoked.
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
select {
|
||||
case <-sCtx.Done():
|
||||
case mode := <-modeChangeHandler.modeCh:
|
||||
t.Fatalf("Unexpected mode change callback with new mode %v", mode)
|
||||
}
|
||||
|
||||
// Make sure RPCs continue to succeed.
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Cancel the context to ensure that the WaitForStateChange call exits early
|
||||
// and returns false.
|
||||
cancel()
|
||||
if err := <-errCh; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the route configuration contains an unsupported route
|
||||
// action. Verifies that RPCs fail with UNAVAILABLE.
|
||||
func (s) TestServer_FailWithRouteActionRoute(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Configure the managegement server with a listener and route configuration
|
||||
// resource for the above xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)},
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeOpt := xds.ServingModeCallback(func(addr net.Addr, args xds.ServingModeChangeArgs) {
|
||||
t.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
|
||||
})
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create a gRPC channel and verify that RPCs succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Update the route config resource to contain an unsupported action.
|
||||
//
|
||||
// "NonForwardingAction is expected for all Routes used on server-side; a
|
||||
// route with an inappropriate action causes RPCs matching that route to
|
||||
// fail with UNAVAILABLE." - A36
|
||||
resources.Routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigFilterAction("routeName")}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "the incoming RPC matched to a route that was not of action type non forwarding", nodeID)
|
||||
}
|
||||
|
||||
// Tests the case where the listener resource is removed from the management
|
||||
// server. This should cause the xDS server to transition to NOT_SERVING mode,
|
||||
// and the error message should contain the xDS node ID.
|
||||
func (s) TestServer_ListenerResourceRemoved(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Configure the managegement server with a listener and route configuration
|
||||
// resource for the above xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a gRPC channel and verify that RPCs succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Remove the listener resource from the management server. This should
|
||||
// cause the server to go NOT_SERVING, and the error message should contain
|
||||
// the xDS node ID.
|
||||
resources.Listeners = nil
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for server to go NOT_SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
gotErr := <-modeChangeHandler.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), nodeID) {
|
||||
t.Fatalf("Unexpected error: %v, want xDS Node id: %s", gotErr, nodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the listener resource points to a route configuration
|
||||
// name that is NACKed. This should trigger the server to move to SERVING,
|
||||
// successfully accept connections, and fail at RPCs with an expected error
|
||||
// message.
|
||||
func (s) TestServer_RouteConfiguration_ResourceNACK(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Configure the managegement server with a listener and route configuration
|
||||
// resource (that will be NACKed) for the above xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.RouteConfigNoRouteMatch(routeConfigName)},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create a gRPC channel and verify that RPCs succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the server to start serving RPCs")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "error from xDS configuration for matched route configuration", nodeID)
|
||||
}
|
||||
|
||||
// Tests the case where the listener resource points to multiple route
|
||||
// configuration resources.
|
||||
//
|
||||
// - Initially the listener resource points to three route configuration
|
||||
// resources (A, B and C). The filter chain in the listener matches incoming
|
||||
// connections to route A, and RPCs are expected to succeed.
|
||||
// - A streaming RPC is also kept open at this point.
|
||||
// - The listener resource is then updated to point to two route configuration
|
||||
// resources (A and B). The filter chain in the listener resource does not
|
||||
// match to any of the configured routes. The default filter chain though
|
||||
// matches to route B, which contains a route action of type "Route", and this
|
||||
// is not supported on the server side. New RPCs are expected to fail, while
|
||||
// any ongoing RPCs should be allowed to complete.
|
||||
// - The listener resource is then updated to point to a single route
|
||||
// configuration (A), and the filter chain in the listener matches to route A.
|
||||
// New RPCs are expected to succeed at this point.
|
||||
func (s) TestServer_MultipleRouteConfigurations(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Setup the management server to respond with a listener resource that
|
||||
// specifies three route names to watch, and the corresponding route
|
||||
// configuration resources.
|
||||
const routeConfigNameA = "routeName-A"
|
||||
const routeConfigNameB = "routeName-B"
|
||||
const routeConfigNameC = "routeName-C"
|
||||
ldsResource := e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, routeConfigNameA)
|
||||
ldsResource.FilterChains = append(ldsResource.FilterChains,
|
||||
filterChainWontMatch(t, routeConfigNameB, "1.1.1.1", []uint32{1}),
|
||||
filterChainWontMatch(t, routeConfigNameC, "2.2.2.2", []uint32{2}),
|
||||
)
|
||||
routeConfigA := e2e.RouteConfigNonForwardingAction(routeConfigNameA)
|
||||
routeConfigB := e2e.RouteConfigFilterAction(routeConfigNameB) // Unsupported route action on server.
|
||||
routeConfigC := e2e.RouteConfigFilterAction(routeConfigNameC) // Unsupported route action on server.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{ldsResource},
|
||||
Routes: []*v3routepb.RouteConfiguration{routeConfigA, routeConfigB, routeConfigC},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeOpt := xds.ServingModeCallback(func(addr net.Addr, args xds.ServingModeChangeArgs) {
|
||||
t.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
|
||||
})
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create a gRPC channel and verify that RPCs succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%q) failed: %v", lis.Addr(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Start a streaming RPC and keep the stream open.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
stream, err := client.FullDuplexCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("FullDuplexCall failed: %v", err)
|
||||
}
|
||||
if err = stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
|
||||
t.Fatalf("stream.Send() failed: %v, should continue to work due to graceful stop", err)
|
||||
}
|
||||
|
||||
// Update the listener resource such that the filter chain does not match
|
||||
// incoming connections to route A. Instead a default filter chain matches
|
||||
// to route B, which contains an unsupported route action.
|
||||
ldsResource = e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, routeConfigNameA)
|
||||
ldsResource.FilterChains = []*v3listenerpb.FilterChain{filterChainWontMatch(t, routeConfigNameA, "1.1.1.1", []uint32{1})}
|
||||
ldsResource.DefaultFilterChain = filterChainWontMatch(t, routeConfigNameB, "2.2.2.2", []uint32{2})
|
||||
resources.Listeners = []*v3listenerpb.Listener{ldsResource}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// xDS is eventually consistent. So simply poll for the new change to be
|
||||
// reflected.
|
||||
// "NonForwardingAction is expected for all Routes used on server-side; a
|
||||
// route with an inappropriate action causes RPCs matching that route to
|
||||
// fail with UNAVAILABLE." - A36
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "the incoming RPC matched to a route that was not of action type non forwarding", nodeID)
|
||||
|
||||
// Stream should be allowed to continue on the old working configuration -
|
||||
// as it on a connection that is gracefully closed (old FCM/LDS
|
||||
// Configuration which is allowed to continue).
|
||||
if err = stream.CloseSend(); err != nil {
|
||||
t.Fatalf("stream.CloseSend() failed: %v, should continue to work due to graceful stop", err)
|
||||
}
|
||||
if _, err = stream.Recv(); err != io.EOF {
|
||||
t.Fatalf("unexpected error: %v, expected an EOF error", err)
|
||||
}
|
||||
|
||||
// Update the listener resource to point to a single route configuration
|
||||
// that is expected to match and verify that RPCs succeed.
|
||||
resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, routeConfigNameA)}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
}
|
||||
|
||||
// filterChainWontMatch returns a filter chain that won't match if running the
|
||||
// test locally.
|
||||
func filterChainWontMatch(t *testing.T, routeName string, addressPrefix string, srcPorts []uint32) *v3listenerpb.FilterChain {
|
||||
hcm := &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
|
||||
Rds: &v3httppb.Rds{
|
||||
ConfigSource: &v3corepb.ConfigSource{
|
||||
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
|
||||
},
|
||||
RouteConfigName: routeName,
|
||||
},
|
||||
},
|
||||
HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
|
||||
}
|
||||
return &v3listenerpb.FilterChain{
|
||||
Name: routeName + "-wont-match",
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: addressPrefix,
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
|
||||
SourcePorts: srcPorts,
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: addressPrefix,
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []*v3listenerpb.Filter{
|
||||
{
|
||||
Name: "filter-1",
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: testutils.MarshalAny(t, hcm)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
475
xds/server_security_ext_test.go
Normal file
475
xds/server_security_ext_test.go
Normal file
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
xdscreds "google.golang.org/grpc/credentials/xds"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/xds"
|
||||
"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"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
// Tests the case where the bootstrap configuration contains no certificate
|
||||
// providers, and xDS credentials with an insecure fallback is specified at
|
||||
// server creation time. The management server is configured to return a
|
||||
// server-side xDS Listener resource with no security configuration. The test
|
||||
// verifies that a gRPC client configured with insecure credentials is able to
|
||||
// make RPCs to the backend. This ensures that the insecure fallback
|
||||
// credentials are getting used on the server.
|
||||
func (s) TestServer_Security_NoCertProvidersInBootstrap_Success(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start an xDS management server.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure the managegement server with a listener and route configuration
|
||||
// resource for the above xDS enabled gRPC server.
|
||||
const routeConfigName = "routeName"
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration
|
||||
// and configure xDS credentials to be used on the server-side.
|
||||
creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{
|
||||
FallbackCreds: insecure.NewCredentials(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeOpt := xds.ServingModeCallback(func(addr net.Addr, args xds.ServingModeChangeArgs) {
|
||||
t.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
|
||||
})
|
||||
createStubServer(t, lis, grpc.Creds(creds), modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create a client that uses insecure creds and verify RPCs.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the bootstrap configuration contains no certificate
|
||||
// providers, and xDS credentials with an insecure fallback is specified at
|
||||
// server creation time. The management server is configured to return a
|
||||
// server-side xDS Listener resource with mTLS security configuration. The xDS
|
||||
// client is expected to NACK this resource because the certificate provider
|
||||
// instance name specified in the Listener resource will not be present in the
|
||||
// bootstrap file. The test verifies that server creation does not fail and that
|
||||
// if the xDS-enabled gRPC server receives resource error causing mode change,
|
||||
// it does not enter "serving" mode.
|
||||
func (s) TestServer_Security_NoCertificateProvidersInBootstrap_Failure(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Spin up an xDS management server that pushes on a channel when it
|
||||
// receives a NACK for an LDS response.
|
||||
nackCh := make(chan struct{}, 1)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() != "type.googleapis.com/envoy.config.listener.v3.Listener" {
|
||||
return nil
|
||||
}
|
||||
if req.GetErrorDetail() == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case nackCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create bootstrap configuration with no certificate providers.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(fmt.Sprintf(`[{
|
||||
"server_uri": %q,
|
||||
"channel_creds": [{"type": "insecure"}]
|
||||
}]`, mgmtServer.Address)),
|
||||
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
|
||||
ServerListenerResourceNameTemplate: e2e.ServerListenerResourceNameTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap configuration: %v", err)
|
||||
}
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen to local port: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration
|
||||
// and configure xDS credentials to be used on the server-side.
|
||||
creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{
|
||||
FallbackCreds: insecure.NewCredentials(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, grpc.Creds(creds), modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create an inbound xDS listener resource for the server side that contains
|
||||
// mTLS security configuration. Since the received certificate provider
|
||||
// instance name would be missing in the bootstrap configuration, this
|
||||
// resource is expected to NACKed by the xDS client.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelMTLS, "routeName")},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the NACK from the xDS client.
|
||||
select {
|
||||
case <-nackCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for an NACK from the xDS client for the LDS response")
|
||||
}
|
||||
|
||||
// Wait a short duration and ensure that if the server receive mode change
|
||||
// it does not enter "serving" mode.
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
select {
|
||||
case <-sCtx.Done():
|
||||
case stateCh := <-modeChangeHandler.modeCh:
|
||||
if stateCh == connectivity.ServingModeServing {
|
||||
t.Fatal("Server entered serving mode before the route config was received")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a client that uses insecure creds and verify that RPCs don't
|
||||
// succeed.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "", "")
|
||||
}
|
||||
|
||||
// Tests the case where the bootstrap configuration contains one certificate
|
||||
// provider, and xDS credentials with an insecure fallback is specified at
|
||||
// server creation time. Two listeners are configured on the xDS-enabled gRPC
|
||||
// server. The management server responds with two listener resources:
|
||||
// 1. contains valid security configuration pointing to the certificate provider
|
||||
// instance specified in the bootstrap
|
||||
// 2. contains invalid security configuration pointing to a non-existent
|
||||
// certificate provider instance
|
||||
//
|
||||
// The test verifies that an RPC to the first listener succeeds, while the
|
||||
// second listener receive a resource error which cause the server mode change
|
||||
// but never moves to "serving" mode.
|
||||
func (s) TestServer_Security_WithValidAndInvalidSecurityConfiguration(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Spin up an xDS management server that pushes on a channel when it
|
||||
// receives a NACK for an LDS response.
|
||||
nackCh := make(chan struct{}, 1)
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() != "type.googleapis.com/envoy.config.listener.v3.Listener" {
|
||||
return nil
|
||||
}
|
||||
if req.GetErrorDetail() == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case nackCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create two xDS-enabled gRPC servers using the above bootstrap configs.
|
||||
lis1, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
lis2, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
|
||||
modeChangeHandler1 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt1 := xds.ServingModeCallback(modeChangeHandler1.modeChangeCallback)
|
||||
modeChangeHandler2 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt2 := xds.ServingModeCallback(modeChangeHandler2.modeChangeCallback)
|
||||
creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
createStubServer(t, lis1, grpc.Creds(creds), modeChangeOpt1, xds.ClientPoolForTesting(pool))
|
||||
createStubServer(t, lis2, grpc.Creds(creds), modeChangeOpt2, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Create inbound xDS listener resources for the server side that contains
|
||||
// mTLS security configuration.
|
||||
// lis1 --> security configuration pointing to a valid cert provider
|
||||
// lis2 --> security configuration pointing to a non-existent cert provider
|
||||
host1, port1, err := hostPortFromListener(lis1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
resource1 := e2e.DefaultServerListener(host1, port1, e2e.SecurityLevelMTLS, "routeName")
|
||||
host2, port2, err := hostPortFromListener(lis2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
hcm := &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: &v3routepb.RouteConfiguration{
|
||||
Name: "routeName",
|
||||
VirtualHosts: []*v3routepb.VirtualHost{{
|
||||
Domains: []string{"*"},
|
||||
Routes: []*v3routepb.Route{{
|
||||
Match: &v3routepb.RouteMatch{
|
||||
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
|
||||
},
|
||||
Action: &v3routepb.Route_NonForwardingAction{},
|
||||
}}}}},
|
||||
},
|
||||
HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
|
||||
}
|
||||
ts := &v3corepb.TransportSocket{
|
||||
Name: "envoy.transport_sockets.tls",
|
||||
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
|
||||
RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
|
||||
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
||||
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
||||
InstanceName: "non-existent-certificate-provider",
|
||||
},
|
||||
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
||||
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
||||
InstanceName: "non-existent-certificate-provider",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
resource2 := &v3listenerpb.Listener{
|
||||
Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host2, strconv.Itoa(int(port2)))),
|
||||
Address: &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: host2,
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: port2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
Name: "v4-wildcard",
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: "0.0.0.0",
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: "0.0.0.0",
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []*v3listenerpb.Filter{
|
||||
{
|
||||
Name: "filter-1",
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: testutils.MarshalAny(t, hcm)},
|
||||
},
|
||||
},
|
||||
TransportSocket: ts,
|
||||
},
|
||||
{
|
||||
Name: "v6-wildcard",
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: "::",
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{
|
||||
{
|
||||
AddressPrefix: "::",
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []*v3listenerpb.Filter{
|
||||
{
|
||||
Name: "filter-1",
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: testutils.MarshalAny(t, hcm)},
|
||||
},
|
||||
},
|
||||
TransportSocket: ts,
|
||||
},
|
||||
},
|
||||
}
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{resource1, resource2},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a client that uses TLS creds and verify RPCs to listener1.
|
||||
clientCreds := testutils.CreateClientTLSCredentials(t)
|
||||
cc1, err := grpc.NewClient(lis1.Addr().String(), grpc.WithTransportCredentials(clientCreds))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc1.Close()
|
||||
|
||||
client1 := testgrpc.NewTestServiceClient(cc1)
|
||||
if _, err := client1.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for the NACK from the xDS client.
|
||||
select {
|
||||
case <-nackCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for an NACK from the xDS client for the LDS response")
|
||||
}
|
||||
|
||||
// Wait a short duration and ensure that if the server receives mode change
|
||||
// it does not enter "serving" mode.
|
||||
select {
|
||||
case <-time.After(2 * defaultTestShortTimeout):
|
||||
case mode := <-modeChangeHandler2.modeCh:
|
||||
if mode == connectivity.ServingModeServing {
|
||||
t.Fatal("Server changed to serving mode when not expected to")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a client that uses insecure creds and verify that RPCs don't
|
||||
// succeed to listener2.
|
||||
cc2, err := grpc.NewClient(lis2.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc2.Close()
|
||||
|
||||
waitForFailedRPCWithStatus(ctx, t, cc2, codes.Unavailable, "", "")
|
||||
}
|
||||
606
xds/server_serving_mode_ext_test.go
Normal file
606
xds/server_serving_mode_ext_test.go
Normal file
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/xds"
|
||||
|
||||
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"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
// Tests the Server's logic as it transitions from NOT_SERVING to SERVING, then
|
||||
// to NOT_SERVING again. Before it goes to SERVING, connections should be
|
||||
// accepted and closed. After it goes SERVING, RPC's should proceed as normal
|
||||
// according to matched route configuration. After it transitions back into
|
||||
// NOT_SERVING, (through an explicit LDS Resource Not Found), previously running
|
||||
// RPC's should be gracefully closed and still work, and new RPC's should fail.
|
||||
func (s) TestServer_ServingModeChanges_SingleServer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Setup the management server to respond with a listener resource that
|
||||
// specifies a route name to watch. Due to not having received the full
|
||||
// configuration, this should cause the server to be in mode NOT_SERVING.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
listener := e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Start a gRPC channel to the above server. The server is yet to receive
|
||||
// route configuration, and therefore RPCs must fail at this time.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "", "")
|
||||
|
||||
// Setup the route configuration resource on the management server. This
|
||||
// should cause the xDS-enabled gRPC server to move to SERVING mode.
|
||||
routeConfig := e2e.RouteConfigNonForwardingAction("routeName")
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
Routes: []*v3routepb.RouteConfiguration{routeConfig},
|
||||
SkipValidation: true,
|
||||
}
|
||||
defer cancel()
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
|
||||
// Start a stream before switching the server to not serving. Due to the
|
||||
// stream being created before the graceful stop of the underlying
|
||||
// connection, it should be able to continue even after the server switches
|
||||
// to not serving.
|
||||
c := testgrpc.NewTestServiceClient(cc)
|
||||
stream, err := c.FullDuplexCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("cc.FullDuplexCall failed: %f", err)
|
||||
}
|
||||
|
||||
// Remove the listener resource from the management server.
|
||||
resources.Listeners = nil
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the server is in NOT_SERVING mode.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go NOT_SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
gotErr := <-modeChangeHandler.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), nodeID) {
|
||||
t.Fatalf("Unexpected error: %v, want xDS Node id: %s", gotErr, nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Due to graceful stop, any started streams continue to work.
|
||||
if err = stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
|
||||
t.Fatalf("stream.Send() failed: %v, should continue to work due to graceful stop", err)
|
||||
}
|
||||
if err = stream.CloseSend(); err != nil {
|
||||
t.Fatalf("stream.CloseSend() failed: %v, should continue to work due to graceful stop", err)
|
||||
}
|
||||
if _, err = stream.Recv(); err != io.EOF {
|
||||
t.Fatalf("stream.Recv() failed with %v, want io.EOF", err)
|
||||
}
|
||||
|
||||
// New RPCs on that connection should eventually start failing.
|
||||
waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "", "")
|
||||
}
|
||||
|
||||
// Tests the serving mode functionality with multiple xDS enabled gRPC servers.
|
||||
func (s) TestServer_ServingModeChanges_MultipleServers(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer, nodeID, bootstrapContents, _ := setup.ManagementServerAndResolver(t)
|
||||
|
||||
// Create two local listeners and pass it to Serve().
|
||||
lis1, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
lis2, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a server option to get notified about serving mode changes.
|
||||
modeChangeHandler1 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt1 := xds.ServingModeCallback(modeChangeHandler1.modeChangeCallback)
|
||||
modeChangeHandler2 := newServingModeChangeHandler(t)
|
||||
modeChangeOpt2 := xds.ServingModeCallback(modeChangeHandler2.modeChangeCallback)
|
||||
|
||||
// Start two xDS-enabled gRPC servers with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
createStubServer(t, lis1, modeChangeOpt1, xds.ClientPoolForTesting(pool))
|
||||
createStubServer(t, lis2, modeChangeOpt2, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Setup the management server to respond with server-side Listener
|
||||
// resources for both listeners.
|
||||
host1, port1, err := hostPortFromListener(lis1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
listener1 := e2e.DefaultServerListener(host1, port1, e2e.SecurityLevelNone, "routeName")
|
||||
host2, port2, err := hostPortFromListener(lis2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
listener2 := e2e.DefaultServerListener(host2, port2, e2e.SecurityLevelNone, "routeName")
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener1, listener2},
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for both listeners to move to "serving" mode.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler1.modeCh:
|
||||
if mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Listener 1 received new mode %v, want %v", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler2.modeCh:
|
||||
if mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Listener 2 received new mode %v, want %v", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ClientConn to the first listener and make a successful RPCs.
|
||||
cc1, err := grpc.NewClient(lis1.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
defer cc1.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc1)
|
||||
|
||||
// Create a ClientConn to the second listener and make a successful RPCs.
|
||||
cc2, err := grpc.NewClient(lis2.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
defer cc2.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc2)
|
||||
|
||||
// Update the management server to remove the second listener resource. This
|
||||
// should push only the second listener into "not-serving" mode.
|
||||
if err := managementServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener1},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for lis2 to move to "not-serving" mode.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler2.modeCh:
|
||||
if mode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Listener received new mode %v, want %v", mode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
gotErr := <-modeChangeHandler2.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), nodeID) {
|
||||
t.Fatalf("Unexpected error: %v, want xDS Node id: %s", gotErr, nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure RPCs succeed on cc1 and fail on cc2.
|
||||
waitForSuccessfulRPC(ctx, t, cc1)
|
||||
waitForFailedRPCWithStatus(ctx, t, cc2, codes.Unavailable, "", "")
|
||||
|
||||
// Update the management server to remove the first listener resource as
|
||||
// well. This should push the first listener into "not-serving" mode. Second
|
||||
// listener is already in "not-serving" mode.
|
||||
if err := managementServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for lis1 to move to "not-serving" mode. lis2 was already removed
|
||||
// from the xdsclient's resource cache. So, lis2's callback will not be
|
||||
// invoked this time around.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler1.modeCh:
|
||||
if mode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Listener received new mode %v, want %v", mode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
gotErr := <-modeChangeHandler1.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), nodeID) {
|
||||
t.Fatalf("Unexpected error: %v, want xDS Node id: %s", gotErr, nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure RPCs fail on both.
|
||||
waitForFailedRPCWithStatus(ctx, t, cc1, codes.Unavailable, "", "")
|
||||
waitForFailedRPCWithStatus(ctx, t, cc2, codes.Unavailable, "", "")
|
||||
|
||||
// Make sure new connection attempts to "not-serving" servers fail.
|
||||
if cc1, err = grpc.NewClient(lis1.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
|
||||
t.Fatal("Failed to create clientConn to a server in \"not-serving\" state")
|
||||
}
|
||||
defer cc1.Close()
|
||||
if _, err := testgrpc.NewTestServiceClient(cc1).FullDuplexCall(ctx); status.Code(err) != codes.Unavailable {
|
||||
t.Fatalf("FullDuplexCall failed with status code: %v, want: Unavailable", status.Code(err))
|
||||
}
|
||||
|
||||
// Update the management server with both listener resources.
|
||||
if err := managementServer.Update(ctx, e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener1, listener2},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for both listeners to move to "serving" mode.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler1.modeCh:
|
||||
if mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Listener received new mode %v, want %v", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for a mode change update: %v", err)
|
||||
case mode := <-modeChangeHandler2.modeCh:
|
||||
if mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Listener received new mode %v, want %v", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// The clientConns created earlier should be able to make RPCs now.
|
||||
waitForSuccessfulRPC(ctx, t, cc1)
|
||||
waitForSuccessfulRPC(ctx, t, cc2)
|
||||
}
|
||||
|
||||
// Tests the case where the server receives a client-side listener resource.
|
||||
// The server should transition to NotServing mode and reject incoming connections.
|
||||
func (s) TestServer_ClientSideListenerReceivedOnServer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure a client-side listener resource on the management server.
|
||||
// We use the server-side listening address as the resource name, but return a
|
||||
// client-side listener resource (meaning it has ApiListener and lacks inbound TCP config).
|
||||
listenerName := fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port))))
|
||||
listener := e2e.DefaultClientListener(listenerName, "routeName")
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Ensure the server transitions to NOT_SERVING mode due to client-side listener being sent.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go NOT_SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
const wantErr = "received client-side listener resource"
|
||||
gotErr := <-modeChangeHandler.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), wantErr) {
|
||||
t.Fatalf("Unexpected error: %v, want error containing '%s'", gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the management server with the expected server-side listener and route config.
|
||||
// This should cause the server to recover and transition back to SERVING mode.
|
||||
listenerServer := e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
routeConfig := e2e.RouteConfigNonForwardingAction("routeName")
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listenerServer},
|
||||
Routes: []*v3routepb.RouteConfiguration{routeConfig},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a gRPC client to the server and verify that we can make a successful RPC.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
}
|
||||
|
||||
// Tests the case where the server receives a listener resource with a mismatched port/address.
|
||||
// The server should transition to NotServing mode and reject incoming connections.
|
||||
func (s) TestServer_MismatchedAddressListenerReceivedOnServer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure a server-side listener with a mismatched port on the management server.
|
||||
listener := e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
listener.Address.GetSocketAddress().PortSpecifier = &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: port + 1,
|
||||
}
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Ensure the server transitions to NOT_SERVING mode due to the address/port mismatch.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go NOT_SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
const wantErr = "does not match listening address"
|
||||
gotErr := <-modeChangeHandler.errCh
|
||||
if gotErr == nil || !strings.Contains(gotErr.Error(), wantErr) {
|
||||
t.Fatalf("Unexpected error: %v, want error containing '%s'", gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the management server with the expected matching server-side listener and route config.
|
||||
// This should cause the server to recover and transition back to SERVING mode.
|
||||
listenerServer := e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
routeConfig := e2e.RouteConfigNonForwardingAction("routeName")
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listenerServer},
|
||||
Routes: []*v3routepb.RouteConfiguration{routeConfig},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a gRPC client to the server and verify that we can make a successful RPC.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
}
|
||||
|
||||
// Tests the case where the server receives a listener resource with a wildcard
|
||||
// port. The server should match the listener address and transition to Serving
|
||||
// mode regardless of the port on which it is listening.
|
||||
func (s) TestServer_ListenerMatchWithWildcardPort(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
// Create a listener on a local port to act as the xDS enabled gRPC server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
host, port, err := hostPortFromListener(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve host and port of server: %v", err)
|
||||
}
|
||||
|
||||
// Configure a server-side listener with a wildcard port on the management
|
||||
// server. Construct the listener with the actual port first so its resource
|
||||
// name matches the server's default watch name, then replace only the port
|
||||
// value in the socket address with zero.
|
||||
listener := e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")
|
||||
listener.Address.GetSocketAddress().PortSpecifier = &v3corepb.SocketAddress_PortValue{
|
||||
PortValue: 0,
|
||||
}
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Start an xDS-enabled gRPC server with the above bootstrap configuration.
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
modeChangeHandler := newServingModeChangeHandler(t)
|
||||
modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
|
||||
createStubServer(t, lis, modeChangeOpt, xds.ClientPoolForTesting(pool))
|
||||
|
||||
// Ensure the server transitions to SERVING mode despite the wildcard port.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
|
||||
case gotMode := <-modeChangeHandler.modeCh:
|
||||
if gotMode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Mode changed to %v, want %v", gotMode, connectivity.ServingModeServing)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a gRPC client to the server and verify that we can make a
|
||||
// successful RPC.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
waitForSuccessfulRPC(ctx, t, cc)
|
||||
}
|
||||
814
xds/server_test.go
Normal file
814
xds/server_test.go
Normal file
@@ -0,0 +1,814 @@
|
||||
/*
|
||||
*
|
||||
* 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 xds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/credentials/xds"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
internalserver "google.golang.org/grpc/internal/xds/server"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 5 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond
|
||||
nonExistentManagementServer = "non-existent-management-server"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
type fakeGRPCServer struct {
|
||||
done chan struct{}
|
||||
registerServiceCh *testutils.Channel
|
||||
serveCh *testutils.Channel
|
||||
}
|
||||
|
||||
func (f *fakeGRPCServer) RegisterService(*grpc.ServiceDesc, any) {
|
||||
f.registerServiceCh.Send(nil)
|
||||
}
|
||||
|
||||
func (f *fakeGRPCServer) Serve(lis net.Listener) error {
|
||||
f.serveCh.Send(nil)
|
||||
<-f.done
|
||||
lis.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGRPCServer) Stop() {
|
||||
close(f.done)
|
||||
}
|
||||
func (f *fakeGRPCServer) GracefulStop() {
|
||||
close(f.done)
|
||||
}
|
||||
|
||||
func (f *fakeGRPCServer) GetServiceInfo() map[string]grpc.ServiceInfo {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func newFakeGRPCServer() *fakeGRPCServer {
|
||||
return &fakeGRPCServer{
|
||||
done: make(chan struct{}),
|
||||
registerServiceCh: testutils.NewChannel(),
|
||||
serveCh: testutils.NewChannel(),
|
||||
}
|
||||
}
|
||||
|
||||
func generateBootstrapContents(t *testing.T, nodeID, serverURI string) []byte {
|
||||
bs := e2e.DefaultBootstrapContents(t, nodeID, serverURI)
|
||||
return bs
|
||||
}
|
||||
|
||||
func (s) TestNewServer_Success(t *testing.T) {
|
||||
xdsCreds, err := xds.NewServerCredentials(xds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create xds server credentials: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
serverOpts []grpc.ServerOption
|
||||
wantXDSCredsInUse bool
|
||||
}{
|
||||
{
|
||||
desc: "without_xds_creds",
|
||||
serverOpts: []grpc.ServerOption{
|
||||
grpc.Creds(insecure.NewCredentials()),
|
||||
BootstrapContentsForTesting(generateBootstrapContents(t, uuid.NewString(), nonExistentManagementServer)),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with_xds_creds",
|
||||
serverOpts: []grpc.ServerOption{
|
||||
grpc.Creds(xdsCreds),
|
||||
BootstrapContentsForTesting(generateBootstrapContents(t, uuid.NewString(), nonExistentManagementServer)),
|
||||
},
|
||||
wantXDSCredsInUse: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// The xds package adds 1 server option (stream wrapper) to the server
|
||||
// options passed in by the user.
|
||||
wantServerOpts := len(test.serverOpts) + 1
|
||||
|
||||
origNewGRPCServer := newGRPCServer
|
||||
newGRPCServer = func(opts ...grpc.ServerOption) grpcServer {
|
||||
if got := len(opts); got != wantServerOpts {
|
||||
t.Fatalf("%d ServerOptions passed to grpc.Server, want %d", got, wantServerOpts)
|
||||
}
|
||||
// Verify that the user passed ServerOptions are forwarded as is.
|
||||
if diff := cmp.Diff(test.serverOpts, opts[1:], cmp.Comparer(func(a, b grpc.ServerOption) bool { return a == b })); diff != "" {
|
||||
t.Fatalf("ServerOptions mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
return grpc.NewServer(opts...)
|
||||
}
|
||||
defer func() {
|
||||
newGRPCServer = origNewGRPCServer
|
||||
}()
|
||||
|
||||
s, err := NewGRPCServer(test.serverOpts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
defer s.Stop()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestNewServer_Failure(t *testing.T) {
|
||||
xdsCreds, err := xds.NewServerCredentials(xds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create xds server credentials: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
serverOpts []grpc.ServerOption
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap env var not set",
|
||||
serverOpts: []grpc.ServerOption{grpc.Creds(xdsCreds), BootstrapContentsForTesting(nil)},
|
||||
wantErr: "failed to read xDS bootstrap config from env vars",
|
||||
},
|
||||
{
|
||||
desc: "empty bootstrap config",
|
||||
serverOpts: []grpc.ServerOption{
|
||||
grpc.Creds(xdsCreds),
|
||||
BootstrapContentsForTesting(nil),
|
||||
},
|
||||
wantErr: "xDS client creation failed",
|
||||
},
|
||||
{
|
||||
desc: "server_listener_resource_name_template is missing",
|
||||
serverOpts: []grpc.ServerOption{
|
||||
grpc.Creds(xdsCreds),
|
||||
func() grpc.ServerOption {
|
||||
bs, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(fmt.Sprintf(`[{
|
||||
"server_uri": %q,
|
||||
"channel_creds": [{"type": "insecure"}]
|
||||
}]`, nonExistentManagementServer)),
|
||||
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, uuid.New().String())),
|
||||
CertificateProviders: map[string]json.RawMessage{
|
||||
"cert-provider-instance": json.RawMessage("{}"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap configuration: %v", err)
|
||||
}
|
||||
return BootstrapContentsForTesting(bs)
|
||||
}(),
|
||||
},
|
||||
wantErr: "missing server_listener_resource_name_template in the bootstrap configuration",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
s, err := NewGRPCServer(test.serverOpts...)
|
||||
if err == nil {
|
||||
s.Stop()
|
||||
t.Fatal("NewGRPCServer() succeeded when expected to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("NewGRPCServer() failed with error: %v, want: %s", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer_OverrideListenerResourceNameOverridesMissingTemplate verifies
|
||||
// that an internal listener resource name override takes precedence over
|
||||
// server_listener_resource_name_template from bootstrap, including when the
|
||||
// template is absent, and that its returned name is used for the LDS watch.
|
||||
func (s) TestServer_OverrideListenerResourceNameOverridesMissingTemplate(t *testing.T) {
|
||||
const wantResourceName = "xdstp://foo/bar"
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ldsResourceNamesCh := make(chan []string, 1)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() == version.V3ListenerURL {
|
||||
select {
|
||||
case ldsResourceNamesCh <- req.GetResourceNames():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
bs, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(fmt.Sprintf(`[{
|
||||
"server_uri": %q,
|
||||
"channel_creds": [{"type": "insecure"}]
|
||||
}]`, mgmtServer.Address)),
|
||||
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, uuid.New().String())),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap configuration: %v", err)
|
||||
}
|
||||
|
||||
fs := newFakeGRPCServer()
|
||||
origNewGRPCServer := newGRPCServer
|
||||
newGRPCServer = func(...grpc.ServerOption) grpcServer { return fs }
|
||||
defer func() { newGRPCServer = origNewGRPCServer }()
|
||||
|
||||
lisAddrCh := make(chan net.Addr, 1)
|
||||
resourceNameOpt := internalserver.OverrideListenerResourceName(func(addr net.Addr) string {
|
||||
lisAddrCh <- addr
|
||||
return wantResourceName
|
||||
})
|
||||
srv, err := NewGRPCServer(resourceNameOpt, BootstrapContentsForTesting(bs))
|
||||
if err != nil {
|
||||
t.Fatalf("NewGRPCServer() failed: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
|
||||
// Verify that OverrideListenerResourceName receives the listener address.
|
||||
select {
|
||||
case gotAddr := <-lisAddrCh:
|
||||
if gotAddr.String() != lis.Addr().String() {
|
||||
t.Fatalf("OverrideListenerResourceName() called with address %q, want %q", gotAddr, lis.Addr())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for OverrideListenerResourceName to be called")
|
||||
}
|
||||
|
||||
// Verify that the LDS watch uses the resource name returned by the override.
|
||||
var gotResourceNames []string
|
||||
select {
|
||||
case gotResourceNames = <-ldsResourceNamesCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for an LDS request")
|
||||
}
|
||||
wantResourceNames := []string{wantResourceName}
|
||||
if !cmp.Equal(gotResourceNames, wantResourceNames) {
|
||||
t.Fatalf("LDS watch registered for names %v, want %v", gotResourceNames, wantResourceNames)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestRegisterService(t *testing.T) {
|
||||
fs := newFakeGRPCServer()
|
||||
|
||||
origNewGRPCServer := newGRPCServer
|
||||
newGRPCServer = func(...grpc.ServerOption) grpcServer { return fs }
|
||||
defer func() { newGRPCServer = origNewGRPCServer }()
|
||||
|
||||
s, err := NewGRPCServer(BootstrapContentsForTesting(generateBootstrapContents(t, uuid.NewString(), "non-existent-management-server")))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
defer s.Stop()
|
||||
|
||||
s.RegisterService(&grpc.ServiceDesc{}, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := fs.registerServiceCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timeout when expecting RegisterService() to called on grpc.Server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
fakeProvider1Name = "fake-certificate-provider-1"
|
||||
fakeProvider2Name = "fake-certificate-provider-2"
|
||||
)
|
||||
|
||||
var (
|
||||
fpb1, fpb2 *fakeProviderBuilder
|
||||
fakeProvider1Config json.RawMessage
|
||||
fakeProvider2Config json.RawMessage
|
||||
)
|
||||
|
||||
func init() {
|
||||
fpb1 = &fakeProviderBuilder{
|
||||
name: fakeProvider1Name,
|
||||
buildCh: testutils.NewChannel(),
|
||||
}
|
||||
fpb2 = &fakeProviderBuilder{
|
||||
name: fakeProvider2Name,
|
||||
buildCh: testutils.NewChannel(),
|
||||
}
|
||||
certprovider.Register(fpb1)
|
||||
certprovider.Register(fpb2)
|
||||
|
||||
fakeProvider1Config = json.RawMessage(fmt.Sprintf(`{
|
||||
"plugin_name": "%s",
|
||||
"config": "my fake config 1"
|
||||
}`, fakeProvider1Name))
|
||||
fakeProvider2Config = json.RawMessage(fmt.Sprintf(`{
|
||||
"plugin_name": "%s",
|
||||
"config": "my fake config 2"
|
||||
}`, fakeProvider2Name))
|
||||
}
|
||||
|
||||
// fakeProviderBuilder builds new instances of fakeProvider and interprets the
|
||||
// config provided to it as a string.
|
||||
type fakeProviderBuilder struct {
|
||||
name string
|
||||
buildCh *testutils.Channel
|
||||
}
|
||||
|
||||
func (b *fakeProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) {
|
||||
var config string
|
||||
if err := json.Unmarshal(cfg.(json.RawMessage), &config); err != nil {
|
||||
return nil, fmt.Errorf("providerBuilder %s failed to unmarshal config: %v", b.name, cfg)
|
||||
}
|
||||
return certprovider.NewBuildableConfig(b.name, []byte(config), func(certprovider.BuildOptions) certprovider.Provider {
|
||||
b.buildCh.Send(nil)
|
||||
return &fakeProvider{
|
||||
Distributor: certprovider.NewDistributor(),
|
||||
config: config,
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (b *fakeProviderBuilder) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// fakeProvider is an implementation of the Provider interface which provides a
|
||||
// method for tests to invoke to push new key materials.
|
||||
type fakeProvider struct {
|
||||
*certprovider.Distributor
|
||||
config string
|
||||
}
|
||||
|
||||
// Close helps implement the Provider interface.
|
||||
func (p *fakeProvider) Close() {
|
||||
p.Distributor.Stop()
|
||||
}
|
||||
|
||||
func verifyCertProviderNotCreated() error {
|
||||
sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := fpb1.buildCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
return errors.New("certificate provider created when no xDS creds were specified")
|
||||
}
|
||||
sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := fpb2.buildCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
return errors.New("certificate provider created when no xDS creds were specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostPortFromListener(t *testing.T, lis net.Listener) (string, uint32) {
|
||||
t.Helper()
|
||||
|
||||
host, p, err := net.SplitHostPort(lis.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("net.SplitHostPort(%s) failed: %v", lis.Addr().String(), err)
|
||||
}
|
||||
port, err := strconv.ParseInt(p, 10, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("strconv.ParseInt(%s, 10, 32) failed: %v", p, err)
|
||||
}
|
||||
return host, uint32(port)
|
||||
}
|
||||
|
||||
// TestServeSuccess tests the successful case of creating an xDS enabled gRPC
|
||||
// server and calling Serve() on it. The test verifies that an LDS request is
|
||||
// sent out for the expected name, and also verifies that the serving mode
|
||||
// changes appropriately.
|
||||
func (s) TestServeSuccess(t *testing.T) {
|
||||
// Setup an xDS management server that pushes on a channel when an LDS
|
||||
// request is received by it.
|
||||
ldsRequestCh := make(chan []string, 1)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() == version.V3ListenerURL {
|
||||
select {
|
||||
case ldsRequestCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
|
||||
// Override the function to create the underlying grpc.Server to allow the
|
||||
// test to verify that Serve() is called on the underlying server.
|
||||
fs := newFakeGRPCServer()
|
||||
origNewGRPCServer := newGRPCServer
|
||||
newGRPCServer = func(...grpc.ServerOption) grpcServer { return fs }
|
||||
defer func() { newGRPCServer = origNewGRPCServer }()
|
||||
|
||||
// Create a new xDS enabled gRPC server and pass it a server option to get
|
||||
// notified about serving mode changes.
|
||||
modeChangeCh := testutils.NewChannel()
|
||||
modeChangeOption := ServingModeCallback(func(addr net.Addr, args ServingModeChangeArgs) {
|
||||
t.Logf("Server mode change callback invoked for listener %q with mode %q and error %v", addr.String(), args.Mode, args.Err)
|
||||
modeChangeCh.Send(args.Mode)
|
||||
})
|
||||
server, err := NewGRPCServer(modeChangeOption, BootstrapContentsForTesting(bootstrapContents))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Call Serve() in a goroutine.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
go func() {
|
||||
if err := server.Serve(lis); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure that the LDS request is sent out for the expected name.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
var gotNames []string
|
||||
select {
|
||||
case gotNames = <-ldsRequestCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout when waiting for an LDS request to be sent out")
|
||||
}
|
||||
wantNames := []string{strings.ReplaceAll(e2e.ServerListenerResourceNameTemplate, "%s", lis.Addr().String())}
|
||||
if !cmp.Equal(gotNames, wantNames) {
|
||||
t.Fatalf("LDS watch registered for names %v, want %v", gotNames, wantNames)
|
||||
}
|
||||
|
||||
// Update the management server with a good listener resource.
|
||||
host, port := hostPortFromListener(t, lis)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the serving mode reports SERVING.
|
||||
v, err := modeChangeCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timeout when waiting for serving mode to change: %v", err)
|
||||
}
|
||||
if mode := v.(connectivity.ServingMode); mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Serving mode is %q, want %q", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
|
||||
// Verify that Serve() is called on the underlying gRPC server.
|
||||
if _, err := fs.serveCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timeout when waiting for Serve() to be invoked on the grpc.Server")
|
||||
}
|
||||
|
||||
// Update the listener resource on the management server in such a way that
|
||||
// it will be NACKed by our xDS client. The listener_filters field is
|
||||
// unsupported and will be NACKed.
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelNone, "routeName")},
|
||||
}
|
||||
resources.Listeners[0].ListenerFilters = []*v3listenerpb.ListenerFilter{{Name: "foo"}}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that there is no change in the serving mode. The server should
|
||||
// continue using the previously received good configuration.
|
||||
sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if v, err := modeChangeCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Fatalf("Unexpected change in serving mode. New mode is %v", v.(connectivity.ServingMode))
|
||||
}
|
||||
|
||||
// Remove the listener resource from the management server. This should
|
||||
// result in a resource-not-found error from the xDS client and should
|
||||
// result in the server moving to NOT_SERVING mode.
|
||||
resources.Listeners = nil
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v, err = modeChangeCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timeout when waiting for serving mode to change: %v", err)
|
||||
}
|
||||
if mode := v.(connectivity.ServingMode); mode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Serving mode is %q, want %q", mode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewServer_ClientCreationFailure tests the case where the xDS client
|
||||
// creation fails and verifies that the call to NewGRPCServer() fails.
|
||||
func (s) TestNewServer_ClientCreationFailure(t *testing.T) {
|
||||
origXDSClientPool := xdsClientPool
|
||||
xdsClientPool = xdsclient.NewPool(nil)
|
||||
defer func() { xdsClientPool = origXDSClientPool }()
|
||||
|
||||
if _, err := NewGRPCServer(); err == nil {
|
||||
t.Fatal("NewGRPCServer() succeeded when expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleListenerUpdate_NoXDSCreds tests the case where an xds-enabled gRPC
|
||||
// server is not configured with xDS credentials. Verifies that the security
|
||||
// config received as part of a Listener update is not acted upon.
|
||||
func (s) TestHandleListenerUpdate_NoXDSCreds(t *testing.T) {
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Generate bootstrap configuration pointing to the above management server
|
||||
// with certificate provider configuration pointing to fake certificate
|
||||
// providers.
|
||||
nodeID := uuid.NewString()
|
||||
bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(fmt.Sprintf(`[{
|
||||
"server_uri": %q,
|
||||
"channel_creds": [{"type": "insecure"}]
|
||||
}]`, mgmtServer.Address)),
|
||||
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
|
||||
CertificateProviders: map[string]json.RawMessage{
|
||||
e2e.ServerSideCertProviderInstance: fakeProvider1Config,
|
||||
e2e.ClientSideCertProviderInstance: fakeProvider2Config,
|
||||
},
|
||||
ServerListenerResourceNameTemplate: e2e.ServerListenerResourceNameTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap configuration: %v", err)
|
||||
}
|
||||
|
||||
// Create a new xDS enabled gRPC server and pass it a server option to get
|
||||
// notified about serving mode changes. Also pass the above bootstrap
|
||||
// configuration to be used during xDS client creation.
|
||||
modeChangeCh := testutils.NewChannel()
|
||||
modeChangeOption := ServingModeCallback(func(addr net.Addr, args ServingModeChangeArgs) {
|
||||
t.Logf("Server mode change callback invoked for listener %q with mode %q and error %v", addr.String(), args.Mode, args.Err)
|
||||
modeChangeCh.Send(args.Mode)
|
||||
})
|
||||
server, err := NewGRPCServer(modeChangeOption, BootstrapContentsForTesting(bootstrapContents))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Call Serve() in a goroutine.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
go func() {
|
||||
if err := server.Serve(lis); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Update the management server with a good listener resource that contains
|
||||
// security configuration.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
host, port := hostPortFromListener(t, lis)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelMTLS, "routeName")},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the serving mode reports SERVING.
|
||||
v, err := modeChangeCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timeout when waiting for serving mode to change: %v", err)
|
||||
}
|
||||
if mode := v.(connectivity.ServingMode); mode != connectivity.ServingModeServing {
|
||||
t.Fatalf("Serving mode is %q, want %q", mode, connectivity.ServingModeServing)
|
||||
}
|
||||
|
||||
// Make sure the security configuration is not acted upon.
|
||||
if err := verifyCertProviderNotCreated(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleListenerUpdate_ErrorUpdate tests the case where an xds-enabled gRPC
|
||||
// server is configured with xDS credentials, but receives a Listener update
|
||||
// with an error. Verifies that no certificate providers are created.
|
||||
func (s) TestHandleListenerUpdate_ErrorUpdate(t *testing.T) {
|
||||
// Setup an xDS management server that pushes on a channel when an LDS
|
||||
// request is received by it.
|
||||
ldsRequestCh := make(chan []string, 1)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() == version.V3ListenerURL {
|
||||
select {
|
||||
case ldsRequestCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// Generate bootstrap configuration pointing to the above management server
|
||||
// with certificate provider configuration pointing to fake certificate
|
||||
// providers.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: []byte(fmt.Sprintf(`[{
|
||||
"server_uri": %q,
|
||||
"channel_creds": [{"type": "insecure"}]
|
||||
}]`, mgmtServer.Address)),
|
||||
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
|
||||
CertificateProviders: map[string]json.RawMessage{
|
||||
e2e.ServerSideCertProviderInstance: fakeProvider1Config,
|
||||
e2e.ClientSideCertProviderInstance: fakeProvider2Config,
|
||||
},
|
||||
ServerListenerResourceNameTemplate: e2e.ServerListenerResourceNameTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bootstrap configuration: %v", err)
|
||||
}
|
||||
|
||||
// Create a new xDS enabled gRPC server and pass it a server option to get
|
||||
// notified about serving mode changes. Also pass the above bootstrap
|
||||
// configuration to be used during xDS client creation.
|
||||
modeChangeCh := testutils.NewChannel()
|
||||
modeChangeOption := ServingModeCallback(func(addr net.Addr, args ServingModeChangeArgs) {
|
||||
// The serving mode callback may be invoked multiple times as the xDS
|
||||
// client NACKs the updates and the management server responds back with
|
||||
// the same version of the LDS resource.
|
||||
t.Logf("Server mode change callback invoked for listener %q with mode %q and error %v", addr.String(), args.Mode, args.Err)
|
||||
modeChangeCh.Replace(args.Mode)
|
||||
})
|
||||
server, err := NewGRPCServer(modeChangeOption, BootstrapContentsForTesting(bootstrapContents))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Call Serve() in a goroutine.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
go server.Serve(lis)
|
||||
|
||||
// Update the listener resource on the management server in such a way that
|
||||
// it will be NACKed by our xDS client. The listener_filters field is
|
||||
// unsupported and will be NACKed.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
host, port := hostPortFromListener(t, lis)
|
||||
listener := e2e.DefaultServerListener(host, port, e2e.SecurityLevelMTLS, "routeName")
|
||||
listener.ListenerFilters = []*v3listenerpb.ListenerFilter{{Name: "foo"}}
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{listener},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure that the LDS request is sent out for the expected name.
|
||||
var gotNames []string
|
||||
select {
|
||||
case gotNames = <-ldsRequestCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout when waiting for an LDS request to be sent out")
|
||||
}
|
||||
wantNames := []string{strings.ReplaceAll(e2e.ServerListenerResourceNameTemplate, "%s", lis.Addr().String())}
|
||||
if !cmp.Equal(gotNames, wantNames) {
|
||||
t.Fatalf("LDS watch registered for names %v, want %v", gotNames, wantNames)
|
||||
}
|
||||
|
||||
// Make sure that no certificate providers are created.
|
||||
if err := verifyCertProviderNotCreated(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Also make sure that serving mode updates are received. The serving
|
||||
// mode changes to NOT_SERVING. This happens because watcher received a
|
||||
// resource error for the invalid resource from the server.
|
||||
mode, err := modeChangeCh.Receive(ctx)
|
||||
if err == context.DeadlineExceeded {
|
||||
t.Fatal("Serving mode did not change when expected to change")
|
||||
}
|
||||
if mode != connectivity.ServingModeNotServing {
|
||||
t.Fatalf("Serving mode = %q, want %q", mode, connectivity.ServingModeNotServing)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeReturnsErrorAfterClose tests that the xds Server returns
|
||||
// grpc.ErrServerStopped if Serve is called after Close on the server.
|
||||
func (s) TestServeReturnsErrorAfterClose(t *testing.T) {
|
||||
server, err := NewGRPCServer(BootstrapContentsForTesting(generateBootstrapContents(t, uuid.NewString(), nonExistentManagementServer)))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
server.Stop()
|
||||
err = server.Serve(lis)
|
||||
if err == nil || !strings.Contains(err.Error(), grpc.ErrServerStopped.Error()) {
|
||||
t.Fatalf("server erorred with wrong error, want: %v, got :%v", grpc.ErrServerStopped, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeAndCloseDoNotRace tests that Serve and Close on the xDS Server do
|
||||
// not race and leak the xDS Client. A leak would be found by the leak checker.
|
||||
func (s) TestServeAndCloseDoNotRace(t *testing.T) {
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
|
||||
// Generate bootstrap contents up front for all servers.
|
||||
bootstrapContents := generateBootstrapContents(t, uuid.NewString(), nonExistentManagementServer)
|
||||
|
||||
// Override the default ServingModeCallback with a noop function because the
|
||||
// serverURI is invalid which will result in xDS channel creation failure
|
||||
// while registering the watch for listener resource. This will trigger
|
||||
// resource error notifications for the invalid listener resource leading
|
||||
// to service mode change to "not serving" each time.
|
||||
//
|
||||
// Even if the server is currently NOT_SERVING and the new mode is also
|
||||
// NOT_SERVING, the update is not suppressed as:
|
||||
// 1. the error may have change
|
||||
// 2. it provides a timestamp of the last backoff attempt
|
||||
noopModeChangeCallback := func(_ net.Addr, _ ServingModeChangeArgs) {}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(200)
|
||||
for i := 0; i < 100; i++ {
|
||||
server, err := NewGRPCServer(BootstrapContentsForTesting(bootstrapContents), ServingModeCallback(noopModeChangeCallback))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create an xDS enabled gRPC server: %v", err)
|
||||
}
|
||||
go func() {
|
||||
server.Serve(lis)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
server.Stop()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
188
xds/test/eds_resource_missing_test.go
Normal file
188
xds/test/eds_resource_missing_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package xds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"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"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
|
||||
_ "google.golang.org/grpc/xds" // To register the xDS resolver and LB policies.
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestWatchExpiryTimeout = 500 * time.Millisecond
|
||||
defaultTestTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
// Test verifies the xDS-enabled gRPC channel's behavior when the management
|
||||
// server fails to send an EDS resource referenced by a Cluster resource. The
|
||||
// expected outcome is an RPC failure with a status code Unavailable and a
|
||||
// message indicating the absence of available targets.
|
||||
func (s) TestEDS_MissingResource(t *testing.T) {
|
||||
// Start an xDS management server.
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
config, err := bootstrap.NewConfigFromContents(bc)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err)
|
||||
}
|
||||
|
||||
// Create an xDS client with a short resource expiry timer.
|
||||
pool := xdsclient.NewPool(config)
|
||||
xdsC, xdsClose, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: t.Name(),
|
||||
WatchExpiryTimeout: defaultTestWatchExpiryTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer xdsClose()
|
||||
|
||||
// Create an xDS resolver for the test that uses the above xDS client.
|
||||
resolverBuilder := internal.NewXDSResolverWithClientForTesting.(func(xdsclient.XDSClient) (resolver.Builder, error))
|
||||
xdsResolver, err := resolverBuilder(xdsC)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
|
||||
}
|
||||
|
||||
// Create resources on the management server. No EDS resource is configured.
|
||||
const serviceName = "my-service-client-side-xds"
|
||||
const routeConfigName = "route-" + serviceName
|
||||
const clusterName = "cluster-" + serviceName
|
||||
const endpointsName = "endpoints-" + serviceName
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
SkipValidation: true, // Cluster resource refers to an EDS resource that is not configured.
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, endpointsName, e2e.SecurityLevelNone)},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a ClientConn with the xds:/// scheme.
|
||||
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a grpc channel: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Make an RPC and verify that it fails with the expected error.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err = client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if err == nil {
|
||||
t.Fatal("EmptyCall() succeeded, want failure")
|
||||
}
|
||||
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
|
||||
t.Errorf("EmptyCall() failed with code = %v, want %s", gotCode, wantCode)
|
||||
}
|
||||
if gotMsg, wantMsg := err.Error(), "no targets to pick from"; !strings.Contains(gotMsg, wantMsg) {
|
||||
t.Errorf("EmptyCall() failed with message = %q, want to contain %q", gotMsg, wantMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies the xDS-enabled gRPC channel's behavior when the management
|
||||
// server sends an EDS resource with no endpoints. The expected outcome is an
|
||||
// RPC failure with a status code Unavailable and a message indicating the
|
||||
// absence of available targets.
|
||||
func (s) TestEDS_NoEndpointsInResource(t *testing.T) {
|
||||
managementServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
||||
|
||||
// Create resources on the management server, with the EDS resource
|
||||
// containing no endpoints.
|
||||
const serviceName = "my-service-client-side-xds"
|
||||
const routeConfigName = "route-" + serviceName
|
||||
const clusterName = "cluster-" + serviceName
|
||||
const endpointsName = "endpoints-" + serviceName
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
SkipValidation: true, // Cluster resource refers to an EDS resource that is not configured.
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, endpointsName, e2e.SecurityLevelNone)},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{
|
||||
e2e.EndpointResourceWithOptions(e2e.EndpointOptions{
|
||||
ClusterName: "endpoints-" + serviceName,
|
||||
Host: "localhost",
|
||||
}),
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a ClientConn with the xds:/// scheme.
|
||||
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a grpc channel: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Make an RPC and verify that it fails with the expected error.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err = client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if err == nil {
|
||||
t.Fatal("EmptyCall() succeeded, want failure")
|
||||
}
|
||||
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
|
||||
t.Errorf("EmptyCall() failed with code = %v, want %s", gotCode, wantCode)
|
||||
}
|
||||
if gotMsg, wantMsg := err.Error(), "no targets to pick from"; !strings.Contains(gotMsg, wantMsg) {
|
||||
t.Errorf("EmptyCall() failed with message = %q, want to contain %q", gotMsg, wantMsg)
|
||||
}
|
||||
}
|
||||
100
xds/xds.go
Normal file
100
xds/xds.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package xds contains an implementation of the xDS suite of protocols, to be
|
||||
// used by gRPC client and server applications.
|
||||
//
|
||||
// On the client-side, users simply need to import this package to get all xDS
|
||||
// functionality. On the server-side, users need to use the GRPCServer type
|
||||
// exported by this package instead of the regular grpc.Server.
|
||||
//
|
||||
// See https://github.com/grpc/grpc-go/tree/master/examples/features/xds for
|
||||
// example.
|
||||
package xds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal"
|
||||
internaladmin "google.golang.org/grpc/internal/admin"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/xds/csds"
|
||||
|
||||
_ "google.golang.org/grpc/credentials/tls/certprovider/pemfile" // Register the file watcher certificate provider plugin.
|
||||
_ "google.golang.org/grpc/internal/xds/balancer" // Register the balancers.
|
||||
_ "google.golang.org/grpc/internal/xds/clusterspecifier/rls" // Register the RLS cluster specifier plugin. Note that this does not register the RLS LB policy.
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/extproc" // Register the extproc filter.
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/fault" // Register the fault injection filter.
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/rbac" // Register the RBAC filter.
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter.
|
||||
_ "google.golang.org/grpc/internal/xds/resolver" // Register the xds_resolver.
|
||||
_ "google.golang.org/grpc/internal/xds/xdsclient/xdslbregistry/converter" // Register the xDS LB Registry Converters.
|
||||
|
||||
v3statusgrpc "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
func init() {
|
||||
internaladmin.AddService(func(registrar grpc.ServiceRegistrar) (func(), error) {
|
||||
var grpcServer *grpc.Server
|
||||
switch ss := registrar.(type) {
|
||||
case *grpc.Server:
|
||||
grpcServer = ss
|
||||
case *GRPCServer:
|
||||
sss, ok := ss.gs.(*grpc.Server)
|
||||
if !ok {
|
||||
logger.Warning("grpc server within xds.GRPCServer is not *grpc.Server, CSDS will not be registered")
|
||||
return nil, nil
|
||||
}
|
||||
grpcServer = sss
|
||||
default:
|
||||
// Returning an error would cause the top level admin.Register() to
|
||||
// fail. Log a warning instead.
|
||||
logger.Error("Server to register service on is neither a *grpc.Server or a *xds.GRPCServer, CSDS will not be registered")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
csdss, err := csds.NewClientStatusDiscoveryServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create csds server: %v", err)
|
||||
}
|
||||
v3statusgrpc.RegisterClientStatusDiscoveryServiceServer(grpcServer, csdss)
|
||||
return csdss.Close, nil
|
||||
})
|
||||
}
|
||||
|
||||
// NewXDSResolverWithConfigForTesting creates a new xDS resolver builder using
|
||||
// the provided xDS bootstrap config instead of the global configuration from
|
||||
// the supported environment variables. The resolver.Builder is meant to be
|
||||
// used in conjunction with the grpc.WithResolvers DialOption.
|
||||
//
|
||||
// # Testing Only
|
||||
//
|
||||
// This function should ONLY be used for testing and may not work with some
|
||||
// other features, including the CSDS service.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func NewXDSResolverWithConfigForTesting(bootstrapConfig []byte) (resolver.Builder, error) {
|
||||
return internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapConfig)
|
||||
}
|
||||
Reference in New Issue
Block a user