// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package events import ( "context" "fmt" "time" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" eventsv1 "k8s.io/api/events/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8sevents "k8s.io/client-go/tools/events" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/projectcapsule/capsule/pkg/runtime/configuration" ) const ( ReportingController = "controller.projectcapsule.dev" ReportingInstance = "capsule-admission" eventQueueSize = 1024 eventCreateTimeout = 10 * time.Second ) type EventRecorder interface { k8sevents.EventRecorder LabeledEvent(regarding runtime.Object, eventType string, reason string, action string, note string) LabeledEvent } type eventEmitter interface { Emit(ctx context.Context, e LabeledEvent) } type eventRecorder struct { k8sevents.EventRecorder client client.Client configuration configuration.Configuration log logr.Logger queue chan *eventsv1.Event } func NewEventRecorder( c client.Client, log logr.Logger, recorder k8sevents.EventRecorder, configuration configuration.Configuration, ) EventRecorder { r := &eventRecorder{ EventRecorder: recorder, client: c, log: log.WithName("event-recorder"), configuration: configuration, } if c != nil { r.queue = make(chan *eventsv1.Event, eventQueueSize) go r.run() } return r } func (r *eventRecorder) Emit(ctx context.Context, e LabeledEvent) { if r == nil { return } if r.client == nil { r.log.Error(nil, "cannot emit labeled event: client is nil") return } if e == nil { r.log.Error(nil, "cannot emit labeled event: event is nil") return } if e.Reason() == "" { r.log.Error(nil, "cannot emit labeled event: reason is empty") return } if e.Action() == "" { r.log.Error(nil, "cannot emit labeled event: action is empty") return } regardingRef, metaObj, err := objectReference(e.Regarding()) if err != nil { r.log.Error(err, "cannot emit labeled event: build regarding reference") return } namespace := metaObj.GetNamespace() if namespace == "" { namespace = r.configuration.Events().ClusterEventNamespace } if namespace == "" { r.log.Error(nil, "cannot emit labeled event: namespace is empty") return } event := &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ GenerateName: metaObj.GetName(), Namespace: namespace, Labels: e.Labels(), Annotations: e.Annotations(), }, EventTime: metav1.MicroTime{Time: time.Now()}, ReportingController: ReportingController, ReportingInstance: ReportingInstance, Action: e.Action(), Reason: e.Reason(), Regarding: regardingRef, Note: e.Note(), Type: e.EventType(), } if e.Related() != nil { relatedRef, _, err := objectReference(e.Related()) if err != nil { r.log.Error(err, "cannot emit labeled event: build related reference") return } event.Related = &relatedRef } select { case r.queue <- event: default: r.log.Error( nil, "cannot enqueue labeled event: queue is full", "reason", e.Reason(), "action", e.Action(), "type", e.EventType(), "regarding", regardingRef.Name, "namespace", namespace, ) } } func (r *eventRecorder) run() { for event := range r.queue { ctx, cancel := context.WithTimeout(context.Background(), eventCreateTimeout) err := r.client.Create(ctx, event) cancel() if err == nil { continue } r.log.Error( err, "cannot emit labeled event", "reason", event.Reason, "action", event.Action, "type", event.Type, "regarding", event.Regarding.Name, "namespace", event.Namespace, ) } } func objectReference(obj runtime.Object) (corev1.ObjectReference, metav1.Object, error) { if obj == nil { return corev1.ObjectReference{}, nil, fmt.Errorf("object is nil") } metaObj, ok := obj.(metav1.Object) if !ok { return corev1.ObjectReference{}, nil, fmt.Errorf("%T does not implement metav1.Object", obj) } gvk := obj.GetObjectKind().GroupVersionKind() return corev1.ObjectReference{ APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind, Namespace: metaObj.GetNamespace(), Name: metaObj.GetName(), UID: metaObj.GetUID(), }, metaObj, nil }