1/*
2 *
3 * Copyright 2020 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
19package client
20
21import (
22	"testing"
23
24	v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
25	v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
26	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
27	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
28	v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
29	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
30	"github.com/golang/protobuf/proto"
31	anypb "github.com/golang/protobuf/ptypes/any"
32	"github.com/google/go-cmp/cmp"
33	"github.com/google/go-cmp/cmp/cmpopts"
34	"google.golang.org/grpc/xds/internal/version"
35)
36
37const (
38	clusterName = "clusterName"
39	serviceName = "service"
40)
41
42var emptyUpdate = ClusterUpdate{ServiceName: "", EnableLRS: false}
43
44func (s) TestValidateCluster_Failure(t *testing.T) {
45	tests := []struct {
46		name       string
47		cluster    *v3clusterpb.Cluster
48		wantUpdate ClusterUpdate
49		wantErr    bool
50	}{
51		{
52			name: "non-eds-cluster-type",
53			cluster: &v3clusterpb.Cluster{
54				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC},
55				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
56					EdsConfig: &v3corepb.ConfigSource{
57						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
58							Ads: &v3corepb.AggregatedConfigSource{},
59						},
60					},
61				},
62				LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
63			},
64			wantUpdate: emptyUpdate,
65			wantErr:    true,
66		},
67		{
68			name: "no-eds-config",
69			cluster: &v3clusterpb.Cluster{
70				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
71				LbPolicy:             v3clusterpb.Cluster_ROUND_ROBIN,
72			},
73			wantUpdate: emptyUpdate,
74			wantErr:    true,
75		},
76		{
77			name: "no-ads-config-source",
78			cluster: &v3clusterpb.Cluster{
79				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
80				EdsClusterConfig:     &v3clusterpb.Cluster_EdsClusterConfig{},
81				LbPolicy:             v3clusterpb.Cluster_ROUND_ROBIN,
82			},
83			wantUpdate: emptyUpdate,
84			wantErr:    true,
85		},
86		{
87			name: "non-round-robin-lb-policy",
88			cluster: &v3clusterpb.Cluster{
89				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
90				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
91					EdsConfig: &v3corepb.ConfigSource{
92						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
93							Ads: &v3corepb.AggregatedConfigSource{},
94						},
95					},
96				},
97				LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
98			},
99			wantUpdate: emptyUpdate,
100			wantErr:    true,
101		},
102	}
103
104	for _, test := range tests {
105		t.Run(test.name, func(t *testing.T) {
106			if update, err := validateCluster(test.cluster); err == nil {
107				t.Errorf("validateCluster(%+v) = %v, wanted error", test.cluster, update)
108			}
109		})
110	}
111}
112
113func (s) TestValidateCluster_Success(t *testing.T) {
114	tests := []struct {
115		name       string
116		cluster    *v3clusterpb.Cluster
117		wantUpdate ClusterUpdate
118	}{
119		{
120			name: "happy-case-no-service-name-no-lrs",
121			cluster: &v3clusterpb.Cluster{
122				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
123				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
124					EdsConfig: &v3corepb.ConfigSource{
125						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
126							Ads: &v3corepb.AggregatedConfigSource{},
127						},
128					},
129				},
130				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
131			},
132			wantUpdate: emptyUpdate,
133		},
134		{
135			name: "happy-case-no-lrs",
136			cluster: &v3clusterpb.Cluster{
137				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
138				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
139					EdsConfig: &v3corepb.ConfigSource{
140						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
141							Ads: &v3corepb.AggregatedConfigSource{},
142						},
143					},
144					ServiceName: serviceName,
145				},
146				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
147			},
148			wantUpdate: ClusterUpdate{ServiceName: serviceName, EnableLRS: false},
149		},
150		{
151			name: "happiest-case",
152			cluster: &v3clusterpb.Cluster{
153				Name:                 clusterName,
154				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
155				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
156					EdsConfig: &v3corepb.ConfigSource{
157						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
158							Ads: &v3corepb.AggregatedConfigSource{},
159						},
160					},
161					ServiceName: serviceName,
162				},
163				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
164				LrsServer: &v3corepb.ConfigSource{
165					ConfigSourceSpecifier: &v3corepb.ConfigSource_Self{
166						Self: &v3corepb.SelfConfigSource{},
167					},
168				},
169			},
170			wantUpdate: ClusterUpdate{ServiceName: serviceName, EnableLRS: true},
171		},
172	}
173
174	for _, test := range tests {
175		t.Run(test.name, func(t *testing.T) {
176			update, err := validateCluster(test.cluster)
177			if err != nil {
178				t.Errorf("validateCluster(%+v) failed: %v", test.cluster, err)
179			}
180			if !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
181				t.Errorf("validateCluster(%+v) = %v, want: %v", test.cluster, update, test.wantUpdate)
182			}
183		})
184	}
185}
186
187func (s) TestValidateClusterWithSecurityConfig(t *testing.T) {
188	const (
189		identityPluginInstance = "identityPluginInstance"
190		identityCertName       = "identityCert"
191		rootPluginInstance     = "rootPluginInstance"
192		rootCertName           = "rootCert"
193		serviceName            = "service"
194		san1                   = "san1"
195		san2                   = "san2"
196	)
197
198	tests := []struct {
199		name       string
200		cluster    *v3clusterpb.Cluster
201		wantUpdate ClusterUpdate
202		wantErr    bool
203	}{
204		{
205			name: "transport-socket-unsupported-name",
206			cluster: &v3clusterpb.Cluster{
207				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
208				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
209					EdsConfig: &v3corepb.ConfigSource{
210						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
211							Ads: &v3corepb.AggregatedConfigSource{},
212						},
213					},
214					ServiceName: serviceName,
215				},
216				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
217				TransportSocket: &v3corepb.TransportSocket{
218					Name: "unsupported-foo",
219					ConfigType: &v3corepb.TransportSocket_TypedConfig{
220						TypedConfig: &anypb.Any{
221							TypeUrl: version.V3UpstreamTLSContextURL,
222						},
223					},
224				},
225			},
226			wantErr: true,
227		},
228		{
229			name: "transport-socket-unsupported-typeURL",
230			cluster: &v3clusterpb.Cluster{
231				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
232				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
233					EdsConfig: &v3corepb.ConfigSource{
234						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
235							Ads: &v3corepb.AggregatedConfigSource{},
236						},
237					},
238					ServiceName: serviceName,
239				},
240				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
241				TransportSocket: &v3corepb.TransportSocket{
242					ConfigType: &v3corepb.TransportSocket_TypedConfig{
243						TypedConfig: &anypb.Any{
244							TypeUrl: version.V3HTTPConnManagerURL,
245						},
246					},
247				},
248			},
249			wantErr: true,
250		},
251		{
252			name: "transport-socket-unsupported-type",
253			cluster: &v3clusterpb.Cluster{
254				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
255				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
256					EdsConfig: &v3corepb.ConfigSource{
257						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
258							Ads: &v3corepb.AggregatedConfigSource{},
259						},
260					},
261					ServiceName: serviceName,
262				},
263				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
264				TransportSocket: &v3corepb.TransportSocket{
265					ConfigType: &v3corepb.TransportSocket_TypedConfig{
266						TypedConfig: &anypb.Any{
267							TypeUrl: version.V3UpstreamTLSContextURL,
268							Value:   []byte{1, 2, 3, 4},
269						},
270					},
271				},
272			},
273			wantErr: true,
274		},
275		{
276			name: "transport-socket-unsupported-validation-context",
277			cluster: &v3clusterpb.Cluster{
278				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
279				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
280					EdsConfig: &v3corepb.ConfigSource{
281						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
282							Ads: &v3corepb.AggregatedConfigSource{},
283						},
284					},
285					ServiceName: serviceName,
286				},
287				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
288				TransportSocket: &v3corepb.TransportSocket{
289					ConfigType: &v3corepb.TransportSocket_TypedConfig{
290						TypedConfig: &anypb.Any{
291							TypeUrl: version.V3UpstreamTLSContextURL,
292							Value: func() []byte {
293								tls := &v3tlspb.UpstreamTlsContext{
294									CommonTlsContext: &v3tlspb.CommonTlsContext{
295										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextSdsSecretConfig{
296											ValidationContextSdsSecretConfig: &v3tlspb.SdsSecretConfig{
297												Name: "foo-sds-secret",
298											},
299										},
300									},
301								}
302								mtls, _ := proto.Marshal(tls)
303								return mtls
304							}(),
305						},
306					},
307				},
308			},
309			wantErr: true,
310		},
311		{
312			name: "happy-case-with-no-identity-certs",
313			cluster: &v3clusterpb.Cluster{
314				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
315				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
316					EdsConfig: &v3corepb.ConfigSource{
317						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
318							Ads: &v3corepb.AggregatedConfigSource{},
319						},
320					},
321					ServiceName: serviceName,
322				},
323				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
324				TransportSocket: &v3corepb.TransportSocket{
325					Name: "envoy.transport_sockets.tls",
326					ConfigType: &v3corepb.TransportSocket_TypedConfig{
327						TypedConfig: &anypb.Any{
328							TypeUrl: version.V3UpstreamTLSContextURL,
329							Value: func() []byte {
330								tls := &v3tlspb.UpstreamTlsContext{
331									CommonTlsContext: &v3tlspb.CommonTlsContext{
332										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
333											ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
334												InstanceName:    rootPluginInstance,
335												CertificateName: rootCertName,
336											},
337										},
338									},
339								}
340								mtls, _ := proto.Marshal(tls)
341								return mtls
342							}(),
343						},
344					},
345				},
346			},
347			wantUpdate: ClusterUpdate{
348				ServiceName: serviceName,
349				EnableLRS:   false,
350				SecurityCfg: &SecurityConfig{
351					RootInstanceName: rootPluginInstance,
352					RootCertName:     rootCertName,
353				},
354			},
355		},
356		{
357			name: "happy-case-with-validation-context-provider-instance",
358			cluster: &v3clusterpb.Cluster{
359				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
360				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
361					EdsConfig: &v3corepb.ConfigSource{
362						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
363							Ads: &v3corepb.AggregatedConfigSource{},
364						},
365					},
366					ServiceName: serviceName,
367				},
368				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
369				TransportSocket: &v3corepb.TransportSocket{
370					Name: "envoy.transport_sockets.tls",
371					ConfigType: &v3corepb.TransportSocket_TypedConfig{
372						TypedConfig: &anypb.Any{
373							TypeUrl: version.V3UpstreamTLSContextURL,
374							Value: func() []byte {
375								tls := &v3tlspb.UpstreamTlsContext{
376									CommonTlsContext: &v3tlspb.CommonTlsContext{
377										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
378											InstanceName:    identityPluginInstance,
379											CertificateName: identityCertName,
380										},
381										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
382											ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
383												InstanceName:    rootPluginInstance,
384												CertificateName: rootCertName,
385											},
386										},
387									},
388								}
389								mtls, _ := proto.Marshal(tls)
390								return mtls
391							}(),
392						},
393					},
394				},
395			},
396			wantUpdate: ClusterUpdate{
397				ServiceName: serviceName,
398				EnableLRS:   false,
399				SecurityCfg: &SecurityConfig{
400					RootInstanceName:     rootPluginInstance,
401					RootCertName:         rootCertName,
402					IdentityInstanceName: identityPluginInstance,
403					IdentityCertName:     identityCertName,
404				},
405			},
406		},
407		{
408			name: "happy-case-with-combined-validation-context",
409			cluster: &v3clusterpb.Cluster{
410				ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
411				EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
412					EdsConfig: &v3corepb.ConfigSource{
413						ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
414							Ads: &v3corepb.AggregatedConfigSource{},
415						},
416					},
417					ServiceName: serviceName,
418				},
419				LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
420				TransportSocket: &v3corepb.TransportSocket{
421					Name: "envoy.transport_sockets.tls",
422					ConfigType: &v3corepb.TransportSocket_TypedConfig{
423						TypedConfig: &anypb.Any{
424							TypeUrl: version.V3UpstreamTLSContextURL,
425							Value: func() []byte {
426								tls := &v3tlspb.UpstreamTlsContext{
427									CommonTlsContext: &v3tlspb.CommonTlsContext{
428										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
429											InstanceName:    identityPluginInstance,
430											CertificateName: identityCertName,
431										},
432										ValidationContextType: &v3tlspb.CommonTlsContext_CombinedValidationContext{
433											CombinedValidationContext: &v3tlspb.CommonTlsContext_CombinedCertificateValidationContext{
434												DefaultValidationContext: &v3tlspb.CertificateValidationContext{
435													MatchSubjectAltNames: []*v3matcherpb.StringMatcher{
436														{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: san1}},
437														{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: san2}},
438													},
439												},
440												ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
441													InstanceName:    rootPluginInstance,
442													CertificateName: rootCertName,
443												},
444											},
445										},
446									},
447								}
448								mtls, _ := proto.Marshal(tls)
449								return mtls
450							}(),
451						},
452					},
453				},
454			},
455			wantUpdate: ClusterUpdate{
456				ServiceName: serviceName,
457				EnableLRS:   false,
458				SecurityCfg: &SecurityConfig{
459					RootInstanceName:     rootPluginInstance,
460					RootCertName:         rootCertName,
461					IdentityInstanceName: identityPluginInstance,
462					IdentityCertName:     identityCertName,
463					AcceptedSANs:         []string{san1, san2},
464				},
465			},
466		},
467	}
468
469	for _, test := range tests {
470		t.Run(test.name, func(t *testing.T) {
471			update, err := validateCluster(test.cluster)
472			if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
473				t.Errorf("validateCluster(%+v) = (%+v, %v), want: (%+v, %v)", test.cluster, update, err, test.wantUpdate, test.wantErr)
474			}
475		})
476	}
477}
478
479func (s) TestUnmarshalCluster(t *testing.T) {
480	const (
481		v2ClusterName = "v2clusterName"
482		v3ClusterName = "v3clusterName"
483		v2Service     = "v2Service"
484		v3Service     = "v2Service"
485	)
486	var (
487		v2Cluster = &v2xdspb.Cluster{
488			Name:                 v2ClusterName,
489			ClusterDiscoveryType: &v2xdspb.Cluster_Type{Type: v2xdspb.Cluster_EDS},
490			EdsClusterConfig: &v2xdspb.Cluster_EdsClusterConfig{
491				EdsConfig: &v2corepb.ConfigSource{
492					ConfigSourceSpecifier: &v2corepb.ConfigSource_Ads{
493						Ads: &v2corepb.AggregatedConfigSource{},
494					},
495				},
496				ServiceName: v2Service,
497			},
498			LbPolicy: v2xdspb.Cluster_ROUND_ROBIN,
499			LrsServer: &v2corepb.ConfigSource{
500				ConfigSourceSpecifier: &v2corepb.ConfigSource_Self{
501					Self: &v2corepb.SelfConfigSource{},
502				},
503			},
504		}
505
506		v3Cluster = &v3clusterpb.Cluster{
507			Name:                 v3ClusterName,
508			ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
509			EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
510				EdsConfig: &v3corepb.ConfigSource{
511					ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
512						Ads: &v3corepb.AggregatedConfigSource{},
513					},
514				},
515				ServiceName: v3Service,
516			},
517			LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
518			LrsServer: &v3corepb.ConfigSource{
519				ConfigSourceSpecifier: &v3corepb.ConfigSource_Self{
520					Self: &v3corepb.SelfConfigSource{},
521				},
522			},
523		}
524	)
525
526	tests := []struct {
527		name       string
528		resources  []*anypb.Any
529		wantUpdate map[string]ClusterUpdate
530		wantErr    bool
531	}{
532		{
533			name:      "non-cluster resource type",
534			resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
535			wantErr:   true,
536		},
537		{
538			name: "badly marshaled cluster resource",
539			resources: []*anypb.Any{
540				{
541					TypeUrl: version.V3ClusterURL,
542					Value:   []byte{1, 2, 3, 4},
543				},
544			},
545			wantErr: true,
546		},
547		{
548			name: "bad cluster resource",
549			resources: []*anypb.Any{
550				{
551					TypeUrl: version.V3ClusterURL,
552					Value: func() []byte {
553						cl := &v3clusterpb.Cluster{
554							ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC},
555						}
556						mcl, _ := proto.Marshal(cl)
557						return mcl
558					}(),
559				},
560			},
561			wantErr: true,
562		},
563		{
564			name: "v2 cluster",
565			resources: []*anypb.Any{
566				{
567					TypeUrl: version.V3ClusterURL,
568					Value: func() []byte {
569						mcl, _ := proto.Marshal(v2Cluster)
570						return mcl
571					}(),
572				},
573			},
574			wantUpdate: map[string]ClusterUpdate{
575				v2ClusterName: {ServiceName: v2Service, EnableLRS: true},
576			},
577		},
578		{
579			name: "v3 cluster",
580			resources: []*anypb.Any{
581				{
582					TypeUrl: version.V3ClusterURL,
583					Value: func() []byte {
584						mcl, _ := proto.Marshal(v3Cluster)
585						return mcl
586					}(),
587				},
588			},
589			wantUpdate: map[string]ClusterUpdate{
590				v3ClusterName: {ServiceName: v3Service, EnableLRS: true},
591			},
592		},
593		{
594			name: "multiple clusters",
595			resources: []*anypb.Any{
596				{
597					TypeUrl: version.V3ClusterURL,
598					Value: func() []byte {
599						mcl, _ := proto.Marshal(v2Cluster)
600						return mcl
601					}(),
602				},
603				{
604					TypeUrl: version.V3ClusterURL,
605					Value: func() []byte {
606						mcl, _ := proto.Marshal(v3Cluster)
607						return mcl
608					}(),
609				},
610			},
611			wantUpdate: map[string]ClusterUpdate{
612				v2ClusterName: {ServiceName: v2Service, EnableLRS: true},
613				v3ClusterName: {ServiceName: v3Service, EnableLRS: true},
614			},
615		},
616	}
617	for _, test := range tests {
618		t.Run(test.name, func(t *testing.T) {
619			update, err := UnmarshalCluster(test.resources, nil)
620			if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
621				t.Errorf("UnmarshalCluster(%v) = (%+v, %v) want (%+v, %v)", test.resources, update, err, test.wantUpdate, test.wantErr)
622			}
623		})
624	}
625}
626