Skip to content

Commit 052d7a5

Browse files
authored
Merge pull request kubernetes#129790 from aojea/event_name
events: ensure the name is valid
2 parents 6be1530 + ee36b81 commit 052d7a5

File tree

5 files changed

+331
-2
lines changed

5 files changed

+331
-2
lines changed

staging/src/k8s.io/client-go/tools/events/event_recorder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (recorder *recorderImpl) makeEvent(refRegarding *v1.ObjectReference, refRel
9696
}
9797
return &eventsv1.Event{
9898
ObjectMeta: metav1.ObjectMeta{
99-
Name: fmt.Sprintf("%v.%x", refRegarding.Name, t.UnixNano()),
99+
Name: util.GenerateEventName(refRegarding.Name, t.UnixNano()),
100100
Namespace: namespace,
101101
},
102102
EventTime: timestamp,
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package events
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"testing"
23+
"time"
24+
25+
"github.com/google/go-cmp/cmp"
26+
27+
v1 "k8s.io/api/core/v1"
28+
networkingv1 "k8s.io/api/networking/v1"
29+
30+
eventsv1 "k8s.io/api/events/v1"
31+
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
35+
"k8s.io/apimachinery/pkg/watch"
36+
testclocks "k8s.io/utils/clock/testing"
37+
)
38+
39+
func TestEventf(t *testing.T) {
40+
// use a fixed time for generated names that depend on the unix timestamp
41+
fakeClock := testclocks.NewFakeClock(time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC))
42+
43+
testCases := []struct {
44+
desc string
45+
regarding runtime.Object
46+
related runtime.Object
47+
eventtype string
48+
reason string
49+
action string
50+
note string
51+
args []interface{}
52+
expectedEvent *eventsv1.Event
53+
}{
54+
{
55+
desc: "normal event",
56+
regarding: &v1.Pod{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: "pod1",
59+
Namespace: "ns1",
60+
UID: "12345",
61+
},
62+
},
63+
eventtype: "Normal",
64+
reason: "Started",
65+
action: "starting",
66+
note: "Pod started",
67+
expectedEvent: &eventsv1.Event{
68+
ObjectMeta: metav1.ObjectMeta{
69+
Name: fmt.Sprintf("pod1.%x", fakeClock.Now().UnixNano()),
70+
Namespace: "ns1",
71+
},
72+
Regarding: v1.ObjectReference{
73+
Kind: "Pod",
74+
Name: "pod1",
75+
Namespace: "ns1",
76+
UID: "12345",
77+
APIVersion: "v1",
78+
},
79+
Type: "Normal",
80+
Reason: "Started",
81+
Action: "starting",
82+
Note: "Pod started",
83+
ReportingController: "c1",
84+
ReportingInstance: "i1",
85+
},
86+
},
87+
{
88+
desc: "event with related object and format args",
89+
regarding: &v1.Pod{
90+
ObjectMeta: metav1.ObjectMeta{
91+
Name: "pod1",
92+
Namespace: "ns1",
93+
UID: "12345",
94+
},
95+
},
96+
related: &v1.Node{
97+
ObjectMeta: metav1.ObjectMeta{
98+
Name: "node1",
99+
UID: "67890",
100+
},
101+
},
102+
eventtype: "Warning",
103+
reason: "FailedScheduling",
104+
action: "scheduling",
105+
106+
note: "Pod failed to schedule on %s: %s",
107+
args: []interface{}{"node1", "not enough resources"},
108+
expectedEvent: &eventsv1.Event{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Name: fmt.Sprintf("pod1.%x", fakeClock.Now().UnixNano()),
111+
Namespace: "ns1",
112+
},
113+
Regarding: v1.ObjectReference{
114+
Kind: "Pod",
115+
Name: "pod1",
116+
Namespace: "ns1",
117+
UID: "12345",
118+
APIVersion: "v1",
119+
},
120+
Related: &v1.ObjectReference{
121+
Kind: "Node",
122+
Name: "node1",
123+
UID: "67890",
124+
APIVersion: "v1",
125+
},
126+
Type: "Warning",
127+
Reason: "FailedScheduling",
128+
Action: "scheduling",
129+
Note: "Pod failed to schedule on node1: not enough resources",
130+
ReportingController: "c1",
131+
ReportingInstance: "i1",
132+
},
133+
}, {
134+
desc: "event with invalid Event name",
135+
regarding: &networkingv1.IPAddress{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Name: "2001:db8::123",
138+
UID: "12345",
139+
},
140+
},
141+
eventtype: "Warning",
142+
reason: "IPAddressNotAllocated",
143+
action: "IPAddressAllocation",
144+
note: "Service default/test appears to have leaked",
145+
146+
expectedEvent: &eventsv1.Event{
147+
ObjectMeta: metav1.ObjectMeta{
148+
Namespace: "default",
149+
},
150+
Regarding: v1.ObjectReference{
151+
Kind: "IPAddress",
152+
Name: "2001:db8::123",
153+
UID: "12345",
154+
APIVersion: "networking.k8s.io/v1",
155+
},
156+
Type: "Warning",
157+
Reason: "IPAddressNotAllocated",
158+
Action: "IPAddressAllocation",
159+
Note: "Service default/test appears to have leaked",
160+
ReportingController: "c1",
161+
ReportingInstance: "i1",
162+
},
163+
}, {
164+
desc: "large event name",
165+
regarding: &v1.Pod{
166+
ObjectMeta: metav1.ObjectMeta{
167+
Name: strings.Repeat("x", utilvalidation.DNS1123SubdomainMaxLength*4),
168+
Namespace: "ns1",
169+
UID: "12345",
170+
},
171+
},
172+
eventtype: "Normal",
173+
reason: "Started",
174+
action: "starting",
175+
note: "Pod started",
176+
expectedEvent: &eventsv1.Event{
177+
ObjectMeta: metav1.ObjectMeta{
178+
Namespace: "ns1",
179+
},
180+
Regarding: v1.ObjectReference{
181+
Kind: "Pod",
182+
Name: strings.Repeat("x", utilvalidation.DNS1123SubdomainMaxLength*4),
183+
Namespace: "ns1",
184+
UID: "12345",
185+
APIVersion: "v1",
186+
},
187+
Type: "Normal",
188+
Reason: "Started",
189+
Action: "starting",
190+
Note: "Pod started",
191+
ReportingController: "c1",
192+
ReportingInstance: "i1",
193+
},
194+
},
195+
}
196+
197+
for _, tc := range testCases {
198+
t.Run(tc.desc, func(t *testing.T) {
199+
broadcaster := watch.NewBroadcaster(1, watch.WaitIfChannelFull)
200+
recorder := &recorderImpl{
201+
scheme: runtime.NewScheme(),
202+
reportingController: "c1",
203+
reportingInstance: "i1",
204+
Broadcaster: broadcaster,
205+
clock: fakeClock,
206+
}
207+
208+
if err := v1.AddToScheme(recorder.scheme); err != nil {
209+
t.Fatal(err)
210+
}
211+
if err := networkingv1.AddToScheme(recorder.scheme); err != nil {
212+
t.Fatal(err)
213+
}
214+
ch, err := broadcaster.Watch()
215+
if err != nil {
216+
t.Fatal(err)
217+
}
218+
recorder.Eventf(tc.regarding, tc.related, tc.eventtype, tc.reason, tc.action, tc.note, tc.args...)
219+
220+
select {
221+
case event := <-ch.ResultChan():
222+
actualEvent := event.Object.(*eventsv1.Event)
223+
if errs := apimachineryvalidation.NameIsDNSSubdomain(actualEvent.Name, false); len(errs) > 0 {
224+
t.Errorf("Event Name = %s; not a valid name: %v", actualEvent.Name, errs)
225+
} // Overwrite fields that are not relevant for comparison
226+
tc.expectedEvent.EventTime = actualEvent.EventTime
227+
// invalid event names generate random names
228+
if tc.expectedEvent.Name == "" {
229+
actualEvent.Name = ""
230+
}
231+
if diff := cmp.Diff(tc.expectedEvent, actualEvent); diff != "" {
232+
t.Errorf("Unexpected event diff (-want, +got):\n%s", diff)
233+
}
234+
case <-time.After(time.Second):
235+
t.Errorf("Timeout waiting for event")
236+
}
237+
238+
})
239+
}
240+
}

