/* Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package events import ( "fmt" "strings" "testing" "time" "github.com/google/go-cmp/cmp" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" eventsv1 "k8s.io/api/events/v1" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/watch" testclocks "k8s.io/utils/clock/testing" ) func TestEventf(t *testing.T) { // use a fixed time for generated names that depend on the unix timestamp fakeClock := testclocks.NewFakeClock(time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC)) testCases := []struct { desc string regarding runtime.Object related runtime.Object eventtype string reason string action string note string args []interface{} expectedEvent *eventsv1.Event }{ { desc: "normal event", regarding: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod1", Namespace: "ns1", UID: "12345", }, }, eventtype: "Normal", reason: "Started", action: "starting", note: "Pod started", expectedEvent: &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("pod1.%x", fakeClock.Now().UnixNano()), Namespace: "ns1", }, Regarding: v1.ObjectReference{ Kind: "Pod", Name: "pod1", Namespace: "ns1", UID: "12345", APIVersion: "v1", }, Type: "Normal", Reason: "Started", Action: "starting", Note: "Pod started", ReportingController: "c1", ReportingInstance: "i1", }, }, { desc: "event with related object and format args", regarding: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod1", Namespace: "ns1", UID: "12345", }, }, related: &v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", UID: "67890", }, }, eventtype: "Warning", reason: "FailedScheduling", action: "scheduling", note: "Pod failed to schedule on %s: %s", args: []interface{}{"node1", "not enough resources"}, expectedEvent: &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("pod1.%x", fakeClock.Now().UnixNano()), Namespace: "ns1", }, Regarding: v1.ObjectReference{ Kind: "Pod", Name: "pod1", Namespace: "ns1", UID: "12345", APIVersion: "v1", }, Related: &v1.ObjectReference{ Kind: "Node", Name: "node1", UID: "67890", APIVersion: "v1", }, Type: "Warning", Reason: "FailedScheduling", Action: "scheduling", Note: "Pod failed to schedule on node1: not enough resources", ReportingController: "c1", ReportingInstance: "i1", }, }, { desc: "event with invalid Event name", regarding: &networkingv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "2001:db8::123", UID: "12345", }, }, eventtype: "Warning", reason: "IPAddressNotAllocated", action: "IPAddressAllocation", note: "Service default/test appears to have leaked", expectedEvent: &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", }, Regarding: v1.ObjectReference{ Kind: "IPAddress", Name: "2001:db8::123", UID: "12345", APIVersion: "networking.k8s.io/v1", }, Type: "Warning", Reason: "IPAddressNotAllocated", Action: "IPAddressAllocation", Note: "Service default/test appears to have leaked", ReportingController: "c1", ReportingInstance: "i1", }, }, { desc: "large event name", regarding: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: strings.Repeat("x", utilvalidation.DNS1123SubdomainMaxLength*4), Namespace: "ns1", UID: "12345", }, }, eventtype: "Normal", reason: "Started", action: "starting", note: "Pod started", expectedEvent: &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", }, Regarding: v1.ObjectReference{ Kind: "Pod", Name: strings.Repeat("x", utilvalidation.DNS1123SubdomainMaxLength*4), Namespace: "ns1", UID: "12345", APIVersion: "v1", }, Type: "Normal", Reason: "Started", Action: "starting", Note: "Pod started", ReportingController: "c1", ReportingInstance: "i1", }, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { broadcaster := watch.NewBroadcaster(1, watch.WaitIfChannelFull) recorder := &recorderImpl{ scheme: runtime.NewScheme(), reportingController: "c1", reportingInstance: "i1", Broadcaster: broadcaster, clock: fakeClock, } if err := v1.AddToScheme(recorder.scheme); err != nil { t.Fatal(err) } if err := networkingv1.AddToScheme(recorder.scheme); err != nil { t.Fatal(err) } ch, err := broadcaster.Watch() if err != nil { t.Fatal(err) } recorder.Eventf(tc.regarding, tc.related, tc.eventtype, tc.reason, tc.action, tc.note, tc.args...) select { case event := <-ch.ResultChan(): actualEvent := event.Object.(*eventsv1.Event) if errs := apimachineryvalidation.NameIsDNSSubdomain(actualEvent.Name, false); len(errs) > 0 { t.Errorf("Event Name = %s; not a valid name: %v", actualEvent.Name, errs) } // Overwrite fields that are not relevant for comparison tc.expectedEvent.EventTime = actualEvent.EventTime // invalid event names generate random names if tc.expectedEvent.Name == "" { actualEvent.Name = "" } if diff := cmp.Diff(tc.expectedEvent, actualEvent); diff != "" { t.Errorf("Unexpected event diff (-want, +got):\n%s", diff) } case <-time.After(time.Second): t.Errorf("Timeout waiting for event") } }) } }