Skip to content

Commit 4c83f88

Browse files
slntoppclaude
andauthored
✨ add garbage collection for stale node scan assets (#1435)
* 🐛 fix(bump) cnspec version * 🧹bump mql version v13.1.1 * ✨ add garbage collection for stale node scan assets fix: update MondooClientBuilder reference and clarify garbage collection comment refactor: streamline garbage collection logic for nodes and K8s resources refactor: simplify garbage collection logic by removing cluster UID dependency fix: platform runtime for nodes * ✨ refactor: use ManagedByLabel for consistency in asset management * fix: replace gopkg.in/yaml.v2 with sigs.k8s.io/yaml to fix ManagedBy propagation The ManagedBy field on the protobuf Asset struct has only a JSON tag (json:"managed_by,omitempty") but no yaml tag. gopkg.in/yaml.v2 serializes it as "managedby" (lowercase field name fallback), while cnspec reads inventories with sigs.k8s.io/yaml which expects "managed_by" (from JSON tags). This caused ManagedBy to be silently lost during serialization for k8s resource and container image scans. Node scans already used sigs.k8s.io/yaml, which is why they were unaffected. This also removes the gopkg.in/yaml.v2 dependency entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bffc636 commit 4c83f88

27 files changed

Lines changed: 693 additions & 225 deletions

api/v1alpha2/mondooauditconfig_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,11 @@ type MondooAuditConfigStatus struct {
483483
// garbage collection of stale K8s resource scan assets.
484484
// +optional
485485
LastK8sResourceGarbageCollectionTime *metav1.Time `json:"lastK8sResourceGarbageCollectionTime,omitempty"`
486+
487+
// LastNodeScanGarbageCollectionTime tracks the last time the operator performed
488+
// garbage collection of stale node scan assets.
489+
// +optional
490+
LastNodeScanGarbageCollectionTime *metav1.Time `json:"lastNodeScanGarbageCollectionTime,omitempty"`
486491
}
487492

488493
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.

charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,12 @@ spec:
13661366
garbage collection of stale K8s resource scan assets.
13671367
format: date-time
13681368
type: string
1369+
lastNodeScanGarbageCollectionTime:
1370+
description: |-
1371+
LastNodeScanGarbageCollectionTime tracks the last time the operator performed
1372+
garbage collection of stale node scan assets.
1373+
format: date-time
1374+
type: string
13691375
pods:
13701376
description: Pods store the name of the pods which are running mondoo
13711377
instances

charts/mondoo-operator/files/crds/k8s.mondoo.com_mondooauditconfigs.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,12 @@ spec:
13661366
garbage collection of stale K8s resource scan assets.
13671367
format: date-time
13681368
type: string
1369+
lastNodeScanGarbageCollectionTime:
1370+
description: |-
1371+
LastNodeScanGarbageCollectionTime tracks the last time the operator performed
1372+
garbage collection of stale node scan assets.
1373+
format: date-time
1374+
type: string
13691375
pods:
13701376
description: Pods store the name of the pods which are running mondoo
13711377
instances

cmd/mondoo-operator/garbage_collect/cmd.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11-
"strings"
1211
"time"
1312

1413
"github.com/go-logr/logr"
@@ -31,7 +30,6 @@ func init() {
3130
filterPlatformRuntime := Cmd.Flags().String("filter-platform-runtime", "", "Cleanup assets by an asset's PlatformRuntime (k8s-cluster or docker-image).")
3231
filterManagedBy := Cmd.Flags().String("filter-managed-by", "", "Cleanup assets with matching ManagedBy field.")
3332
filterOlderThan := Cmd.Flags().String("filter-older-than", "", "Cleanup assets which have not been updated in over the time provided (eg 12m or 48h or anything time.ParseDuration() accepts).")
34-
labelsInput := Cmd.Flags().StringSlice("labels", []string{}, "Cleanup assets with matching labels (eg --labels key1=value1,key2=value2).")
3533
Cmd.RunE = func(cmd *cobra.Command, args []string) error {
3634
log.SetLogger(logger.NewLogger())
3735
logger := log.Log.WithName("garbage-collect")
@@ -43,15 +41,6 @@ func init() {
4341
return fmt.Errorf("--timeout must be greater than 0")
4442
}
4543

46-
labels := make(map[string]string)
47-
for _, l := range *labelsInput {
48-
split := strings.Split(l, "=")
49-
if len(split) != 2 {
50-
return fmt.Errorf("invalid label provided %s. Labels should be in the form of key=value", l)
51-
}
52-
labels[split[0]] = split[1]
53-
}
54-
5544
// Read the service account credentials from the config file
5645
configData, err := os.ReadFile(*configPath)
5746
if err != nil {
@@ -88,14 +77,18 @@ func init() {
8877
return fmt.Errorf("no filters provided to garbage collect by")
8978
}
9079

91-
return GarbageCollectCmd(ctx, client, *filterPlatformRuntime, *filterOlderThan, *filterManagedBy, labels, logger)
80+
spaceMrn := serviceAccount.SpaceMrn
81+
if spaceMrn == "" {
82+
spaceMrn = mondoo.SpaceMrnFromServiceAccountMrn(serviceAccount.Mrn)
83+
}
84+
return GarbageCollectCmd(ctx, client, spaceMrn, *filterPlatformRuntime, *filterOlderThan, *filterManagedBy, logger)
9285
}
9386
}
9487

95-
func GarbageCollectCmd(ctx context.Context, client mondooclient.MondooClient, platformRuntime, olderThan, managedBy string, labels map[string]string, logger logr.Logger) error {
96-
gcOpts := &mondooclient.GarbageCollectOptions{
88+
func GarbageCollectCmd(ctx context.Context, client mondooclient.MondooClient, spaceMrn, platformRuntime, olderThan, managedBy string, logger logr.Logger) error {
89+
req := &mondooclient.DeleteAssetsRequest{
90+
SpaceMrn: spaceMrn,
9791
ManagedBy: managedBy,
98-
Labels: labels,
9992
}
10093

10194
if olderThan != "" {
@@ -105,19 +98,23 @@ func GarbageCollectCmd(ctx context.Context, client mondooclient.MondooClient, pl
10598
return err
10699
}
107100

108-
gcOpts.OlderThan = timestamp
101+
req.DateFilter = &mondooclient.DateFilter{
102+
Timestamp: timestamp,
103+
Comparison: mondooclient.Comparison_LESS_THAN,
104+
Field: mondooclient.DateFilterField_FILTER_LAST_UPDATED,
105+
}
109106
}
110107

111108
if platformRuntime != "" {
112109
switch platformRuntime {
113110
case "k8s-cluster", "docker-image":
114-
gcOpts.PlatformRuntime = platformRuntime
111+
req.PlatformRuntime = platformRuntime
115112
default:
116113
return fmt.Errorf("no matching platform runtime found for (%s)", platformRuntime)
117114
}
118115
}
119116

120-
err := client.GarbageCollectAssets(ctx, gcOpts)
117+
resp, err := client.DeleteAssets(ctx, req)
121118
if err != nil {
122119
if errors.Is(err, context.DeadlineExceeded) {
123120
logger.Error(err, "failed to receive a response before timeout was exceeded")
@@ -127,6 +124,13 @@ func GarbageCollectCmd(ctx context.Context, client mondooclient.MondooClient, pl
127124
return err
128125
}
129126

127+
if len(resp.AssetMrns) > 0 {
128+
logger.Info("Deleted assets", "count", len(resp.AssetMrns))
129+
}
130+
if len(resp.Errors) > 0 {
131+
logger.Info("DeleteAssets completed with errors", "errors", resp.Errors)
132+
}
133+
130134
return nil
131135
}
132136

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,12 @@ spec:
13661366
garbage collection of stale K8s resource scan assets.
13671367
format: date-time
13681368
type: string
1369+
lastNodeScanGarbageCollectionTime:
1370+
description: |-
1371+
LastNodeScanGarbageCollectionTime tracks the last time the operator performed
1372+
garbage collection of stale node scan assets.
1373+
format: date-time
1374+
type: string
13691375
pods:
13701376
description: Pods store the name of the pods which are running mondoo
13711377
instances

controllers/container_image/resources.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import (
1313
"go.mondoo.com/mondoo-operator/pkg/constants"
1414
"go.mondoo.com/mondoo-operator/pkg/feature_flags"
1515
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
16+
mondoo "go.mondoo.com/mondoo-operator/pkg/utils/mondoo"
1617
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
17-
"gopkg.in/yaml.v2"
1818
batchv1 "k8s.io/api/batch/v1"
1919
corev1 "k8s.io/api/core/v1"
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121
"k8s.io/utils/ptr"
22+
"sigs.k8s.io/yaml"
2223
)
2324

2425
const (
@@ -233,7 +234,7 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig,
233234
Labels: map[string]string{
234235
"k8s.mondoo.com/kind": "node",
235236
},
236-
ManagedBy: "mondoo-operator-" + clusterUID,
237+
ManagedBy: mondoo.ManagedByLabel(clusterUID),
237238
},
238239
},
239240
},

controllers/container_image/resources_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99

1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
12-
"gopkg.in/yaml.v2"
1312
corev1 "k8s.io/api/core/v1"
1413
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1514
"k8s.io/utils/ptr"
15+
"sigs.k8s.io/yaml"
1616

1717
"go.mondoo.com/mondoo-operator/api/v1alpha2"
1818
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"

controllers/k8s_scan/deployment_handler.go

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020

2121
"go.mondoo.com/mondoo-operator/api/v1alpha2"
2222
"go.mondoo.com/mondoo-operator/pkg/client/mondooclient"
23-
"go.mondoo.com/mondoo-operator/pkg/constants"
2423
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
2524
"go.mondoo.com/mondoo-operator/pkg/utils/mondoo"
2625
)
@@ -599,8 +598,7 @@ func (n *DeploymentHandler) garbageCollectIfNeeded(ctx context.Context, clusterU
599598
return
600599
}
601600

602-
managedBy := "mondoo-operator-" + clusterUid
603-
if err := n.performGarbageCollection(ctx, managedBy); err != nil {
601+
if err := n.performGarbageCollection(ctx, mondoo.ManagedByLabel(clusterUid)); err != nil {
604602
logger.Error(err, "Failed to perform garbage collection of K8s resource scan assets")
605603
}
606604

@@ -610,61 +608,20 @@ func (n *DeploymentHandler) garbageCollectIfNeeded(ctx context.Context, clusterU
610608
n.Mondoo.Status.LastK8sResourceGarbageCollectionTime = &now
611609
}
612610

613-
// performGarbageCollection calls the Mondoo API to garbage collect stale K8s resource scan assets.
611+
// performGarbageCollection calls the Mondoo API to delete stale K8s resource scan assets.
614612
func (n *DeploymentHandler) performGarbageCollection(ctx context.Context, managedBy string) error {
615-
if n.MondooClientBuilder == nil {
616-
logger.Info("MondooClientBuilder not configured, skipping garbage collection")
617-
return nil
618-
}
619-
620-
// Read service account credentials from the creds secret
621-
credsSecret := &corev1.Secret{}
622-
credsSecretKey := client.ObjectKey{
623-
Namespace: n.Mondoo.Namespace,
624-
Name: n.Mondoo.Spec.MondooCredsSecretRef.Name,
625-
}
626-
if err := n.KubeClient.Get(ctx, credsSecretKey, credsSecret); err != nil {
627-
return fmt.Errorf("failed to get credentials secret: %w", err)
628-
}
629-
630-
saData, ok := credsSecret.Data[constants.MondooCredsSecretServiceAccountKey]
631-
if !ok {
632-
return fmt.Errorf("credentials secret missing key %q", constants.MondooCredsSecretServiceAccountKey)
633-
}
634-
635-
sa, err := mondoo.LoadServiceAccountFromFile(saData)
636-
if err != nil {
637-
return fmt.Errorf("failed to load service account: %w", err)
638-
}
639-
640-
token, err := mondoo.GenerateTokenFromServiceAccount(*sa, logger)
641-
if err != nil {
642-
return fmt.Errorf("failed to generate token: %w", err)
643-
}
644-
645-
opts := mondooclient.MondooClientOptions{
646-
ApiEndpoint: sa.ApiEndpoint,
647-
Token: token,
648-
}
649-
if n.MondooOperatorConfig != nil {
650-
opts.HttpProxy = n.MondooOperatorConfig.Spec.HttpProxy
651-
opts.HttpsProxy = n.MondooOperatorConfig.Spec.HttpsProxy
652-
opts.NoProxy = n.MondooOperatorConfig.Spec.NoProxy
653-
}
654-
655-
mondooClient, err := n.MondooClientBuilder(opts)
656-
if err != nil {
657-
return fmt.Errorf("failed to create mondoo client: %w", err)
658-
}
659-
660-
gcOpts := &mondooclient.GarbageCollectOptions{
613+
req := &mondooclient.DeleteAssetsRequest{
661614
ManagedBy: managedBy,
662615
PlatformRuntime: "k8s-cluster",
663-
OlderThan: time.Now().Add(-2 * time.Hour).Format(time.RFC3339),
616+
DateFilter: &mondooclient.DateFilter{
617+
Timestamp: time.Now().Add(-mondoo.GCOlderThan()).Format(time.RFC3339),
618+
Comparison: mondooclient.Comparison_LESS_THAN,
619+
Field: mondooclient.DateFilterField_FILTER_LAST_UPDATED,
620+
},
664621
}
665622

666-
if err := mondooClient.GarbageCollectAssets(ctx, gcOpts); err != nil {
667-
return fmt.Errorf("garbage collection API call failed: %w", err)
623+
if err := mondoo.DeleteStaleAssets(ctx, n.KubeClient, n.Mondoo, n.MondooOperatorConfig, n.MondooClientBuilder, req, logger); err != nil {
624+
return err
668625
}
669626

670627
logger.Info("Successfully performed garbage collection of K8s resource scan assets")

controllers/k8s_scan/deployment_handler_test.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,11 +1528,14 @@ func TestExternalClusterNaming(t *testing.T) {
15281528

15291529
func (s *DeploymentHandlerSuite) TestGarbageCollection_RunsAfterSuccessfulScan() {
15301530
gcCalled := false
1531-
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, opts *mondooclient.GarbageCollectOptions) error {
1531+
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, req *mondooclient.DeleteAssetsRequest) error {
15321532
gcCalled = true
1533-
s.Equal("k8s-cluster", opts.PlatformRuntime)
1534-
s.Contains(opts.ManagedBy, "mondoo-operator-")
1535-
s.NotEmpty(opts.OlderThan)
1533+
s.Equal("k8s-cluster", req.PlatformRuntime)
1534+
s.Contains(req.ManagedBy, "mondoo-operator-")
1535+
s.NotNil(req.DateFilter)
1536+
s.NotEmpty(req.DateFilter.Timestamp)
1537+
s.Equal(mondooclient.Comparison_LESS_THAN, req.DateFilter.Comparison)
1538+
s.Equal(mondooclient.DateFilterField_FILTER_LAST_UPDATED, req.DateFilter.Field)
15361539
return nil
15371540
})
15381541
s.NoError(d.KubeClient.Create(s.ctx, &s.auditConfig))
@@ -1556,13 +1559,13 @@ func (s *DeploymentHandlerSuite) TestGarbageCollection_RunsAfterSuccessfulScan()
15561559
s.NoError(err)
15571560
s.True(result.IsZero())
15581561

1559-
s.True(gcCalled, "GarbageCollectAssets should have been called")
1562+
s.True(gcCalled, "DeleteAssets should have been called")
15601563
s.NotNil(d.Mondoo.Status.LastK8sResourceGarbageCollectionTime, "GC timestamp should be set in status")
15611564
}
15621565

15631566
func (s *DeploymentHandlerSuite) TestGarbageCollection_SkipsWhenAlreadyRun() {
15641567
gcCalled := false
1565-
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, opts *mondooclient.GarbageCollectOptions) error {
1568+
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, opts *mondooclient.DeleteAssetsRequest) error {
15661569
gcCalled = true
15671570
return nil
15681571
})
@@ -1591,11 +1594,11 @@ func (s *DeploymentHandlerSuite) TestGarbageCollection_SkipsWhenAlreadyRun() {
15911594
s.NoError(err)
15921595
s.True(result.IsZero())
15931596

1594-
s.False(gcCalled, "GarbageCollectAssets should NOT have been called")
1597+
s.False(gcCalled, "DeleteAssets should NOT have been called")
15951598
}
15961599

15971600
func (s *DeploymentHandlerSuite) TestGarbageCollection_FailureStillUpdatesTimestamp() {
1598-
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, opts *mondooclient.GarbageCollectOptions) error {
1601+
d := s.createDeploymentHandlerWithGCMock(func(ctx context.Context, opts *mondooclient.DeleteAssetsRequest) error {
15991602
return fmt.Errorf("API error")
16001603
})
16011604
s.NoError(d.KubeClient.Create(s.ctx, &s.auditConfig))
@@ -1624,8 +1627,8 @@ func (s *DeploymentHandlerSuite) TestGarbageCollection_FailureStillUpdatesTimest
16241627
}
16251628

16261629
// createDeploymentHandlerWithGCMock creates a DeploymentHandler with a mock MondooClientBuilder
1627-
// that captures calls to GarbageCollectAssets.
1628-
func (s *DeploymentHandlerSuite) createDeploymentHandlerWithGCMock(gcFunc func(context.Context, *mondooclient.GarbageCollectOptions) error) DeploymentHandler {
1630+
// that captures calls to DeleteAssets.
1631+
func (s *DeploymentHandlerSuite) createDeploymentHandlerWithGCMock(gcFunc func(context.Context, *mondooclient.DeleteAssetsRequest) error) DeploymentHandler {
16291632
// Create a mock credentials secret so GC can read it
16301633
key := credentials.MondooServiceAccount(s.T())
16311634
mockSA := mondooclient.ServiceAccountCredentials{
@@ -1660,14 +1663,14 @@ func (s *DeploymentHandlerSuite) createDeploymentHandlerWithGCMock(gcFunc func(c
16601663
// fakeMondooClient implements just enough of MondooClient to test GC
16611664
type fakeMondooClient struct {
16621665
mondooclient.MondooClient
1663-
gcFunc func(context.Context, *mondooclient.GarbageCollectOptions) error
1666+
gcFunc func(context.Context, *mondooclient.DeleteAssetsRequest) error
16641667
}
16651668

1666-
func (f *fakeMondooClient) GarbageCollectAssets(ctx context.Context, opts *mondooclient.GarbageCollectOptions) error {
1669+
func (f *fakeMondooClient) DeleteAssets(ctx context.Context, req *mondooclient.DeleteAssetsRequest) (*mondooclient.DeleteAssetsConfirmation, error) {
16671670
if f.gcFunc != nil {
1668-
return f.gcFunc(ctx, opts)
1671+
return &mondooclient.DeleteAssetsConfirmation{}, f.gcFunc(ctx, req)
16691672
}
1670-
return nil
1673+
return &mondooclient.DeleteAssetsConfirmation{}, nil
16711674
}
16721675

16731676
func TestDeploymentHandlerSuite(t *testing.T) {

0 commit comments

Comments
 (0)