Skip to content

Commit a26bc55

Browse files
authored
Update authentication for K8s (#237)
* Updated authentication for Kubernetes * Updated template name and comment * Updated login functionality * Altered config_check() function * Altered comments and changed config_check() function * Added logic for handling current namespace when a user authenticates via kube client * Changed formatting * Made handler functions generic and altered get_current_namespace() functionality * Changed error message for cluster configuration * Removed default values for token + server * Added check for correct credentials * Changed how using certs works with certifi.where * Added unit tests for new authentication methods * Fixed formatting and updated .gitignore to include test created files * Fixed .gitignore * Updated unit authentication tests
1 parent 64659f3 commit a26bc55

File tree

6 files changed

+199
-165
lines changed

6 files changed

+199
-165
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ Pipfile.lock
77
poetry.lock
88
.venv*
99
build/
10+
tls-cluster-namespace
11+
quicktest.yaml

src/codeflare_sdk/cluster/auth.py

+103-48
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
"""
2121

2222
import abc
23-
import openshift as oc
24-
from openshift import OpenShiftPythonException
23+
from kubernetes import client, config
24+
25+
global api_client
26+
api_client = None
27+
global config_path
28+
config_path = None
2529

2630

2731
class Authentication(metaclass=abc.ABCMeta):
@@ -43,80 +47,131 @@ def logout(self):
4347
pass
4448

4549

50+
class KubeConfiguration(metaclass=abc.ABCMeta):
51+
"""
52+
An abstract class that defines the method for loading a user defined config file using the `load_kube_config()` function
53+
"""
54+
55+
def load_kube_config(self):
56+
"""
57+
Method for setting your Kubernetes configuration to a certain file
58+
"""
59+
pass
60+
61+
def logout(self):
62+
"""
63+
Method for logging out of the remote cluster
64+
"""
65+
pass
66+
67+
4668
class TokenAuthentication(Authentication):
4769
"""
48-
`TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to an OpenShift
70+
`TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to a Kubernetes
4971
cluster when the user has an API token and the API server address.
5072
"""
5173

52-
def __init__(self, token: str = None, server: str = None, skip_tls: bool = False):
74+
def __init__(
75+
self,
76+
token: str,
77+
server: str,
78+
skip_tls: bool = False,
79+
ca_cert_path: str = None,
80+
):
5381
"""
5482
Initialize a TokenAuthentication object that requires a value for `token`, the API Token
55-
and `server`, the API server address for authenticating to an OpenShift cluster.
83+
and `server`, the API server address for authenticating to a Kubernetes cluster.
5684
"""
5785

5886
self.token = token
5987
self.server = server
6088
self.skip_tls = skip_tls
89+
self.ca_cert_path = ca_cert_path
6190

6291
def login(self) -> str:
6392
"""
64-
This function is used to login to an OpenShift cluster using the user's API token and API server address.
65-
Depending on the cluster, a user can choose to login in with "--insecure-skip-tls-verify` by setting `skip_tls`
66-
to `True`.
93+
This function is used to log in to a Kubernetes cluster using the user's API token and API server address.
94+
Depending on the cluster, a user can choose to login in with `--insecure-skip-tls-verify` by setting `skip_tls`
95+
to `True` or `--certificate-authority` by setting `skip_tls` to False and providing a path to a ca bundle with `ca_cert_path`.
6796
"""
68-
args = [f"--token={self.token}", f"--server={self.server}"]
69-
if self.skip_tls:
70-
args.append("--insecure-skip-tls-verify")
97+
global config_path
98+
global api_client
7199
try:
72-
response = oc.invoke("login", args)
73-
except OpenShiftPythonException as osp: # pragma: no cover
74-
error_msg = osp.result.err()
75-
if "The server uses a certificate signed by unknown authority" in error_msg:
76-
return "Error: certificate auth failure, please set `skip_tls=True` in TokenAuthentication"
77-
elif "invalid" in error_msg:
78-
raise PermissionError(error_msg)
100+
configuration = client.Configuration()
101+
configuration.api_key_prefix["authorization"] = "Bearer"
102+
configuration.host = self.server
103+
configuration.api_key["authorization"] = self.token
104+
if self.skip_tls == False and self.ca_cert_path == None:
105+
configuration.verify_ssl = True
106+
elif self.skip_tls == False:
107+
configuration.ssl_ca_cert = self.ca_cert_path
79108
else:
80-
return error_msg
81-
return response.out()
109+
configuration.verify_ssl = False
110+
api_client = client.ApiClient(configuration)
111+
client.AuthenticationApi(api_client).get_api_group()
112+
config_path = None
113+
return "Logged into %s" % self.server
114+
except client.ApiException: # pragma: no cover
115+
api_client = None
116+
print("Authentication Error please provide the correct token + server")
82117

83118
def logout(self) -> str:
84119
"""
85-
This function is used to logout of an OpenShift cluster.
120+
This function is used to logout of a Kubernetes cluster.
86121
"""
87-
args = [f"--token={self.token}", f"--server={self.server}"]
88-
response = oc.invoke("logout", args)
89-
return response.out()
122+
global config_path
123+
config_path = None
124+
global api_client
125+
api_client = None
126+
return "Successfully logged out of %s" % self.server
90127

91128

92-
class PasswordUserAuthentication(Authentication):
129+
class KubeConfigFileAuthentication(KubeConfiguration):
93130
"""
94-
`PasswordUserAuthentication` is a subclass of `Authentication`. It can be used to authenticate to an OpenShift
95-
cluster when the user has a username and password.
131+
A class that defines the necessary methods for passing a user's own Kubernetes config file.
132+
Specifically this class defines the `load_kube_config()` and `config_check()` functions.
96133
"""
97134

98-
def __init__(
99-
self,
100-
username: str = None,
101-
password: str = None,
102-
):
103-
"""
104-
Initialize a PasswordUserAuthentication object that requires a value for `username`
105-
and `password` for authenticating to an OpenShift cluster.
106-
"""
107-
self.username = username
108-
self.password = password
135+
def __init__(self, kube_config_path: str = None):
136+
self.kube_config_path = kube_config_path
109137

110-
def login(self) -> str:
138+
def load_kube_config(self):
111139
"""
112-
This function is used to login to an OpenShift cluster using the user's `username` and `password`.
140+
Function for loading a user's own predefined Kubernetes config file.
113141
"""
114-
response = oc.login(self.username, self.password)
115-
return response.out()
142+
global config_path
143+
global api_client
144+
try:
145+
if self.kube_config_path == None:
146+
return "Please specify a config file path"
147+
config_path = self.kube_config_path
148+
api_client = None
149+
config.load_kube_config(config_path)
150+
response = "Loaded user config file at path %s" % self.kube_config_path
151+
except config.ConfigException: # pragma: no cover
152+
config_path = None
153+
raise Exception("Please specify a config file path")
154+
return response
155+
156+
157+
def config_check() -> str:
158+
"""
159+
Function for loading the config file at the default config location ~/.kube/config if the user has not
160+
specified their own config file or has logged in with their token and server.
161+
"""
162+
global config_path
163+
global api_client
164+
if config_path == None and api_client == None:
165+
config.load_kube_config()
166+
if config_path != None and api_client == None:
167+
return config_path
116168

117-
def logout(self) -> str:
118-
"""
119-
This function is used to logout of an OpenShift cluster.
120-
"""
121-
response = oc.invoke("logout")
122-
return response.out()
169+
170+
def api_config_handler() -> str:
171+
"""
172+
This function is used to load the api client if the user has logged in
173+
"""
174+
if api_client != None and config_path == None:
175+
return api_client
176+
else:
177+
return None

src/codeflare_sdk/cluster/awload.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from kubernetes import client, config
2626
from ..utils.kube_api_helpers import _kube_api_error_handling
27+
from .auth import config_check, api_config_handler
2728

2829

2930
class AWManager:
@@ -57,8 +58,8 @@ def submit(self) -> None:
5758
Attempts to create the AppWrapper custom resource using the yaml file
5859
"""
5960
try:
60-
config.load_kube_config()
61-
api_instance = client.CustomObjectsApi()
61+
config_check()
62+
api_instance = client.CustomObjectsApi(api_config_handler())
6263
api_instance.create_namespaced_custom_object(
6364
group="mcad.ibm.com",
6465
version="v1beta1",
@@ -82,8 +83,8 @@ def remove(self) -> None:
8283
return
8384

8485
try:
85-
config.load_kube_config()
86-
api_instance = client.CustomObjectsApi()
86+
config_check()
87+
api_instance = client.CustomObjectsApi(api_config_handler())
8788
api_instance.delete_namespaced_custom_object(
8889
group="mcad.ibm.com",
8990
version="v1beta1",

src/codeflare_sdk/cluster/cluster.py

+44-27
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from ray.job_submission import JobSubmissionClient
2525

26+
from .auth import config_check, api_config_handler
2627
from ..utils import pretty_print
2728
from ..utils.generate_yaml import generate_appwrapper
2829
from ..utils.kube_api_helpers import _kube_api_error_handling
@@ -35,8 +36,8 @@
3536
RayClusterStatus,
3637
)
3738
from kubernetes import client, config
38-
3939
import yaml
40+
import os
4041

4142

4243
class Cluster:
@@ -68,7 +69,9 @@ def create_app_wrapper(self):
6869

6970
if self.config.namespace is None:
7071
self.config.namespace = get_current_namespace()
71-
if type(self.config.namespace) is not str:
72+
if self.config.namespace is None:
73+
print("Please specify with namespace=<your_current_namespace>")
74+
elif type(self.config.namespace) is not str:
7275
raise TypeError(
7376
f"Namespace {self.config.namespace} is of type {type(self.config.namespace)}. Check your Kubernetes Authentication."
7477
)
@@ -114,8 +117,8 @@ def up(self):
114117
"""
115118
namespace = self.config.namespace
116119
try:
117-
config.load_kube_config()
118-
api_instance = client.CustomObjectsApi()
120+
config_check()
121+
api_instance = client.CustomObjectsApi(api_config_handler())
119122
with open(self.app_wrapper_yaml) as f:
120123
aw = yaml.load(f, Loader=yaml.FullLoader)
121124
api_instance.create_namespaced_custom_object(
@@ -135,8 +138,8 @@ def down(self):
135138
"""
136139
namespace = self.config.namespace
137140
try:
138-
config.load_kube_config()
139-
api_instance = client.CustomObjectsApi()
141+
config_check()
142+
api_instance = client.CustomObjectsApi(api_config_handler())
140143
api_instance.delete_namespaced_custom_object(
141144
group="mcad.ibm.com",
142145
version="v1beta1",
@@ -247,8 +250,8 @@ def cluster_dashboard_uri(self) -> str:
247250
Returns a string containing the cluster's dashboard URI.
248251
"""
249252
try:
250-
config.load_kube_config()
251-
api_instance = client.CustomObjectsApi()
253+
config_check()
254+
api_instance = client.CustomObjectsApi(api_config_handler())
252255
routes = api_instance.list_namespaced_custom_object(
253256
group="route.openshift.io",
254257
version="v1",
@@ -376,15 +379,29 @@ def list_all_queued(namespace: str, print_to_console: bool = True):
376379

377380

378381
def get_current_namespace(): # pragma: no cover
379-
try:
380-
config.load_kube_config()
381-
_, active_context = config.list_kube_config_contexts()
382-
except Exception as e:
383-
return _kube_api_error_handling(e)
384-
try:
385-
return active_context["context"]["namespace"]
386-
except KeyError:
387-
return "default"
382+
if api_config_handler() != None:
383+
if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"):
384+
try:
385+
file = open(
386+
"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r"
387+
)
388+
active_context = file.readline().strip("\n")
389+
return active_context
390+
except Exception as e:
391+
print("Unable to find current namespace")
392+
return None
393+
else:
394+
print("Unable to find current namespace")
395+
return None
396+
else:
397+
try:
398+
_, active_context = config.list_kube_config_contexts(config_check())
399+
except Exception as e:
400+
return _kube_api_error_handling(e)
401+
try:
402+
return active_context["context"]["namespace"]
403+
except KeyError:
404+
return None
388405

389406

390407
def get_cluster(cluster_name: str, namespace: str = "default"):
@@ -423,8 +440,8 @@ def _get_ingress_domain():
423440

424441
def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]:
425442
try:
426-
config.load_kube_config()
427-
api_instance = client.CustomObjectsApi()
443+
config_check()
444+
api_instance = client.CustomObjectsApi(api_config_handler())
428445
aws = api_instance.list_namespaced_custom_object(
429446
group="mcad.ibm.com",
430447
version="v1beta1",
@@ -442,8 +459,8 @@ def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]:
442459

443460
def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]:
444461
try:
445-
config.load_kube_config()
446-
api_instance = client.CustomObjectsApi()
462+
config_check()
463+
api_instance = client.CustomObjectsApi(api_config_handler())
447464
rcs = api_instance.list_namespaced_custom_object(
448465
group="ray.io",
449466
version="v1alpha1",
@@ -462,8 +479,8 @@ def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]:
462479
def _get_ray_clusters(namespace="default") -> List[RayCluster]:
463480
list_of_clusters = []
464481
try:
465-
config.load_kube_config()
466-
api_instance = client.CustomObjectsApi()
482+
config_check()
483+
api_instance = client.CustomObjectsApi(api_config_handler())
467484
rcs = api_instance.list_namespaced_custom_object(
468485
group="ray.io",
469486
version="v1alpha1",
@@ -484,8 +501,8 @@ def _get_app_wrappers(
484501
list_of_app_wrappers = []
485502

486503
try:
487-
config.load_kube_config()
488-
api_instance = client.CustomObjectsApi()
504+
config_check()
505+
api_instance = client.CustomObjectsApi(api_config_handler())
489506
aws = api_instance.list_namespaced_custom_object(
490507
group="mcad.ibm.com",
491508
version="v1beta1",
@@ -511,8 +528,8 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]:
511528
else:
512529
status = RayClusterStatus.UNKNOWN
513530

514-
config.load_kube_config()
515-
api_instance = client.CustomObjectsApi()
531+
config_check()
532+
api_instance = client.CustomObjectsApi(api_config_handler())
516533
routes = api_instance.list_namespaced_custom_object(
517534
group="route.openshift.io",
518535
version="v1",

0 commit comments

Comments
 (0)