Skip to content

Commit 94f1bbc

Browse files
committed
rewrite tests for related resources to be table driven
On-behalf-of: @SAP [email protected]
1 parent 3a61e1d commit 94f1bbc

File tree

4 files changed

+247
-117
lines changed

4 files changed

+247
-117
lines changed

test/crds/backup.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 crds
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
type Backup struct {
24+
metav1.TypeMeta `json:",inline"`
25+
metav1.ObjectMeta `json:"metadata,omitempty"`
26+
27+
Spec BackupSpec `json:"spec"`
28+
}
29+
30+
type BackupSpec struct {
31+
Source string `json:"source"`
32+
Destination string `json:"destination"`
33+
}

test/crds/crontab.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 crds
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
type Crontab struct {
24+
metav1.TypeMeta `json:",inline"`
25+
metav1.ObjectMeta `json:"metadata,omitempty"`
26+
27+
Spec CrontabSpec `json:"spec"`
28+
}
29+
30+
type CrontabSpec struct {
31+
CronSpec string `json:"cronSpec"`
32+
Image string `json:"image"`
33+
Replicas int `json:"replicas"`
34+
}

test/e2e/sync/related_test.go

Lines changed: 170 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -31,53 +31,60 @@ import (
3131

3232
"github.com/kcp-dev/api-syncagent/internal/test/diff"
3333
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
34+
"github.com/kcp-dev/api-syncagent/test/crds"
3435
"github.com/kcp-dev/api-syncagent/test/utils"
3536

3637
corev1 "k8s.io/api/core/v1"
38+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3739
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3940
"k8s.io/apimachinery/pkg/runtime/schema"
40-
"k8s.io/apimachinery/pkg/types"
4141
"k8s.io/apimachinery/pkg/util/wait"
4242
ctrlruntime "sigs.k8s.io/controller-runtime"
43+
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
4344
"sigs.k8s.io/controller-runtime/pkg/kontext"
4445
)
4546

