Skip to content

Commit 8a37bf5

Browse files
authored
test: Add Feast Milvus Jupyter Notebook Execution for downstream testing (#5446)
Signed-off-by: Srihari <[email protected]>
1 parent ae7e20e commit 8a37bf5

File tree

9 files changed

+1120
-0
lines changed

9 files changed

+1120
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2025 Feast Community.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2erhoai
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
)
26+
27+
// Run e2e feast Notebook tests using the Ginkgo runner.
28+
func TestNotebookRunE2E(t *testing.T) {
29+
RegisterFailHandler(Fail)
30+
_, _ = fmt.Fprintf(GinkgoWriter, "Feast Jupyter Notebook Test suite\n")
31+
RunSpecs(t, "e2erhoai Feast Notebook test suite")
32+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
Copyright 2025 Feast Community.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package e2erhoai provides end-to-end (E2E) test coverage for Feast integration with
18+
// Red Hat OpenShift AI (RHOAI) environments. This specific test validates the functionality
19+
// of executing a Feast Jupyter notebook within a fully configured OpenShift namespace
20+
package e2erhoai
21+
22+
import (
23+
"fmt"
24+
"os"
25+
"os/exec"
26+
"strings"
27+
28+
utils "github.com/feast-dev/feast/infra/feast-operator/test/utils"
29+
. "github.com/onsi/ginkgo/v2"
30+
. "github.com/onsi/gomega"
31+
)
32+
33+
var _ = Describe("Feast Jupyter Notebook Testing", Ordered, func() {
34+
const (
35+
namespace = "test-ns-feast-wb"
36+
configMapName = "feast-wb-cm"
37+
rolebindingName = "rb-feast-test"
38+
notebookFile = "test/e2e_rhoai/resources/feast-test.ipynb"
39+
pvcFile = "test/e2e_rhoai/resources/pvc.yaml"
40+
notebookPVC = "jupyterhub-nb-kube-3aadmin-pvc"
41+
testDir = "/test/e2e_rhoai"
42+
notebookName = "feast-test.ipynb"
43+
feastMilvusTest = "TestFeastMilvusNotebook"
44+
)
45+
46+
BeforeAll(func() {
47+
By(fmt.Sprintf("Creating test namespace: %s", namespace))
48+
Expect(utils.CreateNamespace(namespace, testDir)).To(Succeed())
49+
fmt.Printf("Namespace %s created successfully\n", namespace)
50+
})
51+
52+
AfterAll(func() {
53+
By(fmt.Sprintf("Deleting test namespace: %s", namespace))
54+
Expect(utils.DeleteNamespace(namespace, testDir)).To(Succeed())
55+
fmt.Printf("Namespace %s deleted successfully\n", namespace)
56+
})
57+
58+
runNotebookTest := func() {
59+
env := func(key string) string {
60+
val, _ := os.LookupEnv(key)
61+
return val
62+
}
63+
64+
username := utils.GetOCUser(testDir)
65+
66+
// set namespace context
67+
By(fmt.Sprintf("Setting namespace context to : %s", namespace))
68+
cmd := exec.Command("kubectl", "config", "set-context", "--current", "--namespace", namespace)
69+
output, err := utils.Run(cmd, "/test/e2e_rhoai")
70+
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf(
71+
"Failed to set namespace context to %s.\nError: %v\nOutput: %s\n",
72+
namespace, err, output,
73+
))
74+
fmt.Printf("Successfully set namespace context to: %s\n", namespace)
75+
76+
// create config map
77+
By(fmt.Sprintf("Creating Config map: %s", configMapName))
78+
cmd = exec.Command("kubectl", "create", "configmap", configMapName, "--from-file="+notebookFile, "--from-file=test/e2e_rhoai/resources/feature_repo")
79+
output, err = utils.Run(cmd, "/test/e2e_rhoai")
80+
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf(
81+
"Failed to create ConfigMap %s.\nError: %v\nOutput: %s\n",
82+
configMapName, err, output,
83+
))
84+
fmt.Printf("ConfigMap %s created successfully\n", configMapName)
85+
86+
// create pvc
87+
By(fmt.Sprintf("Creating Persistent volume claim: %s", notebookPVC))
88+
cmd = exec.Command("kubectl", "apply", "-f", "test/e2e_rhoai/resources/pvc.yaml")
89+
_, err = utils.Run(cmd, "/test/e2e_rhoai")
90+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
91+
fmt.Printf("Persistent Volume Claim %s created successfully", notebookPVC)
92+
93+
// create rolebinding
94+
By(fmt.Sprintf("Creating rolebinding %s for the user", rolebindingName))
95+
cmd = exec.Command("kubectl", "create", "rolebinding", rolebindingName, "-n", namespace, "--role=admin", "--user="+username)
96+
_, err = utils.Run(cmd, "/test/e2e_rhoai")
97+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
98+
fmt.Printf("Created rolebinding %s successfully\n", rolebindingName)
99+
100+
// configure papermill notebook command execution
101+
command := []string{
102+
"/bin/sh",
103+
"-c",
104+
fmt.Sprintf(
105+
"pip install papermill && "+
106+
"mkdir -p /opt/app-root/src/feature_repo && "+
107+
"cp -rL /opt/app-root/notebooks/* /opt/app-root/src/feature_repo/ && "+
108+
"oc login --token=%s --server=%s --insecure-skip-tls-verify=true && "+
109+
"(papermill /opt/app-root/notebooks/%s /opt/app-root/src/output.ipynb --kernel python3 && "+
110+
"echo '✅ Notebook executed successfully' || "+
111+
"(echo '❌ Notebook execution failed' && "+
112+
"cp /opt/app-root/src/output.ipynb /opt/app-root/src/failed_output.ipynb && "+
113+
"echo '📄 Copied failed notebook to failed_output.ipynb')) && "+
114+
"jupyter nbconvert --to notebook --stdout /opt/app-root/src/output.ipynb || echo '⚠️ nbconvert failed' && "+
115+
"sleep 100; exit 0",
116+
utils.GetOCToken("test/e2e_rhoai"),
117+
utils.GetOCServer("test/e2e_rhoai"),
118+
"feast-test.ipynb",
119+
),
120+
}
121+
122+
// Defining notebook parameters
123+
nbParams := utils.NotebookTemplateParams{
124+
Namespace: namespace,
125+
IngressDomain: utils.GetIngressDomain(testDir),
126+
OpenDataHubNamespace: env("APPLICATIONS_NAMESPACE"),
127+
NotebookImage: env("NOTEBOOK_IMAGE"),
128+
NotebookConfigMapName: configMapName,
129+
NotebookPVC: notebookPVC,
130+
Username: username,
131+
OC_TOKEN: utils.GetOCToken(testDir),
132+
OC_SERVER: utils.GetOCServer(testDir),
133+
NotebookFile: notebookName,
134+
Command: "[\"" + strings.Join(command, "\",\"") + "\"]",
135+
PipIndexUrl: env("PIP_INDEX_URL"),
136+
PipTrustedHost: env("PIP_TRUSTED_HOST"),
137+
FeastVerison: env("FEAST_VERSION"),
138+
OpenAIAPIKey: env("OPENAI_API_KEY"),
139+
}
140+
141+
By("Creating Jupyter Notebook")
142+
Expect(utils.CreateNotebook(nbParams)).To(Succeed(), "Failed to create notebook")
143+
144+
By("Monitoring notebook logs")
145+
Expect(utils.MonitorNotebookPod(namespace, "jupyter-nb-", notebookName)).To(Succeed(), "Notebook execution failed")
146+
}
147+
148+
Context("Feast Jupyter Notebook Test", func() {
149+
It("Should create and run a "+feastMilvusTest+" successfully", runNotebookTest)
150+
})
151+
})
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# This template maybe used to spin up a custom notebook image
2+
# i.e.: sed s/{{.IngressDomain}}/$(oc get ingresses.config/cluster -o jsonpath={.spec.domain})/g tests/resources/custom-nb.template | oc apply -f -
3+
# resources generated:
4+
# pod/jupyter-nb-kube-3aadmin-0
5+
# service/jupyter-nb-kube-3aadmin
6+
# route.route.openshift.io/jupyter-nb-kube-3aadmin (jupyter-nb-kube-3aadmin-opendatahub.apps.tedbig412.cp.fyre.ibm.com)
7+
# service/jupyter-nb-kube-3aadmin-tls
8+
apiVersion: kubeflow.org/v1
9+
kind: Notebook
10+
metadata:
11+
annotations:
12+
notebooks.opendatahub.io/inject-oauth: "true"
13+
notebooks.opendatahub.io/last-size-selection: Small
14+
notebooks.opendatahub.io/oauth-logout-url: https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home
15+
opendatahub.io/link: https://jupyter-nb-kube-3aadmin-{{.Namespace}}.{{.IngressDomain}}/notebook/{{.Namespace}}/jupyter-nb-kube-3aadmin
16+
opendatahub.io/username: {{.Username}}
17+
generation: 1
18+
labels:
19+
app: jupyter-nb-kube-3aadmin
20+
opendatahub.io/dashboard: "true"
21+
opendatahub.io/odh-managed: "true"
22+
opendatahub.io/user: {{.Username}}
23+
name: jupyter-nb-kube-3aadmin
24+
namespace: {{.Namespace}}
25+
spec:
26+
template:
27+
spec:
28+
affinity:
29+
nodeAffinity:
30+
preferredDuringSchedulingIgnoredDuringExecution:
31+
- preference:
32+
matchExpressions:
33+
- key: nvidia.com/gpu.present
34+
operator: NotIn
35+
values:
36+
- "true"
37+
weight: 1
38+
containers:
39+
- env:
40+
- name: NOTEBOOK_ARGS
41+
value: |-
42+
--ServerApp.port=8888
43+
--ServerApp.token=''
44+
--ServerApp.password=''
45+
--ServerApp.base_url=/notebook/test-feast-wb/jupyter-nb-kube-3aadmin
46+
--ServerApp.quit_button=False
47+
--ServerApp.tornado_settings={"user":"{{.Username}}","hub_host":"https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}","hub_prefix":"/notebookController/{{.Username}}"}
48+
- name: JUPYTER_IMAGE
49+
value: {{.NotebookImage}}
50+
- name: JUPYTER_NOTEBOOK_PORT
51+
value: "8888"
52+
- name: PIP_INDEX_URL
53+
value: {{.PipIndexUrl}}
54+
- name: PIP_TRUSTED_HOST
55+
value: {{.PipTrustedHost}}
56+
- name: FEAST_VERSION
57+
value: {{.FeastVerison}}
58+
- name: OPENAI_API_KEY
59+
value: {{.OpenAIAPIKey}}
60+
image: {{.NotebookImage}}
61+
command: {{.Command}}
62+
imagePullPolicy: Always
63+
name: jupyter-nb-kube-3aadmin
64+
ports:
65+
- containerPort: 8888
66+
name: notebook-port
67+
protocol: TCP
68+
resources:
69+
limits:
70+
cpu: "2"
71+
memory: 3Gi
72+
requests:
73+
cpu: "1"
74+
memory: 3Gi
75+
volumeMounts:
76+
- mountPath: /opt/app-root/src
77+
name: jupyterhub-nb-kube-3aadmin-pvc
78+
- mountPath: /opt/app-root/notebooks
79+
name: {{.NotebookConfigMapName}}
80+
workingDir: /opt/app-root/src
81+
- args:
82+
- --provider=openshift
83+
- --https-address=:8443
84+
- --http-address=
85+
- --openshift-service-account=jupyter-nb-kube-3aadmin
86+
- --cookie-secret-file=/etc/oauth/config/cookie_secret
87+
- --cookie-expire=24h0m0s
88+
- --tls-cert=/etc/tls/private/tls.crt
89+
- --tls-key=/etc/tls/private/tls.key
90+
- --upstream=http://localhost:8888
91+
- --upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
92+
- --skip-auth-regex=^(?:/notebook/test-feast-wb/jupyter-nb-kube-3aadmin)?/api$
93+
- --email-domain=*
94+
- --skip-provider-button
95+
- --openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"jupyter-nb-kube-3aadmin","namespace":$(NAMESPACE)}
96+
- --logout-url=https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home
97+
env:
98+
- name: NAMESPACE
99+
valueFrom:
100+
fieldRef:
101+
fieldPath: metadata.namespace
102+
image: registry.redhat.io/openshift4/ose-oauth-proxy:v4.10
103+
imagePullPolicy: Always
104+
livenessProbe:
105+
failureThreshold: 3
106+
httpGet:
107+
path: /oauth/healthz
108+
port: oauth-proxy
109+
scheme: HTTPS
110+
initialDelaySeconds: 30
111+
periodSeconds: 5
112+
successThreshold: 1
113+
timeoutSeconds: 1
114+
name: oauth-proxy
115+
ports:
116+
- containerPort: 8443
117+
name: oauth-proxy
118+
protocol: TCP
119+
readinessProbe:
120+
failureThreshold: 3
121+
httpGet:
122+
path: /oauth/healthz
123+
port: oauth-proxy
124+
scheme: HTTPS
125+
initialDelaySeconds: 5
126+
periodSeconds: 5
127+
successThreshold: 1
128+
timeoutSeconds: 1
129+
resources:
130+
limits:
131+
cpu: 100m
132+
memory: 64Mi
133+
requests:
134+
cpu: 100m
135+
memory: 64Mi
136+
volumeMounts:
137+
- mountPath: /etc/oauth/config
138+
name: oauth-config
139+
- mountPath: /etc/tls/private
140+
name: tls-certificates
141+
enableServiceLinks: false
142+
serviceAccountName: jupyter-nb-kube-3aadmin
143+
volumes:
144+
- name: jupyterhub-nb-kube-3aadmin-pvc
145+
persistentVolumeClaim:
146+
claimName: {{.NotebookPVC}}
147+
- name: oauth-config
148+
secret:
149+
defaultMode: 420
150+
secretName: jupyter-nb-kube-3aadmin-oauth-config
151+
- name: tls-certificates
152+
secret:
153+
defaultMode: 420
154+
secretName: jupyter-nb-kube-3aadmin-tls
155+
- name: {{.NotebookConfigMapName}}
156+
configMap:
157+
name: {{.NotebookConfigMapName}}

0 commit comments

Comments
 (0)