Files
2026-07-13 12:24:33 +08:00

475 lines
18 KiB
Go

/*
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"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"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 (
// testContainerVLLM is the default target container name in these specs.
testContainerVLLM = "vllm"
// testUnknownContainer names a container that does not exist on the pod.
testUnknownContainer = "does-not-exist"
// testPayloadRepo is the payload image repository used in staging specs.
testPayloadRepo = "registry.example.com/lmcache/lmcache-payload"
// testPayloadPullSecret is the engine's default payload pull secret name.
testPayloadPullSecret = "lmcache-payload-pull"
)
// newLMCacheInjector returns an LMCachePodInjector backed by a fake client,
// seeded with the test engine's connection ConfigMap when seedConn is true. The
// LMCacheEngine CR itself is not seeded — the injector reads only the ConfigMap.
func newLMCacheInjector(seedConn bool) *LMCachePodInjector {
scheme := newTestScheme()
builder := fake.NewClientBuilder().WithScheme(scheme)
if seedConn {
engine := &lmcachev1alpha1.LMCacheEngine{
ObjectMeta: metav1.ObjectMeta{Name: testEngineName, Namespace: testNamespace},
}
builder = builder.WithRuntimeObjects(runtime.Object(resources.BuildConnectionConfigMap(engine)))
}
return &LMCachePodInjector{
Client: builder.Build(),
Decoder: admission.NewDecoder(scheme),
}
}
// lmcacheEngineWithInjection returns an LMCacheEngine whose name/namespace match
// the connection ConfigMap the injector reads. When payloadRepo is non-empty the
// engine carries an injection sub-spec with that payload image (tag "pinned") and
// a default payload pull secret; an empty payloadRepo yields injection=nil.
func lmcacheEngineWithInjection(payloadRepo string) *lmcachev1alpha1.LMCacheEngine {
engine := &lmcachev1alpha1.LMCacheEngine{
ObjectMeta: metav1.ObjectMeta{Name: testEngineName, Namespace: testNamespace},
Spec: lmcachev1alpha1.LMCacheEngineSpec{L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10}},
}
if payloadRepo != "" {
tag := "pinned"
engine.Spec.Injection = &lmcachev1alpha1.LMCacheInjectionSpec{
PayloadImage: &lmcachev1alpha1.ImageSpec{Repository: &payloadRepo, Tag: &tag},
ImagePullSecrets: []corev1.LocalObjectReference{{Name: testPayloadPullSecret}},
}
}
return engine
}
// newLMCacheInjectorForEngine returns an injector seeded with the given engine CR
// and its connection ConfigMap. Unlike newLMCacheInjector this seeds the CR
// itself, exercising the injection sub-spec the injector reads.
func newLMCacheInjectorForEngine(engine *lmcachev1alpha1.LMCacheEngine) *LMCachePodInjector {
scheme := newTestScheme()
builder := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(
engine, resources.BuildConnectionConfigMap(engine),
)
return &LMCachePodInjector{
Client: builder.Build(),
Decoder: admission.NewDecoder(scheme),
}
}
// findVolume returns the named volume from the pod, or nil.
func findVolume(pod *corev1.Pod, name string) *corev1.Volume {
for i := range pod.Spec.Volumes {
if pod.Spec.Volumes[i].Name == name {
return &pod.Spec.Volumes[i]
}
}
return nil
}
// findVolumeMount returns the named volume mount from the container, or nil.
func findVolumeMount(c *corev1.Container, name string) *corev1.VolumeMount {
for i := range c.VolumeMounts {
if c.VolumeMounts[i].Name == name {
return &c.VolumeMounts[i]
}
}
return nil
}
// lmcachePod returns a minimal args-only vLLM pod (no command override) opted in
// to LMCache injection via annotation. mutate may further customize it.
func lmcachePod(mutate func(*corev1.Pod)) *corev1.Pod {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Annotations: map[string]string{
LMCacheAnnotationEngine: testEngineName,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: testContainerVLLM,
Image: "vllm/vllm-openai:latest",
Args: []string{"--model", "Qwen/Qwen2.5-0.5B"},
},
},
},
}
if mutate != nil {
mutate(pod)
}
return pod
}
var _ = Describe("LMCachePodInjector", func() {
ctx := context.Background()
Describe("full injection", func() {
It("injects hostIPC, --kv-transfer-config, and PYTHONHASHSEED for an opted-in pod", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
By("hostIPC")
Expect(out.Spec.HostIPC).To(BeTrue())
c := findContainer(out, testContainerVLLM)
Expect(c).NotTo(BeNil())
By("--kv-transfer-config carries LMCacheMPConnector + tcp:// host")
kv := argsFlagValue(c.Args, lmcFlagKVTransferConfig)
Expect(kv).NotTo(BeEmpty())
Expect(kv).To(ContainSubstring("LMCacheMPConnector"))
Expect(kv).To(ContainSubstring(testSvcHost))
By("the original --model arg is preserved")
Expect(argsFlagValue(c.Args, "--model")).To(Equal("Qwen/Qwen2.5-0.5B"))
By("PYTHONHASHSEED set to 0")
Expect(envValue(c, pythonHashSeedEnvName)).To(Equal(pythonHashSeedValue))
By("idempotency annotation stamped, no skip reason")
Expect(out.Annotations[LMCacheAnnotationInjected]).To(Equal(valueTrue))
Expect(out.Annotations).NotTo(HaveKey(LMCacheAnnotationSkipReason))
})
It("respects a user-set PYTHONHASHSEED", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(func(p *corev1.Pod) {
p.Spec.Containers[0].Env = []corev1.EnvVar{
{Name: pythonHashSeedEnvName, Value: "42"},
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, testContainerVLLM)
Expect(envValue(c, pythonHashSeedEnvName)).To(Equal("42"))
})
})
Describe("gating", func() {
It("allows a pod with no engine annotation unchanged", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(func(p *corev1.Pod) {
delete(p.Annotations, LMCacheAnnotationEngine)
})
resp := injector.Handle(ctx, makeRequest(pod))
Expect(resp.Allowed).To(BeTrue())
Expect(resp.Patches).To(BeEmpty())
})
It("allows an already-injected pod as a no-op", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(func(p *corev1.Pod) {
p.Annotations[LMCacheAnnotationInjected] = valueTrue
})
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() {
injector := newLMCacheInjector(false) // no connection ConfigMap seeded
pod := lmcachePod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[LMCacheAnnotationSkipReason]).To(Equal(SkipReasonEngineNotFound))
Expect(out.Annotations).NotTo(HaveKey(LMCacheAnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
})
It("skips + stamps command-override when the target container overrides command", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(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[LMCacheAnnotationSkipReason]).To(Equal(SkipReasonCommandOverride))
Expect(out.Annotations).NotTo(HaveKey(LMCacheAnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
})
It("skips + stamps target-container-not-found for an unknown container name", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(func(p *corev1.Pod) {
p.Annotations[LMCacheAnnotationContainer] = testUnknownContainer
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Annotations[LMCacheAnnotationSkipReason]).To(Equal(SkipReasonTargetContainerNotFound))
Expect(out.Annotations).NotTo(HaveKey(LMCacheAnnotationInjected))
Expect(out.Spec.HostIPC).To(BeFalse())
})
})
Describe("user-supplied --kv-transfer-config", func() {
It("skips + stamps but still applies hostIPC + PYTHONHASHSEED", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(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, testContainerVLLM)
By("the user's kv-transfer-config JSON is untouched")
Expect(argsFlagValue(c.Args, lmcFlagKVTransferConfig)).To(Equal(`{"kv_connector":"Other"}`))
Expect(argsFlagValue(c.Args, lmcFlagKVTransferConfig)).NotTo(ContainSubstring("LMCacheMPConnector"))
By("the skip reason is stamped but the rest of the injection still applies")
Expect(out.Annotations[LMCacheAnnotationSkipReason]).To(Equal(SkipReasonKVTransferConfigPresent))
Expect(out.Annotations[LMCacheAnnotationInjected]).To(Equal(valueTrue))
Expect(out.Spec.HostIPC).To(BeTrue())
Expect(envValue(c, pythonHashSeedEnvName)).To(Equal(pythonHashSeedValue))
})
})
Describe("target container resolution", func() {
It("injects into the annotation-named non-first container", func() {
injector := newLMCacheInjector(true)
pod := lmcachePod(func(p *corev1.Pod) {
p.Annotations[LMCacheAnnotationContainer] = testContainerVLLM
p.Spec.Containers = []corev1.Container{
{Name: "sidecar", Image: "busybox", Args: []string{"sleep"}},
{Name: testContainerVLLM, 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, testContainerVLLM)
Expect(argsFlagValue(vllm.Args, lmcFlagKVTransferConfig)).To(ContainSubstring("LMCacheMPConnector"))
Expect(envValue(vllm, pythonHashSeedEnvName)).To(Equal(pythonHashSeedValue))
By("the sidecar container is untouched")
sidecar := findContainer(out, "sidecar")
Expect(sidecar.Args).To(Equal([]string{"sleep"}))
Expect(envValue(sidecar, pythonHashSeedEnvName)).To(BeEmpty())
})
})
Describe("code-payload staging", func() {
It("stages the payload (volume + init + mount + PYTHONPATH + secret) and still wires the connection", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(testPayloadRepo))
pod := lmcachePod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
By("emptyDir payload volume")
vol := findVolume(out, lmcPayloadVolumeName)
Expect(vol).NotTo(BeNil())
Expect(vol.EmptyDir).NotTo(BeNil())
By("payload init container with image + SHARED_DIR + read-write mount")
Expect(out.Spec.InitContainers).To(HaveLen(1))
init := out.Spec.InitContainers[0]
Expect(init.Name).To(Equal(lmcPayloadInitName))
Expect(init.Image).To(Equal(testPayloadRepo + ":pinned"))
Expect(envValue(&init, lmcSharedDirEnvName)).To(Equal(lmcPayloadMountPath))
By("no command override — the payload image's own busybox cp ENTRYPOINT runs")
Expect(init.Command).To(BeEmpty())
Expect(init.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent))
Expect(init.VolumeMounts).To(HaveLen(1))
Expect(init.VolumeMounts[0].Name).To(Equal(lmcPayloadVolumeName))
Expect(init.VolumeMounts[0].MountPath).To(Equal(lmcPayloadMountPath))
Expect(init.VolumeMounts[0].ReadOnly).To(BeFalse())
c := findContainer(out, testContainerVLLM)
By("read-only mount on the vLLM container")
mount := findVolumeMount(c, lmcPayloadVolumeName)
Expect(mount).NotTo(BeNil())
Expect(mount.ReadOnly).To(BeTrue())
Expect(mount.MountPath).To(Equal(lmcPayloadMountPath))
By("PYTHONPATH set to the payload mount path")
Expect(envValue(c, pythonPathEnvName)).To(Equal(lmcPayloadMountPath))
By("the engine's payload pull secret is merged onto the pod")
Expect(out.Spec.ImagePullSecrets).To(ContainElement(corev1.LocalObjectReference{Name: testPayloadPullSecret}))
By("connection wiring still applied")
Expect(out.Spec.HostIPC).To(BeTrue())
Expect(argsFlagValue(c.Args, lmcFlagKVTransferConfig)).To(ContainSubstring("LMCacheMPConnector"))
Expect(envValue(c, pythonHashSeedEnvName)).To(Equal(pythonHashSeedValue))
Expect(out.Annotations[LMCacheAnnotationInjected]).To(Equal(valueTrue))
})
It("does not stage when injection.payloadImage is unset (connection-only)", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(""))
pod := lmcachePod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Spec.InitContainers).To(BeEmpty())
Expect(findVolume(out, lmcPayloadVolumeName)).To(BeNil())
c := findContainer(out, testContainerVLLM)
Expect(envValue(c, pythonPathEnvName)).To(BeEmpty())
By("connection wiring still applied")
Expect(out.Spec.HostIPC).To(BeTrue())
Expect(argsFlagValue(c.Args, lmcFlagKVTransferConfig)).To(ContainSubstring("LMCacheMPConnector"))
})
It("does not stage when the engine CR is absent (connection-only via ConfigMap)", func() {
injector := newLMCacheInjector(true) // seeds only the connection ConfigMap, not the CR
pod := lmcachePod(nil)
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Spec.InitContainers).To(BeEmpty())
Expect(findVolume(out, lmcPayloadVolumeName)).To(BeNil())
Expect(out.Spec.HostIPC).To(BeTrue())
Expect(out.Annotations[LMCacheAnnotationInjected]).To(Equal(valueTrue))
})
It("prepends to a pre-existing PYTHONPATH", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(testPayloadRepo))
pod := lmcachePod(func(p *corev1.Pod) {
p.Spec.Containers[0].Env = []corev1.EnvVar{{Name: pythonPathEnvName, Value: "/opt/extra"}}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
c := findContainer(out, testContainerVLLM)
Expect(envValue(c, pythonPathEnvName)).To(Equal(lmcPayloadMountPath + ":/opt/extra"))
})
It("honors the image-pull-secrets annotation override", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(testPayloadRepo))
pod := lmcachePod(func(p *corev1.Pod) {
p.Annotations[LMCacheAnnotationImagePullSecrets] = "override-a,override-b"
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Spec.ImagePullSecrets).To(ContainElement(corev1.LocalObjectReference{Name: "override-a"}))
Expect(out.Spec.ImagePullSecrets).To(ContainElement(corev1.LocalObjectReference{Name: "override-b"}))
By("the annotation overrides the engine default rather than appending to it")
Expect(out.Spec.ImagePullSecrets).NotTo(ContainElement(corev1.LocalObjectReference{Name: testPayloadPullSecret}))
})
It("does not duplicate a payload pull secret the pod already lists", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(testPayloadRepo))
pod := lmcachePod(func(p *corev1.Pod) {
p.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: testPayloadPullSecret}}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
count := 0
for _, s := range out.Spec.ImagePullSecrets {
if s.Name == testPayloadPullSecret {
count++
}
}
Expect(count).To(Equal(1), "the engine's payload pull secret must be merged, not duplicated")
})
It("uses the engine injection.targetContainer default to select a non-first container", func() {
engine := lmcacheEngineWithInjection(testPayloadRepo)
targetName := testContainerVLLM
engine.Spec.Injection.TargetContainer = &targetName
injector := newLMCacheInjectorForEngine(engine)
pod := lmcachePod(func(p *corev1.Pod) {
p.Spec.Containers = []corev1.Container{
{Name: "sidecar", Image: "busybox", Args: []string{"sleep"}},
{Name: testContainerVLLM, Image: "vllm/vllm-openai:latest", Args: []string{"--model", "m"}},
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
By("the engine-named vLLM container is wired + staged")
vllm := findContainer(out, testContainerVLLM)
Expect(argsFlagValue(vllm.Args, lmcFlagKVTransferConfig)).To(ContainSubstring("LMCacheMPConnector"))
Expect(envValue(vllm, pythonPathEnvName)).To(Equal(lmcPayloadMountPath))
Expect(findVolumeMount(vllm, lmcPayloadVolumeName)).NotTo(BeNil())
By("the first (sidecar) container is untouched")
sidecar := findContainer(out, "sidecar")
Expect(sidecar.Args).To(Equal([]string{"sleep"}))
Expect(findVolumeMount(sidecar, lmcPayloadVolumeName)).To(BeNil())
})
It("is idempotent: pre-staged volume + init container are not duplicated", func() {
injector := newLMCacheInjectorForEngine(lmcacheEngineWithInjection(testPayloadRepo))
pod := lmcachePod(func(p *corev1.Pod) {
p.Spec.Volumes = []corev1.Volume{lmcStaging.volume()}
p.Spec.InitContainers = []corev1.Container{
lmcStaging.initContainer(testPayloadRepo+":pinned", corev1.PullIfNotPresent),
}
})
resp := injector.Handle(ctx, makeRequest(pod))
out := applyResponse(pod, resp)
Expect(out.Spec.Volumes).To(HaveLen(1))
Expect(out.Spec.InitContainers).To(HaveLen(1))
})
})
})