@@ -23,12 +23,15 @@ package apibindingtemplate
2323import (
2424 "context"
2525 "fmt"
26+ "net/url"
2627 "strings"
2728
2829 apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
2930 apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
3031 "k8s.io/apimachinery/pkg/api/errors"
32+ "k8s.io/apimachinery/pkg/runtime"
3133 "k8s.io/apimachinery/pkg/types"
34+ "k8s.io/client-go/rest"
3235 ctrl "sigs.k8s.io/controller-runtime"
3336 "sigs.k8s.io/controller-runtime/pkg/client"
3437 "sigs.k8s.io/controller-runtime/pkg/cluster"
@@ -51,6 +54,11 @@ type APIBindingTemplateReconciler struct {
5154 manager mcmanager.Manager
5255 opts controller.TypedOptions [mcreconcile.Request ]
5356 ignorePrefixes []string
57+
58+ // baseConfig is the kcp admin/root REST config used to construct
59+ // VW clients for the apiresourceschema virtual workspace.
60+ baseConfig * rest.Config
61+ scheme * runtime.Scheme
5462}
5563
5664// New returns a new APIBindingTemplateReconciler.
@@ -59,11 +67,15 @@ func New(
5967 mgr mcmanager.Manager ,
6068 opts controller.TypedOptions [mcreconcile.Request ],
6169 ignorePrefixes []string ,
70+ baseConfig * rest.Config ,
71+ scheme * runtime.Scheme ,
6272) (* APIBindingTemplateReconciler , error ) {
6373 r := & APIBindingTemplateReconciler {
6474 manager : mgr ,
6575 opts : opts ,
6676 ignorePrefixes : ignorePrefixes ,
77+ baseConfig : baseConfig ,
78+ scheme : scheme ,
6779 }
6880
6981 return r , nil
@@ -80,6 +92,39 @@ func (r *APIBindingTemplateReconciler) shouldIgnore(name string) bool {
8092 return false
8193}
8294
95+ // extractClusterID extracts the cluster ID from an apiexport virtual workspace
96+ // URL. The URL format is:
97+ // https://host:port/services/apiexport/root:org:ws/<apiexport-name>/clusters/{cluster-id}
98+ func extractClusterID (clusterConfig * rest.Config ) (string , error ) {
99+ u , err := url .Parse (clusterConfig .Host )
100+ if err != nil {
101+ return "" , fmt .Errorf ("failed to parse cluster host URL: %w" , err )
102+ }
103+
104+ pathParts := strings .Split (strings .Trim (u .Path , "/" ), "/" )
105+ if len (pathParts ) < 6 || pathParts [4 ] != "clusters" {
106+ return "" , fmt .Errorf ("unexpected apiexport URL format: %s" , u .Path )
107+ }
108+
109+ return pathParts [5 ], nil
110+ }
111+
112+ // newVWClient creates a client pointing at the apiresourceschema virtual workspace
113+ // for the given cluster ID:
114+ // https://host:port/services/apiresourceschema/{clusterID}/clusters/*/
115+ func (r * APIBindingTemplateReconciler ) newVWClient (clusterID string ) (client.Client , error ) {
116+ cfg := rest .CopyConfig (r .baseConfig )
117+ u , err := url .Parse (cfg .Host )
118+ if err != nil {
119+ return nil , fmt .Errorf ("failed to parse base config host: %w" , err )
120+ }
121+
122+ u .Path = fmt .Sprintf ("/services/apiresourceschema/%s/clusters/*" , clusterID )
123+ cfg .Host = u .String ()
124+
125+ return client .New (cfg , client.Options {Scheme : r .scheme })
126+ }
127+
83128// Reconcile implements reconcile.Reconciler for multicluster-runtime.
84129func (r * APIBindingTemplateReconciler ) Reconcile (ctx context.Context , req mcreconcile.Request ) (ctrl.Result , error ) {
85130 logger := log .FromContext (ctx )
@@ -97,6 +142,7 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
97142 }
98143
99144 c := cl .GetClient ()
145+ clusterConfig := cl .GetConfig ()
100146
101147 binding := & apisv1alpha2.APIBinding {}
102148 if err := c .Get (ctx , req .NamespacedName , binding ); err != nil {
@@ -106,15 +152,12 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
106152 return ctrl.Result {}, fmt .Errorf ("failed to get APIBinding %q: %w" , req .Name , err )
107153 }
108154
155+ // Build the schema getter with VW fallback.
156+ getSchema := r .schemaGetterWithFallback (c , clusterConfig )
157+
109158 rec := reconciler {
110- client : c ,
111- getAPIResourceSchema : func (ctx context.Context , name string ) (* apisv1alpha1.APIResourceSchema , error ) {
112- var schema apisv1alpha1.APIResourceSchema
113- if err := c .Get (ctx , client.ObjectKey {Name : name }, & schema ); err != nil {
114- return nil , err
115- }
116- return & schema , nil
117- },
159+ client : c ,
160+ getAPIResourceSchema : getSchema ,
118161 }
119162
120163 if err := rec .reconcile (ctx , binding ); err != nil {
@@ -125,6 +168,48 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
125168 return ctrl.Result {}, nil
126169}
127170
171+ // schemaGetterWithFallback returns a function that first tries to get the
172+ // APIResourceSchema from the current workspace, and if not found, falls back
173+ // to the apiresourceschema virtual workspace.
174+ func (r * APIBindingTemplateReconciler ) schemaGetterWithFallback (
175+ workspaceClient client.Client ,
176+ clusterConfig * rest.Config ,
177+ ) func (ctx context.Context , name string ) (* apisv1alpha1.APIResourceSchema , error ) {
178+ return func (ctx context.Context , name string ) (* apisv1alpha1.APIResourceSchema , error ) {
179+ logger := log .FromContext (ctx )
180+
181+ // 1. Try the current workspace first.
182+ var schema apisv1alpha1.APIResourceSchema
183+ err := workspaceClient .Get (ctx , client.ObjectKey {Name : name }, & schema )
184+ if err == nil {
185+ return & schema , nil
186+ }
187+ if ! errors .IsNotFound (err ) {
188+ return nil , err
189+ }
190+
191+ // 2. Fallback: try the apiresourceschema virtual workspace.
192+ logger .V (2 ).Info ("APIResourceSchema not found in workspace, trying VW fallback" , "schema" , name )
193+
194+ clusterID , err := extractClusterID (clusterConfig )
195+ if err != nil {
196+ return nil , fmt .Errorf ("cannot build VW fallback client: %w" , err )
197+ }
198+
199+ vwClient , err := r .newVWClient (clusterID )
200+ if err != nil {
201+ return nil , fmt .Errorf ("failed to create VW client for cluster %q: %w" , clusterID , err )
202+ }
203+
204+ var vwSchema apisv1alpha1.APIResourceSchema
205+ if err := vwClient .Get (ctx , client.ObjectKey {Name : name }, & vwSchema ); err != nil {
206+ return nil , fmt .Errorf ("APIResourceSchema %q not found in workspace or VW: %w" , name , err )
207+ }
208+
209+ return & vwSchema , nil
210+ }
211+ }
212+
128213// getTemplateMapper returns a mapper that enqueues the owning APIBinding when
129214// an APIServiceExportTemplate changes.
130215func getTemplateMapper (clusterName string , cl cluster.Cluster ) handler.TypedEventHandler [client.Object , mcreconcile.Request ] {
0 commit comments