Skip to content

Commit 7247040

Browse files
committed
WIP
On-behalf-of: @SAP christoph.mewes@sap.com
1 parent fac12df commit 7247040

File tree

17 files changed

+467
-109
lines changed

17 files changed

+467
-109
lines changed

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,11 @@ spec:
373373
provided by an APIExport and not Kube-native.
374374
type: string
375375
kind:
376-
description: Kind is the object kind of the related resource (for example "Secret").
376+
description: |-
377+
Kind is the object kind of the related resource (for example "Secret").
378+
379+
Deprecated: Use "Resource" instead. This field is limited to "ConfigMap" and "Secret" and will
380+
be removed in the future. Kind and Resource cannot be specified at the same time.
377381
type: string
378382
mutation:
379383
description: |-
@@ -710,21 +714,23 @@ spec:
710714
group:
711715
description: The API group, for example "myservice.example.com". Leave empty to not modify the API group.
712716
type: string
713-
kind:
714-
description: The resource Kind, for example "Database". Leave empty to not modify the kind.
717+
resource:
718+
description: The resource name, for example "databases". Leave empty to not modify the resource.
715719
type: string
716720
version:
717721
description: The API version, for example "v1beta1". Leave empty to not modify the version.
718722
type: string
719723
type: object
724+
resource:
725+
description: Resource is the name of the related resource (for example "secrets").
726+
type: string
720727
version:
721728
description: |-
722729
Version is the API version of the related resource. This can be left blank to automatically
723730
use the preferred version.
724731
type: string
725732
required:
726733
- identifier
727-
- kind
728734
- object
729735
- origin
730736
type: object

hack/update-codegen-sdk.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ $GOBIN/controller-gen \
3939
"object:headerFile=$BOILERPLATE_HEADER" \
4040
paths=./internal/sync/apis/...
4141

42+
$GOBIN/controller-gen \
43+
"object:headerFile=$BOILERPLATE_HEADER" \
44+
paths=./test/crds/...
45+
4246
cd sdk
4347
rm -rf -- applyconfiguration clientset informers listers
4448

internal/controller/apiexport/controller.go

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ import (
2828
predicateutil "github.com/kcp-dev/api-syncagent/internal/controllerutil/predicate"
2929
"github.com/kcp-dev/api-syncagent/internal/projection"
3030
"github.com/kcp-dev/api-syncagent/internal/resources/reconciling"
31+
"github.com/kcp-dev/api-syncagent/internal/validation"
3132
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3233

3334
kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
3435

3536
"k8s.io/apimachinery/pkg/labels"
3637
"k8s.io/apimachinery/pkg/runtime/schema"
38+
"k8s.io/apimachinery/pkg/types"
3739
"k8s.io/apimachinery/pkg/util/sets"
3840
"k8s.io/client-go/tools/record"
3941
"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -110,10 +112,16 @@ func Add(
110112

111113
func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
112114
r.log.Debug("Processing")
113-
return reconcile.Result{}, r.reconcile(ctx)
115+
116+
apiExport := &kcpdevv1alpha1.APIExport{}
117+
if err := r.kcpClient.Get(ctx, types.NamespacedName{Name: r.apiExportName}, apiExport); err != nil {
118+
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
119+
}
120+
121+
return reconcile.Result{}, r.reconcile(ctx, apiExport)
114122
}
115123

