From a4b8261ffefe1c46e6e131a84a9b2f49a4bb2997 Mon Sep 17 00:00:00 2001 From: Pranjal Date: Sat, 22 Aug 2026 00:42:58 +0530 Subject: [PATCH] fix(cli): show n/a for expiration on stalled New backups (#10326) Backups stuck in New state have no Status.Expiration yet. The CLI was estimating expiration from CreationTimestamp + TTL, which made long-queued backups appear already expired. Fixes #3555 Signed-off-by: PranjalManhgaye --- pkg/cmd/util/output/backup_printer.go | 8 ++++++-- pkg/cmd/util/output/printer_timestamp_test.go | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/util/output/backup_printer.go b/pkg/cmd/util/output/backup_printer.go index 53a950828..420b73129 100644 --- a/pkg/cmd/util/output/backup_printer.go +++ b/pkg/cmd/util/output/backup_printer.go @@ -90,8 +90,12 @@ func printBackup(backup *velerov1api.Backup) []metav1.TableRow { if backup.Status.Expiration != nil { expiration = backup.Status.Expiration.Time } - if expiration.IsZero() && backup.Spec.TTL.Duration > 0 { - expiration = backup.CreationTimestamp.Add(backup.Spec.TTL.Duration) + // Only estimate expiration from TTL after the backup has started. Backups + // stalled in New have no Status.Expiration yet; using CreationTimestamp + // would incorrectly show them as already expired (issue #3555). + if expiration.IsZero() && backup.Spec.TTL.Duration > 0 && + backup.Status.StartTimestamp != nil && !backup.Status.StartTimestamp.Time.IsZero() { + expiration = backup.Status.StartTimestamp.Time.Add(backup.Spec.TTL.Duration) } status := string(backup.Status.Phase) diff --git a/pkg/cmd/util/output/printer_timestamp_test.go b/pkg/cmd/util/output/printer_timestamp_test.go index f967b3b3f..e38d487c4 100644 --- a/pkg/cmd/util/output/printer_timestamp_test.go +++ b/pkg/cmd/util/output/printer_timestamp_test.go @@ -76,6 +76,26 @@ func TestPrintBackupWithoutStartTimestamp(t *testing.T) { assert.Equal(t, string(velerov1api.BackupPhaseFailedValidation), rows[0].Cells[1]) } +func TestPrintBackupExpiresForStalledNewBackup(t *testing.T) { + created := metav1.NewTime(time.Now().Add(-20 * 24 * time.Hour)) + backup := &velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterstate-20210128123759", + CreationTimestamp: created, + }, + Spec: velerov1api.BackupSpec{ + TTL: metav1.Duration{Duration: 10 * 24 * time.Hour}, + }, + Status: velerov1api.BackupStatus{ + Phase: velerov1api.BackupPhaseNew, + }, + } + + rows := printBackup(backup) + require.Len(t, rows, 1) + assert.Equal(t, "n/a", rows[0].Cells[5], "stalled New backup should not show expiration in the past") +} + func TestPrintBackupWithStartTimestamp(t *testing.T) { started := metav1.NewTime(time.Date(2026, 8, 8, 21, 6, 28, 0, time.UTC)) backup := &velerov1api.Backup{