Skip to content

Commit 952aac1

Browse files
committed
schema fallback
1 parent 10f04c0 commit 952aac1

2 files changed

Lines changed: 94 additions & 9 deletions

File tree

backend/provider/kcp/controllers/apibindingtemplate/controller.go

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ package apibindingtemplate
2323
import (
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.
84129
func (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.
130215
func getTemplateMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] {

backend/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
271271
// Setup kcp-specific APIBindingTemplate controller. Only active when
272272
// provider is kcp (APIBindings only exist in kcp workspaces).
273273
if c.Options.Provider == "kcp" {
274-
s.APIBindingTemplate, err = apibindingtemplate.New(ctx, s.Config.Manager, opts, c.Options.ProviderKcp.APIBindingIgnorePrefixes)
274+
s.APIBindingTemplate, err = apibindingtemplate.New(ctx, s.Config.Manager, opts, c.Options.ProviderKcp.APIBindingIgnorePrefixes, c.ClientConfig, c.Scheme)
275275
if err != nil {
276276
return nil, fmt.Errorf("error setting up APIBindingTemplate Controller: %w", err)
277277
}

0 commit comments

Comments
 (0)