Skip to content

Commit e4b5b79

Browse files
Use actual bracketing times to present duration. (#1033)
* Use actual bracketing times to present duration. * Put in a comment on what to do if there's no status.completionTime * Add unit tests for the duration calculator code.
1 parent e0180e2 commit e4b5b79

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

pkg/resources/common/formatter.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,14 @@ func convertMetadataTimestampFields(request *types.APIRequest, gvk schema2.Group
323323
if !hasCRDDateField && !hasGVKDateFieldMapping {
324324
continue
325325
}
326+
if col.Name == "Duration" {
327+
humanDuration, ok := getHumanDurationFromActualStartEndTime(unstr)
328+
if ok {
329+
curValue[index] = humanDuration
330+
changedFields = true
331+
continue
332+
}
333+
}
326334

327335
timeValue, ok := curValue[index].(string)
328336
if !ok {
@@ -363,6 +371,34 @@ func convertMetadataTimestampFields(request *types.APIRequest, gvk schema2.Group
363371
}
364372
}
365373

374+
func getHumanDurationFromActualStartEndTime(unstr *unstructured.Unstructured) (string, bool) {
375+
startTime, found, err := unstructured.NestedString(unstr.Object, "status", "startTime")
376+
if !found || err != nil {
377+
return "", false
378+
}
379+
endTime, found, err := unstructured.NestedString(unstr.Object, "status", "completionTime")
380+
if !found || err != nil {
381+
// This is deliberate -- if there's no completionTime,
382+
// Duration should be the same as Age, which is what will happen in the fallback code.
383+
return "", false
384+
}
385+
startTimeRFC, err := time.Parse(time.RFC3339, startTime)
386+
if err != nil {
387+
return "", false
388+
}
389+
endTimeRFC, err := time.Parse(time.RFC3339, endTime)
390+
if err != nil {
391+
return "", false
392+
}
393+
durationTime := endTimeRFC.Sub(startTimeRFC)
394+
humanDuration := duration.HumanDuration(durationTime)
395+
if humanDuration == "<invalid>" {
396+
logrus.Warnf(`couldn't convert value %v into human duration for "Duration" column`, durationTime)
397+
return "", false
398+
}
399+
return humanDuration, true
400+
}
401+
366402
func isDuration(value string) (time.Duration, bool) {
367403
d, err := ParseTimestampOrHumanReadableDuration(value)
368404
return d, err == nil

pkg/resources/common/formatter_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,3 +1363,170 @@ func TestFormatterAddsResourcePermissions(t *testing.T) {
13631363
})
13641364
}
13651365
}
1366+
1367+
func Test_getHumanDurationFromActualStartEndTime(t *testing.T) {
1368+
tests := []struct {
1369+
name string
1370+
unstr *unstructured.Unstructured
1371+
wantStatus bool
1372+
wantNewTime string
1373+
}{
1374+
{
1375+
name: "calculate duration from start and end time",
1376+
unstr: &unstructured.Unstructured{
1377+
Object: map[string]interface{}{
1378+
"metadata": map[string]interface{}{
1379+
"creationTimestamp": "2022-04-11T22:05:27Z",
1380+
"fields": []string{
1381+
"item1",
1382+
"Complete",
1383+
"1/1",
1384+
"63s",
1385+
"63s",
1386+
"item1",
1387+
},
1388+
"name": "item1",
1389+
},
1390+
"status": map[string]interface{}{
1391+
"startTime": "2026-02-25T22:54:00Z",
1392+
"completionTime": "2026-02-25T22:54:05Z",
1393+
},
1394+
},
1395+
},
1396+
wantStatus: true,
1397+
wantNewTime: "5s",
1398+
},
1399+
{
1400+
name: "fallback if no completion time is given",
1401+
unstr: &unstructured.Unstructured{
1402+
Object: map[string]interface{}{
1403+
"metadata": map[string]interface{}{
1404+
"creationTimestamp": "2022-04-11T22:05:27Z",
1405+
"fields": []string{
1406+
"item1",
1407+
"Complete",
1408+
"1/1",
1409+
"64s",
1410+
"64s",
1411+
"item1",
1412+
},
1413+
"name": "item1",
1414+
},
1415+
"status": map[string]interface{}{
1416+
"startTime": "2026-02-25T22:54:00Z",
1417+
},
1418+
},
1419+
},
1420+
wantStatus: false,
1421+
wantNewTime: "",
1422+
},
1423+
{
1424+
name: "fallback if no start time is given",
1425+
unstr: &unstructured.Unstructured{
1426+
Object: map[string]interface{}{
1427+
"metadata": map[string]interface{}{
1428+
"creationTimestamp": "2022-04-11T22:05:27Z",
1429+
"fields": []string{
1430+
"item1",
1431+
"Complete",
1432+
"1/1",
1433+
"64s",
1434+
"64s",
1435+
"item1",
1436+
},
1437+
"name": "item1",
1438+
},
1439+
},
1440+
},
1441+
wantStatus: false,
1442+
wantNewTime: "",
1443+
},
1444+
{
1445+
name: "fallback if no completion time is given",
1446+
unstr: &unstructured.Unstructured{
1447+
Object: map[string]interface{}{
1448+
"metadata": map[string]interface{}{
1449+
"creationTimestamp": "2022-04-11T22:05:27Z",
1450+
"fields": []string{
1451+
"item1",
1452+
"Complete",
1453+
"1/1",
1454+
"64s",
1455+
"64s",
1456+
"item1",
1457+
},
1458+
"name": "item1",
1459+
},
1460+
"status": map[string]interface{}{
1461+
"startTime": "2026-02-25T22:54:00Z",
1462+
},
1463+
},
1464+
},
1465+
wantStatus: false,
1466+
wantNewTime: "",
1467+
},
1468+
{
1469+
name: "fallback if start time isn't processable",
1470+
unstr: &unstructured.Unstructured{
1471+
Object: map[string]interface{}{
1472+
"metadata": map[string]interface{}{
1473+
"creationTimestamp": "2022-04-11T22:05:27Z",
1474+
"fields": []string{
1475+
"item1",
1476+
"Complete",
1477+
"1/1",
1478+
"64s",
1479+
"64s",
1480+
"item1",
1481+
},
1482+
"name": "item1",
1483+
},
1484+
"status": map[string]interface{}{
1485+
"startTime": "Midnight!",
1486+
},
1487+
},
1488+
},
1489+
wantStatus: false,
1490+
wantNewTime: "",
1491+
},
1492+
{
1493+
name: "fallback if end time isn't processable",
1494+
unstr: &unstructured.Unstructured{
1495+
Object: map[string]interface{}{
1496+
"metadata": map[string]interface{}{
1497+
"creationTimestamp": "2022-04-11T22:05:27Z",
1498+
"fields": []string{
1499+
"item1",
1500+
"Complete",
1501+
"1/1",
1502+
"64s",
1503+
"64s",
1504+
"item1",
1505+
},
1506+
"name": "item1",
1507+
},
1508+
"status": map[string]interface{}{
1509+
"completionTime": "Midnight!",
1510+
},
1511+
},
1512+
},
1513+
wantStatus: false,
1514+
wantNewTime: "",
1515+
},
1516+
}
1517+
1518+
for _, test := range tests {
1519+
t.Run(test.name, func(t *testing.T) {
1520+
t.Parallel()
1521+
duration, useDuration := getHumanDurationFromActualStartEndTime(test.unstr)
1522+
if test.wantStatus {
1523+
require.Equal(t, test.wantNewTime, duration)
1524+
require.True(t, useDuration)
1525+
} else {
1526+
require.Equal(t, "", duration)
1527+
require.False(t, useDuration)
1528+
}
1529+
})
1530+
}
1531+
1532+
}

0 commit comments

Comments
 (0)