109 lines
4.4 KiB
Go
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")
|
|
}
|
|
}
|