package controller
import (
"context"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
infernexv1alpha1 "gitcode.com/openFuyao/InferNex/api/v1alpha1"
)
func TestReconcileInferNexServiceDeletion_RemovesFinalizer(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := gatewayTestScheme(t)
insvc := &infernexv1alpha1.InferNexService{
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
Namespace: "ns-a",
Finalizers: []string{infernexServiceFinalizer},
},
}
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(insvc).Build()
r := &InferNexServiceReconciler{Client: cl, Scheme: s}
res, err := r.reconcileInferNexServiceDeletion(ctx, insvc)
if err != nil {
t.Fatalf("reconcileInferNexServiceDeletion error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected requeue: %#v", res)
}
fresh := &infernexv1alpha1.InferNexService{}
if err := cl.Get(ctx, types.NamespacedName{Namespace: "ns-a", Name: "demo"}, fresh); err != nil {
t.Fatalf("get updated infernexservice failed: %v", err)
}
if len(fresh.Finalizers) != 0 {
t.Fatalf("expected finalizers removed, got %v", fresh.Finalizers)
}
}
func TestReconcileInferNexServiceDeletion_ReleasesSingletonOwnerRefs(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := gatewayTestScheme(t)
ownerA := &infernexv1alpha1.InferNexService{
ObjectMeta: metav1.ObjectMeta{
Name: "isvc-a",
Namespace: "ns-a",
UID: types.UID("uid-a"),
Finalizers: []string{infernexServiceFinalizer},
},
}
ownerB := &infernexv1alpha1.InferNexService{
ObjectMeta: metav1.ObjectMeta{
Name: "isvc-b",
Namespace: "ns-a",
UID: types.UID("uid-b"),
Finalizers: []string{infernexServiceFinalizer},
},
}
refs := []metav1.OwnerReference{
{APIVersion: "infernex.io/v1alpha1", Kind: "InferNexService", Name: ownerA.Name, UID: ownerA.UID},
{APIVersion: "infernex.io/v1alpha1", Kind: "InferNexService", Name: ownerB.Name, UID: ownerB.UID},
}
svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{
Name: "redis-service",
Namespace: "ns-a",
OwnerReferences: refs,
}}
sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{
Name: componentControllerSAName(cacheIndexerComponent),
Namespace: "ns-a",
OwnerReferences: refs,
}}
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(ownerA, ownerB, svc, sa).Build()
r := &InferNexServiceReconciler{Client: cl, Scheme: s}
if _, err := r.reconcileInferNexServiceDeletion(ctx, ownerA); err != nil {
t.Fatalf("reconcileInferNexServiceDeletion(%s) error: %v", ownerA.Name, err)
}
if err := cl.Get(ctx, types.NamespacedName{Namespace: "ns-a", Name: "redis-service"}, svc); err != nil {
t.Fatalf("expected singleton service after first owner deletion: %v", err)
}
assertMissingOwnerRef(t, svc.OwnerReferences, ownerA.Name, ownerA.UID)
assertNonControllerOwnerRef(t, svc.OwnerReferences, ownerB.Name, ownerB.UID)
if err := cl.Get(ctx, types.NamespacedName{Namespace: "ns-a", Name: componentControllerSAName(cacheIndexerComponent)}, sa); err != nil {
t.Fatalf("expected singleton serviceaccount after first owner deletion: %v", err)
}
assertMissingOwnerRef(t, sa.OwnerReferences, ownerA.Name, ownerA.UID)
assertNonControllerOwnerRef(t, sa.OwnerReferences, ownerB.Name, ownerB.UID)
if _, err := r.reconcileInferNexServiceDeletion(ctx, ownerB); err != nil {
t.Fatalf("reconcileInferNexServiceDeletion(%s) error: %v", ownerB.Name, err)
}
mustNotFound(t, cl, types.NamespacedName{Namespace: "ns-a", Name: "redis-service"}, &corev1.Service{})
mustNotFound(t, cl, types.NamespacedName{Namespace: "ns-a", Name: componentControllerSAName(cacheIndexerComponent)}, &corev1.ServiceAccount{})
}
func TestReconcileFetchInferNexService(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := controllerTestScheme(t)
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "ns-a", Name: "demo"}}
t.Run("returns existing object", func(t *testing.T) {
insvc := &infernexv1alpha1.InferNexService{ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "ns-a"}}
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(insvc).Build()
r := &InferNexServiceReconciler{Client: cl, Scheme: s}
got, err := r.reconcileFetchInferNexService(ctx, req)
if err != nil {
t.Fatalf("reconcileFetchInferNexService error: %v", err)
}
if got == nil || got.Name != "demo" {
t.Fatalf("expected fetched object, got %#v", got)
}
})
t.Run("notfound triggers llm build path", func(t *testing.T) {
llm := newLinkedLLM("ns-a", "demo", infernexRuntimeValue)
base := fake.NewClientBuilder().WithScheme(s).WithObjects(llm).Build()
cl := &llmAwareClient{
Client: base,
llms: map[types.NamespacedName]*unstructured.Unstructured{{Namespace: "ns-a", Name: "demo"}: llm},
}
r := &InferNexServiceReconciler{Client: cl, Scheme: s}
got, err := r.reconcileFetchInferNexService(ctx, req)
if err != nil {
t.Fatalf("reconcileFetchInferNexService build-from-llm error: %v", err)
}
if got != nil {
t.Fatalf("expected nil result on create-on-miss path, got %#v", got)
}
created := &infernexv1alpha1.InferNexService{}
if err := base.Get(ctx, req.NamespacedName, created); err != nil {
t.Fatalf("expected infernexservice created from llm: %v", err)
}
})
}
func TestEmitGatewayRoutingConditionEvent(t *testing.T) {
t.Parallel()
rec := record.NewFakeRecorder(10)
r := &InferNexServiceReconciler{Recorder: rec}
obj := &infernexv1alpha1.InferNexService{ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "ns-a"}}
r.emitGatewayRoutingConditionEvent(obj, nil, []metav1.Condition{{
Type: "GatewayRoutingReady",
Status: metav1.ConditionFalse,
Reason: "GatewayNotFound",
Message: "gateway ns-a/gw not found",
}})
select {
case e := <-rec.Events:
if e == "" {
t.Fatal("expected warning event content")
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for warning event")
}
r.emitGatewayRoutingConditionEvent(obj, nil, []metav1.Condition{{
Type: "GatewayRoutingReady",
Status: metav1.ConditionTrue,
Reason: "GatewayRoutingReady",
Message: "gateway routing linked",
}})
select {
case e := <-rec.Events:
if e == "" {
t.Fatal("expected normal event content")
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for normal event")
}
old := []metav1.Condition{{Type: "GatewayRoutingReady", Status: metav1.ConditionTrue, Reason: "GatewayRoutingReady", Message: "same"}}
r.emitGatewayRoutingConditionEvent(obj, old, old)
select {
case <-rec.Events:
t.Fatal("unexpected event for unchanged condition")
case <-time.After(200 * time.Millisecond):
}
}
func TestRecordWarn_NoPanicPaths(t *testing.T) {
t.Parallel()
r := &InferNexServiceReconciler{}
r.recordWarn(nil, "X", "msg")
r.recordNormal(nil, "X", "msg")
r.Recorder = record.NewFakeRecorder(4)
obj := &infernexv1alpha1.InferNexService{ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "ns-a"}}
r.recordWarn(obj, "Warn", "hello %s", "x")
r.recordNormal(obj, "Normal", "hello")
}
func TestReconcile_AddsFinalizerAndRequeues(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := gatewayTestScheme(t)
insvc := &infernexv1alpha1.InferNexService{
ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "ns-a", Generation: 1},
Spec: infernexv1alpha1.InferNexServiceSpec{
SourceRef: &infernexv1alpha1.SourceRef{Kind: "LLMInferenceService", Name: "llm"},
},
}
llm := newLinkedLLM("ns-a", "llm", infernexRuntimeValue)
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(insvc, llm).Build()
r := &InferNexServiceReconciler{Client: cl, Scheme: s, TemplateNamespace: "tpl-ns"}
res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "ns-a", Name: "demo"}})
if err != nil {
t.Fatalf("Reconcile add-finalizer error: %v", err)
}
if !res.Requeue {
t.Fatalf("expected requeue after adding finalizer, got %#v", res)
}
fresh := &infernexv1alpha1.InferNexService{}
if err := cl.Get(ctx, types.NamespacedName{Namespace: "ns-a", Name: "demo"}, fresh); err != nil {
t.Fatalf("get infernexservice: %v", err)
}
if !containsString(fresh.Finalizers, infernexServiceFinalizer) {
t.Fatalf("expected finalizer added, got %v", fresh.Finalizers)
}
}
func TestReconcilePersistStatus_UpdatesStatus(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := gatewayTestScheme(t)
insvc := &infernexv1alpha1.InferNexService{
ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "ns-a", Generation: 3},
Spec: infernexv1alpha1.InferNexServiceSpec{
SourceRef: &infernexv1alpha1.SourceRef{Kind: "LLMInferenceService", Name: "llm"},
},
}
cl := fake.NewClientBuilder().
WithScheme(s).
WithStatusSubresource(&infernexv1alpha1.InferNexService{}).
WithObjects(insvc).
Build()
r := &InferNexServiceReconciler{Client: cl, Scheme: s, Recorder: record.NewFakeRecorder(4)}
res, err := r.reconcilePersistStatus(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "ns-a", Name: "demo"}}, map[string]componentPlan{}, infernexv1alpha1.InferNexServiceSpec{}, "aggregate")
if err != nil {
t.Fatalf("reconcilePersistStatus error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected requeue: %#v", res)
}
fresh := &infernexv1alpha1.InferNexService{}
if err := cl.Get(ctx, types.NamespacedName{Namespace: "ns-a", Name: "demo"}, fresh); err != nil {
t.Fatalf("get infernexservice: %v", err)
}
if fresh.Status.Mode != "aggregate" || fresh.Status.ObservedGeneration != 3 {
t.Fatalf("unexpected status after persist: %#v", fresh.Status)
}
if len(fresh.Status.Conditions) == 0 {
t.Fatal("expected conditions written to status")
}
}
func containsString(list []string, target string) bool {
for _, s := range list {
if s == target {
return true
}
}
return false
}