🧪 test: add operator reconcile coverage

This commit is contained in:
2026-08-13 02:45:40 +07:00
parent 8a73e50e2c
commit 6fedbeedde
15 changed files with 1543 additions and 61 deletions
+35 -14
View File
@@ -1,30 +1,18 @@
package controller
import (
"context"
"encoding/json"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
)
func testScheme(t *testing.T) *runtime.Scheme {
t.Helper()
s := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(s); err != nil {
t.Fatalf("add client-go scheme: %v", err)
}
if err := v1alpha1.AddToScheme(s); err != nil {
t.Fatalf("add v1alpha1 scheme: %v", err)
}
return s
}
func TestTypedObjectMarshalsWithoutTypeMeta(t *testing.T) {
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "b"}}
@@ -89,3 +77,36 @@ func TestApplySetsTypeMeta(t *testing.T) {
t.Error("owner reference should be a controller reference")
}
}
func TestApplyCreatesAndUpdates(t *testing.T) {
owner := testMinecraft("smp", v1alpha1.ServerStateful)
c := newFakeClient(t, owner)
scheme := testScheme(t)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "minecraft-smp-config", Namespace: owner.Namespace},
Data: map[string]string{"server-type": "STATEFUL"},
}
if err := apply(context.Background(), c, owner, cm, scheme); err != nil {
t.Fatalf("create apply: %v", err)
}
var got corev1.ConfigMap
mustGet(t, c, client.ObjectKeyFromObject(cm), &got)
if got.Data["server-type"] != "STATEFUL" {
t.Errorf("data = %v", got.Data)
}
if len(got.OwnerReferences) != 1 {
t.Fatalf("ownerReferences = %d, want 1", len(got.OwnerReferences))
}
updated := got.DeepCopy()
updated.Data["server-type"] = "STATELESS"
if err := apply(context.Background(), c, owner, updated, scheme); err != nil {
t.Fatalf("update apply: %v", err)
}
mustGet(t, c, client.ObjectKeyFromObject(cm), &got)
if got.Data["server-type"] != "STATELESS" {
t.Errorf("updated data = %v", got.Data)
}
}
@@ -0,0 +1,103 @@
package controller
import (
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
)
func testScheme(t *testing.T) *runtime.Scheme {
t.Helper()
s := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(s); err != nil {
t.Fatalf("add client-go scheme: %v", err)
}
if err := v1alpha1.AddToScheme(s); err != nil {
t.Fatalf("add v1alpha1 scheme: %v", err)
}
return s
}
func newFakeClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
return newInterceptedClient(t, interceptor.Funcs{}, objs...)
}
func newInterceptedClient(t *testing.T, funcs interceptor.Funcs, objs ...client.Object) client.Client {
t.Helper()
return fake.NewClientBuilder().
WithScheme(testScheme(t)).
WithStatusSubresource(
&v1alpha1.MinecraftServer{},
&v1alpha1.ReverseProxyServer{},
&appsv1.Deployment{},
&appsv1.StatefulSet{},
&corev1.Service{},
).
WithObjects(objs...).
WithInterceptorFuncs(funcs).
Build()
}
func requestFor(obj client.Object) reconcile.Request {
return reconcile.Request{NamespacedName: types.NamespacedName{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
}}
}
func testMinecraft(name string, kind v1alpha1.ServerKind) *v1alpha1.MinecraftServer {
return &v1alpha1.MinecraftServer{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.GroupVersion.String(),
Kind: "MinecraftServer",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "minikura",
UID: types.UID("uid-" + name),
Generation: 1,
},
Spec: v1alpha1.MinecraftServerSpec{
Type: kind,
ListenPort: 25565,
ServiceType: v1alpha1.ExposureClusterIP,
JarType: "PAPER",
MinecraftVersion: "1.20.4",
StorageSize: "10Gi",
},
}
}
func testProxy(name string, kind v1alpha1.ProxyKind) *v1alpha1.ReverseProxyServer {
return &v1alpha1.ReverseProxyServer{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.GroupVersion.String(),
Kind: "ReverseProxyServer",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "minikura",
UID: types.UID("uid-" + name),
Generation: 1,
},
Spec: v1alpha1.ReverseProxyServerSpec{
Type: kind,
ExternalAddress: "play.example.com",
ExternalPort: 25565,
ListenPort: 25577,
ServiceType: v1alpha1.ExposureLoadBalancer,
},
}
}
@@ -2,7 +2,6 @@ package controller
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -116,10 +115,7 @@ func (r *MinecraftServerReconciler) updateStatus(ctx context.Context, mc *v1alph
}
}
phase := v1alpha1.PhasePending
if ready > 0 {
phase = v1alpha1.PhaseRunning
}
phase := phaseFromReady(ready)
endpoint, err := r.endpoint(ctx, mc)
if err != nil {
@@ -156,21 +152,17 @@ func (r *MinecraftServerReconciler) endpoint(ctx context.Context, mc *v1alpha1.M
}
func (r *MinecraftServerReconciler) fail(ctx context.Context, mc *v1alpha1.MinecraftServer, reason string, cause error) (ctrl.Result, error) {
patch := client.MergeFrom(mc.DeepCopy())
mc.Status.Phase = v1alpha1.PhaseFailed
mc.Status.Message = cause.Error()
setCondition(&mc.Status.Conditions, metav1.Condition{
Type: v1alpha1.ConditionReady,
Status: metav1.ConditionFalse,
Reason: reason,
Message: cause.Error(),
ObservedGeneration: mc.Generation,
return failWithStatus(ctx, r.Client, mc, cause, func() {
mc.Status.Phase = v1alpha1.PhaseFailed
mc.Status.Message = cause.Error()
setCondition(&mc.Status.Conditions, metav1.Condition{
Type: v1alpha1.ConditionReady,
Status: metav1.ConditionFalse,
Reason: reason,
Message: cause.Error(),
ObservedGeneration: mc.Generation,
})
})
if err := r.Status().Patch(ctx, mc, patch); err != nil {
return ctrl.Result{}, fmt.Errorf("%w (status patch failed: %v)", cause, err)
}
return ctrl.Result{}, cause
}
func (r *MinecraftServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -0,0 +1,400 @@
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 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), &current)
current.Spec.Type = v1alpha1.ServerStateful
current.Generation = 2
if err := c.Update(context.Background(), &current); err != nil {
t.Fatalf("update type: %v", err)
}
if _, err := r.Reconcile(context.Background(), requestFor(&current)); 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")
}
}
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 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 TestMinecraftReconcileContinuesAfterPruneError(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.Fatalf("prune errors should not fail reconcile: %v", err)
}
}
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)
}
}
@@ -136,10 +136,7 @@ func (r *ReverseProxyServerReconciler) updateStatus(ctx context.Context, rp *v1a
return err
}
phase := v1alpha1.PhasePending
if ready > 0 {
phase = v1alpha1.PhaseRunning
}
phase := phaseFromReady(ready)
patch := client.MergeFrom(rp.DeepCopy())
rp.Status.Phase = phase
@@ -160,21 +157,17 @@ func (r *ReverseProxyServerReconciler) updateStatus(ctx context.Context, rp *v1a
}
func (r *ReverseProxyServerReconciler) fail(ctx context.Context, rp *v1alpha1.ReverseProxyServer, reason string, cause error) (ctrl.Result, error) {
patch := client.MergeFrom(rp.DeepCopy())
rp.Status.Phase = v1alpha1.PhaseFailed
rp.Status.Message = cause.Error()
setCondition(&rp.Status.Conditions, metav1.Condition{
Type: v1alpha1.ConditionReady,
Status: metav1.ConditionFalse,
Reason: reason,
Message: cause.Error(),
ObservedGeneration: rp.Generation,
return failWithStatus(ctx, r.Client, rp, cause, func() {
rp.Status.Phase = v1alpha1.PhaseFailed
rp.Status.Message = cause.Error()
setCondition(&rp.Status.Conditions, metav1.Condition{
Type: v1alpha1.ConditionReady,
Status: metav1.ConditionFalse,
Reason: reason,
Message: cause.Error(),
ObservedGeneration: rp.Generation,
})
})
if err := r.Status().Patch(ctx, rp, patch); err != nil {
return ctrl.Result{}, fmt.Errorf("%w (status patch failed: %v)", cause, err)
}
return ctrl.Result{}, cause
}
func (r *ReverseProxyServerReconciler) proxiesForServer(ctx context.Context, obj client.Object) []reconcile.Request {
@@ -0,0 +1,484 @@
package controller
import (
"context"
"errors"
"reflect"
"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/types"
"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 TestProxyReconcileCreatesResources(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("reconcile: %v", err)
}
name := resources.ProxyName(rp.Spec.Type, rp.Name)
ns := rp.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 got v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &got)
if got.Status.Phase != v1alpha1.PhasePending {
t.Errorf("phase = %q, want Pending", got.Status.Phase)
}
if got.Status.Endpoint != "" {
t.Errorf("endpoint = %q, want empty until LB is assigned", got.Status.Endpoint)
}
}
func TestProxyReconcilePrunesStaleType(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("first reconcile: %v", err)
}
var current v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &current)
current.Spec.Type = v1alpha1.ProxyBungeeCord
current.Generation = 2
if err := c.Update(context.Background(), &current); err != nil {
t.Fatalf("update type: %v", err)
}
if _, err := r.Reconcile(context.Background(), requestFor(&current)); err != nil {
t.Fatalf("second reconcile: %v", err)
}
oldName := resources.ProxyName(v1alpha1.ProxyVelocity, rp.Name)
newName := resources.ProxyName(v1alpha1.ProxyBungeeCord, rp.Name)
mustGet(t, c, client.ObjectKey{Name: newName, Namespace: rp.Namespace}, &appsv1.Deployment{})
var stale appsv1.Deployment
if err := c.Get(context.Background(), client.ObjectKey{Name: oldName, Namespace: rp.Namespace}, &stale); !apierrors.IsNotFound(err) {
t.Fatalf("expected stale velocity deployment pruned, got %v", err)
}
}
func TestProxyBackendsRespectsSelector(t *testing.T) {
lobby := testMinecraft("lobby", v1alpha1.ServerStateless)
lobby.Labels = map[string]string{"tier": "lobby"}
smp := testMinecraft("smp", v1alpha1.ServerStateful)
smp.Labels = map[string]string{"tier": "survival"}
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{
MatchLabels: map[string]string{"tier": "lobby"},
}
c := newFakeClient(t, lobby, smp, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("reconcile: %v", err)
}
var got v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &got)
if !reflect.DeepEqual(got.Status.Backends, []string{"lobby"}) {
t.Errorf("backends = %v, want [lobby]", got.Status.Backends)
}
}
func TestProxyBackendsListsAllWhenUnfiltered(t *testing.T) {
c := newFakeClient(t,
testMinecraft("b", v1alpha1.ServerStateless),
testMinecraft("a", v1alpha1.ServerStateful),
testProxy("edge", v1alpha1.ProxyVelocity),
)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
names, err := r.backends(context.Background(), testProxy("edge", v1alpha1.ProxyVelocity))
if err != nil {
t.Fatalf("backends: %v", err)
}
if !reflect.DeepEqual(names, []string{"a", "b"}) {
t.Errorf("backends = %v, want [a b]", names)
}
}
func TestMatchesBackend(t *testing.T) {
obj := testMinecraft("lobby", v1alpha1.ServerStateless)
obj.Labels = map[string]string{"tier": "lobby"}
t.Run("nil selector matches all", func(t *testing.T) {
if !matchesBackend(*testProxy("edge", v1alpha1.ProxyVelocity), obj) {
t.Error("expected match")
}
})
t.Run("matching labels", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "lobby"}}
if !matchesBackend(*rp, obj) {
t.Error("expected match")
}
})
t.Run("non matching labels", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "survival"}}
if matchesBackend(*rp, obj) {
t.Error("expected no match")
}
})
}
func TestProxiesForServer(t *testing.T) {
all := testProxy("all", v1alpha1.ProxyVelocity)
filtered := testProxy("filtered", v1alpha1.ProxyVelocity)
filtered.Spec.BackendSelector = &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "lobby"}}
other := testProxy("other", v1alpha1.ProxyVelocity)
other.Spec.BackendSelector = &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "survival"}}
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
mc.Labels = map[string]string{"tier": "lobby"}
c := newFakeClient(t, all, filtered, other)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
reqs := r.proxiesForServer(context.Background(), mc)
got := map[string]bool{}
for _, req := range reqs {
got[req.Name] = true
}
if !got["all"] || !got["filtered"] {
t.Errorf("requests = %v, want all and filtered", got)
}
if got["other"] {
t.Error("did not expect the survival-only proxy")
}
}
func TestProxyBackendsInvalidSelector(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: "tier",
Operator: "NotARealOperator",
Values: []string{"lobby"},
}},
}
r := &ReverseProxyServerReconciler{Client: newFakeClient(t), Scheme: testScheme(t)}
if _, err := r.backends(context.Background(), rp); err == nil {
t.Fatal("expected invalid backendSelector to fail")
}
}
func TestProxyReconcileIgnoresNotFound(t *testing.T) {
r := &ReverseProxyServerReconciler{Client: newFakeClient(t), Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: "missing", Namespace: "minikura"},
}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestProxyReconcileSkipsDeleting(t *testing.T) {
now := metav1.Now()
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.DeletionTimestamp = &now
rp.Finalizers = []string{"test.finalizer"}
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("reconcile: %v", err)
}
var cm corev1.ConfigMap
err := c.Get(context.Background(), client.ObjectKey{
Name: resources.ConfigMapName(resources.ProxyName(rp.Spec.Type, rp.Name)),
Namespace: rp.Namespace,
}, &cm)
if !apierrors.IsNotFound(err) {
t.Fatalf("expected no resources for deleting object, got %v", err)
}
}
func TestProxyReconcileApplyFailure(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
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...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err == nil {
t.Fatal("expected apply failure")
}
var got v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &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 TestProxyReconcileServiceAndDeploymentFailures(t *testing.T) {
t.Run("service", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
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...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err == nil {
t.Fatal("expected service failure")
}
})
t.Run("deployment", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
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.(*appsv1.Deployment); ok {
return errors.New("deployment apply failed")
}
return c.Patch(ctx, obj, patch, opts...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err == nil {
t.Fatal("expected deployment failure")
}
})
}
func TestProxyPruneSkipsUnowned(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
stale := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: resources.ProxyName(v1alpha1.ProxyBungeeCord, rp.Name),
Namespace: rp.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, rp, stale)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if err := r.pruneStaleResources(context.Background(), rp); err != nil {
t.Fatalf("prune: %v", err)
}
mustGet(t, c, client.ObjectKeyFromObject(stale), &appsv1.Deployment{})
}
func TestProxyPruneGetError(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newInterceptedClient(t, interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if key.Name == resources.ProxyName(v1alpha1.ProxyBungeeCord, rp.Name) {
return errors.New("get failed")
}
return c.Get(ctx, key, obj, opts...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if err := r.pruneStaleResources(context.Background(), rp); err == nil {
t.Fatal("expected prune get error")
}
}
func TestProxyReconcilePruneFailure(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("seed: %v", err)
}
var current v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &current)
current.Spec.Type = v1alpha1.ProxyBungeeCord
if err := c.Update(context.Background(), &current); err != nil {
t.Fatalf("update type: %v", err)
}
watch, ok := c.(client.WithWatch)
if !ok {
t.Fatal("fake client does not implement WithWatch")
}
r.Client = interceptor.NewClient(watch, interceptor.Funcs{
Delete: func(ctx context.Context, inner client.WithWatch, obj client.Object, opts ...client.DeleteOption) error {
return errors.New("delete failed")
},
})
if _, err := r.Reconcile(context.Background(), requestFor(&current)); err == nil {
t.Fatal("expected prune failure")
}
}
func TestProxyPruneDeleteError(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("seed: %v", err)
}
var current v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &current)
current.Spec.Type = v1alpha1.ProxyBungeeCord
watch, ok := c.(client.WithWatch)
if !ok {
t.Fatal("fake client does not implement WithWatch")
}
r.Client = interceptor.NewClient(watch, interceptor.Funcs{
Delete: func(ctx context.Context, inner client.WithWatch, obj client.Object, opts ...client.DeleteOption) error {
return errors.New("delete failed")
},
})
if err := r.pruneStaleResources(context.Background(), &current); err == nil {
t.Fatal("expected prune delete error")
}
}
func TestProxyUpdateStatusErrors(t *testing.T) {
t.Run("deployment get", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
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...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if err := r.updateStatus(context.Background(), rp); err == nil {
t.Fatal("expected deployment get error")
}
})
t.Run("service get", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
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...)
},
}, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if err := r.updateStatus(context.Background(), rp); err == nil {
t.Fatal("expected service get error")
}
})
t.Run("invalid selector", func(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: "tier", Operator: "NotARealOperator", Values: []string{"x"},
}},
}
r := &ReverseProxyServerReconciler{Client: newFakeClient(t, rp), Scheme: testScheme(t)}
if err := r.updateStatus(context.Background(), rp); err == nil {
t.Fatal("expected backend selector error")
}
})
}
func TestProxyStatusReadyAndEndpoint(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
c := newFakeClient(t, rp)
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("reconcile: %v", err)
}
name := resources.ProxyName(rp.Spec.Type, rp.Name)
var dep appsv1.Deployment
mustGet(t, c, client.ObjectKey{Name: name, Namespace: rp.Namespace}, &dep)
dep.Status.Replicas = 1
dep.Status.ReadyReplicas = 1
if err := c.Status().Update(context.Background(), &dep); err != nil {
t.Fatalf("dep status: %v", err)
}
var svc corev1.Service
mustGet(t, c, client.ObjectKey{Name: name, Namespace: rp.Namespace}, &svc)
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{{IP: "9.9.9.9"}}
if err := c.Status().Update(context.Background(), &svc); err != nil {
t.Fatalf("svc status: %v", err)
}
if _, err := r.Reconcile(context.Background(), requestFor(rp)); err != nil {
t.Fatalf("second reconcile: %v", err)
}
var got v1alpha1.ReverseProxyServer
mustGet(t, c, client.ObjectKeyFromObject(rp), &got)
if got.Status.Phase != v1alpha1.PhaseRunning {
t.Errorf("phase = %q", got.Status.Phase)
}
if got.Status.Endpoint != "9.9.9.9:25565" {
t.Errorf("endpoint = %q", got.Status.Endpoint)
}
}
func TestProxiesForServerListError(t *testing.T) {
c := newInterceptedClient(t, interceptor.Funcs{
List: func(ctx context.Context, c client.WithWatch, list client.ObjectList, opts ...client.ListOption) error {
return errors.New("list failed")
},
})
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if reqs := r.proxiesForServer(context.Background(), testMinecraft("lobby", v1alpha1.ServerStateless)); reqs != nil {
t.Errorf("requests = %v, want nil", reqs)
}
}
func TestMatchesBackendInvalidSelector(t *testing.T) {
rp := testProxy("edge", v1alpha1.ProxyVelocity)
rp.Spec.BackendSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: "tier", Operator: "NotARealOperator", Values: []string{"x"},
}},
}
if matchesBackend(*rp, testMinecraft("lobby", v1alpha1.ServerStateless)) {
t.Error("invalid selector should not match")
}
}
func TestProxyBackendsListError(t *testing.T) {
c := newInterceptedClient(t, interceptor.Funcs{
List: func(ctx context.Context, c client.WithWatch, list client.ObjectList, opts ...client.ListOption) error {
return errors.New("list failed")
},
})
r := &ReverseProxyServerReconciler{Client: c, Scheme: testScheme(t)}
if _, err := r.backends(context.Background(), testProxy("edge", v1alpha1.ProxyVelocity)); err == nil {
t.Fatal("expected list error")
}
}
+21
View File
@@ -1,13 +1,25 @@
package controller
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
)
func phaseFromReady(ready int32) string {
if ready > 0 {
return v1alpha1.PhaseRunning
}
return v1alpha1.PhasePending
}
func conditionStatus(ok bool) metav1.ConditionStatus {
if ok {
return metav1.ConditionTrue
@@ -22,6 +34,15 @@ func setCondition(conditions *[]metav1.Condition, c metav1.Condition) {
meta.SetStatusCondition(conditions, c)
}
func failWithStatus(ctx context.Context, c client.Client, obj client.Object, cause error, mutate func()) (ctrl.Result, error) {
patch := client.MergeFrom(obj.DeepCopyObject().(client.Object))
mutate()
if err := c.Status().Patch(ctx, obj, patch); err != nil {
return ctrl.Result{}, fmt.Errorf("%w (status patch failed: %v)", cause, err)
}
return ctrl.Result{}, cause
}
func serviceEndpoint(svc *corev1.Service, namespace string) string {
internal := fmt.Sprintf("%s.%s.svc.cluster.local", svc.Name, namespace)
port := int32(0)
+152
View File
@@ -0,0 +1,152 @@
package controller
import (
"context"
"errors"
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
v1alpha1 "github.com/YuzuZensai/Minikura/operator/api/v1alpha1"
)
func TestPhaseFromReady(t *testing.T) {
if got := phaseFromReady(0); got != v1alpha1.PhasePending {
t.Errorf("ready=0 -> %q, want Pending", got)
}
if got := phaseFromReady(1); got != v1alpha1.PhaseRunning {
t.Errorf("ready=1 -> %q, want Running", got)
}
}
func TestConditionStatus(t *testing.T) {
if got := conditionStatus(true); got != metav1.ConditionTrue {
t.Errorf("true -> %q", got)
}
if got := conditionStatus(false); got != metav1.ConditionFalse {
t.Errorf("false -> %q", got)
}
}
func TestSetConditionFillsReason(t *testing.T) {
var conditions []metav1.Condition
setCondition(&conditions, metav1.Condition{Type: v1alpha1.ConditionReady})
if conditions[0].Reason != "Unknown" {
t.Errorf("reason = %q, want Unknown", conditions[0].Reason)
}
}
func TestFailWithStatusWrapsPatchError(t *testing.T) {
mc := testMinecraft("lobby", v1alpha1.ServerStateless)
c := newInterceptedClient(t, interceptor.Funcs{
SubResourcePatch: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
return errors.New("status denied")
},
}, mc)
_, err := failWithStatus(context.Background(), c, mc, errors.New("root cause"), func() {
mc.Status.Phase = v1alpha1.PhaseFailed
})
if err == nil {
t.Fatal("expected wrapped status patch error")
}
if !strings.Contains(err.Error(), "root cause") || !strings.Contains(err.Error(), "status denied") {
t.Errorf("error = %v", err)
}
}
func TestServiceEndpoint(t *testing.T) {
tests := []struct {
name string
svc *corev1.Service
want string
}{
{
name: "cluster ip",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "minecraft-smp"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Ports: []corev1.ServicePort{{Port: 25565}},
},
},
want: "minecraft-smp.minikura.svc.cluster.local:25565",
},
{
name: "load balancer hostname",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "velocity-edge"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{{Port: 25565}},
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{{Hostname: "lb.example.com"}},
},
},
},
want: "lb.example.com:25565",
},
{
name: "load balancer ip",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "velocity-edge"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{{Port: 25565}},
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{{IP: "1.2.3.4"}},
},
},
},
want: "1.2.3.4:25565",
},
{
name: "load balancer pending",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "velocity-edge"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{{Port: 25565}},
},
},
want: "",
},
{
name: "node port assigned",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "minecraft-smp"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeNodePort,
Ports: []corev1.ServicePort{{Port: 25565, NodePort: 30123}},
},
},
want: "<node-ip>:30123",
},
{
name: "node port unassigned",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "minecraft-smp"},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeNodePort,
Ports: []corev1.ServicePort{{Port: 25565}},
},
},
want: "minecraft-smp.minikura.svc.cluster.local",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := serviceEndpoint(tt.svc, "minikura"); got != tt.want {
t.Errorf("serviceEndpoint() = %q, want %q", got, tt.want)
}
})
}
}