diff --git a/internal/backupcontroller/mariadbstrategy_controller_test.go b/internal/backupcontroller/mariadbstrategy_controller_test.go index 307301223..0c706574c 100644 --- a/internal/backupcontroller/mariadbstrategy_controller_test.go +++ b/internal/backupcontroller/mariadbstrategy_controller_test.go @@ -871,30 +871,64 @@ func TestReconcileMariaDBRestore_KindCheckedBeforeSrcBackupGet(t *testing.T) { // JobFailed (SetCompleteWithJob in mariadb-operator 25.10.2), so the // Complete status alone does not tell success from failure. var mariadbCompleteOutcomeCases = []struct { - name string - status metav1.ConditionStatus - reason string - wantFailed bool - wantDone bool + name string + noCondition bool + status metav1.ConditionStatus + reason string + message string + expired bool + wantFailed bool + wantDone bool + wantMessage []string }{ - {name: "Complete=True/JobComplete succeeds", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobComplete, wantDone: true}, - {name: "Complete=True/JobFailed fails", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobFailed, wantFailed: true}, - {name: "Complete=False/JobFailed fails", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobFailed, wantFailed: true}, - {name: "Complete=True with an unknown reason keeps waiting", status: metav1.ConditionTrue, reason: "SomethingElse"}, - {name: "Complete=False/JobComplete keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobComplete}, - {name: "Complete=False/JobRunning keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobRunning}, + {name: "Complete=True/JobComplete succeeds", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobComplete, message: "Success", wantDone: true}, + {name: "Complete=True/JobFailed fails", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobFailed, message: "Failed", wantFailed: true, + wantMessage: []string{"tenant/op"}}, + {name: "Complete=False/JobFailed fails", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobFailed, message: "Failed", wantFailed: true, + wantMessage: []string{"tenant/op"}}, + {name: "Complete=True with an unknown reason keeps waiting", status: metav1.ConditionTrue, reason: "SomethingElse", message: "Something"}, + {name: "Complete=False/JobComplete keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobComplete, message: "Success"}, + {name: "Complete=False/JobRunning keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobRunning, message: "Running"}, + {name: "no Complete condition keeps waiting", noCondition: true}, + {name: "no Complete condition past the deadline fails", noCondition: true, expired: true, wantFailed: true, + wantMessage: []string{"no Complete condition observed"}}, + {name: "Complete=True with an unknown reason past the deadline fails", status: metav1.ConditionTrue, reason: "SomethingElse", message: "Something", expired: true, wantFailed: true, + wantMessage: []string{"Complete=True/SomethingElse"}}, } -func mariadbCompleteCondition(status metav1.ConditionStatus, reason string) []metav1.Condition { +func mariadbCompleteConditions(noCondition bool, status metav1.ConditionStatus, reason, message string) []metav1.Condition { + if noCondition { + return nil + } return []metav1.Condition{{ Type: mariadbtypes.ConditionTypeComplete, Status: status, Reason: reason, - Message: "Failed", + Message: message, LastTransitionTime: metav1.Now(), }} } +// mariadbOutcomeStartedAt returns a StartedAt that is fresh, or older than +// both the backup and the default restore deadline. +func mariadbOutcomeStartedAt(expired bool) *metav1.Time { + if !expired { + now := metav1.Now() + return &now + } + past := metav1.NewTime(time.Now().Add(-2 * max(mariadbDefaultBackupDeadline, mariadbDefaultRestoreDeadline))) + return &past +} + +func assertMariaDBFailureMessage(t *testing.T, message string, want []string) { + t.Helper() + for _, w := range want { + if !strings.Contains(message, w) { + t.Errorf("failure message must contain %q, got %q", w, message) + } + } +} + func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) { strategyAPIGroup := strategyv1alpha1.GroupVersion.Group resolved := &ResolvedBackupConfig{ @@ -907,9 +941,8 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) { for _, tc := range mariadbCompleteOutcomeCases { t.Run(tc.name, func(t *testing.T) { - now := metav1.Now() seed := newMariaDBBackupJob("bj", "tenant") - seed.Status.StartedAt = &now + seed.Status.StartedAt = mariadbOutcomeStartedAt(tc.expired) strategy := &strategyv1alpha1.MariaDB{ ObjectMeta: metav1.ObjectMeta{Name: "mdb-strategy"}, Spec: strategyv1alpha1.MariaDBSpec{Template: *newRenderedMariaDBTemplate()}, @@ -920,13 +953,13 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) { operatorBackup := &mariadbtypes.Backup{ ObjectMeta: metav1.ObjectMeta{ Namespace: "tenant", - Name: "bj-op", + Name: "op", Labels: map[string]string{ backupsv1alpha1.OwningJobNameLabel: seed.Name, backupsv1alpha1.OwningJobNamespaceLabel: seed.Namespace, }, }, - Status: mariadbtypes.BackupStatus{Conditions: mariadbCompleteCondition(tc.status, tc.reason)}, + Status: mariadbtypes.BackupStatus{Conditions: mariadbCompleteConditions(tc.noCondition, tc.status, tc.reason, tc.message)}, } c := newMariaDBStrategyTestClient(t, seed, newMariaDBApp(seed.Spec.ApplicationRef.Name, "tenant"), strategy, operatorMariaDB, operatorBackup) @@ -941,31 +974,33 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) { t.Fatalf("reconcileMariaDB: %v", err) } + stored := &backupsv1alpha1.BackupJob{} + if err := c.Get(context.Background(), client.ObjectKeyFromObject(seed), stored); err != nil { + t.Fatalf("persisted Get: %v", err) + } artifacts := &backupsv1alpha1.BackupList{} if err := c.List(context.Background(), artifacts, client.InNamespace("tenant")); err != nil { t.Fatalf("list Backups: %v", err) } switch { case tc.wantFailed: - if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseFailed { - t.Errorf("expected Phase=Failed, got %q", bj.Status.Phase) + if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseFailed { + t.Errorf("expected Phase=Failed, got %q", stored.Status.Phase) } - if !strings.Contains(bj.Status.Message, "bj-op") { - t.Errorf("failure message must name the operator Backup, got %q", bj.Status.Message) - } - if bj.Status.BackupRef != nil || len(artifacts.Items) != 0 { - t.Errorf("a failed run must not produce a Backup: ref=%v artifacts=%d", bj.Status.BackupRef, len(artifacts.Items)) + assertMariaDBFailureMessage(t, stored.Status.Message, tc.wantMessage) + if stored.Status.BackupRef != nil || len(artifacts.Items) != 0 { + t.Errorf("a failed run must not produce a Backup: ref=%v artifacts=%d", stored.Status.BackupRef, len(artifacts.Items)) } case tc.wantDone: - if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseSucceeded { - t.Errorf("expected Phase=Succeeded, got %q (message %q)", bj.Status.Phase, bj.Status.Message) + if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseSucceeded { + t.Errorf("expected Phase=Succeeded, got %q (message %q)", stored.Status.Phase, stored.Status.Message) } - if bj.Status.BackupRef == nil || len(artifacts.Items) != 1 { - t.Errorf("a successful run must produce exactly one Backup: ref=%v artifacts=%d", bj.Status.BackupRef, len(artifacts.Items)) + if stored.Status.BackupRef == nil || len(artifacts.Items) != 1 { + t.Errorf("a successful run must produce exactly one Backup: ref=%v artifacts=%d", stored.Status.BackupRef, len(artifacts.Items)) } default: - if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning { - t.Errorf("expected Phase=Running, got %q", bj.Status.Phase) + if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning { + t.Errorf("expected Phase=Running, got %q", stored.Status.Phase) } if res.RequeueAfter == 0 { t.Errorf("expected a requeue while the run is in progress, got %+v", res) @@ -983,7 +1018,6 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) { for _, tc := range mariadbCompleteOutcomeCases { t.Run(tc.name, func(t *testing.T) { - now := metav1.Now() cozyBackup := &backupsv1alpha1.Backup{ ObjectMeta: metav1.ObjectMeta{Namespace: "tenant", Name: "src-cozy-bk"}, Spec: backupsv1alpha1.BackupSpec{ @@ -997,18 +1031,18 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) { BackupRef: corev1.LocalObjectReference{Name: cozyBackup.Name}, TargetApplicationRef: &corev1.TypedLocalObjectReference{Kind: "MariaDB", Name: "target", APIGroup: &apps}, }, - Status: backupsv1alpha1.RestoreJobStatus{StartedAt: &now, Phase: backupsv1alpha1.RestoreJobPhaseRunning}, + Status: backupsv1alpha1.RestoreJobStatus{StartedAt: mariadbOutcomeStartedAt(tc.expired), Phase: backupsv1alpha1.RestoreJobPhaseRunning}, } operatorRestore := &mariadbtypes.Restore{ ObjectMeta: metav1.ObjectMeta{ Namespace: "tenant", - Name: "rj-op", + Name: "op", Labels: map[string]string{ backupsv1alpha1.OwningJobNameLabel: seed.Name, backupsv1alpha1.OwningJobNamespaceLabel: seed.Namespace, }, }, - Status: mariadbtypes.RestoreStatus{Conditions: mariadbCompleteCondition(tc.status, tc.reason)}, + Status: mariadbtypes.RestoreStatus{Conditions: mariadbCompleteConditions(tc.noCondition, tc.status, tc.reason, tc.message)}, } c := newMariaDBStrategyTestClient(t, cozyBackup, seed, operatorRestore, &mariadbtypes.Backup{ObjectMeta: metav1.ObjectMeta{Namespace: "tenant", Name: "src-op-bk"}}, @@ -1024,21 +1058,23 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) { t.Fatalf("reconcileMariaDBRestore: %v", err) } + stored := &backupsv1alpha1.RestoreJob{} + if err := c.Get(context.Background(), client.ObjectKeyFromObject(seed), stored); err != nil { + t.Fatalf("persisted Get: %v", err) + } switch { case tc.wantFailed: - if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseFailed { - t.Errorf("expected Phase=Failed, got %q", rj.Status.Phase) - } - if !strings.Contains(rj.Status.Message, "rj-op") { - t.Errorf("failure message must name the operator Restore, got %q", rj.Status.Message) + if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseFailed { + t.Errorf("expected Phase=Failed, got %q", stored.Status.Phase) } + assertMariaDBFailureMessage(t, stored.Status.Message, tc.wantMessage) case tc.wantDone: - if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseSucceeded { - t.Errorf("expected Phase=Succeeded, got %q (message %q)", rj.Status.Phase, rj.Status.Message) + if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseSucceeded { + t.Errorf("expected Phase=Succeeded, got %q (message %q)", stored.Status.Phase, stored.Status.Message) } default: - if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseRunning { - t.Errorf("expected Phase=Running, got %q", rj.Status.Phase) + if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseRunning { + t.Errorf("expected Phase=Running, got %q", stored.Status.Phase) } if res.RequeueAfter == 0 { t.Errorf("expected a requeue while the run is in progress, got %+v", res)