Skip to content

Commit cb3ed23

Browse files
committed
add apiVersion and identityHash to pubres CRD
On-behalf-of: @SAP christoph.mewes@sap.com
1 parent 6b75d0a commit cb3ed23

29 files changed

+1457
-116
lines changed

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,35 @@ spec:
349349
type: object
350350
type: object
351351
related:
352+
description: |-
353+
Related configures additional resources that semantically belong to the synced
354+
resource, like a Secret containing generated credentials. Related objects are
355+
synced along the main resource.
352356
items:
353357
properties:
358+
group:
359+
description: |-
360+
Group is the API group of the related resource. This should be left blank for resources
361+
in the core API group.
362+
type: string
354363
identifier:
355364
description: |-
356365
Identifier is a unique name for this related resource. The name must be unique within one
357366
PublishedResource and is the key by which consumers (end users) can identify and consume the
358367
related resource. Common names are "connection-details" or "credentials".
359368
The identifier must be an alphanumeric string.
360369
type: string
370+
identityHash:
371+
description: |-
372+
IdentityHash is the identity hash of a kcp APIExport, in case the given Kind is
373+
provided by an APIExport and not Kube-native.
374+
type: string
361375
kind:
362-
description: ConfigMap or 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.
363381
type: string
364382
mutation:
365383
description: |-
@@ -686,9 +704,33 @@ spec:
686704
- service
687705
- kcp
688706
type: string
707+
projection:
708+
description: |-
709+
Projection is used to change the GVK of a related resource on the opposite side of
710+
its origin.
711+
All fields in the projection are optional. If a field is set, it will overwrite
712+
that field in the GVK.
713+
properties:
714+
group:
715+
description: The API group, for example "myservice.example.com". Leave empty to not modify the API group.
716+
type: string
717+
resource:
718+
description: The resource name, for example "databases". Leave empty to not modify the resource.
719+
type: string
720+
version:
721+
description: The API version, for example "v1beta1". Leave empty to not modify the version.
722+
type: string
723+
type: object
724+
resource:
725+
description: Resource is the name of the related resource (for example "secrets").
726+
type: string
727+
version:
728+
description: |-
729+
Version is the API version of the related resource. This can be left blank to automatically
730+
use the preferred version.
731+
type: string
689732
required:
690733
- identifier
691-
- kind
692734
- object
693735
- origin
694736
type: object
@@ -723,6 +765,22 @@ spec:
723765
- apiGroup
724766
- kind
725767
type: object
768+
synchronization:
769+
description: Synchronization allows to configure how the syncagent processes this resource.
770+
properties:
771+
enabled:
772+
description: |-
773+
Enabled can be used to toggle the synchronization as a whole. When set to
774+
false, the syncagent will only copy the CRD and include it in the APIExport,
775+
but not will attempt to synchronize objects of this resource from the kcp
776+
workspaces to the provider.
777+
Synchronization must be disabled for resources that are used as related
778+
resources for other PublishedResources. Otherwise the syncagent would
779+
potentially loop and never finish processing an object.
780+
type: boolean
781+
required:
782+
- enabled
783+
type: object
726784
required:
727785
- resource
728786
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: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ 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"
31+
"github.com/kcp-dev/api-syncagent/internal/validation"
3032
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3133

3234
kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
3335

3436
"k8s.io/apimachinery/pkg/labels"
3537
"k8s.io/apimachinery/pkg/runtime/schema"
38+
"k8s.io/apimachinery/pkg/types"
3639
"k8s.io/apimachinery/pkg/util/sets"
3740
"k8s.io/client-go/tools/record"
3841
"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -109,10 +112,16 @@ func Add(
109112

110113
func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
111114
r.log.Debug("Processing")
112-
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)
113122
}
114123

