Files

109 lines
4.4 KiB
Go

package operator_test
import (
"context"
"testing"
"time"
"felis.lolicon.best/internal/apis/felis/v1alpha1"
"felis.lolicon.best/internal/maintenance"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func maintenanceJob(managedBy string, finished bool) *batchv1.Job {
j := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "restore-survival", Namespace: "minecraft",
Labels: map[string]string{maintenance.LabelServer: "survival", maintenance.LabelManagedBy: managedBy}}}
if finished {
j.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue}}
}
return j
}
// A desiredState flipped to Running behind felis-api's back (kubectl, a script)
// while a restore Job is unpacking into the world must not get a game pod.
func TestReconcileRunning_HeldDuringMaintenance(t *testing.T) {
r, c := newReconciler(t, fakeProber{}, runningServer(), rconSecret(), maintenanceJob("felis-restore", false))
r.Jobs = c
res := reconcile(t, r, "survival")
if res.RequeueAfter <= 0 {
t.Fatal("a held server must requeue to notice the Job finishing")
}
var sts appsv1.StatefulSet
err := c.Get(context.Background(), types.NamespacedName{Namespace: "minecraft", Name: "survival"}, &sts)
if !apierrors.IsNotFound(err) {
t.Fatalf("StatefulSet created while the restore runs (err=%v)", err)
}
s := getServer(t, c, "survival")
cond := meta.FindStatusCondition(s.Status.Conditions, v1alpha1.ConditionReady)
if cond == nil || cond.Reason != "MaintenanceInProgress" {
t.Fatalf("Ready condition = %+v, want reason MaintenanceInProgress", cond)
}
if s.Status.StartRequestedAt != nil {
t.Fatal("the hold must not start the startup-timeout clock")
}
// The Job finishes: the next pass starts the server.
var j batchv1.Job
if err := c.Get(context.Background(), types.NamespacedName{Namespace: "minecraft", Name: "restore-survival"}, &j); err != nil {
t.Fatalf("get job: %v", err)
}
j.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue}}
if err := c.Status().Update(context.Background(), &j); err != nil {
if err := c.Update(context.Background(), &j); err != nil {
t.Fatalf("finish job: %v", err)
}
}
reconcile(t, r, "survival")
if err := c.Get(context.Background(), types.NamespacedName{Namespace: "minecraft", Name: "survival"}, &sts); err != nil {
t.Fatalf("StatefulSet not created after the restore finished: %v", err)
}
}
func TestReconcileRunning_FreshLockHolds(t *testing.T) {
ms := runningServer()
ms.Annotations = map[string]string{maintenance.Annotation: maintenance.LockValue(maintenance.KindBackup, fixedNow().Add(-5*time.Second))}
r, c := newReconciler(t, fakeProber{}, ms, rconSecret())
r.Jobs = c
reconcile(t, r, "survival")
var sts appsv1.StatefulSet
if err := c.Get(context.Background(), types.NamespacedName{Namespace: "minecraft", Name: "survival"}, &sts); !apierrors.IsNotFound(err) {
t.Fatalf("StatefulSet created under a fresh maintenance lock (err=%v)", err)
}
}
func TestReconcileRunning_NotHeldByReadsOrFinishedJobs(t *testing.T) {
read := maintenanceJob("felis-files", false)
read.Name = "files-survival-read"
read.Labels[maintenance.LabelFilesMode] = "read"
r, c := newReconciler(t, fakeProber{}, runningServer(), rconSecret(), read, maintenanceJob("felis-backup", true))
r.Jobs = c
reconcile(t, r, "survival")
var sts appsv1.StatefulSet
if err := c.Get(context.Background(), types.NamespacedName{Namespace: "minecraft", Name: "survival"}, &sts); err != nil {
t.Fatalf("StatefulSet not created: %v", err)
}
}
// A server that is already up is never held: its pod is the one on the volume,
// and holding would only stall its readiness bookkeeping.
func TestReconcileRunning_RunningServerNotHeld(t *testing.T) {
r, c := newReconciler(t, fakeProber{}, runningServer(), rconSecret())
reconcile(t, r, "survival") // creates the StatefulSet at replicas 1
if err := c.Create(context.Background(), maintenanceJob("felis-restore", false)); err != nil {
t.Fatalf("create job: %v", err)
}
r.Jobs = c
reconcile(t, r, "survival")
cond := meta.FindStatusCondition(getServer(t, c, "survival").Status.Conditions, v1alpha1.ConditionReady)
if cond != nil && cond.Reason == "MaintenanceInProgress" {
t.Fatal("a scaled-up server was held")
}
}