From 2fee6279abe98486ad547ec7ec7b4da7f2bb47a3 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Thu, 1 Oct 2026 13:00:03 +0000 Subject: [PATCH] controller: Place events in the operator namespace Cluster-scoped CRDs (BootcNodePool, BootcNode) have no namespace, so client-go's EventRecorder defaults to creating events in "default". Deep-copy the regarding/related objects and inject the operator's own namespace before recording, so events appear alongside the operator. The namespace is read from POD_NAMESPACE (set via downward API) with a fallback to the in-cluster service-account namespace file. Fixes: https://github.com/bootc-dev/bootc-operator/issues/209 Assisted-by: AI Signed-off-by: Alice Frosi --- cmd/controller/main.go | 14 ++++++++++++++ config/manager/manager.yaml | 5 +++++ internal/controller/bootcnodepool_controller.go | 4 ++++ internal/controller/events.go | 14 ++++++++++++++ internal/controller/events_test.go | 2 +- internal/controller/suite_test.go | 16 +++++++++++++--- test/e2e/bootcnode_test.go | 4 +++- 7 files changed, 54 insertions(+), 5 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 76699e1b..f15223d3 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -5,6 +5,7 @@ package main import ( "flag" "os" + "strings" "time" "k8s.io/apimachinery/pkg/runtime" @@ -21,11 +22,23 @@ import ( "github.com/bootc-dev/bootc-operator/internal/version" ) +const namespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) +func detectNamespace() string { + if ns := os.Getenv("POD_NAMESPACE"); ns != "" { + return ns + } + if data, err := os.ReadFile(namespacePath); err == nil { + return strings.TrimSpace(string(data)) + } + return "" +} + func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(bootcv1alpha1.AddToScheme(scheme)) @@ -96,6 +109,7 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), KubeClient: kubeClient, + EventNamespace: detectNamespace(), TagResolver: ®istry.GGCRResolver{AllowInsecure: allowInsecureRegistry}, TagResolutionInterval: tagResolutionInterval, }).SetupWithManager(mgr); err != nil { diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 8260fceb..e34d6944 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -72,6 +72,11 @@ spec: - --health-probe-bind-address=:8081 image: ghcr.io/bootc-dev/bootc-operator:latest name: manager + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace ports: - containerPort: 8081 name: health diff --git a/internal/controller/bootcnodepool_controller.go b/internal/controller/bootcnodepool_controller.go index 24f9ae2d..787e654b 100644 --- a/internal/controller/bootcnodepool_controller.go +++ b/internal/controller/bootcnodepool_controller.go @@ -58,6 +58,10 @@ type BootcNodePoolReconciler struct { KubeClient kubernetes.Interface Recorder events.EventRecorder + // EventNamespace is the namespace where events for cluster-scoped + // resources are created. Without this, client-go defaults to "default". + EventNamespace string + TagResolver TagResolver TagResolutionInterval time.Duration diff --git a/internal/controller/events.go b/internal/controller/events.go index 20211446..3d5389b0 100644 --- a/internal/controller/events.go +++ b/internal/controller/events.go @@ -420,9 +420,23 @@ func (r *BootcNodePoolReconciler) recordEvent( eventType, reason, action string, note EventNote, ) { + if r.EventNamespace != "" { + regarding = withNamespace(regarding, r.EventNamespace) + if related != nil { + related = withNamespace(related, r.EventNamespace) + } + } r.Recorder.Eventf(regarding, related, eventType, reason, action, "%s", note.Note()) } +func withNamespace(obj runtime.Object, ns string) runtime.Object { + c := obj.DeepCopyObject() + if o, ok := c.(metav1.Object); ok { + o.SetNamespace(ns) + } + return c +} + // capNote keeps a note within the events.k8s.io/v1 1 KiB limit and never splits // a UTF-8 sequence. It is only needed for notes built from unbounded free text // (condition messages, error strings); notes assembled from bounded fields fit diff --git a/internal/controller/events_test.go b/internal/controller/events_test.go index 9a7e7a04..ccdb5c62 100644 --- a/internal/controller/events_test.go +++ b/internal/controller/events_test.go @@ -855,7 +855,7 @@ func eventsForObject( if err := k8sClient.List( ctx, &eventList, - client.InNamespace(metav1.NamespaceDefault), + client.InNamespace(testutil.OperatorNamespaceName), ); err != nil { return nil, err } diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 688a9314..a85cb868 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -9,6 +9,8 @@ import ( "path/filepath" "testing" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -18,6 +20,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" bootcv1alpha1 "github.com/bootc-dev/bootc-operator/api/v1alpha1" + testutil "github.com/bootc-dev/bootc-operator/test/util" ) var ( @@ -74,10 +77,17 @@ func TestMain(m *testing.M) { os.Exit(1) } + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testutil.OperatorNamespaceName}} + if err := k8sClient.Create(context.Background(), ns); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create event namespace: %v\n", err) + os.Exit(1) + } + testReconciler = &BootcNodePoolReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - KubeClient: kubeClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + KubeClient: kubeClient, + EventNamespace: testutil.OperatorNamespaceName, } if err := testReconciler.SetupWithManager(mgr); err != nil { fmt.Fprintf(os.Stderr, "Failed to setup reconciler: %v\n", err) diff --git a/test/e2e/bootcnode_test.go b/test/e2e/bootcnode_test.go index 9df430e2..09b39fe1 100644 --- a/test/e2e/bootcnode_test.go +++ b/test/e2e/bootcnode_test.go @@ -937,7 +937,9 @@ func fetchEvents( ) func() ([]eventsv1.Event, error) { return func() ([]eventsv1.Event, error) { var eventList eventsv1.EventList - if err := c.List(ctx, &eventList); err != nil { + if err := c.List(ctx, &eventList, + client.InNamespace(testutil.OperatorNamespaceName), + ); err != nil { return nil, err }