Skip to content

Commit b6e576d

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents b519083 + 1445fa6 commit b6e576d

16 files changed

+484
-121
lines changed

tests/odh/environment.go renamed to tests/common/environment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package odh
17+
package common
1818

1919
import (
2020
"os"

tests/odh/notebook.go renamed to tests/common/notebook.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package odh
17+
package common
1818

1919
import (
2020
"bytes"
21+
"embed"
22+
"strings"
2123

2224
gomega "github.com/onsi/gomega"
2325
. "github.com/project-codeflare/codeflare-common/support"
@@ -34,6 +36,16 @@ const (
3436
NOTEBOOK_CONTAINER_NAME = "jupyter-nb-kube-3aadmin"
3537
)
3638

39+
//go:embed resources/*
40+
var files embed.FS
41+
42+
func readFile(t Test, fileName string) []byte {
43+
t.T().Helper()
44+
file, err := files.ReadFile(fileName)
45+
t.Expect(err).NotTo(gomega.HaveOccurred())
46+
return file
47+
}
48+
3749
var notebookResource = schema.GroupVersionResource{Group: "kubeflow.org", Version: "v1", Resource: "notebooks"}
3850

3951
type NotebookProps struct {
@@ -42,7 +54,7 @@ type NotebookProps struct {
4254
KubernetesUserBearerToken string
4355
Namespace string
4456
OpenDataHubNamespace string
45-
RayImage string
57+
Command string
4658
NotebookImage string
4759
NotebookConfigMapName string
4860
NotebookConfigMapFileName string
@@ -57,14 +69,15 @@ type NotebookProps struct {
5769
S3DefaultRegion string
5870
}
5971

60-
func createNotebook(test Test, namespace *corev1.Namespace, notebookUserToken, rayImage string, jupyterNotebookConfigMapName, jupyterNotebookConfigMapFileName string, numGpus int) {
72+
func CreateNotebook(test Test, namespace *corev1.Namespace, notebookUserToken string, command []string, jupyterNotebookConfigMapName, jupyterNotebookConfigMapFileName string, numGpus int) {
6173
// Create PVC for Notebook
6274
notebookPVC := CreatePersistentVolumeClaim(test, namespace.Name, "10Gi", corev1.ReadWriteOnce)
6375
s3BucketName, s3BucketNameExists := GetStorageBucketName()
6476
s3AccessKeyId, _ := GetStorageBucketAccessKeyId()
6577
s3SecretAccessKey, _ := GetStorageBucketSecretKey()
6678
s3Endpoint, _ := GetStorageBucketDefaultEndpoint()
6779
s3DefaultRegion, _ := GetStorageBucketDefaultRegion()
80+
strCommand := "[\"" + strings.Join(command, "\",\"") + "\"]"
6881

6982
if !s3BucketNameExists {
7083
s3BucketName = "''"
@@ -81,7 +94,7 @@ func createNotebook(test Test, namespace *corev1.Namespace, notebookUserToken, r
8194
KubernetesUserBearerToken: notebookUserToken,
8295
Namespace: namespace.Name,
8396
OpenDataHubNamespace: GetOpenDataHubNamespace(test),
84-
RayImage: rayImage,
97+
Command: strCommand,
8598
NotebookImage: GetNotebookImage(test),
8699
NotebookConfigMapName: jupyterNotebookConfigMapName,
87100
NotebookConfigMapFileName: jupyterNotebookConfigMapFileName,
@@ -98,6 +111,7 @@ func createNotebook(test Test, namespace *corev1.Namespace, notebookUserToken, r
98111
notebookTemplate, err := files.ReadFile("resources/custom-nb-small.yaml")
99112
test.Expect(err).NotTo(gomega.HaveOccurred())
100113

114+
notebookTemplate = ParseTemplate(test, notebookTemplate, notebookProps)
101115
parsedNotebookTemplate := ParseTemplate(test, notebookTemplate, notebookProps)
102116

103117
// Create Notebook CR
@@ -108,12 +122,12 @@ func createNotebook(test Test, namespace *corev1.Namespace, notebookUserToken, r
108122
test.Expect(err).NotTo(gomega.HaveOccurred())
109123
}
110124

111-
func deleteNotebook(test Test, namespace *corev1.Namespace) {
125+
func DeleteNotebook(test Test, namespace *corev1.Namespace) {
112126
err := test.Client().Dynamic().Resource(notebookResource).Namespace(namespace.Name).Delete(test.Ctx(), "jupyter-nb-kube-3aadmin", metav1.DeleteOptions{})
113127
test.Expect(err).NotTo(gomega.HaveOccurred())
114128
}
115129

116-
func listNotebooks(test Test, namespace *corev1.Namespace) []*unstructured.Unstructured {
130+
func ListNotebooks(test Test, namespace *corev1.Namespace) []*unstructured.Unstructured {
117131
ntbs, err := test.Client().Dynamic().Resource(notebookResource).Namespace(namespace.Name).List(test.Ctx(), metav1.ListOptions{})
118132
test.Expect(err).NotTo(gomega.HaveOccurred())
119133

tests/odh/resources/custom-nb-small.yaml renamed to tests/common/resources/custom-nb-small.yaml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,12 @@ spec:
5050
value: {{.NotebookImage}}
5151
- name: JUPYTER_NOTEBOOK_PORT
5252
value: "8888"
53-
- name: AWS_ACCESS_KEY_ID
54-
value: {{.S3AccessKeyId}}
55-
- name: AWS_SECRET_ACCESS_KEY
56-
value: {{.S3SecretAccessKey}}
57-
- name: AWS_S3_ENDPOINT
58-
value: {{.S3Endpoint}}
59-
- name: AWS_DEFAULT_REGION
60-
value: {{.S3DefaultRegion}}
61-
- name: AWS_S3_BUCKET
62-
value: {{.S3BucketName}}
6353
- name: PIP_INDEX_URL
6454
value: {{.PipIndexUrl}}
6555
- name: PIP_TRUSTED_HOST
6656
value: {{.PipTrustedHost}}
6757
image: {{.NotebookImage}}
68-
command: ["/bin/sh", "-c", "pip install papermill && papermill /opt/app-root/notebooks/{{.NotebookConfigMapFileName}} /opt/app-root/src/mcad-out.ipynb -p namespace {{.Namespace}} -p ray_image {{.RayImage}} -p openshift_api_url {{.OpenShiftApiUrl}} -p kubernetes_user_bearer_token {{.KubernetesUserBearerToken}} -p num_gpus {{ .NumGpus }} --log-output && sleep infinity"]
58+
command: {{.Command}}
6959
# args: ["pip install papermill && oc login --token=${OCP_TOKEN} --server=${OCP_SERVER} --insecure-skip-tls-verify=true && papermill /opt/app-root/notebooks/mcad.ipynb /opt/app-root/src/mcad-out.ipynb" ]
7060
imagePullPolicy: Always
7161
# livenessProbe:

tests/common/template.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2024.
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 common
18+
19+
import (
20+
"bytes"
21+
"text/template"
22+
23+
"github.com/onsi/gomega"
24+
"github.com/project-codeflare/codeflare-common/support"
25+
)
26+
27+
func ParseAWSArgs(t support.Test, inputTemplate []byte) []byte {
28+
storage_bucket_endpoint, storage_bucket_endpoint_exists := support.GetStorageBucketDefaultEndpoint()
29+
storage_bucket_access_key_id, storage_bucket_access_key_id_exists := support.GetStorageBucketAccessKeyId()
30+
storage_bucket_secret_key, storage_bucket_secret_key_exists := support.GetStorageBucketSecretKey()
31+
storage_bucket_name, storage_bucket_name_exists := support.GetStorageBucketName()
32+
storage_bucket_mnist_dir, storage_bucket_mnist_dir_exists := support.GetStorageBucketMnistDir()
33+
34+
props := struct {
35+
StorageBucketDefaultEndpoint string
36+
StorageBucketDefaultEndpointExists bool
37+
StorageBucketAccessKeyId string
38+
StorageBucketAccessKeyIdExists bool
39+
StorageBucketSecretKey string
40+
StorageBucketSecretKeyExists bool
41+
StorageBucketName string
42+
StorageBucketNameExists bool
43+
StorageBucketMnistDir string
44+
StorageBucketMnistDirExists bool
45+
}{
46+
StorageBucketDefaultEndpoint: storage_bucket_endpoint,
47+
StorageBucketDefaultEndpointExists: storage_bucket_endpoint_exists,
48+
StorageBucketAccessKeyId: storage_bucket_access_key_id,
49+
StorageBucketAccessKeyIdExists: storage_bucket_access_key_id_exists,
50+
StorageBucketSecretKey: storage_bucket_secret_key,
51+
StorageBucketSecretKeyExists: storage_bucket_secret_key_exists,
52+
StorageBucketName: storage_bucket_name,
53+
StorageBucketNameExists: storage_bucket_name_exists,
54+
StorageBucketMnistDir: storage_bucket_mnist_dir,
55+
StorageBucketMnistDirExists: storage_bucket_mnist_dir_exists,
56+
}
57+
58+
return ParseTemplate(t, inputTemplate, props)
59+
}
60+
61+
func ParseTemplate(t support.Test, inputTemplate []byte, props interface{}) []byte {
62+
t.T().Helper()
63+
64+
// Parse input template
65+
parsedTemplate, err := template.New("template").Parse(string(inputTemplate))
66+
t.Expect(err).NotTo(gomega.HaveOccurred())
67+
68+
// Filter template and store results to the buffer
69+
buffer := new(bytes.Buffer)
70+
err = parsedTemplate.Execute(buffer, props)
71+
t.Expect(err).NotTo(gomega.HaveOccurred())
72+
err = parsedTemplate.Execute(buffer, props) // NOTE: not sure if template package handles recursive case
73+
t.Expect(err).NotTo(gomega.HaveOccurred())
74+
75+
return buffer.Bytes()
76+
}

tests/kfto/kfto_mnist_sdk_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2025.
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 kfto
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
. "github.com/onsi/gomega"
24+
. "github.com/project-codeflare/codeflare-common/support"
25+
26+
v1 "k8s.io/api/core/v1"
27+
28+
. "github.com/opendatahub-io/distributed-workloads/tests/common"
29+
)
30+
31+
func TestMnistSDK(t *testing.T) {
32+
test := With(t)
33+
// Create a namespace
34+
namespace := test.NewTestNamespace()
35+
userName := GetNotebookUserName(test)
36+
userToken := GetNotebookUserToken(test)
37+
jupyterNotebookConfigMapFileName := "mnist_kfto.ipynb"
38+
mnist := ParseAWSArgs(test, readFile(test, "resources/kfto_sdk_mnist.py"))
39+
40+
// Create role binding with Namespace specific admin cluster role
41+
CreateUserRoleBindingWithClusterRole(test, userName, namespace.Name, "admin")
42+
43+
requiredChangesInNotebook := map[string]string{
44+
"${api_url}": GetOpenShiftApiUrl(test),
45+
"${password}": userToken,
46+
"${num_gpus}": "0",
47+
"${namespace}": namespace.Name,
48+
}
49+
50+
jupyterNotebook := string(readFile(test, "resources/mnist_kfto.ipynb"))
51+
requirements := readFile(test, "resources/requirements.txt")
52+
for oldValue, newValue := range requiredChangesInNotebook {
53+
jupyterNotebook = strings.Replace(string(jupyterNotebook), oldValue, newValue, -1)
54+
}
55+
56+
config := CreateConfigMap(test, namespace.Name, map[string][]byte{
57+
jupyterNotebookConfigMapFileName: []byte(jupyterNotebook),
58+
"kfto_sdk_mnist.py": mnist,
59+
"requirements.txt": requirements,
60+
})
61+
62+
notebookCommand := []string{
63+
"/bin/sh",
64+
"-c",
65+
"pip install papermill && papermill /opt/app-root/notebooks/{{.NotebookConfigMapFileName}}" +
66+
" /opt/app-root/src/mcad-out.ipynb -p namespace {{.Namespace}} -p openshift_api_url {{.OpenShiftApiUrl}}" +
67+
" -p kubernetes_user_bearer_token {{.KubernetesUserBearerToken}}" +
68+
" -p num_gpus {{.NumGpus}} --log-output && sleep infinity",
69+
}
70+
// Create Notebook CR
71+
CreateNotebook(test, namespace, userToken, notebookCommand, config.Name, jupyterNotebookConfigMapFileName, 0)
72+
73+
// Gracefully cleanup Notebook
74+
defer func() {
75+
DeleteNotebook(test, namespace)
76+
test.Eventually(ListNotebooks(test, namespace), TestTimeoutGpuProvisioning).Should(HaveLen(0))
77+
}()
78+
79+
// Make sure pytorch job is created
80+
test.Eventually(PyTorchJob(test, namespace.Name, "pytorch-ddp")).
81+
Should(WithTransform(PyTorchJobConditionRunning, Equal(v1.ConditionTrue)))
82+
83+
// Make sure that the job eventually succeeds
84+
test.Eventually(PyTorchJob(test, namespace.Name, "pytorch-ddp")).
85+
Should(WithTransform(PyTorchJobConditionSucceeded, Equal(v1.ConditionTrue)))
86+
87+
// TODO: write torch job logs?
88+
// time.Sleep(60 * time.Second)
89+
}

tests/kfto/kfto_mnist_training_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ func runKFTOPyTorchMnistJob(t *testing.T, accelerator Accelerator, image string,
5959
// Create a namespace
6060
namespace := test.NewTestNamespace()
6161

62-
mnist := ReadFile(test, "resources/mnist.py")
63-
download_mnist_dataset := ReadFile(test, "resources/download_mnist_datasets.py")
64-
requirementsFileName := ReadFile(test, requirementsFile)
62+
mnist := readFile(test, "resources/mnist.py")
63+
download_mnist_dataset := readFile(test, "resources/download_mnist_datasets.py")
64+
requirementsFileName := readFile(test, requirementsFile)
6565

6666
if accelerator.isGpu() {
6767
mnist = bytes.Replace(mnist, []byte("accelerator=\"has to be specified\""), []byte("accelerator=\"gpu\""), 1)

tests/kfto/kfto_training_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func runKFTOPyTorchJob(t *testing.T, image string, gpu Accelerator, numGpus, num
7070

7171
// Create a ConfigMap with training script
7272
configData := map[string][]byte{
73-
"hf_llm_training.py": ReadFile(test, "resources/hf_llm_training.py"),
73+
"hf_llm_training.py": readFile(test, "resources/hf_llm_training.py"),
7474
}
7575
config := CreateConfigMap(test, namespace, configData)
7676

0 commit comments

Comments
 (0)