Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1alpha2/mondooauditconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ type MondooAuditConfigSpec struct {
Filtering Filtering `json:"filtering,omitempty"`
Containers Containers `json:"containers,omitempty"`

// Annotations allows adding custom annotations to all scanned assets. These key-value pairs
// will be attached to every asset discovered by the operator, making them searchable
// and filterable in the Mondoo Console.
Annotations map[string]string `json:"annotations,omitempty"`

// Admission is DEPRECATED and ignored. Admission webhooks were removed in v12.1.0.
// The operator will automatically clean up any orphaned admission resources.
// See docs/admission-migration-guide.md for migration instructions.
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions charts/mondoo-operator/templates/mondooauditconfig-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ spec:
during its operation.
type: string
type: object
annotations:
additionalProperties:
type: string
description: |-
Annotations allows adding custom annotations to all scanned assets. These key-value pairs
will be attached to every asset discovered by the operator, making them searchable
and filterable in the Mondoo Console.
type: object
consoleIntegration:
properties:
enable:
Expand Down
17 changes: 13 additions & 4 deletions cmd/mondoo-operator/resource_watcher/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"

"go.mondoo.com/mondoo-operator/controllers/resource_watcher"
annot "go.mondoo.com/mondoo-operator/pkg/annotations"
"go.mondoo.com/mondoo-operator/pkg/utils/logger"
)

Expand Down Expand Up @@ -54,6 +55,7 @@ func init() {
resourceTypes := Cmd.Flags().StringSlice("resource-types", nil, "Resource types to watch (comma-separated). Overrides --watch-all-resources if specified.")
apiProxy := Cmd.Flags().String("api-proxy", "", "HTTP proxy to use for API requests.")
timeout := Cmd.Flags().Duration("timeout", 25*time.Minute, "Timeout for scan operations.")
annotations := Cmd.Flags().StringToString("annotation", nil, "Annotations to add to scanned assets (can specify multiple, e.g., --annotation env=prod --annotation team=platform).")

Cmd.RunE = func(cmd *cobra.Command, args []string) error {
log.SetLogger(logger.NewLogger())
Expand Down Expand Up @@ -109,6 +111,11 @@ func init() {
}
}

// Validate annotations
if err := annot.Validate(*annotations); err != nil {
return fmt.Errorf("invalid annotations: %w", err)
}

logger.Info("Starting resource watcher",
"config", *configPath,
"namespaces", namespacesList,
Expand All @@ -117,7 +124,8 @@ func init() {
"minimumScanInterval", *minimumScanInterval,
"watchAllResources", *watchAllResources,
"resourceTypes", resourceTypesList,
"timeout", *timeout)
"timeout", *timeout,
"annotations", *annotations)

// Create context with signal handling
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -159,9 +167,10 @@ func init() {

// Create scanner
scanner := resource_watcher.NewScanner(resource_watcher.ScannerConfig{
ConfigPath: *configPath,
APIProxy: *apiProxy,
Timeout: *timeout,
ConfigPath: *configPath,
APIProxy: *apiProxy,
Timeout: *timeout,
Annotations: *annotations,
})

// Create debouncer with rate limiting
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ spec:
description: ServiceAccountName is DEPRECATED.
type: string
type: object
annotations:
additionalProperties:
type: string
description: |-
Annotations allows adding custom annotations to all scanned assets. These key-value pairs
will be attached to every asset discovered by the operator, making them searchable
and filterable in the Mondoo Console.
type: object
consoleIntegration:
properties:
enable:
Expand Down
7 changes: 7 additions & 0 deletions controllers/container_image/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig,
}
}

// Add user-defined annotations to all assets
if len(m.Spec.Annotations) > 0 {
for i := range inv.Spec.Assets {
inv.Spec.Assets[i].AddAnnotations(m.Spec.Annotations)
}
}

invBytes, err := yaml.Marshal(inv)
if err != nil {
return "", err
Expand Down
42 changes: 42 additions & 0 deletions controllers/container_image/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright Mondoo, Inc. 2026
// SPDX-License-Identifier: BUSL-1.1

package container_image

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
"go.mondoo.com/mondoo-operator/api/v1alpha2"
)

