Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/actions/link-check/config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"aliveStatusCodes": [429, 200, 406]
}
"aliveStatusCodes": [200, 406, 429]
}
7 changes: 7 additions & 0 deletions charts/mondoo-operator/templates/manager-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand Down
1 change: 1 addition & 0 deletions cmd/mondoo-operator/operator/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func init() {
ContainerImageResolver: containerImageResolver,
StatusReporter: status.NewStatusReporter(mgr.GetClient(), controllers.MondooClientBuilder, v, containerImageResolver),
RunningOnOpenShift: isOpenShift,
EventRecorder: mgr.GetEventRecorderFor("mondooauditconfig-controller"),
}).SetupWithManager(mgr, mondooOperatorConfigExists); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "MondooAuditConfig")
return err
Expand Down
7 changes: 7 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand Down
54 changes: 54 additions & 0 deletions controllers/mondooauditconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -50,6 +51,7 @@ type MondooAuditConfigReconciler struct {
ContainerImageResolver mondoo.ContainerImageResolver
StatusReporter *status.StatusReporter
RunningOnOpenShift bool
EventRecorder record.EventRecorder
}

// so we can mock out the mondoo client for testing
Expand All @@ -68,6 +70,7 @@ var MondooClientBuilder = mondooclient.NewClient
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=delete;deletecollection
//+kubebuilder:rbac:groups=batch,resources=cronjobs;jobs,verbs=get;list;watch
//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
//+kubebuilder:rbac:groups=core,resources=pods;namespaces;nodes,verbs=get;list;watch
//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// Need to be able to create/update/delete Secrets for Mondoo service account credentials and Vault kubeconfig
Expand Down Expand Up @@ -228,6 +231,9 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re
}

deferFuncErr = mondoo.UpdateMondooAuditStatus(deferCtx, r.Client, mondooAuditConfigCopy, mondooAuditConfig, log)
if deferFuncErr == nil {
r.recordConditionEvents(mondooAuditConfig, mondooAuditConfigCopy.Status.Conditions, mondooAuditConfig.Status.Conditions)
}
// do not overwrite errors which happened before the deferred function is called
// but in case an error happened in the deferred func, also overwrite the reconcileResult
if reconcileError == nil && deferFuncErr != nil {
Expand Down Expand Up @@ -357,6 +363,54 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re
return ctrl.Result{Requeue: true, RequeueAfter: time.Hour * 24 * 7}, nil
}

func (r *MondooAuditConfigReconciler) recordConditionEvents(
config *v1alpha2.MondooAuditConfig,
oldConditions []v1alpha2.MondooAuditConfigCondition,
newConditions []v1alpha2.MondooAuditConfigCondition,
) {
if r.EventRecorder == nil {
return
}

oldByType := make(map[v1alpha2.MondooAuditConfigConditionType]v1alpha2.MondooAuditConfigCondition, len(oldConditions))
for _, condition := range oldConditions {
oldByType[condition.Type] = condition
}

for _, condition := range newConditions {
oldCondition, ok := oldByType[condition.Type]
if ok {
if conditionEventFieldsEqual(oldCondition, condition) {
continue
}
} else if condition.Status == corev1.ConditionFalse {
continue
}

eventType := corev1.EventTypeNormal
if condition.Status == corev1.ConditionTrue {
eventType = corev1.EventTypeWarning
}

reason := condition.Reason
if reason == "" {
reason = string(condition.Type)
Comment thread
mondoo-code-review[bot] marked this conversation as resolved.
}
message := condition.Message
if message == "" {
message = fmt.Sprintf("%s is %s", condition.Type, condition.Status)
}

r.EventRecorder.Event(config, eventType, reason, message)
}
}

func conditionEventFieldsEqual(a, b v1alpha2.MondooAuditConfigCondition) bool {
return a.Status == b.Status &&
a.Reason == b.Reason &&
a.Message == b.Message
}

// nodeEventsRequestMapper Maps node events to enqueue all MondooAuditConfigs that have node scanning enabled for
// reconciliation.
func (r *MondooAuditConfigReconciler) nodeEventsRequestMapper(ctx context.Context, o client.Object) []reconcile.Request {
Expand Down
133 changes: 133 additions & 0 deletions controllers/mondooauditconfig_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"time"

Expand All @@ -22,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
scheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
Expand Down Expand Up @@ -558,6 +560,137 @@ func TestMondooAuditConfig_Containers_Enable(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%d %d * * *", cronStart.Minute(), cronStart.Hour()), mondooAuditConfig.Spec.Containers.Schedule)
}

func TestMondooAuditConfigRecordsConditionEventsFromReconcile(t *testing.T) {
mondooAuditConfig := testMondooAuditConfig()
mondooAuditConfig.Spec.Annotations = map[string]string{"invalid": ""}

fakeClient := fake.NewClientBuilder().
WithStatusSubresource(mondooAuditConfig).
WithObjects(mondooAuditConfig).
Build()

recorder := record.NewFakeRecorder(1)
reconciler := &MondooAuditConfigReconciler{
Client: fakeClient,
StatusReporter: status.NewStatusReporter(fakeClient, nil, k8sVersion, mondoofake.NewNoOpContainerImageResolver()),
ContainerImageResolver: mondoofake.NewNoOpContainerImageResolver(),
EventRecorder: recorder,
}

_, err := reconciler.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{
Name: testMondooAuditConfigName,
Namespace: testNamespace,
},
})
require.NoError(t, err)

