Skip to content

Commit da26ad9

Browse files
committed
Wire in apibinding backend flow
1 parent 952aac1 commit da26ad9

6 files changed

Lines changed: 380 additions & 8 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
157157

158158
rec := reconciler{
159159
client: c,
160+
scheme: r.scheme,
160161
getAPIResourceSchema: getSchema,
161162
}
162163

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626
"k8s.io/apimachinery/pkg/api/equality"
2727
"k8s.io/apimachinery/pkg/api/errors"
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/runtime"
2930
"k8s.io/apimachinery/pkg/types"
3031
"k8s.io/klog/v2"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3234

3335
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
3436
)
@@ -45,6 +47,7 @@ const annotationOwnerBinding = "apibindingtemplate.kube-bind.io/owner-binding"
4547

4648
type reconciler struct {
4749
client client.Client
50+
scheme *runtime.Scheme
4851
getAPIResourceSchema func(ctx context.Context, name string) (*apisv1alpha1.APIResourceSchema, error)
4952
}
5053

@@ -66,6 +69,11 @@ func (r *reconciler) reconcile(ctx context.Context, binding *apisv1alpha2.APIBin
6669

6770
templateName := templateNameForBinding(binding.Name)
6871

72+
// Set owner reference: APIBinding owns the template.
73+
if err := controllerutil.SetOwnerReference(binding, desired, r.scheme); err != nil {
74+
return fmt.Errorf("failed to set owner reference on APIServiceExportTemplate %q: %w", templateName, err)
75+
}
76+
6977
existing := &kubebindv1alpha2.APIServiceExportTemplate{}
7078
err = r.client.Get(ctx, types.NamespacedName{Name: templateName}, existing)
7179
if errors.IsNotFound(err) {
@@ -79,14 +87,27 @@ func (r *reconciler) reconcile(ctx context.Context, binding *apisv1alpha2.APIBin
7987
return fmt.Errorf("failed to get APIServiceExportTemplate %q: %w", templateName, err)
8088
}
8189

82-
if equality.Semantic.DeepEqual(existing.Spec, desired.Spec) {
90+
needsUpdate := false
91+
92+
if !equality.Semantic.DeepEqual(existing.Spec, desired.Spec) {
93+
existing.Spec = desired.Spec
94+
needsUpdate = true
95+
}
96+
97+
// Ensure owner reference is set.
98+
if !metav1.IsControlledBy(existing, binding) {
99+
if err := controllerutil.SetOwnerReference(binding, existing, r.scheme); err != nil {
100+
return fmt.Errorf("failed to set owner reference on APIServiceExportTemplate %q: %w", templateName, err)
101+
}
102+
needsUpdate = true
103+
}
104+
105+
if !needsUpdate {
83106
return nil
84107
}
85108

86-
updated := existing.DeepCopy()
87-
updated.Spec = desired.Spec
88109
logger.Info("Updating APIServiceExportTemplate", "name", templateName, "binding", binding.Name)
89-
if err := r.client.Update(ctx, updated); err != nil {
110+
if err := r.client.Update(ctx, existing); err != nil {
90111
return fmt.Errorf("failed to update APIServiceExportTemplate %q: %w", templateName, err)
91112
}
92113

@@ -97,7 +118,7 @@ func (r *reconciler) reconcile(ctx context.Context, binding *apisv1alpha2.APIBin
97118
// APIBinding's bound resources, fetching each APIResourceSchema to extract
98119
// version and scope information.
99120
func (r *reconciler) buildTemplate(ctx context.Context, binding *apisv1alpha2.APIBinding) (*kubebindv1alpha2.APIServiceExportTemplate, error) {
100-
var resources []kubebindv1alpha2.APIServiceExportResource
121+
resources := make([]kubebindv1alpha2.APIServiceExportResource, 0, len(binding.Status.BoundResources))
101122
scope := kubebindv1alpha2.NamespacedScope
102123

103124
for _, boundRes := range binding.Status.BoundResources {
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package apiresourceschema contains a kcp-specific controller that watches
18+
// APIServiceExportTemplates and copies the corresponding APIResourceSchemas
19+
// into the workspace so that the serviceexportrequest controller can find them.
20+
package apiresourceschema
21+
22+
import (
23+
"context"
24+
"fmt"
25+
"net/url"
26+
"strings"
27+
28+
apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
29+
apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
30+
"k8s.io/apimachinery/pkg/api/errors"
31+
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/apimachinery/pkg/types"
33+
"k8s.io/client-go/rest"
34+
ctrl "sigs.k8s.io/controller-runtime"
35+
"sigs.k8s.io/controller-runtime/pkg/client"
36+
"sigs.k8s.io/controller-runtime/pkg/controller"
37+
"sigs.k8s.io/controller-runtime/pkg/log"
38+
mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
39+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
40+
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
41+
42+
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
43+
)
44+
45+
const controllerName = "kube-bind-kcp-apiresourceschema"
46+
47+
// annotationOwnerBinding is the annotation key linking a template to its APIBinding.
48+
// Shared with the apibindingtemplate controller.
49+
const annotationOwnerBinding = "apibindingtemplate.kube-bind.io/owner-binding"
50+
51+
// APIResourceSchemaReconciler watches APIServiceExportTemplates and ensures
52+
// that the APIResourceSchemas referenced by the owning APIBinding are copied
53+
// into the workspace with the kube-bind.io/exported=true label.
54+
type APIResourceSchemaReconciler struct {
55+
manager mcmanager.Manager
56+
opts controller.TypedOptions[mcreconcile.Request]
57+
baseConfig *rest.Config
58+
scheme *runtime.Scheme
59+
}
60+
61+
// New returns a new APIResourceSchemaReconciler.
62+
func New(
63+
ctx context.Context,
64+
mgr mcmanager.Manager,
65+
opts controller.TypedOptions[mcreconcile.Request],
66+
baseConfig *rest.Config,
67+
scheme *runtime.Scheme,
68+
) (*APIResourceSchemaReconciler, error) {
69+
return &APIResourceSchemaReconciler{
70+
manager: mgr,
71+
opts: opts,
72+
baseConfig: baseConfig,
73+
scheme: scheme,
74+
}, nil
75+
}
76+
77+
// Reconcile implements reconcile.Reconciler for multicluster-runtime.
78+
func (r *APIResourceSchemaReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
79+
logger := log.FromContext(ctx)
80+
logger.Info("Reconciling APIServiceExportTemplate for schema copy", "request", req)
81+
82+
cl, err := r.manager.GetCluster(ctx, req.ClusterName)
83+
if err != nil {
84+
return ctrl.Result{}, fmt.Errorf("failed to get cluster %q: %w", req.ClusterName, err)
85+
}
86+
87+
c := cl.GetClient()
88+
clusterConfig := cl.GetConfig()
89+
90+
// Get the template.
91+
tmpl := &kubebindv1alpha2.APIServiceExportTemplate{}
92+
if err := c.Get(ctx, req.NamespacedName, tmpl); err != nil {
93+
if errors.IsNotFound(err) {
94+
return ctrl.Result{}, nil
95+
}
96+
return ctrl.Result{}, fmt.Errorf("failed to get APIServiceExportTemplate %q: %w", req.Name, err)
97+
}
98+
99+
// Find the owning APIBinding via annotation.
100+
bindingName, ok := tmpl.Annotations[annotationOwnerBinding]
101+
if !ok {
102+
logger.V(4).Info("Template has no owner-binding annotation, skipping", "name", tmpl.Name)
103+
return ctrl.Result{}, nil
104+
}
105+
106+
binding := &apisv1alpha2.APIBinding{}
107+
if err := c.Get(ctx, types.NamespacedName{Name: bindingName}, binding); err != nil {
108+
if errors.IsNotFound(err) {
109+
logger.Info("Owning APIBinding not found, skipping", "binding", bindingName)
110+
return ctrl.Result{}, nil
111+
}
112+
return ctrl.Result{}, fmt.Errorf("failed to get APIBinding %q: %w", bindingName, err)
113+
}
114+
115+
if binding.Status.Phase != apisv1alpha2.APIBindingPhaseBound {
116+
return ctrl.Result{}, nil
117+
}
118+
119+
// Build schema getter with VW fallback.
120+
getSchema := r.schemaGetterWithFallback(c, clusterConfig)
121+
122+
rec := reconciler{
123+
client: c,
124+
scheme: r.scheme,
125+
getAPIResourceSchema: getSchema,
126+
}
127+
128+
if err := rec.reconcile(ctx, tmpl, binding); err != nil {
129+
logger.Error(err, "Failed to reconcile schemas for template", "name", tmpl.Name)
130+
return ctrl.Result{}, err
131+
}
132+
133+
return ctrl.Result{}, nil
134+
}
135+
136+
// extractClusterID extracts the cluster ID from an apiexport virtual workspace URL.
137+
func extractClusterID(clusterConfig *rest.Config) (string, error) {
138+
u, err := url.Parse(clusterConfig.Host)
139+
if err != nil {
140+
return "", fmt.Errorf("failed to parse cluster host URL: %w", err)
141+
}
142+
143+
pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
144+
if len(pathParts) < 6 || pathParts[4] != "clusters" {
145+
return "", fmt.Errorf("unexpected apiexport URL format: %s", u.Path)
146+
}
147+
148+
return pathParts[5], nil
149+
}
150+
151+
// newVWClient creates a client pointing at the apiresourceschema virtual workspace.
152+
func (r *APIResourceSchemaReconciler) newVWClient(clusterID string) (client.Client, error) {
153+
cfg := rest.CopyConfig(r.baseConfig)
154+
u, err := url.Parse(cfg.Host)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to parse base config host: %w", err)
157+
}
158+
159+
u.Path = fmt.Sprintf("/services/apiresourceschema/%s/clusters/*", clusterID)
160+
cfg.Host = u.String()
161+
162+
return client.New(cfg, client.Options{Scheme: r.scheme})
163+
}
164+
165+
// schemaGetterWithFallback returns a function that first tries the workspace,
166+
// then falls back to the apiresourceschema virtual workspace.
167+
func (r *APIResourceSchemaReconciler) schemaGetterWithFallback(
168+
workspaceClient client.Client,
169+
clusterConfig *rest.Config,
170+
) func(ctx context.Context, name string) (*apisv1alpha1.APIResourceSchema, error) {
171+
return func(ctx context.Context, name string) (*apisv1alpha1.APIResourceSchema, error) {
172+
logger := log.FromContext(ctx)
173+
174+
var schema apisv1alpha1.APIResourceSchema
175+
err := workspaceClient.Get(ctx, client.ObjectKey{Name: name}, &schema)
176+
if err == nil {
177+
return &schema, nil
178+
}
179+
if !errors.IsNotFound(err) {
180+
return nil, err
181+
}
182+
183+
logger.V(2).Info("APIResourceSchema not found in workspace, trying VW fallback", "schema", name)
184+
185+
clusterID, err := extractClusterID(clusterConfig)
186+
if err != nil {
187+
return nil, fmt.Errorf("cannot build VW fallback client: %w", err)
188+
}
189+
190+
vwClient, err := r.newVWClient(clusterID)
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to create VW client for cluster %q: %w", clusterID, err)
193+
}
194+
195+
var vwSchema apisv1alpha1.APIResourceSchema
196+
if err := vwClient.Get(ctx, client.ObjectKey{Name: name}, &vwSchema); err != nil {
197+
return nil, fmt.Errorf("APIResourceSchema %q not found in workspace or VW: %w", name, err)
198+
}
199+
200+
return &vwSchema, nil
201+
}
202+
}
203+
204+
// SetupWithManager registers the controller with the multicluster-runtime Manager.
205+
func (r *APIResourceSchemaReconciler) SetupWithManager(mgr mcmanager.Manager) error {
206+
return mcbuilder.ControllerManagedBy(mgr).
207+
For(&kubebindv1alpha2.APIServiceExportTemplate{}).
208+
WithOptions(r.opts).
209+
Named(controllerName).
210+
Complete(r)
211+
}

0 commit comments

Comments
 (0)