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 c723c6be Complete SDS delta xDS support (#924)
c723c6be is described below

commit c723c6be93e0e6f6ccb184f1d71fc2159ab2f98d
Author: mfordjody <[email protected]>
AuthorDate: Sat Jun 27 22:08:38 2026 +0800

    Complete SDS delta xDS support (#924)
    
    * Complete SDS delta xDS support
    
    * Use released xds-api secret resources
    
    * Update moviereview samples
---
 dubbod/security/pkg/nodeagent/sds/sdsservice.go    | 278 +++++++++++++++++++-
 .../security/pkg/nodeagent/sds/sdsservice_test.go  | 291 +++++++++++++++++++++
 go.mod                                             |   2 +-
 go.sum                                             |   2 +
 pkg/xds/server.go                                  |   3 +
 pkg/xds/server_test.go                             |  18 ++
 samples/moviereview/deployment.yaml                |   2 +-
 7 files changed, 589 insertions(+), 7 deletions(-)

diff --git a/dubbod/security/pkg/nodeagent/sds/sdsservice.go 
b/dubbod/security/pkg/nodeagent/sds/sdsservice.go
index c909b219..1ffe8abf 100644
--- a/dubbod/security/pkg/nodeagent/sds/sdsservice.go
+++ b/dubbod/security/pkg/nodeagent/sds/sdsservice.go
@@ -18,26 +18,34 @@ package sds
 
 import (
        "context"
-       "github.com/apache/dubbo-kubernetes/pkg/log"
+       "fmt"
+       "io"
        "strconv"
        "sync"
        "sync/atomic"
+       "time"
 
        "github.com/apache/dubbo-kubernetes/pkg/backoff"
+       "github.com/apache/dubbo-kubernetes/pkg/log"
+       xdsmodel "github.com/apache/dubbo-kubernetes/pkg/model"
        "github.com/apache/dubbo-kubernetes/pkg/security"
        "github.com/apache/dubbo-kubernetes/pkg/util/sets"
        "github.com/apache/dubbo-kubernetes/pkg/xds"
        core "github.com/kdubbo/xds-api/core/v1"
+       tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
        discovery "github.com/kdubbo/xds-api/service/discovery/v1"
        sds "github.com/kdubbo/xds-api/service/secret/v1"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
+       "google.golang.org/protobuf/types/known/anypb"
 )
 
 var sdsServiceLog = log.RegisterScope("sds", "SDS service debugging")
 
 var connectionNumber = int64(0)
+var deltaNonceNumber = int64(0)
+var versionNumber = int64(0)
 
 type sdsservice struct {
        sds.UnimplementedSecretDiscoveryServiceServer
@@ -117,11 +125,81 @@ func (s *sdsservice) StreamSecrets(stream 
sds.SecretDiscoveryService_StreamSecre
 }
 
 func (s *sdsservice) DeltaSecrets(stream 
sds.SecretDiscoveryService_DeltaSecretsServer) error {
-       return status.Error(codes.Unimplemented, "DeltaSecrets not implemented")
+       ctx := &Context{
+               BaseConnection: xds.NewConnection("", nil),
+               s:              s,
+               w:              &Watch{},
+       }
+       reqCh := make(chan *discovery.DeltaDiscoveryRequest, 1)
+       errCh := make(chan error, 1)
+       go func() {
+               defer close(reqCh)
+               for {
+                       req, err := stream.Recv()
+                       if err != nil {
+                               errCh <- err
+                               return
+                       }
+                       select {
+                       case reqCh <- req:
+                       case <-stream.Context().Done():
+                               errCh <- stream.Context().Err()
+                               return
+                       }
+               }
+       }()
+
+       initialized := false
+       for {
+               select {
+               case req, ok := <-reqCh:
+                       if !ok {
+                               err := <-errCh
+                               if err == io.EOF {
+                                       return nil
+                               }
+                               return err
+                       }
+                       if !initialized {
+                               if req.Node == nil || req.Node.Id == "" {
+                                       return 
status.New(codes.InvalidArgument, "missing node information").Err()
+                               }
+                               if err := ctx.Initialize(req.Node); err != nil {
+                                       return err
+                               }
+                               defer ctx.Close()
+                               initialized = true
+                       }
+                       shouldRespond, delta := ctx.shouldRespondDelta(req)
+                       if !shouldRespond {
+                               continue
+                       }
+                       resourceNames := ctx.deltaResourceNames(req.TypeUrl, 
delta)
+                       if err := ctx.sendDelta(stream, req.TypeUrl, 
resourceNames, delta.Unsubscribed.UnsortedList()); err != nil {
+                               return err
+                       }
+               case ev := <-ctx.XdsConnection().PushCh():
+                       secretName := ev.(string)
+                       if !ctx.w.requested(secretName) {
+                               continue
+                       }
+                       if err := ctx.sendDelta(stream, xdsmodel.SecretType, 
[]string{secretName}, nil); err != nil {
+                               return err
+                       }
+               case <-stream.Context().Done():
+                       if initialized {
+                               ctx.Close()
+                       }
+                       return stream.Context().Err()
+               }
+       }
 }
 
-func (s *sdsservice) FetchSecrets(ctx context.Context, discReq 
*discovery.DiscoveryRequest) (*discovery.DiscoveryResponse, error) {
-       return nil, status.Error(codes.Unimplemented, "FetchSecrets not 
implemented")
+func (s *sdsservice) FetchSecrets(_ context.Context, discReq 
*discovery.DiscoveryRequest) (*discovery.DiscoveryResponse, error) {
+       if discReq == nil {
+               return nil, status.New(codes.InvalidArgument, "missing 
discovery request").Err()
+       }
+       return s.generate(discReq.ResourceNames)
 }
 
 // register adds the SDS handle to the grpc server
@@ -130,7 +208,197 @@ func (s *sdsservice) register(rpcs *grpc.Server) {
 }
 
 func (s *sdsservice) generate(resourceNames []string) 
(*discovery.DiscoveryResponse, error) {
-       return &discovery.DiscoveryResponse{}, nil
+       resources := make([]*anypb.Any, 0, len(resourceNames))
+       for _, resourceName := range resourceNames {
+               secret, err := s.st.GenerateSecret(resourceName)
+               if err != nil {
+                       return nil, fmt.Errorf("failed to generate secret for 
%s: %v", resourceName, err)
+               }
+               if secret == nil {
+                       return nil, fmt.Errorf("failed to generate secret for 
%s: secret is nil", resourceName)
+               }
+               res, err := anypb.New(toXdsSecret(secret, resourceName))
+               if err != nil {
+                       return nil, fmt.Errorf("failed to marshal secret for 
%s: %v", resourceName, err)
+               }
+               resources = append(resources, res)
+       }
+       version := strconv.FormatInt(atomic.AddInt64(&versionNumber, 1), 10)
+       return &discovery.DiscoveryResponse{
+               TypeUrl:     xdsmodel.SecretType,
+               VersionInfo: version,
+               Nonce:       version,
+               Resources:   resources,
+       }, nil
+}
+
+func toXdsSecret(item *security.SecretItem, requestedName string) 
*tlsv1.Secret {
+       name := item.ResourceName
+       if name == "" {
+               name = requestedName
+       }
+       secret := &tlsv1.Secret{Name: name}
+       if name == security.RootCertReqResourceName {
+               secret.Type = &tlsv1.Secret_ValidationContext{
+                       ValidationContext: &tlsv1.CertificateValidationContext{
+                               TrustedCa: inlineBytes(item.RootCert),
+                       },
+               }
+               return secret
+       }
+       if cfg, ok := security.SdsCertificateConfigFromResourceName(name); ok 
&& cfg.IsRootCertificate() {
+               secret.Type = &tlsv1.Secret_ValidationContext{
+                       ValidationContext: &tlsv1.CertificateValidationContext{
+                               TrustedCa: inlineBytes(item.RootCert),
+                       },
+               }
+               return secret
+       }
+       secret.Type = &tlsv1.Secret_TlsCertificate{
+               TlsCertificate: &tlsv1.TlsCertificate{
+                       CertificateChain: inlineBytes(item.CertificateChain),
+                       PrivateKey:       inlineBytes(item.PrivateKey),
+               },
+       }
+       return secret
+}
+
+func inlineBytes(value []byte) *core.DataSource {
+       return &core.DataSource{
+               Specifier: &core.DataSource_InlineBytes{
+                       InlineBytes: value,
+               },
+       }
+}
+
+func (c *Context) shouldRespondDelta(req *discovery.DeltaDiscoveryRequest) 
(bool, xds.ResourceDelta) {
+       if req.ErrorDetail != nil {
+               errCode := codes.Code(req.ErrorDetail.Code)
+               sdsServiceLog.Warnf("%s: ACK ERROR %s %s:%s", 
xdsmodel.GetShortType(req.TypeUrl), c.XdsConnection().ID(), errCode.String(), 
req.ErrorDetail.GetMessage())
+               c.w.UpdateWatchedResource(req.TypeUrl, func(wr 
*xds.WatchedResource) *xds.WatchedResource {
+                       if wr == nil {
+                               wr = &xds.WatchedResource{TypeUrl: req.TypeUrl}
+                       }
+                       wr.LastError = req.ErrorDetail.GetMessage()
+                       return wr
+               })
+               return false, xds.ResourceDelta{}
+       }
+
+       previous := c.w.GetWatchedResource(req.TypeUrl)
+       if previous == nil {
+               names, _ := deltaWatchedSecrets(nil, req)
+               c.w.NewWatchedResource(req.TypeUrl, names.UnsortedList())
+               return true, xds.ResourceDelta{Subscribed: names, Unsubscribed: 
sets.New(req.ResourceNamesUnsubscribe...)}
+       }
+
+       if req.ResponseNonce != "" && req.ResponseNonce != previous.NonceSent {
+               sdsServiceLog.Debugf("%s: REQ %s expired nonce received %s, 
sent %s",
+                       xdsmodel.GetShortType(req.TypeUrl), 
c.XdsConnection().ID(), req.ResponseNonce, previous.NonceSent)
+               return false, xds.ResourceDelta{}
+       }
+
+       spontaneousReq := req.ResponseNonce == ""
+       var subChanged bool
+       var alwaysRespond bool
+       var subscribed sets.String
+       c.w.UpdateWatchedResource(req.TypeUrl, func(wr *xds.WatchedResource) 
*xds.WatchedResource {
+               if wr == nil {
+                       wr = &xds.WatchedResource{TypeUrl: req.TypeUrl}
+               }
+               before := wr.ResourceNames
+               wr.ResourceNames, subChanged = 
deltaWatchedSecrets(wr.ResourceNames, req)
+               subscribed = wr.ResourceNames.Difference(before)
+               if !spontaneousReq {
+                       wr.LastError = ""
+                       wr.NonceAcked = req.ResponseNonce
+               }
+               alwaysRespond = wr.AlwaysRespond
+               wr.AlwaysRespond = false
+               return wr
+       })
+
+       if !subChanged {
+               if alwaysRespond {
+                       return true, xds.ResourceDelta{}
+               }
+               return false, xds.ResourceDelta{}
+       }
+       return true, xds.ResourceDelta{
+               Subscribed:   subscribed,
+               Unsubscribed: sets.New(req.ResourceNamesUnsubscribe...),
+       }
+}
+
+func deltaWatchedSecrets(existing sets.String, req 
*discovery.DeltaDiscoveryRequest) (sets.String, bool) {
+       out := sets.New[string]()
+       if existing != nil {
+               out = existing.Copy()
+       }
+       before := out.Copy()
+       out.InsertAll(req.ResourceNamesSubscribe...)
+       for name := range req.InitialResourceVersions {
+               out.Insert(name)
+       }
+       out.DeleteAll(req.ResourceNamesUnsubscribe...)
+       return out, !out.Equals(before)
+}
+
+func (c *Context) deltaResourceNames(typeURL string, delta xds.ResourceDelta) 
[]string {
+       if len(delta.Subscribed) > 0 {
+               return delta.Subscribed.UnsortedList()
+       }
+       if len(delta.Unsubscribed) > 0 {
+               return nil
+       }
+       wr := c.w.GetWatchedResource(typeURL)
+       if wr == nil || wr.ResourceNames == nil {
+               return nil
+       }
+       return wr.ResourceNames.UnsortedList()
+}
+
+func (c *Context) sendDelta(stream 
sds.SecretDiscoveryService_DeltaSecretsServer, typeURL string, resourceNames 
[]string, removed []string) error {
+       res, err := c.s.generate(resourceNames)
+       if err != nil {
+               return err
+       }
+       nonce := strconv.FormatInt(atomic.AddInt64(&deltaNonceNumber, 1), 10)
+       resp := &discovery.DeltaDiscoveryResponse{
+               SystemVersionInfo: res.VersionInfo,
+               TypeUrl:           typeURL,
+               RemovedResources:  removed,
+               Nonce:             nonce,
+               ControlPlane:      res.ControlPlane,
+       }
+       if resp.SystemVersionInfo == "" {
+               resp.SystemVersionInfo = nonce
+       }
+       if res.TypeUrl != "" {
+               resp.TypeUrl = res.TypeUrl
+       }
+       for i, resource := range res.Resources {
+               name := ""
+               if i < len(resourceNames) {
+                       name = resourceNames[i]
+               }
+               resp.Resources = append(resp.Resources, &discovery.Resource{
+                       Name:     name,
+                       Resource: resource,
+               })
+       }
+       if err := stream.Send(resp); err != nil {
+               return err
+       }
+       c.w.UpdateWatchedResource(resp.TypeUrl, func(wr *xds.WatchedResource) 
*xds.WatchedResource {
+               if wr == nil {
+                       wr = &xds.WatchedResource{TypeUrl: resp.TypeUrl}
+               }
+               wr.NonceSent = resp.Nonce
+               wr.LastSendTime = time.Now()
+               return wr
+       })
+       return nil
 }
 
 func (s *sdsservice) push(secretName string) {
diff --git a/dubbod/security/pkg/nodeagent/sds/sdsservice_test.go 
b/dubbod/security/pkg/nodeagent/sds/sdsservice_test.go
new file mode 100644
index 00000000..bf9712c8
--- /dev/null
+++ b/dubbod/security/pkg/nodeagent/sds/sdsservice_test.go
@@ -0,0 +1,291 @@
+//
+// 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 sds
+
+import (
+       "bytes"
+       "context"
+       "io"
+       "testing"
+       "time"
+
+       xdsmodel "github.com/apache/dubbo-kubernetes/pkg/model"
+       "github.com/apache/dubbo-kubernetes/pkg/security"
+       "github.com/apache/dubbo-kubernetes/pkg/util/sets"
+       "github.com/apache/dubbo-kubernetes/pkg/xds"
+       core "github.com/kdubbo/xds-api/core/v1"
+       tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
+       discovery "github.com/kdubbo/xds-api/service/discovery/v1"
+       "google.golang.org/grpc/metadata"
+)
+
+func TestDeltaSecretsRespondsToSubscribeAndPush(t *testing.T) {
+       service := &sdsservice{
+               st: &fakeSecretManager{
+                       secrets: map[string]*security.SecretItem{
+                               "default": {
+                                       ResourceName:     "default",
+                                       CertificateChain: []byte("cert-v1"),
+                                       PrivateKey:       []byte("key-v1"),
+                               },
+                       },
+               },
+               stop:    make(chan struct{}),
+               clients: map[string]*Context{},
+       }
+       stream := newFakeDeltaSecretsStream()
+       errCh := make(chan error, 1)
+       go func() {
+               errCh <- service.DeltaSecrets(stream)
+       }()
+
+       stream.recvCh <- &discovery.DeltaDiscoveryRequest{
+               Node:                   &core.Node{Id: 
"proxyless~10.0.0.1~pod-1~app.svc.cluster.local"},
+               TypeUrl:                xdsmodel.SecretType,
+               ResourceNamesSubscribe: []string{"default"},
+       }
+       resp := stream.takeResponse(t)
+       if resp.TypeUrl != xdsmodel.SecretType {
+               t.Fatalf("TypeUrl = %s, want %s", resp.TypeUrl, 
xdsmodel.SecretType)
+       }
+       if resp.Nonce == "" || resp.SystemVersionInfo == "" {
+               t.Fatalf("nonce/version = %q/%q, want both set", resp.Nonce, 
resp.SystemVersionInfo)
+       }
+       assertTLSSecret(t, resp, "default", []byte("cert-v1"), []byte("key-v1"))
+
+       service.st.(*fakeSecretManager).secrets["default"] = 
&security.SecretItem{
+               ResourceName:     "default",
+               CertificateChain: []byte("cert-v2"),
+               PrivateKey:       []byte("key-v2"),
+       }
+       service.push("default")
+       pushResp := stream.takeResponse(t)
+       if pushResp.TypeUrl != xdsmodel.SecretType {
+               t.Fatalf("push TypeUrl = %s, want %s", pushResp.TypeUrl, 
xdsmodel.SecretType)
+       }
+       if pushResp.Nonce == resp.Nonce {
+               t.Fatalf("push nonce = %s, want a new nonce", pushResp.Nonce)
+       }
+       assertTLSSecret(t, pushResp, "default", []byte("cert-v2"), 
[]byte("key-v2"))
+
+       close(stream.recvCh)
+       select {
+       case err := <-errCh:
+               if err != nil {
+                       t.Fatalf("DeltaSecrets() error = %v", err)
+               }
+       case <-time.After(time.Second):
+               t.Fatal("DeltaSecrets() did not exit after client close")
+       }
+}
+
+func TestGenerateBuildsRootValidationContext(t *testing.T) {
+       service := &sdsservice{
+               st: &fakeSecretManager{
+                       secrets: map[string]*security.SecretItem{
+                               security.RootCertReqResourceName: {
+                                       ResourceName: 
security.RootCertReqResourceName,
+                                       RootCert:     []byte("root"),
+                               },
+                       },
+               },
+       }
+
+       resp, err := 
service.generate([]string{security.RootCertReqResourceName})
+       if err != nil {
+               t.Fatalf("generate() error = %v", err)
+       }
+       if resp.TypeUrl != xdsmodel.SecretType {
+               t.Fatalf("TypeUrl = %s, want %s", resp.TypeUrl, 
xdsmodel.SecretType)
+       }
+       if len(resp.Resources) != 1 {
+               t.Fatalf("resources = %d, want 1", len(resp.Resources))
+       }
+       secret := &tlsv1.Secret{}
+       if err := resp.Resources[0].UnmarshalTo(secret); err != nil {
+               t.Fatalf("unmarshal secret: %v", err)
+       }
+       if secret.GetName() != security.RootCertReqResourceName {
+               t.Fatalf("secret name = %s, want %s", secret.GetName(), 
security.RootCertReqResourceName)
+       }
+       if got := 
secret.GetValidationContext().GetTrustedCa().GetInlineBytes(); 
!bytes.Equal(got, []byte("root")) {
+               t.Fatalf("trusted CA = %q, want root", got)
+       }
+}
+
+func TestFetchSecretsReturnsGeneratedResources(t *testing.T) {
+       service := &sdsservice{
+               st: &fakeSecretManager{
+                       secrets: map[string]*security.SecretItem{
+                               "default": {
+                                       ResourceName:     "default",
+                                       CertificateChain: []byte("cert"),
+                                       PrivateKey:       []byte("key"),
+                               },
+                       },
+               },
+       }
+
+       resp, err := service.FetchSecrets(context.Background(), 
&discovery.DiscoveryRequest{
+               ResourceNames: []string{"default"},
+       })
+       if err != nil {
+               t.Fatalf("FetchSecrets() error = %v", err)
+       }
+       if len(resp.Resources) != 1 {
+               t.Fatalf("resources = %d, want 1", len(resp.Resources))
+       }
+       secret := &tlsv1.Secret{}
+       if err := resp.Resources[0].UnmarshalTo(secret); err != nil {
+               t.Fatalf("unmarshal secret: %v", err)
+       }
+       if got := 
secret.GetTlsCertificate().GetCertificateChain().GetInlineBytes(); 
!bytes.Equal(got, []byte("cert")) {
+               t.Fatalf("cert = %q, want cert", got)
+       }
+}
+
+func TestShouldRespondDeltaReportsNewSubscriptions(t *testing.T) {
+       ctx := &Context{
+               BaseConnection: xds.NewConnection("", nil),
+               w:              &Watch{},
+       }
+
+       shouldRespond, _ := 
ctx.shouldRespondDelta(&discovery.DeltaDiscoveryRequest{
+               TypeUrl:                xdsmodel.SecretType,
+               ResourceNamesSubscribe: []string{"default"},
+       })
+       if !shouldRespond {
+               t.Fatalf("initial subscribe should respond")
+       }
+
+       shouldRespond, delta := 
ctx.shouldRespondDelta(&discovery.DeltaDiscoveryRequest{
+               TypeUrl:                xdsmodel.SecretType,
+               ResourceNamesSubscribe: []string{"next"},
+       })
+       if !shouldRespond {
+               t.Fatalf("new subscribe should respond")
+       }
+       if !delta.Subscribed.Contains("next") {
+               t.Fatalf("Subscribed = %v, want next", 
delta.Subscribed.UnsortedList())
+       }
+       if delta.Subscribed.Contains("default") {
+               t.Fatalf("Subscribed = %v, want only newly subscribed 
resources", delta.Subscribed.UnsortedList())
+       }
+}
+
+func TestDeltaResourceNamesSkipsUnsubscribeOnlyResources(t *testing.T) {
+       ctx := &Context{w: &Watch{}}
+       ctx.w.NewWatchedResource(xdsmodel.SecretType, []string{"kept"})
+
+       names := ctx.deltaResourceNames(xdsmodel.SecretType, xds.ResourceDelta{
+               Unsubscribed: sets.New("removed"),
+       })
+       if len(names) != 0 {
+               t.Fatalf("resource names = %v, want none for unsubscribe-only 
delta", names)
+       }
+}
+
+type fakeSecretManager struct {
+       secrets map[string]*security.SecretItem
+}
+
+func (f *fakeSecretManager) GenerateSecret(resourceName string) 
(*security.SecretItem, error) {
+       return f.secrets[resourceName], nil
+}
+
+func assertTLSSecret(t *testing.T, resp *discovery.DeltaDiscoveryResponse, 
name string, cert, key []byte) {
+       t.Helper()
+       if len(resp.Resources) != 1 {
+               t.Fatalf("resources = %d, want 1", len(resp.Resources))
+       }
+       if resp.Resources[0].GetName() != name {
+               t.Fatalf("resource name = %s, want %s", 
resp.Resources[0].GetName(), name)
+       }
+       secret := &tlsv1.Secret{}
+       if err := resp.Resources[0].GetResource().UnmarshalTo(secret); err != 
nil {
+               t.Fatalf("unmarshal secret: %v", err)
+       }
+       if secret.GetName() != name {
+               t.Fatalf("secret name = %s, want %s", secret.GetName(), name)
+       }
+       if got := 
secret.GetTlsCertificate().GetCertificateChain().GetInlineBytes(); 
!bytes.Equal(got, cert) {
+               t.Fatalf("cert = %q, want %q", got, cert)
+       }
+       if got := secret.GetTlsCertificate().GetPrivateKey().GetInlineBytes(); 
!bytes.Equal(got, key) {
+               t.Fatalf("key = %q, want %q", got, key)
+       }
+}
+
+type fakeDeltaSecretsStream struct {
+       ctx    context.Context
+       recvCh chan *discovery.DeltaDiscoveryRequest
+       sendCh chan *discovery.DeltaDiscoveryResponse
+}
+
+func newFakeDeltaSecretsStream() *fakeDeltaSecretsStream {
+       return &fakeDeltaSecretsStream{
+               ctx:    context.Background(),
+               recvCh: make(chan *discovery.DeltaDiscoveryRequest, 8),
+               sendCh: make(chan *discovery.DeltaDiscoveryResponse, 8),
+       }
+}
+
+func (f *fakeDeltaSecretsStream) Send(resp *discovery.DeltaDiscoveryResponse) 
error {
+       f.sendCh <- resp
+       return nil
+}
+
+func (f *fakeDeltaSecretsStream) Recv() (*discovery.DeltaDiscoveryRequest, 
error) {
+       req, ok := <-f.recvCh
+       if !ok {
+               return nil, io.EOF
+       }
+       return req, nil
+}
+
+func (f *fakeDeltaSecretsStream) SetHeader(metadata.MD) error {
+       return nil
+}
+
+func (f *fakeDeltaSecretsStream) SendHeader(metadata.MD) error {
+       return nil
+}
+
+func (f *fakeDeltaSecretsStream) SetTrailer(metadata.MD) {}
+
+func (f *fakeDeltaSecretsStream) Context() context.Context {
+       return f.ctx
+}
+
+func (f *fakeDeltaSecretsStream) SendMsg(any) error {
+       return nil
+}
+
+func (f *fakeDeltaSecretsStream) RecvMsg(any) error {
+       return nil
+}
+
+func (f *fakeDeltaSecretsStream) takeResponse(t *testing.T) 
*discovery.DeltaDiscoveryResponse {
+       t.Helper()
+       select {
+       case resp := <-f.sendCh:
+               return resp
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for delta SDS response")
+               return nil
+       }
+}
diff --git a/go.mod b/go.mod
index 5fe70718..14e0205f 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-20260618170142-573d9453aff2
+       github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f
        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 c19e77a7..52a98f00 100644
--- a/go.sum
+++ b/go.sum
@@ -341,6 +341,8 @@ github.com/kdubbo/client-go 
v0.0.0-20260514125254-aa566bd2f6d5 h1:ynMV7FtdzoEMBG
 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/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=
diff --git a/pkg/xds/server.go b/pkg/xds/server.go
index ba999ab2..fd3cfa31 100644
--- a/pkg/xds/server.go
+++ b/pkg/xds/server.go
@@ -267,6 +267,9 @@ func (conn *Connection) StopCh() chan struct{} {
 }
 
 func (conn *Connection) StreamDone() <-chan struct{} {
+       if conn.stream == nil {
+               return conn.stop
+       }
        return conn.stream.Context().Done()
 }
 
diff --git a/pkg/xds/server_test.go b/pkg/xds/server_test.go
index 812abba1..2132844d 100644
--- a/pkg/xds/server_test.go
+++ b/pkg/xds/server_test.go
@@ -17,6 +17,7 @@ package xds
 
 import (
        "testing"
+       "time"
 
        "github.com/apache/dubbo-kubernetes/pkg/model"
        "github.com/apache/dubbo-kubernetes/pkg/util/sets"
@@ -56,6 +57,23 @@ func (w *testWatcher) GetID() string {
        return "test"
 }
 
+func TestStreamDoneHandlesNilStream(t *testing.T) {
+       conn := NewConnection("", nil)
+
+       select {
+       case <-conn.StreamDone():
+               t.Fatalf("StreamDone closed before StopCh")
+       default:
+       }
+
+       close(conn.StopCh())
+       select {
+       case <-conn.StreamDone():
+       case <-time.After(time.Second):
+               t.Fatalf("StreamDone did not close after StopCh")
+       }
+}
+
 func TestShouldRespondPreservesNonceAckWithoutResourceNames(t *testing.T) {
        watcher := newTestWatcher()
        routeName := "outbound|80||nginx.app.svc.cluster.local"
diff --git a/samples/moviereview/deployment.yaml 
b/samples/moviereview/deployment.yaml
index 92777d57..cd649908 100644
--- a/samples/moviereview/deployment.yaml
+++ b/samples/moviereview/deployment.yaml
@@ -34,7 +34,7 @@ spec:
   - name: http
     port: 80
     targetPort: 9080
-    nodePort: 30080
+    nodePort: 19080
     protocol: TCP
 ---
 apiVersion: v1

Reply via email to