assertRecordedEvent(t, recorder, "Warning", "InvalidAnnotations", "Invalid annotations in MondooAuditConfig")
assertNoRecordedEvent(t, recorder)
}

func TestRecordConditionEvents(t *testing.T) {
config := testMondooAuditConfig()
recorder := record.NewFakeRecorder(4)
reconciler := &MondooAuditConfigReconciler{EventRecorder: recorder}

oldConditions := []v1alpha2.MondooAuditConfigCondition{
{
Type: v1alpha2.K8sResourcesScanningDegraded,
Status: corev1.ConditionFalse,
Reason: "KubernetesResourcesScanningAvailable",
Message: "Kubernetes Resources Scanning is available",
},
{
Type: v1alpha2.NodeScanningDegraded,
Status: corev1.ConditionTrue,
Reason: "NodeScanningUnavailable",
Message: "Node Scanning is unavailable",
},
}
newConditions := []v1alpha2.MondooAuditConfigCondition{
{
Type: v1alpha2.K8sResourcesScanningDegraded,
Status: corev1.ConditionTrue,
Reason: "KubernetesResourcesScanningUnavailable",
Message: "Kubernetes Resources Scanning is unavailable",
},
{
Type: v1alpha2.NodeScanningDegraded,
Status: corev1.ConditionFalse,
Reason: "NodeScanningAvailable",
Message: "Node Scanning is available",
},
}

reconciler.recordConditionEvents(config, oldConditions, newConditions)

assertRecordedEvent(t, recorder, "Warning", "KubernetesResourcesScanningUnavailable", "Kubernetes Resources Scanning is unavailable")
assertRecordedEvent(t, recorder, "Normal", "NodeScanningAvailable", "Node Scanning is available")
assertNoRecordedEvent(t, recorder)
}

func TestRecordConditionEventsSkipsInitialHealthyConditions(t *testing.T) {
config := testMondooAuditConfig()
recorder := record.NewFakeRecorder(1)
reconciler := &MondooAuditConfigReconciler{EventRecorder: recorder}

newConditions := []v1alpha2.MondooAuditConfigCondition{
{
Type: v1alpha2.K8sContainerImageScanningDegraded,
Status: corev1.ConditionFalse,
Reason: "KubernetesContainerImageScanningAvailable",
Message: "Kubernetes Container Image Scanning is available",
},
}

reconciler.recordConditionEvents(config, nil, newConditions)

assertNoRecordedEvent(t, recorder)
}

func TestRecordConditionEventsSkipsUnchangedConditions(t *testing.T) {
config := testMondooAuditConfig()
recorder := record.NewFakeRecorder(1)
reconciler := &MondooAuditConfigReconciler{EventRecorder: recorder}

conditions := []v1alpha2.MondooAuditConfigCondition{
{
Type: v1alpha2.K8sContainerImageScanningDegraded,
Status: corev1.ConditionFalse,
Reason: "KubernetesContainerImageScanningAvailable",
Message: "Kubernetes Container Image Scanning is available",
},
}

reconciler.recordConditionEvents(config, conditions, conditions)

assertNoRecordedEvent(t, recorder)
}

func assertRecordedEvent(t *testing.T, recorder *record.FakeRecorder, eventType, reason, message string) {
t.Helper()

select {
case event := <-recorder.Events:
if !strings.Contains(event, eventType) || !strings.Contains(event, reason) || !strings.Contains(event, message) {
t.Fatalf("expected event to contain %q, %q, and %q, got %q", eventType, reason, message, event)
}
case <-time.After(time.Second):
t.Fatalf("expected event %s/%s", eventType, reason)
}
}

func assertNoRecordedEvent(t *testing.T, recorder *record.FakeRecorder) {
t.Helper()

select {
case event := <-recorder.Events:
t.Fatalf("expected no event, got %q", event)
default:
}
}

func testMondooAuditConfig() *v1alpha2.MondooAuditConfig {
return &v1alpha2.MondooAuditConfig{
ObjectMeta: metav1.ObjectMeta{
Expand Down
7 changes: 7 additions & 0 deletions docs/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand Down
Loading