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,37 @@
# 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.
# Dockerfile for building the xDS interop client. To build the image, run the
# following command from grpc-go directory:
# docker build -t <TAG> -f interop/xds/client/Dockerfile .
FROM golang:1.25-alpine as build
# Make a grpc-go directory and copy the repo into it.
WORKDIR /go/src/grpc-go
COPY . .
# Build a static binary without cgo so that we can copy just the binary in the
# final image, and can get rid of Go compiler and gRPC-Go dependencies.
RUN cd interop/xds/client && go build -tags osusergo,netgo .
# Second stage of the build which copies over only the client binary and skips
# the Go compiler and gRPC repo from the earlier stage. This significantly
# reduces the docker image size.
FROM alpine
COPY --from=build /go/src/grpc-go/interop/xds/client/client .
ENV GRPC_GO_LOG_VERBOSITY_LEVEL=99
ENV GRPC_GO_LOG_SEVERITY_LEVEL="info"
ENV GRPC_GO_LOG_FORMATTER="json"
ENTRYPOINT ["./client"]

View File

@@ -0,0 +1,554 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Binary client for xDS interop tests.
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/admin"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/credentials/xds"
"google.golang.org/grpc/grpclog"
_ "google.golang.org/grpc/interop/xds" // to register Custom LB.
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/stats/opentelemetry"
"google.golang.org/grpc/stats/opentelemetry/csm"
"google.golang.org/grpc/status"
_ "google.golang.org/grpc/xds"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/sdk/metric"
)
func init() {
rpcCfgs.Store([]*rpcConfig{{typ: unaryCall}})
}
type statsWatcherKey struct {
startID int32
endID int32
}
// rpcInfo contains the rpc type and the hostname where the response is received
// from.
type rpcInfo struct {
typ string
hostname string
}
type statsWatcher struct {
rpcsByPeer map[string]int32
rpcsByType map[string]map[string]int32
numFailures int32
remainingRPCs int32
chanHosts chan *rpcInfo
}
func (watcher *statsWatcher) buildResp() *testpb.LoadBalancerStatsResponse {
rpcsByType := make(map[string]*testpb.LoadBalancerStatsResponse_RpcsByPeer, len(watcher.rpcsByType))
for t, rpcsByPeer := range watcher.rpcsByType {
rpcsByType[t] = &testpb.LoadBalancerStatsResponse_RpcsByPeer{
RpcsByPeer: rpcsByPeer,
}
}
return &testpb.LoadBalancerStatsResponse{
NumFailures: watcher.numFailures + watcher.remainingRPCs,
RpcsByPeer: watcher.rpcsByPeer,
RpcsByMethod: rpcsByType,
}
}
type accumulatedStats struct {
mu sync.Mutex
numRPCsStartedByMethod map[string]int32
numRPCsSucceededByMethod map[string]int32
numRPCsFailedByMethod map[string]int32
rpcStatusByMethod map[string]map[int32]int32
}
func convertRPCName(in string) string {
switch in {
case unaryCall:
return testpb.ClientConfigureRequest_UNARY_CALL.String()
case emptyCall:
return testpb.ClientConfigureRequest_EMPTY_CALL.String()
}
logger.Warningf("unrecognized rpc type: %s", in)
return in
}
// copyStatsMap makes a copy of the map.
func copyStatsMap(originalMap map[string]int32) map[string]int32 {
newMap := make(map[string]int32, len(originalMap))
for k, v := range originalMap {
newMap[k] = v
}
return newMap
}
// copyStatsIntMap makes a copy of the map.
func copyStatsIntMap(originalMap map[int32]int32) map[int32]int32 {
newMap := make(map[int32]int32, len(originalMap))
for k, v := range originalMap {
newMap[k] = v
}
return newMap
}
func (as *accumulatedStats) makeStatsMap() map[string]*testpb.LoadBalancerAccumulatedStatsResponse_MethodStats {
m := make(map[string]*testpb.LoadBalancerAccumulatedStatsResponse_MethodStats)
for k, v := range as.numRPCsStartedByMethod {
m[k] = &testpb.LoadBalancerAccumulatedStatsResponse_MethodStats{RpcsStarted: v}
}
for k, v := range as.rpcStatusByMethod {
if m[k] == nil {
m[k] = &testpb.LoadBalancerAccumulatedStatsResponse_MethodStats{}
}
m[k].Result = copyStatsIntMap(v)
}
return m
}
func (as *accumulatedStats) buildResp() *testpb.LoadBalancerAccumulatedStatsResponse {
as.mu.Lock()
defer as.mu.Unlock()
return &testpb.LoadBalancerAccumulatedStatsResponse{
NumRpcsStartedByMethod: copyStatsMap(as.numRPCsStartedByMethod),
NumRpcsSucceededByMethod: copyStatsMap(as.numRPCsSucceededByMethod),
NumRpcsFailedByMethod: copyStatsMap(as.numRPCsFailedByMethod),
StatsPerMethod: as.makeStatsMap(),
}
}
func (as *accumulatedStats) startRPC(rpcType string) {
as.mu.Lock()
defer as.mu.Unlock()
as.numRPCsStartedByMethod[convertRPCName(rpcType)]++
}
func (as *accumulatedStats) finishRPC(rpcType string, err error) {
as.mu.Lock()
defer as.mu.Unlock()
name := convertRPCName(rpcType)
if as.rpcStatusByMethod[name] == nil {
as.rpcStatusByMethod[name] = make(map[int32]int32)
}
as.rpcStatusByMethod[name][int32(status.Convert(err).Code())]++
if err != nil {
as.numRPCsFailedByMethod[name]++
return
}
as.numRPCsSucceededByMethod[name]++
}
var (
failOnFailedRPC = flag.Bool("fail_on_failed_rpc", false, "Fail client if any RPCs fail after first success")
numChannels = flag.Int("num_channels", 1, "Num of channels")
printResponse = flag.Bool("print_response", false, "Write RPC response to stdout")
qps = flag.Int("qps", 1, "QPS per channel, for each type of RPC")
rpc = flag.String("rpc", "UnaryCall", "Types of RPCs to make, ',' separated string. RPCs can be EmptyCall or UnaryCall. Deprecated: Use Configure RPC to XdsUpdateClientConfigureServiceServer instead.")
rpcMetadata = flag.String("metadata", "", "The metadata to send with RPC, in format EmptyCall:key1:value1,UnaryCall:key2:value2. Deprecated: Use Configure RPC to XdsUpdateClientConfigureServiceServer instead.")
rpcTimeout = flag.Duration("rpc_timeout", 20*time.Second, "Per RPC timeout")
server = flag.String("server", "localhost:8080", "Address of server to connect to")
statsPort = flag.Int("stats_port", 8081, "Port to expose peer distribution stats service")
secureMode = flag.Bool("secure_mode", false, "If true, retrieve security configuration from the management server. Else, use insecure credentials.")
enableCSMObservability = flag.Bool("enable_csm_observability", false, "Whether to enable CSM Observability")
requestPayloadSize = flag.Int("request_payload_size", 0, "Ask the server to respond with SimpleResponse.payload.body of the given length (may not be implemented on the server).")
responsePayloadSize = flag.Int("response_payload_size", 0, "Ask the server to respond with SimpleResponse.payload.body of the given length (may not be implemented on the server).")
rpcCfgs atomic.Value
mu sync.Mutex
currentRequestID int32
watchers = make(map[statsWatcherKey]*statsWatcher)
accStats = accumulatedStats{
numRPCsStartedByMethod: make(map[string]int32),
numRPCsSucceededByMethod: make(map[string]int32),
numRPCsFailedByMethod: make(map[string]int32),
rpcStatusByMethod: make(map[string]map[int32]int32),
}
// 0 or 1 representing an RPC has succeeded. Use hasRPCSucceeded and
// setRPCSucceeded to access in a safe manner.
rpcSucceeded uint32
logger = grpclog.Component("interop")
)
type statsService struct {
testgrpc.UnimplementedLoadBalancerStatsServiceServer
}
func hasRPCSucceeded() bool {
return atomic.LoadUint32(&rpcSucceeded) > 0
}
func setRPCSucceeded() {
atomic.StoreUint32(&rpcSucceeded, 1)
}
// Wait for the next LoadBalancerStatsRequest.GetNumRpcs to start and complete,
// and return the distribution of remote peers. This is essentially a clientside
// LB reporting mechanism that is designed to be queried by an external test
// driver when verifying that the client is distributing RPCs as expected.
func (s *statsService) GetClientStats(ctx context.Context, in *testpb.LoadBalancerStatsRequest) (*testpb.LoadBalancerStatsResponse, error) {
mu.Lock()
watcherKey := statsWatcherKey{currentRequestID, currentRequestID + in.GetNumRpcs()}
watcher, ok := watchers[watcherKey]
if !ok {
watcher = &statsWatcher{
rpcsByPeer: make(map[string]int32),
rpcsByType: make(map[string]map[string]int32),
numFailures: 0,
remainingRPCs: in.GetNumRpcs(),
chanHosts: make(chan *rpcInfo),
}
watchers[watcherKey] = watcher
}
mu.Unlock()
ctx, cancel := context.WithTimeout(ctx, time.Duration(in.GetTimeoutSec())*time.Second)
defer cancel()
defer func() {
mu.Lock()
delete(watchers, watcherKey)
mu.Unlock()
}()
// Wait until the requested RPCs have all been recorded or timeout occurs.
for {
select {
case info := <-watcher.chanHosts:
if info != nil {
watcher.rpcsByPeer[info.hostname]++
rpcsByPeerForType := watcher.rpcsByType[info.typ]
if rpcsByPeerForType == nil {
rpcsByPeerForType = make(map[string]int32)
watcher.rpcsByType[info.typ] = rpcsByPeerForType
}
rpcsByPeerForType[info.hostname]++
} else {
watcher.numFailures++
}
watcher.remainingRPCs--
if watcher.remainingRPCs == 0 {
return watcher.buildResp(), nil
}
case <-ctx.Done():
logger.Info("Timed out, returning partial stats")
return watcher.buildResp(), nil
}
}
}
func (s *statsService) GetClientAccumulatedStats(context.Context, *testpb.LoadBalancerAccumulatedStatsRequest) (*testpb.LoadBalancerAccumulatedStatsResponse, error) {
return accStats.buildResp(), nil
}
type configureService struct {
testgrpc.UnimplementedXdsUpdateClientConfigureServiceServer
}
func (s *configureService) Configure(_ context.Context, in *testpb.ClientConfigureRequest) (*testpb.ClientConfigureResponse, error) {
rpcsToMD := make(map[testpb.ClientConfigureRequest_RpcType][]string)
for _, typ := range in.GetTypes() {
rpcsToMD[typ] = nil
}
for _, md := range in.GetMetadata() {
typ := md.GetType()
strs, ok := rpcsToMD[typ]
if !ok {
continue
}
rpcsToMD[typ] = append(strs, md.GetKey(), md.GetValue())
}
cfgs := make([]*rpcConfig, 0, len(rpcsToMD))
for typ, md := range rpcsToMD {
var rpcType string
switch typ {
case testpb.ClientConfigureRequest_UNARY_CALL:
rpcType = unaryCall
case testpb.ClientConfigureRequest_EMPTY_CALL:
rpcType = emptyCall
default:
return nil, fmt.Errorf("unsupported RPC type: %v", typ)
}
cfgs = append(cfgs, &rpcConfig{
typ: rpcType,
md: metadata.Pairs(md...),
timeout: in.GetTimeoutSec(),
})
}
rpcCfgs.Store(cfgs)
return &testpb.ClientConfigureResponse{}, nil
}
const (
unaryCall string = "UnaryCall"
emptyCall string = "EmptyCall"
)
func parseRPCTypes(rpcStr string) []string {
if len(rpcStr) == 0 {
return []string{unaryCall}
}
rpcs := strings.Split(rpcStr, ",")
ret := make([]string, 0, len(rpcStr))
for _, r := range rpcs {
switch r {
case unaryCall, emptyCall:
ret = append(ret, r)
default:
flag.PrintDefaults()
log.Fatalf("unsupported RPC type: %v", r)
}
}
return ret
}
type rpcConfig struct {
typ string
md metadata.MD
timeout int32
}
// parseRPCMetadata turns EmptyCall:key1:value1 into
//
// {typ: emptyCall, md: {key1:value1}}.
func parseRPCMetadata(rpcMetadataStr string, rpcs []string) []*rpcConfig {
rpcMetadataSplit := strings.Split(rpcMetadataStr, ",")
rpcsToMD := make(map[string][]string)
for _, rm := range rpcMetadataSplit {
rmSplit := strings.Split(rm, ":")
if len(rmSplit)%2 != 1 {
log.Fatalf("invalid metadata config %v, want EmptyCall:key1:value1", rm)
}
rpcsToMD[rmSplit[0]] = append(rpcsToMD[rmSplit[0]], rmSplit[1:]...)
}
ret := make([]*rpcConfig, 0, len(rpcs))
for _, rpcT := range rpcs {
rpcC := &rpcConfig{
typ: rpcT,
}
if md := rpcsToMD[string(rpcT)]; len(md) > 0 {
rpcC.md = metadata.Pairs(md...)
}
ret = append(ret, rpcC)
}
return ret
}
func main() {
flag.Parse()
if *enableCSMObservability {
exporter, err := prometheus.New()
if err != nil {
logger.Fatalf("Failed to start prometheus exporter: %v", err)
}
provider := metric.NewMeterProvider(
metric.WithReader(exporter),
)
var addr string
var ok bool
if addr, ok = os.LookupEnv("OTEL_EXPORTER_PROMETHEUS_HOST"); !ok {
addr = ""
}
var port string
if port, ok = os.LookupEnv("OTEL_EXPORTER_PROMETHEUS_PORT"); !ok {
port = "9464"
}
go func() {
if err := http.ListenAndServe(addr+":"+port, promhttp.Handler()); err != nil {
logger.Fatalf("error listening: %v", err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
cleanup := csm.EnableObservability(ctx, opentelemetry.Options{MetricsOptions: opentelemetry.MetricsOptions{MeterProvider: provider}})
defer cleanup()
}
rpcCfgs.Store(parseRPCMetadata(*rpcMetadata, parseRPCTypes(*rpc)))
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *statsPort))
if err != nil {
logger.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
defer s.Stop()
testgrpc.RegisterLoadBalancerStatsServiceServer(s, &statsService{})
testgrpc.RegisterXdsUpdateClientConfigureServiceServer(s, &configureService{})
reflection.Register(s)
cleanup, err := admin.Register(s)
if err != nil {
logger.Fatalf("Failed to register admin: %v", err)
}
defer cleanup()
go s.Serve(lis)
creds := insecure.NewCredentials()
if *secureMode {
var err error
creds, err = xds.NewClientCredentials(xds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
if err != nil {
logger.Fatalf("Failed to create xDS credentials: %v", err)
}
}
clients := make([]testgrpc.TestServiceClient, *numChannels)
for i := 0; i < *numChannels; i++ {
conn, err := grpc.NewClient(*server, grpc.WithTransportCredentials(creds))
if err != nil {
logger.Fatalf("grpc.NewClient(%q) = %v", *server, err)
}
defer conn.Close()
clients[i] = testgrpc.NewTestServiceClient(conn)
}
ticker := time.NewTicker(time.Second / time.Duration(*qps**numChannels))
defer ticker.Stop()
sendRPCs(clients, ticker)
}
func makeOneRPC(c testgrpc.TestServiceClient, cfg *rpcConfig) (*peer.Peer, *rpcInfo, error) {
timeout := *rpcTimeout
if cfg.timeout != 0 {
timeout = time.Duration(cfg.timeout) * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if len(cfg.md) != 0 {
ctx = metadata.NewOutgoingContext(ctx, cfg.md)
}
info := rpcInfo{typ: cfg.typ}
var (
p peer.Peer
header metadata.MD
err error
)
accStats.startRPC(cfg.typ)
switch cfg.typ {
case unaryCall:
sr := &testpb.SimpleRequest{FillServerId: true}
if *requestPayloadSize > 0 {
sr.Payload = &testpb.Payload{Body: make([]byte, *requestPayloadSize)}
}
if *responsePayloadSize > 0 {
sr.ResponseSize = int32(*responsePayloadSize)
}
sr.ResponseSize = int32(*responsePayloadSize)
var resp *testpb.SimpleResponse
resp, err = c.UnaryCall(ctx, sr, grpc.Peer(&p), grpc.Header(&header))
// For UnaryCall, also read hostname from response, in case the server
// isn't updated to send headers.
if resp != nil {
info.hostname = resp.Hostname
}
case emptyCall:
_, err = c.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&p), grpc.Header(&header))
}
accStats.finishRPC(cfg.typ, err)
if err != nil {
return nil, nil, err
}
hosts := header["hostname"]
if len(hosts) > 0 {
info.hostname = hosts[0]
}
return &p, &info, err
}
func sendRPCs(clients []testgrpc.TestServiceClient, ticker *time.Ticker) {
var i int
for range ticker.C {
// Get and increment request ID, and save a list of watchers that are
// interested in this RPC.
mu.Lock()
savedRequestID := currentRequestID
currentRequestID++
savedWatchers := []*statsWatcher{}
for key, value := range watchers {
if key.startID <= savedRequestID && savedRequestID < key.endID {
savedWatchers = append(savedWatchers, value)
}
}
mu.Unlock()
// Get the RPC metadata configurations from the Configure RPC.
cfgs := rpcCfgs.Load().([]*rpcConfig)
c := clients[i]
for _, cfg := range cfgs {
go func(cfg *rpcConfig) {
p, info, err := makeOneRPC(c, cfg)
for _, watcher := range savedWatchers {
// This sends an empty string if the RPC failed.
watcher.chanHosts <- info
}
if err != nil && *failOnFailedRPC && hasRPCSucceeded() {
logger.Fatalf("RPC failed: %v", err)
}
if err == nil {
setRPCSucceeded()
}
if *printResponse {
if err == nil {
if cfg.typ == unaryCall {
// Need to keep this format, because some tests are
// relying on stdout.
fmt.Printf("Greeting: Hello world, this is %s, from %v\n", info.hostname, p.Addr)
} else {
fmt.Printf("RPC %q, from host %s, addr %v\n", cfg.typ, info.hostname, p.Addr)
}
} else {
fmt.Printf("RPC %q, failed with %v\n", cfg.typ, err)
}
}
}(cfg)
}
i = (i + 1) % len(clients)
}
}

140
interop/xds/custom_lb.go Normal file
View File

@@ -0,0 +1,140 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package xds contains various xds interop helpers for usage in interop tests.
package xds
import (
"encoding/json"
"fmt"
"sync"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/internal/pretty"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/serviceconfig"
)
func init() {
balancer.Register(rpcBehaviorBB{})
}
const name = "test.RpcBehaviorLoadBalancer"
type rpcBehaviorBB struct{}
func (rpcBehaviorBB) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
b := &rpcBehaviorLB{
ClientConn: cc,
}
// round_robin child to complete balancer tree with a usable leaf policy and
// have RPCs actually work.
builder := balancer.Get(roundrobin.Name)
if builder == nil {
// Shouldn't happen, defensive programming. Registered from import of
// roundrobin package.
return nil
}
rr := builder.Build(b, bOpts)
if rr == nil {
// Shouldn't happen, defensive programming.
return nil
}
b.Balancer = rr
return b
}
func (rpcBehaviorBB) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
lbCfg := &lbConfig{}
if err := json.Unmarshal(s, lbCfg); err != nil {
return nil, fmt.Errorf("rpc-behavior-lb: unable to marshal lbConfig: %s, error: %v", string(s), err)
}
return lbCfg, nil
}
func (rpcBehaviorBB) Name() string {
return name
}
type lbConfig struct {
serviceconfig.LoadBalancingConfig `json:"-"`
RPCBehavior string `json:"rpcBehavior,omitempty"`
}
// rpcBehaviorLB is a load balancer that wraps a round robin balancer and
// appends the rpc-behavior metadata field to any metadata in pick results based
// on what is specified in configuration.
type rpcBehaviorLB struct {
// embed a ClientConn to wrap only UpdateState() operation
balancer.ClientConn
// embed a Balancer to wrap only UpdateClientConnState() operation
balancer.Balancer
mu sync.Mutex
cfg *lbConfig
}
func (b *rpcBehaviorLB) UpdateClientConnState(s balancer.ClientConnState) error {
lbCfg, ok := s.BalancerConfig.(*lbConfig)
if !ok {
return fmt.Errorf("test.RpcBehaviorLoadBalancer:received config with unexpected type %T: %s", s.BalancerConfig, pretty.ToJSON(s.BalancerConfig))
}
b.mu.Lock()
b.cfg = lbCfg
b.mu.Unlock()
return b.Balancer.UpdateClientConnState(balancer.ClientConnState{
ResolverState: s.ResolverState,
})
}
func (b *rpcBehaviorLB) UpdateState(state balancer.State) {
b.mu.Lock()
rpcBehavior := b.cfg.RPCBehavior
b.mu.Unlock()
b.ClientConn.UpdateState(balancer.State{
ConnectivityState: state.ConnectivityState,
Picker: newRPCBehaviorPicker(state.Picker, rpcBehavior),
})
}
// rpcBehaviorPicker wraps a picker and adds the rpc-behavior metadata field
// into the child pick result's metadata.
type rpcBehaviorPicker struct {
childPicker balancer.Picker
rpcBehavior string
}
// Pick appends the rpc-behavior metadata entry to the pick result of the child.
func (p *rpcBehaviorPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
pr, err := p.childPicker.Pick(info)
if err != nil {
return balancer.PickResult{}, err
}
pr.Metadata = metadata.Join(pr.Metadata, metadata.Pairs("rpc-behavior", p.rpcBehavior))
return pr, nil
}
func newRPCBehaviorPicker(childPicker balancer.Picker, rpcBehavior string) *rpcBehaviorPicker {
return &rpcBehaviorPicker{
childPicker: childPicker,
rpcBehavior: rpcBehavior,
}
}

