Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions infra/feast-operator/test/e2e_rhoai/e2e_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2025 Feast Community.

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 e2erhoai

import (
"fmt"
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

// Run e2e feast Notebook tests using the Ginkgo runner.
func TestNotebookRunE2E(t *testing.T) {
RegisterFailHandler(Fail)
_, _ = fmt.Fprintf(GinkgoWriter, "Feast Jupyter Notebook Test suite\n")
RunSpecs(t, "e2erhoai Feast Notebook test suite")
}
151 changes: 151 additions & 0 deletions infra/feast-operator/test/e2e_rhoai/feast_wb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copyright 2025 Feast Community.

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 e2erhoai provides end-to-end (E2E) test coverage for Feast integration with
// Red Hat OpenShift AI (RHOAI) environments. This specific test validates the functionality
// of executing a Feast Jupyter notebook within a fully configured OpenShift namespace
package e2erhoai

import (
"fmt"
"os"
"os/exec"
"strings"

utils "github.com/feast-dev/feast/infra/feast-operator/test/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Feast Jupyter Notebook Testing", Ordered, func() {
const (
namespace = "test-ns-feast-wb"
configMapName = "feast-wb-cm"
rolebindingName = "rb-feast-test"
notebookFile = "test/e2e_rhoai/resources/feast-test.ipynb"
pvcFile = "test/e2e_rhoai/resources/pvc.yaml"
notebookPVC = "jupyterhub-nb-kube-3aadmin-pvc"
testDir = "/test/e2e_rhoai"
notebookName = "feast-test.ipynb"
feastMilvusTest = "TestFeastMilvusNotebook"
)

BeforeAll(func() {
By(fmt.Sprintf("Creating test namespace: %s", namespace))
Expect(utils.CreateNamespace(namespace, testDir)).To(Succeed())
fmt.Printf("Namespace %s created successfully\n", namespace)
})

AfterAll(func() {
By(fmt.Sprintf("Deleting test namespace: %s", namespace))
Expect(utils.DeleteNamespace(namespace, testDir)).To(Succeed())
fmt.Printf("Namespace %s deleted successfully\n", namespace)
})

runNotebookTest := func() {
env := func(key string) string {
val, _ := os.LookupEnv(key)
return val
}

username := utils.GetOCUser(testDir)

// set namespace context
By(fmt.Sprintf("Setting namespace context to : %s", namespace))
cmd := exec.Command("kubectl", "config", "set-context", "--current", "--namespace", namespace)
output, err := utils.Run(cmd, "/test/e2e_rhoai")
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf(
"Failed to set namespace context to %s.\nError: %v\nOutput: %s\n",
namespace, err, output,
))
fmt.Printf("Successfully set namespace context to: %s\n", namespace)

// create config map
By(fmt.Sprintf("Creating Config map: %s", configMapName))
cmd = exec.Command("kubectl", "create", "configmap", configMapName, "--from-file="+notebookFile, "--from-file=test/e2e_rhoai/resources/feature_repo")
output, err = utils.Run(cmd, "/test/e2e_rhoai")
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf(
"Failed to create ConfigMap %s.\nError: %v\nOutput: %s\n",
configMapName, err, output,
))
fmt.Printf("ConfigMap %s created successfully\n", configMapName)

// create pvc
By(fmt.Sprintf("Creating Persistent volume claim: %s", notebookPVC))
cmd = exec.Command("kubectl", "apply", "-f", "test/e2e_rhoai/resources/pvc.yaml")
_, err = utils.Run(cmd, "/test/e2e_rhoai")
ExpectWithOffset(1, err).NotTo(HaveOccurred())
fmt.Printf("Persistent Volume Claim %s created successfully", notebookPVC)

// create rolebinding
By(fmt.Sprintf("Creating rolebinding %s for the user", rolebindingName))
cmd = exec.Command("kubectl", "create", "rolebinding", rolebindingName, "-n", namespace, "--role=admin", "--user="+username)
_, err = utils.Run(cmd, "/test/e2e_rhoai")
ExpectWithOffset(1, err).NotTo(HaveOccurred())
fmt.Printf("Created rolebinding %s successfully\n", rolebindingName)

// configure papermill notebook command execution
command := []string{
"/bin/sh",
"-c",
fmt.Sprintf(
"pip install papermill && "+
"mkdir -p /opt/app-root/src/feature_repo && "+
"cp -rL /opt/app-root/notebooks/* /opt/app-root/src/feature_repo/ && "+
"oc login --token=%s --server=%s --insecure-skip-tls-verify=true && "+
"(papermill /opt/app-root/notebooks/%s /opt/app-root/src/output.ipynb --kernel python3 && "+
"echo '✅ Notebook executed successfully' || "+
"(echo '❌ Notebook execution failed' && "+
"cp /opt/app-root/src/output.ipynb /opt/app-root/src/failed_output.ipynb && "+
"echo '📄 Copied failed notebook to failed_output.ipynb')) && "+
"jupyter nbconvert --to notebook --stdout /opt/app-root/src/output.ipynb || echo '⚠️ nbconvert failed' && "+
"sleep 100; exit 0",
utils.GetOCToken("test/e2e_rhoai"),
utils.GetOCServer("test/e2e_rhoai"),
"feast-test.ipynb",
),
}

// Defining notebook parameters
nbParams := utils.NotebookTemplateParams{
Namespace: namespace,
IngressDomain: utils.GetIngressDomain(testDir),
OpenDataHubNamespace: env("APPLICATIONS_NAMESPACE"),
NotebookImage: env("NOTEBOOK_IMAGE"),
NotebookConfigMapName: configMapName,
NotebookPVC: notebookPVC,
Username: username,
OC_TOKEN: utils.GetOCToken(testDir),
OC_SERVER: utils.GetOCServer(testDir),
NotebookFile: notebookName,
Command: "[\"" + strings.Join(command, "\",\"") + "\"]",
PipIndexUrl: env("PIP_INDEX_URL"),
PipTrustedHost: env("PIP_TRUSTED_HOST"),
FeastVerison: env("FEAST_VERSION"),
OpenAIAPIKey: env("OPENAI_API_KEY"),
}

By("Creating Jupyter Notebook")
Expect(utils.CreateNotebook(nbParams)).To(Succeed(), "Failed to create notebook")

By("Monitoring notebook logs")
Expect(utils.MonitorNotebookPod(namespace, "jupyter-nb-", notebookName)).To(Succeed(), "Notebook execution failed")
}

Context("Feast Jupyter Notebook Test", func() {
It("Should create and run a "+feastMilvusTest+" successfully", runNotebookTest)
})
})
157 changes: 157 additions & 0 deletions infra/feast-operator/test/e2e_rhoai/resources/custom-nb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# This template maybe used to spin up a custom notebook image
# i.e.: sed s/{{.IngressDomain}}/$(oc get ingresses.config/cluster -o jsonpath={.spec.domain})/g tests/resources/custom-nb.template | oc apply -f -
# resources generated:
# pod/jupyter-nb-kube-3aadmin-0
# service/jupyter-nb-kube-3aadmin
# route.route.openshift.io/jupyter-nb-kube-3aadmin (jupyter-nb-kube-3aadmin-opendatahub.apps.tedbig412.cp.fyre.ibm.com)
# service/jupyter-nb-kube-3aadmin-tls
apiVersion: kubeflow.org/v1
kind: Notebook
metadata:
annotations:
notebooks.opendatahub.io/inject-oauth: "true"
notebooks.opendatahub.io/last-size-selection: Small
notebooks.opendatahub.io/oauth-logout-url: https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home
opendatahub.io/link: https://jupyter-nb-kube-3aadmin-{{.Namespace}}.{{.IngressDomain}}/notebook/{{.Namespace}}/jupyter-nb-kube-3aadmin
opendatahub.io/username: {{.Username}}
generation: 1
labels:
app: jupyter-nb-kube-3aadmin
opendatahub.io/dashboard: "true"
opendatahub.io/odh-managed: "true"
opendatahub.io/user: {{.Username}}
name: jupyter-nb-kube-3aadmin
namespace: {{.Namespace}}
spec:
template:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: nvidia.com/gpu.present
operator: NotIn
values:
- "true"
weight: 1
containers:
- env:
- name: NOTEBOOK_ARGS
value: |-
--ServerApp.port=8888
--ServerApp.token=''
--ServerApp.password=''
--ServerApp.base_url=/notebook/test-feast-wb/jupyter-nb-kube-3aadmin
--ServerApp.quit_button=False
--ServerApp.tornado_settings={"user":"{{.Username}}","hub_host":"https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}","hub_prefix":"/notebookController/{{.Username}}"}
- name: JUPYTER_IMAGE
value: {{.NotebookImage}}
- name: JUPYTER_NOTEBOOK_PORT
value: "8888"
- name: PIP_INDEX_URL
value: {{.PipIndexUrl}}
- name: PIP_TRUSTED_HOST
value: {{.PipTrustedHost}}
- name: FEAST_VERSION
value: {{.FeastVerison}}
- name: OPENAI_API_KEY
value: {{.OpenAIAPIKey}}
image: {{.NotebookImage}}
command: {{.Command}}
imagePullPolicy: Always
name: jupyter-nb-kube-3aadmin
ports:
- containerPort: 8888
name: notebook-port
protocol: TCP
resources:
limits:
cpu: "2"
memory: 3Gi
requests:
cpu: "1"
memory: 3Gi
volumeMounts:
- mountPath: /opt/app-root/src
name: jupyterhub-nb-kube-3aadmin-pvc
- mountPath: /opt/app-root/notebooks
name: {{.NotebookConfigMapName}}
workingDir: /opt/app-root/src
- args:
- --provider=openshift
- --https-address=:8443
- --http-address=
- --openshift-service-account=jupyter-nb-kube-3aadmin
- --cookie-secret-file=/etc/oauth/config/cookie_secret
- --cookie-expire=24h0m0s
- --tls-cert=/etc/tls/private/tls.crt
- --tls-key=/etc/tls/private/tls.key
- --upstream=http://localhost:8888
- --upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
- --skip-auth-regex=^(?:/notebook/test-feast-wb/jupyter-nb-kube-3aadmin)?/api$
- --email-domain=*
- --skip-provider-button
- --openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"jupyter-nb-kube-3aadmin","namespace":$(NAMESPACE)}
- --logout-url=https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: registry.redhat.io/openshift4/ose-oauth-proxy:v4.10
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
httpGet:
path: /oauth/healthz
port: oauth-proxy
scheme: HTTPS
initialDelaySeconds: 30
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
name: oauth-proxy
ports:
- containerPort: 8443
name: oauth-proxy
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /oauth/healthz
port: oauth-proxy
scheme: HTTPS
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 100m
memory: 64Mi
volumeMounts:
- mountPath: /etc/oauth/config
name: oauth-config
- mountPath: /etc/tls/private
name: tls-certificates
enableServiceLinks: false
serviceAccountName: jupyter-nb-kube-3aadmin
volumes:
- name: jupyterhub-nb-kube-3aadmin-pvc
persistentVolumeClaim:
claimName: {{.NotebookPVC}}
- name: oauth-config
secret:
defaultMode: 420
secretName: jupyter-nb-kube-3aadmin-oauth-config
- name: tls-certificates
secret:
defaultMode: 420
secretName: jupyter-nb-kube-3aadmin-tls
- name: {{.NotebookConfigMapName}}
configMap:
name: {{.NotebookConfigMapName}}
Loading
Loading