46-
func TestSyncSecretBackToKcp(t *testing.T) {
47-
const (
48-
apiExportName = "kcp.example.com"
49-
orgWorkspace = "sync-related-secret-to-kcp"
50-
)
47+
func TestSyncRelatedObjects(t *testing.T) {
48+
const apiExportName = "kcp.example.com"
5149

52-
ctx := context.Background()
5350
ctrlruntime.SetLogger(logr.Discard())
5451

55-
// setup a test environment in kcp
56-
orgKubconfig := utils.CreateOrganization(t, ctx, orgWorkspace, apiExportName)
52+
testcases := []struct {
53+
// the name of this testcase
54+
name string
55+
//the org workspace everything should happen in
56+
workspace logicalcluster.Name
57+
// the configuration for the related resource
58+
relatedConfig syncagentv1alpha1.RelatedResourceSpec
59+
// the primary object created by the user in kcp
60+
mainResource crds.Crontab
61+
// the original related object (will automatically be created on either the
62+
// kcp or service side, depending on the relatedConfig above)
63+
sourceRelatedObject corev1.Secret
5764

58-
// start a service cluster
59-
envtestKubeconfig, envtestClient, _ := utils.RunEnvtest(t, []string{
60-
"test/crds/crontab.yaml",
61-
})
62-
63-
// publish Crontabs and Backups
64-
t.Logf("Publishing CRDs…")
65-
prCrontabs := &syncagentv1alpha1.PublishedResource{
66-
ObjectMeta: metav1.ObjectMeta{
67-
Name: "publish-crontabs",
68-
},
69-
Spec: syncagentv1alpha1.PublishedResourceSpec{
70-
Resource: syncagentv1alpha1.SourceResourceDescriptor{
71-
APIGroup: "example.com",
72-
Version: "v1",
73-
Kind: "CronTab",
74-
},
75-
// These rules make finding the local object easier, but should not be used in production.
76-
Naming: &syncagentv1alpha1.ResourceNaming{
77-
Name: "$remoteName",
78-
Namespace: "synced-$remoteNamespace",
65+
// expectation: this is how the copy of the related object should look
66+
// like after the sync has completed
67+
expectedSyncedRelatedObject corev1.Secret
68+
// expectation: how the original primary object should have been updated
69+
// (not the primary object's copy, but the source)
70+
//
71+
// not yet implemented
72+
// expectedUpdatedMainObject crds.Crontab
73+
}{
74+
{
75+
name: "sync referenced Secret up from service cluster to kcp",
76+
workspace: "sync-referenced-secret-up",
77+
mainResource: crds.Crontab{
78+
ObjectMeta: metav1.ObjectMeta{
79+
Name: "my-crontab",
80+
Namespace: "default",
81+
},
82+
Spec: crds.CrontabSpec{
83+
CronSpec: "* * *",
84+
Image: "ubuntu:latest",
85+
},
7986
},
80-
Related: []syncagentv1alpha1.RelatedResourceSpec{{
87+
relatedConfig: syncagentv1alpha1.RelatedResourceSpec{
8188
Identifier: "credentials",
8289
Origin: "service",
8390
Kind: "Secret",
@@ -101,106 +108,152 @@ func TestSyncSecretBackToKcp(t *testing.T) {
101108
},
102109
},
103110
},
104-
}},
111+
},
112+
sourceRelatedObject: corev1.Secret{
113+
ObjectMeta: metav1.ObjectMeta{
114+
Name: "my-credentials",
115+
Namespace: "synced-default",
116+
},
117+
Data: map[string][]byte{
118+
"password": []byte("hunter2"),
119+
},
120+
Type: corev1.SecretTypeOpaque,
121+
},
122+
123+
expectedSyncedRelatedObject: corev1.Secret{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: "my-credentials",
126+
Namespace: "default",
127+
},
128+
Data: map[string][]byte{
129+
"password": []byte("hunter2"),
130+
},
131+
Type: corev1.SecretTypeOpaque,
132+
},
105133
},
106134
}
107135

108-
if err := envtestClient.Create(ctx, prCrontabs); err != nil {
109-
t.Fatalf("Failed to create PublishedResource: %v", err)
110-
}
136+
for _, testcase := range testcases {
137+
t.Run(testcase.name, func(t *testing.T) {
138+
ctx := context.Background()
111139

112-
// start the agent in the background to update the APIExport with the CronTabs API
113-
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName)
114-
115-
// wait until the API is available
116-
teamCtx := kontext.WithCluster(ctx, logicalcluster.Name(fmt.Sprintf("root:%s:team-1", orgWorkspace)))
117-
kcpClient := utils.GetKcpAdminClusterClient(t)
118-
utils.WaitForBoundAPI(t, teamCtx, kcpClient, schema.GroupVersionResource{
119-
Group: apiExportName,
120-
Version: "v1",
121-
Resource: "crontabs",
122-
})
123-
124-
// create a Crontab object in a team workspace
125-
t.Log("Creating CronTab in kcp…")
126-
crontab := yamlToUnstructured(t, `
127-
apiVersion: kcp.example.com/v1
128-
kind: CronTab
129-
metadata:
130-
namespace: default
131-
name: my-crontab
132-
spec:
133-
cronSpec: '* * *'
134-
image: ubuntu:latest
135-
`)
136-
137-
if err := kcpClient.Create(teamCtx, crontab); err != nil {
138-
t.Fatalf("Failed to create CronTab in kcp: %v", err)
139-
}
140+
// setup a test environment in kcp
141+
orgKubconfig := utils.CreateOrganization(t, ctx, testcase.workspace, apiExportName)
140142

141-
// fake operator: create a credential Secret
142-
t.Log("Creating credential Secret in service cluster…")
143-
namespace := &corev1.Namespace{}
144-
namespace.Name = "synced-default"
143+
// start a service cluster
144+
envtestKubeconfig, envtestClient, _ := utils.RunEnvtest(t, []string{
145+
"test/crds/crontab.yaml",
146+
})
145147

146-
if err := envtestClient.Create(ctx, namespace); err != nil {
147-
t.Fatalf("Failed to create namespace in kcp: %v", err)
148-
}
148+
// publish Crontabs and Backups
149+
t.Logf("Publishing CRDs…")
150+
prCrontabs := &syncagentv1alpha1.PublishedResource{
151+
ObjectMeta: metav1.ObjectMeta{
152+
Name: "publish-crontabs",
153+
},
154+
Spec: syncagentv1alpha1.PublishedResourceSpec{
155+
Resource: syncagentv1alpha1.SourceResourceDescriptor{
156+
APIGroup: "example.com",
157+
Version: "v1",
158+
Kind: "CronTab",
159+
},
160+
// These rules make finding the local object easier, but should not be used in production.
161+
Naming: &syncagentv1alpha1.ResourceNaming{
162+
Name: "$remoteName",
163+
Namespace: "synced-$remoteNamespace",
164+
},
165+
Related: []syncagentv1alpha1.RelatedResourceSpec{testcase.relatedConfig},
166+
},
167+
}
149168

150-
credentials := &corev1.Secret{}
151-
credentials.Name = "my-credentials"
152-
credentials.Namespace = namespace.Name
153-
credentials.Labels = map[string]string{
154-
"hello": "world",
155-
}
156-
credentials.Data = map[string][]byte{
157-
"password": []byte("hunter2"),
158-
}
169+
if err := envtestClient.Create(ctx, prCrontabs); err != nil {
170+
t.Fatalf("Failed to create PublishedResource: %v", err)
171+
}
159172

160-
if err := envtestClient.Create(ctx, credentials); err != nil {
161-
t.Fatalf("Failed to create Secret in service cluster: %v", err)
162-
}
173+
// start the agent in the background to update the APIExport with the CronTabs API
174+
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName)
163175

164-
// wait for the agent to sync the object down into the service cluster and
165-
// the Secret back up to kcp
166-
t.Logf("Wait for CronTab/Secret to be synced…")
167-
copy := &unstructured.Unstructured{}
168-
copy.SetAPIVersion("example.com/v1")
169-
copy.SetKind("CronTab")
170-
171-
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
172-
copyKey := types.NamespacedName{Namespace: "synced-default", Name: "my-crontab"}
173-
return envtestClient.Get(ctx, copyKey, copy) == nil, nil
174-
})
175-
if err != nil {
176-
t.Fatalf("Failed to wait for CronTab to be synced down: %v", err)
177-
}
176+
// wait until the API is available
177+
teamCtx := kontext.WithCluster(ctx, logicalcluster.Name(fmt.Sprintf("root:%s:team-1", testcase.workspace)))
178+
kcpClient := utils.GetKcpAdminClusterClient(t)
179+
utils.WaitForBoundAPI(t, teamCtx, kcpClient, schema.GroupVersionResource{
180+
Group: apiExportName,
181+
Version: "v1",
182+
Resource: "crontabs",
183+
})
178184

179-
copySecret := &corev1.Secret{}
185+
// create a Crontab object in a team workspace
186+
t.Log("Creating CronTab in kcp…")
180187

181-
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
182-
copyKey := types.NamespacedName{Namespace: "default", Name: "my-credentials"}
183-
return kcpClient.Get(teamCtx, copyKey, copySecret) == nil, nil
184-
})
185-
if err != nil {
186-
t.Fatalf("Failed to wait for Secret to be synced up: %v", err)
187-
}
188+
crontab := utils.ToUnstructured(t, &testcase.mainResource)
189+
crontab.SetAPIVersion("kcp.example.com/v1")
190+
crontab.SetKind("CronTab")
188191