const testClusterUID = "abcdefg"

func TestInventory_WithAnnotations(t *testing.T) {
auditConfig := v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"},
Spec: v1alpha2.MondooAuditConfigSpec{
Annotations: map[string]string{
"env": "prod",
"team": "platform",
},
},
}

invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
require.NoError(t, err, "unexpected error generating inventory")

var inv inventory.Inventory
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")

for _, asset := range inv.Spec.Assets {
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
}
}
18 changes: 16 additions & 2 deletions controllers/k8s_scan/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ func ConfigMap(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig,
}

func ExternalClusterConfigMap(integrationMRN, operatorClusterUID string, cluster v1alpha2.ExternalCluster, m v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) (*corev1.ConfigMap, error) {
inv, err := ExternalClusterInventory(integrationMRN, operatorClusterUID, cluster, cfg)
inv, err := ExternalClusterInventory(integrationMRN, operatorClusterUID, cluster, m, cfg)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -932,6 +932,13 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig,
}
}

// Add user-defined annotations to all assets
if len(m.Spec.Annotations) > 0 {
for i := range inv.Spec.Assets {
inv.Spec.Assets[i].AddAnnotations(m.Spec.Annotations)
}
}

invBytes, err := yaml.Marshal(inv)
if err != nil {
return "", err
Expand All @@ -940,7 +947,7 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig,
return string(invBytes), nil
}

func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster v1alpha2.ExternalCluster, cfg v1alpha2.MondooOperatorConfig) (string, error) {
func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster v1alpha2.ExternalCluster, m v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) (string, error) {
// Use cluster-specific filtering if provided, otherwise fall back to empty filtering
filtering := cluster.Filtering

Expand Down Expand Up @@ -994,6 +1001,13 @@ func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster
}
}

// Add user-defined annotations to all assets
if len(m.Spec.Annotations) > 0 {
for i := range inv.Spec.Assets {
inv.Spec.Assets[i].AddAnnotations(m.Spec.Annotations)
}
}

invBytes, err := yaml.Marshal(inv)
if err != nil {
return "", err
Expand Down
70 changes: 70 additions & 0 deletions controllers/k8s_scan/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright Mondoo, Inc. 2026
// SPDX-License-Identifier: BUSL-1.1

package k8s_scan

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
"go.mondoo.com/mondoo-operator/api/v1alpha2"
)

const testClusterUID = "abcdefg"

func TestInventory_WithAnnotations(t *testing.T) {
auditConfig := v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"},
Spec: v1alpha2.MondooAuditConfigSpec{
Annotations: map[string]string{
"env": "prod",
"team": "platform",
},
},
}

invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{})
require.NoError(t, err, "unexpected error generating inventory")

var inv inventory.Inventory
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")

for _, asset := range inv.Spec.Assets {
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
}
}

func TestExternalClusterInventory_WithAnnotations(t *testing.T) {
auditConfig := v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"},
Spec: v1alpha2.MondooAuditConfigSpec{
Annotations: map[string]string{
"env": "staging",
"team": "security",
},
},
}

cluster := v1alpha2.ExternalCluster{
Name: "remote-cluster",
}

invStr, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, v1alpha2.MondooOperatorConfig{})
require.NoError(t, err, "unexpected error generating inventory")

var inv inventory.Inventory
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")

for _, asset := range inv.Spec.Assets {
assert.Equal(t, "staging", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
assert.Equal(t, "security", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
}
}
29 changes: 29 additions & 0 deletions controllers/mondooauditconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"go.mondoo.com/mondoo-operator/controllers/nodes"
resourcewatcher "go.mondoo.com/mondoo-operator/controllers/resource_watcher"
"go.mondoo.com/mondoo-operator/controllers/status"
"go.mondoo.com/mondoo-operator/pkg/annotations"
"go.mondoo.com/mondoo-operator/pkg/client/mondooclient"
"go.mondoo.com/mondoo-operator/pkg/constants"
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
Expand Down Expand Up @@ -221,6 +222,34 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re
}
}()

