Skip to content

Commit 32fe908

Browse files
chris-rockclaude
andcommitted
Migrate K8s scanner to plain cnspec image and move GC to operator
The local cluster K8s scanner CronJob previously used the mondoo-operator image and ran `/mondoo-operator k8s-scan`, which was a wrapper that shelled out to `cnspec scan k8s` and then performed garbage collection. This created an unnecessary coupling — the only value-add over plain cnspec was GC. Switch the CronJob to use the cnspec image directly (matching what external clusters, node scanning, and container scanning already do), and move GC into the operator's reconciliation loop: - CronJob now runs `cnspec scan k8s` with `--score-threshold 0` - Operator performs GC after detecting a successful scan job completion - GC reads credentials from the mondoo secret and calls the Mondoo API - GC failure is non-fatal and always advances the timestamp to avoid retrying on every reconcile (will retry after next successful scan) - Remove the `k8s-scan` subcommand (keep `garbage-collect` for debugging) - Add `LastK8sResourceGarbageCollectionTime` to Status for tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 99ec99d commit 32fe908

9 files changed

Lines changed: 296 additions & 145 deletions

File tree

api/v1alpha2/mondooauditconfig_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,11 @@ type MondooAuditConfigStatus struct {
420420

421421
// ReconciledByOperatorVersion contains the version of the operator which reconciled this MondooAuditConfig
422422
ReconciledByOperatorVersion string `json:"reconciledByOperatorVersion,omitempty"`
423+
424+
// LastK8sResourceGarbageCollectionTime tracks the last time the operator performed
425+
// garbage collection of stale K8s resource scan assets.
426+
// +optional
427+
LastK8sResourceGarbageCollectionTime *metav1.Time `json:"lastK8sResourceGarbageCollectionTime,omitempty"`
423428
}
424429

425430
type MondooAuditConfigCondition struct {

api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/mondoo-operator/k8s_scan/cmd.go

Lines changed: 0 additions & 131 deletions
This file was deleted.

cmd/mondoo-operator/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/spf13/cobra"
88
"go.mondoo.com/mondoo-operator/cmd/mondoo-operator/cleanup"
99
"go.mondoo.com/mondoo-operator/cmd/mondoo-operator/garbage_collect"
10-
"go.mondoo.com/mondoo-operator/cmd/mondoo-operator/k8s_scan"
1110
"go.mondoo.com/mondoo-operator/cmd/mondoo-operator/operator"
1211
resourcewatcher "go.mondoo.com/mondoo-operator/cmd/mondoo-operator/resource_watcher"
1312
"go.mondoo.com/mondoo-operator/cmd/mondoo-operator/version"
@@ -20,7 +19,7 @@ var rootCmd = &cobra.Command{
2019
}
2120

2221
func main() {
23-
rootCmd.AddCommand(operator.Cmd, version.Cmd, garbage_collect.Cmd, k8s_scan.Cmd, resourcewatcher.Cmd, cleanup.Cmd)
22+
rootCmd.AddCommand(operator.Cmd, version.Cmd, garbage_collect.Cmd, resourcewatcher.Cmd, cleanup.Cmd)
2423

2524
if err := rootCmd.Execute(); err != nil {
2625
panic(err)

config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,12 @@ spec:
12781278
- type
12791279
type: object
12801280
type: array
1281+
lastK8sResourceGarbageCollectionTime:
1282+
description: |-
1283+
LastK8sResourceGarbageCollectionTime tracks the last time the operator performed
1284+
garbage collection of stale K8s resource scan assets.
1285+
format: date-time
1286+
type: string
12811287
pods:
12821288
description: Pods store the name of the pods which are running mondoo
12831289
instances

controllers/k8s_scan/deployment_handler.go

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package k8s_scan
66
import (
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

2427
var 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

108112
func (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

138152
func (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
487608
func (n *DeploymentHandler) syncWIFServiceAccount(ctx context.Context, cluster v1alpha2.ExternalCluster) error {
488609
desired := WIFServiceAccount(cluster, n.Mondoo)

0 commit comments

Comments
 (0)