189-
// ensure the secret in kcp does not have any sync-related metadata
190-
maps.DeleteFunc(copySecret.Labels, func(k, v string) bool {
191-
return strings.HasPrefix(k, "claimed.internal.apis.kcp.io/")
192-
})
192+
if err := kcpClient.Create(teamCtx, crontab); err != nil {
193+
t.Fatalf("Failed to create CronTab in kcp: %v", err)
194+
}
193195

194-
if changes := diff.ObjectDiff(credentials.Labels, copySecret.Labels); changes != "" {
195-
t.Errorf("Secret in kcp has unexpected labels:\n%s", changes)
196-
}
196+
// fake operator: create a credential Secret
197+
t.Logf("Creating credential Secret on the %s side…", testcase.relatedConfig.Origin)
198+
199+
originClient := envtestClient
200+
originContext := ctx
201+
destClient := kcpClient
202+
destContext := teamCtx
197203

198-
delete(copySecret.Annotations, "kcp.io/cluster")
199-
if len(copySecret.Annotations) == 0 {
200-
copySecret.Annotations = nil
204+
if testcase.relatedConfig.Origin == "kcp" {
205+
originClient, destClient = destClient, originClient
206+
originContext, destContext = destContext, originContext
207+
}
208+
209+
ensureNamespace(t, originContext, originClient, testcase.sourceRelatedObject.Namespace)
210+
211+
if err := originClient.Create(originContext, &testcase.sourceRelatedObject); err != nil {
212+
t.Fatalf("Failed to create Secret: %v", err)
213+
}
214+
215+
// wait for the agent to do its magic
216+
t.Log("Wait for Secret to be synced…")
217+
copySecret := &corev1.Secret{}
218+
err := wait.PollUntilContextTimeout(destContext, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
219+
copyKey := ctrlruntimeclient.ObjectKeyFromObject(&testcase.expectedSyncedRelatedObject)
220+
return destClient.Get(ctx, copyKey, copySecret) == nil, nil
221+
})
222+
if err != nil {
223+
t.Fatalf("Failed to wait for Secret to be synced: %v", err)
224+
}
225+
226+
// ensure the secret in kcp does not have any sync-related metadata
227+
maps.DeleteFunc(copySecret.Labels, func(k, v string) bool {
228+
return strings.HasPrefix(k, "claimed.internal.apis.kcp.io/")
229+
})
230+
231+
delete(copySecret.Annotations, "kcp.io/cluster")
232+
if len(copySecret.Annotations) == 0 {
233+
copySecret.Annotations = nil
234+
}
235+
236+
orig := testcase.expectedSyncedRelatedObject
237+
copySecret.CreationTimestamp = orig.CreationTimestamp
238+
copySecret.Generation = orig.Generation
239+
copySecret.ResourceVersion = orig.ResourceVersion
240+
copySecret.ManagedFields = orig.ManagedFields
241+
copySecret.UID = orig.UID
242+
243+
if changes := diff.ObjectDiff(orig, copySecret); changes != "" {
244+
t.Errorf("Synced secret does not match expected Secret:\n%s", changes)
245+
}
246+
})
201247
}
248+
}
249+
250+
func ensureNamespace(t *testing.T, ctx context.Context, client ctrlruntimeclient.Client, name string) {
251+
namespace := &corev1.Namespace{}
252+
namespace.Name = name
202253

203-
if changes := diff.ObjectDiff(credentials.Annotations, copySecret.Annotations); changes != "" {
204-
t.Errorf("Secret in kcp has unexpected annotations:\n%s", changes)
254+
if err := client.Create(ctx, namespace); err != nil {
255+
if !apierrors.IsAlreadyExists(err) {
256+
t.Fatalf("Failed to create namespace %s in kcp: %v", name, err)
257+
}
205258
}
206259
}

0 commit comments

Comments
 (0)