Skip to content

Commit c53499a

Browse files
authored
Merge pull request #42 from kcp-dev/add-tests-related-resources
🐛 Fix labels on related resources, fix syncing related resources, add first e2e test
2 parents 187fa93 + 4eff88f commit c53499a

File tree

4 files changed

+215
-5
lines changed

4 files changed

+215
-5
lines changed

internal/controller/syncmanager/lifecycle/cluster.go

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
kcpdevcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
3131

32+
corev1 "k8s.io/api/core/v1"
3233
"k8s.io/apimachinery/pkg/api/meta"
3334
"k8s.io/apimachinery/pkg/runtime"
3435
"k8s.io/client-go/rest"
@@ -135,6 +136,10 @@ func NewCluster(address string, baseRestConfig *rest.Config) (*Cluster, error) {
135136

136137
scheme := runtime.NewScheme()
137138

139+
if err := corev1.AddToScheme(scheme); err != nil {
140+
return nil, fmt.Errorf("failed to register scheme %s: %w", corev1.SchemeGroupVersion, err)
141+
}
142+
138143
if err := kcpdevcorev1alpha1.AddToScheme(scheme); err != nil {
139144
return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevcorev1alpha1.SchemeGroupVersion, err)
140145
}

internal/sync/metadata.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,16 @@ var unsyncableLabels = sets.New(
9494

9595
// filterUnsyncableLabels removes all unwanted remote labels and returns a new label set.
9696
func filterUnsyncableLabels(original labels.Set) labels.Set {
97-
return filterLabels(original, unsyncableLabels)
97+
filtered := filterLabels(original, unsyncableLabels)
98+
99+
out := labels.Set{}
100+
for k, v := range filtered {
101+
if !strings.HasPrefix(k, "claimed.internal.apis.kcp.io/") {
102+
out[k] = v
103+
}
104+
}
105+
106+
return out
98107
}
99108

100109
// unsyncableAnnotations are annotations we never want to copy from the remote to local objects.

internal/sync/object_syncer.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,13 @@ func (s *objectSyncer) ensureDestinationObject(log *zap.SugaredLogger, source, d
302302

303303
// remember the connection between the source and destination object
304304
sourceObjKey := newObjectKey(source.object, source.clusterName, source.workspacePath)
305-
ensureLabels(destObj, sourceObjKey.Labels())
306-
ensureAnnotations(destObj, sourceObjKey.Annotations())
305+
if s.metadataOnDestination {
306+
ensureLabels(destObj, sourceObjKey.Labels())
307+
ensureAnnotations(destObj, sourceObjKey.Annotations())
307308

308-
// remember what agent synced this object
309-
s.labelWithAgent(destObj)
309+
// remember what agent synced this object
310+
s.labelWithAgent(destObj)
311+
}
310312

311313
// finally, we can create the destination object
312314
objectLog := log.With("dest-object", newObjectKey(destObj, dest.clusterName, logicalcluster.None))

test/e2e/sync/related_test.go

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//go:build e2e
2+
3+
/*
4+
Copyright 2025 The KCP Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package sync
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"maps"
25+
"strings"
26+
"testing"
27+
"time"
28+
29+
"github.com/go-logr/logr"
30+
"github.com/kcp-dev/logicalcluster/v3"
31+
32+
"github.com/kcp-dev/api-syncagent/internal/test/diff"
33+
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
34+
"github.com/kcp-dev/api-syncagent/test/utils"
35+
36+
corev1 "k8s.io/api/core/v1"
37+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
39+
"k8s.io/apimachinery/pkg/runtime/schema"
40+
"k8s.io/apimachinery/pkg/types"
41+
"k8s.io/apimachinery/pkg/util/wait"
42+
ctrlruntime "sigs.k8s.io/controller-runtime"
43+
"sigs.k8s.io/controller-runtime/pkg/kontext"
44+
)
45+
46+
func TestSyncSecretBackToKcp(t *testing.T) {
47+
const (
48+
apiExportName = "kcp.example.com"
49+
orgWorkspace = "sync-related-secret-to-kcp"
50+
)
51+
52+
ctx := context.Background()
53+
ctrlruntime.SetLogger(logr.Discard())
54+
55+
// setup a test environment in kcp
56+
orgKubconfig := utils.CreateOrganization(t, ctx, orgWorkspace, apiExportName)
57+
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",
79+
},
80+
Related: []syncagentv1alpha1.RelatedResourceSpec{{
81+
Identifier: "credentials",
82+
Origin: "service",
83+
Kind: "Secret",
84+
Reference: syncagentv1alpha1.RelatedResourceReference{
85+
Name: syncagentv1alpha1.ResourceLocator{
86+
Path: "metadata.name", // irrelevant
87+
Regex: &syncagentv1alpha1.RegexResourceLocator{
88+
Replacement: "my-credentials",
89+
},
90+
},
91+
},
92+
}},
93+
},
94+
}
95+
96+
if err := envtestClient.Create(ctx, prCrontabs); err != nil {
97+
t.Fatalf("Failed to create PublishedResource: %v", err)
98+
}
99+
100+
// start the agent in the background to update the APIExport with the CronTabs API
101+
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName)
102+
103+
// wait until the API is available
104+
teamCtx := kontext.WithCluster(ctx, logicalcluster.Name(fmt.Sprintf("root:%s:team-1", orgWorkspace)))
105+
kcpClient := utils.GetKcpAdminClusterClient(t)
106+
utils.WaitForBoundAPI(t, teamCtx, kcpClient, schema.GroupVersionResource{
107+
Group: apiExportName,
108+
Version: "v1",
109+
Resource: "crontabs",
110+
})
111+
112+
// create a Crontab object in a team workspace
113+
t.Log("Creating CronTab in kcp…")
114+
crontab := yamlToUnstructured(t, `
115+
apiVersion: kcp.example.com/v1
116+
kind: CronTab
117+
metadata:
118+
namespace: default
119+
name: my-crontab
120+
spec:
121+
cronSpec: '* * *'
122+
image: ubuntu:latest
123+
`)
124+
125+
if err := kcpClient.Create(teamCtx, crontab); err != nil {
126+
t.Fatalf("Failed to create CronTab in kcp: %v", err)
127+
}
128+
129+
// fake operator: create a credential Secret
130+
t.Log("Creating credential Secret in service cluster…")
131+
namespace := &corev1.Namespace{}
132+
namespace.Name = "synced-default"
133+
134+
if err := envtestClient.Create(ctx, namespace); err != nil {
135+
t.Fatalf("Failed to create namespace in kcp: %v", err)
136+
}
137+
138+
credentials := &corev1.Secret{}
139+
credentials.Name = "my-credentials"
140+
credentials.Namespace = namespace.Name
141+
credentials.Labels = map[string]string{
142+
"hello": "world",
143+
}
144+
credentials.Data = map[string][]byte{
145+
"password": []byte("hunter2"),
146+
}
147+
148+
if err := envtestClient.Create(ctx, credentials); err != nil {
149+
t.Fatalf("Failed to create Secret in service cluster: %v", err)
150+
}
151+
152+
// wait for the agent to sync the object down into the service cluster and
153+
// the Secret back up to kcp
154+
t.Logf("Wait for CronTab/Secret to be synced…")
155+
copy := &unstructured.Unstructured{}
156+
copy.SetAPIVersion("example.com/v1")
157+
copy.SetKind("CronTab")
158+
159+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
160+
copyKey := types.NamespacedName{Namespace: "synced-default", Name: "my-crontab"}
161+
return envtestClient.Get(ctx, copyKey, copy) == nil, nil
162+
})
163+
if err != nil {
164+
t.Fatalf("Failed to wait for CronTab to be synced down: %v", err)
165+
}
166+
167+
copySecret := &corev1.Secret{}
168+
169+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
170+
copyKey := types.NamespacedName{Namespace: "default", Name: "my-credentials"}
171+
return kcpClient.Get(teamCtx, copyKey, copySecret) == nil, nil
172+
})
173+
if err != nil {
174+
t.Fatalf("Failed to wait for Secret to be synced up: %v", err)
175+
}
176+
177+
// ensure the secret in kcp does not have any sync-related metadata
178+
maps.DeleteFunc(copySecret.Labels, func(k, v string) bool {
179+
return strings.HasPrefix(k, "claimed.internal.apis.kcp.io/")
180+
})
181+
182+
if changes := diff.ObjectDiff(credentials.Labels, copySecret.Labels); changes != "" {
183+
t.Errorf("Secret in kcp has unexpected labels:\n%s", changes)
184+
}
185+
186+
delete(copySecret.Annotations, "kcp.io/cluster")
187+
if len(copySecret.Annotations) == 0 {
188+
copySecret.Annotations = nil
189+
}
190+
191+
if changes := diff.ObjectDiff(credentials.Annotations, copySecret.Annotations); changes != "" {
192+
t.Errorf("Secret in kcp has unexpected annotations:\n%s", changes)
193+
}
194+
}

0 commit comments

Comments
 (0)