Skip to content

Commit aad3540

Browse files
committed
WIP first e2e test for projection
On-behalf-of: @SAP christoph.mewes@sap.com
1 parent 7ae99db commit aad3540

File tree

13 files changed

+985
-66
lines changed

13 files changed

+985
-66
lines changed

deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,10 @@ spec:
351351
related:
352352
items:
353353
properties:
354-
apiVersion:
355-
description: APIVersion is the API group and version of the related resource.
354+
group:
355+
description: |-
356+
Group is the API group of the related resource. This should be left blank for resources
357+
in the core API group.
356358
type: string
357359
identifier:
358360
description: |-
@@ -367,7 +369,7 @@ spec:
367369
provided by an APIExport and not Kube-native.
368370
type: string
369371
kind:
370-
description: ConfigMap or Secret
372+
description: Kind is the object kind of the related resource (for example "Secret").
371373
type: string
372374
mutation:
373375
description: |-
@@ -694,8 +696,29 @@ spec:
694696
- service
695697
- kcp
696698
type: string
699+
projection:
700+
description: |-
701+
Projection is used to change the GVK of a related resource on the opposite side of
702+
its origin.
703+
All fields in the projection are optional. If a field is set, it will overwrite
704+
that field in the GVK.
705+
properties:
706+
group:
707+
description: The API group, for example "myservice.example.com". Leave empty to not modify the API group.
708+
type: string
709+
kind:
710+
description: The resource Kind, for example "Database". Leave empty to not modify the kind.
711+
type: string
712+
version:
713+
description: The API version, for example "v1beta1". Leave empty to not modify the version.
714+
type: string
715+
type: object
716+
version:
717+
description: |-
718+
Version is the API version of the related resource. This can be left blank to automatically
719+
use the preferred version.
720+
type: string
697721
required:
698-
- apiVersion
699722
- identifier
700723
- kind
701724
- object

internal/controller/apiexport/controller.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/kcp-dev/api-syncagent/internal/controllerutil"
2828
predicateutil "github.com/kcp-dev/api-syncagent/internal/controllerutil/predicate"
29+
"github.com/kcp-dev/api-syncagent/internal/projection"
2930
"github.com/kcp-dev/api-syncagent/internal/resources/reconciling"
3031
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3132

@@ -128,7 +129,7 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
128129

129130
// for each PR, we note down the created ARS and also the GVKs of related resources
130131
arsList := sets.New[string]()
131-
claimedResources := sets.New[kcpdevv1alpha1.GroupResource]()
132+
claimedResources := sets.New[permissionClaim]()
132133

133134
// PublishedResources use kinds, but the PermissionClaims use resource names (plural),
134135
// so we must translate accordingly
@@ -139,45 +140,57 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
139140

140141
// to evaluate the namespace filter, the agent needs to fetch the namespace
141142
if filter := pubResource.Spec.Filter; filter != nil && filter.Namespace != nil {
142-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
143+
claimedResources.Insert(permissionClaim{
143144
Group: "",
144145
Resource: "namespaces",
145146
})
146147
}
147148

148149
if pubResource.Spec.EnableWorkspacePaths {
149-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
150+
claimedResources.Insert(permissionClaim{
150151
Group: "core.kcp.io",
151152
Resource: "logicalclusters",
152153
})
153154
}
154155

156+
// Add a claim for every related resource, but make sure to project the GVK
157+
// of related resources to their kcp-side equivalent first if they originate
158+
// on the service cluster.
155159
for _, rr := range pubResource.Spec.Related {
156-
resource, err := mapper.ResourceFor(schema.GroupVersionResource{
157-
Resource: rr.Kind,
158-
})
160+
kcpGVK := projection.RelatedResourceKcpGVK(&rr)
161+
162+
versions := []string{}
163+
if kcpGVK.Version != "" {
164+
versions = append(versions, kcpGVK.Version)
165+
}
166+
167+
mapping, err := mapper.RESTMapping(schema.GroupKind{
168+
Group: kcpGVK.Group,
169+
Kind: kcpGVK.Kind,
170+
}, versions...)
159171
if err != nil {
160172
return fmt.Errorf("unknown related resource kind %q: %w", rr.Kind, err)
161173
}
162174

163-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
164-
Group: "",
165-
Resource: resource.Resource,
175+
claimedResources.Insert(permissionClaim{
176+
Group: kcpGVK.Group,
177+
Resource: mapping.Resource.Resource,
178+
IdentityHash: rr.IdentityHash,
166179
})
167180
}
168181
}
169182

