From 6206f6b199235122a615daf36bec84c98922ad28 Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 2 Sep 2025 10:54:08 +0300 Subject: [PATCH 1/6] Added a test configuration structure Signed-off-by: Shmuel Kallner --- test/utils/utils.go | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/utils/utils.go b/test/utils/utils.go index 8cc19b7ac..c7dceb31f 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -39,8 +39,61 @@ import ( v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2" + "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/env" ) +const ( + // defaultExistsTimeout is the default timeout for a resource to exist in the api server. + defaultExistsTimeout = 30 * time.Second + // defaultReadyTimeout is the default timeout for a resource to report a ready state. + defaultReadyTimeout = 3 * time.Minute + // defaultModelReadyTimeout is the default timeout for the model server deployment to report a ready state. + defaultModelReadyTimeout = 10 * time.Minute + // defaultInterval is the default interval to check if a resource exists or ready conditions. + defaultInterval = time.Millisecond * 250 +) + +type TestConfig struct { + Context context.Context + KubeCli *kubernetes.Clientset + K8sClient client.Client + RestConfig *rest.Config + NsName string + Scheme *runtime.Scheme + ExistsTimeout time.Duration + ReadyTimeout time.Duration + ModelReadyTimeout time.Duration + Interval time.Duration +} + +func NewTestConfig(nsName string) *TestConfig { + cfg := config.GetConfigOrDie() + gomega.Expect(cfg).NotTo(gomega.BeNil()) + + kubeCli, err := kubernetes.NewForConfig(cfg) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(kubeCli).NotTo(gomega.BeNil()) + + return &TestConfig{ + Context: context.Background(), + KubeCli: kubeCli, + NsName: nsName, + RestConfig: cfg, + Scheme: runtime.NewScheme(), + ExistsTimeout: env.GetEnvDuration("EXISTS_TIMEOUT", defaultExistsTimeout, ginkgo.GinkgoLogr), + ReadyTimeout: env.GetEnvDuration("READY_TIMEOUT", defaultReadyTimeout, ginkgo.GinkgoLogr), + ModelReadyTimeout: env.GetEnvDuration("MODEL_READY_TIMEOUT", defaultModelReadyTimeout, ginkgo.GinkgoLogr), + Interval: defaultInterval, + } +} + +func (testConfig *TestConfig) CreateCli() { + var err error + testConfig.K8sClient, err = client.New(testConfig.RestConfig, client.Options{Scheme: testConfig.Scheme}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(testConfig.K8sClient).NotTo(gomega.BeNil()) +} + // DeleteClusterResources deletes all cluster-scoped objects the tests typically create. func DeleteClusterResources(ctx context.Context, cli client.Client) error { binding := &rbacv1.ClusterRoleBinding{ From 5168979d24b36f5050880c17c48d48b892d8fe69 Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 2 Sep 2025 10:57:10 +0300 Subject: [PATCH 2/6] Explot new test configuration structure Signed-off-by: Shmuel Kallner --- test/utils/utils.go | 99 ++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/test/utils/utils.go b/test/utils/utils.go index c7dceb31f..b8f19f7dc 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -95,13 +95,13 @@ func (testConfig *TestConfig) CreateCli() { } // DeleteClusterResources deletes all cluster-scoped objects the tests typically create. -func DeleteClusterResources(ctx context.Context, cli client.Client) error { +func DeleteClusterResources(testConfig *TestConfig) error { binding := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "auth-reviewer-binding", }, } - err := cli.Delete(ctx, binding, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err := testConfig.K8sClient.Delete(testConfig.Context, binding, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -110,7 +110,7 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { Name: "auth-reviewer", }, } - err = cli.Delete(ctx, role, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.Delete(testConfig.Context, role, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -119,7 +119,7 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { Name: "inference-gateway-sa-metrics-reader-role-binding", }, } - err = cli.Delete(ctx, metricsReaderBinding, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.Delete(testConfig.Context, metricsReaderBinding, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -128,7 +128,7 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { Name: "inference-gateway-metrics-reader", }, } - err = cli.Delete(ctx, metricsReaderRole, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.Delete(testConfig.Context, metricsReaderRole, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -137,7 +137,7 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { Name: "inferenceobjectives.inference.networking.x-k8s.io", }, } - err = cli.Delete(ctx, model, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.Delete(testConfig.Context, model, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -146,7 +146,7 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { Name: "inferencepools.inference.networking.x-k8s.io", }, } - err = cli.Delete(ctx, pool, client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.Delete(testConfig.Context, pool, client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -155,49 +155,49 @@ func DeleteClusterResources(ctx context.Context, cli client.Client) error { // DeleteNamespacedResources deletes all namespace-scoped objects the tests typically create. // The given namespace will also be deleted if it's not "default". -func DeleteNamespacedResources(ctx context.Context, cli client.Client, ns string) error { - if ns == "" { +func DeleteNamespacedResources(testConfig *TestConfig) error { + if testConfig.NsName == "" { return nil } - err := cli.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err := testConfig.K8sClient.DeleteAllOf(testConfig.Context, &appsv1.Deployment{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &corev1.Service{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &corev1.Service{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &corev1.Pod{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &corev1.Pod{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &corev1.ConfigMap{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &corev1.ConfigMap{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &corev1.Secret{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &corev1.Secret{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &corev1.ServiceAccount{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &corev1.ServiceAccount{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &v1.InferencePool{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &v1.InferencePool{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - err = cli.DeleteAllOf(ctx, &v1alpha2.InferenceObjective{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err = testConfig.K8sClient.DeleteAllOf(testConfig.Context, &v1alpha2.InferenceObjective{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } - if ns != "default" { + if testConfig.NsName != "default" { ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: ns, + Name: testConfig.NsName, }, } - if err := cli.Delete(ctx, ns, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !apierrors.IsNotFound(err) { + if err := testConfig.K8sClient.Delete(testConfig.Context, ns, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !apierrors.IsNotFound(err) { return err } } @@ -205,11 +205,11 @@ func DeleteNamespacedResources(ctx context.Context, cli client.Client, ns string } // DeleteInferenceObjectiveResources deletes all InferenceObjective objects in the given namespace. -func DeleteInferenceObjectiveResources(ctx context.Context, cli client.Client, ns string) error { - if ns == "" { +func DeleteInferenceObjectiveResources(testConfig *TestConfig) error { + if testConfig.NsName == "" { return nil } - err := cli.DeleteAllOf(ctx, &v1alpha2.InferenceObjective{}, client.InNamespace(ns), client.PropagationPolicy(metav1.DeletePropagationForeground)) + err := testConfig.K8sClient.DeleteAllOf(testConfig.Context, &v1alpha2.InferenceObjective{}, client.InNamespace(testConfig.NsName), client.PropagationPolicy(metav1.DeletePropagationForeground)) if err != nil && !apierrors.IsNotFound(err) { return err } @@ -217,7 +217,7 @@ func DeleteInferenceObjectiveResources(ctx context.Context, cli client.Client, n } // PodReady checks if the given Pod reports the "Ready" status condition before the given timeout. -func PodReady(ctx context.Context, cli client.Client, pod *corev1.Pod, timeout, interval time.Duration) { +func PodReady(testConfig *TestConfig, pod *corev1.Pod) { ginkgo.By(fmt.Sprintf("Checking pod %s/%s status is: %s", pod.Namespace, pod.Name, corev1.PodReady)) conditions := []corev1.PodCondition{ { @@ -225,13 +225,14 @@ func PodReady(ctx context.Context, cli client.Client, pod *corev1.Pod, timeout, Status: corev1.ConditionTrue, }, } - gomega.Eventually(checkPodStatus, timeout, interval).WithArguments(ctx, cli, pod, conditions).Should(gomega.BeTrue()) + gomega.Eventually(checkPodStatus, testConfig.ExistsTimeout, testConfig.Interval). + WithArguments(testConfig, pod, conditions).Should(gomega.BeTrue()) } // checkPodStatus checks if the given Pod status matches the expected conditions. -func checkPodStatus(ctx context.Context, cli client.Client, pod *corev1.Pod, conditions []corev1.PodCondition) (bool, error) { +func checkPodStatus(testConfig *TestConfig, pod *corev1.Pod, conditions []corev1.PodCondition) (bool, error) { var fetchedPod corev1.Pod - if err := cli.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, &fetchedPod); err != nil { + if err := testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, &fetchedPod); err != nil { return false, err } found := 0 @@ -246,7 +247,7 @@ func checkPodStatus(ctx context.Context, cli client.Client, pod *corev1.Pod, con } // DeploymentAvailable checks if the given Deployment reports the "Available" status condition before the given timeout. -func DeploymentAvailable(ctx context.Context, cli client.Client, deploy *appsv1.Deployment, timeout, interval time.Duration) { +func DeploymentAvailable(testConfig *TestConfig, deploy *appsv1.Deployment) { ginkgo.By(fmt.Sprintf("Checking if deployment %s/%s status is: %s", deploy.Namespace, deploy.Name, appsv1.DeploymentAvailable)) conditions := []appsv1.DeploymentCondition{ { @@ -254,19 +255,21 @@ func DeploymentAvailable(ctx context.Context, cli client.Client, deploy *appsv1. Status: corev1.ConditionTrue, }, } - gomega.Eventually(checkDeploymentStatus, timeout, interval).WithArguments(ctx, cli, deploy, conditions).Should(gomega.BeTrue()) + gomega.Eventually(checkDeploymentStatus, testConfig.ModelReadyTimeout, testConfig.Interval). + WithArguments(testConfig.Context, testConfig.K8sClient, deploy, conditions). + Should(gomega.BeTrue()) } // DeploymentReadyReplicas checks if the given Deployment has at least `count` ready replicas before the given timeout. -func DeploymentReadyReplicas(ctx context.Context, cli client.Client, deploy *appsv1.Deployment, count int, timeout, interval time.Duration) { +func DeploymentReadyReplicas(testConfig *TestConfig, deploy *appsv1.Deployment, count int) { ginkgo.By(fmt.Sprintf("Checking if deployment %s/%s has at least %d ready replica(s)", deploy.Namespace, deploy.Name, count)) gomega.Eventually(func(g gomega.Gomega) { var fetchedDeploy appsv1.Deployment - err := cli.Get(ctx, types.NamespacedName{Namespace: deploy.Namespace, Name: deploy.Name}, &fetchedDeploy) + err := testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: deploy.Namespace, Name: deploy.Name}, &fetchedDeploy) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(fetchedDeploy.Status.ReadyReplicas).To(gomega.BeNumerically(">=", count), fmt.Sprintf("Deployment only has %d ready replicas, want at least %d", fetchedDeploy.Status.ReadyReplicas, count)) - }, timeout, interval).Should(gomega.Succeed()) + }, testConfig.ModelReadyTimeout, testConfig.Interval).Should(gomega.Succeed()) } // checkDeploymentStatus checks if the given Deployment status matches the expected conditions. @@ -287,7 +290,7 @@ func checkDeploymentStatus(ctx context.Context, cli client.Client, deploy *appsv } // CRDEstablished checks if the given CRD reports the "Established" status condition before the given timeout. -func CRDEstablished(ctx context.Context, cli client.Client, crd *apiextv1.CustomResourceDefinition, timeout, interval time.Duration) { +func CRDEstablished(testConfig *TestConfig, crd *apiextv1.CustomResourceDefinition) { ginkgo.By(fmt.Sprintf("Checking CRD %s status is: %s", crd.Name, apiextv1.Established)) conditions := []apiextv1.CustomResourceDefinitionCondition{ { @@ -295,7 +298,9 @@ func CRDEstablished(ctx context.Context, cli client.Client, crd *apiextv1.Custom Status: apiextv1.ConditionTrue, }, } - gomega.Eventually(checkCrdStatus, timeout, interval).WithArguments(ctx, cli, crd, conditions).Should(gomega.BeTrue()) + gomega.Eventually(checkCrdStatus, testConfig.ReadyTimeout, testConfig.Interval). + WithArguments(testConfig.Context, testConfig.K8sClient, crd, conditions). + Should(gomega.BeTrue()) } // checkCrdStatus checks if the given CRD status matches the expected conditions. @@ -321,22 +326,14 @@ func checkCrdStatus( } // ExecCommandInPod runs a command in a given container of a given Pod, returning combined stdout+stderr. -func ExecCommandInPod( - ctx context.Context, - cfg *rest.Config, - scheme *runtime.Scheme, - kubeClient *kubernetes.Clientset, - podNamespace, podName, containerName string, - cmd []string, -) (string, error) { - - parameterCodec := runtime.NewParameterCodec(scheme) +func ExecCommandInPod(testConfig *TestConfig, podName, containerName string, cmd []string) (string, error) { + parameterCodec := runtime.NewParameterCodec(testConfig.Scheme) - req := kubeClient.CoreV1().RESTClient(). + req := testConfig.KubeCli.CoreV1().RESTClient(). Post(). Resource("pods"). Name(podName). - Namespace(podNamespace). + Namespace(testConfig.NsName). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, @@ -347,13 +344,13 @@ func ExecCommandInPod( TTY: false, }, parameterCodec) - exec, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL()) + exec, err := remotecommand.NewSPDYExecutor(testConfig.RestConfig, "POST", req.URL()) if err != nil { return "", fmt.Errorf("could not initialize executor: %w", err) } var stdout, stderr bytes.Buffer - execErr := exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + execErr := exec.StreamWithContext(testConfig.Context, remotecommand.StreamOptions{ Stdout: &stdout, Stderr: &stderr, }) @@ -369,8 +366,10 @@ func ExecCommandInPod( // EventuallyExists checks if a Kubernetes resource exists and returns nil if successful. // It takes a function `getResource` which retrieves the resource and returns an error if it doesn't exist. -func EventuallyExists(ctx context.Context, getResource func() error, timeout, interval time.Duration) { +func EventuallyExists(testConfig *TestConfig, getResource func() error) { gomega.Eventually(func() error { return getResource() - }, timeout, interval).Should(gomega.Succeed()) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) +} + } From e3802c7f663d830b471cc6672eb64831b6797136 Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 2 Sep 2025 10:58:13 +0300 Subject: [PATCH 3/6] Refactored private test helper functions into common test code Signed-off-by: Shmuel Kallner --- test/utils/utils.go | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/test/utils/utils.go b/test/utils/utils.go index b8f19f7dc..06f93bb48 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -20,6 +20,8 @@ import ( "bytes" "context" "fmt" + "os" + "strings" "time" "github.com/onsi/ginkgo/v2" @@ -30,12 +32,15 @@ import ( apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2" @@ -372,4 +377,128 @@ func EventuallyExists(testConfig *TestConfig, getResource func() error) { }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) } +// CreateObjsFromYaml creates K8S objects from yaml and waits for them to be instantiated +func CreateObjsFromYaml(testConfig *TestConfig, docs []string) []string { + objNames := []string{} + + // For each doc, decode and create + decoder := serializer.NewCodecFactory(testConfig.Scheme).UniversalDeserializer() + for _, doc := range docs { + trimmed := strings.TrimSpace(doc) + if trimmed == "" { + continue + } + // Decode into a runtime.Object + obj, gvk, decodeErr := decoder.Decode([]byte(trimmed), nil, nil) + gomega.Expect(decodeErr).NotTo(gomega.HaveOccurred(), + "Failed to decode YAML document to a Kubernetes object") + + ginkgo.By(fmt.Sprintf("Decoded GVK: %s", gvk)) + + unstrObj, ok := obj.(*unstructured.Unstructured) + if !ok { + // Fallback if it's a typed object + unstrObj = &unstructured.Unstructured{} + // Convert typed to unstructured + err := testConfig.Scheme.Convert(obj, unstrObj, nil) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + } + + unstrObj.SetNamespace(testConfig.NsName) + kind := unstrObj.GetKind() + name := unstrObj.GetName() + objNames = append(objNames, kind+"/"+name) + + // Create the object + err := testConfig.K8sClient.Create(testConfig.Context, unstrObj, &client.CreateOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), + "Failed to create object from YAML") + + // Wait for the created object to exist. + clientObj := getClientObject(kind) + EventuallyExists(testConfig, func() error { + return testConfig.K8sClient.Get(testConfig.Context, + types.NamespacedName{Namespace: testConfig.NsName, Name: name}, clientObj) + }) + + switch kind { + case "CustomResourceDefinition": + // Wait for the CRD to be established. + CRDEstablished(testConfig, clientObj.(*apiextv1.CustomResourceDefinition)) + case "Deployment": + // Wait for the deployment to be available. + DeploymentAvailable(testConfig, clientObj.(*appsv1.Deployment)) + case "Pod": + // Wait for the pod to be ready. + PodReady(testConfig, clientObj.(*corev1.Pod)) + } + } + return objNames +} + +func DeleteObjects(testConfig *TestConfig, kindAndNames []string) { + for _, kindAndName := range kindAndNames { + split := strings.Split(kindAndName, "/") + clientObj := getClientObject(split[0]) + err := testConfig.K8sClient.Get(testConfig.Context, + types.NamespacedName{Namespace: testConfig.NsName, Name: split[1]}, clientObj) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = testConfig.K8sClient.Delete(testConfig.Context, clientObj) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Eventually(func() bool { + clientObj := getClientObject(split[0]) + err := testConfig.K8sClient.Get(testConfig.Context, + types.NamespacedName{Namespace: testConfig.NsName, Name: split[1]}, clientObj) + return apierrors.IsNotFound(err) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.BeTrue()) + } +} + +// applyYAMLFile reads a file containing YAML (possibly multiple docs) +// and applies each object to the cluster. +func ApplyYAMLFile(testConfig *TestConfig, filePath string) { + // Create the resources from the manifest file + CreateObjsFromYaml(testConfig, ReadYaml(filePath)) +} + +// ReadYaml is a helper function to read in K8S YAML files and split by the --- separator +func ReadYaml(filePath string) []string { + ginkgo.By("Reading YAML file: " + filePath) + yamlBytes, err := os.ReadFile(filePath) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Split multiple docs, if needed + return strings.Split(string(yamlBytes), "\n---") +} + +func getClientObject(kind string) client.Object { + switch strings.ToLower(kind) { + case "clusterrole": + return &rbacv1.ClusterRole{} + case "clusterrolebinding": + return &rbacv1.ClusterRoleBinding{} + case "configmap": + return &corev1.ConfigMap{} + case "customresourcedefinition": + return &apiextv1.CustomResourceDefinition{} + case "deployment": + return &appsv1.Deployment{} + case "inferencepool": + return &v1.InferencePool{} + case "pod": + return &corev1.Pod{} + case "role": + return &rbacv1.Role{} + case "rolebinding": + return &rbacv1.RoleBinding{} + case "secret": + return &corev1.Secret{} + case "service": + return &corev1.Service{} + case "serviceaccount": + return &corev1.ServiceAccount{} + default: + ginkgo.Fail("unsupported K8S kind "+kind, 1) + return nil + } } From f464495f34e698689cb7cb95990ad7865f853588 Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 2 Sep 2025 10:59:27 +0300 Subject: [PATCH 4/6] Changes due to refactoring of private test helper functions Signed-off-by: Shmuel Kallner --- test/e2e/epp/e2e_suite_test.go | 310 ++++++++------------------------- test/e2e/epp/e2e_test.go | 58 +++--- 2 files changed, 100 insertions(+), 268 deletions(-) diff --git a/test/e2e/epp/e2e_suite_test.go b/test/e2e/epp/e2e_suite_test.go index e6e5b83ba..b41f108f8 100644 --- a/test/e2e/epp/e2e_suite_test.go +++ b/test/e2e/epp/e2e_suite_test.go @@ -17,7 +17,6 @@ limitations under the License. package epp import ( - "context" "errors" "fmt" "os" @@ -29,34 +28,20 @@ import ( "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" infextv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" infextv1a2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2" + "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/env" testutils "sigs.k8s.io/gateway-api-inference-extension/test/utils" ) const ( - // defaultExistsTimeout is the default timeout for a resource to exist in the api server. - defaultExistsTimeout = 30 * time.Second - // defaultReadyTimeout is the default timeout for a resource to report a ready state. - defaultReadyTimeout = 3 * time.Minute - // defaultModelReadyTimeout is the default timeout for the model server deployment to report a ready state. - defaultModelReadyTimeout = 10 * time.Minute // defaultCurlTimeout is the default timeout for the curl command to get a response. defaultCurlTimeout = 30 * time.Second - // defaultInterval is the default interval to check if a resource exists or ready conditions. - defaultInterval = time.Millisecond * 250 // defaultCurlInterval is the default interval to run the test curl command. defaultCurlInterval = time.Second * 5 // defaultNsName is the default name of the Namespace used for tests. Can override using the E2E_NS environment variable. @@ -100,13 +85,8 @@ const ( const e2eLeaderElectionEnabledEnvVar = "E2E_LEADER_ELECTION_ENABLED" var ( - ctx = context.Background() - cli client.Client + testConfig *testutils.TestConfig // Required for exec'ing in curl pod - kubeCli *kubernetes.Clientset - scheme = runtime.NewScheme() - cfg = config.GetConfigOrDie() - nsName string e2eImage string leaderElectionEnabled bool ) @@ -119,10 +99,12 @@ func TestAPIs(t *testing.T) { } var _ = ginkgo.BeforeSuite(func() { - nsName = os.Getenv("E2E_NS") + nsName := os.Getenv("E2E_NS") if nsName == "" { nsName = defaultNsName } + testConfig = testutils.NewTestConfig(nsName) + e2eImage = os.Getenv("E2E_IMAGE") gomega.Expect(e2eImage).NotTo(gomega.BeEmpty(), "E2E_IMAGE environment variable is not set") @@ -143,11 +125,11 @@ func setupInfra() { // run this before createNs to fail fast in case it doesn't. modelServerManifestPath := readModelServerManifestPath() - createNamespace(cli, nsName) + createNamespace(testConfig) modelServerManifestArray := getYamlsFromModelServerManifest(modelServerManifestPath) if strings.Contains(modelServerManifestArray[0], "hf-token") { - createHfSecret(cli, modelServerSecretManifest) + createHfSecret(testConfig, modelServerSecretManifest) } crds := map[string]string{ "inferencepools.inference.networking.x-k8s.io": xInferPoolManifest, @@ -155,19 +137,19 @@ func setupInfra() { "inferencepools.inference.networking.k8s.io": inferPoolManifest, } - createCRDs(cli, crds) + createCRDs(testConfig, crds) inferExtManifestPath := inferExtManifestDefault if leaderElectionEnabled { inferExtManifestPath = inferExtManifestLeaderElection } - createInferExt(cli, inferExtManifestPath) - createClient(cli, clientManifest) - createEnvoy(cli, envoyManifest) - createMetricsRbac(cli, metricsRbacManifest) + createInferExt(testConfig, inferExtManifestPath) + createClient(testConfig, clientManifest) + createEnvoy(testConfig, envoyManifest) + createMetricsRbac(testConfig, metricsRbacManifest) // Run this step last, as it requires additional time for the model server to become ready. ginkgo.By("Creating model server resources from manifest: " + modelServerManifestPath) - createModelServer(cli, modelServerManifestArray) + createModelServer(testConfig, modelServerManifestArray) } var _ = ginkgo.AfterSuite(func() { @@ -192,77 +174,57 @@ var _ = ginkgo.AfterSuite(func() { // setupSuite initializes the test suite by setting up the Kubernetes client, // loading required API schemes, and validating configuration. func setupSuite() { - gomega.ExpectWithOffset(1, cfg).NotTo(gomega.BeNil()) - - err := clientgoscheme.AddToScheme(scheme) + err := clientgoscheme.AddToScheme(testConfig.Scheme) gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) - err = apiextv1.AddToScheme(scheme) + err = apiextv1.AddToScheme(testConfig.Scheme) gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) - err = infextv1a2.Install(scheme) + err = infextv1a2.Install(testConfig.Scheme) gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) - err = infextv1.Install(scheme) + err = infextv1.Install(testConfig.Scheme) gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) - cli, err = client.New(cfg, client.Options{Scheme: scheme}) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(cli).NotTo(gomega.BeNil()) - - kubeCli, err = kubernetes.NewForConfig(cfg) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(kubeCli).NotTo(gomega.BeNil()) + testConfig.CreateCli() } func cleanupResources() { - if cli == nil { + if testConfig.K8sClient == nil { return // could happen if BeforeSuite had an error } - gomega.Expect(testutils.DeleteClusterResources(ctx, cli)).To(gomega.Succeed()) - gomega.Expect(testutils.DeleteNamespacedResources(ctx, cli, nsName)).To(gomega.Succeed()) + gomega.Expect(testutils.DeleteClusterResources(testConfig)).To(gomega.Succeed()) + gomega.Expect(testutils.DeleteNamespacedResources(testConfig)).To(gomega.Succeed()) } func cleanupInferModelResources() { - gomega.Expect(testutils.DeleteInferenceObjectiveResources(ctx, cli, nsName)).To(gomega.Succeed()) -} - -func getTimeout(key string, fallback time.Duration) time.Duration { - if value, ok := os.LookupEnv(key); ok { - if parsed, err := time.ParseDuration(value); err == nil { - return parsed - } - } - return fallback + gomega.Expect(testutils.DeleteInferenceObjectiveResources(testConfig)).To(gomega.Succeed()) } var ( - existsTimeout = getTimeout("EXISTS_TIMEOUT", defaultExistsTimeout) - readyTimeout = getTimeout("READY_TIMEOUT", defaultReadyTimeout) - modelReadyTimeout = getTimeout("MODEL_READY_TIMEOUT", defaultModelReadyTimeout) - curlTimeout = getTimeout("CURL_TIMEOUT", defaultCurlTimeout) - interval = defaultInterval - curlInterval = defaultCurlInterval + curlTimeout = env.GetEnvDuration("CURL_TIMEOUT", defaultCurlTimeout, ginkgo.GinkgoLogr) + curlInterval = defaultCurlInterval ) -func createNamespace(k8sClient client.Client, ns string) { - ginkgo.By("Creating e2e namespace: " + ns) +func createNamespace(testConfig *testutils.TestConfig) { + ginkgo.By("Creating e2e namespace: " + testConfig.NsName) obj := &corev1.Namespace{ ObjectMeta: v1.ObjectMeta{ - Name: ns, + Name: testConfig.NsName, }, } - err := k8sClient.Create(ctx, obj) + err := testConfig.K8sClient.Create(testConfig.Context, obj) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create e2e test namespace") } // namespaceExists ensures that a specified namespace exists and is ready for use. -func namespaceExists(k8sClient client.Client, ns string) { - ginkgo.By("Ensuring namespace exists: " + ns) - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: ns}, &corev1.Namespace{}) - }, existsTimeout, interval) +func namespaceExists(testConfig *testutils.TestConfig) { + ginkgo.By("Ensuring namespace exists: " + testConfig.NsName) + testutils.EventuallyExists(testConfig, func() error { + return testConfig.K8sClient.Get(testConfig.Context, + types.NamespacedName{Name: testConfig.NsName}, &corev1.Namespace{}) + }) } // readModelServerManifestPath reads from env var the absolute filepath to model server deployment for testing. @@ -275,57 +237,39 @@ func readModelServerManifestPath() string { func getYamlsFromModelServerManifest(modelServerManifestPath string) []string { ginkgo.By("Ensuring the model server manifest points to an existing file") - modelServerManifestArray := readYaml(modelServerManifestPath) + modelServerManifestArray := testutils.ReadYaml(modelServerManifestPath) gomega.Expect(modelServerManifestArray).NotTo(gomega.BeEmpty()) return modelServerManifestArray } // createCRDs creates the Inference Extension CRDs used for testing. -func createCRDs(k8sClient client.Client, crds map[string]string) { - for name, path := range crds { +func createCRDs(testConfig *testutils.TestConfig, crds map[string]string) { + for _, path := range crds { ginkgo.By("Creating CRD resource from manifest: " + path) - applyYAMLFile(k8sClient, path) - - // Wait for the CRD to exist. - crd := &apiextv1.CustomResourceDefinition{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd) - }, existsTimeout, interval) - - // Wait for the CRD to be established. - testutils.CRDEstablished(ctx, k8sClient, crd, readyTimeout, interval) + testutils.ApplyYAMLFile(testConfig, path) } } // createClient creates the client pod used for testing from the given filePath. -func createClient(k8sClient client.Client, filePath string) { +func createClient(testConfig *testutils.TestConfig, filePath string) { ginkgo.By("Creating client resources from manifest: " + filePath) - applyYAMLFile(k8sClient, filePath) - - // Wait for the pod to exist. - pod := &corev1.Pod{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: "curl"}, pod) - }, existsTimeout, interval) - - // Wait for the pod to be ready. - testutils.PodReady(ctx, k8sClient, pod, readyTimeout, interval) + testutils.ApplyYAMLFile(testConfig, filePath) } // createMetricsRbac creates the metrics RBAC resources from the manifest file. -func createMetricsRbac(k8sClient client.Client, filePath string) { - inManifests := readYaml(filePath) +func createMetricsRbac(testConfig *testutils.TestConfig, filePath string) { + inManifests := testutils.ReadYaml(filePath) ginkgo.By("Replacing placeholder namespace with E2E_NS environment variable") outManifests := []string{} for _, m := range inManifests { - outManifests = append(outManifests, strings.ReplaceAll(m, "$E2E_NS", nsName)) + outManifests = append(outManifests, strings.ReplaceAll(m, "$E2E_NS", testConfig.NsName)) } ginkgo.By("Creating RBAC resources for scraping metrics from manifest: " + filePath) - createObjsFromYaml(k8sClient, outManifests) + testutils.CreateObjsFromYaml(testConfig, outManifests) // wait for sa token to exist - testutils.EventuallyExists(ctx, func() error { - token, err := getMetricsReaderToken(k8sClient) + testutils.EventuallyExists(testConfig, func() error { + token, err := getMetricsReaderToken(testConfig.K8sClient) if err != nil { return err } @@ -333,30 +277,21 @@ func createMetricsRbac(k8sClient client.Client, filePath string) { return errors.New("failed to get metrics reader token") } return nil - }, existsTimeout, interval) + }) } // createModelServer creates the model server resources used for testing from the given filePaths. -func createModelServer(k8sClient client.Client, modelServerManifestArray []string) { - createObjsFromYaml(k8sClient, modelServerManifestArray) - - // Wait for the deployment to exist. - deploy := &appsv1.Deployment{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: modelServerName}, deploy) - }, existsTimeout, interval) - - // Wait for the deployment to be available. - testutils.DeploymentAvailable(ctx, k8sClient, deploy, modelReadyTimeout, interval) +func createModelServer(testConfig *testutils.TestConfig, modelServerManifestArray []string) { + testutils.CreateObjsFromYaml(testConfig, modelServerManifestArray) } // createHfSecret read HF_TOKEN from env var and creates a secret that contains the access token. -func createHfSecret(k8sClient client.Client, secretPath string) { +func createHfSecret(testConfig *testutils.TestConfig, secretPath string) { ginkgo.By("Ensuring the HF_TOKEN environment variable is set") token := os.Getenv("HF_TOKEN") gomega.Expect(token).NotTo(gomega.BeEmpty(), "HF_TOKEN is not set") - inManifests := readYaml(secretPath) + inManifests := testutils.ReadYaml(secretPath) ginkgo.By("Replacing placeholder secret data with HF_TOKEN environment variable") outManifests := []string{} for _, m := range inManifests { @@ -364,152 +299,49 @@ func createHfSecret(k8sClient client.Client, secretPath string) { } ginkgo.By("Creating model server secret resource") - createObjsFromYaml(k8sClient, outManifests) - - // Wait for the secret to exist before proceeding with test. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: "hf-token"}, &corev1.Secret{}) - }, existsTimeout, interval) + testutils.CreateObjsFromYaml(testConfig, outManifests) } // createEnvoy creates the envoy proxy resources used for testing from the given filePath. -func createEnvoy(k8sClient client.Client, filePath string) { - inManifests := readYaml(filePath) +func createEnvoy(testConfig *testutils.TestConfig, filePath string) { + inManifests := testutils.ReadYaml(filePath) ginkgo.By("Replacing placeholder namespace with E2E_NS environment variable") outManifests := []string{} for _, m := range inManifests { - outManifests = append(outManifests, strings.ReplaceAll(m, "$E2E_NS", nsName)) + outManifests = append(outManifests, strings.ReplaceAll(m, "$E2E_NS", testConfig.NsName)) } ginkgo.By("Creating envoy proxy resources from manifest: " + filePath) - createObjsFromYaml(k8sClient, outManifests) - - // Wait for the configmap to exist before proceeding with test. - cfgMap := &corev1.ConfigMap{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: envoyName}, cfgMap) - }, existsTimeout, interval) - - // Wait for the deployment to exist. - deploy := &appsv1.Deployment{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: envoyName}, deploy) - }, existsTimeout, interval) - - // Wait for the deployment to be available. - testutils.DeploymentAvailable(ctx, k8sClient, deploy, readyTimeout, interval) - - // Wait for the service to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: envoyName}, &corev1.Service{}) - }, existsTimeout, interval) + testutils.CreateObjsFromYaml(testConfig, outManifests) } // createInferExt creates the inference extension resources used for testing from the given filePath. -func createInferExt(k8sClient client.Client, filePath string) { - inManifests := readYaml(filePath) +func createInferExt(testConfig *testutils.TestConfig, filePath string) { + inManifests := testutils.ReadYaml(filePath) ginkgo.By("Replacing placeholders with environment variables") outManifests := []string{} + replacer := strings.NewReplacer( + "$E2E_NS", testConfig.NsName, + "$E2E_IMAGE", e2eImage, + ) for _, manifest := range inManifests { - replacer := strings.NewReplacer( - "$E2E_NS", nsName, - "$E2E_IMAGE", e2eImage, - ) outManifests = append(outManifests, replacer.Replace(manifest)) } ginkgo.By("Creating inference extension resources from manifest: " + filePath) - createObjsFromYaml(k8sClient, outManifests) - - // Wait for the serviceaccount to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, &corev1.ServiceAccount{}) - }, existsTimeout, interval) - - // Wait for the role to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: "pod-read"}, &rbacv1.Role{}) - }, existsTimeout, interval) - - // Wait for the rolebinding to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: "pod-read-binding"}, &rbacv1.RoleBinding{}) - }, existsTimeout, interval) - - // Wait for the clusterrole to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: "auth-reviewer"}, &rbacv1.ClusterRole{}) - }, existsTimeout, interval) - - // Wait for the clusterrolebinding to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: "auth-reviewer-binding"}, &rbacv1.ClusterRoleBinding{}) - }, existsTimeout, interval) + testutils.CreateObjsFromYaml(testConfig, outManifests) // Wait for the deployment to exist. - deploy := &appsv1.Deployment{} - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, deploy) - }, existsTimeout, interval) - + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: inferExtName, + Namespace: testConfig.NsName, + }, + } if leaderElectionEnabled { // With leader election enabled, only 1 replica will be "Ready" at any given time (the leader). - testutils.DeploymentReadyReplicas(ctx, k8sClient, deploy, 1, modelReadyTimeout, interval) + testutils.DeploymentReadyReplicas(testConfig, deploy, 1) } else { - testutils.DeploymentAvailable(ctx, k8sClient, deploy, modelReadyTimeout, interval) - } - - // Wait for the service to exist. - testutils.EventuallyExists(ctx, func() error { - return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, &corev1.Service{}) - }, existsTimeout, interval) -} - -// applyYAMLFile reads a file containing YAML (possibly multiple docs) -// and applies each object to the cluster. -func applyYAMLFile(k8sClient client.Client, filePath string) { - // Create the resources from the manifest file - createObjsFromYaml(k8sClient, readYaml(filePath)) -} - -func readYaml(filePath string) []string { - ginkgo.By("Reading YAML file: " + filePath) - yamlBytes, err := os.ReadFile(filePath) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - // Split multiple docs, if needed - return strings.Split(string(yamlBytes), "\n---") -} - -func createObjsFromYaml(k8sClient client.Client, docs []string) { - // For each doc, decode and create - decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() - for _, doc := range docs { - trimmed := strings.TrimSpace(doc) - if trimmed == "" { - continue - } - // Decode into a runtime.Object - obj, gvk, decodeErr := decoder.Decode([]byte(trimmed), nil, nil) - gomega.Expect(decodeErr).NotTo(gomega.HaveOccurred(), - "Failed to decode YAML document to a Kubernetes object") - - ginkgo.By(fmt.Sprintf("Decoded GVK: %s", gvk)) - - unstrObj, ok := obj.(*unstructured.Unstructured) - if !ok { - // Fallback if it's a typed object - unstrObj = &unstructured.Unstructured{} - // Convert typed to unstructured - err := scheme.Convert(obj, unstrObj, nil) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - } - - unstrObj.SetNamespace(nsName) - - // Create the object - err := k8sClient.Create(ctx, unstrObj) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), - "Failed to create object from YAML") + testutils.DeploymentAvailable(testConfig, deploy) } } diff --git a/test/e2e/epp/e2e_test.go b/test/e2e/epp/e2e_test.go index e01240d8c..f9e996c4b 100644 --- a/test/e2e/epp/e2e_test.go +++ b/test/e2e/epp/e2e_test.go @@ -43,23 +43,23 @@ var _ = ginkgo.Describe("InferencePool", func() { var infObjective *v1alpha2.InferenceObjective ginkgo.BeforeEach(func() { ginkgo.By("Waiting for the namespace to exist.") - namespaceExists(cli, nsName) + namespaceExists(testConfig) ginkgo.By("Creating an InferenceObjective resource") - infObjective = newInferenceObjective(nsName) - gomega.Expect(cli.Create(ctx, infObjective)).To(gomega.Succeed()) + infObjective = newInferenceObjective(testConfig.NsName) + gomega.Expect(testConfig.K8sClient.Create(testConfig.Context, infObjective)).To(gomega.Succeed()) ginkgo.By("Ensuring the InferenceObjective resource exists in the namespace") gomega.Eventually(func() error { - return cli.Get(ctx, types.NamespacedName{Namespace: infObjective.Namespace, Name: infObjective.Name}, infObjective) - }, existsTimeout, interval).Should(gomega.Succeed()) + return testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: infObjective.Namespace, Name: infObjective.Name}, infObjective) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) }) ginkgo.AfterEach(func() { ginkgo.By("Deleting the InferenceObjective test resource.") cleanupInferModelResources() gomega.Eventually(func() error { - err := cli.Get(ctx, types.NamespacedName{Namespace: infObjective.Namespace, Name: infObjective.Name}, infObjective) + err := testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: infObjective.Namespace, Name: infObjective.Name}, infObjective) if err == nil { return errors.New("InferenceObjective resource still exists") } @@ -67,7 +67,7 @@ var _ = ginkgo.Describe("InferencePool", func() { return nil } return nil - }, existsTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) }) ginkgo.When("The Inference Extension is running", func() { @@ -89,7 +89,7 @@ var _ = ginkgo.Describe("InferencePool", func() { ginkgo.By("Verifying that exactly one EPP pod is ready") gomega.Eventually(func(g gomega.Gomega) { podList := &corev1.PodList{} - err := cli.List(ctx, podList, client.InNamespace(nsName), client.MatchingLabels{"app": inferExtName}) + err := testConfig.K8sClient.List(testConfig.Context, podList, client.InNamespace(testConfig.NsName), client.MatchingLabels{"app": inferExtName}) g.Expect(err).NotTo(gomega.HaveOccurred()) // The deployment should have 3 replicas for leader election. @@ -104,7 +104,7 @@ var _ = ginkgo.Describe("InferencePool", func() { } } g.Expect(readyPods).To(gomega.Equal(1), "Expected exactly one pod to be ready") - }, readyTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, testConfig.Interval).Should(gomega.Succeed()) }) ginkgo.It("Should successfully failover and serve traffic after the leader pod is deleted", func() { @@ -121,26 +121,26 @@ var _ = ginkgo.Describe("InferencePool", func() { ginkgo.By("Found initial leader pod: " + oldLeaderPod.Name) ginkgo.By(fmt.Sprintf("Deleting leader pod %s to trigger failover", oldLeaderPod.Name)) - gomega.Expect(cli.Delete(ctx, oldLeaderPod)).To(gomega.Succeed()) + gomega.Expect(testConfig.K8sClient.Delete(testConfig.Context, oldLeaderPod)).To(gomega.Succeed()) ginkgo.By("STEP 3: Waiting for a new leader to be elected") // The deployment controller will create a new pod. We need to wait for the total number of pods // to be back to 3, and for one of the other pods to become the new leader. deploy := &appsv1.Deployment{} gomega.Eventually(func() error { - return cli.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, deploy) - }, existsTimeout, interval).Should(gomega.Succeed()) + return testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: testConfig.NsName, Name: inferExtName}, deploy) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) // Wait for one replica to become ready again. - testutils.DeploymentReadyReplicas(ctx, cli, deploy, 1, readyTimeout, interval) + testutils.DeploymentReadyReplicas(testConfig, deploy, 1) // Also wait for the total number of replicas to be back to 3. gomega.Eventually(func(g gomega.Gomega) { d := &appsv1.Deployment{} - err := cli.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, d) + err := testConfig.K8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: testConfig.NsName, Name: inferExtName}, d) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(d.Status.Replicas).To(gomega.Equal(int32(3)), "Deployment should have 3 replicas") - }, readyTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, testConfig.Interval).Should(gomega.Succeed()) ginkgo.By("STEP 4: Verifying a new, different leader is elected") var newLeaderPod *corev1.Pod @@ -152,7 +152,7 @@ var _ = ginkgo.Describe("InferencePool", func() { // This guards against a race condition where we might find the old leader // before its status is updated to NotReady. g.Expect(newLeaderPod.Name).NotTo(gomega.Equal(oldLeaderPod.Name), "The new leader should not be the same as the old deleted leader") - }, readyTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, testConfig.Interval).Should(gomega.Succeed()) ginkgo.By("Found new leader pod: " + newLeaderPod.Name) ginkgo.By("STEP 5: Verifying the new leader is working correctly after failover") @@ -207,11 +207,11 @@ func verifyTrafficRouting() { // Ensure the expected responses include the InferenceObjective target model names. var expected []string expected = append(expected, targetModelName) - curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, t.api, t.promptOrMessages, false) + curlCmd := getCurlCommand(envoyName, testConfig.NsName, envoyPort, modelName, curlTimeout, t.api, t.promptOrMessages, false) actual := make(map[string]int) gomega.Eventually(func() error { - resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd) + resp, err := testutils.ExecCommandInPod(testConfig, "curl", "curl", curlCmd) if err != nil { return err } @@ -232,7 +232,7 @@ func verifyTrafficRouting() { return fmt.Errorf("actual (%v) != expected (%v); resp=%q", got, expected, resp) } return nil - }, readyTimeout, curlInterval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, curlInterval).Should(gomega.Succeed()) } } @@ -260,18 +260,18 @@ func verifyMetrics() { // Generate traffic by sending requests through the inference extension ginkgo.By("Generating traffic through the inference extension") - curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, "/completions", "Write as if you were a critic: San Francisco", true) + curlCmd := getCurlCommand(envoyName, testConfig.NsName, envoyPort, modelName, curlTimeout, "/completions", "Write as if you were a critic: San Francisco", true) // Run the curl command multiple times to generate some metrics data for i := 0; i < 5; i++ { - _, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd) + _, err := testutils.ExecCommandInPod(testConfig, "curl", "curl", curlCmd) gomega.Expect(err).NotTo(gomega.HaveOccurred()) } // modify the curl command to generate some error metrics curlCmd[len(curlCmd)-1] = "invalid input" for i := 0; i < 5; i++ { - _, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd) + _, err := testutils.ExecCommandInPod(testConfig, "curl", "curl", curlCmd) gomega.Expect(err).NotTo(gomega.HaveOccurred()) } @@ -282,11 +282,11 @@ func verifyMetrics() { // Get the authorization token for reading metrics token := "" gomega.Eventually(func(g gomega.Gomega) { - t, err := getMetricsReaderToken(cli) + t, err := getMetricsReaderToken(testConfig.K8sClient) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(t).NotTo(gomega.BeEmpty()) token = t - }, existsTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ExistsTimeout, testConfig.Interval).Should(gomega.Succeed()) // Construct the metric scraping curl command using Pod IP metricScrapeCmd := getMetricsScrapeCommand(podIP, token) @@ -294,7 +294,7 @@ func verifyMetrics() { ginkgo.By("Verifying that all expected metrics are present.") gomega.Eventually(func() error { // Execute the metrics scrape command inside the curl pod - resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", metricScrapeCmd) + resp, err := testutils.ExecCommandInPod(testConfig, "curl", "curl", metricScrapeCmd) if err != nil { return err } @@ -309,12 +309,12 @@ func verifyMetrics() { } } return nil - }, readyTimeout, curlInterval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, curlInterval).Should(gomega.Succeed()) } func getMetricsReaderToken(k8sClient client.Client) (string, error) { secret := &corev1.Secret{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: metricsReaderSecretName}, secret) + err := k8sClient.Get(testConfig.Context, types.NamespacedName{Namespace: testConfig.NsName, Name: metricsReaderSecretName}, secret) if err != nil { return "", err } @@ -327,7 +327,7 @@ func findReadyPod() *corev1.Pod { var readyPod *corev1.Pod gomega.Eventually(func(g gomega.Gomega) { podList := &corev1.PodList{} - err := cli.List(ctx, podList, client.InNamespace(nsName), client.MatchingLabels{"app": inferExtName}) + err := testConfig.K8sClient.List(testConfig.Context, podList, client.InNamespace(testConfig.NsName), client.MatchingLabels{"app": inferExtName}) g.Expect(err).NotTo(gomega.HaveOccurred()) foundReadyPod := false @@ -346,7 +346,7 @@ func findReadyPod() *corev1.Pod { } } g.Expect(foundReadyPod).To(gomega.BeTrue(), "No ready EPP pod found") - }, readyTimeout, interval).Should(gomega.Succeed()) + }, testConfig.ReadyTimeout, testConfig.Interval).Should(gomega.Succeed()) return readyPod } From 2c4093ae3572f720178f8ea51ee53871535e0aae Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 2 Sep 2025 11:00:33 +0300 Subject: [PATCH 5/6] Create RBAC and ConfigMap before deployment Signed-off-by: Shmuel Kallner --- test/testdata/inferencepool-e2e.yaml | 162 ++++++++--------- .../inferencepool-leader-election-e2e.yaml | 170 +++++++++--------- 2 files changed, 166 insertions(+), 166 deletions(-) diff --git a/test/testdata/inferencepool-e2e.yaml b/test/testdata/inferencepool-e2e.yaml index b8d7fb697..77d454e6f 100644 --- a/test/testdata/inferencepool-e2e.yaml +++ b/test/testdata/inferencepool-e2e.yaml @@ -37,6 +37,87 @@ metadata: name: vllm-llama3-8b-instruct-epp namespace: $E2E_NS --- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pod-read + namespace: $E2E_NS +rules: +- apiGroups: [ "inference.networking.x-k8s.io" ] + resources: [ "inferenceobjectives", "inferencepools" ] + verbs: [ "get", "watch", "list" ] +- apiGroups: [ "inference.networking.k8s.io" ] + resources: [ "inferencepools" ] + verbs: [ "get", "watch", "list" ] +- apiGroups: [ "" ] + resources: [ "pods" ] + verbs: [ "get", "watch", "list" ] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pod-read-binding + namespace: $E2E_NS +subjects: +- kind: ServiceAccount + name: vllm-llama3-8b-instruct-epp + namespace: $E2E_NS +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: pod-read +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: auth-reviewer +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: auth-reviewer-binding +subjects: +- kind: ServiceAccount + name: vllm-llama3-8b-instruct-epp + namespace: $E2E_NS +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: auth-reviewer +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: plugins-config + namespace: $E2E_NS +data: + default-plugins.yaml: | + apiVersion: inference.networking.x-k8s.io/v1alpha1 + kind: EndpointPickerConfig + plugins: + - type: queue-scorer + - type: kv-cache-utilization-scorer + - type: prefix-cache-scorer + schedulingProfiles: + - name: default + plugins: + - pluginRef: queue-scorer + - pluginRef: kv-cache-utilization-scorer + - pluginRef: prefix-cache-scorer +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -100,84 +181,3 @@ spec: - name: plugins-config-volume configMap: name: plugins-config ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: plugins-config - namespace: $E2E_NS -data: - default-plugins.yaml: | - apiVersion: inference.networking.x-k8s.io/v1alpha1 - kind: EndpointPickerConfig - plugins: - - type: queue-scorer - - type: kv-cache-utilization-scorer - - type: prefix-cache-scorer - schedulingProfiles: - - name: default - plugins: - - pluginRef: queue-scorer - - pluginRef: kv-cache-utilization-scorer - - pluginRef: prefix-cache-scorer ---- -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: pod-read - namespace: $E2E_NS -rules: -- apiGroups: [ "inference.networking.x-k8s.io" ] - resources: [ "inferenceobjectives", "inferencepools" ] - verbs: [ "get", "watch", "list" ] -- apiGroups: [ "inference.networking.k8s.io" ] - resources: [ "inferencepools" ] - verbs: [ "get", "watch", "list" ] -- apiGroups: [ "" ] - resources: [ "pods" ] - verbs: [ "get", "watch", "list" ] ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: pod-read-binding - namespace: $E2E_NS -subjects: -- kind: ServiceAccount - name: vllm-llama3-8b-instruct-epp - namespace: $E2E_NS -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: pod-read ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: auth-reviewer -rules: -- apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: auth-reviewer-binding -subjects: -- kind: ServiceAccount - name: vllm-llama3-8b-instruct-epp - namespace: $E2E_NS -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: auth-reviewer diff --git a/test/testdata/inferencepool-leader-election-e2e.yaml b/test/testdata/inferencepool-leader-election-e2e.yaml index 9ba5dcb4a..976fbbd02 100644 --- a/test/testdata/inferencepool-leader-election-e2e.yaml +++ b/test/testdata/inferencepool-leader-election-e2e.yaml @@ -35,91 +35,6 @@ metadata: name: vllm-llama3-8b-instruct-epp namespace: $E2E_NS --- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: vllm-llama3-8b-instruct-epp - namespace: $E2E_NS - labels: - app: vllm-llama3-8b-instruct-epp -spec: - replicas: 3 - selector: - matchLabels: - app: vllm-llama3-8b-instruct-epp - template: - metadata: - labels: - app: vllm-llama3-8b-instruct-epp - spec: - serviceAccountName: vllm-llama3-8b-instruct-epp - # Conservatively, this timeout should mirror the longest grace period of the pods within the pool - terminationGracePeriodSeconds: 130 - containers: - - name: epp - image: $E2E_IMAGE - imagePullPolicy: IfNotPresent - args: - - --pool-name - - "vllm-llama3-8b-instruct" - - --pool-namespace - - "$E2E_NS" - - --v - - "4" - - --zap-encoder - - "json" - - --grpc-port - - "9002" - - --grpc-health-port - - "9003" - - --ha-enable-leader-election - - "--config-file" - - "/config/default-plugins.yaml" - ports: - - containerPort: 9002 - - containerPort: 9003 - - name: metrics - containerPort: 9090 - livenessProbe: - grpc: - port: 9003 - service: liveness - initialDelaySeconds: 5 - periodSeconds: 10 - readinessProbe: - grpc: - port: 9003 - service: readiness - initialDelaySeconds: 5 - periodSeconds: 10 - volumeMounts: - - name: plugins-config-volume - mountPath: "/config" - volumes: - - name: plugins-config-volume - configMap: - name: plugins-config ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: plugins-config - namespace: $E2E_NS -data: - default-plugins.yaml: | - apiVersion: inference.networking.x-k8s.io/v1alpha1 - kind: EndpointPickerConfig - plugins: - - type: queue-scorer - - type: kv-cache-utilization-scorer - - type: prefix-cache-scorer - schedulingProfiles: - - name: default - plugins: - - pluginRef: queue-scorer - - pluginRef: kv-cache-utilization-scorer - - pluginRef: prefix-cache-scorer ---- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -207,3 +122,88 @@ roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: auth-reviewer +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: plugins-config + namespace: $E2E_NS +data: + default-plugins.yaml: | + apiVersion: inference.networking.x-k8s.io/v1alpha1 + kind: EndpointPickerConfig + plugins: + - type: queue-scorer + - type: kv-cache-utilization-scorer + - type: prefix-cache-scorer + schedulingProfiles: + - name: default + plugins: + - pluginRef: queue-scorer + - pluginRef: kv-cache-utilization-scorer + - pluginRef: prefix-cache-scorer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vllm-llama3-8b-instruct-epp + namespace: $E2E_NS + labels: + app: vllm-llama3-8b-instruct-epp +spec: + replicas: 3 + selector: + matchLabels: + app: vllm-llama3-8b-instruct-epp + template: + metadata: + labels: + app: vllm-llama3-8b-instruct-epp + spec: + serviceAccountName: vllm-llama3-8b-instruct-epp + # Conservatively, this timeout should mirror the longest grace period of the pods within the pool + terminationGracePeriodSeconds: 130 + containers: + - name: epp + image: $E2E_IMAGE + imagePullPolicy: IfNotPresent + args: + - --pool-name + - "vllm-llama3-8b-instruct" + - --pool-namespace + - "$E2E_NS" + - --v + - "4" + - --zap-encoder + - "json" + - --grpc-port + - "9002" + - --grpc-health-port + - "9003" + - --ha-enable-leader-election + - "--config-file" + - "/config/default-plugins.yaml" + ports: + - containerPort: 9002 + - containerPort: 9003 + - name: metrics + containerPort: 9090 + livenessProbe: + grpc: + port: 9003 + service: liveness + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + grpc: + port: 9003 + service: readiness + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: plugins-config-volume + mountPath: "/config" + volumes: + - name: plugins-config-volume + configMap: + name: plugins-config From fe412beb9f7bee644ad03f19b2c7a684a4cec0ea Mon Sep 17 00:00:00 2001 From: Shmuel Kallner Date: Tue, 16 Sep 2025 22:22:24 +0300 Subject: [PATCH 6/6] Fixed review issues Signed-off-by: Shmuel Kallner --- test/utils/utils.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/utils/utils.go b/test/utils/utils.go index 06f93bb48..b65834cb6 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -58,6 +58,7 @@ const ( defaultInterval = time.Millisecond * 250 ) +// TestConfig groups various fields together for use in the test helpers type TestConfig struct { Context context.Context KubeCli *kubernetes.Clientset @@ -71,6 +72,7 @@ type TestConfig struct { Interval time.Duration } +// NewTestConfig creates a new TestConfig instance func NewTestConfig(nsName string) *TestConfig { cfg := config.GetConfigOrDie() gomega.Expect(cfg).NotTo(gomega.BeNil()) @@ -92,6 +94,7 @@ func NewTestConfig(nsName string) *TestConfig { } } +// CreateCli creates the Kubernetes client used in the tests, invoked after the scheme has been setup. func (testConfig *TestConfig) CreateCli() { var err error testConfig.K8sClient, err = client.New(testConfig.RestConfig, client.Options{Scheme: testConfig.Scheme}) @@ -436,6 +439,7 @@ func CreateObjsFromYaml(testConfig *TestConfig, docs []string) []string { return objNames } +// DeleteObjects deletes set of Kubernetes objects in the form of kind/name func DeleteObjects(testConfig *TestConfig, kindAndNames []string) { for _, kindAndName := range kindAndNames { split := strings.Split(kindAndName, "/")