staging/src/k8s.io/client-go/tools/record/event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ func (recorder *recorderImpl) makeEvent(ref *v1.ObjectReference, annotations map
489489
}
490490
return &v1.Event{
491491
ObjectMeta: metav1.ObjectMeta{
492-
Name: fmt.Sprintf("%v.%x", ref.Name, t.UnixNano()),
492+
Name: util.GenerateEventName(ref.Name, t.UnixNano()),
493493
Namespace: namespace,
494494
Annotations: annotations,
495495
},

staging/src/k8s.io/client-go/tools/record/util/util.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ limitations under the License.
1717
package util
1818

1919
import (
20+
"fmt"
2021
"net/http"
2122

23+
"github.com/google/uuid"
24+
2225
v1 "k8s.io/api/core/v1"
2326
"k8s.io/apimachinery/pkg/api/errors"
27+
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
2428
)
2529

2630
// ValidateEventType checks that eventtype is an expected type of event
@@ -38,3 +42,16 @@ func IsKeyNotFoundError(err error) bool {
3842

3943
return statusErr != nil && statusErr.Status().Code == http.StatusNotFound
4044
}
45+
46+
// GenerateEventName generates a valid Event name from the referenced name and the passed UNIX timestamp.
47+
// The referenced Object name may not be a valid name for Events and cause the Event to fail
48+
// to be created, so we need to generate a new one in that case.
49+
// Ref: https://issues.k8s.io/127594
50+
func GenerateEventName(refName string, unixNano int64) string {
51+
name := fmt.Sprintf("%s.%x", refName, unixNano)
52+
if errs := apimachineryvalidation.NameIsDNSSubdomain(name, false); len(errs) > 0 {
53+
// Using an uuid guarantees uniqueness and correctness
54+
name = uuid.New().String()
55+
}
56+
return name
57+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
24+
)
25+
26+
func TestGenerateEventName(t *testing.T) {
27+
timestamp := int64(105999103295324396)
28+
testCases := []struct {
29+
name string
30+
refName string
31+
expected string
32+
}{
33+
{
34+
name: "valid name",
35+
refName: "test-pod",
36+
expected: "test-pod.178959f726d80ec",
37+
},
38+
{
39+
name: "invalid name - too long",
40+
refName: strings.Repeat("x", 300),
41+
},
42+
{
43+
name: "invalid name - upper case",
44+
refName: "test.POD",
45+
},
46+
{
47+
name: "invalid name - special chars",
48+
refName: "test.pod/invalid!chars?",
49+
},
50+
{
51+
name: "invalid name - special chars and non alphanumeric starting character",
52+
refName: "--test.pod/invalid!chars?",
53+
},
54+
}
55+
56+
for _, tc := range testCases {
57+
t.Run(tc.name, func(t *testing.T) {
58+
actual := GenerateEventName(tc.refName, timestamp)
59+
60+
if errs := apimachineryvalidation.NameIsDNSSubdomain(actual, false); len(errs) > 0 {
61+
t.Errorf("generateEventName(%s) = %s; not a valid name: %v", tc.refName, actual, errs)
62+
63+
}
64+
65+
if tc.expected != "" && (actual != tc.expected) {
66+
t.Errorf("generateEventName(%s) returned %s expected %s", tc.refName, actual, tc.expected)
67+
}
68+
69+
})
70+
71+
}
72+
}

0 commit comments

Comments
 (0)