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

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View 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}}
}

View 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 ""
}

View 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")
}
}

View 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)
}
}