Files
lmcache--lmcache/operator/internal/webhook/cacheblend_pod_injector_test.go
2026-07-13 12:24:33 +08:00

576 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"context"
"encoding/json"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
jsonpatch "github.com/evanphx/json-patch/v5"
admissionv1 "k8s.io/api/admission/v1"
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/fake"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
lmcachev1alpha1 "github.com/LMCache/LMCache/api/v1alpha1"
"github.com/LMCache/LMCache/internal/resources"
)
const (
testEngineName = "cb"
testNamespace = "vllm-ns"
testPodName = "vllm-pod"
testSvcHost = "tcp://cb.vllm-ns.svc.cluster.local"
)
// newTestScheme returns a scheme with clientgo + the lmcache v1alpha1 types.
func newTestScheme() *runtime.Scheme {
s := runtime.NewScheme()
Expect(clientgoscheme.AddToScheme(s)).To(Succeed())
Expect(lmcachev1alpha1.AddToScheme(s)).To(Succeed())
return s
}
// newTestEngine returns a defaulted CacheBlendEngine with the given injection
// overrides applied via mutate (nil = pure defaults).
func newTestEngine(mutate func(*lmcachev1alpha1.CacheBlendEngine)) *lmcachev1alpha1.CacheBlendEngine {
payloadRepo := "registry.example.com/lmcache/cacheblend-payload"
payloadTag := "pinned"
engine := &lmcachev1alpha1.CacheBlendEngine{
ObjectMeta: metav1.ObjectMeta{Name: testEngineName, Namespace: testNamespace},
Spec: lmcachev1alpha1.CacheBlendEngineSpec{
L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
Injection: &lmcachev1alpha1.InjectionSpec{
PayloadImage: &lmcachev1alpha1.ImageSpec{Repository: &payloadRepo, Tag: &payloadTag},
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "cb-payload-pull"},
},
},
},
}
engine.SetDefaults()
if mutate != nil {
mutate(engine)
}
return engine
}
// newPodInjector returns a CacheBlendPodInjector backed by a fake client seeded
// with the given objects, plus the engine's connection ConfigMap when seedConn
// is true.
func newPodInjector(
engine *lmcachev1alpha1.CacheBlendEngine,
seedConn bool,
) *CacheBlendPodInjector {
scheme := newTestScheme()
builder := fake.NewClientBuilder().WithScheme(scheme)
objs := []runtime.Object{engine}
if seedConn {
objs = append(objs, resources.BuildCBConnectionConfigMap(engine))
}
builder = builder.WithRuntimeObjects(objs...)
return &CacheBlendPodInjector{
Client: builder.Build(),
Decoder: admission.NewDecoder(scheme),
}
}
// makeRequest builds a CREATE admission.Request carrying the given pod as raw
// JSON in req.Object.
func makeRequest(pod *corev1.Pod) admission.Request {
pod.TypeMeta = metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}
if pod.Namespace == "" {
pod.Namespace = testNamespace
}
raw, err := json.Marshal(pod)
Expect(err).NotTo(HaveOccurred())
return admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Namespace: pod.Namespace,
Object: runtime.RawExtension{Raw: raw},
},
}
}
// applyResponse applies the response's JSON patches to the original pod JSON and
// returns the mutated pod. It asserts the response is Allowed.
func applyResponse(original *corev1.Pod, resp admission.Response) *corev1.Pod {
Expect(resp.Allowed).To(BeTrue(), "expected the response to be Allowed")
origRaw, err := json.Marshal(original)
Expect(err).NotTo(HaveOccurred())
if len(resp.Patches) == 0 {
out := &corev1.Pod{}
Expect(json.Unmarshal(origRaw, out)).To(Succeed())
return out
}
patchRaw, err := json.Marshal(resp.Patches)
Expect(err).NotTo(HaveOccurred())
patch, err := jsonpatch.DecodePatch(patchRaw)
Expect(err).NotTo(HaveOccurred())
mutatedRaw, err := patch.Apply(origRaw)
Expect(err).NotTo(HaveOccurred())
out := &corev1.Pod{}
Expect(json.Unmarshal(mutatedRaw, out)).To(Succeed())
return out
}
// vllmPod returns a minimal args-only vLLM pod (no command override) bound to
// the test engine via annotation. mutate may further customize it.
func vllmPod(mutate func(*corev1.Pod)) *corev1.Pod {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Annotations: map[string]string{
AnnotationEngine: testEngineName,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "vllm",
Image: "vllm/vllm-openai:latest",
Args: []string{"--model", "Qwen/Qwen2.5-0.5B"},
},
},
},
}
if mutate != nil {
mutate(pod)
}
return pod
}
// findContainer returns the named container from the pod, or nil.
func findContainer(pod *corev1.Pod, name string) *corev1.Container {
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == name {
return &pod.Spec.Containers[i]
}
}
return nil
}
// envValue returns the value of the named env var on the container, or "".
func envValue(c *corev1.Container, name string) string {
for _, e := range c.Env {
if e.Name == name {
return e.Value
}
}
return ""
}
// pullSecretNames returns the names in the pod's imagePullSecrets.
func pullSecretNames(pod *corev1.Pod) []string {
out := make([]string, 0, len(pod.Spec.ImagePullSecrets))
for _, s := range pod.Spec.ImagePullSecrets {
out = append(out, s.Name)
}
return out
}
var _ = Describe("CacheBlendPodInjector", func() {
ctx := context.Background()
Describe("full M0M7 injection", func() {
It("injects all required mutations for an opted-in pod", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
By("M0: hostIPC")
Expect(out.Spec.HostIPC).To(BeTrue())
By("M1: cb-plugin emptyDir volume")
var vol *corev1.Volume
for i := range out.Spec.Volumes {
if out.Spec.Volumes[i].Name == cbPluginVolumeName {
vol = &out.Spec.Volumes[i]
}
}
Expect(vol).NotTo(BeNil())
Expect(vol.EmptyDir).NotTo(BeNil())
By("M2: payload init container with image + pull policy + SHARED_DIR")
Expect(out.Spec.InitContainers).To(HaveLen(1))
init := out.Spec.InitContainers[0]
Expect(init.Image).To(Equal("registry.example.com/lmcache/cacheblend-payload:pinned"))
Expect(init.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent))
Expect(init.Command).To(BeEmpty())
Expect(envValue(&init, cbSharedDirEnvName)).To(Equal(cbPluginMountPath))
Expect(init.VolumeMounts).To(HaveLen(1))
Expect(init.VolumeMounts[0].Name).To(Equal(cbPluginVolumeName))
Expect(init.VolumeMounts[0].MountPath).To(Equal(cbPluginMountPath))
Expect(init.VolumeMounts[0].ReadOnly).To(BeFalse())
c := findContainer(out, "vllm")
Expect(c).NotTo(BeNil())
By("M3: read-only mount on the target container")
var mount *corev1.VolumeMount
for i := range c.VolumeMounts {
if c.VolumeMounts[i].Name == cbPluginVolumeName {
mount = &c.VolumeMounts[i]
}
}
Expect(mount).NotTo(BeNil())
Expect(mount.ReadOnly).To(BeTrue())
Expect(mount.MountPath).To(Equal(cbPluginMountPath))
By("M4: PYTHONPATH on the container")
Expect(envValue(c, pythonPathEnvName)).To(Equal(cbPluginMountPath))
By("M5: required vLLM args asserted individually")
Expect(argsHasFlagValue(c.Args, cbFlagAttentionBackend, cbValAttentionBackend)).To(BeTrue(),
"--attention-backend=CUSTOM")
Expect(argsHasFlag(c.Args, cbFlagNoChunkedPrefill)).To(BeTrue(),
"--no-enable-chunked-prefill")
Expect(argsHasFlagValue(c.Args, cbFlagBlockSize, cbValBlockSize)).To(BeTrue(),
"--block-size=64")
Expect(argsHasFlagValue(c.Args, cbFlagPipelineParallelSize, cbValPipelineParallelSize)).To(BeTrue(),
"--pipeline-parallel-size=1")
Expect(argsHasFlag(c.Args, cbFlagNoAsyncScheduling)).To(BeTrue(),
"--no-async-scheduling")
Expect(argsHasFlag(c.Args, cbFlagEnforceEager)).To(BeTrue(),
"default cudagraph eager -> --enforce-eager")
By("M5: --kv-transfer-config carries CBKVConnector + tcp:// host")
kv := argsFlagValue(c.Args, cbFlagKVTransferConfig)
Expect(kv).NotTo(BeEmpty())
Expect(kv).To(ContainSubstring("CBKVConnector"))
Expect(kv).To(ContainSubstring(testSvcHost))
By("M7: injection.imagePullSecrets appended to spec.imagePullSecrets")
Expect(pullSecretNames(out)).To(ContainElement("cb-payload-pull"))
By("M6: idempotency annotation stamped")
Expect(out.Annotations[AnnotationInjected]).To(Equal(valueTrue))
Expect(out.Annotations).NotTo(HaveKey(AnnotationSkipReason))
})
})
Describe("M7 image pull secrets", func() {
It("does not duplicate a secret the pod already lists", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: "cb-payload-pull"}}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(pullSecretNames(out)).To(Equal([]string{"cb-payload-pull"}))
})
It("honors the cacheblend-image-pull-secrets annotation override", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Annotations[AnnotationImagePullSecrets] = "override-a, override-b"
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
names := pullSecretNames(out)
Expect(names).To(ContainElements("override-a", "override-b"))
Expect(names).NotTo(ContainElement("cb-payload-pull"))
})
})
Describe("gating", func() {
It("allows a pod with no engine annotation unchanged", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
delete(p.Annotations, AnnotationEngine)
})
resp := injector.Handle(ctx, makeRequest(pod))
Expect(resp.Allowed).To(BeTrue())
Expect(resp.Patches).To(BeEmpty())
})
It("skips + stamps engine-not-found when the connection ConfigMap is absent", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, false) // no connection ConfigMap seeded
pod := vllmPod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[AnnotationSkipReason]).To(Equal(SkipReasonEngineNotFound))
Expect(out.Annotations).NotTo(HaveKey(AnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
Expect(out.Spec.InitContainers).To(BeEmpty())
})
It("skips + stamps engine-not-found when the engine CR is absent", func() {
engine := newTestEngine(nil)
// Seed an injector whose engine name differs from the pod's annotation.
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Annotations[AnnotationEngine] = "does-not-exist"
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[AnnotationSkipReason]).To(Equal(SkipReasonEngineNotFound))
Expect(out.Spec.HostIPC).To(BeFalse())
})
It("allows an already-injected pod as a no-op", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Annotations[AnnotationInjected] = valueTrue
})
resp := injector.Handle(ctx, makeRequest(pod))
Expect(resp.Allowed).To(BeTrue())
Expect(resp.Patches).To(BeEmpty())
})
It("skips + stamps command-override when the target container overrides command", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers[0].Command = []string{"/bin/sh", "-c", "exec vllm serve"}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[AnnotationSkipReason]).To(Equal(SkipReasonCommandOverride))
Expect(out.Annotations).NotTo(HaveKey(AnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
Expect(out.Spec.InitContainers).To(BeEmpty())
})
})
Describe("append-or-replace arg semantics", func() {
It("replaces a pre-existing --attention-backend value", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers[0].Args = []string{"--attention-backend", "FLASH_ATTN", "--model", "m"}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, "vllm")
Expect(argsHasFlagValue(c.Args, cbFlagAttentionBackend, cbValAttentionBackend)).To(BeTrue())
Expect(argsHasFlagValue(c.Args, cbFlagAttentionBackend, "FLASH_ATTN")).To(BeFalse())
// Not duplicated.
Expect(countFlag(c.Args, cbFlagAttentionBackend)).To(Equal(1))
})
It("replaces a pre-existing --attention-backend=value (single-token form)", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers[0].Args = []string{"--attention-backend=FLASH_ATTN", "--model", "m"}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, "vllm")
Expect(c.Args).To(ContainElement("--attention-backend=CUSTOM"))
Expect(c.Args).NotTo(ContainElement("--attention-backend=FLASH_ATTN"))
})
It("skips + stamps when the user already supplies --kv-transfer-config", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers[0].Args = []string{"--kv-transfer-config", `{"kv_connector":"Other"}`}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, "vllm")
By("the user's kv-transfer-config JSON is untouched")
Expect(argsFlagValue(c.Args, cbFlagKVTransferConfig)).To(Equal(`{"kv_connector":"Other"}`))
Expect(argsFlagValue(c.Args, cbFlagKVTransferConfig)).NotTo(ContainSubstring("CBKVConnector"))
By("the skip reason is stamped but the rest of the injection still applies")
Expect(out.Annotations[AnnotationSkipReason]).To(Equal(SkipReasonKVTransferConfigPresent))
Expect(out.Annotations[AnnotationInjected]).To(Equal(valueTrue))
Expect(out.Spec.HostIPC).To(BeTrue())
Expect(argsHasFlagValue(c.Args, cbFlagBlockSize, cbValBlockSize)).To(BeTrue())
})
It("prepends to a pre-existing PYTHONPATH", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers[0].Env = []corev1.EnvVar{
{Name: "PYTHONPATH", Value: "/opt/extra"},
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, "vllm")
Expect(envValue(c, pythonPathEnvName)).To(Equal("/cb-plugin:/opt/extra"))
})
})
Describe("target container resolution", func() {
It("injects into the annotation-named non-first container", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Annotations[AnnotationContainer] = "vllm"
p.Spec.Containers = []corev1.Container{
{Name: "sidecar", Image: "busybox", Args: []string{"sleep"}},
{Name: "vllm", Image: "vllm/vllm-openai:latest", Args: []string{"--model", "m"}},
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
By("the vLLM container is mutated")
vllm := findContainer(out, "vllm")
Expect(envValue(vllm, pythonPathEnvName)).To(Equal(cbPluginMountPath))
Expect(argsHasFlagValue(vllm.Args, cbFlagAttentionBackend, cbValAttentionBackend)).To(BeTrue())
By("the sidecar container is untouched")
sidecar := findContainer(out, "sidecar")
Expect(envValue(sidecar, pythonPathEnvName)).To(BeEmpty())
Expect(sidecar.Args).To(Equal([]string{"sleep"}))
})
It("uses the engine injection.targetContainer default when set", func() {
named := "vllm"
engine := newTestEngine(func(e *lmcachev1alpha1.CacheBlendEngine) {
e.Spec.Injection.TargetContainer = &named
})
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Spec.Containers = []corev1.Container{
{Name: "sidecar", Image: "busybox", Args: []string{"sleep"}},
{Name: "vllm", Image: "vllm/vllm-openai:latest", Args: []string{"--model", "m"}},
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
vllm := findContainer(out, "vllm")
Expect(argsHasFlagValue(vllm.Args, cbFlagAttentionBackend, cbValAttentionBackend)).To(BeTrue())
sidecar := findContainer(out, "sidecar")
Expect(sidecar.Args).To(Equal([]string{"sleep"}))
})
It("skips + stamps target-container-not-found for an unknown container name", func() {
engine := newTestEngine(nil)
injector := newPodInjector(engine, true)
pod := vllmPod(func(p *corev1.Pod) {
p.Annotations[AnnotationContainer] = "does-not-exist"
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[AnnotationSkipReason]).To(Equal(SkipReasonTargetContainerNotFound))
Expect(out.Annotations).NotTo(HaveKey(AnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
Expect(out.Spec.InitContainers).To(BeEmpty())
By("the original vLLM container is left untouched")
vllm := findContainer(out, "vllm")
Expect(envValue(vllm, pythonPathEnvName)).To(BeEmpty())
})
})
Describe("cudagraph modes", func() {
It("emits decode-only compilation config for full_decode_only", func() {
mode := lmcachev1alpha1.CudagraphFullDecodeOnly
engine := newTestEngine(func(e *lmcachev1alpha1.CacheBlendEngine) {
e.Spec.Injection.Cudagraph = &mode
})
injector := newPodInjector(engine, true)
pod := vllmPod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, "vllm")
Expect(argsHasFlag(c.Args, cbFlagEnforceEager)).To(BeFalse())
Expect(argsFlagValue(c.Args, cbFlagCompilationConfig)).To(ContainSubstring("FULL_DECODE_ONLY"))
})
})
})
// --- test arg helpers (mirror the package's two-token / =-token recognition) ---
// argsHasFlagValue reports whether args carries flag with the given value in
// either the two-token or single-token form.
func argsHasFlagValue(args []string, flag, value string) bool {
return argsFlagValue(args, flag) == value
}
// argsFlagValue returns the value bound to flag in args (two-token or
// single-token form), or "" if the flag is absent.
func argsFlagValue(args []string, flag string) string {
eqPrefix := flag + "="
for i := range len(args) {
if args[i] == flag && i+1 < len(args) {
return args[i+1]
}
if after, ok := strings.CutPrefix(args[i], eqPrefix); ok {
return after
}
}
return ""
}
// countFlag returns how many times flag appears (two-token or single-token).
func countFlag(args []string, flag string) int {
eqPrefix := flag + "="
n := 0
for _, a := range args {
if a == flag || strings.HasPrefix(a, eqPrefix) {
n++
}
}
return n
}