Skip to content

Commit 876d626

Browse files
Add DeviceClass lifecycle management to DRA reconciler
Extend the DRAReconciler to manage cluster-scoped DeviceClass resources declared in spec.dra.deviceClasses. DeviceClasses use label-based ownership (ModuleNameLabel + ModuleNamespaceLabel) since ownerReferences cannot reference namespaced resources from cluster-scoped objects. The reconciler performs declarative convergence on each cycle: - Creates missing DeviceClasses with ownership labels - Patches existing DeviceClasses to correct spec drift - Deletes stale DeviceClasses no longer in the desired list - Cleans up all DeviceClasses on Module deletion or DRA removal Errors are aggregated via errors.Join to attempt all operations before requeueing. NotFound errors on delete are tolerated to handle races. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a834e7f commit 876d626

4 files changed

Lines changed: 526 additions & 60 deletions

File tree

internal/constants/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package constants
22

33
const (
44
ModuleNameLabel = "kmm.node.kubernetes.io/module.name"
5+
ModuleNamespaceLabel = "kmm.node.kubernetes.io/module.namespace"
56
NodeLabelerFinalizer = "kmm.node.kubernetes.io/node-labeler"
67
TargetKernelTarget = "kmm.node.kubernetes.io/target-kernel"
78
ResourceType = "kmm.node.kubernetes.io/resource-type"

internal/controllers/dra_reconciler.go

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
"github.com/kubernetes-sigs/kernel-module-management/internal/utils"
2828
appsv1 "k8s.io/api/apps/v1"
2929
v1 "k8s.io/api/core/v1"
30+
resourcev1 "k8s.io/api/resource/v1"
31+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3032
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3133
"k8s.io/apimachinery/pkg/runtime"
3234
ctrl "sigs.k8s.io/controller-runtime"
@@ -82,14 +84,18 @@ func (r *DRAReconciler) Reconcile(ctx context.Context, mod *kmmv1beta1.Module) (
8284
return res, fmt.Errorf("could not get DRA DaemonSets for module %s, namespace %s: %v", mod.Name, mod.Namespace, err)
8385
}
8486

87+
existingDCs, err := r.reconHelperAPI.getModuleDeviceClasses(ctx, mod.Name, mod.Namespace)
88+
if err != nil {
89+
return res, fmt.Errorf("could not get DeviceClasses for module %s, namespace %s: %v", mod.Name, mod.Namespace, err)
90+
}
91+
8592
if mod.GetDeletionTimestamp() != nil {
86-
err = r.reconHelperAPI.deleteDRADaemonSets(ctx, existingDRADS)
87-
return ctrl.Result{}, err
93+
return ctrl.Result{}, r.reconHelperAPI.deleteDRAResources(ctx, existingDRADS, existingDCs)
8894
}
8995

9096
if mod.Spec.DRA == nil {
91-
if len(existingDRADS) > 0 {
92-
if err = r.reconHelperAPI.deleteDRADaemonSets(ctx, existingDRADS); err != nil {
97+
if len(existingDRADS) > 0 || len(existingDCs) > 0 {
98+
if err = r.reconHelperAPI.deleteDRAResources(ctx, existingDRADS, existingDCs); err != nil {
9399
return ctrl.Result{}, err
94100
}
95101
}
@@ -101,6 +107,11 @@ func (r *DRAReconciler) Reconcile(ctx context.Context, mod *kmmv1beta1.Module) (
101107
return res, fmt.Errorf("could not handle DRA: %v", err)
102108
}
103109

110+
err = r.reconHelperAPI.handleDeviceClasses(ctx, mod, existingDCs)
111+
if err != nil {
112+
return res, fmt.Errorf("could not handle DeviceClasses: %v", err)
113+
}
114+
104115
err = r.reconHelperAPI.moduleUpdateDRAStatus(ctx, mod, existingDRADS)
105116
if err != nil {
106117
return res, fmt.Errorf("failed to update DRA status of the module: %v", err)
@@ -116,9 +127,11 @@ func (r *DRAReconciler) Reconcile(ctx context.Context, mod *kmmv1beta1.Module) (
116127
type draReconcilerHelperAPI interface {
117128
getModuleDRADaemonSets(ctx context.Context, name, namespace string) ([]appsv1.DaemonSet, error)
118129
handleDRA(ctx context.Context, mod *kmmv1beta1.Module, existingDRADS []appsv1.DaemonSet) error
119-
deleteDRADaemonSets(ctx context.Context, existingDRADS []appsv1.DaemonSet) error
130+
deleteDRAResources(ctx context.Context, daemonSets []appsv1.DaemonSet, deviceClasses []resourcev1.DeviceClass) error
120131
moduleUpdateDRAStatus(ctx context.Context, mod *kmmv1beta1.Module, existingDRADS []appsv1.DaemonSet) error
121132
clearDRAStatus(ctx context.Context, mod *kmmv1beta1.Module) error
133+
getModuleDeviceClasses(ctx context.Context, name, namespace string) ([]resourcev1.DeviceClass, error)
134+
handleDeviceClasses(ctx context.Context, mod *kmmv1beta1.Module, existingDCs []resourcev1.DeviceClass) error
122135
}
123136

124137
type draReconcilerHelper struct {
@@ -183,14 +196,21 @@ func (drh *draReconcilerHelper) handleDRA(ctx context.Context, mod *kmmv1beta1.M
183196
return err
184197
}
185198

186-
func (drh *draReconcilerHelper) deleteDRADaemonSets(ctx context.Context, existingDRADS []appsv1.DaemonSet) error {
187-
for _, ds := range existingDRADS {
188-
err := drh.client.Delete(ctx, &ds)
189-
if err != nil {
190-
return fmt.Errorf("failed to delete DRA DaemonSet %s/%s: %v", ds.Namespace, ds.Name, err)
199+
// deleteDRAResources deletes all DRA-owned DaemonSets and DeviceClasses.
200+
// It attempts every deletion and returns a joined error so the controller requeues on partial failure.
201+
func (drh *draReconcilerHelper) deleteDRAResources(ctx context.Context, daemonSets []appsv1.DaemonSet, deviceClasses []resourcev1.DeviceClass) error {
202+
var errs []error
203+
for _, ds := range daemonSets {
204+
if err := drh.client.Delete(ctx, &ds); err != nil && !apierrors.IsNotFound(err) {
205+
errs = append(errs, fmt.Errorf("failed to delete DRA DaemonSet %s/%s: %v", ds.Namespace, ds.Name, err))
191206
}
192207
}
193-
return nil
208+
for _, dc := range deviceClasses {
209+
if err := drh.client.Delete(ctx, &dc); err != nil && !apierrors.IsNotFound(err) {
210+
errs = append(errs, fmt.Errorf("failed to delete DeviceClass %s: %v", dc.Name, err))
211+
}
212+
}
213+
return errors.Join(errs...)
194214
}
195215

196216
func (drh *draReconcilerHelper) moduleUpdateDRAStatus(ctx context.Context,
@@ -233,6 +253,110 @@ func (drh *draReconcilerHelper) clearDRAStatus(ctx context.Context, mod *kmmv1be
233253
return drh.client.Status().Patch(ctx, mod, client.MergeFrom(unmodifiedMod))
234254
}
235255

256+
func (drh *draReconcilerHelper) getModuleDeviceClasses(ctx context.Context, name, namespace string) ([]resourcev1.DeviceClass, error) {
257+
dcList := resourcev1.DeviceClassList{}
258+
opts := []client.ListOption{
259+
client.MatchingLabels(map[string]string{
260+
constants.ModuleNameLabel: name,
261+
constants.ModuleNamespaceLabel: namespace,
262+
}),
263+
}
264+
if err := drh.client.List(ctx, &dcList, opts...); err != nil {
265+
return nil, fmt.Errorf("could not list DeviceClasses: %v", err)
266+
}
267+
268+
return dcList.Items, nil
269+
}
270+
271+
// handleDeviceClasses reconciles cluster-scoped DeviceClass resources to match the desired state
272+
// declared in mod.Spec.DRA.DeviceClasses. It performs declarative convergence:
273+
// - DeviceClasses present in the spec but missing from the cluster are created.
274+
// - DeviceClasses present in both are patched to reflect the current spec (drift correction).
275+
// - DeviceClasses present in the cluster but absent from the spec are deleted (stale cleanup).
276+
func (drh *draReconcilerHelper) handleDeviceClasses(ctx context.Context, mod *kmmv1beta1.Module, existingDCs []resourcev1.DeviceClass) error {
277+
if mod.Spec.DRA == nil {
278+
return nil
279+
}
280+
281+
logger := log.FromContext(ctx)
282+
283+
existingByName := make(map[string]resourcev1.DeviceClass, len(existingDCs))
284+
for _, dc := range existingDCs {
285+
existingByName[dc.Name] = dc
286+
}
287+
288+
desiredByName := make(map[string]kmmv1beta1.DeviceClassSpec, len(mod.Spec.DRA.DeviceClasses))
289+
for _, dc := range mod.Spec.DRA.DeviceClasses {
290+
desiredByName[dc.Name] = dc
291+
}
292+
293+
var errs []error
294+
295+
// Create missing or patch existing DeviceClasses to match the desired spec.
296+
for _, desired := range mod.Spec.DRA.DeviceClasses {
297+
existingDC, exists := existingByName[desired.Name]
298+
if err := drh.createOrPatchDeviceClass(ctx, mod, desired, existingDC, exists); err != nil {
299+
errs = append(errs, err)
300+
}
301+
}
302+
303+
// Delete DeviceClasses that exist in the cluster but are no longer in the desired spec.
304+
for name, dc := range existingByName {
305+
if _, desired := desiredByName[name]; !desired {
306+
if deleteErr := drh.client.Delete(ctx, &dc); deleteErr != nil && !apierrors.IsNotFound(deleteErr) {
307+
errs = append(errs, fmt.Errorf("failed to delete DeviceClass %s: %v", name, deleteErr))
308+
} else {
309+
logger.Info("Deleted extra DeviceClass", "name", name)
310+
}
311+
}
312+
}
313+
314+
return errors.Join(errs...)
315+
}
316+
317+
// createOrPatchDeviceClass ensures a single DeviceClass matches the desired spec.
318+
// If the DeviceClass does not exist, it creates one with ownership labels.
319+
// If it already exists, it patches selectors and config to correct any drift.
320+
func (drh *draReconcilerHelper) createOrPatchDeviceClass(
321+
ctx context.Context,
322+
mod *kmmv1beta1.Module,
323+
desired kmmv1beta1.DeviceClassSpec,
324+
existingDC resourcev1.DeviceClass,
325+
exists bool,
326+
) error {
327+
logger := log.FromContext(ctx)
328+
329+
if !exists {
330+
dc := resourcev1.DeviceClass{
331+
ObjectMeta: metav1.ObjectMeta{
332+
Name: desired.Name,
333+
Labels: map[string]string{
334+
constants.ModuleNameLabel: mod.Name,
335+
constants.ModuleNamespaceLabel: mod.Namespace,
336+
},
337+
},
338+
Spec: resourcev1.DeviceClassSpec{
339+
Selectors: desired.Selectors,
340+
Config: desired.Config,
341+
},
342+
}
343+
if err := drh.client.Create(ctx, &dc); err != nil {
344+
return fmt.Errorf("failed to create DeviceClass %s: %v", desired.Name, err)
345+
}
346+
logger.Info("Created DeviceClass", "name", desired.Name)
347+
return nil
348+
}
349+
350+
patch := client.MergeFrom(existingDC.DeepCopy())
351+
existingDC.Spec.Selectors = desired.Selectors
352+
existingDC.Spec.Config = desired.Config
353+
if err := drh.client.Patch(ctx, &existingDC, patch); err != nil {
354+
return fmt.Errorf("failed to patch DeviceClass %s: %v", desired.Name, err)
355+
}
356+
logger.Info("Patched DeviceClass", "name", desired.Name)
357+
return nil
358+
}
359+
236360
type draDaemonSetCreator interface {
237361
setDRAAsDesired(ctx context.Context, ds *appsv1.DaemonSet, mod *kmmv1beta1.Module) error
238362
}

0 commit comments

Comments
 (0)