170-
// Related resources (Secrets, ConfigMaps) are namespaced and so the Sync Agent will
171-
// always need to be able to see and manage namespaces.
183+
// We always want to see namespaces, for simplicity. Technically if we knew the scope
184+
// of every related resource, we could determine if a namespace claim is truly necessary.
172185
if claimedResources.Len() > 0 {
173-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
186+
claimedResources.Insert(permissionClaim{
174187
Group: "",
175188
Resource: "namespaces",
176189
})
177190
}
178191

179192
// We always want to create events.
180-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
193+
claimedResources.Insert(permissionClaim{
181194
Group: "",
182195
Resource: "events",
183196
})

internal/controller/apiexport/reconciler.go

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,29 @@ import (
3232
"k8s.io/client-go/tools/record"
3333
)
3434

35+
// permissionClaim is the same as kcp's PermissionClaim, just trimmed down to
36+
// kind, group and identity and more importantly, without all the other fields
37+
// that makes this struct not comparable (i.e. not suitable for a Set).
38+
type permissionClaim struct {
39+
Group string
40+
Resource string
41+
IdentityHash string
42+
}
43+
44+
func (c permissionClaim) String() string {
45+
if c.Group == "" {
46+
return c.Resource
47+
}
48+
49+
return fmt.Sprintf("%s/%s", c.Group, c.Resource)
50+
}
51+
3552
// createAPIExportReconciler creates the reconciler for the APIExport.
3653
// WARNING: The APIExport in this is NOT created by the Sync Agent, it's created
3754
// by a controller in kcp. Make sure you don't create a reconciling conflict!
3855
func (r *Reconciler) createAPIExportReconciler(
3956
availableResourceSchemas sets.Set[string],
40-
claimedResourceKinds sets.Set[kcpdevv1alpha1.GroupResource],
57+
claimedResourceKinds sets.Set[permissionClaim],
4158
agentName string,
4259
apiExportName string,
4360
recorder record.EventRecorder,
@@ -60,17 +77,21 @@ func (r *Reconciler) createAPIExportReconciler(
6077
// only ensure the ones originating from the published resources;
6178
// step 1 is to collect all existing claims with the same properties
6279
// as ours.
63-
existingClaims := sets.New[kcpdevv1alpha1.GroupResource]()
80+
existingClaims := sets.New[permissionClaim]()
6481
for _, claim := range existing.Spec.PermissionClaims {
6582
if claim.All && len(claim.ResourceSelector) == 0 {
66-
existingClaims.Insert(claim.GroupResource)
83+
existingClaims.Insert(permissionClaim{
84+
Group: claim.Group,
85+
Resource: claim.Resource,
86+
IdentityHash: claim.IdentityHash,
87+
})
6788
}
6889
}
6990

7091
missingClaims := claimedResourceKinds.Difference(existingClaims)
7192

7293
claimsToAdd := missingClaims.UnsortedList()
73-
slices.SortStableFunc(claimsToAdd, func(a, b kcpdevv1alpha1.GroupResource) int {
94+
slices.SortStableFunc(claimsToAdd, func(a, b permissionClaim) int {
7495
if a.Group != b.Group {
7596
return strings.Compare(a.Group, b.Group)
7697
}
@@ -81,15 +102,19 @@ func (r *Reconciler) createAPIExportReconciler(
81102
// add our missing claims
82103
for _, claimed := range claimsToAdd {
83104
existing.Spec.PermissionClaims = append(existing.Spec.PermissionClaims, kcpdevv1alpha1.PermissionClaim{
84-
GroupResource: claimed,
85-
All: true,
105+
GroupResource: kcpdevv1alpha1.GroupResource{
106+
Group: claimed.Group,
107+
Resource: claimed.Resource,
108+
},
109+
All: true,
110+
IdentityHash: claimed.IdentityHash,
86111
})
87112
}
88113

89114
if missingClaims.Len() > 0 {
90115
claims := make([]string, 0, len(claimsToAdd))
91116
for _, claimed := range claimsToAdd {
92-
claims = append(claims, groupResourceToString(claimed))
117+
claims = append(claims, claimed.String())
93118
}
94119
recorder.Eventf(existing, corev1.EventTypeNormal, "AddingPermissionClaims", "Added new permission claim(s) for all %s.", strings.Join(claims, ", "))
95120
}
@@ -112,14 +137,6 @@ func (r *Reconciler) createAPIExportReconciler(
112137
}
113138
}
114139

115-
func groupResourceToString(gr kcpdevv1alpha1.GroupResource) string {
116-
if gr.Group == "" {
117-
return gr.Resource
118-
}
119-
120-
return fmt.Sprintf("%s/%s", gr.Group, gr.Resource)
121-
}
122-
123140
func mergeResourceSchemas(existing []string, configured sets.Set[string]) []string {
124141
var result []string
125142

internal/projection/projection.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,59 @@ func ProjectCRD(crd *apiextensionsv1.CustomResourceDefinition, pubRes *syncagent
113113
return result, nil
114114
}
115115

116+
// RelatedResourceProjectedGVK returns the effective GVK on the destination side,
117+
// after the projection rules for the related resource have been applied.
118+
func RelatedResourceProjectedGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionKind {
119+
resultGVK := schema.GroupVersionKind{
120+
Group: rr.Group,
121+
Version: rr.Version,
122+
Kind: rr.Kind,
123+
}
124+
125+
projection := rr.Projection
126+
if projection == nil {
127+
return resultGVK
128+
}
129+
130+
if projection.Group != "" {
131+
resultGVK.Group = projection.Group
132+
}
133+
134+
if projection.Version != "" {
135+
resultGVK.Version = projection.Version
136+
}
137+
138+
if projection.Kind != "" {
139+
resultGVK.Kind = projection.Kind
140+
}
141+
142+
return resultGVK
143+
}
144+
145+
func RelatedResourceKcpGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionKind {
146+
if rr.Origin == syncagentv1alpha1.RelatedResourceOriginKcp {
147+
return schema.GroupVersionKind{
148+
Group: rr.Group,
149+
Version: rr.Version,
150+
Kind: rr.Kind,
151+
}
152+
}
153+
154+
return RelatedResourceProjectedGVK(rr)
155+
}
156+
157+
func RelatedResourceServiceGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionKind {
158+
if rr.Origin == syncagentv1alpha1.RelatedResourceOriginService {
159+
return schema.GroupVersionKind{
160+
Group: rr.Group,
161+
Version: rr.Version,
162+
Kind: rr.Kind,
163+
}
164+
}
165+
166+
return RelatedResourceProjectedGVK(rr)
167+
}
168+
116169
func stripUnwantedVersions(crd *apiextensionsv1.CustomResourceDefinition, pubRes *syncagentv1alpha1.PublishedResource) (*apiextensionsv1.CustomResourceDefinition, error) {
117170
src := pubRes.Spec.Resource
118171

internal/sync/syncer_related.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import (
2828
"github.com/tidwall/gjson"
2929
"go.uber.org/zap"
3030

31+
"github.com/kcp-dev/api-syncagent/internal/projection"
3132
"github.com/kcp-dev/api-syncagent/internal/sync/templating"
3233
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3334

3435
corev1 "k8s.io/api/core/v1"
3536
apierrors "k8s.io/apimachinery/pkg/api/errors"
3637
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3738
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
39+
"k8s.io/apimachinery/pkg/runtime/schema"
3840
"k8s.io/apimachinery/pkg/types"
3941
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
4042
)
@@ -97,11 +99,13 @@ func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.Su
9799
return strings.Compare(aKey, bKey)
98100
})
99101

100-
// Synchronize objects the same way the parent object was synchronized.
102+
// Synchronize related objects the same way the parent object was synchronized.
103+
projectedAPIVersion, projectedKind := projection.RelatedResourceProjectedGVK(&relRes).ToAPIVersionAndKind()
104+
101105
for idx, resolved := range resolvedObjects {
102106
destObject := &unstructured.Unstructured{}
103-
destObject.SetAPIVersion("v1") // we only support ConfigMaps and Secrets, both are in core/v1
104-
destObject.SetKind(relRes.Kind)
107+
destObject.SetAPIVersion(projectedAPIVersion)
108+
destObject.SetKind(projectedKind)
105109

106110
if err = dest.client.Get(ctx, resolved.destination, destObject); err != nil {
107111
destObject = nil
@@ -119,6 +123,8 @@ func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.Su
119123
object: destObject,
120124
}
121125

126+
fmt.Printf("destobjct: %+v\n", destObject)
127+
122128
syncer := objectSyncer{
123129
// Related objects within kcp are not labelled with the agent name because it's unnecessary.
124130
// agentName: "",
@@ -127,13 +133,16 @@ func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.Su
127133
stateStore: stateStore,
128134
// how to create a new destination object
129135
destCreator: func(source *unstructured.Unstructured) (*unstructured.Unstructured, error) {
136+
fmt.Printf("source: %+v\n", *source)
130137
dest := source.DeepCopy()
138+
dest.SetAPIVersion(projectedAPIVersion)
139+
dest.SetKind(projectedKind)
131140
dest.SetName(resolved.destination.Name)
132141
dest.SetNamespace(resolved.destination.Namespace)
133142

134143
return dest, nil
135144
},
136-
// ConfigMaps and Secrets have no subresources
145+
// reloated resources have no subresources
137146
subresources: nil,
138147
// only sync the status back if the object originates in kcp,
139148
// as the service side should never have to rely on new status infos coming
@@ -171,8 +180,8 @@ func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.Su
171180
value, err := json.Marshal(relatedObjectAnnotation{
172181
Namespace: resolved.destination.Namespace,
173182
Name: resolved.destination.Name,
174-
APIVersion: "v1", // we only support ConfigMaps and Secrets
175-
Kind: relRes.Kind,
183+
APIVersion: resolved.original.GetAPIVersion(),
184+
Kind: resolved.original.GetKind(),
176185
})
177186
if err != nil {
178187
return false, fmt.Errorf("failed to encode related object annotation: %w", err)
@@ -355,8 +364,10 @@ func resolveRelatedResourceObjectsInNamespaces(ctx context.Context, relatedOrigi
355364
}
356365

357366
for originName, destName := range nameMap {
367+
apiVersion := schema.GroupVersion{Group: relRes.Group, Version: relRes.Version}.String()
368+
358369
originObj := &unstructured.Unstructured{}
359-
originObj.SetAPIVersion("v1") // we only support ConfigMaps and Secrets, both are in core/v1
370+
originObj.SetAPIVersion(apiVersion)
360371
originObj.SetKind(relRes.Kind)
361372

362373
err = relatedOrigin.client.Get(ctx, types.NamespacedName{Name: originName, Namespace: originNamespace}, originObj)
@@ -407,8 +418,10 @@ func resolveRelatedResourceObjectsInNamespace(ctx context.Context, relatedOrigin
407418
return mapSlices(originNames, destNames), nil
408419

409420
case spec.Selector != nil:
421+
apiVersion := schema.GroupVersion{Group: relRes.Group, Version: relRes.Version}.String()
422+
410423
originObjects := &unstructured.UnstructuredList{}
411-
originObjects.SetAPIVersion("v1") // we only support ConfigMaps and Secrets, both are in core/v1
424+
originObjects.SetAPIVersion(apiVersion)
412425
originObjects.SetKind(relRes.Kind)
413426

414427
labelSelector, err := templateLabelSelector(relatedOrigin, relatedDest, relRes.Origin, &spec.Selector.LabelSelector)

internal/sync/syncer_related_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ func TestResolveRelatedResourceObjects(t *testing.T) {
216216
Identifier: "test",
217217
Origin: syncagentv1alpha1.RelatedResourceOriginService,
218218
Kind: "Secret",
219+
Group: "",
220+
Version: "v1",
219221
Object: testcase.objectSpec,
220222
}
221223

0 commit comments

Comments
 (0)