// Validate annotations before using them in inventory or CLI args.
// Set a degraded condition so users can see the problem via kubectl describe.
if err := annotations.Validate(mondooAuditConfig.Spec.Annotations); err != nil {
mondooAuditConfig.Status.Conditions = mondoo.SetMondooAuditCondition(
mondooAuditConfig.Status.Conditions,
v1alpha2.MondooOperatorDegraded,
corev1.ConditionTrue,
"InvalidAnnotations",
fmt.Sprintf("Invalid annotations in MondooAuditConfig: %s", err),
mondoo.UpdateConditionIfReasonOrMessageChange,
nil, "",
)
log.Error(err, "invalid annotations in MondooAuditConfig, skipping reconciliation")
return ctrl.Result{}, nil
}
// Clear any previous annotation validation error
if cond := mondoo.FindMondooAuditConditions(mondooAuditConfig.Status.Conditions, v1alpha2.MondooOperatorDegraded); cond != nil && cond.Reason == "InvalidAnnotations" {
mondooAuditConfig.Status.Conditions = mondoo.SetMondooAuditCondition(
mondooAuditConfig.Status.Conditions,
v1alpha2.MondooOperatorDegraded,
corev1.ConditionFalse,
"AnnotationsValid",
"Annotations are valid",
mondoo.UpdateConditionAlways,
nil, "",
)
}

// If spec.MondooTokenSecretRef != "" and the Secret referenced in spec.MondooCredsSecretRef
// does not exist, then attempt to trade the token for a Mondoo service account and save it
// in the Secret referenced in .spec.MondooCredsSecretRef
Expand Down
7 changes: 7 additions & 0 deletions controllers/nodes/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,13 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig)
}
}

// Add user-defined annotations to all assets
if len(m.Spec.Annotations) > 0 {
for i := range inv.Spec.Assets {
inv.Spec.Assets[i].AddAnnotations(m.Spec.Annotations)
}
}

invBytes, err := yaml.Marshal(inv)
if err != nil {
return "", err
Expand Down
28 changes: 28 additions & 0 deletions controllers/nodes/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"

"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
"go.mondoo.com/mondoo-operator/api/v1alpha2"
"go.mondoo.com/mondoo-operator/pkg/constants"
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
Expand Down Expand Up @@ -242,6 +246,30 @@ func TestInventory(t *testing.T) {
assert.Contains(t, inventory, integrationMRN)
}

func TestInventory_WithAnnotations(t *testing.T) {
auditConfig := v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"},
Spec: v1alpha2.MondooAuditConfigSpec{
Annotations: map[string]string{
"env": "prod",
"team": "platform",
},
},
}

invStr, err := Inventory("", testClusterUID, auditConfig)
require.NoError(t, err, "unexpected error generating inventory")

var inv inventory.Inventory
require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv))
require.NotEmpty(t, inv.Spec.Assets, "expected at least one asset")

for _, asset := range inv.Spec.Assets {
assert.Equal(t, "prod", asset.Annotations["env"], "asset %s missing env annotation", asset.Name)
assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name)
}
}

func testMondooAuditConfig() *v1alpha2.MondooAuditConfig {
return &v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{
Expand Down
4 changes: 4 additions & 0 deletions controllers/resource_watcher/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"k8s.io/utils/ptr"

"go.mondoo.com/mondoo-operator/api/v1alpha2"
"go.mondoo.com/mondoo-operator/pkg/annotations"
"go.mondoo.com/mondoo-operator/pkg/constants"
"go.mondoo.com/mondoo-operator/pkg/feature_flags"
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
Expand Down Expand Up @@ -85,6 +86,9 @@ func Deployment(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.Mondoo
cmd = append(cmd, "--api-proxy", *cfg.Spec.HttpProxy)
}

// Add annotations (sorted for deterministic ordering)
cmd = append(cmd, annotations.AnnotationArgs(m.Spec.Annotations)...)

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

Expand Down
Loading
Loading