Skip to content

Commit 072c03e

Browse files
chris-rockclaude
andcommitted
Extract annotation helpers, add validation, and strengthen tests
- Extract duplicated annotation CLI arg building into pkg/annotations.AnnotationArgs() - Add annotations.Validate() to reject empty keys, keys containing '=', and empty values - Call validation in the reconciler and resource-watcher CLI entrypoint - Strengthen test assertions to unmarshal inventory YAML and check asset annotations directly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f4f0efa commit 072c03e

10 files changed

Lines changed: 205 additions & 47 deletions

File tree

cmd/mondoo-operator/resource_watcher/cmd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"sigs.k8s.io/controller-runtime/pkg/log"
2222

2323
"go.mondoo.com/mondoo-operator/controllers/resource_watcher"
24+
annot "go.mondoo.com/mondoo-operator/pkg/annotations"
2425
"go.mondoo.com/mondoo-operator/pkg/utils/logger"
2526
)
2627

@@ -110,6 +111,11 @@ func init() {
110111
}
111112
}
112113

114+
// Validate annotations
115+
if err := annot.Validate(*annotations); err != nil {
116+
return fmt.Errorf("invalid annotations: %w", err)
117+
}
118+
113119
logger.Info("Starting resource watcher",
114120
"config", *configPath,
115121
"namespaces", namespacesList,

controllers/container_image/resources_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"gopkg.in/yaml.v2"
1012
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1113

14+
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
1215
"go.mondoo.com/mondoo-operator/api/v1alpha2"
1316
)
1417

@@ -25,10 +28,15 @@ func TestInventory_WithAnnotations(t *testing.T) {
2528
},
2629
}
2730

28-
inv, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
29-
assert.NoError(t, err, "unexpected error generating inventory")
30-
assert.Contains(t, inv, "env")
31-
assert.Contains(t, inv, "prod")
32-
assert.Contains(t, inv, "team")
33-
assert.Contains(t, inv, "platform")
31+
invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
32+
require.NoError(t, err, "unexpected error generating inventory")
33+
34+
var inv inventory.Inventory
35+
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
36+
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")
37+
38+
for _, asset := range inv.Spec.Assets {
39+
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
40+
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
41+
}
3442
}

controllers/k8s_scan/resources_test.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"gopkg.in/yaml.v2"
1012
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1113

14+
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
1215
"go.mondoo.com/mondoo-operator/api/v1alpha2"
1316
)
1417

@@ -25,12 +28,17 @@ func TestInventory_WithAnnotations(t *testing.T) {
2528
},
2629
}
2730

28-
inv, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
29-
assert.NoError(t, err, "unexpected error generating inventory")
30-
assert.Contains(t, inv, "env")
31-
assert.Contains(t, inv, "prod")
32-
assert.Contains(t, inv, "team")
33-
assert.Contains(t, inv, "platform")
31+
invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
32+
require.NoError(t, err, "unexpected error generating inventory")
33+
34+
var inv inventory.Inventory
35+
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
36+
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")
37+
38+
for _, asset := range inv.Spec.Assets {
39+
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
40+
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
41+
}
3442
}
3543

3644
func TestExternalClusterInventory_WithAnnotations(t *testing.T) {
@@ -48,10 +56,15 @@ func TestExternalClusterInventory_WithAnnotations(t *testing.T) {
4856
Name: "remote-cluster",
4957
}
5058

51-
inv, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, v1alpha2.MondooOperatorConfig{})
52-
assert.NoError(t, err, "unexpected error generating inventory")
53-
assert.Contains(t, inv, "env")
54-
assert.Contains(t, inv, "staging")
55-
assert.Contains(t, inv, "team")
56-
assert.Contains(t, inv, "security")
59+
invStr, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, v1alpha2.MondooOperatorConfig{})
60+
require.NoError(t, err, "unexpected error generating inventory")
61+
62+
var inv inventory.Inventory
63+
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
64+
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")
65+
66+
for _, asset := range inv.Spec.Assets {
67+
assert.Equal(t, "staging", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
68+
assert.Equal(t, "security", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
69+
}
5770
}

controllers/mondooauditconfig_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"go.mondoo.com/mondoo-operator/controllers/nodes"
3333
resourcewatcher "go.mondoo.com/mondoo-operator/controllers/resource_watcher"
3434
"go.mondoo.com/mondoo-operator/controllers/status"
35+
"go.mondoo.com/mondoo-operator/pkg/annotations"
3536
"go.mondoo.com/mondoo-operator/pkg/client/mondooclient"
3637
"go.mondoo.com/mondoo-operator/pkg/constants"
3738
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
@@ -183,6 +184,12 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re
183184
return ctrl.Result{Requeue: true}, nil
184185
}
185186