View File

@@ -0,0 +1,135 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package xds
import (
"context"
"errors"
"fmt"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
"google.golang.org/grpc/serviceconfig"
)
var defaultTestTimeout = 5 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// TestCustomLB tests the Custom LB for the interop client. It configures the
// custom lb as the top level Load Balancing policy of the channel, then asserts
// it can successfully make an RPC and also that the rpc behavior the Custom LB
// is configured with makes it's way to the server in metadata.
func (s) TestCustomLB(t *testing.T) {
errCh := testutils.NewChannel()
// Setup a backend which verifies the expected rpc-behavior metadata is
// present in the request.
backend := &stubserver.StubServer{
UnaryCallF: func(ctx context.Context, _ *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
errCh.Send(errors.New("failed to receive metadata"))
return &testpb.SimpleResponse{}, nil
}
rpcBMD := md.Get("rpc-behavior")
if len(rpcBMD) != 1 {
errCh.Send(fmt.Errorf("received %d values for metadata key \"rpc-behavior\", want 1", len(rpcBMD)))
return &testpb.SimpleResponse{}, nil
}
wantVal := "error-code-0"
if rpcBMD[0] != wantVal {
errCh.Send(fmt.Errorf("metadata val for key \"rpc-behavior\": got val %v, want val %v", rpcBMD[0], wantVal))
return &testpb.SimpleResponse{}, nil
}
// Success.
errCh.Send(nil)
return &testpb.SimpleResponse{}, nil
},
}
if err := backend.StartServer(); err != nil {
t.Fatalf("Failed to start backend: %v", err)
}
t.Logf("Started good TestService backend at: %q", backend.Address)
defer backend.Stop()
lbCfgJSON := `{
"loadBalancingConfig": [
{
"test.RpcBehaviorLoadBalancer": {
"rpcBehavior": "error-code-0"
}
}
]
}`
sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(lbCfgJSON)
mr := manual.NewBuilderWithScheme("customlb-e2e")
defer mr.Close()
mr.InitialState(resolver.State{
Addresses: []resolver.Address{
{Addr: backend.Address},
},
ServiceConfig: sc,
})
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient() failed: %v", err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
testServiceClient := testgrpc.NewTestServiceClient(cc)
// Make a Unary RPC. This RPC should be successful due to the round_robin
// leaf balancer. Also, the custom load balancer should inject the
// "rpc-behavior" string it is configured with into the metadata sent to
// server.
if _, err := testServiceClient.UnaryCall(ctx, &testpb.SimpleRequest{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
val, err := errCh.Receive(ctx)
if err != nil {
t.Fatalf("error receiving from errCh: %v", err)
}
// Should receive nil on the error channel which implies backend verified it
// correctly received the correct "rpc-behavior" metadata.
if err, ok := val.(error); ok {
t.Fatalf("error in backend verifications on metadata received: %v", err)
}
}

56
interop/xds/go.mod Normal file
View File

@@ -0,0 +1,56 @@
module google.golang.org/grpc/interop/xds
go 1.25.0
replace google.golang.org/grpc => ../..
require (
github.com/prometheus/client_golang v1.24.1
go.opentelemetry.io/otel/exporters/prometheus v0.67.0
go.opentelemetry.io/otel/sdk/metric v1.45.0
google.golang.org/grpc v1.83.0
)
require (
cel.dev/expr v0.25.3 // indirect
cloud.google.com/go/auth v0.23.1 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/api v0.293.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)

119
interop/xds/go.sum Normal file
View File

@@ -0,0 +1,119 @@
cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0=
cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go/auth v0.23.1 h1:1tPpBPG02lQHmoiAvs9egyCASqXP0xgobptjZzov/Jg=
cloud.google.com/go/auth v0.23.1/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 h1:bN1gA3of5bXtbnLsRPrwfmbbe7A5UWFlcTHseujLnpc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0/go.mod h1:Yj5vHEz/aAepZGliRJsA6uvHAVAQyEwajq9ORCHPxzM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be h1:SWe0x6yfglnxuvOiYgTTnNq7QD/yvqthOh1RBI8Bj8w=
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be/go.mod h1:PYEOlng9XcrulfyWpm49jECTPV0LT4q8cO7fLW/xwgk=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU=
github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.45.0 h1:9jR0ZPRok9ryaOQ2Wx8rg5F7Aon59mxrqbVI60/vlBk=
go.opentelemetry.io/contrib/detectors/gcp v1.45.0/go.mod h1:VSme3o2fvSg5bVg0dRzyHaj4Z5EVhG+g2Fde6LKzmQA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/prometheus v0.67.0 h1:7IefDa35e6V3NoiqIeLDMDxMFyZDk5qcoC0Ax4cC16E=
go.opentelemetry.io/otel/exporters/prometheus v0.67.0/go.mod h1:nsPI1awTg5Vmg1YrommL2mVarVGlqc4yXOoKAkPRD0c=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.293.0 h1:p9XIWOf63U4OgYx120ZwVU8+vl4XTPmWfgVPnmOAS9w=
google.golang.org/api v0.293.0/go.mod h1:6n5tjEB1gzwniZTepZ0g5u+wM7Bof5GeULCx/zh8ZE0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 h1:lrupDmKL3p5kEX1M92oan027eCKcouzjuPbH6YBK+Rs=
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,37 @@
# 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.
# Dockerfile for building the xDS interop server. To build the image, run the
# following command from grpc-go directory:
# docker build -t <TAG> -f interop/xds/server/Dockerfile .
FROM golang:1.25-alpine as build
# Make a grpc-go directory and copy the repo into it.
WORKDIR /go/src/grpc-go
COPY . .
# Build a static binary without cgo so that we can copy just the binary in the
# final image, and can get rid of the Go compiler and gRPC-Go dependencies.
RUN cd interop/xds/server && go build -tags osusergo,netgo .
# Second stage of the build which copies over only the server binary and skips
# the Go compiler and gRPC repo from the earlier stage. This significantly
# reduces the docker image size.
FROM alpine
COPY --from=build /go/src/grpc-go/interop/xds/server/server .
ENV GRPC_GO_LOG_VERBOSITY_LEVEL=99
ENV GRPC_GO_LOG_SEVERITY_LEVEL="info"
ENV GRPC_GO_LOG_FORMATTER="json"
ENTRYPOINT ["./server"]

View File

@@ -0,0 +1,342 @@
/*
*
* 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.
*
*/
// Binary server is the server used for xDS interop tests.
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/admin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/health"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/stats/opentelemetry"
"google.golang.org/grpc/stats/opentelemetry/csm"
"google.golang.org/grpc/status"
"google.golang.org/grpc/xds"
xdscreds "google.golang.org/grpc/credentials/xds"
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/sdk/metric"
)
var (
port = flag.Int("port", 8080, "Listening port for test service")
maintenancePort = flag.Int("maintenance_port", 8081, "Listening port for maintenance services like health, reflection, channelz etc when -secure_mode is true. When -secure_mode is false, all these services will be registered on -port")
serverID = flag.String("server_id", "go_server", "Server ID included in response")
secureMode = flag.Bool("secure_mode", false, "If true, retrieve security configuration from the management server. Else, use insecure credentials.")
hostNameOverride = flag.String("host_name_override", "", "If set, use this as the hostname instead of the real hostname")
enableCSMObservability = flag.Bool("enable_csm_observability", false, "Whether to enable CSM Observability")
logger = grpclog.Component("interop")
)
const (
rpcBehaviorMDKey = "rpc-behavior"
grpcPreviousRPCAttemptsMDKey = "grpc-previous-rpc-attempts"
sleepPfx = "sleep-"
keepOpenVal = "keep-open"
errorCodePfx = "error-code-"
succeedOnRetryPfx = "succeed-on-retry-attempt-"
hostnamePfx = "hostname="
)
func getHostname() string {
if *hostNameOverride != "" {
return *hostNameOverride
}
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("failed to get hostname: %v", err)
}
return hostname
}
// testServiceImpl provides an implementation of the TestService defined in
// grpc.testing package.
type testServiceImpl struct {
testgrpc.UnimplementedTestServiceServer
hostname string
serverID string
}
func (s *testServiceImpl) EmptyCall(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
grpc.SetHeader(ctx, metadata.Pairs("hostname", s.hostname))
return &testpb.Empty{}, nil
}
func (s *testServiceImpl) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
response := &testpb.SimpleResponse{ServerId: s.serverID, Hostname: s.hostname}
if in.ResponseSize > 0 {
response.Payload = &testpb.Payload{
Body: make([]byte, in.ResponseSize),
}
}
forLoop:
for _, headerVal := range getRPCBehaviorMetadata(ctx) {
// A value can have a prefix "hostname=<string>" followed by a space.
// In that case, the rest of the value should only be applied
// if the specified hostname matches the server's hostname.
if strings.HasPrefix(headerVal, hostnamePfx) {
splitVal := strings.Split(headerVal, " ")
if len(splitVal) <= 1 {
return nil, status.Errorf(codes.InvalidArgument, "invalid format for rpc-behavior header %v, must be 'hostname=<string> <header>=<value>' instead", headerVal)
}
if s.hostname != splitVal[0][len(hostnamePfx):] {
continue forLoop
}
headerVal = splitVal[1]
}
switch {
// If the value matches "sleep-<int>", the server should wait
// the specified number of seconds before resuming
// behavior matching and RPC processing.
case strings.HasPrefix(headerVal, sleepPfx):
sleep, err := strconv.Atoi(headerVal[len(sleepPfx):])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid format for rpc-behavior header %v, must be 'sleep-<int>' instead", headerVal)
}
time.Sleep(time.Duration(sleep) * time.Second)
// If the value matches "keep-open", the server should
// never respond to the request and behavior matching ends.
case strings.HasPrefix(headerVal, keepOpenVal):
<-ctx.Done()
return nil, nil
// If the value matches "error-code-<int>", the server should
// respond with the specified status code and behavior matching ends.
case strings.HasPrefix(headerVal, errorCodePfx):
code, err := strconv.Atoi(headerVal[len(errorCodePfx):])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid format for rpc-behavior header %v, must be 'error-code-<int>' instead", headerVal)
}
return nil, status.Errorf(codes.Code(code), "rpc failed as per the rpc-behavior header value: %v", headerVal)
// If the value matches "success-on-retry-attempt-<int>", and the
// value of the "grpc-previous-rpc-attempts" metadata field is equal to
// the specified number, the normal RPC processing should resume
// and behavior matching ends.
case strings.HasPrefix(headerVal, succeedOnRetryPfx):
wantRetry, err := strconv.Atoi(headerVal[len(succeedOnRetryPfx):])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid format for rpc-behavior header %v, must be 'success-on-retry-attempt-<int>' instead", headerVal)
}
mdRetry := getMetadataValues(ctx, grpcPreviousRPCAttemptsMDKey)
curRetry, err := strconv.Atoi(mdRetry[0])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid format for grpc-previous-rpc-attempts header: %v", mdRetry[0])
}
if curRetry == wantRetry {
break forLoop
}
}
}
grpc.SetHeader(ctx, metadata.Pairs("hostname", s.hostname))
return response, status.Error(codes.OK, "")
}
func getRPCBehaviorMetadata(ctx context.Context) []string {
mdRPCBehavior := getMetadataValues(ctx, rpcBehaviorMDKey)
var rpcBehaviorMetadata []string
for _, mdVal := range mdRPCBehavior {
splitVals := strings.Split(mdVal, ",")
for _, val := range splitVals {
headerVal := strings.TrimSpace(val)
if headerVal == "" {
continue
}
rpcBehaviorMetadata = append(rpcBehaviorMetadata, headerVal)
}
}
return rpcBehaviorMetadata
}
func getMetadataValues(ctx context.Context, metadataKey string) []string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
logger.Error("Failed to retrieve metadata from incoming RPC context")
return nil
}
return md.Get(metadataKey)
}
// xdsUpdateHealthServiceImpl provides an implementation of the
// XdsUpdateHealthService defined in grpc.testing package.
type xdsUpdateHealthServiceImpl struct {
testgrpc.UnimplementedXdsUpdateHealthServiceServer
healthServer *health.Server
}
func (x *xdsUpdateHealthServiceImpl) SetServing(_ context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
x.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
return &testpb.Empty{}, nil
}
func (x *xdsUpdateHealthServiceImpl) SetNotServing(_ context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
x.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
return &testpb.Empty{}, nil
}
func xdsServingModeCallback(addr net.Addr, args xds.ServingModeChangeArgs) {
logger.Infof("Serving mode callback for xDS server at %q invoked with mode: %q, err: %v", addr.String(), args.Mode, args.Err)
}
func main() {
flag.Parse()
if *enableCSMObservability {
exporter, err := prometheus.New()
if err != nil {
logger.Fatalf("Failed to start prometheus exporter: %v", err)
}
var addr string
var ok bool
if addr, ok = os.LookupEnv("OTEL_EXPORTER_PROMETHEUS_HOST"); !ok {
addr = ""
}
var port string
if port, ok = os.LookupEnv("OTEL_EXPORTER_PROMETHEUS_PORT"); !ok {
port = "9464"
}
go func() {
if err := http.ListenAndServe(addr+":"+port, promhttp.Handler()); err != nil {
logger.Fatalf("error listening: %v", err)
}
}()
provider := metric.NewMeterProvider(
metric.WithReader(exporter),
)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
cleanup := csm.EnableObservability(ctx, opentelemetry.Options{MetricsOptions: opentelemetry.MetricsOptions{MeterProvider: provider}})
defer cleanup()
}
if *secureMode && *port == *maintenancePort {
logger.Fatal("-port and -maintenance_port must be different when -secure_mode is set")
}
testService := &testServiceImpl{hostname: getHostname(), serverID: *serverID}
healthServer := health.NewServer()
updateHealthService := &xdsUpdateHealthServiceImpl{healthServer: healthServer}
// If -secure_mode is not set, expose all services on -port with a regular
// gRPC server.
if !*secureMode {
addr := fmt.Sprintf(":%d", *port)
lis, err := net.Listen("tcp4", addr)
if err != nil {
logger.Fatalf("net.Listen(%s) failed: %v", addr, err)
}
server := grpc.NewServer()
testgrpc.RegisterTestServiceServer(server, testService)
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthgrpc.RegisterHealthServer(server, healthServer)
testgrpc.RegisterXdsUpdateHealthServiceServer(server, updateHealthService)
reflection.Register(server)
cleanup, err := admin.Register(server)
if err != nil {
logger.Fatalf("Failed to register admin services: %v", err)
}
defer cleanup()
if err := server.Serve(lis); err != nil {
logger.Errorf("Serve() failed: %v", err)
}
return
}
// Create a listener on -port to expose the test service.
addr := fmt.Sprintf(":%d", *port)
testLis, err := net.Listen("tcp4", addr)
if err != nil {
logger.Fatalf("net.Listen(%s) failed: %v", addr, err)
}
// Create server-side xDS credentials with a plaintext fallback.
creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
if err != nil {
logger.Fatalf("Failed to create xDS credentials: %v", err)
}
// Create an xDS enabled gRPC server, register the test service
// implementation and start serving.
testServer, err := xds.NewGRPCServer(grpc.Creds(creds), xds.ServingModeCallback(xdsServingModeCallback))
if err != nil {
logger.Fatal("Failed to create an xDS enabled gRPC server: %v", err)
}
testgrpc.RegisterTestServiceServer(testServer, testService)
go func() {
if err := testServer.Serve(testLis); err != nil {
logger.Errorf("test server Serve() failed: %v", err)
}
}()
defer testServer.Stop()
// Create a listener on -maintenance_port to expose other services.
addr = fmt.Sprintf(":%d", *maintenancePort)
maintenanceLis, err := net.Listen("tcp4", addr)
if err != nil {
logger.Fatalf("net.Listen(%s) failed: %v", addr, err)
}
// Create a regular gRPC server and register the maintenance services on
// it and start serving.
maintenanceServer := grpc.NewServer()
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthgrpc.RegisterHealthServer(maintenanceServer, healthServer)
testgrpc.RegisterXdsUpdateHealthServiceServer(maintenanceServer, updateHealthService)
reflection.Register(maintenanceServer)
cleanup, err := admin.Register(maintenanceServer)
if err != nil {
logger.Fatalf("Failed to register admin services: %v", err)
}
defer cleanup()
if err := maintenanceServer.Serve(maintenanceLis); err != nil {
logger.Errorf("maintenance server Serve() failed: %v", err)
}
}