Skip to content

Commit 1447333

Browse files
committed
review comments
1 parent 4bdb46a commit 1447333

5 files changed

Lines changed: 217 additions & 180 deletions

File tree

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

Lines changed: 11 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ package apibindingtemplate
2323
import (
2424
"context"
2525
"fmt"
26-
"net/url"
2726
"strings"
2827

29-
apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
3028
apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
3129
"k8s.io/apimachinery/pkg/api/errors"
3230
"k8s.io/apimachinery/pkg/runtime"
@@ -43,6 +41,7 @@ import (
4341
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
4442
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
4543

44+
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/shared"
4645
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4746
)
4847

@@ -54,11 +53,8 @@ type APIBindingTemplateReconciler struct {
5453
manager mcmanager.Manager
5554
opts controller.TypedOptions[mcreconcile.Request]
5655
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
56+
scheme *runtime.Scheme
57+
vwCache *shared.VWClientCache
6258
}
6359

6460
// New returns a new APIBindingTemplateReconciler.
@@ -74,8 +70,8 @@ func New(
7470
manager: mgr,
7571
opts: opts,
7672
ignorePrefixes: ignorePrefixes,
77-
baseConfig: baseConfig,
7873
scheme: scheme,
74+
vwCache: shared.NewVWClientCache(baseConfig, scheme),
7975
}
8076

8177
return r, nil
@@ -92,39 +88,6 @@ func (r *APIBindingTemplateReconciler) shouldIgnore(name string) bool {
9288
return false
9389
}
9490

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-
12891
// Reconcile implements reconcile.Reconciler for multicluster-runtime.
12992
func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
13093
logger := log.FromContext(ctx)
@@ -152,8 +115,8 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
152115
return ctrl.Result{}, fmt.Errorf("failed to get APIBinding %q: %w", req.Name, err)
153116
}
154117

155-
// Build the schema getter with VW fallback.
156-
getSchema := r.schemaGetterWithFallback(c, clusterConfig)
118+
// Build the schema getter with VW fallback using the shared cache.
119+
getSchema := shared.SchemaGetterWithFallback(c, clusterConfig, r.vwCache)
157120

158121
rec := reconciler{
159122
client: c,
@@ -169,57 +132,19 @@ func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreco
169132
return ctrl.Result{}, nil
170133
}
171134

172-
// schemaGetterWithFallback returns a function that first tries to get the
173-
// APIResourceSchema from the current workspace, and if not found, falls back
174-
// to the apiresourceschema virtual workspace.
175-
func (r *APIBindingTemplateReconciler) schemaGetterWithFallback(
176-
workspaceClient client.Client,
177-
clusterConfig *rest.Config,
178-
) func(ctx context.Context, name string) (*apisv1alpha1.APIResourceSchema, error) {
179-
return func(ctx context.Context, name string) (*apisv1alpha1.APIResourceSchema, error) {
180-
logger := log.FromContext(ctx)
181-
182-
// 1. Try the current workspace first.
183-
var schema apisv1alpha1.APIResourceSchema
184-
err := workspaceClient.Get(ctx, client.ObjectKey{Name: name}, &schema)
185-
if err == nil {
186-
return &schema, nil
187-
}
188-
if !errors.IsNotFound(err) {
189-
return nil, err
190-
}
191-
192-
// 2. Fallback: try the apiresourceschema virtual workspace.
193-
logger.V(2).Info("APIResourceSchema not found in workspace, trying VW fallback", "schema", name)
194-
195-
clusterID, err := extractClusterID(clusterConfig)
196-
if err != nil {
197-
return nil, fmt.Errorf("cannot build VW fallback client: %w", err)
198-
}
199-
200-
vwClient, err := r.newVWClient(clusterID)
201-
if err != nil {
202-
return nil, fmt.Errorf("failed to create VW client for cluster %q: %w", clusterID, err)
203-
}
204-
205-
var vwSchema apisv1alpha1.APIResourceSchema
206-
if err := vwClient.Get(ctx, client.ObjectKey{Name: name}, &vwSchema); err != nil {
207-
return nil, fmt.Errorf("APIResourceSchema %q not found in workspace or VW: %w", name, err)
208-
}
209-
210-
return &vwSchema, nil
211-
}
212-
}
213-
214135
// getTemplateMapper returns a mapper that enqueues the owning APIBinding when
215136
// an APIServiceExportTemplate changes.
137+
//
138+
// This function has the signature func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler
139+
// because multicluster-runtime's mcbuilder.Watches accepts a "per-cluster event handler factory"
140+
// rather than a plain handler — it calls this factory for each cluster that is engaged.
216141
func getTemplateMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] {
217142
return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []mcreconcile.Request {
218143
annotations := obj.GetAnnotations()
219144
if annotations == nil {
220145
return nil
221146
}
222-
ownerName, ok := annotations[annotationOwnerBinding]
147+
ownerName, ok := annotations[shared.AnnotationOwnerBinding]
223148
if !ok {
224149
return nil
225150
}

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"sigs.k8s.io/controller-runtime/pkg/client"
3333
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3434

35+
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/shared"
3536
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
3637
)
3738

@@ -46,10 +47,6 @@ func templateNameForBinding(bindingName string, scope kubebindv1alpha2.InformerS
4647
}
4748
}
4849

