Skip to content

Commit a5c2ebc

Browse files
committed
add functions for creating ray with oauth proxy in front of the dashboard
Signed-off-by: Kevin <[email protected]>
1 parent 0d9b23c commit a5c2ebc

File tree

4 files changed

+212
-2
lines changed

4 files changed

+212
-2
lines changed

src/codeflare_sdk/cluster/cluster.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
from typing import List, Optional, Tuple, Dict
2424

2525
import openshift as oc
26+
from kubernetes import config
2627
from ray.job_submission import JobSubmissionClient
2728

2829
from ..utils import pretty_print
2930
from ..utils.generate_yaml import generate_appwrapper
31+
from ..utils.openshift_oauth import create_openshift_oauth_objects, delete_openshift_oauth_objects
3032
from .config import ClusterConfiguration
3133
from .model import (
3234
AppWrapper,
@@ -37,6 +39,9 @@
3739
)
3840

3941

42+
k8_client = config.new_client_from_config()
43+
44+
4045
class Cluster:
4146
"""
4247
An object for requesting, bringing up, and taking down resources.
@@ -57,6 +62,21 @@ def __init__(self, config: ClusterConfiguration):
5762
self.config = config
5863
self.app_wrapper_yaml = self.create_app_wrapper()
5964
self.app_wrapper_name = self.app_wrapper_yaml.split(".")[0]
65+
self._client = None
66+
67+
@property
68+
def client(self):
69+
if self._client:
70+
return self._client
71+
if self.config.openshift_oauth:
72+
# user must be logged in to OpenShift
73+
self._client = JobSubmissionClient(
74+
self.cluster_dashboard_uri,
75+
headers={"Authorization": k8_client.configuration.auth_settings()["BearerToken"]["value"]}
76+
)
77+
else:
78+
self._client = JobSubmissionClient(self.cluster_dashboard_uri)
79+
return self._client
6080

6181
def create_app_wrapper(self):
6282
"""
@@ -102,6 +122,7 @@ def create_app_wrapper(self):
102122
env=env,
103123
local_interactive=local_interactive,
104124
image_pull_secrets=image_pull_secrets,
125+
openshift_oauth=self.config.openshift_oauth,
105126
)
106127

