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

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,19 @@
Build client and server binaries.
```sh
go build -o ./binaries/client ../../../../interop/xds/client/
go build -o ./binaries/server ../../../../interop/xds/server/
```
Run the test
```sh
go test . -v
```
The client/server paths are flags
```sh
go test . -v -client=$HOME/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-client
```
Note that grpc logs are only turned on for Go.

View File

@@ -0,0 +1,45 @@
/*
*
* 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 e2e
import (
"testing"
"github.com/google/uuid"
"google.golang.org/grpc/internal/testutils/xds/e2e"
)
type controlPlane struct {
server *e2e.ManagementServer
nodeID string
bootstrapContent string
}
func newControlPlane(t *testing.T) (*controlPlane, error) {
// Spin up an xDS management server on a local port.
server := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
nodeID := uuid.New().String()
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, server.Address)
return &controlPlane{
server: server,
nodeID: nodeID,
bootstrapContent: string(bootstrapContents),
}, nil
}

View File

@@ -0,0 +1,173 @@
/*
*
* 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 e2e implements xds e2e tests using go-control-plane.
package e2e
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"google.golang.org/grpc"
channelzgrpc "google.golang.org/grpc/channelz/grpc_channelz_v1"
channelzpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
"google.golang.org/grpc/credentials/insecure"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
func cmd(path string, logger io.Writer, args []string, env []string) *exec.Cmd {
cmd := exec.Command(path, args...)
cmd.Env = append(os.Environ(), env...)
cmd.Stdout = logger
cmd.Stderr = logger
return cmd
}
const (
clientStatsPort = 60363 // TODO: make this different per-test, only needed for parallel tests.
)
type client struct {
cmd *exec.Cmd
target string
statsCC *grpc.ClientConn
}
// newClient create a client with the given target and bootstrap content.
func newClient(target, binaryPath, bootstrap string, logger io.Writer, flags ...string) (*client, error) {
cmd := cmd(
binaryPath,
logger,
append([]string{
"--server=" + target,
"--print_response=true",
"--qps=100",
fmt.Sprintf("--stats_port=%d", clientStatsPort),
}, flags...), // Append any flags from caller.
[]string{
"GRPC_GO_LOG_VERBOSITY_LEVEL=99",
"GRPC_GO_LOG_SEVERITY_LEVEL=info",
"GRPC_XDS_BOOTSTRAP_CONFIG=" + bootstrap, // The bootstrap content doesn't need to be quoted.
},
)
cmd.Start()
cc, err := grpc.NewClient(fmt.Sprintf("localhost:%d", clientStatsPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.WaitForReady(true)))
if err != nil {
return nil, err
}
return &client{
cmd: cmd,
target: target,
statsCC: cc,
}, nil
}
func (c *client) clientStats(ctx context.Context) (*testpb.LoadBalancerStatsResponse, error) {
ccc := testgrpc.NewLoadBalancerStatsServiceClient(c.statsCC)
return ccc.GetClientStats(ctx, &testpb.LoadBalancerStatsRequest{
NumRpcs: 100,
TimeoutSec: 10,
})
}
func (c *client) configRPCs(ctx context.Context, req *testpb.ClientConfigureRequest) error {
ccc := testgrpc.NewXdsUpdateClientConfigureServiceClient(c.statsCC)
_, err := ccc.Configure(ctx, req)
return err
}
func (c *client) channelzSubChannels(ctx context.Context) ([]*channelzpb.Subchannel, error) {
ccc := channelzgrpc.NewChannelzClient(c.statsCC)
r, err := ccc.GetTopChannels(ctx, &channelzpb.GetTopChannelsRequest{})
if err != nil {
return nil, err
}
var ret []*channelzpb.Subchannel
for _, cc := range r.Channel {
if cc.Data.Target != c.target {
continue
}
for _, sc := range cc.SubchannelRef {
rr, err := ccc.GetSubchannel(ctx, &channelzpb.GetSubchannelRequest{SubchannelId: sc.SubchannelId})
if err != nil {
return nil, err
}
ret = append(ret, rr.Subchannel)
}
}
return ret, nil
}
func (c *client) stop() {
c.cmd.Process.Kill()
c.cmd.Wait()
}
const (
serverPort = 50051 // TODO: make this different per-test, only needed for parallel tests.
)
type server struct {
cmd *exec.Cmd
port int
}
// newServer creates multiple servers with the given bootstrap content.
//
// Each server gets a different hostname, in the format of
// <hostnamePrefix>-<index>.
func newServers(hostnamePrefix, binaryPath, bootstrap string, logger io.Writer, count int) (_ []*server, err error) {
var ret []*server
defer func() {
if err != nil {
for _, s := range ret {
s.stop()
}
}
}()
for i := 0; i < count; i++ {
port := serverPort + i
cmd := cmd(
binaryPath,
logger,
[]string{
fmt.Sprintf("--port=%d", port),
fmt.Sprintf("--host_name_override=%s-%d", hostnamePrefix, i),
},
[]string{
"GRPC_GO_LOG_VERBOSITY_LEVEL=99",
"GRPC_GO_LOG_SEVERITY_LEVEL=info",
"GRPC_XDS_BOOTSTRAP_CONFIG=" + bootstrap, // The bootstrap content doesn't need to be quoted.,
},
)
cmd.Start()
ret = append(ret, &server{cmd: cmd, port: port})
}
return ret, nil
}
func (s *server) stop() {
s.cmd.Process.Kill()
s.cmd.Wait()
}

View File

@@ -0,0 +1,258 @@
/*
*
* 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 e2e
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"strconv"
"testing"
"time"
"google.golang.org/grpc/internal/testutils/xds/e2e"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
channelzpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
var (
clientPath = flag.String("client", "./binaries/client", "The interop client")
serverPath = flag.String("server", "./binaries/server", "The interop server")
)
type testOpts struct {
testName string
backendCount int
clientFlags []string
}
func setup(t *testing.T, opts testOpts) (*controlPlane, *client, []*server) {
t.Helper()
if _, err := os.Stat(*clientPath); os.IsNotExist(err) {
t.Skip("skipped because client is not found")
}
if _, err := os.Stat(*serverPath); os.IsNotExist(err) {
t.Skip("skipped because server is not found")
}
backendCount := 1
if opts.backendCount != 0 {
backendCount = opts.backendCount
}
cp, err := newControlPlane(t)
if err != nil {
t.Fatalf("failed to start control-plane: %v", err)
}
var clientLog bytes.Buffer
c, err := newClient(fmt.Sprintf("xds:///%s", opts.testName), *clientPath, cp.bootstrapContent, &clientLog, opts.clientFlags...)
if err != nil {
t.Fatalf("failed to start client: %v", err)
}
t.Cleanup(c.stop)
var serverLog bytes.Buffer
servers, err := newServers(opts.testName, *serverPath, cp.bootstrapContent, &serverLog, backendCount)
if err != nil {
t.Fatalf("failed to start server: %v", err)
}
t.Cleanup(func() {
for _, s := range servers {
s.stop()
}
})
t.Cleanup(func() {
// TODO: find a better way to print the log. They are long, and hide the failure.
t.Logf("\n----- client logs -----\n%v", clientLog.String())
t.Logf("\n----- server logs -----\n%v", serverLog.String())
})
return cp, c, servers
}
func TestPingPong(t *testing.T) {
const testName = "pingpong"
cp, c, _ := setup(t, testOpts{testName: testName})
resources := e2e.DefaultClientResources(e2e.ResourceParams{
DialTarget: testName,
NodeID: cp.nodeID,
Host: "localhost",
Port: serverPort,
SecLevel: e2e.SecurityLevelNone,
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := cp.server.Update(ctx, resources); err != nil {
t.Fatalf("failed to update control plane resources: %v", err)
}
st, err := c.clientStats(ctx)
if err != nil {
t.Fatalf("failed to get client stats: %v", err)
}
if st.NumFailures != 0 {
t.Fatalf("Got %v failures: %+v", st.NumFailures, st)
}
}
// TestAffinity covers the affinity tests with ringhash policy.
// - client is configured to use ringhash, with 3 backends
// - all RPCs will hash a specific metadata header
// - verify that
// - all RPCs with the same metadata value are sent to the same backend
// - only one backend is Ready
//
// - send more RPCs with different metadata values until a new backend is picked, and verify that
// - only two backends are in Ready
func TestAffinity(t *testing.T) {
const (
testName = "affinity"
backendCount = 3
testMDKey = "xds_md"
testMDValue = "unary_yranu"
)
cp, c, servers := setup(t, testOpts{
testName: testName,
backendCount: backendCount,
clientFlags: []string{"--rpc=EmptyCall", fmt.Sprintf("--metadata=EmptyCall:%s:%s", testMDKey, testMDValue)},
})
resources := e2e.DefaultClientResources(e2e.ResourceParams{
DialTarget: testName,
NodeID: cp.nodeID,
Host: "localhost",
Port: serverPort,
SecLevel: e2e.SecurityLevelNone,
})
// Update EDS to multiple backends.
var ports []uint32
for _, s := range servers {
ports = append(ports, uint32(s.port))
}
edsMsg := resources.Endpoints[0]
resources.Endpoints[0] = e2e.DefaultEndpoint(
edsMsg.ClusterName,
"localhost",
ports,
)
// Update CDS lbpolicy to ringhash.
cdsMsg := resources.Clusters[0]
cdsMsg.LbPolicy = v3clusterpb.Cluster_RING_HASH
// Update RDS to hash the header.
rdsMsg := resources.Routes[0]
rdsMsg.VirtualHosts[0].Routes[0].Action = &v3routepb.Route_Route{Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: cdsMsg.Name},
HashPolicy: []*v3routepb.RouteAction_HashPolicy{{
PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
Header: &v3routepb.RouteAction_HashPolicy_Header{
HeaderName: testMDKey,
},
},
}},
}}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := cp.server.Update(ctx, resources); err != nil {
t.Fatalf("failed to update control plane resources: %v", err)
}
// Note: We can skip CSDS check because there's no long delay as in TD.
//
// The client stats check doesn't race with the xds resource update because
// there's only one version of xds resource, updated at the beginning of the
// test. So there's no need to retry the stats call.
//
// In the future, we may add tests that update xds in the middle. Then we
// either need to retry clientStats(), or make a CSDS check before so the
// result is stable.
st, err := c.clientStats(ctx)
if err != nil {
t.Fatalf("failed to get client stats: %v", err)
}
if st.NumFailures != 0 {
t.Fatalf("Got %v failures: %+v", st.NumFailures, st)
}
if len(st.RpcsByPeer) != 1 {
t.Fatalf("more than 1 backends got traffic: %v, want 1", st.RpcsByPeer)
}
// Call channelz to verify that only one subchannel is in state Ready.
scs, err := c.channelzSubChannels(ctx)
if err != nil {
t.Fatalf("failed to fetch channelz: %v", err)
}
verifySubConnStates(t, scs, map[channelzpb.ChannelConnectivityState_State]int{
channelzpb.ChannelConnectivityState_READY: 1,
channelzpb.ChannelConnectivityState_IDLE: 2,
})
// Send Unary call with different metadata value with integers starting from
// 0. Stop when a second peer is picked.
var (
diffPeerPicked bool
mdValue int
)
for !diffPeerPicked {
if err := c.configRPCs(ctx, &testpb.ClientConfigureRequest{
Types: []testpb.ClientConfigureRequest_RpcType{
testpb.ClientConfigureRequest_EMPTY_CALL,
testpb.ClientConfigureRequest_UNARY_CALL,
},
Metadata: []*testpb.ClientConfigureRequest_Metadata{
{Type: testpb.ClientConfigureRequest_EMPTY_CALL, Key: testMDKey, Value: testMDValue},
{Type: testpb.ClientConfigureRequest_UNARY_CALL, Key: testMDKey, Value: strconv.Itoa(mdValue)},
},
}); err != nil {
t.Fatalf("failed to configure RPC: %v", err)
}
st, err := c.clientStats(ctx)
if err != nil {
t.Fatalf("failed to get client stats: %v", err)
}
if st.NumFailures != 0 {
t.Fatalf("Got %v failures: %+v", st.NumFailures, st)
}
if len(st.RpcsByPeer) == 2 {
break
}
mdValue++
}
// Call channelz to verify that only one subchannel is in state Ready.
scs2, err := c.channelzSubChannels(ctx)
if err != nil {
t.Fatalf("failed to fetch channelz: %v", err)
}
verifySubConnStates(t, scs2, map[channelzpb.ChannelConnectivityState_State]int{
channelzpb.ChannelConnectivityState_READY: 2,
channelzpb.ChannelConnectivityState_IDLE: 1,
})
}

View File

@@ -0,0 +1,179 @@
/*
*
* 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 e2e
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"github.com/cespare/xxhash/v2"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
channelzpb "google.golang.org/grpc/channelz/grpc_channelz_v1"
"google.golang.org/grpc/internal/xds/clients"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/resolver"
)
func verifySubConnStates(t *testing.T, scs []*channelzpb.Subchannel, want map[channelzpb.ChannelConnectivityState_State]int) {
t.Helper()
var scStatsCount = map[channelzpb.ChannelConnectivityState_State]int{}
for _, sc := range scs {
scStatsCount[sc.Data.State.State]++
}
if diff := cmp.Diff(scStatsCount, want); diff != "" {
t.Fatalf("got unexpected number of subchannels in state Ready, %v, scs: %v", diff, scs)
}
}
// This function determines the stable, canonical order for any two
// resolver.Endpoint structs.
func lessEndpoint(a, b resolver.Endpoint) bool {
return getHash(a) < getHash(b)
}
func getHash(e resolver.Endpoint) uint64 {
h := xxhash.New()
// We iterate through all addresses to ensure the hash represents
// the full endpoint identity.
for _, addr := range e.Addresses {
h.Write([]byte(addr.Addr))
h.Write([]byte(addr.ServerName))
}
return h.Sum64()
}
// VerifyXDSConfig waits for an update on the xdsCh channel or an error on the
// errCh channel, and verifies that the received XDSConfig matches the expected
// XDS configuration.
func VerifyXDSConfig(ctx context.Context, xdsCh chan *xdsresource.XDSConfig, errCh chan error, want *xdsresource.XDSConfig) error {
select {
case <-ctx.Done():
return fmt.Errorf("timeout waiting for update from dependency manager")
case update := <-xdsCh:
if err := CompareXDSConfig(update, want); err != nil {
return err
}
case err := <-errCh:
return fmt.Errorf("received unexpected error from dependency manager: %v", err)
}
return nil
}
// CompareXDSConfig compares two XDSConfig structs and returns an error if they
// do not match.
func CompareXDSConfig(gotXDSConfig, wantXDSConfig *xdsresource.XDSConfig) error {
cmpOpts := []cmp.Option{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(xdsresource.HTTPFilter{}, "Filter", "Config"),
cmpopts.IgnoreFields(xdsresource.ListenerUpdate{}, "Raw"),
cmpopts.IgnoreFields(xdsresource.RouteConfigUpdate{}, "Raw"),
cmpopts.IgnoreFields(xdsresource.ClusterUpdate{}, "Raw", "LBPolicy", "TelemetryLabels"),
cmpopts.IgnoreFields(xdsresource.EndpointsUpdate{}, "Raw"),
// Used for EndpointConfig.ResolutionNote and ClusterResult.Err fields.
cmp.Transformer("ErrorsToString", func(in error) string {
if in == nil {
return "" // Treat nil as an empty string
}
s := in.Error()
// Replace all sequences of whitespace (including newlines and
// tabs) with a single standard space.
s = regexp.MustCompile(`\s+`).ReplaceAllString(s, " ")
// Trim any leading/trailing space that might be left over and
// return error as string.
return strings.TrimSpace(s)
}),
cmpopts.SortSlices(lessEndpoint),
}
if diff := cmp.Diff(gotXDSConfig, wantXDSConfig, cmpOpts...); diff != "" {
return fmt.Errorf("received unexpected update from dependency manager. Diff (-got +want):\n%v", diff)
}
return nil
}
// MakeXDSConfig creates a mock XDSConfig with a standard structure containing
// a Listener, RouteConfig, VirtualHost, and a single EDS Cluster, populated
// with the provided parameters.
func MakeXDSConfig(servicename, routeConfigName, clusterName, edsServiceName, addr string) *xdsresource.XDSConfig {
prefix := "/"
return &xdsresource.XDSConfig{
Listener: &xdsresource.ListenerUpdate{
APIListener: &xdsresource.HTTPConnectionManagerConfig{
RouteConfigName: routeConfigName,
HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}},
},
},
RouteConfig: &xdsresource.RouteConfigUpdate{
VirtualHosts: []*xdsresource.VirtualHost{
{
Domains: []string{servicename},
Routes: []*xdsresource.Route{{
Prefix: &prefix,
WeightedClusters: []xdsresource.WeightedCluster{{Name: clusterName, Weight: 100}},
ActionType: xdsresource.RouteActionRoute,
}},
},
},
},
VirtualHost: &xdsresource.VirtualHost{
Domains: []string{servicename},
Routes: []*xdsresource.Route{{
Prefix: &prefix,
WeightedClusters: []xdsresource.WeightedCluster{{Name: clusterName, Weight: 100}},
ActionType: xdsresource.RouteActionRoute},
},
},
Clusters: map[string]*xdsresource.ClusterResult{
clusterName: {
Config: xdsresource.ClusterConfig{Cluster: &xdsresource.ClusterUpdate{
ClusterType: xdsresource.ClusterTypeEDS,
ClusterName: clusterName,
EDSServiceName: edsServiceName,
},
EndpointConfig: &xdsresource.EndpointConfig{
EDSUpdate: &xdsresource.EndpointsUpdate{
Localities: []xdsresource.Locality{
{ID: clients.Locality{
Region: "region-1",
Zone: "zone-1",
SubZone: "subzone-1",
},
Endpoints: []xdsresource.Endpoint{
{
ResolverEndpoint: resolver.Endpoint{Addresses: []resolver.Address{{Addr: addr}}},
HealthStatus: xdsresource.EndpointHealthStatusUnknown,
Weight: 1,
},
},
Weight: 1,
},
},
},
},
},
},
},
}
}

6
internal/xds/test/e2e/run.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
mkdir binaries
go build -o ./binaries/client ../../../../interop/xds/client/
go build -o ./binaries/server ../../../../interop/xds/server/
go test .