Skip to content

Commit 2f33c59

Browse files
committed
Move serviceexport to controller-runtime
1 parent c80884d commit 2f33c59

6 files changed

Lines changed: 141 additions & 219 deletions

File tree

cmd/example-backend/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func run(ctx context.Context) error {
8080
if err != nil {
8181
return err
8282
}
83-
server, err := backend.NewServer(config)
83+
server, err := backend.NewServer(ctx, config)
8484
if err != nil {
8585
return err
8686
}

contrib/example-backend/controllers/serviceexport/serviceexport_controller.go

Lines changed: 107 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,47 @@ package serviceexport
1919
import (
2020
"context"
2121
"fmt"
22-
"time"
22+
"reflect"
2323

2424
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
25-
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
26-
apiextensionslisters "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
2725
"k8s.io/apimachinery/pkg/api/errors"
2826
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29-
utilerrors "k8s.io/apimachinery/pkg/util/errors"
30-
"k8s.io/apimachinery/pkg/util/runtime"
31-
"k8s.io/apimachinery/pkg/util/wait"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/types"
3229
"k8s.io/client-go/rest"
33-
"k8s.io/client-go/tools/cache"
34-
"k8s.io/client-go/util/workqueue"
35-
"k8s.io/klog/v2"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/cache"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/handler"
34+
"sigs.k8s.io/controller-runtime/pkg/log"
35+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3636

37-
"github.com/kube-bind/kube-bind/pkg/committer"
3837
"github.com/kube-bind/kube-bind/pkg/indexers"
3938
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4039
bindclient "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned"
41-
bindinformers "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/kubebind/v1alpha2"
42-
bindlisters "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2"
4340
)
4441

4542
const (
4643
controllerName = "kube-bind-example-backend-serviceexport"
4744
)
4845

49-
// NewController returns a new controller to reconcile ServiceExports.
50-
func NewController(
51-
config *rest.Config,
52-
serviceExportInformer bindinformers.APIServiceExportInformer,
53-
crdInformer apiextensionsinformers.CustomResourceDefinitionInformer,
54-
) (*Controller, error) {
55-
queue := workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{Name: controllerName})
46+
// APIServiceExportReconciler reconciles a APIServiceExport object.
47+
type APIServiceExportReconciler struct {
48+
client.Client
49+
Scheme *runtime.Scheme
5650

57-
logger := klog.Background().WithValues("controller", controllerName)
51+
bindClient bindclient.Interface
52+
reconciler reconciler
53+
}
5854

55+
// NewAPIServiceExportReconciler returns a new APIServiceExportReconciler to reconcile APIServiceExports.
56+
func NewAPIServiceExportReconciler(
57+
ctx context.Context,
58+
client client.Client,
59+
scheme *runtime.Scheme,
60+
config *rest.Config,
61+
cache cache.Cache,
62+
) (*APIServiceExportReconciler, error) {
5963
config = rest.CopyConfig(config)
6064
config = rest.AddUserAgent(config, controllerName)
6165

@@ -64,219 +68,119 @@ func NewController(
6468
return nil, err
6569
}
6670

67-
c := &Controller{
68-
queue: queue,
71+
// Set up field indexer for APIServiceExports by CustomResourceDefinition name
72+
// This replicates the behavior of the original indexer setup
73+
if err := cache.IndexField(ctx, &kubebindv1alpha2.APIServiceExport{}, indexers.ServiceExportByCustomResourceDefinition,
74+
indexers.IndexServiceExportByCustomResourceDefinitionControllerRuntime); err != nil {
75+
return nil, fmt.Errorf("failed to setup ServiceExportByCustomResourceDefinition indexer: %w", err)
76+
}
6977

78+
r := &APIServiceExportReconciler{
79+
Client: client,
80+
Scheme: scheme,
7081
bindClient: bindClient,
71-
72-
serviceExportLister: serviceExportInformer.Lister(),
73-
serviceExportIndexer: serviceExportInformer.Informer().GetIndexer(),
74-
75-
crdLister: crdInformer.Lister(),
76-
crdIndexer: crdInformer.Informer().GetIndexer(),
77-
7882
reconciler: reconciler{
79-
getCRD: func(name string) (*apiextensionsv1.CustomResourceDefinition, error) {
80-
return crdInformer.Lister().Get(name)
83+
getCRD: func(ctx context.Context, name string) (*apiextensionsv1.CustomResourceDefinition, error) {
84+
var crd apiextensionsv1.CustomResourceDefinition
85+
key := types.NamespacedName{Name: name}
86+
if err := cache.Get(ctx, key, &crd); err != nil {
87+
return nil, err
88+
}
89+
return &crd, nil
8190
},
8291
getAPIResourceSchema: func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error) {
8392
return bindClient.KubeBindV1alpha2().APIResourceSchemas().Get(ctx, name, metav1.GetOptions{})
8493
},
8594
deleteServiceExport: func(ctx context.Context, ns, name string) error {
8695
return bindClient.KubeBindV1alpha2().APIServiceExports(ns).Delete(ctx, name, metav1.DeleteOptions{})
8796
},
88-
requeue: func(export *kubebindv1alpha2.APIServiceExport) {
89-
key, err := cache.MetaNamespaceKeyFunc(export)
90-
if err != nil {
91-
runtime.HandleError(err)
92-
return
93-
}
94-
queue.Add(key)
95-
},
96-
},
97-
98-
commit: committer.NewCommitter[*kubebindv1alpha2.APIServiceExport, *kubebindv1alpha2.APIServiceExportSpec, *kubebindv1alpha2.APIServiceExportStatus](
99-
func(ns string) committer.Patcher[*kubebindv1alpha2.APIServiceExport] {
100-
return bindClient.KubeBindV1alpha2().APIServiceExports(ns)
101-
},
102-
),
103-
}
104-
105-
indexers.AddIfNotPresentOrDie(serviceExportInformer.Informer().GetIndexer(), cache.Indexers{
106-
indexers.ServiceExportByCustomResourceDefinition: indexers.IndexServiceExportByCustomResourceDefinition,
107-
})
108-
109-
if _, err := serviceExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
110-
AddFunc: func(obj any) {
111-
c.enqueueServiceExport(logger, obj)
11297
},
113-
UpdateFunc: func(old, newObj any) {
114-
c.enqueueServiceExport(logger, newObj)
115-
},
116-
DeleteFunc: func(obj any) {
117-
c.enqueueServiceExport(logger, obj)
118-
},
119-
}); err != nil {
120-
return nil, err
121-
}
122-
123-
if _, err := crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
124-
AddFunc: func(obj any) {
125-
c.enqueueCRD(logger, obj)
126-
},
127-
UpdateFunc: func(old, newObj any) {
128-
c.enqueueCRD(logger, newObj)
129-
},
130-
DeleteFunc: func(obj any) {
131-
c.enqueueCRD(logger, obj)
132-
},
133-
}); err != nil {
134-
return nil, err
13598
}
13699

