Skip to content

Commit 538d540

Browse files
committed
add tests to ensure reference semantics
On-behalf-of: @SAP [email protected]
1 parent 29cf649 commit 538d540

File tree

3 files changed

+243
-1
lines changed

3 files changed

+243
-1
lines changed

internal/sync/init_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
dummyv1alpha1 "github.com/kcp-dev/api-syncagent/internal/sync/apis/dummy/v1alpha1"
2323

24+
corev1 "k8s.io/api/core/v1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
"k8s.io/apimachinery/pkg/runtime"
2627
)
@@ -32,6 +33,9 @@ func init() {
3233
if err := dummyv1alpha1.AddToScheme(testScheme); err != nil {
3334
panic(err)
3435
}
36+
if err := corev1.AddToScheme(testScheme); err != nil {
37+
panic(err)
38+
}
3539
}
3640

3741
var nonEmptyTime = metav1.Time{

internal/sync/syncer_related.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ func resolveObjectReference(object *unstructured.Unstructured, ref syncagentv1al
457457
func resolveReference(jsonData []byte, ref syncagentv1alpha1.RelatedResourceObjectReference) (string, error) {
458458
gval := gjson.Get(string(jsonData), ref.Path)
459459
if !gval.Exists() {
460-
return "", fmt.Errorf("cannot find %s in document", ref.Path)
460+
return "", nil
461461
}
462462

463463
// this does apply some coalescing, like turning numbers into strings

internal/sync/syncer_related_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sync
18+
19+
import (
20+
"testing"
21+
22+
dummyv1alpha1 "github.com/kcp-dev/api-syncagent/internal/sync/apis/dummy/v1alpha1"
23+
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
24+
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
)
28+
29+
func TestResolveRelatedResourceObjects(t *testing.T) {
30+
// in kcp
31+
primaryObject := newUnstructured(&dummyv1alpha1.Thing{
32+
ObjectMeta: metav1.ObjectMeta{
33+
Name: "my-test-thing",
34+
},
35+
Spec: dummyv1alpha1.ThingSpec{
36+
Username: "original-value",
37+
Kink: "taxreturns",
38+
},
39+
}, withKind("RemoteThing"))
40+
41+
// on the service cluster
42+
primaryObjectCopy := newUnstructured(&dummyv1alpha1.Thing{
43+
ObjectMeta: metav1.ObjectMeta{
44+
Name: "my-test-thing",
45+
},
46+
Spec: dummyv1alpha1.ThingSpec{
47+
Username: "mutated-value",
48+
Kink: "",
49+
},
50+
})
51+
52+
// Create a secret that can be found by using a good reference, so we can ensure that references
53+
// do indeed work; all other subtests here ensure that reference support can deal with broken refs.
54+
dummySecret := newUnstructured(&corev1.Secret{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Namespace: "dummy-namespace",
57+
Name: "mutated-value",
58+
},
59+
})
60+
61+
kcpClient := buildFakeClient(primaryObject)
62+
serviceClusterClient := buildFakeClient(primaryObjectCopy, dummySecret)
63+
ctx := t.Context()
64+
65+
// Now we configure origin/dest as if we're syncing a Secret up from the service cluster to kcp,
66+
// i.e. origin=service.
67+
68+
originSide := syncSide{
69+
ctx: ctx,
70+
client: serviceClusterClient,
71+
object: primaryObjectCopy,
72+
}
73+
74+
destSide := syncSide{
75+
ctx: ctx,
76+
client: kcpClient,
77+
object: primaryObject,
78+
// Since this is a just a regular kube client, we do not need to set clusterName/clusterPath.
79+
}
80+
81+
testcases := []struct {
82+
name string
83+
objectSpec syncagentv1alpha1.RelatedResourceObject
84+
expectedSecrets int
85+
}{
86+
{
87+
name: "valid reference to an existing object",
88+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
89+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
90+
//nolint:staticcheck
91+
Reference: &syncagentv1alpha1.RelatedResourceObjectReference{
92+
Path: "spec.username",
93+
},
94+
},
95+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
96+
Template: &syncagentv1alpha1.TemplateExpression{
97+
Template: "dummy-namespace",
98+
},
99+
},
100+
},
101+
expectedSecrets: 1,
102+
},
103+
{
104+
name: "valid template to an existing object",
105+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
106+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
107+
Template: &syncagentv1alpha1.TemplateExpression{
108+
Template: "{{ .Object.spec.username }}",
109+
},
110+
},
111+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
112+
Template: &syncagentv1alpha1.TemplateExpression{
113+
Template: "dummy-namespace",
114+
},
115+
},
116+
},
117+
expectedSecrets: 1,
118+
},
119+
{
120+
name: "valid reference but target object doesn't exist [yet?]",
121+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
122+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
123+
//nolint:staticcheck
124+
Reference: &syncagentv1alpha1.RelatedResourceObjectReference{
125+
Path: "spec.username",
126+
},
127+
},
128+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
129+
Template: &syncagentv1alpha1.TemplateExpression{
130+
Template: "nonexisting-namespace",
131+
},
132+
},
133+
},
134+
expectedSecrets: 0,
135+
},
136+
{
137+
name: "valid template but target object doesn't exist [yet?]",
138+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
139+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
140+
Template: &syncagentv1alpha1.TemplateExpression{
141+
Template: "{{ .Object.spec.username }}",
142+
},
143+
},
144+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
145+
Template: &syncagentv1alpha1.TemplateExpression{
146+
Template: "nonexisting-namespace",
147+
},
148+
},
149+
},
150+
expectedSecrets: 0,
151+
},
152+
{
153+
name: "valid reference to an empty field",
154+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
155+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
156+
//nolint:staticcheck
157+
Reference: &syncagentv1alpha1.RelatedResourceObjectReference{
158+
Path: "spec.kink",
159+
},
160+
},
161+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
162+
Template: &syncagentv1alpha1.TemplateExpression{
163+
Template: "dummy-namespace",
164+
},
165+
},
166+
},
167+
expectedSecrets: 0,
168+
},
169+
{
170+
name: "valid template to an empty field",
171+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
172+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
173+
Template: &syncagentv1alpha1.TemplateExpression{
174+
Template: "{{ .Object.spec.kink }}",
175+
},
176+
},
177+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
178+
Template: &syncagentv1alpha1.TemplateExpression{
179+
Template: "dummy-namespace",
180+
},
181+
},
182+
},
183+
expectedSecrets: 0,
184+
},
185+
{
186+
name: "referring to an omitempty field",
187+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
188+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
189+
//nolint:staticcheck
190+
Reference: &syncagentv1alpha1.RelatedResourceObjectReference{
191+
Path: "spec.address",
192+
},
193+
},
194+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
195+
Template: &syncagentv1alpha1.TemplateExpression{
196+
Template: "dummy-namespace",
197+
},
198+
},
199+
},
200+
expectedSecrets: 0,
201+
},
202+
{
203+
name: "templating an omitempty field",
204+
objectSpec: syncagentv1alpha1.RelatedResourceObject{
205+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
206+
Template: &syncagentv1alpha1.TemplateExpression{
207+
Template: "{{ .Object.spec.address }}",
208+
},
209+
},
210+
Namespace: &syncagentv1alpha1.RelatedResourceObjectSpec{
211+
Template: &syncagentv1alpha1.TemplateExpression{
212+
Template: "dummy-namespace",
213+
},
214+
},
215+
},
216+
expectedSecrets: 0,
217+
},
218+
}
219+
220+
for _, testcase := range testcases {
221+
t.Run(testcase.name, func(t *testing.T) {
222+
pubRes := syncagentv1alpha1.RelatedResourceSpec{
223+
Identifier: "test",
224+
Origin: syncagentv1alpha1.RelatedResourceOriginService,
225+
Kind: "Secret",
226+
Object: testcase.objectSpec,
227+
}
228+
229+
foundObjects, err := resolveRelatedResourceObjects(originSide, destSide, pubRes)
230+
if err != nil {
231+
t.Fatalf("Failed to resolve related objects: %v", err)
232+
}
233+
if len(foundObjects) != testcase.expectedSecrets {
234+
t.Fatalf("Expected %d related object (Secret) to be found, but found %d.", testcase.expectedSecrets, len(foundObjects))
235+
}
236+
})
237+
}
238+
}

0 commit comments

Comments
 (0)