Skip to content

Commit 55a1a53

Browse files
gehrkefctomleb
andauthored
[v2.13] #53604 - VAI: Pod restart field returns stale value (#1035)
* Draft * added unit test to ParseRestarts * added integration tests * added tests and more fixes * removed unecessary comment --------- Co-authored-by: Tom Lebreux <tom.lebreux@suse.com>
1 parent 0a6118a commit 55a1a53

File tree

8 files changed

+482
-4
lines changed

8 files changed

+482
-4
lines changed

pkg/resources/formatters/formatter.go

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"compress/gzip"
66
"encoding/base64"
77
"encoding/json"
8+
"fmt"
89
"io"
10+
"time"
911

1012
// helm v2 is long since deprecated
1113
// Unlike helm v3, it uses Protobuf encoding, so we can't use generic decoding without the message descriptors.
@@ -16,8 +18,10 @@ import (
1618
"github.com/pkg/errors"
1719
"github.com/rancher/apiserver/pkg/types"
1820
"github.com/rancher/norman/types/convert"
21+
rescommon "github.com/rancher/steve/pkg/resources/common"
1922
"github.com/rancher/wrangler/v3/pkg/data"
2023
"github.com/sirupsen/logrus"
24+
"k8s.io/apimachinery/pkg/util/duration"
2125
)
2226

