mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 18:59:25 +00:00
455 lines
16 KiB
Go
455 lines
16 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
|
|
"github.com/YuzuZensai/Minikura/operator/internal/resources"
|
|
)
|
|
|
|
func TestMinecraftReconcileStatelessCreatesResources(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("reconcile: %v", err)
|
|
}
|
|
|
|
name := resources.ServerName(mc.Name)
|
|
ns := mc.Namespace
|
|
mustGet(t, c, client.ObjectKey{Name: resources.ConfigMapName(name), Namespace: ns}, &corev1.ConfigMap{})
|
|
mustGet(t, c, client.ObjectKey{Name: name, Namespace: ns}, &corev1.Service{})
|
|
mustGet(t, c, client.ObjectKey{Name: name, Namespace: ns}, &appsv1.Deployment{})
|
|
|
|
var sts appsv1.StatefulSet
|
|
if err := c.Get(context.Background(), client.ObjectKey{Name: name, Namespace: ns}, &sts); !apierrors.IsNotFound(err) {
|
|
t.Fatalf("expected no statefulset, got %v", err)
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhasePending {
|
|
t.Errorf("phase = %q, want Pending", got.Status.Phase)
|
|
}
|
|
if got.Status.ObservedGeneration != 1 {
|
|
t.Errorf("observedGeneration = %d, want 1", got.Status.ObservedGeneration)
|
|
}
|
|
if got.Status.Endpoint != "minecraft-lobby.minikura.svc.cluster.local:25565" {
|
|
t.Errorf("endpoint = %q", got.Status.Endpoint)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileRejectsInvalidSpec(t *testing.T) {
|
|
mc := testMinecraft("invalid", v1alpha1.ServerStateless)
|
|
mc.Spec.Type = ""
|
|
mc.Spec.ListenPort = 0
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err == nil || !strings.Contains(err.Error(), "unsupported server type") {
|
|
t.Fatalf("reconcile error = %v, want invalid spec error", err)
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhaseFailed || got.Status.ObservedGeneration != mc.Generation {
|
|
t.Fatalf("status = %#v, want failed observed generation", got.Status)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileStatefulCreatesStatefulSet(t *testing.T) {
|
|
mc := testMinecraft("smp", v1alpha1.ServerStateful)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("reconcile: %v", err)
|
|
}
|
|
|
|
name := resources.ServerName(mc.Name)
|
|
mustGet(t, c, client.ObjectKey{Name: name, Namespace: mc.Namespace}, &appsv1.StatefulSet{})
|
|
|
|
var dep appsv1.Deployment
|
|
if err := c.Get(context.Background(), client.ObjectKey{Name: name, Namespace: mc.Namespace}, &dep); !apierrors.IsNotFound(err) {
|
|
t.Fatalf("expected no deployment, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcilePrunesOppositeWorkload(t *testing.T) {
|
|
mc := testMinecraft("swap", v1alpha1.ServerStateless)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("first reconcile: %v", err)
|
|
}
|
|
|
|
var current v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), ¤t)
|
|
current.Spec.Type = v1alpha1.ServerStateful
|
|
current.Generation = 2
|
|
if err := c.Update(context.Background(), ¤t); err != nil {
|
|
t.Fatalf("update type: %v", err)
|
|
}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(¤t)); err != nil {
|
|
t.Fatalf("second reconcile: %v", err)
|
|
}
|
|
|
|
name := resources.ServerName(mc.Name)
|
|
key := client.ObjectKey{Name: name, Namespace: mc.Namespace}
|
|
mustGet(t, c, key, &appsv1.StatefulSet{})
|
|
|
|
var dep appsv1.Deployment
|
|
if err := c.Get(context.Background(), key, &dep); !apierrors.IsNotFound(err) {
|
|
t.Fatalf("expected deployment pruned, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileIgnoresNotFound(t *testing.T) {
|
|
r := &MinecraftServerReconciler{Client: newFakeClient(t), Scheme: testScheme(t)}
|
|
res, err := r.Reconcile(context.Background(), reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: "missing", Namespace: "minikura"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if res != (ctrl.Result{}) {
|
|
t.Errorf("result = %#v, want empty", res)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileSkipsDeleting(t *testing.T) {
|
|
now := metav1.Now()
|
|
mc := testMinecraft("gone", v1alpha1.ServerStateless)
|
|
mc.DeletionTimestamp = &now
|
|
mc.Finalizers = []string{"test.finalizer"}
|
|
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("reconcile: %v", err)
|
|
}
|
|
|
|
var cm corev1.ConfigMap
|
|
err := c.Get(context.Background(), client.ObjectKey{
|
|
Name: resources.ConfigMapName(resources.ServerName(mc.Name)),
|
|
Namespace: mc.Namespace,
|
|
}, &cm)
|
|
if !apierrors.IsNotFound(err) {
|
|
t.Fatalf("expected no resources for deleting object, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileInvalidStorageFails(t *testing.T) {
|
|
mc := testMinecraft("bad", v1alpha1.ServerStateful)
|
|
mc.Spec.StorageSize = "not-a-size"
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err == nil {
|
|
t.Fatal("expected error for invalid storageSize")
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhaseFailed {
|
|
t.Errorf("phase = %q, want Failed", got.Status.Phase)
|
|
}
|
|
if got.Status.Message == "" {
|
|
t.Error("expected a failure message")
|
|
}
|
|
if got.Status.ObservedGeneration != mc.Generation {
|
|
t.Errorf("observedGeneration = %d, want %d", got.Status.ObservedGeneration, mc.Generation)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftPruneSkipsUnownedWorkload(t *testing.T) {
|
|
mc := testMinecraft("swap", v1alpha1.ServerStateful)
|
|
stale := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: resources.ServerName(mc.Name),
|
|
Namespace: mc.Namespace,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "x"}},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "x"}},
|
|
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "c", Image: "busybox"}}},
|
|
},
|
|
},
|
|
}
|
|
c := newFakeClient(t, mc, stale)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if err := r.pruneOppositeWorkload(context.Background(), mc, true); err != nil {
|
|
t.Fatalf("prune: %v", err)
|
|
}
|
|
mustGet(t, c, client.ObjectKeyFromObject(stale), &appsv1.Deployment{})
|
|
}
|
|
|
|
func TestMinecraftStatusUsesReadyReplicas(t *testing.T) {
|
|
mc := testMinecraft("ready", v1alpha1.ServerStateless)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("reconcile: %v", err)
|
|
}
|
|
|
|
var dep appsv1.Deployment
|
|
mustGet(t, c, client.ObjectKey{Name: resources.ServerName(mc.Name), Namespace: mc.Namespace}, &dep)
|
|
dep.Status.Replicas = 1
|
|
dep.Status.ReadyReplicas = 1
|
|
if err := c.Status().Update(context.Background(), &dep); err != nil {
|
|
t.Fatalf("update deployment status: %v", err)
|
|
}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("second reconcile: %v", err)
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhaseRunning {
|
|
t.Errorf("phase = %q, want Running", got.Status.Phase)
|
|
}
|
|
if got.Status.ReadyReplicas != 1 {
|
|
t.Errorf("readyReplicas = %d, want 1", got.Status.ReadyReplicas)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileMutatesServiceAndWorkload(t *testing.T) {
|
|
mc := testMinecraft("mutable", v1alpha1.ServerStateless)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("first reconcile: %v", err)
|
|
}
|
|
|
|
var current v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), ¤t)
|
|
current.Spec.ListenPort = 25566
|
|
current.Spec.Resources.MemoryLimitMB = 4096
|
|
current.Generation = 2
|
|
if err := c.Update(context.Background(), ¤t); err != nil {
|
|
t.Fatalf("update server: %v", err)
|
|
}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(¤t)); err != nil {
|
|
t.Fatalf("second reconcile: %v", err)
|
|
}
|
|
|
|
key := client.ObjectKey{Name: resources.ServerName(mc.Name), Namespace: mc.Namespace}
|
|
var svc corev1.Service
|
|
mustGet(t, c, key, &svc)
|
|
if svc.Spec.Ports[0].Port != 25566 {
|
|
t.Errorf("service port = %d", svc.Spec.Ports[0].Port)
|
|
}
|
|
var dep appsv1.Deployment
|
|
mustGet(t, c, key, &dep)
|
|
if got := dep.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().String(); got != "4Gi" {
|
|
t.Errorf("memory limit = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileApplyFailure(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
|
if _, ok := obj.(*corev1.ConfigMap); ok {
|
|
return errors.New("configmap apply failed")
|
|
}
|
|
return c.Patch(ctx, obj, patch, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err == nil {
|
|
t.Fatal("expected apply failure")
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhaseFailed {
|
|
t.Errorf("phase = %q, want Failed", got.Status.Phase)
|
|
}
|
|
if !strings.Contains(got.Status.Message, "configmap apply failed") {
|
|
t.Errorf("message = %q", got.Status.Message)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileServiceFailure(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
|
if _, ok := obj.(*corev1.Service); ok {
|
|
return errors.New("service apply failed")
|
|
}
|
|
return c.Patch(ctx, obj, patch, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err == nil {
|
|
t.Fatal("expected service apply failure")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftReconcileReportsPruneError(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
if _, ok := obj.(*appsv1.StatefulSet); ok {
|
|
return errors.New("sts lookup failed")
|
|
}
|
|
return c.Get(ctx, key, obj, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err == nil {
|
|
t.Fatal("expected prune failure")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftUpdateStatusWorkloadGetError(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
if _, ok := obj.(*appsv1.Deployment); ok {
|
|
return errors.New("deployment get failed")
|
|
}
|
|
return c.Get(ctx, key, obj, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if err := r.updateStatus(context.Background(), mc, false); err == nil {
|
|
t.Fatal("expected workload get error")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftUpdateStatusStatefulGetError(t *testing.T) {
|
|
mc := testMinecraft("smp", v1alpha1.ServerStateful)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
if _, ok := obj.(*appsv1.StatefulSet); ok {
|
|
return errors.New("sts get failed")
|
|
}
|
|
return c.Get(ctx, key, obj, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if err := r.updateStatus(context.Background(), mc, true); err == nil {
|
|
t.Fatal("expected statefulset get error")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftUpdateStatusEndpointError(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
if _, ok := obj.(*corev1.Service); ok {
|
|
return errors.New("service get failed")
|
|
}
|
|
return c.Get(ctx, key, obj, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if err := r.updateStatus(context.Background(), mc, false); err == nil {
|
|
t.Fatal("expected endpoint get error")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftEndpointGetError(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
c := newInterceptedClient(t, interceptor.Funcs{
|
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
if _, ok := obj.(*corev1.Service); ok {
|
|
return errors.New("service get failed")
|
|
}
|
|
return c.Get(ctx, key, obj, opts...)
|
|
},
|
|
}, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.endpoint(context.Background(), mc); err == nil {
|
|
t.Fatal("expected service get error")
|
|
}
|
|
}
|
|
|
|
func TestMinecraftEndpointMissingService(t *testing.T) {
|
|
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
|
|
r := &MinecraftServerReconciler{Client: newFakeClient(t, mc), Scheme: testScheme(t)}
|
|
got, err := r.endpoint(context.Background(), mc)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("endpoint = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestMinecraftStatusUsesStatefulReadyReplicas(t *testing.T) {
|
|
mc := testMinecraft("smp", v1alpha1.ServerStateful)
|
|
c := newFakeClient(t, mc)
|
|
r := &MinecraftServerReconciler{Client: c, Scheme: testScheme(t)}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("reconcile: %v", err)
|
|
}
|
|
|
|
var sts appsv1.StatefulSet
|
|
mustGet(t, c, client.ObjectKey{Name: resources.ServerName(mc.Name), Namespace: mc.Namespace}, &sts)
|
|
sts.Status.Replicas = 1
|
|
sts.Status.ReadyReplicas = 1
|
|
if err := c.Status().Update(context.Background(), &sts); err != nil {
|
|
t.Fatalf("update sts status: %v", err)
|
|
}
|
|
if _, err := r.Reconcile(context.Background(), requestFor(mc)); err != nil {
|
|
t.Fatalf("second reconcile: %v", err)
|
|
}
|
|
|
|
var got v1alpha1.MinecraftServer
|
|
mustGet(t, c, client.ObjectKeyFromObject(mc), &got)
|
|
if got.Status.Phase != v1alpha1.PhaseRunning || got.Status.ReadyReplicas != 1 {
|
|
t.Errorf("status = %+v", got.Status)
|
|
}
|
|
}
|
|
|
|
func TestApplyRejectsCrossNamespaceOwner(t *testing.T) {
|
|
owner := testMinecraft("smp", v1alpha1.ServerStateful)
|
|
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "x", Namespace: "other"}}
|
|
if err := apply(context.Background(), newFakeClient(t), owner, cm, testScheme(t)); err == nil {
|
|
t.Fatal("expected setOwner to fail across namespaces")
|
|
}
|
|
}
|
|
|
|
func TestApplyRejectsUnknownType(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
if err := v1alpha1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner := testMinecraft("smp", v1alpha1.ServerStateful)
|
|
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "x", Namespace: owner.Namespace}}
|
|
if err := apply(context.Background(), newFakeClient(t, owner), owner, cm, scheme); err == nil {
|
|
t.Fatal("expected GVK lookup to fail")
|
|
}
|
|
}
|
|
|
|
func mustGet(t *testing.T, c client.Client, key client.ObjectKey, obj client.Object) {
|
|
t.Helper()
|
|
if err := c.Get(context.Background(), key, obj); err != nil {
|
|
t.Fatalf("get %T %s: %v", obj, key, err)
|
|
}
|
|
}
|