snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
39
internal/xds/clients/grpctransport/examples_test.go
Normal file
39
internal/xds/clients/grpctransport/examples_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
*
|
||||
* 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 grpctransport_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/internal/xds/clients/grpctransport"
|
||||
)
|
||||
|
||||
// ExampleServerIdentifierExtension demonstrates how to create
|
||||
// clients.ServerIdentifier with grpctransport.ServerIdentifierExtension as
|
||||
// its extensions.
|
||||
//
|
||||
// This example is creating clients.ServerIdentifier to connect to server at
|
||||
// localhost:5678 using the config named "local". Note that "local" must
|
||||
// exist as an entry in the provided configs to grpctransport.Builder.
|
||||
func ExampleServerIdentifierExtension() {
|
||||
// Note the Extensions field is set by value and not by pointer.
|
||||
fmt.Printf("%+v", clients.ServerIdentifier{ServerURI: "localhost:5678", Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "local"}})
|
||||
// Output: {ServerURI:localhost:5678 Extensions:{ConfigName:local}}
|
||||
}
|
||||
237
internal/xds/clients/grpctransport/grpc_transport.go
Normal file
237
internal/xds/clients/grpctransport/grpc_transport.go
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
*
|
||||
* 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 grpctransport provides an implementation of the
|
||||
// clients.TransportBuilder interface using gRPC.
|
||||
package grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = grpclog.Component("grpctransport")
|
||||
)
|
||||
|
||||
// ServerIdentifierExtension holds settings for connecting to a gRPC server,
|
||||
// such as an xDS management or an LRS server.
|
||||
//
|
||||
// It must be set by value (not pointer) in the
|
||||
// clients.ServerIdentifier.Extensions field (See Example).
|
||||
type ServerIdentifierExtension struct {
|
||||
// ConfigName is the name of the configuration to use for this transport.
|
||||
// It must be present as a key in the map of configs passed to NewBuilder.
|
||||
ConfigName string
|
||||
}
|
||||
|
||||
// Builder creates gRPC-based Transports. It must be paired with ServerIdentifiers
|
||||
// that contain an Extension field of type ServerIdentifierExtension.
|
||||
type Builder struct {
|
||||
// configs is a map of configuration names to their respective Config.
|
||||
configs map[string]Config
|
||||
|
||||
mu sync.Mutex
|
||||
// connections is a map of clients.ServerIdentifiers in use by the Builder
|
||||
// to connect to different servers.
|
||||
connections map[clients.ServerIdentifier]*grpc.ClientConn
|
||||
// refs tracks the number of active references to each connection.
|
||||
refs map[clients.ServerIdentifier]int
|
||||
}
|
||||
|
||||
// Config defines the configuration for connecting to a gRPC server, including
|
||||
// credentials and an optional custom new client function.
|
||||
type Config struct {
|
||||
// Credentials is the credentials bundle to be used for the connection.
|
||||
Credentials credentials.Bundle
|
||||
// GRPCNewClient is an optional custom function to establish a gRPC connection.
|
||||
// If nil, grpc.NewClient will be used as the default.
|
||||
GRPCNewClient func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)
|
||||
}
|
||||
|
||||
// NewBuilder provides a builder for creating gRPC-based Transports using
|
||||
// the credentials from provided map of credentials names to
|
||||
// credentials.Bundle.
|
||||
func NewBuilder(configs map[string]Config) *Builder {
|
||||
return &Builder{
|
||||
configs: configs,
|
||||
connections: make(map[clients.ServerIdentifier]*grpc.ClientConn),
|
||||
refs: make(map[clients.ServerIdentifier]int),
|
||||
}
|
||||
}
|
||||
|
||||
// Build returns a gRPC-based clients.Transport.
|
||||
//
|
||||
// The Extension field of the ServerIdentifier must be a ServerIdentifierExtension.
|
||||
func (b *Builder) Build(si clients.ServerIdentifier) (clients.Transport, error) {
|
||||
if si.ServerURI == "" {
|
||||
return nil, fmt.Errorf("grpctransport: ServerURI is not set in ServerIdentifier")
|
||||
}
|
||||
if si.Extensions == nil {
|
||||
return nil, fmt.Errorf("grpctransport: Extensions is not set in ServerIdentifier")
|
||||
}
|
||||
sce, ok := si.Extensions.(ServerIdentifierExtension)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("grpctransport: Extensions field is %T, but must be %T in ServerIdentifier", si.Extensions, ServerIdentifierExtension{})
|
||||
}
|
||||
|
||||
config, ok := b.configs[sce.ConfigName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("grpctransport: unknown config name %q specified in ServerIdentifierExtension", sce.ConfigName)
|
||||
}
|
||||
if config.Credentials == nil {
|
||||
return nil, fmt.Errorf("grpctransport: config %q has nil credentials bundle", sce.ConfigName)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if cc, ok := b.connections[si]; ok {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Reusing existing connection to the server for ServerIdentifier: %v", si)
|
||||
}
|
||||
b.refs[si]++
|
||||
tr := &grpcTransport{cc: cc}
|
||||
tr.cleanup = b.cleanupFunc(si, tr)
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
// Create a new gRPC client/channel for the server with the provided
|
||||
// credentials, server URI, and a byte codec to send and receive messages.
|
||||
// Also set a static keepalive configuration that is common across gRPC
|
||||
// language implementations.
|
||||
kpCfg := grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 5 * time.Minute,
|
||||
Timeout: 20 * time.Second,
|
||||
})
|
||||
dopts := []grpc.DialOption{kpCfg, grpc.WithCredentialsBundle(config.Credentials), grpc.WithDefaultCallOptions(grpc.ForceCodec(&byteCodec{}))}
|
||||
newClientFunc := grpc.NewClient
|
||||
if config.GRPCNewClient != nil {
|
||||
newClientFunc = config.GRPCNewClient
|
||||
}
|
||||
cc, err := newClientFunc(si.ServerURI, dopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpctransport: failed to create connection to server %q: %v", si.ServerURI, err)
|
||||
}
|
||||
tr := &grpcTransport{cc: cc}
|
||||
// Register a cleanup function that decrements the refs to the gRPC
|
||||
// transport each time Close() is called to close it and remove from
|
||||
// transports and connections map if last reference is being released.
|
||||
tr.cleanup = b.cleanupFunc(si, tr)
|
||||
|
||||
// Add the newly created connection to the maps to re-use the transport
|
||||
// channel and track references.
|
||||
b.connections[si] = cc
|
||||
b.refs[si] = 1
|
||||
|
||||
if logger.V(2) {
|
||||
logger.Infof("Created a new transport to the server for ServerIdentifier: %v", si)
|
||||
}
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
func (b *Builder) cleanupFunc(si clients.ServerIdentifier, tr *grpcTransport) func() {
|
||||
return sync.OnceFunc(func() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.refs[si]--
|
||||
if b.refs[si] != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tr.cc.Close()
|
||||
tr.cc = nil
|
||||
delete(b.connections, si)
|
||||
delete(b.refs, si)
|
||||
})
|
||||
}
|
||||
|
||||
type grpcTransport struct {
|
||||
cc *grpc.ClientConn
|
||||
|
||||
// cleanup is the function to be invoked for releasing the references to
|
||||
// the gRPC transport each time Close() is called.
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// NewStream creates a new gRPC stream to the server for the specified method.
|
||||
func (g *grpcTransport) NewStream(ctx context.Context, method string) (clients.Stream, error) {
|
||||
s, err := g.cc.NewStream(ctx, &grpc.StreamDesc{ClientStreams: true, ServerStreams: true}, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stream{stream: s}, nil
|
||||
}
|
||||
|
||||
// Close closes the gRPC channel to the server.
|
||||
func (g *grpcTransport) Close() {
|
||||
g.cleanup()
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
stream grpc.ClientStream
|
||||
}
|
||||
|
||||
// Send sends a message to the server.
|
||||
func (s *stream) Send(msg []byte) error {
|
||||
return s.stream.SendMsg(msg)
|
||||
}
|
||||
|
||||
// Recv receives a message from the server.
|
||||
func (s *stream) Recv() ([]byte, error) {
|
||||
var typedRes []byte
|
||||
|
||||
if err := s.stream.RecvMsg(&typedRes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return typedRes, nil
|
||||
}
|
||||
|
||||
// byteCodec here is still sending proto messages. It's just they are
|
||||
// in []byte form.
|
||||
type byteCodec struct{}
|
||||
|
||||
func (c *byteCodec) Marshal(v any) ([]byte, error) {
|
||||
if b, ok := v.([]byte); ok {
|
||||
return b, nil
|
||||
}
|
||||
return nil, fmt.Errorf("grpctransport: message is %T, but must be a []byte", v)
|
||||
}
|
||||
|
||||
func (c *byteCodec) Unmarshal(data []byte, v any) error {
|
||||
if b, ok := v.(*[]byte); ok {
|
||||
*b = data
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("grpctransport: target is %T, but must be *[]byte", v)
|
||||
}
|
||||
|
||||
func (c *byteCodec) Name() string {
|
||||
// Return "" to ensure the Content-Type header is "application/grpc",
|
||||
// which is expected by standard gRPC servers for protobuf messages.
|
||||
return ""
|
||||
}
|
||||
316
internal/xds/clients/grpctransport/grpc_transport_ext_test.go
Normal file
316
internal/xds/clients/grpctransport/grpc_transport_ext_test.go
Normal file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
*
|
||||
* 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 grpctransport_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/credentials/local"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/internal/xds/clients/grpctransport"
|
||||
"google.golang.org/grpc/internal/xds/clients/internal/testutils"
|
||||
"google.golang.org/grpc/internal/xds/clients/internal/testutils/e2e"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen.
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
type testCredentials struct {
|
||||
credentials.Bundle
|
||||
transportCredentials credentials.TransportCredentials
|
||||
}
|
||||
|
||||
func (tc *testCredentials) TransportCredentials() credentials.TransportCredentials {
|
||||
return tc.transportCredentials
|
||||
}
|
||||
func (tc *testCredentials) PerRPCCredentials() credentials.PerRPCCredentials {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestBuild_Single tests that multiple calls to Build() with the same
|
||||
// clients.ServerIdentifier returns the same transport. Also verifies that
|
||||
// only when all references to the newly created transport are released,
|
||||
// the underlying transport is closed.
|
||||
func (s) TestBuild_Single(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
lis := testutils.NewListenerWrapper(t, nil)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis})
|
||||
|
||||
serverID := clients.ServerIdentifier{
|
||||
ServerURI: mgmtServer.Address,
|
||||
Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
configs := map[string]grpctransport.Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
}
|
||||
|
||||
// Calling Build() first time should create new gRPC transport.
|
||||
builder := grpctransport.NewBuilder(configs)
|
||||
tr, err := builder.Build(serverID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
// Create a new stream to the server and verify that a new transport is
|
||||
// created.
|
||||
if _, err = tr.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
val, err := lis.NewConnCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timed out when waiting for a new transport to be created to the management server: %v", err)
|
||||
}
|
||||
conn := val.(*testutils.ConnWrapper)
|
||||
|
||||
// Calling Build() again should not create new gRPC transport.
|
||||
const count = 9
|
||||
transports := make([]clients.Transport, count)
|
||||
for i := 0; i < count; i++ {
|
||||
func() {
|
||||
transports[i], err = builder.Build(serverID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
// Create a new stream to the server and verify that no connection
|
||||
// is established to the management server at this point. A new
|
||||
// transport is created only when an existing connection for
|
||||
// serverID does not exist.
|
||||
if _, err = tr.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := lis.NewConnCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Fatal("Unexpected new transport created to management server")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Call Close() multiple times on each of the transport received in the
|
||||
// above for loop. Close() calls are idempotent. The underlying gRPC
|
||||
// transport is removed after the Close() call but calling close second
|
||||
// time should not panic and underlying gRPC transport should not be
|
||||
// closed.
|
||||
for i := 0; i < count; i++ {
|
||||
func() {
|
||||
transports[i].Close()
|
||||
transports[i].Close()
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := conn.CloseCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Fatal("Unexpected transport closure to management server")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Call the last Close(). The underlying gRPC transport should be closed
|
||||
// because calls in the above for loop have released all references.
|
||||
tr.Close()
|
||||
if _, err := conn.CloseCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout when waiting for connection to management server to be closed")
|
||||
}
|
||||
|
||||
// Calling Build() again, after the previous transport was actually closed,
|
||||
// should create a new one.
|
||||
tr2, err := builder.Build(serverID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS client: %v", err)
|
||||
}
|
||||
defer tr2.Close()
|
||||
// Create a new stream to the server and verify that a new transport is
|
||||
// created.
|
||||
if _, err = tr2.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
if _, err := lis.NewConnCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timed out when waiting for a new transport to be created to the management server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuild_Multiple tests the scenario where there are multiple calls to
|
||||
// Build() with different clients.ServerIdentifier. Verifies that reference
|
||||
// counts are tracked correctly for each transport and that only when all
|
||||
// references are released for a transport, it is closed.
|
||||
func (s) TestBuild_Multiple(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
lis := testutils.NewListenerWrapper(t, nil)
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis})
|
||||
|
||||
serverID1 := clients.ServerIdentifier{
|
||||
ServerURI: mgmtServer.Address,
|
||||
Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
serverID2 := clients.ServerIdentifier{
|
||||
ServerURI: mgmtServer.Address,
|
||||
Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
|
||||
}
|
||||
configs := map[string]grpctransport.Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
"insecure": {Credentials: insecure.NewBundle()},
|
||||
}
|
||||
|
||||
// Create two gRPC transports.
|
||||
builder := grpctransport.NewBuilder(configs)
|
||||
|
||||
tr1, err := builder.Build(serverID1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
// Create a new stream to the server and verify that a new transport is
|
||||
// created.
|
||||
if _, err = tr1.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
val, err := lis.NewConnCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timed out when waiting for a new transport to be created to the management server: %v", err)
|
||||
}
|
||||
conn1 := val.(*testutils.ConnWrapper)
|
||||
|
||||
tr2, err := builder.Build(serverID2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
// Create a new stream to the server and verify that a new transport is
|
||||
// created because credentials are different.
|
||||
if _, err = tr2.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
val, err = lis.NewConnCh.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Timed out when waiting for a new transport to be created to the management server: %v", err)
|
||||
}
|
||||
conn2 := val.(*testutils.ConnWrapper)
|
||||
|
||||
// Create N more references to each of the two transports.
|
||||
const count = 9
|
||||
transports1 := make([]clients.Transport, count)
|
||||
transports2 := make([]clients.Transport, count)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < count; i++ {
|
||||
var err error
|
||||
transports1[i], err = builder.Build(serverID1)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to build transport: %v", err)
|
||||
}
|
||||
// Create a new stream to the server and verify that no connection
|
||||
// is established to the management server at this point. A new
|
||||
// transport is created only when an existing connection for
|
||||
// serverID does not exist.
|
||||
if _, err = transports1[i].NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Errorf("Failed to create stream: %v", err)
|
||||
}
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := lis.NewConnCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Error("Unexpected new transport created to management server")
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < count; i++ {
|
||||
var err error
|
||||
transports2[i], err = builder.Build(serverID2)
|
||||
if err != nil {
|
||||
t.Errorf("%d-th call to Build() failed with error: %v", i, err)
|
||||
}
|
||||
// Create a new stream to the server and verify that no connection
|
||||
// is established to the management server at this point. A new
|
||||
// transport is created only when an existing connection for
|
||||
// serverID does not exist.
|
||||
if _, err = transports2[i].NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Errorf("Failed to create stream: %v", err)
|
||||
}
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := lis.NewConnCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Error("Unexpected new transport created to management server")
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
if t.Failed() {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// Call Close() multiple times on each of the transport received in the
|
||||
// above for loop. Close() calls are idempotent. The underlying gRPC
|
||||
// transport is removed after the Close() call but calling close second
|
||||
// time should not panic and underlying gRPC transport should not be
|
||||
// closed.
|
||||
for i := 0; i < count; i++ {
|
||||
transports1[i].Close()
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := conn1.CloseCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Fatal("Unexpected transport closure to management server")
|
||||
}
|
||||
transports1[i].Close()
|
||||
}
|
||||
// Call the last Close(). The underlying gRPC transport should be closed
|
||||
// because calls in the above for loop have released all references.
|
||||
tr1.Close()
|
||||
if _, err := conn1.CloseCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout when waiting for connection to management server to be closed")
|
||||
}
|
||||
|
||||
// Call Close() multiple times on each of the transport received in the
|
||||
// above for loop. Close() calls are idempotent. The underlying gRPC
|
||||
// transport is removed after the Close() call but calling close second
|
||||
// time should not panic and underlying gRPC transport should not be
|
||||
// closed.
|
||||
for i := 0; i < count; i++ {
|
||||
transports2[i].Close()
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := conn2.CloseCh.Receive(sCtx); err != context.DeadlineExceeded {
|
||||
t.Fatal("Unexpected transport closure to management server")
|
||||
}
|
||||
transports2[i].Close()
|
||||
}
|
||||
// Call the last Close(). The underlying gRPC transport should be closed
|
||||
// because calls in the above for loop have released all references.
|
||||
tr2.Close()
|
||||
if _, err := conn2.CloseCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout when waiting for connection to management server to be closed")
|
||||
}
|
||||
}
|
||||
425
internal/xds/clients/grpctransport/grpc_transport_test.go
Normal file
425
internal/xds/clients/grpctransport/grpc_transport_test.go
Normal file
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
*
|
||||
* 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 grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/credentials/local"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
v3discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen.
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
// testServer implements the AggregatedDiscoveryServiceServer interface to test
|
||||
// the gRPC transport implementation.
|
||||
type testServer struct {
|
||||
v3discoverygrpc.UnimplementedAggregatedDiscoveryServiceServer
|
||||
|
||||
address string // address of the server
|
||||
requestChan chan *v3discoverypb.DiscoveryRequest // channel to send the received requests on for verification
|
||||
response *v3discoverypb.DiscoveryResponse // response to send back to the client from handler
|
||||
}
|
||||
|
||||
// setupTestServer set up the gRPC server for AggregatedDiscoveryService. It
|
||||
// creates an instance of testServer that returns the provided response from
|
||||
// the StreamAggregatedResources() handler and registers it with a gRPC server.
|
||||
func setupTestServer(t *testing.T, response *v3discoverypb.DiscoveryResponse) *testServer {
|
||||
t.Helper()
|
||||
|
||||
lis, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to listen on localhost:0: %v", err)
|
||||
}
|
||||
ts := &testServer{
|
||||
requestChan: make(chan *v3discoverypb.DiscoveryRequest),
|
||||
address: lis.Addr().String(),
|
||||
response: response,
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
|
||||
v3discoverygrpc.RegisterAggregatedDiscoveryServiceServer(s, ts)
|
||||
go s.Serve(lis)
|
||||
t.Cleanup(s.Stop)
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
// StreamAggregatedResources handles bidirectional streaming of
|
||||
// DiscoveryRequest and DiscoveryResponse. It waits for a message from the
|
||||
// client on the stream, and then sends a discovery response message back to
|
||||
// the client. It also put the received message in requestChan for client to
|
||||
// verify if the correct request was received. It continues until the client
|
||||
// closes the stream.
|
||||
func (s *testServer) StreamAggregatedResources(stream v3discoverygrpc.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error {
|
||||
ctx := stream.Context()
|
||||
|
||||
for {
|
||||
// Receive a DiscoveryRequest from the client
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil // Stream closed by client
|
||||
}
|
||||
if err != nil {
|
||||
return err // Handle other errors
|
||||
}
|
||||
|
||||
// Push received request for client to verify the correct request was
|
||||
// received.
|
||||
select {
|
||||
case s.requestChan <- req:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// Send the response back to the client
|
||||
if err := stream.Send(s.response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testCredentials struct {
|
||||
credentials.Bundle
|
||||
transportCredentials credentials.TransportCredentials
|
||||
}
|
||||
|
||||
func (tc *testCredentials) TransportCredentials() credentials.TransportCredentials {
|
||||
return tc.transportCredentials
|
||||
}
|
||||
func (tc *testCredentials) PerRPCCredentials() credentials.PerRPCCredentials {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestBuild_Success verifies that the Builder successfully creates a new
|
||||
// Transport in both cases when provided clients.ServerIdentifer is same
|
||||
// one of the existing transport or a new one.
|
||||
func (s) TestBuild_Success(t *testing.T) {
|
||||
configs := map[string]Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
"insecure": {Credentials: insecure.NewBundle()},
|
||||
}
|
||||
b := NewBuilder(configs)
|
||||
|
||||
serverID1 := clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
tr1, err := b.Build(serverID1)
|
||||
if err != nil {
|
||||
t.Fatalf("Build(serverID1) call failed: %v", err)
|
||||
}
|
||||
defer tr1.Close()
|
||||
|
||||
serverID2 := clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
tr2, err := b.Build(serverID2)
|
||||
if err != nil {
|
||||
t.Fatalf("Build(serverID2) call failed: %v", err)
|
||||
}
|
||||
defer tr2.Close()
|
||||
|
||||
serverID3 := clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "insecure"},
|
||||
}
|
||||
tr3, err := b.Build(serverID3)
|
||||
if err != nil {
|
||||
t.Fatalf("Build(serverID3) call failed: %v", err)
|
||||
}
|
||||
defer tr3.Close()
|
||||
}
|
||||
|
||||
// TestBuild_Failure verifies that the Builder returns error when incorrect
|
||||
// ServerIdentifier is provided.
|
||||
//
|
||||
// It covers the following scenarios:
|
||||
// - ServerURI is empty.
|
||||
// - Extensions is nil.
|
||||
// - Extensions is not ServerIdentifierExtension.
|
||||
// - Credentials are nil.
|
||||
func (s) TestBuild_Failure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serverID clients.ServerIdentifier
|
||||
}{
|
||||
{
|
||||
name: "ServerURI is empty",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Extensions is nil",
|
||||
serverID: clients.ServerIdentifier{ServerURI: "server-address"},
|
||||
},
|
||||
{
|
||||
name: "Extensions is not a ServerIdentifierExtension",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServerIdentifierExtension without ConfigName",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServerIdentifierExtension ConfigName is not present",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "unknown"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServerIdentifierExtension ConfigName maps to nil credentials",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "nil-credentials"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServerIdentifierExtension is added as pointer",
|
||||
serverID: clients.ServerIdentifier{
|
||||
ServerURI: "server-address",
|
||||
Extensions: &ServerIdentifierExtension{ConfigName: "local"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
configs := map[string]Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
"nil-credentials": {Credentials: nil},
|
||||
}
|
||||
b := NewBuilder(configs)
|
||||
tr, err := b.Build(test.serverID)
|
||||
if err == nil {
|
||||
t.Fatalf("Build() succeeded, want error")
|
||||
}
|
||||
if tr != nil {
|
||||
t.Fatalf("Got non-nil transport from Build(), want nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewStream_Success verifies that NewStream() successfully creates a new
|
||||
// client stream for the server when provided a valid server URI and a config
|
||||
// with valid credentials.
|
||||
func (s) TestNewStream_Success(t *testing.T) {
|
||||
ts := setupTestServer(t, &v3discoverypb.DiscoveryResponse{VersionInfo: "1"})
|
||||
|
||||
serverCfg := clients.ServerIdentifier{
|
||||
ServerURI: ts.address,
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
configs := map[string]Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
}
|
||||
builder := NewBuilder(configs)
|
||||
transport, err := builder.Build(serverCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err = transport.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("transport.NewStream() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewStream_Success_WithCustomGRPCNewClient verifies that NewStream()
|
||||
// successfully creates a new client stream for the server when provided a
|
||||
// valid server URI and a config with valid credentials and a custom gRPC
|
||||
// NewClient function.
|
||||
func (s) TestNewStream_Success_WithCustomGRPCNewClient(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ts := setupTestServer(t, &v3discoverypb.DiscoveryResponse{VersionInfo: "1"})
|
||||
|
||||
// Create a custom dialer function that will be used by the gRPC client.
|
||||
customDialerCalled := make(chan struct{}, 1)
|
||||
customGRPCNewClient := func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||
customDialerCalled <- struct{}{}
|
||||
return grpc.NewClient(target, opts...)
|
||||
}
|
||||
|
||||
configs := map[string]Config{
|
||||
"custom-dialer-config": {
|
||||
Credentials: &testCredentials{transportCredentials: local.NewCredentials()},
|
||||
GRPCNewClient: customGRPCNewClient,
|
||||
},
|
||||
}
|
||||
builder := NewBuilder(configs)
|
||||
|
||||
serverID := clients.ServerIdentifier{
|
||||
ServerURI: ts.address,
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "custom-dialer-config"},
|
||||
}
|
||||
|
||||
transport, err := builder.Build(serverID)
|
||||
if err != nil {
|
||||
t.Fatalf("builder.Build(%+v) failed: %v", serverID, err)
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
select {
|
||||
case <-customDialerCalled:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for custom dialer to be called: %v", ctx.Err())
|
||||
}
|
||||
|
||||
// Verify that the transport works by creating a stream.
|
||||
if _, err = transport.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err != nil {
|
||||
t.Fatalf("transport.NewStream() failed with custom dialer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewStream_Error verifies that NewStream() returns an error
|
||||
// when attempting to create a stream with an invalid server URI.
|
||||
func (s) TestNewStream_Error(t *testing.T) {
|
||||
serverCfg := clients.ServerIdentifier{
|
||||
ServerURI: "invalid-server-uri",
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
configs := map[string]Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
}
|
||||
builder := NewBuilder(configs)
|
||||
transport, err := builder.Build(serverCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err = transport.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"); err == nil {
|
||||
t.Fatal("transport.NewStream() succeeded, want failure")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStream_SendAndRecv verifies that Send() and Recv() successfully send
|
||||
// and receive messages on the stream to and from the gRPC server.
|
||||
//
|
||||
// It starts a gRPC test server using setupTestServer(). The test then sends a
|
||||
// testDiscoverRequest on the stream and verifies that the received discovery
|
||||
// request on the server is same as sent. It then wait to receive a
|
||||
// testDiscoverResponse from the server and verifies that the received
|
||||
// discovery response is same as sent from the server.
|
||||
func (s) TestStream_SendAndRecv(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ts := setupTestServer(t, &v3discoverypb.DiscoveryResponse{VersionInfo: "1"})
|
||||
|
||||
// Build a grpc-based transport to the above server.
|
||||
serverCfg := clients.ServerIdentifier{
|
||||
ServerURI: ts.address,
|
||||
Extensions: ServerIdentifierExtension{ConfigName: "local"},
|
||||
}
|
||||
configs := map[string]Config{
|
||||
"local": {Credentials: &testCredentials{transportCredentials: local.NewCredentials()}},
|
||||
}
|
||||
builder := NewBuilder(configs)
|
||||
transport, err := builder.Build(serverCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build transport: %v", err)
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
// Create a new stream to the server.
|
||||
stream, err := transport.NewStream(ctx, "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Send a discovery request message on the stream.
|
||||
testDiscoverRequest := &v3discoverypb.DiscoveryRequest{VersionInfo: "1"}
|
||||
msg, err := proto.Marshal(testDiscoverRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal DiscoveryRequest: %v", err)
|
||||
}
|
||||
if err := stream.Send(msg); err != nil {
|
||||
t.Fatalf("Failed to send message: %v", err)
|
||||
}
|
||||
|
||||
// Verify that the DiscoveryRequest received on the server was same as
|
||||
// sent.
|
||||
select {
|
||||
case gotReq := <-ts.requestChan:
|
||||
if diff := cmp.Diff(testDiscoverRequest, gotReq, protocmp.Transform()); diff != "" {
|
||||
t.Fatalf("Unexpected diff in request received on server (-want +got):\n%s", diff)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for request to reach server")
|
||||
}
|
||||
|
||||
// Wait until response message is received from the server.
|
||||
res, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to receive message: %v", err)
|
||||
}
|
||||
|
||||
// Verify that the DiscoveryResponse received was same as sent from the
|
||||
// server.
|
||||
var gotRes v3discoverypb.DiscoveryResponse
|
||||
if err := proto.Unmarshal(res, &gotRes); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response from server to DiscoveryResponse: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(ts.response, &gotRes, protocmp.Transform()); diff != "" {
|
||||
t.Fatalf("proto.Unmarshal(res, &gotRes) returned unexpected diff (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user