116-
func (r *Reconciler) reconcile(ctx context.Context) error {
124+
func (r *Reconciler) reconcile(ctx context.Context, apiExport *kcpdevv1alpha1.APIExport) error {
117125
// find all PublishedResources
118126
pubResources := &syncagentv1alpha1.PublishedResourceList{}
119127
if err := r.localClient.List(ctx, pubResources, &ctrlruntimeclient.ListOptions{
@@ -122,22 +130,45 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
122130
return fmt.Errorf("failed to list PublishedResources: %w", err)
123131
}
124132

125-
// filter out those PRs that have not yet been processed into an ARS
133+
// filter out those PRs that have not yet been processed into an ARS or that are invalid
126134
filteredPubResources := slices.DeleteFunc(pubResources.Items, func(pr syncagentv1alpha1.PublishedResource) bool {
135+
// TODO: Turn this into a webhook or CEL expressions.
136+
if err := validation.ValidatePublishedResource(&pr); err != nil {
137+
r.log.With("pr", pr.Name, "error", err).Warn("Ignoring invalid PublishedResource.")
138+
return true
139+
}
140+
127141
return pr.Status.ResourceSchemaName == ""
128142
})
129143

130144
// for each PR, we note down the created ARS and also the GVKs of related resources
131-
arsList := sets.New[string]()
132-
claimedResources := sets.New[permissionClaim]()
145+
newARSList := sets.New[string]()
146+
for _, pubResource := range filteredPubResources {
147+
newARSList.Insert(pubResource.Status.ResourceSchemaName)
148+
}
133149

134-
// PublishedResources use kinds, but the PermissionClaims use resource names (plural),
135-
// so we must translate accordingly
136-
mapper := r.kcpClient.RESTMapper()
150+
// To determine if the GVR of a related resource needs to be listed as a permission claim,
151+
// we first need to figure out all the GVRs our APIExport contains. This is not just the
152+
// list of ARS we just built, but also potentially other ARS's that exist in the APIExport
153+
// and that we would not touch.
154+
allARSList := mergeResourceSchemas(apiExport.Spec.LatestResourceSchemas, newARSList)
155+
156+
// turn the flat list of schema names ("version.resource.group") into a lookup table consisting
157+
// of group/resource only
158+
ourOwnResources := sets.New[schema.GroupResource]()
159+
for _, schemaName := range allARSList {
160+
gvr, err := parseSchemaName(schemaName)
161+
if err != nil {
162+
return fmt.Errorf("failed to assemble own resources: %w", err)
163+
}
137164

138-
for _, pubResource := range filteredPubResources {
139-
arsList.Insert(pubResource.Status.ResourceSchemaName)
165+
ourOwnResources.Insert(gvr.GroupResource())
166+
}
167+
168+
// Now we can finally assemble the list of required permission claims.
169+
claimedResources := sets.New[permissionClaim]()
140170

171+
for _, pubResource := range filteredPubResources {
141172
// to evaluate the namespace filter, the agent needs to fetch the namespace
142173
if filter := pubResource.Spec.Filter; filter != nil && filter.Namespace != nil {
143174
claimedResources.Insert(permissionClaim{
@@ -153,40 +184,27 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
153184
})
154185
}
155186

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.
187+
// Add a claim for every foreign (!) related resource, but make sure to
188+
// project the GVR of related resources to their kcp-side equivalent first
189+
// if they originate on the service cluster.
159190
for _, rr := range pubResource.Spec.Related {
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...)
171-
if err != nil {
172-
return fmt.Errorf("unknown related resource kind %q: %w", rr.Kind, err)
173-
}
191+
kcpGVR := projection.RelatedResourceKcpGVR(&rr)
174192

193+
// We always want to see namespaces, for simplicity. Technically if we knew the scope
194+
// of every related resource, we could determine if a namespace claim is truly necessary.
175195
claimedResources.Insert(permissionClaim{
176-
Group: kcpGVK.Group,
177-
Resource: mapping.Resource.Resource,
178-
IdentityHash: rr.IdentityHash,
196+
Group: "",
197+
Resource: "namespaces",
179198
})
180-
}
181-
}
182199

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.
185-
if claimedResources.Len() > 0 {
186-
claimedResources.Insert(permissionClaim{
187-
Group: "",
188-
Resource: "namespaces",
189-
})
200+
if !ourOwnResources.Has(kcpGVR.GroupResource()) {
201+
claimedResources.Insert(permissionClaim{
202+
Group: kcpGVR.Group,
203+
Resource: kcpGVR.Resource,
204+
IdentityHash: rr.IdentityHash,
205+
})
206+
}
207+
}
190208
}
191209

192210
// We always want to create events.
@@ -195,14 +213,9 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
195213
Resource: "events",
196214
})
197215

198-
if arsList.Len() == 0 {
199-
r.log.Debug("No ready PublishedResources available.")
200-
return nil
201-
}
202-
203216
// reconcile an APIExport in kcp
204217
factories := []reconciling.NamedAPIExportReconcilerFactory{
205-
r.createAPIExportReconciler(arsList, claimedResources, r.agentName, r.apiExportName, r.recorder),
218+
r.createAPIExportReconciler(newARSList, claimedResources, r.agentName, r.apiExportName, r.recorder),
206219
}
207220

208221
if err := reconciling.ReconcileAPIExports(ctx, factories, "", r.kcpClient); err != nil {

internal/controller/apiexport/reconciler.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
corev1 "k8s.io/api/core/v1"
3030
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/runtime/schema"
3132
"k8s.io/apimachinery/pkg/util/sets"
3233
"k8s.io/client-go/tools/record"
3334
)
@@ -179,8 +180,23 @@ func createSchemaEvents(obj runtime.Object, oldSchemas, newSchemas []string, rec
179180
}
180181

