This is an automated email from the ASF dual-hosted git repository.
mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git
The following commit(s) were added to refs/heads/master by this push:
new 6262b0b3 Add request timeout (#927)
6262b0b3 is described below
commit 6262b0b38d3f269acd629e124066997d3122af9d
Author: mfordjody <[email protected]>
AuthorDate: Sun Jun 28 21:13:15 2026 +0800
Add request timeout (#927)
---
dubbod/discovery/cmd/app/cmd.go | 3 +
dubbod/discovery/cmd/app/testproto/xds_test.pb.go | 255 +++++++++++++++++++++
dubbod/discovery/cmd/app/testproto/xds_test.proto | 26 +++
.../cmd/app/testproto/xds_test_grpc.pb.go | 121 ++++++++++
dubbod/discovery/cmd/app/xds_client.go | 83 +++++--
dubbod/discovery/cmd/app/xds_client_test.go | 72 +++++-
dubbod/discovery/cmd/app/xds_test_grpc.go | 191 +++++++++++++++
dubbod/discovery/pkg/networking/grpcgen/rds.go | 31 ++-
.../discovery/pkg/networking/grpcgen/rds_test.go | 27 +++
go.mod | 2 +-
go.sum | 6 +-
11 files changed, 787 insertions(+), 30 deletions(-)
diff --git a/dubbod/discovery/cmd/app/cmd.go b/dubbod/discovery/cmd/app/cmd.go
index 4ab203fd..3a5dbe09 100644
--- a/dubbod/discovery/cmd/app/cmd.go
+++ b/dubbod/discovery/cmd/app/cmd.go
@@ -93,6 +93,9 @@ func newExecuteCommand() *cobra.Command {
if err := discoveryServer.Start(stop); err != nil {
return fmt.Errorf("failed to start discovery
service: %v", err)
}
+ if err := startXDSTestGRPCServer(stop); err != nil {
+ return fmt.Errorf("failed to start xDS test
gRPC server: %v", err)
+ }
// Wait for signal - when received, immediately exit
cmd.WaitSignal(stop)
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test.pb.go
b/dubbod/discovery/cmd/app/testproto/xds_test.pb.go
new file mode 100644
index 00000000..98f96d4e
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test.pb.go
@@ -0,0 +1,255 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v6.33.0
+// source: dubbod/discovery/cmd/app/testproto/xds_test.proto
+
+package testproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type ForwardHTTPRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Url string
`protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+ Path string
`protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
+ Count int32
`protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"`
+ Headers []string
`protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"`
+ RequestInterval string
`protobuf:"bytes,5,opt,name=request_interval,json=requestInterval,proto3"
json:"request_interval,omitempty"`
+ Expect string
`protobuf:"bytes,6,opt,name=expect,proto3" json:"expect,omitempty"`
+ RequestTimeout string
`protobuf:"bytes,7,opt,name=request_timeout,json=requestTimeout,proto3"
json:"request_timeout,omitempty"`
+ WaitTimeout string
`protobuf:"bytes,8,opt,name=wait_timeout,json=waitTimeout,proto3"
json:"wait_timeout,omitempty"`
+ Namespace string
`protobuf:"bytes,9,opt,name=namespace,proto3" json:"namespace,omitempty"`
+ ServiceAccount string
`protobuf:"bytes,10,opt,name=service_account,json=serviceAccount,proto3"
json:"service_account,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ForwardHTTPRequest) Reset() {
+ *x = ForwardHTTPRequest{}
+ mi :=
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ForwardHTTPRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ForwardHTTPRequest) ProtoMessage() {}
+
+func (x *ForwardHTTPRequest) ProtoReflect() protoreflect.Message {
+ mi :=
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ForwardHTTPRequest.ProtoReflect.Descriptor instead.
+func (*ForwardHTTPRequest) Descriptor() ([]byte, []int) {
+ return
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ForwardHTTPRequest) GetUrl() string {
+ if x != nil {
+ return x.Url
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetPath() string {
+ if x != nil {
+ return x.Path
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetCount() int32 {
+ if x != nil {
+ return x.Count
+ }
+ return 0
+}
+
+func (x *ForwardHTTPRequest) GetHeaders() []string {
+ if x != nil {
+ return x.Headers
+ }
+ return nil
+}
+
+func (x *ForwardHTTPRequest) GetRequestInterval() string {
+ if x != nil {
+ return x.RequestInterval
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetExpect() string {
+ if x != nil {
+ return x.Expect
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetRequestTimeout() string {
+ if x != nil {
+ return x.RequestTimeout
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetWaitTimeout() string {
+ if x != nil {
+ return x.WaitTimeout
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetNamespace() string {
+ if x != nil {
+ return x.Namespace
+ }
+ return ""
+}
+
+func (x *ForwardHTTPRequest) GetServiceAccount() string {
+ if x != nil {
+ return x.ServiceAccount
+ }
+ return ""
+}
+
+type ForwardHTTPResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Output []string
`protobuf:"bytes,1,rep,name=output,proto3" json:"output,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ForwardHTTPResponse) Reset() {
+ *x = ForwardHTTPResponse{}
+ mi :=
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ForwardHTTPResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ForwardHTTPResponse) ProtoMessage() {}
+
+func (x *ForwardHTTPResponse) ProtoReflect() protoreflect.Message {
+ mi :=
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ForwardHTTPResponse.ProtoReflect.Descriptor instead.
+func (*ForwardHTTPResponse) Descriptor() ([]byte, []int) {
+ return
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ForwardHTTPResponse) GetOutput() []string {
+ if x != nil {
+ return x.Output
+ }
+ return nil
+}
+
+var File_dubbod_discovery_cmd_app_testproto_xds_test_proto
protoreflect.FileDescriptor
+
+const file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc = "" +
+ "\n" +
+
"1dubbod/discovery/cmd/app/testproto/xds_test.proto\x12\x05proto\"\xc0\x02\n" +
+ "\x12ForwardHTTPRequest\x12\x10\n" +
+ "\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
+ "\x04path\x18\x02 \x01(\tR\x04path\x12\x14\n" +
+ "\x05count\x18\x03 \x01(\x05R\x05count\x12\x18\n" +
+ "\aheaders\x18\x04 \x03(\tR\aheaders\x12)\n" +
+ "\x10request_interval\x18\x05 \x01(\tR\x0frequestInterval\x12\x16\n" +
+ "\x06expect\x18\x06 \x01(\tR\x06expect\x12'\n" +
+ "\x0frequest_timeout\x18\a \x01(\tR\x0erequestTimeout\x12!\n" +
+ "\fwait_timeout\x18\b \x01(\tR\vwaitTimeout\x12\x1c\n" +
+ "\tnamespace\x18\t \x01(\tR\tnamespace\x12'\n" +
+ "\x0fservice_account\x18\n" +
+ " \x01(\tR\x0eserviceAccount\"-\n" +
+ "\x13ForwardHTTPResponse\x12\x16\n" +
+ "\x06output\x18\x01 \x03(\tR\x06output2V\n" +
+ "\x0eXDSTestService\x12D\n" +
+
"\vForwardHTTP\x12\x19.proto.ForwardHTTPRequest\x1a\x1a.proto.ForwardHTTPResponseBQZOgithub.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto;testprotob\x06proto3"
+
+var (
+ file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescOnce
sync.Once
+ file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData
[]byte
+)
+
+func file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP()
[]byte {
+
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescOnce.Do(func() {
+
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData =
protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc),
len(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc)))
+ })
+ return
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData
+}
+
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes =
make([]protoimpl.MessageInfo, 2)
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes = []any{
+ (*ForwardHTTPRequest)(nil), // 0: proto.ForwardHTTPRequest
+ (*ForwardHTTPResponse)(nil), // 1: proto.ForwardHTTPResponse
+}
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs = []int32{
+ 0, // 0: proto.XDSTestService.ForwardHTTP:input_type ->
proto.ForwardHTTPRequest
+ 1, // 1: proto.XDSTestService.ForwardHTTP:output_type ->
proto.ForwardHTTPResponse
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_dubbod_discovery_cmd_app_testproto_xds_test_proto_init() }
+func file_dubbod_discovery_cmd_app_testproto_xds_test_proto_init() {
+ if File_dubbod_discovery_cmd_app_testproto_xds_test_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor:
unsafe.Slice(unsafe.StringData(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc),
len(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes:
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes,
+ DependencyIndexes:
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs,
+ MessageInfos:
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes,
+ }.Build()
+ File_dubbod_discovery_cmd_app_testproto_xds_test_proto = out.File
+ file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes = nil
+ file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs = nil
+}
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test.proto
b/dubbod/discovery/cmd/app/testproto/xds_test.proto
new file mode 100644
index 00000000..73b18b52
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package proto;
+
+option go_package =
"github.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto;testproto";
+
+service XDSTestService {
+ rpc ForwardHTTP(ForwardHTTPRequest) returns (ForwardHTTPResponse);
+}
+
+message ForwardHTTPRequest {
+ string url = 1;
+ string path = 2;
+ int32 count = 3;
+ repeated string headers = 4;
+ string request_interval = 5;
+ string expect = 6;
+ string request_timeout = 7;
+ string wait_timeout = 8;
+ string namespace = 9;
+ string service_account = 10;
+}
+
+message ForwardHTTPResponse {
+ repeated string output = 1;
+}
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go
b/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go
new file mode 100644
index 00000000..22881a7a
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go
@@ -0,0 +1,121 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.6.1
+// - protoc v6.33.0
+// source: dubbod/discovery/cmd/app/testproto/xds_test.proto
+
+package testproto
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ XDSTestService_ForwardHTTP_FullMethodName =
"/proto.XDSTestService/ForwardHTTP"
+)
+
+// XDSTestServiceClient is the client API for XDSTestService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please
refer to
https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type XDSTestServiceClient interface {
+ ForwardHTTP(ctx context.Context, in *ForwardHTTPRequest, opts
...grpc.CallOption) (*ForwardHTTPResponse, error)
+}
+
+type xDSTestServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewXDSTestServiceClient(cc grpc.ClientConnInterface) XDSTestServiceClient
{
+ return &xDSTestServiceClient{cc}
+}
+
+func (c *xDSTestServiceClient) ForwardHTTP(ctx context.Context, in
*ForwardHTTPRequest, opts ...grpc.CallOption) (*ForwardHTTPResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ForwardHTTPResponse)
+ err := c.cc.Invoke(ctx, XDSTestService_ForwardHTTP_FullMethodName, in,
out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// XDSTestServiceServer is the server API for XDSTestService service.
+// All implementations must embed UnimplementedXDSTestServiceServer
+// for forward compatibility.
+type XDSTestServiceServer interface {
+ ForwardHTTP(context.Context, *ForwardHTTPRequest)
(*ForwardHTTPResponse, error)
+ mustEmbedUnimplementedXDSTestServiceServer()
+}
+
+// UnimplementedXDSTestServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedXDSTestServiceServer struct{}
+
+func (UnimplementedXDSTestServiceServer) ForwardHTTP(context.Context,
*ForwardHTTPRequest) (*ForwardHTTPResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ForwardHTTP not
implemented")
+}
+func (UnimplementedXDSTestServiceServer)
mustEmbedUnimplementedXDSTestServiceServer() {}
+func (UnimplementedXDSTestServiceServer) testEmbeddedByValue()
{}
+
+// UnsafeXDSTestServiceServer may be embedded to opt out of forward
compatibility for this service.
+// Use of this interface is not recommended, as added methods to
XDSTestServiceServer will
+// result in compilation errors.
+type UnsafeXDSTestServiceServer interface {
+ mustEmbedUnimplementedXDSTestServiceServer()
+}
+
+func RegisterXDSTestServiceServer(s grpc.ServiceRegistrar, srv
XDSTestServiceServer) {
+ // If the following call panics, it indicates
UnimplementedXDSTestServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at
initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&XDSTestService_ServiceDesc, srv)
+}
+
+func _XDSTestService_ForwardHTTP_Handler(srv interface{}, ctx context.Context,
dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor)
(interface{}, error) {
+ in := new(ForwardHTTPRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(XDSTestServiceServer).ForwardHTTP(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: XDSTestService_ForwardHTTP_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{},
error) {
+ return srv.(XDSTestServiceServer).ForwardHTTP(ctx,
req.(*ForwardHTTPRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// XDSTestService_ServiceDesc is the grpc.ServiceDesc for XDSTestService
service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var XDSTestService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "proto.XDSTestService",
+ HandlerType: (*XDSTestServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "ForwardHTTP",
+ Handler: _XDSTestService_ForwardHTTP_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "dubbod/discovery/cmd/app/testproto/xds_test.proto",
+}
diff --git a/dubbod/discovery/cmd/app/xds_client.go
b/dubbod/discovery/cmd/app/xds_client.go
index 57443df1..02e68c72 100644
--- a/dubbod/discovery/cmd/app/xds_client.go
+++ b/dubbod/discovery/cmd/app/xds_client.go
@@ -100,6 +100,7 @@ type xdsDestination struct {
type xdsRouteSnapshot struct {
Host string `json:"host"`
Port int `json:"port"`
+ Timeout string `json:"timeout,omitempty"`
Destinations []xdsDestination `json:"destinations"`
}
@@ -116,6 +117,7 @@ type sampleADSClient struct {
mu sync.RWMutex
subs map[string][]string
route map[string]uint32
+ routeTimeout time.Duration
endpoints map[string][]xdsEndpoint
clusterTLS map[string]*tlsv1.UpstreamTlsContext
updates chan struct{}
@@ -425,12 +427,13 @@ func (c *sampleADSClient) handleResponse(resp
*discovery.DiscoveryResponse) erro
return c.subscribe(v1.RouteType, routeNames)
}
case v1.RouteType:
- weights, clusters, err :=
routeWeightsFromRoutes(resp.Resources, c.path, c.requestHeaders)
+ weights, clusters, timeout, err :=
routeWeightsFromRoutes(resp.Resources, c.path, c.requestHeaders)
if err != nil {
return err
}
c.mu.Lock()
c.route = weights
+ c.routeTimeout = timeout
c.mu.Unlock()
c.notify()
if len(clusters) > 0 {
@@ -506,6 +509,9 @@ func (c *sampleADSClient) readySnapshot(expected
map[string]uint32) (xdsRouteSna
c.mu.RLock()
defer c.mu.RUnlock()
snapshot := xdsRouteSnapshot{Host: c.host, Port: c.port}
+ if c.routeTimeout > 0 {
+ snapshot.Timeout = c.routeTimeout.String()
+ }
for clusterName, weight := range c.route {
if weight == 0 {
continue
@@ -564,24 +570,32 @@ func routeNamesFromListeners(resources []*anypb.Any)
([]string, error) {
return sortedUnique(out), nil
}
-func routeWeightsFromRoutes(resources []*anypb.Any, requestPath string,
requestHeaders http.Header) (map[string]uint32, []string, error) {
+func routeWeightsFromRoutes(resources []*anypb.Any, requestPath string,
requestHeaders http.Header) (map[string]uint32, []string, time.Duration, error)
{
weights := map[string]uint32{}
for _, resource := range resources {
rc := &routev1.RouteConfiguration{}
if err := proto.Unmarshal(resource.Value, rc); err != nil {
- return nil, nil, err
+ return nil, nil, 0, err
}
for _, vh := range rc.GetVirtualHosts() {
for _, rt := range vh.GetRoutes() {
if !routeMatchesRequest(rt.GetMatch(),
requestPath, requestHeaders) {
continue
}
- addRouteActionWeights(weights, rt.GetRoute())
- return weights,
sortedWeightClusterNames(weights), nil
+ action := rt.GetRoute()
+ addRouteActionWeights(weights, action)
+ return weights,
sortedWeightClusterNames(weights), routeActionTimeout(action), nil
}
}
}
- return weights, sortedWeightClusterNames(weights), nil
+ return weights, sortedWeightClusterNames(weights), 0, nil
+}
+
+func routeActionTimeout(action *routev1.RouteAction) time.Duration {
+ if action == nil || action.GetTimeout() == nil {
+ return 0
+ }
+ return action.GetTimeout().AsDuration()
}
func addRouteActionWeights(weights map[string]uint32, action
*routev1.RouteAction) {
@@ -749,12 +763,18 @@ func destinationFromCluster(clusterName string, weight
uint32, endpoints []xdsEn
}
func runSampleRequests(ctx context.Context, adsClient *sampleADSClient,
snapshot xdsRouteSnapshot, count int, requestInterval, requestTimeout
time.Duration) error {
+ _, err := runSampleRequestsWithOutput(ctx, adsClient, snapshot, count,
requestInterval, requestTimeout, os.Stdout)
+ return err
+}
+
+func runSampleRequestsWithOutput(ctx context.Context, adsClient
*sampleADSClient, snapshot xdsRouteSnapshot, count int, requestInterval,
requestTimeout time.Duration, writer io.Writer) ([]string, error) {
picker, err := newSmoothWeightedPicker(snapshot)
if err != nil {
- return err
+ return nil, err
}
currentSignature := snapshotSignature(snapshot)
clients := newSampleRequestClients(adsClient, requestTimeout)
+ output := make([]string, 0, count)
for i := 0; i < count; i++ {
if updated, ok := adsClient.readySnapshot(nil); ok {
updatedSignature := snapshotSignature(updated)
@@ -763,22 +783,28 @@ func runSampleRequests(ctx context.Context, adsClient
*sampleADSClient, snapshot
currentSignature = updatedSignature
picker, err = newSmoothWeightedPicker(snapshot)
if err != nil {
- return err
+ return nil, err
}
}
}
destination, endpoint, err := picker.Next()
if err != nil {
- return err
+ return nil, err
}
httpClient, scheme, err :=
clients.clientForDestination(destination)
if err != nil {
- return err
+ return nil, err
}
- req, err := http.NewRequestWithContext(ctx, http.MethodGet,
+ requestCtx := ctx
+ cancel := func() {}
+ if timeout, ok := routeTimeoutDuration(snapshot.Timeout); ok {
+ requestCtx, cancel = context.WithTimeout(ctx, timeout)
+ }
+ req, err := http.NewRequestWithContext(requestCtx,
http.MethodGet,
fmt.Sprintf("%s://%s%s", scheme,
net.JoinHostPort(endpoint.Address, strconv.Itoa(int(endpoint.Port))),
adsClient.path), nil)
if err != nil {
- return err
+ cancel()
+ return nil, err
}
req.Host = snapshot.Host
for name, values := range adsClient.requestHeaders {
@@ -788,14 +814,20 @@ func runSampleRequests(ctx context.Context, adsClient
*sampleADSClient, snapshot
}
resp, err := httpClient.Do(req)
if err != nil {
- return err
+ cancel()
+ return nil, err
}
body, readErr := io.ReadAll(resp.Body)
_ = resp.Body.Close()
+ cancel()
if readErr != nil {
- return readErr
+ return nil, readErr
+ }
+ line := strings.TrimSpace(string(body))
+ output = append(output, line+"\n")
+ if writer != nil {
+ fmt.Fprintln(writer, line)
}
- fmt.Println(strings.TrimSpace(string(body)))
if requestInterval > 0 && i+1 < count {
timer := time.NewTimer(requestInterval)
select {
@@ -803,12 +835,23 @@ func runSampleRequests(ctx context.Context, adsClient
*sampleADSClient, snapshot
if !timer.Stop() {
<-timer.C
}
- return ctx.Err()
+ return output, ctx.Err()
case <-timer.C:
}
}
}
- return nil
+ return output, nil
+}
+
+func routeTimeoutDuration(value string) (time.Duration, bool) {
+ if value == "" {
+ return 0, false
+ }
+ timeout, err := time.ParseDuration(value)
+ if err != nil || timeout <= 0 {
+ return 0, false
+ }
+ return timeout, true
}
type sampleRequestClients struct {
@@ -1033,7 +1076,11 @@ func routeSummary(snapshot xdsRouteSnapshot) string {
for _, dest := range snapshot.Destinations {
parts = append(parts, fmt.Sprintf("%s=%d endpoints=%d",
destinationWeightKey(dest), dest.Weight, len(dest.Endpoints)))
}
- return fmt.Sprintf("%s:%d %s", snapshot.Host, snapshot.Port,
strings.Join(parts, ","))
+ timeout := ""
+ if snapshot.Timeout != "" {
+ timeout = " timeout=" + snapshot.Timeout
+ }
+ return fmt.Sprintf("%s:%d%s %s", snapshot.Host, snapshot.Port, timeout,
strings.Join(parts, ","))
}
func sortedUnique(values []string) []string {
diff --git a/dubbod/discovery/cmd/app/xds_client_test.go
b/dubbod/discovery/cmd/app/xds_client_test.go
index 7b759ede..4bf5d64e 100644
--- a/dubbod/discovery/cmd/app/xds_client_test.go
+++ b/dubbod/discovery/cmd/app/xds_client_test.go
@@ -17,6 +17,7 @@ import (
tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
routev1 "github.com/kdubbo/xds-api/route/v1"
"google.golang.org/protobuf/types/known/anypb"
+ "google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
@@ -245,6 +246,41 @@ func TestRunSampleRequestsUsesRequestPathAndHeaders(t
*testing.T) {
}
}
+func TestRunSampleRequestsUsesRouteTimeout(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w
http.ResponseWriter, r *http.Request) {
+ time.Sleep(50 * time.Millisecond)
+ _, _ = w.Write([]byte("late"))
+ }))
+ defer server.Close()
+
+ endpoint := endpointForServer(t, server)
+ snapshot := xdsRouteSnapshot{
+ Host: "reviews.moviereview.svc.cluster.local",
+ Port: 9080,
+ Timeout: "1ms",
+ Destinations: []xdsDestination{{
+ Cluster:
"outbound|9080|v1|reviews.moviereview.svc.cluster.local",
+ Host: "reviews.moviereview.svc.cluster.local",
+ Subset: "v1",
+ Weight: 100,
+ Endpoints: []xdsEndpoint{endpoint},
+ }},
+ }
+ client := &sampleADSClient{
+ host: snapshot.Host,
+ port: snapshot.Port,
+ path: "/",
+ route:
map[string]uint32{snapshot.Destinations[0].Cluster: 100},
+ routeTimeout: time.Millisecond,
+ endpoints:
map[string][]xdsEndpoint{snapshot.Destinations[0].Cluster:
[]xdsEndpoint{endpoint}},
+ }
+
+ err := runSampleRequests(context.Background(), client, snapshot, 1, 0,
time.Second)
+ if err == nil || !strings.Contains(err.Error(), "context deadline
exceeded") {
+ t.Fatalf("runSampleRequests() error = %v, want context deadline
exceeded", err)
+ }
+}
+
func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t *testing.T) {
resources := []*anypb.Any{mustAnyRouteConfig(t,
&routev1.RouteConfiguration{
VirtualHosts: []*routev1.VirtualHost{{
@@ -274,7 +310,7 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t
*testing.T) {
}},
})}
- weights, _, err := routeWeightsFromRoutes(resources, "/",
http.Header{"End-User": []string{"terminal-user"}})
+ weights, _, _, err := routeWeightsFromRoutes(resources, "/",
http.Header{"End-User": []string{"terminal-user"}})
if err != nil {
t.Fatalf("routeWeightsFromRoutes() error = %v", err)
}
@@ -282,7 +318,7 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t
*testing.T) {
t.Fatalf("terminal-user weights = %v, want v1=100 only",
weights)
}
- weights, _, err = routeWeightsFromRoutes(resources, "/", nil)
+ weights, _, _, err = routeWeightsFromRoutes(resources, "/", nil)
if err != nil {
t.Fatalf("routeWeightsFromRoutes() fallback error = %v", err)
}
@@ -293,6 +329,38 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t
*testing.T) {
}
}
+func TestRouteWeightsFromRoutesReadsRequestTimeout(t *testing.T) {
+ resources := []*anypb.Any{mustAnyRouteConfig(t,
&routev1.RouteConfiguration{
+ VirtualHosts: []*routev1.VirtualHost{{
+ Routes: []*routev1.Route{{
+ Match: &routev1.RouteMatch{PathSpecifier:
&routev1.RouteMatch_Prefix{Prefix: "/reviews"}},
+ Action: &routev1.Route_Route{
+ Route: &routev1.RouteAction{
+ ClusterSpecifier:
&routev1.RouteAction_WeightedClusters{
+ WeightedClusters:
&routev1.WeightedCluster{Clusters: []*routev1.WeightedCluster_ClusterWeight{{
+ Name:
"outbound|9080|v1|reviews.moviereview.svc.cluster.local",
+ Weight:
wrapperspb.UInt32(100),
+ }}},
+ },
+ Timeout: durationpb.New(500 *
time.Millisecond),
+ },
+ },
+ }},
+ }},
+ })}
+
+ weights, _, timeout, err := routeWeightsFromRoutes(resources,
"/reviews", nil)
+ if err != nil {
+ t.Fatalf("routeWeightsFromRoutes() error = %v", err)
+ }
+ if got :=
weights["outbound|9080|v1|reviews.moviereview.svc.cluster.local"]; got != 100 {
+ t.Fatalf("weights = %v, want v1=100", weights)
+ }
+ if timeout != 500*time.Millisecond {
+ t.Fatalf("timeout = %v, want 500ms", timeout)
+ }
+}
+
func TestSampleRequestClientsDoNotBypassMTLS(t *testing.T) {
clients := newSampleRequestClients(&sampleADSClient{}, time.Second)
destination := xdsDestination{
diff --git a/dubbod/discovery/cmd/app/xds_test_grpc.go
b/dubbod/discovery/cmd/app/xds_test_grpc.go
new file mode 100644
index 00000000..b9732b54
--- /dev/null
+++ b/dubbod/discovery/cmd/app/xds_test_grpc.go
@@ -0,0 +1,191 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 app
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ neturl "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ testproto
"github.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto"
+ "github.com/apache/dubbo-kubernetes/pkg/config/constants"
+ "github.com/apache/dubbo-kubernetes/pkg/log"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/reflection"
+ "google.golang.org/grpc/status"
+)
+
+const defaultXDSTestGRPCAddr = ":17171"
+
+type xdsTestGRPCServer struct {
+ testproto.UnimplementedXDSTestServiceServer
+}
+
+func startXDSTestGRPCServer(stop <-chan struct{}) error {
+ addr := firstNonEmpty(os.Getenv("XDS_TEST_GRPC_ADDR"),
defaultXDSTestGRPCAddr)
+ if strings.EqualFold(addr, "disabled") {
+ return nil
+ }
+ listener, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
+ }
+ server := grpc.NewServer()
+ testproto.RegisterXDSTestServiceServer(server, &xdsTestGRPCServer{})
+ reflection.Register(server)
+ go func() {
+ <-stop
+ server.GracefulStop()
+ }()
+ go func() {
+ if err := server.Serve(listener); err != nil {
+ log.Warnf("xDS test gRPC server stopped: %v", err)
+ }
+ }()
+ log.Infof("xDS test gRPC server listening on %s", addr)
+ return nil
+}
+
+func (s *xdsTestGRPCServer) ForwardHTTP(ctx context.Context, req
*testproto.ForwardHTTPRequest) (*testproto.ForwardHTTPResponse, error) {
+ opts, err := xdsOptionsFromForwardHTTPRequest(req)
+ if err != nil {
+ return nil, status.Error(codes.InvalidArgument, err.Error())
+ }
+ expected, err := parseExpectedWeights(opts.expect)
+ if err != nil {
+ return nil, status.Error(codes.InvalidArgument, err.Error())
+ }
+ client, err := newSampleADSClient(ctx, opts)
+ if err != nil {
+ return nil, status.Error(codes.Unavailable, err.Error())
+ }
+ defer client.close()
+ if err := client.start(); err != nil {
+ return nil, status.Error(codes.Unavailable, err.Error())
+ }
+ snapshot, err := client.waitForRoute(ctx, expected, opts.waitTimeout)
+ if err != nil {
+ return nil, status.Error(codes.FailedPrecondition, err.Error())
+ }
+ output, err := runSampleRequestsWithOutput(ctx, client, snapshot,
opts.count, opts.requestInterval, opts.requestTimeout, io.Discard)
+ if err != nil {
+ return nil, status.Error(codes.Unknown, err.Error())
+ }
+ return &testproto.ForwardHTTPResponse{Output: output}, nil
+}
+
+func xdsOptionsFromForwardHTTPRequest(req *testproto.ForwardHTTPRequest)
(*xdsClientOptions, error) {
+ if req == nil {
+ return nil, fmt.Errorf("request is required")
+ }
+ host, port, err := parseXDSForwardURL(req.GetUrl())
+ if err != nil {
+ return nil, err
+ }
+ namespace := firstNonEmpty(req.GetNamespace(),
namespaceFromServiceHost(host), os.Getenv("POD_NAMESPACE"), "default")
+ serviceAccount := firstNonEmpty(req.GetServiceAccount(), namespace)
+ requestHeaders, err := parseRequestHeaders(req.GetHeaders())
+ if err != nil {
+ return nil, err
+ }
+ requestInterval, err := durationField(req.GetRequestInterval(), 0)
+ if err != nil {
+ return nil, fmt.Errorf("invalid requestInterval: %w", err)
+ }
+ requestTimeout, err := durationField(req.GetRequestTimeout(),
5*time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("invalid requestTimeout: %w", err)
+ }
+ waitTimeout, err := durationField(req.GetWaitTimeout(), 30*time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("invalid waitTimeout: %w", err)
+ }
+ count := int(req.GetCount())
+ if count <= 0 {
+ count = 1
+ }
+ return &xdsClientOptions{
+ host: host,
+ port: port,
+ path: normalizeRequestPath(req.GetPath()),
+ requestHeaders: requestHeaders,
+ target: net.JoinHostPort(host, strconv.Itoa(port)),
+ xdsAddress:
firstNonEmpty(os.Getenv("XDS_TEST_XDS_ADDRESS"), "127.0.0.1:26010"),
+ namespace: namespace,
+ podName: firstNonEmpty(os.Getenv("POD_NAME"),
os.Getenv("HOSTNAME"), "xclient"),
+ podIP: firstNonEmpty(os.Getenv("INSTANCE_IP"),
os.Getenv("POD_IP"), "127.0.0.1"),
+ serviceAccount: serviceAccount,
+ trustDomain: firstNonEmpty(os.Getenv("TRUST_DOMAIN"),
constants.DefaultClusterLocalDomain),
+ domainSuffix: firstNonEmpty(os.Getenv("DOMAIN_SUFFIX"),
constants.DefaultClusterLocalDomain),
+ clusterID:
firstNonEmpty(os.Getenv("DUBBO_META_CLUSTER_ID"),
string(constants.DefaultClusterName)),
+ count: count,
+ expect: req.GetExpect(),
+ waitTimeout: waitTimeout,
+ requestInterval: requestInterval,
+ requestTimeout: requestTimeout,
+ insecure: true,
+ }, nil
+}
+
+func parseXDSForwardURL(raw string) (string, int, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", 0, fmt.Errorf("url is required")
+ }
+ parsed, err := neturl.Parse(raw)
+ if err != nil {
+ return "", 0, err
+ }
+ if parsed.Scheme != "xds" {
+ return "", 0, fmt.Errorf("unsupported url scheme %q, want xds",
parsed.Scheme)
+ }
+ target := parsed.Host
+ if target == "" {
+ target = strings.TrimPrefix(parsed.Path, "/")
+ }
+ target = strings.TrimSuffix(target, "/")
+ host, portValue, err := net.SplitHostPort(target)
+ if err != nil {
+ return "", 0, fmt.Errorf("invalid xds url target %q: %w",
target, err)
+ }
+ port, err := strconv.Atoi(portValue)
+ if err != nil {
+ return "", 0, fmt.Errorf("invalid xds url port %q: %w",
portValue, err)
+ }
+ return host, port, nil
+}
+
+func namespaceFromServiceHost(host string) string {
+ parts := strings.Split(host, ".")
+ if len(parts) >= 4 && parts[2] == "svc" && parts[1] != "" {
+ return parts[1]
+ }
+ return ""
+}
+
+func durationField(value string, defaultValue time.Duration) (time.Duration,
error) {
+ if strings.TrimSpace(value) == "" {
+ return defaultValue, nil
+ }
+ return time.ParseDuration(value)
+}
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds.go
b/dubbod/discovery/pkg/networking/grpcgen/rds.go
index 55d3f778..249ab63a 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds.go
@@ -20,6 +20,7 @@ import (
"fmt"
"strconv"
"strings"
+ "time"
"github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/util/protoconv"
discovery "github.com/kdubbo/xds-api/service/discovery/v1"
@@ -29,6 +30,7 @@ import (
"github.com/apache/dubbo-kubernetes/pkg/config/host"
route "github.com/kdubbo/xds-api/route/v1"
matcher "github.com/kdubbo/xds-api/type/matcher/v1"
+ "google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1"
)
@@ -413,14 +415,21 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes
[]config.Config, hostName host.N
// Build route match from HTTPRoute matches
routeMatch :=
buildRouteMatchFromHTTPRouteMatches(rule.Matches)
+ routeAction := &route.RouteAction{
+ ClusterSpecifier:
&route.RouteAction_WeightedClusters{
+ WeightedClusters: weightedClusters,
+ },
+ }
+ if rule.Timeouts != nil {
+ if timeout :=
gatewayAPIDurationToProto(rule.Timeouts.Request); timeout != nil {
+ routeAction.Timeout = timeout
+ }
+ }
+
builtRoute := &route.Route{
Match: routeMatch,
Action: &route.Route_Route{
- Route: &route.RouteAction{
- ClusterSpecifier:
&route.RouteAction_WeightedClusters{
- WeightedClusters:
weightedClusters,
- },
- },
+ Route: routeAction,
},
}
@@ -433,6 +442,18 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes
[]config.Config, hostName host.N
return allRoutes
}
+func gatewayAPIDurationToProto(duration *sigsk8siogatewayapiapisv1.Duration)
*durationpb.Duration {
+ if duration == nil {
+ return nil
+ }
+ parsed, err := time.ParseDuration(string(*duration))
+ if err != nil {
+ log.Warnf("invalid HTTPRoute timeout duration %q: %v",
*duration, err)
+ return nil
+ }
+ return durationpb.New(parsed)
+}
+
// filterHTTPRoutesByGateway filters HTTPRoutes by parentRef to match the
given Gateway
func filterHTTPRoutesByGateway(httpRoutes []config.Config, gatewayName,
gatewayNamespace string, port int) []config.Config {
if gatewayName == "" {
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
index 9573a07f..7b28cc3c 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
@@ -18,6 +18,7 @@ package grpcgen
import (
"reflect"
"testing"
+ "time"
"github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/config/memory"
"github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
@@ -108,6 +109,32 @@ func
TestBuildHTTPRouteProxylessOutboundUsesServiceAttachedHTTPRoute(t *testing.
}
}
+func TestBuildHTTPRouteSetsGatewayAPIRequestTimeout(t *testing.T) {
+ cfg := newServiceAttachedHTTPRouteConfig("reviews-timeout",
"moviereview", "reviews", 9080)
+ spec := cfg.Spec.(*gatewayv1.HTTPRouteSpec)
+ spec.Rules[0].Timeouts = &gatewayv1.HTTPRouteTimeouts{
+ Request: ptrTo(gatewayv1.Duration("500ms")),
+ }
+ push := newRDSTestPushContext(t, []config.Config{cfg}, []*model.Service{
+ newRDSTestService("reviews", "moviereview",
"reviews.moviereview.svc.cluster.local", 9080),
+ newRDSTestService("reviews-v1", "moviereview",
"reviews-v1.moviereview.svc.cluster.local", 9080),
+ newRDSTestService("reviews-v2", "moviereview",
"reviews-v2.moviereview.svc.cluster.local", 9080),
+ })
+
+ rc := buildHTTPRoute(
+ &model.Proxy{ID: "moviepage.moviereview", Type:
model.Proxyless},
+ push,
+ "outbound|9080||reviews.moviereview.svc.cluster.local",
+ )
+ if rc == nil {
+ t.Fatal("buildHTTPRoute() returned nil")
+ }
+ timeout := rc.VirtualHosts[0].Routes[0].GetRoute().GetTimeout()
+ if timeout == nil || timeout.AsDuration() != 500*time.Millisecond {
+ t.Fatalf("timeout = %v, want 500ms", timeout)
+ }
+}
+
func newRDSTestPushContext(t *testing.T, configs []config.Config, services
[]*model.Service) *model.PushContext {
t.Helper()
diff --git a/go.mod b/go.mod
index 14e0205f..842f6187 100644
--- a/go.mod
+++ b/go.mod
@@ -55,7 +55,7 @@ require (
github.com/heroku/color v0.0.6
github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56
github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5
- github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f
+ github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162
github.com/moby/term v0.5.2
github.com/ory/viper v1.7.5
github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 52a98f00..c2e32c9c 100644
--- a/go.sum
+++ b/go.sum
@@ -339,10 +339,8 @@ github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56
h1:yNpHQ+NqBCFVHG4mGJ9Q
github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56/go.mod
h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5
h1:ynMV7FtdzoEMBGPy8xvljPXt8fWTzFerZ2ZbSXS4erE=
github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5/go.mod
h1:NYuRXnoNWCR5uZLjLmWi1ACz+GYFHjckMtLJHWRI4VU=
-github.com/kdubbo/xds-api v0.0.0-20260618170142-573d9453aff2
h1:hOAD6w8dNGZL/C2LyWVgLQ/CZ3CqatWlyEb49nRcPWE=
-github.com/kdubbo/xds-api v0.0.0-20260618170142-573d9453aff2/go.mod
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
-github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f
h1:uO3+UyFwH5nyycY8VxHMt0tL4029SNrVknSxkyNf47g=
-github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f/go.mod
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162
h1:8T6FM/7KYiX84f8wMNmWxZ4YMv+uONCkZqTKiM6m/M8=
+github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162/go.mod
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
github.com/kevinburke/ssh_config v1.2.0
h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod
h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod
h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=