@@ -6,6 +6,7 @@ package k8s_scan
66import (
77 "context"
88 "fmt"
9+ "time"
910
1011 appsv1 "k8s.io/api/apps/v1"
1112 batchv1 "k8s.io/api/batch/v1"
@@ -14,11 +15,13 @@ import (
1415 "k8s.io/apimachinery/pkg/labels"
1516 ctrl "sigs.k8s.io/controller-runtime"
1617 "sigs.k8s.io/controller-runtime/pkg/client"
18+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1719
1820 "go.mondoo.com/mondoo-operator/api/v1alpha2"
21+ "go.mondoo.com/mondoo-operator/pkg/client/mondooclient"
22+ "go.mondoo.com/mondoo-operator/pkg/constants"
1923 "go.mondoo.com/mondoo-operator/pkg/utils/k8s"
2024 "go.mondoo.com/mondoo-operator/pkg/utils/mondoo"
21- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2225)
2326
2427var logger = ctrl .Log .WithName ("k8s-resources-scanning" )
@@ -103,6 +106,7 @@ type DeploymentHandler struct {
103106 Mondoo * v1alpha2.MondooAuditConfig
104107 ContainerImageResolver mondoo.ContainerImageResolver
105108 MondooOperatorConfig * v1alpha2.MondooOperatorConfig
109+ MondooClientBuilder func (mondooclient.MondooClientOptions ) (mondooclient.MondooClient , error )
106110}
107111
108112func (n * DeploymentHandler ) Reconcile (ctx context.Context ) (ctrl.Result , error ) {
@@ -132,14 +136,24 @@ func (n *DeploymentHandler) Reconcile(ctx context.Context) (ctrl.Result, error)
132136 }
133137 }
134138
139+ // Perform garbage collection of stale K8s resource scan assets if a new successful scan has completed
140+ if n .Mondoo .Spec .KubernetesResources .Enable || hasExternalClusters {
141+ clusterUid , err := k8s .GetClusterUID (ctx , n .KubeClient , logger )
142+ if err != nil {
143+ logger .Error (err , "Failed to get cluster's UID for garbage collection" )
144+ } else {
145+ n .garbageCollectIfNeeded (ctx , clusterUid )
146+ }
147+ }
148+
135149 return ctrl.Result {}, nil
136150}
137151
138152func (n * DeploymentHandler ) syncCronJob (ctx context.Context ) error {
139- mondooOperatorImage , err := n .ContainerImageResolver .MondooOperatorImage (
140- ctx , n .Mondoo .Spec .Scanner .Image .Name , n .Mondoo .Spec .Scanner .Image .Tag , n .Mondoo .Spec .Scanner .Image .Digest , n .MondooOperatorConfig .Spec .SkipContainerResolution )
153+ cnspecImage , err := n .ContainerImageResolver .CnspecImage (
154+ n .Mondoo .Spec .Scanner .Image .Name , n .Mondoo .Spec .Scanner .Image .Tag , n .Mondoo .Spec .Scanner .Image .Digest , n .MondooOperatorConfig .Spec .SkipContainerResolution )
141155 if err != nil {
142- logger .Error (err , "Failed to resolve mondoo-operator container image" )
156+ logger .Error (err , "Failed to resolve cnspec container image" )
143157 return err
144158 }
145159
@@ -160,7 +174,7 @@ func (n *DeploymentHandler) syncCronJob(ctx context.Context) error {
160174 return err
161175 }
162176
163- desired := CronJob (mondooOperatorImage , integrationMrn , clusterUid , n .Mondoo , * n .MondooOperatorConfig )
177+ desired := CronJob (cnspecImage , n .Mondoo , * n .MondooOperatorConfig )
164178 obj := & batchv1.CronJob {ObjectMeta : metav1.ObjectMeta {Name : desired .Name , Namespace : desired .Namespace }}
165179 op , err := k8s .CreateOrUpdate (ctx , n .KubeClient , obj , n .Mondoo , logger , func () error {
166180 k8s .UpdateCronJobFields (obj , desired )
@@ -483,6 +497,113 @@ func (n *DeploymentHandler) syncExternalClusterSAKubeconfigConfigMap(ctx context
483497 return nil
484498}
485499
500+ // garbageCollectIfNeeded checks whether a new successful K8s scan has completed since the last GC run,
501+ // and if so, performs garbage collection of stale assets via the Mondoo API.
502+ func (n * DeploymentHandler ) garbageCollectIfNeeded (ctx context.Context , clusterUid string ) {
503+ // List all k8s-scan CronJobs (local + external) for this audit config
504+ cronJobs := & batchv1.CronJobList {}
505+ listOpts := & client.ListOptions {
506+ Namespace : n .Mondoo .Namespace ,
507+ LabelSelector : labels .SelectorFromSet (map [string ]string {
508+ "app" : "mondoo-k8s-scan" ,
509+ "mondoo_cr" : n .Mondoo .Name ,
510+ }),
511+ }
512+ if err := n .KubeClient .List (ctx , cronJobs , listOpts ); err != nil {
513+ logger .Error (err , "Failed to list CronJobs for garbage collection" )
514+ return
515+ }
516+
517+ // Find the latest lastSuccessfulTime across all CronJobs
518+ var latestSuccess * metav1.Time
519+ for i := range cronJobs .Items {
520+ t := cronJobs .Items [i ].Status .LastSuccessfulTime
521+ if t != nil && (latestSuccess == nil || t .After (latestSuccess .Time )) {
522+ latestSuccess = t
523+ }
524+ }
525+
526+ if latestSuccess == nil {
527+ // No successful scans yet
528+ return
529+ }
530+
531+ // Skip if we already ran GC for this (or a newer) successful scan
532+ if n .Mondoo .Status .LastK8sResourceGarbageCollectionTime != nil &&
533+ ! latestSuccess .After (n .Mondoo .Status .LastK8sResourceGarbageCollectionTime .Time ) {
534+ return
535+ }
536+
537+ managedBy := "mondoo-operator-" + clusterUid
538+ if err := n .performGarbageCollection (ctx , managedBy ); err != nil {
539+ logger .Error (err , "Failed to perform garbage collection of K8s resource scan assets" )
540+ }
541+
542+ // Always update the timestamp so we don't retry until the next new successful scan.
543+ // GC failure is non-critical — stale assets will be cleaned up on the next attempt.
544+ now := metav1 .Now ()
545+ n .Mondoo .Status .LastK8sResourceGarbageCollectionTime = & now
546+ }
547+
548+ // performGarbageCollection calls the Mondoo API to garbage collect stale K8s resource scan assets.
549+ func (n * DeploymentHandler ) performGarbageCollection (ctx context.Context , managedBy string ) error {
550+ if n .MondooClientBuilder == nil {
551+ logger .Info ("MondooClientBuilder not configured, skipping garbage collection" )
552+ return nil
553+ }
554+
555+ // Read service account credentials from the creds secret
556+ credsSecret := & corev1.Secret {}
557+ credsSecretKey := client.ObjectKey {
558+ Namespace : n .Mondoo .Namespace ,
559+ Name : n .Mondoo .Spec .MondooCredsSecretRef .Name ,
560+ }
561+ if err := n .KubeClient .Get (ctx , credsSecretKey , credsSecret ); err != nil {
562+ return fmt .Errorf ("failed to get credentials secret: %w" , err )
563+ }
564+
565+ saData , ok := credsSecret .Data [constants .MondooCredsSecretServiceAccountKey ]
566+ if ! ok {
567+ return fmt .Errorf ("credentials secret missing key %q" , constants .MondooCredsSecretServiceAccountKey )
568+ }
569+
570+ sa , err := mondoo .LoadServiceAccountFromFile (saData )
571+ if err != nil {
572+ return fmt .Errorf ("failed to load service account: %w" , err )
573+ }
574+
575+ token , err := mondoo .GenerateTokenFromServiceAccount (* sa , logger )
576+ if err != nil {
577+ return fmt .Errorf ("failed to generate token: %w" , err )
578+ }
579+
580+ opts := mondooclient.MondooClientOptions {
581+ ApiEndpoint : sa .ApiEndpoint ,
582+ Token : token ,
583+ }
584+ if n .MondooOperatorConfig != nil {
585+ opts .HttpProxy = n .MondooOperatorConfig .Spec .HttpProxy
586+ }
587+
588+ mondooClient , err := n .MondooClientBuilder (opts )
589+ if err != nil {
590+ return fmt .Errorf ("failed to create mondoo client: %w" , err )
591+ }
592+
593+ gcOpts := & mondooclient.GarbageCollectOptions {
594+ ManagedBy : managedBy ,
595+ PlatformRuntime : "k8s-cluster" ,
596+ OlderThan : time .Now ().Add (- 2 * time .Hour ).Format (time .RFC3339 ),
597+ }
598+
599+ if err := mondooClient .GarbageCollectAssets (ctx , gcOpts ); err != nil {
600+ return fmt .Errorf ("garbage collection API call failed: %w" , err )
601+ }
602+
603+ logger .Info ("Successfully performed garbage collection of K8s resource scan assets" )
604+ return nil
605+ }
606+
486607// syncWIFServiceAccount syncs a ServiceAccount with cloud-specific annotations for Workload Identity Federation
487608func (n * DeploymentHandler ) syncWIFServiceAccount (ctx context.Context , cluster v1alpha2.ExternalCluster ) error {
488609 desired := WIFServiceAccount (cluster , n .Mondoo )
0 commit comments