Skip to content

Commit 3e475d7

Browse files
authored
xdsclient: add test for custom xDS resource subscription (#8834)
Fixes #8449 RELEASE NOTES: none
1 parent 0dc1bcd commit 3e475d7

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package xdsclient_test
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"strings"
25+
"testing"
26+
27+
"github.com/google/uuid"
28+
"google.golang.org/grpc/credentials/insecure"
29+
"google.golang.org/grpc/internal/xds/clients"
30+
"google.golang.org/grpc/internal/xds/clients/grpctransport"
31+
"google.golang.org/grpc/internal/xds/clients/internal/testutils/fakeserver"
32+
"google.golang.org/grpc/internal/xds/clients/xdsclient"
33+
"google.golang.org/protobuf/proto"
34+
"google.golang.org/protobuf/types/known/anypb"
35+
"google.golang.org/protobuf/types/known/wrapperspb"
36+
37+
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
38+
)
39+
40+
// customTestResourceType is a fake resource type used for testing.
41+
var customTestResourceType = xdsclient.ResourceType{
42+
TypeURL: "type.googleapis.com/google.protobuf.StringValue",
43+
TypeName: "CustomResource",
44+
AllResourcesRequiredInSotW: false,
45+
Decoder: customTestDecoder{},
46+
}
47+
48+
type customTestDecoder struct{}
49+
50+
func (customTestDecoder) Decode(resource *xdsclient.AnyProto, _ xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
51+
any := resource.ToAny()
52+
if any.GetTypeUrl() != customTestResourceType.TypeURL {
53+
return nil, fmt.Errorf("unexpected resource type: %q", any.GetTypeUrl())
54+
}
55+
var val wrapperspb.StringValue
56+
if err := proto.Unmarshal(any.GetValue(), &val); err != nil {
57+
return nil, fmt.Errorf("failed to unmarshal resource: %v", err)
58+
}
59+
60+
// We expect the value to be "name|content". This allows us to extract the
61+
// name which is required by the xDS client to match the watch.
62+
str := val.GetValue()
63+
name, content, found := strings.Cut(str, "|")
64+
if !found {
65+
return nil, fmt.Errorf("invalid format: expected 'name|content', got %q", str)
66+
}
67+
68+
return &xdsclient.DecodeResult{
69+
Name: name,
70+
Resource: &customTestResourceData{val: content},
71+
}, nil
72+
}
73+
74+
type customTestResourceData struct {
75+
val string
76+
}
77+
78+
func (c *customTestResourceData) Equal(other xdsclient.ResourceData) bool {
79+
if c == nil && other == nil {
80+
return true
81+
}
82+
if c == nil || other == nil {
83+
return false
84+
}
85+
o, ok := other.(*customTestResourceData)
86+
if !ok {
87+
return false
88+
}
89+
return c.val == o.val
90+
}
91+
92+
func (c *customTestResourceData) Bytes() []byte {
93+
return []byte(c.val)
94+
}
95+
96+
// customTestWatcher is a watcher for the custom resource type.
97+
type customTestWatcher struct {
98+
updateCh chan xdsclient.ResourceData
99+
errCh chan error
100+
}
101+
102+
func newCustomTestWatcher() *customTestWatcher {
103+
return &customTestWatcher{
104+
updateCh: make(chan xdsclient.ResourceData, 1),
105+
errCh: make(chan error, 1),
106+
}
107+
}
108+
109+
func (w *customTestWatcher) ResourceChanged(update xdsclient.ResourceData, onDone func()) {
110+
w.updateCh <- update
111+
onDone()
112+
}
113+
114+
func (w *customTestWatcher) ResourceError(err error, onDone func()) {
115+
w.errCh <- fmt.Errorf("ResourceError: %v", err)
116+
onDone()
117+
}
118+
119+
func (w *customTestWatcher) AmbientError(err error, onDone func()) {
120+
w.errCh <- fmt.Errorf("AmbientError: %v", err)
121+
onDone()
122+
}
123+
124+
// Tests that the xDS client can watch a custom resource type that is injected
125+
// via the config.
126+
func (s) TestCustomResourceWatch(t *testing.T) {
127+
const authority = "my-authority"
128+
resourceName := "xdstp://" + authority + "/" + customTestResourceType.TypeURL + "/my-resource"
129+
130+
tests := []struct {
131+
name string
132+
resourceValue string
133+
wantUpdate string
134+
wantNACK string
135+
}{
136+
{
137+
name: "valid_resource",
138+
resourceValue: resourceName + "|hello world",
139+
wantUpdate: "hello world",
140+
},
141+
{
142+
name: "decode_error",
143+
resourceValue: "malformed-value",
144+
wantNACK: "invalid format: expected 'name|content'",
145+
},
146+
}
147+
148+
for _, test := range tests {
149+
t.Run(test.name, func(t *testing.T) {
150+
// Start a fake xDS management server.
151+
mgmtServer, cleanup, err := fakeserver.StartServer(nil)
152+
if err != nil {
153+
t.Fatalf("Failed to start fake xDS server: %v", err)
154+
}
155+
defer cleanup()
156+
157+
resourceTypes := map[string]xdsclient.ResourceType{
158+
customTestResourceType.TypeURL: customTestResourceType,
159+
}
160+
si := clients.ServerIdentifier{
161+
ServerURI: mgmtServer.Address,
162+
Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
163+
}
164+
165+
configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
166+
nodeID := uuid.New().String()
167+
xdsClientConfig := xdsclient.Config{
168+
Servers: []xdsclient.ServerConfig{{ServerIdentifier: si}},
169+
Node: clients.Node{ID: nodeID},
170+
TransportBuilder: grpctransport.NewBuilder(configs),
171+
ResourceTypes: resourceTypes,
172+
Authorities: map[string]xdsclient.Authority{
173+
authority: {XDSServers: []xdsclient.ServerConfig{}},
174+
},
175+
}
176+
177+
client, err := xdsclient.New(xdsClientConfig)
178+
if err != nil {
179+
t.Fatalf("Failed to create xDS client: %v", err)
180+
}
181+
defer client.Close()
182+
183+
watcher := newCustomTestWatcher()
184+
cancelWatch := client.WatchResource(customTestResourceType.TypeURL, resourceName, watcher)
185+
defer cancelWatch()
186+
187+
// Wait for the discovery request.
188+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
189+
defer cancel()
190+
191+
// Verify the request type URL.
192+
v, err := mgmtServer.XDSRequestChan.Receive(ctx)
193+
if err != nil {
194+
t.Fatalf("Timeout when waiting for a DiscoveryRequest message: %v", err)
195+
}
196+
req := v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
197+
if req.GetTypeUrl() != customTestResourceType.TypeURL {
198+
t.Fatalf("DiscoveryRequest TypeUrl = %v, want %v", req.GetTypeUrl(), customTestResourceType.TypeURL)
199+
}
200+
if len(req.GetResourceNames()) != 1 || req.GetResourceNames()[0] != resourceName {
201+
t.Fatalf("DiscoveryRequest ResourceNames = %v, want [%v]", req.GetResourceNames(), resourceName)
202+
}
203+
204+
// Send a response with the custom resource.
205+
resource, err := anypb.New(&wrapperspb.StringValue{Value: test.resourceValue})
206+
if err != nil {
207+
t.Fatalf("Failed to marshal resource: %v", err)
208+
}
209+
mgmtServer.XDSResponseChan <- &fakeserver.Response{
210+
Resp: &v3discoverypb.DiscoveryResponse{
211+
TypeUrl: customTestResourceType.TypeURL,
212+
VersionInfo: "1",
213+
Resources: []*anypb.Any{resource},
214+
},
215+
}
216+
217+
if test.wantNACK != "" {
218+
// Verify the NACK.
219+
// We expect a new DiscoveryRequest with ErrorDetail set.
220+
v, err = mgmtServer.XDSRequestChan.Receive(ctx)
221+
if err != nil {
222+
t.Fatalf("Timeout when waiting for NACK DiscoveryRequest message: %v", err)
223+
}
224+
req = v.(*fakeserver.Request).Req.(*v3discoverypb.DiscoveryRequest)
225+
if req.GetTypeUrl() != customTestResourceType.TypeURL {
226+
t.Fatalf("DiscoveryRequest TypeUrl = %v, want %v", req.GetTypeUrl(), customTestResourceType.TypeURL)
227+
}
228+
if req.GetErrorDetail() == nil {
229+
t.Fatalf("DiscoveryRequest ErrorDetail is nil, want non-nil")
230+
}
231+
if !strings.Contains(req.GetErrorDetail().GetMessage(), test.wantNACK) {
232+
t.Fatalf("DiscoveryRequest ErrorDetail = %v, want substring %q", req.GetErrorDetail().GetMessage(), test.wantNACK)
233+
}
234+
return
235+
}
236+
237+
// Verify the update.
238+
select {
239+
case <-ctx.Done():
240+
t.Fatalf("Timeout waiting for resource update")
241+
case err := <-watcher.errCh:
242+
t.Fatalf("Received unexpected error: %v", err)
243+
case got := <-watcher.updateCh:
244+
gotData, ok := got.(*customTestResourceData)
245+
if !ok {
246+
t.Fatalf("Received unexpected data type: %T", got)
247+
}
248+
if gotData.val != test.wantUpdate {
249+
t.Fatalf("Received resource value %q, want %q", gotData.val, test.wantUpdate)
250+
}
251+
}
252+
})
253+
}
254+
}

0 commit comments

Comments
 (0)