Skip to content

Commit 55e0308

Browse files
committed
Add RayCLuster Oauth Authentication test
1 parent a4590ea commit 55e0308

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

test/e2e/mnist_raycluster_sdk_auth.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import sys
2+
import os
3+
4+
from time import sleep
5+
6+
from codeflare_sdk.cluster.cluster import Cluster, ClusterConfiguration
7+
from codeflare_sdk.job.jobs import DDPJobDefinition
8+
9+
namespace = sys.argv[1]
10+
ray_image = os.getenv('RAY_IMAGE')
11+
12+
cluster = Cluster(ClusterConfiguration(
13+
name='mnist',
14+
namespace=namespace,
15+
num_workers=1,
16+
head_cpus='500m',
17+
head_memory=2,
18+
min_cpus='500m',
19+
max_cpus=1,
20+
min_memory=0.5,
21+
max_memory=2,
22+
num_gpus=0,
23+
instascale=False,
24+
image=ray_image,
25+
openshift_oauth=True,
26+
))
27+
28+
cluster.up()
29+
30+
cluster.status()
31+
32+
cluster.wait_ready()
33+
34+
cluster.status()
35+
36+
cluster.details()
37+
38+
jobdef = DDPJobDefinition(
39+
name="mnist",
40+
script="mnist.py",
41+
scheduler_args={"requirements": "requirements.txt"},
42+
)
43+
job = jobdef.submit(cluster)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright 2023.
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 e2e
18+
19+
import (
20+
"testing"
21+
22+
"github.com/onsi/gomega"
23+
. "github.com/onsi/gomega"
24+
. "github.com/project-codeflare/codeflare-common/support"
25+
mcadv1beta1 "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/apis/controller/v1beta1"
26+
rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
27+
28+
batchv1 "k8s.io/api/batch/v1"
29+
corev1 "k8s.io/api/core/v1"
30+
rbacv1 "k8s.io/api/rbac/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
)
33+
34+
// This test covers the Ray Cluster creation authentication functionality with openshift_oauth
35+
func TestRayClusterSDKAuth(t *testing.T) {
36+
test := With(t)
37+
test.T().Parallel()
38+
if GetClusterType(test) == KindCluster {
39+
test.T().Skipf("Skipping test as not running on an openshift cluster")
40+
}
41+
42+
// Create a namespace
43+
namespace := test.NewTestNamespace()
44+
// Test configuration
45+
config := CreateConfigMap(test, namespace.Name, map[string][]byte{
46+
// SDK script
47+
"mnist_raycluster_sdk_auth.py": ReadFile(test, "mnist_raycluster_sdk_auth.py"),
48+
// pip requirements
49+
"requirements.txt": ReadFile(test, "mnist_pip_requirements.txt"),
50+
// MNIST training script
51+
"mnist.py": ReadFile(test, "mnist.py"),
52+
// codeflare-sdk installation script
53+
"install-codeflare-sdk.sh": ReadFile(test, "install-codeflare-sdk.sh"),
54+
})
55+
56+
// // Create RBAC, retrieve token for user with limited rights
57+
policyRules := []rbacv1.PolicyRule{
58+
{
59+
Verbs: []string{"get", "create", "delete", "list", "patch", "update"},
60+
APIGroups: []string{mcadv1beta1.GroupName},
61+
Resources: []string{"appwrappers"},
62+
},
63+
{
64+
Verbs: []string{"get", "list"},
65+
APIGroups: []string{rayv1.GroupVersion.Group},
66+
Resources: []string{"rayclusters", "rayclusters/status"},
67+
},
68+
{
69+
Verbs: []string{"get", "list"},
70+
APIGroups: []string{"route.openshift.io"},
71+
Resources: []string{"routes"},
72+
},
73+
{
74+
Verbs: []string{"get", "list", "create", "delete"},
75+
APIGroups: []string{"networking.k8s.io"},
76+
Resources: []string{"ingresses"},
77+
},
78+
}
79+
80+
// // Create cluster wide RBAC, required for SDK OpenShift check
81+
// // TODO reevaluate once SDK change OpenShift detection logic
82+
clusterPolicyRules := []rbacv1.PolicyRule{
83+
{
84+
Verbs: []string{"get", "list"},
85+
APIGroups: []string{"config.openshift.io"},
86+
Resources: []string{"ingresses"},
87+
ResourceNames: []string{"cluster"},
88+
},
89+
{
90+
Verbs: []string{"create", "update"},
91+
APIGroups: []string{""},
92+
Resources: []string{"services", "serviceaccounts"},
93+
},
94+
{
95+
Verbs: []string{"create"},
96+
APIGroups: []string{"authorization.k8s.io"},
97+
Resources: []string{"subjectaccessreviews"},
98+
},
99+
{
100+
Verbs: []string{"get", "create", "delete", "list", "patch", "update"},
101+
APIGroups: []string{"rbac.authorization.k8s.io"},
102+
Resources: []string{"clusterrolebindings"},
103+
},
104+
{
105+
Verbs: []string{"create"},
106+
APIGroups: []string{"authentication.k8s.io"},
107+
Resources: []string{"tokenreviews"},
108+
},
109+
}
110+
111+
sa := CreateServiceAccount(test, namespace.Name)
112+
role := CreateRole(test, namespace.Name, policyRules)
113+
CreateRoleBinding(test, namespace.Name, sa, role)
114+
clusterRole := CreateClusterRole(test, clusterPolicyRules)
115+
CreateClusterRoleBinding(test, sa, clusterRole)
116+
117+
job := &batchv1.Job{
118+
TypeMeta: metav1.TypeMeta{
119+
APIVersion: batchv1.SchemeGroupVersion.String(),
120+
Kind: "Job",
121+
},
122+
ObjectMeta: metav1.ObjectMeta{
123+
Name: "sdk",
124+
Namespace: namespace.Name,
125+
},
126+
Spec: batchv1.JobSpec{
127+
Completions: Ptr(int32(1)),
128+
Parallelism: Ptr(int32(1)),
129+
BackoffLimit: Ptr(int32(0)),
130+
Template: corev1.PodTemplateSpec{
131+
Spec: corev1.PodSpec{
132+
Containers: []corev1.Container{
133+
{
134+
Name: "test",
135+
// FIXME: switch to base Python image once the dependency on OpenShift CLI is removed
136+
// See https://github.com/project-codeflare/codeflare-sdk/pull/146
137+
Image: "quay.io/opendatahub/notebooks:jupyter-minimal-ubi8-python-3.8-4c8f26e",
138+
Env: []corev1.EnvVar{
139+
{Name: "PYTHONUSERBASE", Value: "/workdir"},
140+
{Name: "RAY_IMAGE", Value: GetRayImage()},
141+
},
142+
Command: []string{"/bin/sh", "-c", "cp /test/* . && chmod +x install-codeflare-sdk.sh && ./install-codeflare-sdk.sh && python mnist_raycluster_sdk_auth.py" + " " + namespace.Name},
143+
VolumeMounts: []corev1.VolumeMount{
144+
{
145+
Name: "test",
146+
MountPath: "/test",
147+
},
148+
{
149+
Name: "codeflare-sdk",
150+
MountPath: "/codeflare-sdk",
151+
},
152+
{
153+
Name: "workdir",
154+
MountPath: "/workdir",
155+
},
156+
},
157+
WorkingDir: "/workdir",
158+
SecurityContext: &corev1.SecurityContext{
159+
AllowPrivilegeEscalation: Ptr(false),
160+
SeccompProfile: &corev1.SeccompProfile{
161+
Type: "RuntimeDefault",
162+
},
163+
Capabilities: &corev1.Capabilities{
164+
Drop: []corev1.Capability{"ALL"},
165+
},
166+
RunAsNonRoot: Ptr(true),
167+
},
168+
},
169+
},
170+
Volumes: []corev1.Volume{
171+
{
172+
Name: "test",
173+
VolumeSource: corev1.VolumeSource{
174+
ConfigMap: &corev1.ConfigMapVolumeSource{
175+
LocalObjectReference: corev1.LocalObjectReference{
176+
Name: config.Name,
177+
},
178+
},
179+
},
180+
},
181+
{
182+
Name: "codeflare-sdk",
183+
VolumeSource: corev1.VolumeSource{
184+
EmptyDir: &corev1.EmptyDirVolumeSource{},
185+
},
186+
},
187+
{
188+
Name: "workdir",
189+
VolumeSource: corev1.VolumeSource{
190+
EmptyDir: &corev1.EmptyDirVolumeSource{},
191+
},
192+
},
193+
},
194+
RestartPolicy: corev1.RestartPolicyNever,
195+
ServiceAccountName: sa.Name,
196+
},
197+
},
198+
},
199+
}
200+
201+
job, err := test.Client().Core().BatchV1().Jobs(namespace.Name).Create(test.Ctx(), job, metav1.CreateOptions{})
202+
test.Expect(err).NotTo(HaveOccurred())
203+
test.T().Logf("Created Job %s/%s successfully", job.Namespace, job.Name)
204+
205+
test.T().Logf("Waiting for Job %s/%s to complete", job.Namespace, job.Name)
206+
test.Eventually(Job(test, job.Namespace, job.Name), TestTimeoutLong).Should(
207+
WithTransform(ConditionStatus(batchv1.JobFailed), Equal(corev1.ConditionTrue)))
208+
209+
// get Pod associated with created job
210+
options := metav1.ListOptions{
211+
LabelSelector: "job-name=sdk",
212+
}
213+
pods := GetPods(test, namespace.Name, options)
214+
215+
// get job pod logs and compare the expected error in logs
216+
for _, pod := range pods {
217+
podLogs := GetPodLogs(test, &pod, corev1.PodLogOptions{})
218+
expectedLogError := "kubernetes.client.exceptions.ApiException: (403)\nReason: Forbidden"
219+
test.Expect(string(podLogs)).To(gomega.ContainSubstring(expectedLogError))
220+
}
221+
222+
}

0 commit comments

Comments
 (0)