Skip to content

Commit b2dacba

Browse files
authored
Merge pull request #30796 from dgoodwin/graceful-term-time
NO-JIRA: Fix lack of human readable timestamps in two tests output
2 parents 4939f12 + ead32df commit b2dacba

4 files changed

Lines changed: 126 additions & 3 deletions

File tree

test/extended/apiserver/graceful_termination.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"regexp"
99
"strings"
10+
"time"
1011

1112
g "github.com/onsi/ginkgo/v2"
1213
o "github.com/onsi/gomega"
@@ -60,6 +61,8 @@ var _ = g.Describe("[sig-api-machinery][Feature:APIServer][Late]", func() {
6061
g.It("kubelet terminates kube-apiserver gracefully extended", func() {
6162
var finalMessageBuilder strings.Builder
6263
terminationRegexp := regexp.MustCompile(`Previous pod .* did not terminate gracefully`)
64+
// klog timestamp format: W0120 22:20:50.473381
65+
klogTimestampRegexp := regexp.MustCompile(`^[IWEF](\d{4}) (\d{2}:\d{2}:\d{2}\.\d+)`)
6366

6467
masters, err := oc.AsAdmin().KubeClient().CoreV1().Nodes().List(context.Background(), metav1.ListOptions{LabelSelector: "node-role.kubernetes.io/master"})
6568
o.Expect(err).NotTo(o.HaveOccurred())
@@ -79,7 +82,8 @@ var _ = g.Describe("[sig-api-machinery][Feature:APIServer][Late]", func() {
7982
for kasTerminationFileScanner.Scan() {
8083
line := kasTerminationFileScanner.Text()
8184
if terminationRegexp.MatchString(line) {
82-
finalMessageBuilder.WriteString(fmt.Sprintf("\n kube-apiserver on node %s wasn't gracefully terminated, reason: %s", master.Name, line))
85+
observedAt := parseKlogTimestamp(line, klogTimestampRegexp)
86+
finalMessageBuilder.WriteString(fmt.Sprintf("\n kube-apiserver on node %s wasn't gracefully terminated (observed at %s), reason: %s", master.Name, observedAt, line))
8387
}
8488
}
8589
o.Expect(kasTerminationFileScanner.Err()).NotTo(o.HaveOccurred())
@@ -185,3 +189,20 @@ func extractAPIServerNameFromAuditFile(auditFileName string) string {
185189
func isKASTerminationLogFile(fileName string) bool {
186190
return strings.Contains(fileName, "termination")
187191
}
192+
193+
// parseKlogTimestamp extracts and formats the klog timestamp from a log line.
194+
// klog timestamps have the format: W0120 22:20:50.473381 (MMDD HH:MM:SS.ffffff without year).
195+
// We use the current year as klog does not include it.
196+
func parseKlogTimestamp(line string, klogTimestampRegexp *regexp.Regexp) string {
197+
matches := klogTimestampRegexp.FindStringSubmatch(line)
198+
if len(matches) < 3 {
199+
return "unknown"
200+
}
201+
// matches[1] = "0120" (MMDD), matches[2] = "22:20:50.473381"
202+
timestampStr := fmt.Sprintf("%d-%s-%s", time.Now().Year(), matches[1][:2]+"-"+matches[1][2:], matches[2])
203+
t, err := time.Parse("2006-01-02-15:04:05.000000", timestampStr)
204+
if err != nil {
205+
return matches[1] + " " + matches[2]
206+
}
207+
return t.Format(time.RFC3339)
208+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package apiserver
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestParseKlogTimestamp(t *testing.T) {
11+
klogTimestampRegexp := regexp.MustCompile(`^[IWEF](\d{4}) (\d{2}:\d{2}:\d{2}\.\d+)`)
12+
currentYear := time.Now().Year()
13+
14+
tests := []struct {
15+
name string
16+
line string
17+
expected string
18+
}{
19+
{
20+
name: "warning log line with graceful termination message",
21+
line: `W0120 22:20:50.473381 1 main.go:136] Previous pod kube-apiserver-ip-10-0-48-191.us-west-2.compute.internal started at 2026-01-20 22:01:27.176604246 +0000 UTC did not terminate gracefully`,
22+
expected: fmt.Sprintf("%d-01-20T22:20:50Z", currentYear),
23+
},
24+
{
25+
name: "info level log line",
26+
line: `I0315 10:05:30.123456 1 main.go:100] some info message`,
27+
expected: fmt.Sprintf("%d-03-15T10:05:30Z", currentYear),
28+
},
29+
{
30+
name: "error level log line",
31+
line: `E1231 23:59:59.999999 1 main.go:200] error message`,
32+
expected: fmt.Sprintf("%d-12-31T23:59:59Z", currentYear),
33+
},
34+
{
35+
name: "no klog prefix",
36+
line: `some random log line without a timestamp`,
37+
expected: "unknown",
38+
},
39+
{
40+
name: "empty line",
41+
line: ``,
42+
expected: "unknown",
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
result := parseKlogTimestamp(tt.line, klogTimestampRegexp)
49+
if result != tt.expected {
50+
t.Errorf("parseKlogTimestamp() = %q, want %q", result, tt.expected)
51+
}
52+
})
53+
}
54+
}

test/extended/machines/cluster.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ func (a byTime) Less(i, j int) bool {
244244
return a[i].time.Before(a[j].time)
245245
}
246246

247-
func (e *bootTimelineEntry) String() string {
248-
return fmt.Sprintf("%v - %v", e.time.String(), e.action)
247+
func (e bootTimelineEntry) String() string {
248+
return fmt.Sprintf("%v - %v", e.time.Format(time.RFC3339), e.action)
249249
}
250250

251251
func parseBootInstances(listBootsOutput string) ([]bootTimelineEntry, error) {

test/extended/machines/cluster_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package operators
22

33
import (
4+
"fmt"
45
"reflect"
56
"testing"
67
"time"
@@ -52,6 +53,53 @@ func Test_parseBootInstances(t *testing.T) {
5253
}
5354
}
5455

56+
func Test_bootTimelineEntryString(t *testing.T) {
57+
tests := []struct {
58+
name string
59+
entry bootTimelineEntry
60+
expected string
61+
}{
62+
{
63+
name: "boot entry formats time as RFC3339",
64+
entry: bootTimelineEntry{action: "Boot", time: mustTime("2026-01-20T22:20:50Z")},
65+
expected: "2026-01-20T22:20:50Z - Boot",
66+
},
67+
{
68+
name: "reboot request entry formats time as RFC3339",
69+
entry: bootTimelineEntry{action: "RebootRequest", time: mustTime("2024-03-13T10:20:01-04:00")},
70+
expected: "2024-03-13T10:20:01-04:00 - RebootRequest",
71+
},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
result := tt.entry.String()
76+
if result != tt.expected {
77+
t.Errorf("String() = %q, want %q", result, tt.expected)
78+
}
79+
})
80+
}
81+
}
82+
83+
func Test_bootTimelineSliceFormat(t *testing.T) {
84+
// Verify that formatting a []bootTimelineEntry with %v calls String() on each element,
85+
// producing human-readable timestamps instead of raw struct output.
86+
entries := []bootTimelineEntry{
87+
{action: "Boot", time: mustTime("2026-01-20T22:20:50Z")},
88+
{action: "RebootRequest", time: mustTime("2026-01-20T22:01:27Z")},
89+
}
90+
formatted := fmt.Sprintf("%v", entries)
91+
if formatted == fmt.Sprintf("%v", []struct {
92+
action string
93+
time time.Time
94+
}{{action: "Boot"}, {action: "RebootRequest"}}) {
95+
t.Error("slice formatting is not using String() method")
96+
}
97+
expected := "[2026-01-20T22:20:50Z - Boot 2026-01-20T22:01:27Z - RebootRequest]"
98+
if formatted != expected {
99+
t.Errorf("formatted slice = %q, want %q", formatted, expected)
100+
}
101+
}
102+
55103
func Test_parseRebootInstances(t *testing.T) {
56104
type args struct {
57105
rebootsOutput string

0 commit comments

Comments
 (0)