@@ -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) (
116127type 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
124137type 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
196216func (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+
236360type draDaemonSetCreator interface {
237361 setDRAAsDesired (ctx context.Context , ds * appsv1.DaemonSet , mod * kmmv1beta1.Module ) error
238362}
0 commit comments