@@ -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
110113func (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