49-
// annotationOwnerBinding is the annotation key used to link a template back to
50-
// the APIBinding that owns it.
51-
const annotationOwnerBinding = "apibindingtemplate.kube-bind.io/owner-binding"
52-
5350
type reconciler struct {
5451
client client.Client
5552
scheme *runtime.Scheme
@@ -73,7 +70,10 @@ func (r *reconciler) reconcile(ctx context.Context, binding *apisv1alpha2.APIBin
7370
return err
7471
}
7572

73+
desiredNames := make(map[string]bool, len(templates))
7674
for _, desired := range templates {
75+
desiredNames[desired.Name] = true
76+
7777
// Set owner reference: APIBinding owns the template.
7878
if err := controllerutil.SetOwnerReference(binding, desired, r.scheme); err != nil {
7979
return fmt.Errorf("failed to set owner reference on APIServiceExportTemplate %q: %w", desired.Name, err)
@@ -84,6 +84,36 @@ func (r *reconciler) reconcile(ctx context.Context, binding *apisv1alpha2.APIBin
8484
}
8585
}
8686

87+
// Prune orphaned templates: if a binding used to have resources of a
88+
// given scope but no longer does, the old template must be deleted.
89+
return r.pruneOrphanedTemplates(ctx, binding, desiredNames)
90+
}
91+
92+
// pruneOrphanedTemplates lists templates owned by this binding and deletes any
93+
// that are no longer in the desired set.
94+
func (r *reconciler) pruneOrphanedTemplates(ctx context.Context, binding *apisv1alpha2.APIBinding, desiredNames map[string]bool) error {
95+
logger := klog.FromContext(ctx)
96+
97+
var allTemplates kubebindv1alpha2.APIServiceExportTemplateList
98+
if err := r.client.List(ctx, &allTemplates); err != nil {
99+
return fmt.Errorf("failed to list APIServiceExportTemplates: %w", err)
100+
}
101+
102+
for i := range allTemplates.Items {
103+
tmpl := &allTemplates.Items[i]
104+
ann := tmpl.Annotations
105+
if ann == nil || ann[shared.AnnotationOwnerBinding] != binding.Name {
106+
continue
107+
}
108+
if desiredNames[tmpl.Name] {
109+
continue
110+
}
111+
logger.Info("Deleting orphaned APIServiceExportTemplate", "name", tmpl.Name, "binding", binding.Name)
112+
if err := r.client.Delete(ctx, tmpl); err != nil && !errors.IsNotFound(err) {
113+
return fmt.Errorf("failed to delete orphaned APIServiceExportTemplate %q: %w", tmpl.Name, err)
114+
}
115+
}
116+
87117
return nil
88118
}
89119

@@ -184,7 +214,7 @@ func (r *reconciler) buildTemplates(ctx context.Context, binding *apisv1alpha2.A
184214
ObjectMeta: metav1.ObjectMeta{
185215
Name: templateName,
186216
Annotations: map[string]string{
187-
annotationOwnerBinding: binding.Name,
217+
shared.AnnotationOwnerBinding: binding.Name,
188218
},
189219
},
190220
Spec: kubebindv1alpha2.APIServiceExportTemplateSpec{

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

Lines changed: 12 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,33 @@ package apiresourceschema
2222
import (
2323
"context"
2424
"fmt"
25-
"net/url"
26-
"strings"
2725

28-
apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
2926
apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
3027
"k8s.io/apimachinery/pkg/api/errors"
3128
"k8s.io/apimachinery/pkg/runtime"
3229
"k8s.io/apimachinery/pkg/types"
3330
"k8s.io/client-go/rest"
3431
ctrl "sigs.k8s.io/controller-runtime"
35-
"sigs.k8s.io/controller-runtime/pkg/client"
3632
"sigs.k8s.io/controller-runtime/pkg/controller"
3733
"sigs.k8s.io/controller-runtime/pkg/log"
3834
mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
3935
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
4036
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
4137

38+
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/shared"
4239
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4340
)
4441

4542
const controllerName = "kube-bind-kcp-apiresourceschema"
4643

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-
5144
// APIResourceSchemaReconciler watches APIServiceExportTemplates and ensures
5245
// that the APIResourceSchemas referenced by the owning APIBinding are copied
5346
// into the workspace with the kube-bind.io/exported=true label.
5447
type APIResourceSchemaReconciler struct {
55-
manager mcmanager.Manager
56-
opts controller.TypedOptions[mcreconcile.Request]
57-
baseConfig *rest.Config
58-
scheme *runtime.Scheme
48+
manager mcmanager.Manager
49+
opts controller.TypedOptions[mcreconcile.Request]
50+
scheme *runtime.Scheme
51+
vwCache *shared.VWClientCache
5952
}
6053

6154
// New returns a new APIResourceSchemaReconciler.
@@ -67,10 +60,10 @@ func New(
6760
scheme *runtime.Scheme,
6861
) (*APIResourceSchemaReconciler, error) {
6962
return &APIResourceSchemaReconciler{
70-
manager: mgr,
71-
opts: opts,
72-
baseConfig: baseConfig,
73-
scheme: scheme,
63+
manager: mgr,
64+
opts: opts,
65+
scheme: scheme,
66+
vwCache: shared.NewVWClientCache(baseConfig, scheme),
7467
}, nil
7568
}
7669

@@ -97,7 +90,7 @@ func (r *APIResourceSchemaReconciler) Reconcile(ctx context.Context, req mcrecon
9790
}
9891

9992
// Find the owning APIBinding via annotation.
100-
bindingName, ok := tmpl.Annotations[annotationOwnerBinding]
93+
bindingName, ok := tmpl.Annotations[shared.AnnotationOwnerBinding]
10194
if !ok {
10295
logger.V(4).Info("Template has no owner-binding annotation, skipping", "name", tmpl.Name)
10396
return ctrl.Result{}, nil
@@ -116,8 +109,8 @@ func (r *APIResourceSchemaReconciler) Reconcile(ctx context.Context, req mcrecon
116109
return ctrl.Result{}, nil
117110
}
118111

119-
// Build schema getter with VW fallback.
120-
getSchema := r.schemaGetterWithFallback(c, clusterConfig)
112+
// Build schema getter with VW fallback using the shared cache.
113+
getSchema := shared.SchemaGetterWithFallback(c, clusterConfig, r.vwCache)
121114

122115
rec := reconciler{
123116
client: c,
@@ -133,74 +126,6 @@ func (r *APIResourceSchemaReconciler) Reconcile(ctx context.Context, req mcrecon
133126
return ctrl.Result{}, nil
134127
}
135128

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-
204129
// SetupWithManager registers the controller with the multicluster-runtime Manager.
205130
func (r *APIResourceSchemaReconciler) SetupWithManager(mgr mcmanager.Manager) error {
206131
return mcbuilder.ControllerManagedBy(mgr).

0 commit comments

Comments
 (0)