115-
func (r *Reconciler) reconcile(ctx context.Context) error {
124+
func (r *Reconciler) reconcile(ctx context.Context, apiExport *kcpdevv1alpha1.APIExport) error {
116125
// find all PublishedResources
117126
pubResources := &syncagentv1alpha1.PublishedResourceList{}
118127
if err := r.localClient.List(ctx, pubResources, &ctrlruntimeclient.ListOptions{
@@ -121,75 +130,99 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
121130
return fmt.Errorf("failed to list PublishedResources: %w", err)
122131
}
123132

124-
// filter out those PRs that have not yet been processed into an ARS
133+
// filter out those PRs that are invalid; we keep those that are not yet converted into ARS,
134+
// just to reduce the amount of re-reconciles when the agent processes a number of PRs in a row
135+
// and would constantly update the APIExport; instead we rely on kcp to handle the eventual
136+
// consistency.
137+
// This allows us to already determine all resources we "own", which helps us in
138+
// bilding the proper permission claims if one of the related resources is using a resource
139+
// type managed via PublishedResource. Otherwise the controller might not see the PR for the
140+
// related resource and temporarily incorrectly assume it needs to add a permission claim.
125141
filteredPubResources := slices.DeleteFunc(pubResources.Items, func(pr syncagentv1alpha1.PublishedResource) bool {
126-
return pr.Status.ResourceSchemaName == ""
142+
// TODO: Turn this into a webhook or CEL expressions.
143+
err := validation.ValidatePublishedResource(&pr)
144+
if err != nil {
145+
r.log.With("pr", pr.Name, "error", err).Warn("Ignoring invalid PublishedResource.")
146+
}
147+
148+
return err != nil
127149
})
128150

129151
// for each PR, we note down the created ARS and also the GVKs of related resources
130-
arsList := sets.New[string]()
131-
claimedResources := sets.New[kcpdevv1alpha1.GroupResource]()
152+
newARSList := sets.New[string]()
153+
for _, pubResource := range filteredPubResources {
154+
newARSList.Insert(pubResource.Status.ResourceSchemaName)
155+
}
132156

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

137-
for _, pubResource := range filteredPubResources {
138-
arsList.Insert(pubResource.Status.ResourceSchemaName)
172+
ourOwnResources.Insert(gvr.GroupResource())
173+
}
174+
175+
// Now we can finally assemble the list of required permission claims.
176+
claimedResources := sets.New[permissionClaim]()
139177

178+
for _, pubResource := range filteredPubResources {
140179
// to evaluate the namespace filter, the agent needs to fetch the namespace
141180
if filter := pubResource.Spec.Filter; filter != nil && filter.Namespace != nil {
142-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
181+
claimedResources.Insert(permissionClaim{
143182
Group: "",
144183
Resource: "namespaces",
145184
})
146185
}
147186

148187
if pubResource.Spec.EnableWorkspacePaths {
149-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
188+
claimedResources.Insert(permissionClaim{
150189
Group: "core.kcp.io",
151190
Resource: "logicalclusters",
152191
})
153192
}
154193

194+
// Add a claim for every foreign (!) related resource, but make sure to
195+
// project the GVR of related resources to their kcp-side equivalent first
196+
// if they originate on the service cluster.
155197
for _, rr := range pubResource.Spec.Related {
156-
resource, err := mapper.ResourceFor(schema.GroupVersionResource{
157-
Resource: rr.Kind,
158-
})
159-
if err != nil {
160-
return fmt.Errorf("unknown related resource kind %q: %w", rr.Kind, err)
161-
}
198+
kcpGVR := projection.RelatedResourceKcpGVR(&rr)
162199

163-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
200+
// We always want to see namespaces, for simplicity. Technically if we knew the scope
201+
// of every related resource, we could determine if a namespace claim is truly necessary.
202+
claimedResources.Insert(permissionClaim{
164203
Group: "",
165-
Resource: resource.Resource,
204+
Resource: "namespaces",
166205
})
167-
}
168-
}
169206

170-
// Related resources (Secrets, ConfigMaps) are namespaced and so the Sync Agent will
171-
// always need to be able to see and manage namespaces.
172-
if claimedResources.Len() > 0 {
173-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
174-
Group: "",
175-
Resource: "namespaces",
176-
})
207+
if !ourOwnResources.Has(kcpGVR.GroupResource()) {
208+
claimedResources.Insert(permissionClaim{
209+
Group: kcpGVR.Group,
210+
Resource: kcpGVR.Resource,
211+
IdentityHash: rr.IdentityHash,
212+
})
213+
}
214+
}
177215
}
178216

179217
// We always want to create events.
180-
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
218+
claimedResources.Insert(permissionClaim{
181219
Group: "",
182220
Resource: "events",
183221
})
184222

185-
if arsList.Len() == 0 {
186-
r.log.Debug("No ready PublishedResources available.")
187-
return nil
188-
}
189-
190223
// reconcile an APIExport in kcp
191224
factories := []reconciling.NamedAPIExportReconcilerFactory{
192-
r.createAPIExportReconciler(arsList, claimedResources, r.agentName, r.apiExportName, r.recorder),
225+
r.createAPIExportReconciler(newARSList, claimedResources, r.agentName, r.apiExportName, r.recorder),
193226
}
194227

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

0 commit comments

Comments
 (0)