181182
func parseResourceGroup(schema string) string {
183+
gvr, _ := parseSchemaName(schema)
184+
return gvr.GroupResource().String()
185+
}
186+
187+
// parseSchemaName parses an APIResourceSchema name and returns it in form of
188+
// a GVR. Note: the version in the result will not be a Kubernetes version, but
189+
// the version of the ARS!
190+
func parseSchemaName(name string) (schema.GroupVersionResource, error) {
182191
// <version>.<resource>.<group>
183-
parts := strings.SplitN(schema, ".", 2)
192+
parts := strings.SplitN(name, ".", 3)
193+
if len(parts) != 3 {
194+
return schema.GroupVersionResource{}, fmt.Errorf("invalid schema name %q, must consist of version.resource.group.", name)
195+
}
184196

185-
return parts[1]
197+
return schema.GroupVersionResource{
198+
Group: parts[2],
199+
Version: parts[0],
200+
Resource: parts[1],
201+
}, nil
186202
}

internal/controller/syncmanager/controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/kcp-dev/api-syncagent/internal/controllerutil"
2929
"github.com/kcp-dev/api-syncagent/internal/controllerutil/predicate"
3030
"github.com/kcp-dev/api-syncagent/internal/discovery"
31+
"github.com/kcp-dev/api-syncagent/internal/validation"
3132
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3233

3334
kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
@@ -415,6 +416,12 @@ func isSyncEnabled(pr *syncagentv1alpha1.PublishedResource) bool {
415416
func (r *Reconciler) ensureSyncControllers(ctx context.Context, log *zap.SugaredLogger, publishedResources []syncagentv1alpha1.PublishedResource) error {
416417
requiredWorkers := sets.New[string]()
417418
for _, pr := range publishedResources {
419+
// TODO: Turn this into a webhook or CEL expressions.
420+
if err := validation.ValidatePublishedResource(&pr); err != nil {
421+
r.log.With("pr", pr.Name, "error", err).Warn("Ignoring invalid PublishedResource.")
422+
continue
423+
}
424+
418425
if isSyncEnabled(&pr) {
419426
requiredWorkers.Insert(getPublishedResourceKey(&pr))
420427
}

internal/projection/projection.go

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -113,57 +113,64 @@ 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,
116+
func RelatedResourceGVR(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionResource {
117+
resultGVR := schema.GroupVersionResource{
118+
Group: rr.Group,
119+
Version: rr.Version,
120+
Resource: rr.Resource,
121+
}
122+
123+
// handle legacy kinds
124+
//nolint:staticcheck
125+
switch rr.Kind {
126+
case "ConfigMap":
127+
resultGVR.Resource = "configmaps"
128+
case "Secret":
129+
resultGVR.Resource = "secrets"
123130
}
124131

132+
return resultGVR
133+
}
134+
135+
// RelatedResourceProjectedGVR returns the effective GVR on the destination side,
136+
// after the projection rules for the related resource have been applied.
137+
func RelatedResourceProjectedGVR(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionResource {
138+
resultGVR := RelatedResourceGVR(rr)
139+
125140
projection := rr.Projection
126141
if projection == nil {
127-
return resultGVK
142+
return resultGVR
128143
}
129144

130145
if projection.Group != "" {
131-
resultGVK.Group = projection.Group
146+
resultGVR.Group = projection.Group
132147
}
133148

134149
if projection.Version != "" {
135-
resultGVK.Version = projection.Version
150+
resultGVR.Version = projection.Version
136151
}
137152

138-
if projection.Kind != "" {
139-
resultGVK.Kind = projection.Kind
153+
if projection.Resource != "" {
154+
resultGVR.Resource = projection.Resource
140155
}
141156

142-
return resultGVK
157+
return resultGVR
143158
}
144159

145-
func RelatedResourceKcpGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionKind {
160+
func RelatedResourceKcpGVR(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionResource {
146161
if rr.Origin == syncagentv1alpha1.RelatedResourceOriginKcp {
147-
return schema.GroupVersionKind{
148-
Group: rr.Group,
149-
Version: rr.Version,
150-
Kind: rr.Kind,
151-
}
162+
return RelatedResourceGVR(rr)
152163
}
153164

154-
return RelatedResourceProjectedGVK(rr)
165+
return RelatedResourceProjectedGVR(rr)
155166
}
156167

157-
func RelatedResourceServiceGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionKind {
168+
func RelatedResourceServiceGVK(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionResource {
158169
if rr.Origin == syncagentv1alpha1.RelatedResourceOriginService {
159-
return schema.GroupVersionKind{
160-
Group: rr.Group,
161-
Version: rr.Version,
162-
Kind: rr.Kind,
163-
}
170+
return RelatedResourceGVR(rr)
164171
}
165172

166-
return RelatedResourceProjectedGVK(rr)
173+
return RelatedResourceProjectedGVR(rr)
167174
}
168175

169176
func stripUnwantedVersions(crd *apiextensionsv1.CustomResourceDefinition, pubRes *syncagentv1alpha1.PublishedResource) (*apiextensionsv1.CustomResourceDefinition, error) {

0 commit comments

Comments
 (0)