Skip to content

Commit 4d36ba0

Browse files
authored
contrib: add kcp schema serving (#493)
* contrib kcp: add apibinding serving Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt> On-behalf-of: SAP <mangirdas.judeikis@sap.com> * review comments * bump kcp * address reviews * partially revert go-install to use unreleased versions * review round 2 --------- Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
1 parent d904a78 commit 4d36ba0

14 files changed

Lines changed: 1596 additions & 141 deletions

File tree

Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ GOLANGCI_LINT_VERSION := 2.1.6
3636
GORELEASER_VERSION := 2.13.0
3737
GOTESTSUM_VERSION := 1.8.1
3838
HELM_VERSION := 3.18.6
39-
KCP_VERSION := 0.29.0
39+
# unreleased kcp version with vw code for schemas
40+
KCP_VERSION := 301a8f749e7b99a0c81f43b37aa5b5e5ff0fc0b4
4041
KUBE_APPLYCONFIGURATION_GEN_VERSION := v0.32.0
4142
KUBE_CLIENT_GEN_VERSION := v0.32.0
4243
KUBE_INFORMER_GEN_VERSION := v0.32.0
@@ -156,7 +157,11 @@ install-boilerplate:
156157

157158
.PHONY: install-kcp
158159
install-kcp:
159-
@hack/uget.sh https://github.com/kcp-dev/kcp/releases/download/v{VERSION}/kcp_{VERSION}_{GOOS}_{GOARCH}.tar.gz kcp $(KCP_VERSION)
160+
@if echo "$(KCP_VERSION)" | grep -qE '^v?[0-9]+\.[0-9]+'; then \
161+
hack/uget.sh https://github.com/kcp-dev/kcp/releases/download/v{VERSION}/kcp_{VERSION}_{GOOS}_{GOARCH}.tar.gz kcp $(KCP_VERSION); \
162+
else \
163+
GOBIN=$(abspath $(UGET_DIRECTORY)) hack/go-install.sh github.com/kcp-dev/kcp/cmd/kcp kcp $(KCP_VERSION); \
164+
fi
160165

161166
GORELEASER = $(UGET_DIRECTORY)/goreleaser-$(GORELEASER_VERSION)
162167

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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 apibindingtemplate contains a kcp-specific controller that watches
18+
// APIBindings in provider/backend workspaces and automatically creates or
19+
// updates APIServiceExportTemplates based on the APIResourceSchemas exposed by
20+
// each bound APIExport.
21+
package apibindingtemplate
22+
23+
import (
24+
"context"
25+
"fmt"
26+
"strings"
27+
28+
apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
29+
"k8s.io/apimachinery/pkg/api/errors"
30+
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/client-go/rest"
33+
ctrl "sigs.k8s.io/controller-runtime"
34+
"sigs.k8s.io/controller-runtime/pkg/client"
35+
"sigs.k8s.io/controller-runtime/pkg/cluster"
36+
"sigs.k8s.io/controller-runtime/pkg/controller"
37+
"sigs.k8s.io/controller-runtime/pkg/handler"
38+
"sigs.k8s.io/controller-runtime/pkg/log"
39+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
40+
mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
41+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
42+
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
43+
44+
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/shared"
45+
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
46+
)
47+
48+
const controllerName = "kube-bind-kcp-apibinding-template"
49+
50+
// APIBindingTemplateReconciler watches APIBindings and ensures an
51+
// APIServiceExportTemplate exists for every bound APIExport.
52+
type APIBindingTemplateReconciler struct {
53+
manager mcmanager.Manager
54+
opts controller.TypedOptions[mcreconcile.Request]
55+
ignorePrefixes []string
56+
scheme *runtime.Scheme
57+
vwCache *shared.VWClientCache
58+
}
59+
60+
// New returns a new APIBindingTemplateReconciler.
61+
func New(
62+
ctx context.Context,
63+
mgr mcmanager.Manager,
64+
opts controller.TypedOptions[mcreconcile.Request],
65+
ignorePrefixes []string,
66+
baseConfig *rest.Config,
67+
scheme *runtime.Scheme,
68+
) (*APIBindingTemplateReconciler, error) {
69+
r := &APIBindingTemplateReconciler{
70+
manager: mgr,
71+
opts: opts,
72+
ignorePrefixes: ignorePrefixes,
73+
scheme: scheme,
74+
vwCache: shared.NewVWClientCache(baseConfig, scheme),
75+
}
76+
77+
return r, nil
78+
}
79+
80+
// shouldIgnore returns true if the APIBinding name matches any of the
81+
// configured ignore prefixes.
82+
func (r *APIBindingTemplateReconciler) shouldIgnore(name string) bool {
83+
for _, prefix := range r.ignorePrefixes {
84+
if strings.HasPrefix(name, prefix) {
85+
return true
86+
}
87+
}
88+
return false
89+
}
90+
91+
// Reconcile implements reconcile.Reconciler for multicluster-runtime.
92+
func (r *APIBindingTemplateReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
93+
logger := log.FromContext(ctx)
94+
95+
if r.shouldIgnore(req.Name) {
96+
logger.V(4).Info("Ignoring APIBinding matching ignore prefix", "name", req.Name)
97+
return ctrl.Result{}, nil
98+
}
99+
100+
logger.Info("Reconciling APIBinding", "request", req)
101+
102+
cl, err := r.manager.GetCluster(ctx, req.ClusterName)
103+
if err != nil {
104+
return ctrl.Result{}, fmt.Errorf("failed to get client for cluster %q: %w", req.ClusterName, err)
105+
}
106+
107+
c := cl.GetClient()
108+
clusterConfig := cl.GetConfig()
109+
110+
binding := &apisv1alpha2.APIBinding{}
111+
if err := c.Get(ctx, req.NamespacedName, binding); err != nil {
112+
if errors.IsNotFound(err) {
113+
return ctrl.Result{}, nil
114+
}
115+
return ctrl.Result{}, fmt.Errorf("failed to get APIBinding %q: %w", req.Name, err)
116+
}
117+
118+
// Build the schema getter with VW fallback using the shared cache.
119+
getSchema := shared.SchemaGetterWithFallback(c, clusterConfig, r.vwCache)
120+
121+
rec := reconciler{
122+
client: c,
123+
scheme: r.scheme,
124+
getAPIResourceSchema: getSchema,
125+
}
126+
127+
if err := rec.reconcile(ctx, binding); err != nil {
128+
logger.Error(err, "Failed to reconcile APIBinding", "name", req.Name)
129+
return ctrl.Result{}, err
130+
}
131+
132+
return ctrl.Result{}, nil
133+
}
134+
135+
// getTemplateMapper returns a mapper that enqueues the owning APIBinding when
136+
// 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.
141+
func getTemplateMapper(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] {
142+
return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []mcreconcile.Request {
143+
annotations := obj.GetAnnotations()
144+
if annotations == nil {
145+
return nil
146+
}
147+
ownerName, ok := annotations[shared.AnnotationOwnerBinding]
148+
if !ok {
149+
return nil
150+
}
151+
152+
c := cl.GetClient()
153+
var binding apisv1alpha2.APIBinding
154+
if err := c.Get(ctx, client.ObjectKey{Name: ownerName}, &binding); err != nil {
155+
return nil
156+
}
157+
158+
return []mcreconcile.Request{
159+
{
160+
Request: reconcile.Request{
161+
NamespacedName: types.NamespacedName{Name: ownerName},
162+
},
163+
ClusterName: clusterName,
164+
},
165+
}
166+
})
167+
}
168+
169+
// SetupWithManager registers the controller with the multicluster-runtime Manager.
170+
func (r *APIBindingTemplateReconciler) SetupWithManager(mgr mcmanager.Manager) error {
171+
return mcbuilder.ControllerManagedBy(mgr).
172+
For(&apisv1alpha2.APIBinding{}).
173+
Watches(
174+
&kubebindv1alpha2.APIServiceExportTemplate{},
175+
getTemplateMapper,
176+
).
177+
WithOptions(r.opts).
178+
Named(controllerName).
179+
Complete(r)
180+
}

0 commit comments

Comments
 (0)