2327
var (
@@ -61,11 +65,82 @@ func HandleHelmData(request *types.APIRequest, resource *types.RawResource) {
6165
}
6266

6367
func Pod(_ *types.APIRequest, resource *types.RawResource) {
64-
data := resource.APIObject.Data()
65-
fields := data.StringSlice("metadata", "fields")
68+
objData := resource.APIObject.Data()
69+
fields := objData.StringSlice("metadata", "fields")
6670
if len(fields) > 2 {
67-
data.SetNested(convert.LowerTitle(fields[2]), "metadata", "state", "name")
71+
objData.SetNested(convert.LowerTitle(fields[2]), "metadata", "state", "name")
72+
}
73+
74+
if resource.Schema == nil {
75+
return
6876
}
77+
78+
cols := rescommon.GetColumnDefinitions(resource.Schema)
79+
for _, col := range cols {
80+
if col.Name != "Restarts" {
81+
continue
82+
}
83+
84+
index := rescommon.GetIndexValueFromString(col.Field)
85+
if index == -1 {
86+
continue
87+
}
88+
89+
rawFieldsRaw, ok := data.GetValue(objData, "metadata", "fields")
90+
if !ok {
91+
continue
92+
}
93+
94+
rawFields, ok := rawFieldsRaw.([]interface{})
95+
if !ok || index >= len(rawFields) {
96+
continue
97+
}
98+
99+
valMap, ok := rawFields[index].(map[string]interface{})
100+
if !ok {
101+
continue
102+
}
103+
104+
rawFields[index] = FormatRestarts(valMap)
105+
objData.SetNested(rawFields, "metadata", "fields")
106+
}
107+
}
108+
109+
// FormatRestarts formats a restart map as a display string.
110+
func FormatRestarts(valMap map[string]interface{}) string {
111+
var count int64
112+
var timestamp int64
113+
114+
if c, ok := valMap["count"]; ok {
115+
switch v := c.(type) {
116+
case int64:
117+
count = v
118+
case int:
119+
count = int64(v)
120+
case float64:
121+
count = int64(v)
122+
}
123+
}
124+
125+
if t, ok := valMap["timestamp"]; ok {
126+
switch v := t.(type) {
127+
case int64:
128+
timestamp = v
129+
case int:
130+
timestamp = int64(v)
131+
case float64:
132+
timestamp = int64(v)
133+
}
134+
}
135+
136+
if timestamp == 0 {
137+
return fmt.Sprintf("%d", count)
138+
}
139+
140+
t := time.UnixMilli(timestamp)
141+
dur := time.Since(t)
142+
humanDur := duration.HumanDuration(dur)
143+
return fmt.Sprintf("%d (%s ago)", count, humanDur)
69144
}
70145

71146
// decodeHelm3 receives a helm3 release data string, decodes the string data using the standard base64 library

pkg/resources/virtual/virtual.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ package virtual
44

55
import (
66
"fmt"
7+
"regexp"
78
"slices"
9+
"strconv"
810
"time"
911

1012
rescommon "github.com/rancher/steve/pkg/resources/common"
@@ -20,6 +22,27 @@ import (
2022

2123
var now = time.Now
2224

25+
var restartsPattern = regexp.MustCompile(`^(\d+)(?:\s+\((.+?)\s+ago\))?`)
26+
27+
// ParseRestarts parses pod restart values like "4 (3h38m ago)" into a map
28+
func ParseRestarts(value string) (map[string]any, error) {
29+
matches := restartsPattern.FindStringSubmatch(value)
30+
if matches == nil {
31+
return nil, fmt.Errorf("invalid restarts format: %q", value)
32+
}
33+
count, _ := strconv.ParseInt(matches[1], 10, 64)
34+
var timestamp int64
35+
if matches[2] != "" {
36+
dur, err := rescommon.ParseTimestampOrHumanReadableDuration(matches[2])
37+
if err != nil {
38+
logrus.Errorf("failed to parse restart duration %q: %v", matches[2], err)
39+
} else {
40+
timestamp = now().Add(-dur).UnixMilli()
41+
}
42+
}
43+
return map[string]any{"count": count, "timestamp": timestamp}, nil
44+
}
45+
2346
// TransformBuilder builds transform functions for specified GVKs through GetTransformFunc
2447
type TransformBuilder struct {
2548
defaultFields *common.DefaultFields
@@ -43,6 +66,40 @@ func (t *TransformBuilder) GetTransformFunc(gvk schema.GroupVersionKind, columns
4366
converters = append(converters, clusters.TransformManagedCluster)
4467
}
4568

69+
// Pod Logic
70+
if gvk.Kind == "Pod" && gvk.Version == "v1" {
71+
for _, col := range columns {
72+
if col.Name == "Restarts" {
73+
converters = append(converters, func(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
74+
index := rescommon.GetIndexValueFromString(col.Field)
75+
if index == -1 {
76+
return obj, nil
77+
}
78+
79+
fields, found, err := unstructured.NestedSlice(obj.Object, "metadata", "fields")
80+
if err != nil || !found || index >= len(fields) {
81+
return obj, nil
82+
}
83+
84+
val, ok := fields[index].(string)
85+
if !ok {
86+
return obj, nil
87+
}
88+
89+
parsed, err := ParseRestarts(val)
90+
if err != nil {
91+
logrus.Warnf("Failed to parse restarts: %v", err)
92+
return obj, nil
93+
}
94+
95+
fields[index] = parsed
96+
unstructured.SetNestedSlice(obj.Object, fields, "metadata", "fields")
97+
return obj, nil
98+
})
99+
}
100+
}
101+
}
102+
46103
// Detecting if we need to convert date fields
47104
for _, col := range columns {
48105
gvkDateFields, gvkFound := rescommon.DateFieldsByGVK[gvk]

pkg/resources/virtual/virtual_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,77 @@ import (
1010
"github.com/rancher/steve/pkg/resources/virtual/common"
1111
"github.com/rancher/steve/pkg/summarycache"
1212
"github.com/rancher/wrangler/v3/pkg/summary"
13+
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
1415
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1617
"k8s.io/apimachinery/pkg/runtime/schema"
1718
)
1819

20+
func TestParseRestarts(t *testing.T) {
21+
// Mock Now for deterministic tests
22+
fixedNow := time.Unix(1737462000, 0) // 2026-01-21 13:00:00 UTC
23+
now = func() time.Time {
24+
return fixedNow
25+
}
26+
defer func() { now = time.Now }()
27+
28+
tests := []struct {
29+
name string
30+
input string
31+
wantCount int
32+
wantTimestamp int64 // nil means no timestamp expected
33+
wantErr bool
34+
}{
35+
{
36+
name: "no restarts",
37+
input: "0",
38+
wantCount: 0,
39+
wantTimestamp: 0,
40+
},
41+
{
42+
name: "single restart with time (3h37m ago)",
43+
input: "1 (3h37m ago)",
44+
wantCount: 1,
45+
wantTimestamp: fixedNow.Add(-3*time.Hour - 37*time.Minute).UnixMilli(),
46+
},
47+
{
48+
name: "multiple restarts with time (3h38m ago)",
49+
input: "4 (3h38m ago)",
50+
wantCount: 4,
51+
wantTimestamp: fixedNow.Add(-3*time.Hour - 38*time.Minute).UnixMilli(),
52+
},
53+
{
54+
name: "invalid format",
55+
input: "invalid",
56+
wantErr: true,
57+
},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
result, err := ParseRestarts(tt.input)
63+
64+
if tt.wantErr {
65+
assert.Error(t, err)
66+
return
67+
}
68+
69+
require.NoError(t, err)
70+
require.Len(t, result, 2)
71+
72+
count, ok := result["count"].(int64)
73+
require.True(t, ok)
74+
assert.Equal(t, int64(tt.wantCount), count)
75+
76+
require.NotNil(t, result["timestamp"])
77+
timestamp, ok := result["timestamp"].(int64)
78+
require.True(t, ok)
79+
assert.Equal(t, tt.wantTimestamp, timestamp)
80+
})
81+
}
82+
}
83+
1984
func TestTransformChain(t *testing.T) {
2085
now = func() time.Time { return time.Date(1992, 9, 2, 0, 0, 0, 0, time.UTC) }
2186
noColumns := []rescommon.ColumnDefinition{}

pkg/sqlcache/informer/listoption_indexer.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@ func (l *ListOptionIndexer) decryptScanEvent(rows db.Rows, into runtime.Object)
418418
}
419419
if err := l.Deserialize(serialized, into); err != nil {
420420
return watch.Error, err
421-
422421
}
423422
return watch.EventType(typ), nil
424423
}
@@ -630,6 +629,31 @@ func (l *ListOptionIndexer) addIndexFields(key string, obj any, tx db.TxClient)
630629
args = append(args, fmt.Sprint(typedValue))
631630
case []string:
632631
args = append(args, strings.Join(typedValue, "|"))
632+
case map[string]any:
633+
isPod := false
634+
if u, ok := obj.(*unstructured.Unstructured); ok {
635+
isPod = u.GroupVersionKind().Kind == "Pod"
636+
}
637+
if isPod {
638+
// The pod restart transform func will split pod restart column (eg: `X (D ago)`)
639+
// into two fields. We want to sort / filter on count.
640+
if count, ok := typedValue["count"]; ok {
641+
switch c := count.(type) {
642+
case int64:
643+
args = append(args, c)
644+
case int:
645+
args = append(args, fmt.Sprint(c))
646+
case float64:
647+
args = append(args, fmt.Sprint(int64(c)))
648+
default:
649+
args = append(args, "")
650+
}
651+
} else {
652+
args = append(args, "")
653+
}
654+
} else {
655+
args = append(args, "")
656+
}
633657
default:
634658
err2 := fmt.Errorf("field %v has a non-supported type value: %v", field, value)
635659
return err2

pkg/stores/sqlproxy/proxy_store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,9 @@ var TypeGuidanceTable = map[schema.GroupVersionKind]map[string]string{
925925
"status.requested.memoryRaw": "REAL",
926926
"status.requested.pods": "INT",
927927
},
928+
schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}: {
929+
"metadata.fields[3]": "INT", // name: Restarts
930+
},
928931
schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}: {
929932
"metadata.fields[2]": "INT", // name: Data
930933
},

0 commit comments

Comments
 (0)