107128
# creates a new cluster with the provided or default spec
@@ -111,6 +132,9 @@ def up(self):
111132
the MCAD queue.
112133
"""
113134
namespace = self.config.namespace
135+
if self.config.openshift_oauth:
136+
create_openshift_oauth_objects(cluster_name=self.config.name, namespace=namespace)
137+
114138
try:
115139
with oc.project(namespace):
116140
oc.invoke("apply", ["-f", self.app_wrapper_yaml])
@@ -146,6 +170,8 @@ def down(self):
146170
print("Cluster not found, have you run cluster.up() yet?")
147171
else:
148172
raise osp
173+
if self.config.openshift_oauth:
174+
delete_openshift_oauth_objects(cluster_name=self.config.name, namespace=namespace)
149175

150176
def status(
151177
self, print_to_console: bool = True
@@ -254,7 +280,7 @@ def cluster_dashboard_uri(self) -> str:
254280
route = route.out().split(" ")
255281
route = [x for x in route if f"ray-dashboard-{self.config.name}" in x]
256282
route = route[0].strip().strip("'")
257-
return f"http://{route}"
283+
return f"https://{route}"
258284
except:
259285
return "Dashboard route not available yet, have you run cluster.up()?"
260286

src/codeflare_sdk/cluster/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ class ClusterConfiguration:
5050
image: str = "quay.io/project-codeflare/ray:2.5.0-py38-cu116"
5151
local_interactive: bool = False
5252
image_pull_secrets: list = field(default_factory=list)
53+
openshift_oauth: bool = False

src/codeflare_sdk/utils/generate_yaml.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
import sys
2222
import argparse
2323
import uuid
24+
from os import urandom
25+
from base64 import b64encode
26+
from urllib3.util import parse_url
27+
2428
import openshift as oc
29+
from kubernetes import client, config
2530

31+
k8_client = config.new_client_from_config()
2632

2733
def read_template(template):
2834
with open(template, "r") as stream:
@@ -44,12 +50,14 @@ def gen_names(name):
4450

4551
def update_dashboard_route(route_item, cluster_name, namespace):
4652
metadata = route_item.get("generictemplate", {}).get("metadata")
47-
metadata["name"] = f"ray-dashboard-{cluster_name}"
53+
metadata["name"] = gen_dashboard_route_name(cluster_name)
4854
metadata["namespace"] = namespace
4955
metadata["labels"]["odh-ray-cluster-service"] = f"{cluster_name}-head-svc"
5056
spec = route_item.get("generictemplate", {}).get("spec")
5157
spec["to"]["name"] = f"{cluster_name}-head-svc"
5258

59+
def gen_dashboard_route_name(cluster_name):
60+
return f"ray-dashboard-{cluster_name}"
5361

5462
# ToDo: refactor the update_x_route() functions
5563
def update_rayclient_route(route_item, cluster_name, namespace):
@@ -289,6 +297,64 @@ def write_user_appwrapper(user_yaml, output_file_name):
289297
print(f"Written to: {output_file_name}")
290298

291299

300+
def enable_openshift_oauth(user_yaml, cluster_name, namespace):
301+
tls_mount_location = "/etc/tls/private"
302+
oauth_port = 443
303+
oauth_sa_name = f"{cluster_name}-oauth-proxy"
304+
tls_secret_name = f"{cluster_name}-proxy-tls-secret"
305+
tls_volume_name = "proxy-tls-secret"
306+
port_name = "oauth-proxy"
307+
_,_,host,_,_,_,_ = parse_url(k8_client.configuration.host)
308+
host = host.replace("api.", f"{gen_dashboard_route_name(cluster_name)}-{namespace}.apps.")
309+
oauth_sidecar = _create_oauth_sidecar_object(
310+
namespace, tls_mount_location, oauth_port, oauth_sa_name, tls_volume_name, port_name
311+
)
312+
tls_secret_volume = client.V1Volume(
313+
name=tls_volume_name,secret=client.V1SecretVolumeSource(secret_name=tls_secret_name)
314+
)
315+
# allows for setting value of Cluster object when initializing object from an existing AppWrapper on cluster
316+
user_yaml["metadata"]["annotations"] = user_yaml["metadata"].get("annotations", {})
317+
user_yaml["metadata"]["annotations"]["codeflare-sdk-use-oauth"] = "true" # if the user gets an
318+
ray_headgroup_pod = user_yaml["spec"]["resources"]["GenericItems"][0]["generictemplate"]["spec"]["headGroupSpec"]["template"]["spec"]
319+
user_yaml["spec"]["resources"]["GenericItems"].pop(1)
320+
ray_headgroup_pod["serviceAccount"] = oauth_sa_name
321+
ray_headgroup_pod["volumes"] = ray_headgroup_pod.get("volumes", [])
322+
ray_headgroup_pod["volumes"].append(k8_client.sanitize_for_serialization(tls_secret_volume))
323+
ray_headgroup_pod["containers"].append(k8_client.sanitize_for_serialization(oauth_sidecar))
324+
# add volume to headnode
325+
# add sidecar container to ray object
326+
327+
def _create_oauth_sidecar_object(
328+
namespace: str,
329+
tls_mount_location: str,
330+
oauth_port: int,
331+
oauth_sa_name: str,
332+
tls_volume_name: str,
333+
port_name: str
334+
) -> client.V1Container:
335+
return client.V1Container(
336+
args=[
337+
f"--https-address=:{oauth_port}",
338+
"--provider=openshift",
339+
f"--openshift-service-account={oauth_sa_name}",
340+
"--upstream=http://localhost:8265",
341+
f"--tls-cert={tls_mount_location}/tls.crt",
342+
f"--tls-key={tls_mount_location}/tls.key",
343+
"--cookie-secret=SECRET",
344+
# f"--cookie-secret={b64encode(urandom(64)).decode('utf-8')}", # create random string for encrypting cookie
345+
f'--openshift-delegate-urls={{"/":{{"resource":"pods","namespace":"{namespace}","verb":"get"}}}}'
346+
],
347+
image="registry.redhat.io/openshift4/ose-oauth-proxy@sha256:1ea6a01bf3e63cdcf125c6064cbd4a4a270deaf0f157b3eabb78f60556840366",
348+
name="oauth-proxy",
349+
ports=[client.V1ContainerPort(container_port=oauth_port,name=port_name)],
350+
resources = client.V1ResourceRequirements(limits=None,requests=None),
351+
volume_mounts=[
352+
client.V1VolumeMount(
353+
mount_path=tls_mount_location,name=tls_volume_name,read_only=True
354+
)
355+
],
356+
)
357+
292358
def generate_appwrapper(
293359
name: str,
294360
namespace: str,
@@ -305,6 +371,7 @@ def generate_appwrapper(
305371
env,
306372
local_interactive: bool,
307373
image_pull_secrets: list,
374+
openshift_oauth: bool,
308375
):
309376
user_yaml = read_template(template)
310377
appwrapper_name, cluster_name = gen_names(name)
@@ -335,6 +402,10 @@ def generate_appwrapper(
335402
enable_local_interactive(resources, cluster_name, namespace)
336403
else:
337404
disable_raycluster_tls(resources["resources"])
405+
406+
if openshift_oauth:
407+
enable_openshift_oauth(user_yaml, cluster_name, namespace)
408+
338409
outfile = appwrapper_name + ".yaml"
339410
write_user_appwrapper(user_yaml, outfile)
340411
return outfile
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from urllib3.util import parse_url
2+
from .generate_yaml import gen_dashboard_route_name
3+
4+
from kubernetes import config, client
5+
6+
k8_client = config.new_client_from_config()
7+
core_api = client.CoreV1Api(k8_client)
8+
rbac_auth_api = client.RbacAuthorizationV1Api(k8_client)
9+
networking_api = client.NetworkingV1Api(k8_client)
10+
11+
def create_openshift_oauth_objects(cluster_name, namespace):
12+
oauth_port = 443
13+
oauth_sa_name = f"{cluster_name}-oauth-proxy"
14+
tls_secret_name = f"{cluster_name}-proxy-tls-secret"
15+
service_name = f"{cluster_name}-oauth"
16+
port_name = "oauth-proxy"
17+
_,_,host,_,_,_,_ = parse_url(k8_client.configuration.host)
18+
host = host.replace("api.", f"{gen_dashboard_route_name(cluster_name)}-{namespace}.apps.")
19+
oauth_crb = client.V1ClusterRoleBinding(
20+
api_version="rbac.authorization.k8s.io/v1", kind="ClusterRoleBinding",
21+
metadata=client.V1ObjectMeta(name=f"{cluster_name}-rb"),
22+
role_ref=client.V1RoleRef(
23+
api_group="rbac.authorization.k8s.io",
24+
kind="ClusterRole",
25+
name="system:auth-delegator",
26+
),
27+
subjects=[client.V1Subject(kind="ServiceAccount", name=oauth_sa_name, namespace=namespace)]
28+
)
29+
oauth_sa = client.V1ServiceAccount(
30+
api_version="v1",
31+
kind="ServiceAccount",
32+
metadata=client.V1ObjectMeta(
33+
name=oauth_sa_name,
34+
namespace=namespace,
35+
annotations={"serviceaccounts.openshift.io/oauth-redirecturi.first": f"https://{host}"}
36+
)
37+
)
38+
oauth_service = _create_oauth_service_obj(
39+
cluster_name, namespace, oauth_port, tls_secret_name, service_name, port_name
40+
)
41+
ingress = _create_oauth_ingress_object(cluster_name, namespace, service_name, port_name, host)
42+
core_api.create_namespaced_service_account(namespace=namespace, body=oauth_sa)
43+
core_api.create_namespaced_service(namespace=namespace, body=oauth_service)
44+
networking_api.create_namespaced_ingress(namespace=namespace, body=ingress)
45+
rbac_auth_api.create_cluster_role_binding(body=oauth_crb)
46+
47+
def delete_openshift_oauth_objects(cluster_name, namespace):
48+
oauth_sa_name = f"{cluster_name}-oauth-proxy"
49+
service_name = f"{cluster_name}-oauth"
50+
core_api.delete_namespaced_service_account(name=oauth_sa_name, namespace=namespace)
51+
core_api.delete_namespaced_service(name=service_name, namespace=namespace)
52+
networking_api.delete_namespaced_ingress(name=f"{cluster_name}-ingress", namespace=namespace)
53+
rbac_auth_api.delete_cluster_role_binding(name= f"{cluster_name}-rb")
54+
55+
56+
def _create_oauth_service_obj(
57+
cluster_name: str,
58+
namespace: str,
59+
oauth_port: int,
60+
tls_secret_name: str,
61+
service_name: str,
62+
port_name: str,
63+
) -> client.V1Service:
64+
return client.V1Service(
65+
api_version="v1",
66+
kind="Service",
67+
metadata=client.V1ObjectMeta(
68+
annotations={"service.beta.openshift.io/serving-cert-secret-name": tls_secret_name},
69+
name=service_name,
70+
namespace=namespace
71+
),
72+
spec=client.V1ServiceSpec(
73+
ports=[client.V1ServicePort(name=port_name, protocol="TCP", port=oauth_port, target_port=oauth_port)],
74+
selector={
75+
"app.kubernetes.io/created-by": "kuberay-operator",
76+
"app.kubernetes.io/name": "kuberay",
77+
"ray.io/cluster": cluster_name,
78+
"ray.io/identifier": f"{cluster_name}-head",
79+
"ray.io/node-type": "head",
80+
}
81+
)
82+
)
83+
84+
def _create_oauth_ingress_object(
85+
cluster_name: str,
86+
namespace: str,
87+
service_name: str,
88+
port_name: str,
89+
host: str,
90+
) -> client.V1Ingress:
91+
return client.V1Ingress(
92+
api_version="networking.k8s.io/v1",
93+
kind="Ingress",
94+
metadata=client.V1ObjectMeta(
95+
annotations={"route.openshift.io/termination": "passthrough"},
96+
name=f"{cluster_name}-ingress",
97+
namespace=namespace
98+
),
99+
spec=client.V1IngressSpec(rules=[client.V1IngressRule(
100+
host=host,
101+
http=client.V1HTTPIngressRuleValue(paths=[
102+
client.V1HTTPIngressPath(
103+
backend=client.V1IngressBackend(
104+
service=client.V1IngressServiceBackend(
105+
name=service_name,port=client.V1ServiceBackendPort(name=port_name)
106+
)
107+
),
108+
path_type="ImplementationSpecific"
109+
)
110+
])
111+
)]),
112+
)

0 commit comments

Comments
 (0)