187+
// Validate annotations before using them in inventory or CLI args
188+
if err := annotations.Validate(mondooAuditConfig.Spec.Annotations); err != nil {
189+
log.Error(err, "invalid annotations in MondooAuditConfig")
190+
return ctrl.Result{}, err
191+
}
192+
186193
mondooAuditConfigCopy := mondooAuditConfig.DeepCopy()
187194

188195
// Conditions might be updated before this reconciler reaches the end

controllers/nodes/resources_test.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"gopkg.in/yaml.v2"
14+
15+
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
1216
"go.mondoo.com/mondoo-operator/api/v1alpha2"
1317
"go.mondoo.com/mondoo-operator/pkg/constants"
1418
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
@@ -253,12 +257,17 @@ func TestInventory_WithAnnotations(t *testing.T) {
253257
},
254258
}
255259

256-
inv, err := Inventory("", testClusterUID, auditConfig)
257-
assert.NoError(t, err, "unexpected error generating inventory")
258-
assert.Contains(t, inv, "env")
259-
assert.Contains(t, inv, "prod")
260-
assert.Contains(t, inv, "team")
261-
assert.Contains(t, inv, "platform")
260+
invStr, err := Inventory("", testClusterUID, auditConfig)
261+
require.NoError(t, err, "unexpected error generating inventory")
262+
263+
var inv inventory.Inventory
264+
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
265+
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")
266+
267+
for _, asset := range inv.Spec.Assets {
268+
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
269+
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
270+
}
262271
}
263272