137-
return c, nil
100+
return r, nil
138101
}
139102

140-
type Resource = committer.Resource[*kubebindv1alpha2.APIServiceExportSpec, *kubebindv1alpha2.APIServiceExportStatus]
141-
type CommitFunc = func(context.Context, *Resource, *Resource) error
103+
// createCRDMapper creates a mapping function that can access the client for looking up related APIServiceExports.
104+
func (r *APIServiceExportReconciler) createCRDMapper() handler.MapFunc {
105+
return func(ctx context.Context, obj client.Object) []reconcile.Request {
106+
crd := obj.(*apiextensionsv1.CustomResourceDefinition)
142107

143-
// Controller reconciles ServiceNamespaces by creating a Namespace for each, and deleting it if
144-
// the APIServiceNamespace is deleted.
145-
type Controller struct {
146-
queue workqueue.TypedRateLimitingInterface[string]
108+
// Use the field indexer to find APIServiceExports related to this CRD
109+
// The indexer key should match the CRD name (namespace/name format for namespaced objects)
110+
crdKey := crd.Name // CRDs are cluster-scoped, so just the name
147111

148-
bindClient bindclient.Interface
149-
150-
serviceExportLister bindlisters.APIServiceExportLister
151-
serviceExportIndexer cache.Indexer
152-
153-
crdLister apiextensionslisters.CustomResourceDefinitionLister
154-
crdIndexer cache.Indexer
155-
156-
reconciler
112+
var exports kubebindv1alpha2.APIServiceExportList
113+
if err := r.List(ctx, &exports, client.MatchingFields{indexers.ServiceExportByCustomResourceDefinition: crdKey}); err != nil {
114+
return []reconcile.Request{}
115+
}
157116

158-
commit CommitFunc
159-
}
117+
var requests []reconcile.Request
118+
for _, export := range exports.Items {
119+
requests = append(requests, reconcile.Request{
120+
NamespacedName: types.NamespacedName{
121+
Namespace: export.Namespace,
122+
Name: export.Name,
123+
},
124+
})
125+
}
160126

161-
func (c *Controller) enqueueServiceExport(logger klog.Logger, obj any) {
162-
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
163-
if err != nil {
164-
runtime.HandleError(err)
165-
return
127+
return requests
166128
}
167-
168-
logger.V(2).Info("queueing APIServiceExport", "key", key)
169-
c.queue.Add(key)
170129
}
171130

172-
func (c *Controller) enqueueCRD(logger klog.Logger, obj any) {
173-
crdKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
174-
if err != nil {
175-
runtime.HandleError(err)
176-
return
177-
}
178-
179-
exports, err := c.serviceExportIndexer.ByIndex(indexers.ServiceExportByCustomResourceDefinition, crdKey)
180-
if err != nil {
181-
runtime.HandleError(err)
182-
return
183-
}
184-
185-
for _, obj := range exports {
186-
export, ok := obj.(*kubebindv1alpha2.APIServiceExport)
187-
if !ok {
188-
runtime.HandleError(fmt.Errorf("unexpected type %T", obj))
189-
return
190-
}
191-
key, err := cache.MetaNamespaceKeyFunc(export)
192-
if err != nil {
193-
runtime.HandleError(err)
194-
continue
131+
//+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiserviceexports,verbs=get;list;watch;create;update;patch;delete
132+
//+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiserviceexports/status,verbs=get;update;patch
133+
//+kubebuilder:rbac:groups=kubebind.k8s.io,resources=apiserviceexports/finalizers,verbs=update
134+
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch
135+
136+
// Reconcile is part of the main kubernetes reconciliation loop which aims to
137+
// move the current state of the cluster closer to the desired state.
138+
func (r *APIServiceExportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
139+
logger := log.FromContext(ctx)
140+
logger.Info("Reconciling APIServiceExport", "namespace", req.Namespace, "name", req.Name)
141+
142+
// Fetch the APIServiceExport instance
143+
apiServiceExport := &kubebindv1alpha2.APIServiceExport{}
144+
if err := r.Get(ctx, req.NamespacedName, apiServiceExport); err != nil {
145+
if errors.IsNotFound(err) {
146+
// Request object not found, could have been deleted after reconcile request.
147+
logger.Info("APIServiceExport not found, ignoring")
148+
return ctrl.Result{}, nil
195149
}
196-
logger.V(2).Info("queueing APIServiceExport", "key", key, "reason", "CustomResourceDefinition", "CustomResourceDefinitionKey", crdKey)
197-
c.queue.Add(key)
150+
// Error reading the object - requeue the request.
151+
return ctrl.Result{}, fmt.Errorf("failed to get APIServiceExport: %w", err)
198152
}
199-
}
200-
201-
// Start starts the controller, which stops when ctx.Done() is closed.
202-
func (c *Controller) Start(ctx context.Context, numThreads int) {
203-
defer runtime.HandleCrash()
204-
defer c.queue.ShutDown()
205-
206-
logger := klog.FromContext(ctx).WithValues("controller", controllerName)
207-
208-
logger.Info("Starting controller")
209-
defer logger.Info("Shutting down controller")
210-
211-
for i := 0; i < numThreads; i++ {
212-
go wait.UntilWithContext(ctx, c.startWorker, time.Second)
213-
}
214-
215-
<-ctx.Done()
216-
}
217153

218-
func (c *Controller) startWorker(ctx context.Context) {
219-
defer runtime.HandleCrash()
154+
// Create a copy to modify
155+
original := apiServiceExport.DeepCopy()
220156

221-
for c.processNextWorkItem(ctx) {
157+
// Run the reconciliation logic
158+
if err := r.reconciler.reconcile(ctx, apiServiceExport); err != nil {
159+
logger.Error(err, "Failed to reconcile APIServiceExport")
160+
return ctrl.Result{}, err
222161
}
223-
}
224162

225-
func (c *Controller) processNextWorkItem(ctx context.Context) bool {
226-
// Wait until there is a new item in the working queue
227-
key, quit := c.queue.Get()
228-
if quit {
229-
return false
163+
// Update status if it has changed
164+
if !reflect.DeepEqual(original, apiServiceExport) {
165+
err := r.Update(ctx, apiServiceExport)
166+
if err != nil {
167+
logger.Error(err, "Failed to update APIServiceExport status")
168+
return ctrl.Result{}, fmt.Errorf("failed to update APIServiceExport status: %w", err)
169+
}
170+
logger.Info("APIServiceExport status updated", "namespace", apiServiceExport.Namespace, "name", apiServiceExport.Name)
230171
}
231172

232-
logger := klog.FromContext(ctx).WithValues("key", key)
233-
ctx = klog.NewContext(ctx, logger)
234-
logger.V(2).Info("processing key")
235-
236-
// No matter what, tell the queue we're done with this key, to unblock
237-
// other workers.
238-
defer c.queue.Done(key)
239-
240-
if err := c.process(ctx, key); err != nil {
241-
runtime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err))
242-
c.queue.AddRateLimited(key)
243-
return true
244-
}
245-
c.queue.Forget(key)
246-
return true
173+
return ctrl.Result{}, nil
247174
}
248175

249-
func (c *Controller) process(ctx context.Context, key string) error {
250-
ns, name, err := cache.SplitMetaNamespaceKey(key)
251-
if err != nil {
252-
runtime.HandleError(err)
253-
return nil // we cannot do anything
254-
}
255-
256-
obj, err := c.serviceExportLister.APIServiceExports(ns).Get(name)
257-
if err != nil && !errors.IsNotFound(err) {
258-
return err
259-
} else if errors.IsNotFound(err) {
260-
return nil // nothing we can do
261-
}
262-
263-
old := obj
264-
obj = obj.DeepCopy()
265-
266-
var errs []error
267-
if err := c.reconcile(ctx, obj); err != nil {
268-
errs = append(errs, err)
269-
}
270-
271-
// Regardless of whether reconcile returned an error or not, always try to patch status if needed. Return the
272-
// reconciliation error at the end.
273-
274-
// If the object being reconciled changed as a result, update it.
275-
oldResource := &Resource{ObjectMeta: old.ObjectMeta, Spec: &old.Spec, Status: &old.Status}
276-
newResource := &Resource{ObjectMeta: obj.ObjectMeta, Spec: &obj.Spec, Status: &obj.Status}
277-
if err := c.commit(ctx, oldResource, newResource); err != nil {
278-
errs = append(errs, err)
279-
}
280-
281-
return utilerrors.NewAggregate(errs)
176+
// SetupWithManager sets up the controller with the Manager.
177+
func (r *APIServiceExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
178+
return ctrl.NewControllerManagedBy(mgr).
179+
For(&kubebindv1alpha2.APIServiceExport{}).
180+
Watches(
181+
&apiextensionsv1.CustomResourceDefinition{},
182+
handler.EnqueueRequestsFromMapFunc(r.createCRDMapper()),
183+
).
184+
Named(controllerName).
185+
Complete(r)
282186
}

contrib/example-backend/controllers/serviceexport/serviceexport_reconcile.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,9 @@ import (
3434
)
3535

3636
type reconciler struct {
37-
getCRD func(name string) (*apiextensionsv1.CustomResourceDefinition, error)
37+
getCRD func(ctx context.Context, name string) (*apiextensionsv1.CustomResourceDefinition, error)
3838
getAPIResourceSchema func(ctx context.Context, name string) (*kubebindv1alpha2.APIResourceSchema, error)
3939
deleteServiceExport func(ctx context.Context, namespace, name string) error
40-
41-
requeue func(export *kubebindv1alpha2.APIServiceExport)
4240
}
4341

4442
func (r *reconciler) reconcile(ctx context.Context, export *kubebindv1alpha2.APIServiceExport) error {

0 commit comments

Comments
 (0)