264273
func testMondooAuditConfig() *v1alpha2.MondooAuditConfig {

controllers/resource_watcher/resources.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package resource_watcher
55

66
import (
77
"fmt"
8-
"sort"
98
"strings"
109
"time"
1110

@@ -15,6 +14,7 @@ import (
1514
"k8s.io/utils/ptr"
1615

1716
"go.mondoo.com/mondoo-operator/api/v1alpha2"
17+
"go.mondoo.com/mondoo-operator/pkg/annotations"
1818
"go.mondoo.com/mondoo-operator/pkg/constants"
1919
"go.mondoo.com/mondoo-operator/pkg/feature_flags"
2020
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
@@ -87,14 +87,7 @@ func Deployment(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.Mondoo
8787
}
8888

8989
// Add annotations (sorted for deterministic ordering)
90-
annotationKeys := make([]string, 0, len(m.Spec.Annotations))
91-
for k := range m.Spec.Annotations {
92-
annotationKeys = append(annotationKeys, k)
93-
}
94-
sort.Strings(annotationKeys)
95-
for _, key := range annotationKeys {
96-
cmd = append(cmd, "--annotation", fmt.Sprintf("%s=%s", key, m.Spec.Annotations[key]))
97-
}
90+
cmd = append(cmd, annotations.AnnotationArgs(m.Spec.Annotations)...)
9891

9992
envVars := feature_flags.AllFeatureFlagsAsEnv()
10093
envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"})

controllers/resource_watcher/resources_test.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,17 @@ func TestDeployment_WithAnnotations(t *testing.T) {
259259
deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", config, operatorConfig)
260260

261261
container := deployment.Spec.Template.Spec.Containers[0]
262-
cmdStr := ""
263-
for _, c := range container.Command {
264-
cmdStr += c + " "
262+
cmd := container.Command
263+
264+
// Find --annotation flags and collect their values
265+
annotationArgs := map[string]bool{}
266+
for i, arg := range cmd {
267+
if arg == "--annotation" && i+1 < len(cmd) {
268+
annotationArgs[cmd[i+1]] = true
269+
}
265270
}
266-
assert.Contains(t, cmdStr, "--annotation env=prod")
267-
assert.Contains(t, cmdStr, "--annotation team=platform")
271+
assert.True(t, annotationArgs["env=prod"], "expected --annotation env=prod")
272+
assert.True(t, annotationArgs["team=platform"], "expected --annotation team=platform")
268273
}
269274

270275
func TestDeployment_HighPriorityByDefault(t *testing.T) {

controllers/resource_watcher/scanner.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import (
88
"fmt"
99
"os"
1010
"os/exec"
11-
"sort"
1211
"time"
1312

1413
ctrl "sigs.k8s.io/controller-runtime"
14+
15+
"go.mondoo.com/mondoo-operator/pkg/annotations"
1516
)
1617

1718
var scannerLogger = ctrl.Log.WithName("resource-watcher-scanner")
@@ -82,14 +83,7 @@ func (s *Scanner) ScanManifests(ctx context.Context, manifests []byte) error {
8283
cnspecArgs = append(cnspecArgs, "--api-proxy", s.config.APIProxy)
8384
}
8485
// Add annotations as command-line arguments (sorted for deterministic ordering)
85-
annotationKeys := make([]string, 0, len(s.config.Annotations))
86-
for k := range s.config.Annotations {
87-
annotationKeys = append(annotationKeys, k)
88-
}
89-
sort.Strings(annotationKeys)
90-
for _, key := range annotationKeys {
91-
cnspecArgs = append(cnspecArgs, "--annotation", fmt.Sprintf("%s=%s", key, s.config.Annotations[key]))
92-
}
86+
cnspecArgs = append(cnspecArgs, annotations.AnnotationArgs(s.config.Annotations)...)
9387

9488
// Create context with timeout
9589
scanCtx := ctx

pkg/annotations/annotations.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright Mondoo, Inc. 2026
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package annotations
5+
6+
import (
7+
"fmt"
8+
"sort"
9+
"strings"
10+
)
11+
12+
// AnnotationArgs converts a map of annotations into sorted CLI arguments
13+
// suitable for passing to cnspec via --annotation key=value flags.
14+
func AnnotationArgs(annotations map[string]string) []string {
15+
if len(annotations) == 0 {
16+
return nil
17+
}
18+
19+
keys := make([]string, 0, len(annotations))
20+
for k := range annotations {
21+
keys = append(keys, k)
22+
}
23+
sort.Strings(keys)
24+
25+
args := make([]string, 0, len(annotations)*2)
26+
for _, key := range keys {
27+
args = append(args, "--annotation", fmt.Sprintf("%s=%s", key, annotations[key]))
28+
}
29+
return args
30+
}
31+
32+
// Validate checks that annotation keys and values are well-formed for use as
33+
// cnspec --annotation key=value CLI arguments. Keys must be non-empty and must
34+
// not contain '='. Values must be non-empty.
35+
func Validate(annotations map[string]string) error {
36+
for k, v := range annotations {
37+
if k == "" {
38+
return fmt.Errorf("annotation key must not be empty")
39+
}
40+
if strings.Contains(k, "=") {
41+
return fmt.Errorf("annotation key %q must not contain '='", k)
42+
}
43+
if v == "" {
44+
return fmt.Errorf("annotation value for key %q must not be empty", k)
45+
}
46+
}
47+
return nil
48+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright Mondoo, Inc. 2026
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package annotations
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestAnnotationArgs(t *testing.T) {
14+
t.Run("nil map returns nil", func(t *testing.T) {
15+
assert.Nil(t, AnnotationArgs(nil))
16+
})
17+
18+
t.Run("empty map returns nil", func(t *testing.T) {
19+
assert.Nil(t, AnnotationArgs(map[string]string{}))
20+
})
21+
22+
t.Run("single annotation", func(t *testing.T) {
23+
args := AnnotationArgs(map[string]string{"env": "prod"})
24+
assert.Equal(t, []string{"--annotation", "env=prod"}, args)
25+
})
26+
27+
t.Run("multiple annotations are sorted by key", func(t *testing.T) {
28+
args := AnnotationArgs(map[string]string{
29+
"team": "platform",
30+
"env": "prod",
31+
"app": "mondoo",
32+
})
33+
assert.Equal(t, []string{
34+
"--annotation", "app=mondoo",
35+
"--annotation", "env=prod",
36+
"--annotation", "team=platform",
37+
}, args)
38+
})
39+
}
40+
41+
func TestValidate(t *testing.T) {
42+
t.Run("valid annotations", func(t *testing.T) {
43+
err := Validate(map[string]string{
44+
"env": "prod",
45+
"team": "platform",
46+
})
47+
assert.NoError(t, err)
48+
})
49+
50+
t.Run("nil map is valid", func(t *testing.T) {
51+
assert.NoError(t, Validate(nil))
52+
})
53+
54+
t.Run("empty map is valid", func(t *testing.T) {
55+
assert.NoError(t, Validate(map[string]string{}))
56+
})
57+
58+
t.Run("empty key is rejected", func(t *testing.T) {
59+
err := Validate(map[string]string{"": "value"})
60+
require.Error(t, err)
61+
assert.Contains(t, err.Error(), "must not be empty")
62+
})
63+
64+
t.Run("key with equals sign is rejected", func(t *testing.T) {
65+
err := Validate(map[string]string{"key=bad": "value"})
66+
require.Error(t, err)
67+
assert.Contains(t, err.Error(), "must not contain '='")
68+
})
69+
70+
t.Run("empty value is rejected", func(t *testing.T) {
71+
err := Validate(map[string]string{"key": ""})
72+
require.Error(t, err)
73+
assert.Contains(t, err.Error(), "must not be empty")
74+
})
75+
}

0 commit comments

Comments
 (0)