From 924c2a5fc4d5de7f0b64c9b668185e64dee7b1b9 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Wed, 28 Jun 2023 16:25:54 +0100 Subject: [PATCH 01/16] Updated authentication for Kubernetes --- requirements.txt | 1 + src/codeflare_sdk/cluster/auth.py | 252 ++++++++++++++++++----- src/codeflare_sdk/cluster/awload.py | 4 +- src/codeflare_sdk/cluster/cluster.py | 19 +- src/codeflare_sdk/templates/config.yaml | 19 ++ src/codeflare_sdk/utils/generate_cert.py | 3 +- 6 files changed, 238 insertions(+), 60 deletions(-) create mode 100644 src/codeflare_sdk/templates/config.yaml diff --git a/requirements.txt b/requirements.txt index 2a48812aa..b2a263d68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ codeflare-torchx==0.6.0.dev0 pydantic<2 # 2.0+ broke ray[default] see detail: https://github.com/ray-project/ray/pull/37000 cryptography==40.0.2 executing==1.2.0 +jinja2==3.1.2 \ No newline at end of file diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 33ad8cf7d..3f6f778e7 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -20,8 +20,22 @@ """ import abc -import openshift as oc -from openshift import OpenShiftPythonException +import pathlib +from kubernetes import config +from jinja2 import Environment, FileSystemLoader +import os + +global path_set +path_set = False + +""" +auth = KubeConfigFileAuthentication( + kube_config_path="config" + ) +auth.load_kube_config() + + +""" class Authentication(metaclass=abc.ABCMeta): @@ -43,13 +57,44 @@ def logout(self): pass +class KubeConfiguration(metaclass=abc.ABCMeta): + """ + An abstract class that defines the method for loading a user defined config file using the `load_kube_config()` function + """ + + def load_kube_config(self): + """ + Method for setting your Kubernetes configuration to a certain file + """ + pass + + def config_check(self): + """ + Method for setting your Kubernetes configuration to a certain file + """ + pass + + def logout(self): + """ + Method for logging out of the remote cluster + """ + pass + + class TokenAuthentication(Authentication): """ `TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to an OpenShift cluster when the user has an API token and the API server address. """ - def __init__(self, token: str = None, server: str = None, skip_tls: bool = False): + def __init__( + self, + token: str = None, + server: str = None, + skip_tls: bool = False, + ca_cert_path: str = "/etc/pki/tls/certs/ca-bundle.crt", + username: str = "user", + ): """ Initialize a TokenAuthentication object that requires a value for `token`, the API Token and `server`, the API server address for authenticating to an OpenShift cluster. @@ -58,65 +103,176 @@ def __init__(self, token: str = None, server: str = None, skip_tls: bool = False self.token = token self.server = server self.skip_tls = skip_tls + self.ca_cert_path = ca_cert_path + self.username = username def login(self) -> str: """ - This function is used to login to an OpenShift cluster using the user's API token and API server address. - Depending on the cluster, a user can choose to login in with "--insecure-skip-tls-verify` by setting `skip_tls` - to `True`. + This function is used to login to a Kubernetes cluster using the user's API token and API server address. + Depending on the cluster, a user can choose to login in with `--insecure-skip-tls-verify` by setting `skip_tls` + to `True` or `--certificate-authority` by setting `skip_tls` to false and providing a path to a ca bundle with `ca_cert_path`. + + If a user does not have a Kubernetes config file one is created from a template with the appropriate user functionality + and if they do it is updated with new credentials. """ - args = [f"--token={self.token}", f"--server={self.server}"] - if self.skip_tls: - args.append("--insecure-skip-tls-verify") + dir = pathlib.Path(__file__).parent.parent.resolve() + home = os.path.expanduser("~") try: - response = oc.invoke("login", args) - except OpenShiftPythonException as osp: # pragma: no cover - error_msg = osp.result.err() - if "The server uses a certificate signed by unknown authority" in error_msg: - return "Error: certificate auth failure, please set `skip_tls=True` in TokenAuthentication" - elif "invalid" in error_msg: - raise PermissionError(error_msg) + security = "insecure-skip-tls-verify: false" + if self.skip_tls == False: + security = "certificate-authority: %s" % self.ca_cert_path + else: + security = "insecure-skip-tls-verify: true" + + env = Environment( + loader=FileSystemLoader(f"{dir}/templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + template = env.get_template("config.yaml") + server = self.server + cluster_name = server[8:].replace(".", "-") + # If there is no .kube folder it is created. + if not os.path.isdir("%s/.kube" % home): + os.mkdir("%s/.kube" % home) + + # If a config file exists then it will be updated with new fields and values. + if os.path.isfile("%s/.kube/config" % home): + file = open(r"%s/.kube/config" % home, "r").readlines() + write_file = open(r"%s/.kube/config" % home, "w") + existing = False + # Check for existing config + for line in file: + if self.server in line: + existing = True + + if existing == False: + for line in file: + # All of these fields are given new lines underneath with credentials info. + if "clusters:" in line: + write_file.write(line) + write_file.write( + "- cluster:\n %(security)s\n server: %(server)s\n name: %(cluster)s\n" + % { + "security": security, + "server": self.server, + "cluster": cluster_name, + } + ) + continue + if "contexts:" in line: + write_file.write(line) + write_file.write( + "- context:\n cluster: %(cluster)s\n namespace: default\n user: %(user)s/%(cluster)s\n name: default/%(cluster)s/%(user)s\n" + % {"cluster": cluster_name, "user": self.username} + ) + continue + if "current-context:" in line: + write_file.write( + "current-context: default/{}/{}\n".format( + cluster_name, self.username + ) + ) + continue + if "users:" in line: + write_file.write(line) + write_file.write( + "- name: {}/{}\n user:\n token: {}\n".format( + self.username, cluster_name, self.token + ) + ) + continue + + write_file.write(line) + else: + # If there is an existing config just update the token and username + for line in file: + if "users:" in line: + write_file.write(line) + write_file.write( + "- name: {}/{}\n user:\n token: {}\n".format( + self.username, cluster_name, self.token + ) + ) + continue + write_file.write(line) + + response = "Updated config file at %s/.kube/config" % home else: - return error_msg - return response.out() + # Create a new config file from the config template and store it in HOME/.kube + file = open("%s/.kube/config" % home, "w") + file.write( + template.render( + security=security, + server=server, + cluster=cluster_name, + context_name="default/{}/{}".format( + cluster_name, self.username + ), + current_context="default/{}/{}".format( + cluster_name, self.username + ), + username="{}/{}".format(self.username, cluster_name), + token=self.token, + ) + ) + response = ( + "Logged in and created new config file at %s/.kube/config" % home + ) + except: + response = "Error logging in. Have you inputted correct credentials?" + return response def logout(self) -> str: """ - This function is used to logout of an OpenShift cluster. + This function is used to logout of a Kubernetes cluster. """ - args = [f"--token={self.token}", f"--server={self.server}"] - response = oc.invoke("logout", args) - return response.out() + home = os.path.expanduser("~") + file = open(r"%s/.kube/config" % home, "r") + lines = file.readlines() + line_count = 0 + for line in lines: + if ( + "- name: {}/{}".format(self.username, self.server[8:].replace(".", "-")) + not in line.strip() + ): + line_count = line_count + 1 + else: + break + # The name, user and token are removed from the config file + with open(r"%s/.kube/config" % home, "w") as file: + for number, line in enumerate(lines): + if number not in [line_count, line_count + 1, line_count + 2]: + file.write(line) + print("logged out of user %s" % self.username) -class PasswordUserAuthentication(Authentication): +class KubeConfigFileAuthentication(KubeConfiguration): """ - `PasswordUserAuthentication` is a subclass of `Authentication`. It can be used to authenticate to an OpenShift - cluster when the user has a username and password. + An abstract class that defines the necessary methods for passing a user's own Kubernetes config file. + Specifically this class defines the `load_kube_config()`, `config_check()` and `remove_config()` functions. """ - def __init__( - self, - username: str = None, - password: str = None, - ): - """ - Initialize a PasswordUserAuthentication object that requires a value for `username` - and `password` for authenticating to an OpenShift cluster. - """ - self.username = username - self.password = password + def __init__(self, kube_config_path: str = None): + self.kube_config_path = kube_config_path - def login(self) -> str: - """ - This function is used to login to an OpenShift cluster using the user's `username` and `password`. - """ - response = oc.login(self.username, self.password) - return response.out() + def load_kube_config(self): + global path_set + try: + path_set = True + print("Loaded user config file at path %s" % self.kube_config_path) + response = config.load_kube_config(self.kube_config_path) + except config.ConfigException: + path_set = False + raise Exception("Please specify a config file path") + return response - def logout(self) -> str: - """ - This function is used to logout of an OpenShift cluster. - """ - response = oc.invoke("logout") - return response.out() + def config_check(): + if path_set == False: + config.load_kube_config() + + def remove_config(self) -> str: + global path_set + path_set = False + os.remove(self.kube_config_path) + print("Removed config file") diff --git a/src/codeflare_sdk/cluster/awload.py b/src/codeflare_sdk/cluster/awload.py index ecf432133..fdb16a356 100644 --- a/src/codeflare_sdk/cluster/awload.py +++ b/src/codeflare_sdk/cluster/awload.py @@ -57,7 +57,7 @@ def submit(self) -> None: Attempts to create the AppWrapper custom resource using the yaml file """ try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() api_instance.create_namespaced_custom_object( group="mcad.ibm.com", @@ -82,7 +82,7 @@ def remove(self) -> None: return try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index 6fb57abbe..c66eb73e4 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -23,6 +23,7 @@ from ray.job_submission import JobSubmissionClient +from .auth import KubeConfigFileAuthentication from ..utils import pretty_print from ..utils.generate_yaml import generate_appwrapper from ..utils.kube_api_helpers import _kube_api_error_handling @@ -114,7 +115,7 @@ def up(self): """ namespace = self.config.namespace try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() with open(self.app_wrapper_yaml) as f: aw = yaml.load(f, Loader=yaml.FullLoader) @@ -135,7 +136,7 @@ def down(self): """ namespace = self.config.namespace try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", @@ -247,7 +248,7 @@ def cluster_dashboard_uri(self) -> str: Returns a string containing the cluster's dashboard URI. """ try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", @@ -377,7 +378,7 @@ def list_all_queued(namespace: str, print_to_console: bool = True): def get_current_namespace(): # pragma: no cover try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() _, active_context = config.list_kube_config_contexts() except Exception as e: return _kube_api_error_handling(e) @@ -423,7 +424,7 @@ def _get_ingress_domain(): def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", @@ -442,7 +443,7 @@ def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() rcs = api_instance.list_namespaced_custom_object( group="ray.io", @@ -462,7 +463,7 @@ def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: def _get_ray_clusters(namespace="default") -> List[RayCluster]: list_of_clusters = [] try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() rcs = api_instance.list_namespaced_custom_object( group="ray.io", @@ -484,7 +485,7 @@ def _get_app_wrappers( list_of_app_wrappers = [] try: - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", @@ -511,7 +512,7 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]: else: status = RayClusterStatus.UNKNOWN - config.load_kube_config() + KubeConfigFileAuthentication.config_check() api_instance = client.CustomObjectsApi() routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", diff --git a/src/codeflare_sdk/templates/config.yaml b/src/codeflare_sdk/templates/config.yaml new file mode 100644 index 000000000..7b73d5e29 --- /dev/null +++ b/src/codeflare_sdk/templates/config.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + {{ security }} + server: {{ server }} + name: {{ cluster }} +contexts: +- context: + cluster: {{ cluster }} + namespace: default + user: {{ username }} + name: {{ context_name }} +current-context: {{ current_context }} +kind: Config +preferences: {} +users: +- name: {{ username }} + user: + token: {{ token }} diff --git a/src/codeflare_sdk/utils/generate_cert.py b/src/codeflare_sdk/utils/generate_cert.py index 2d73621b8..8ba630570 100644 --- a/src/codeflare_sdk/utils/generate_cert.py +++ b/src/codeflare_sdk/utils/generate_cert.py @@ -19,6 +19,7 @@ from cryptography import x509 from cryptography.x509.oid import NameOID import datetime +from ..cluster.auth import KubeConfigFileAuthentication from kubernetes import client, config @@ -82,7 +83,7 @@ def generate_tls_cert(cluster_name, namespace, days=30): # Similar to: # oc get secret ca-secret- -o template='{{index .data "ca.key"}}' # oc get secret ca-secret- -o template='{{index .data "ca.crt"}}'|base64 -d > ${TLSDIR}/ca.crt - config.load_kube_config() + KubeConfigFileAuthentication.config_check() v1 = client.CoreV1Api() secret = v1.read_namespaced_secret(f"ca-secret-{cluster_name}", namespace).data ca_cert = secret.get("ca.crt") From c5700a3ec157e52f7ab5c3db1d87c0a23051954f Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Thu, 29 Jun 2023 08:59:29 +0100 Subject: [PATCH 02/16] Updated template name and comment --- src/codeflare_sdk/cluster/auth.py | 2 +- .../templates/{config.yaml => kubconfig-template.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/codeflare_sdk/templates/{config.yaml => kubconfig-template.yaml} (100%) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 3f6f778e7..cf7c35d5f 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -249,7 +249,7 @@ def logout(self) -> str: class KubeConfigFileAuthentication(KubeConfiguration): """ - An abstract class that defines the necessary methods for passing a user's own Kubernetes config file. + A class that defines the necessary methods for passing a user's own Kubernetes config file. Specifically this class defines the `load_kube_config()`, `config_check()` and `remove_config()` functions. """ diff --git a/src/codeflare_sdk/templates/config.yaml b/src/codeflare_sdk/templates/kubconfig-template.yaml similarity index 100% rename from src/codeflare_sdk/templates/config.yaml rename to src/codeflare_sdk/templates/kubconfig-template.yaml From 87f067444dd47b72e919b1f06c88497b71c03f6c Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Thu, 29 Jun 2023 11:55:12 +0100 Subject: [PATCH 03/16] Updated login functionality --- requirements.txt | 1 - src/codeflare_sdk/cluster/auth.py | 193 ++++-------------- src/codeflare_sdk/cluster/awload.py | 9 +- src/codeflare_sdk/cluster/cluster.py | 25 ++- .../templates/kubconfig-template.yaml | 19 -- src/codeflare_sdk/utils/generate_cert.py | 4 +- 6 files changed, 66 insertions(+), 185 deletions(-) delete mode 100644 src/codeflare_sdk/templates/kubconfig-template.yaml diff --git a/requirements.txt b/requirements.txt index b2a263d68..2a48812aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,3 @@ codeflare-torchx==0.6.0.dev0 pydantic<2 # 2.0+ broke ray[default] see detail: https://github.com/ray-project/ray/pull/37000 cryptography==40.0.2 executing==1.2.0 -jinja2==3.1.2 \ No newline at end of file diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index cf7c35d5f..862dc7261 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -20,23 +20,12 @@ """ import abc -import pathlib -from kubernetes import config -from jinja2 import Environment, FileSystemLoader import os +from kubernetes import client, config global path_set path_set = False -""" -auth = KubeConfigFileAuthentication( - kube_config_path="config" - ) -auth.load_kube_config() - - -""" - class Authentication(metaclass=abc.ABCMeta): """ @@ -83,7 +72,7 @@ def logout(self): class TokenAuthentication(Authentication): """ - `TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to an OpenShift + `TokenAuthentication` is a subclass of `Authentication`. It can be used to authenticate to a Kubernetes cluster when the user has an API token and the API server address. """ @@ -93,186 +82,88 @@ def __init__( server: str = None, skip_tls: bool = False, ca_cert_path: str = "/etc/pki/tls/certs/ca-bundle.crt", - username: str = "user", ): """ Initialize a TokenAuthentication object that requires a value for `token`, the API Token - and `server`, the API server address for authenticating to an OpenShift cluster. + and `server`, the API server address for authenticating to a Kubernetes cluster. """ self.token = token self.server = server self.skip_tls = skip_tls self.ca_cert_path = ca_cert_path - self.username = username def login(self) -> str: """ - This function is used to login to a Kubernetes cluster using the user's API token and API server address. + This function is used to log in to a Kubernetes cluster using the user's API token and API server address. Depending on the cluster, a user can choose to login in with `--insecure-skip-tls-verify` by setting `skip_tls` to `True` or `--certificate-authority` by setting `skip_tls` to false and providing a path to a ca bundle with `ca_cert_path`. - - If a user does not have a Kubernetes config file one is created from a template with the appropriate user functionality - and if they do it is updated with new credentials. """ - dir = pathlib.Path(__file__).parent.parent.resolve() - home = os.path.expanduser("~") + global path_set + global api_client try: - security = "insecure-skip-tls-verify: false" + configuration = client.Configuration() + configuration.api_key_prefix["authorization"] = "Bearer" + configuration.host = self.server + configuration.api_key["authorization"] = self.token if self.skip_tls == False: - security = "certificate-authority: %s" % self.ca_cert_path + configuration.ssl_ca_cert = self.ca_cert_path else: - security = "insecure-skip-tls-verify: true" - - env = Environment( - loader=FileSystemLoader(f"{dir}/templates"), - trim_blocks=True, - lstrip_blocks=True, - ) - template = env.get_template("config.yaml") - server = self.server - cluster_name = server[8:].replace(".", "-") - # If there is no .kube folder it is created. - if not os.path.isdir("%s/.kube" % home): - os.mkdir("%s/.kube" % home) - - # If a config file exists then it will be updated with new fields and values. - if os.path.isfile("%s/.kube/config" % home): - file = open(r"%s/.kube/config" % home, "r").readlines() - write_file = open(r"%s/.kube/config" % home, "w") - existing = False - # Check for existing config - for line in file: - if self.server in line: - existing = True - - if existing == False: - for line in file: - # All of these fields are given new lines underneath with credentials info. - if "clusters:" in line: - write_file.write(line) - write_file.write( - "- cluster:\n %(security)s\n server: %(server)s\n name: %(cluster)s\n" - % { - "security": security, - "server": self.server, - "cluster": cluster_name, - } - ) - continue - if "contexts:" in line: - write_file.write(line) - write_file.write( - "- context:\n cluster: %(cluster)s\n namespace: default\n user: %(user)s/%(cluster)s\n name: default/%(cluster)s/%(user)s\n" - % {"cluster": cluster_name, "user": self.username} - ) - continue - if "current-context:" in line: - write_file.write( - "current-context: default/{}/{}\n".format( - cluster_name, self.username - ) - ) - continue - if "users:" in line: - write_file.write(line) - write_file.write( - "- name: {}/{}\n user:\n token: {}\n".format( - self.username, cluster_name, self.token - ) - ) - continue - - write_file.write(line) - else: - # If there is an existing config just update the token and username - for line in file: - if "users:" in line: - write_file.write(line) - write_file.write( - "- name: {}/{}\n user:\n token: {}\n".format( - self.username, cluster_name, self.token - ) - ) - continue - write_file.write(line) + configuration.verify_ssl = False + api_client = client.ApiClient(configuration) + path_set = False + return "Logged into %s" % self.server + except client.ApiException as exception: + return exception - response = "Updated config file at %s/.kube/config" % home - else: - # Create a new config file from the config template and store it in HOME/.kube - file = open("%s/.kube/config" % home, "w") - file.write( - template.render( - security=security, - server=server, - cluster=cluster_name, - context_name="default/{}/{}".format( - cluster_name, self.username - ), - current_context="default/{}/{}".format( - cluster_name, self.username - ), - username="{}/{}".format(self.username, cluster_name), - token=self.token, - ) - ) - response = ( - "Logged in and created new config file at %s/.kube/config" % home - ) - except: - response = "Error logging in. Have you inputted correct credentials?" - return response + def api_config_handler(): + """ + This function is used to load the api client if the user has logged in + """ + if api_client != None and path_set == False: + return api_client + else: + return None def logout(self) -> str: """ This function is used to logout of a Kubernetes cluster. """ - home = os.path.expanduser("~") - file = open(r"%s/.kube/config" % home, "r") - lines = file.readlines() - line_count = 0 - for line in lines: - if ( - "- name: {}/{}".format(self.username, self.server[8:].replace(".", "-")) - not in line.strip() - ): - line_count = line_count + 1 - else: - break - # The name, user and token are removed from the config file - with open(r"%s/.kube/config" % home, "w") as file: - for number, line in enumerate(lines): - if number not in [line_count, line_count + 1, line_count + 2]: - file.write(line) - print("logged out of user %s" % self.username) + global path_set + path_set = False + global api_client + api_client = None class KubeConfigFileAuthentication(KubeConfiguration): """ A class that defines the necessary methods for passing a user's own Kubernetes config file. - Specifically this class defines the `load_kube_config()`, `config_check()` and `remove_config()` functions. + Specifically this class defines the `load_kube_config()` and `config_check()` functions. """ def __init__(self, kube_config_path: str = None): self.kube_config_path = kube_config_path def load_kube_config(self): + """ + Function for loading a user's own predefined Kubernetes config file. + """ global path_set + global api_client try: path_set = True - print("Loaded user config file at path %s" % self.kube_config_path) - response = config.load_kube_config(self.kube_config_path) + api_client = None + config.load_kube_config(self.kube_config_path) + response = "Loaded user config file at path %s" % self.kube_config_path except config.ConfigException: path_set = False raise Exception("Please specify a config file path") return response def config_check(): - if path_set == False: + """ + Function for loading the config file at the default config location ~/.kube/config if the user has not + specified their own config file or has logged in with their token and server. + """ + if path_set == False and api_client == None: config.load_kube_config() - - def remove_config(self) -> str: - global path_set - path_set = False - os.remove(self.kube_config_path) - print("Removed config file") diff --git a/src/codeflare_sdk/cluster/awload.py b/src/codeflare_sdk/cluster/awload.py index fdb16a356..12f76a63f 100644 --- a/src/codeflare_sdk/cluster/awload.py +++ b/src/codeflare_sdk/cluster/awload.py @@ -24,6 +24,7 @@ from kubernetes import client, config from ..utils.kube_api_helpers import _kube_api_error_handling +from .auth import KubeConfigFileAuthentication, TokenAuthentication class AWManager: @@ -58,7 +59,9 @@ def submit(self) -> None: """ try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi( + TokenAuthentication.api_config_handler() + ) api_instance.create_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -83,7 +86,9 @@ def remove(self) -> None: try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi( + TokenAuthentication.api_config_handler() + ) api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index c66eb73e4..a7121094f 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -23,7 +23,7 @@ from ray.job_submission import JobSubmissionClient -from .auth import KubeConfigFileAuthentication +from .auth import KubeConfigFileAuthentication, TokenAuthentication from ..utils import pretty_print from ..utils.generate_yaml import generate_appwrapper from ..utils.kube_api_helpers import _kube_api_error_handling @@ -36,7 +36,6 @@ RayClusterStatus, ) from kubernetes import client, config - import yaml @@ -116,7 +115,9 @@ def up(self): namespace = self.config.namespace try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi( + TokenAuthentication.api_config_handler() + ) with open(self.app_wrapper_yaml) as f: aw = yaml.load(f, Loader=yaml.FullLoader) api_instance.create_namespaced_custom_object( @@ -137,7 +138,9 @@ def down(self): namespace = self.config.namespace try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi( + TokenAuthentication.api_config_handler() + ) api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -249,7 +252,9 @@ def cluster_dashboard_uri(self) -> str: """ try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi( + TokenAuthentication.api_config_handler() + ) routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", version="v1", @@ -425,7 +430,7 @@ def _get_ingress_domain(): def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -444,7 +449,7 @@ def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) rcs = api_instance.list_namespaced_custom_object( group="ray.io", version="v1alpha1", @@ -464,7 +469,7 @@ def _get_ray_clusters(namespace="default") -> List[RayCluster]: list_of_clusters = [] try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) rcs = api_instance.list_namespaced_custom_object( group="ray.io", version="v1alpha1", @@ -486,7 +491,7 @@ def _get_app_wrappers( try: KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -513,7 +518,7 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]: status = RayClusterStatus.UNKNOWN KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi() + api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", version="v1", diff --git a/src/codeflare_sdk/templates/kubconfig-template.yaml b/src/codeflare_sdk/templates/kubconfig-template.yaml deleted file mode 100644 index 7b73d5e29..000000000 --- a/src/codeflare_sdk/templates/kubconfig-template.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - {{ security }} - server: {{ server }} - name: {{ cluster }} -contexts: -- context: - cluster: {{ cluster }} - namespace: default - user: {{ username }} - name: {{ context_name }} -current-context: {{ current_context }} -kind: Config -preferences: {} -users: -- name: {{ username }} - user: - token: {{ token }} diff --git a/src/codeflare_sdk/utils/generate_cert.py b/src/codeflare_sdk/utils/generate_cert.py index 8ba630570..0e28f6fae 100644 --- a/src/codeflare_sdk/utils/generate_cert.py +++ b/src/codeflare_sdk/utils/generate_cert.py @@ -19,7 +19,7 @@ from cryptography import x509 from cryptography.x509.oid import NameOID import datetime -from ..cluster.auth import KubeConfigFileAuthentication +from ..cluster.auth import KubeConfigFileAuthentication, TokenAuthentication from kubernetes import client, config @@ -84,7 +84,7 @@ def generate_tls_cert(cluster_name, namespace, days=30): # oc get secret ca-secret- -o template='{{index .data "ca.key"}}' # oc get secret ca-secret- -o template='{{index .data "ca.crt"}}'|base64 -d > ${TLSDIR}/ca.crt KubeConfigFileAuthentication.config_check() - v1 = client.CoreV1Api() + v1 = client.CoreV1Api(TokenAuthentication.api_config_handler()) secret = v1.read_namespaced_secret(f"ca-secret-{cluster_name}", namespace).data ca_cert = secret.get("ca.crt") ca_key = secret.get("ca.key") From d8e2c6c99867ea939d9158252ae5fca0fecfd8d1 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Thu, 29 Jun 2023 12:19:42 +0100 Subject: [PATCH 04/16] Altered config_check() function --- src/codeflare_sdk/cluster/auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 862dc7261..6ea62e4e0 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -25,6 +25,8 @@ global path_set path_set = False +global api_client +api_client = None class Authentication(metaclass=abc.ABCMeta): @@ -165,5 +167,7 @@ def config_check(): Function for loading the config file at the default config location ~/.kube/config if the user has not specified their own config file or has logged in with their token and server. """ + global path_set + global api_client if path_set == False and api_client == None: config.load_kube_config() From 49555d5c4b56df7b7e017cc4edfe365b36e0f626 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Mon, 3 Jul 2023 14:11:36 +0100 Subject: [PATCH 05/16] Altered comments and changed config_check() function --- src/codeflare_sdk/cluster/auth.py | 38 +++++++++++++++------------- src/codeflare_sdk/cluster/cluster.py | 6 +++-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 6ea62e4e0..e20284b2b 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -23,10 +23,10 @@ import os from kubernetes import client, config -global path_set -path_set = False global api_client api_client = None +global config_path +config_path = None class Authentication(metaclass=abc.ABCMeta): @@ -61,7 +61,7 @@ def load_kube_config(self): def config_check(self): """ - Method for setting your Kubernetes configuration to a certain file + Method for checking if a user is authenticated via token and server or with their own config file """ pass @@ -99,9 +99,9 @@ def login(self) -> str: """ This function is used to log in to a Kubernetes cluster using the user's API token and API server address. Depending on the cluster, a user can choose to login in with `--insecure-skip-tls-verify` by setting `skip_tls` - to `True` or `--certificate-authority` by setting `skip_tls` to false and providing a path to a ca bundle with `ca_cert_path`. + to `True` or `--certificate-authority` by setting `skip_tls` to False and providing a path to a ca bundle with `ca_cert_path`. """ - global path_set + global config_path global api_client try: configuration = client.Configuration() @@ -113,16 +113,16 @@ def login(self) -> str: else: configuration.verify_ssl = False api_client = client.ApiClient(configuration) - path_set = False + config_path = None return "Logged into %s" % self.server except client.ApiException as exception: return exception - def api_config_handler(): + def api_config_handler() -> str: """ This function is used to load the api client if the user has logged in """ - if api_client != None and path_set == False: + if api_client != None and config_path == None: return api_client else: return None @@ -131,10 +131,11 @@ def logout(self) -> str: """ This function is used to logout of a Kubernetes cluster. """ - global path_set - path_set = False + global config_path + config_path = None global api_client api_client = None + return "Successfully logged out of %s" % self.server class KubeConfigFileAuthentication(KubeConfiguration): @@ -150,24 +151,27 @@ def load_kube_config(self): """ Function for loading a user's own predefined Kubernetes config file. """ - global path_set + global config_path global api_client try: - path_set = True + if self.kube_config_path == None: + return "Please specify a config file path" + config_path = self.kube_config_path api_client = None - config.load_kube_config(self.kube_config_path) response = "Loaded user config file at path %s" % self.kube_config_path except config.ConfigException: - path_set = False + config_path = None raise Exception("Please specify a config file path") return response - def config_check(): + def config_check() -> str: """ Function for loading the config file at the default config location ~/.kube/config if the user has not specified their own config file or has logged in with their token and server. """ - global path_set + global config_path global api_client - if path_set == False and api_client == None: + if config_path == None and api_client == None: config.load_kube_config() + if config_path != None and api_client == None: + return config_path diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index a7121094f..4525f3b67 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -383,8 +383,10 @@ def list_all_queued(namespace: str, print_to_console: bool = True): def get_current_namespace(): # pragma: no cover try: - KubeConfigFileAuthentication.config_check() - _, active_context = config.list_kube_config_contexts() + # KubeConfigFileAuthentication.config_check() + _, active_context = config.list_kube_config_contexts( + KubeConfigFileAuthentication.config_check() + ) except Exception as e: return _kube_api_error_handling(e) try: From d35ae482d2ba3f7edd93d1fe6e7722058ab0eadc Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Fri, 14 Jul 2023 12:34:31 +0100 Subject: [PATCH 06/16] Added logic for handling current namespace when a user authenticates via kube client --- src/codeflare_sdk/cluster/auth.py | 1 + src/codeflare_sdk/cluster/cluster.py | 36 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index e20284b2b..3013910d2 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -158,6 +158,7 @@ def load_kube_config(self): return "Please specify a config file path" config_path = self.kube_config_path api_client = None + config.load_kube_config(config_path) response = "Loaded user config file at path %s" % self.kube_config_path except config.ConfigException: config_path = None diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index 4525f3b67..f0cddec15 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -37,7 +37,7 @@ ) from kubernetes import client, config import yaml - +import os class Cluster: """ @@ -382,17 +382,29 @@ def list_all_queued(namespace: str, print_to_console: bool = True): def get_current_namespace(): # pragma: no cover - try: - # KubeConfigFileAuthentication.config_check() - _, active_context = config.list_kube_config_contexts( - KubeConfigFileAuthentication.config_check() - ) - except Exception as e: - return _kube_api_error_handling(e) - try: - return active_context["context"]["namespace"] - except KeyError: - return "default" + namespace_error = "Unable to find current namespace please specify with namespace=" + if TokenAuthentication.api_config_handler() != None: + if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"): + try: + file = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") + active_context = file.readline().strip('\n') + return active_context + except Exception as e: + return namespace_error + else: + return namespace_error + else: + try: + # KubeConfigFileAuthentication.config_check() + _, active_context = config.list_kube_config_contexts( + KubeConfigFileAuthentication.config_check() + ) + except Exception as e: + return _kube_api_error_handling(e) + try: + return active_context["context"]["namespace"] + except KeyError: + return namespace_error def get_cluster(cluster_name: str, namespace: str = "default"): From 804894803e41df63b982a7088ce086eadf4bf6a3 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Fri, 14 Jul 2023 12:48:32 +0100 Subject: [PATCH 07/16] Changed formatting --- src/codeflare_sdk/cluster/cluster.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index f0cddec15..faee8e6d8 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -39,6 +39,7 @@ import yaml import os + class Cluster: """ An object for requesting, bringing up, and taking down resources. @@ -386,12 +387,14 @@ def get_current_namespace(): # pragma: no cover if TokenAuthentication.api_config_handler() != None: if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"): try: - file = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") - active_context = file.readline().strip('\n') + file = open( + "/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r" + ) + active_context = file.readline().strip("\n") return active_context except Exception as e: return namespace_error - else: + else: return namespace_error else: try: From 3ba4b9389fc40e4d72657ea99bc4fc5d52e35584 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Mon, 17 Jul 2023 11:55:10 +0100 Subject: [PATCH 08/16] Made handler functions generic and altered get_current_namespace() functionality --- src/codeflare_sdk/cluster/auth.py | 48 +++++++++--------- src/codeflare_sdk/cluster/awload.py | 14 ++---- src/codeflare_sdk/cluster/cluster.py | 62 +++++++++++------------- src/codeflare_sdk/utils/generate_cert.py | 6 +-- 4 files changed, 59 insertions(+), 71 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 3013910d2..5888592fa 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -59,12 +59,6 @@ def load_kube_config(self): """ pass - def config_check(self): - """ - Method for checking if a user is authenticated via token and server or with their own config file - """ - pass - def logout(self): """ Method for logging out of the remote cluster @@ -118,15 +112,6 @@ def login(self) -> str: except client.ApiException as exception: return exception - def api_config_handler() -> str: - """ - This function is used to load the api client if the user has logged in - """ - if api_client != None and config_path == None: - return api_client - else: - return None - def logout(self) -> str: """ This function is used to logout of a Kubernetes cluster. @@ -165,14 +150,25 @@ def load_kube_config(self): raise Exception("Please specify a config file path") return response - def config_check() -> str: - """ - Function for loading the config file at the default config location ~/.kube/config if the user has not - specified their own config file or has logged in with their token and server. - """ - global config_path - global api_client - if config_path == None and api_client == None: - config.load_kube_config() - if config_path != None and api_client == None: - return config_path + +def config_check() -> str: + """ + Function for loading the config file at the default config location ~/.kube/config if the user has not + specified their own config file or has logged in with their token and server. + """ + global config_path + global api_client + if config_path == None and api_client == None: + config.load_kube_config() + if config_path != None and api_client == None: + return config_path + + +def api_config_handler() -> str: + """ + This function is used to load the api client if the user has logged in + """ + if api_client != None and config_path == None: + return api_client + else: + return None diff --git a/src/codeflare_sdk/cluster/awload.py b/src/codeflare_sdk/cluster/awload.py index 12f76a63f..12544ebac 100644 --- a/src/codeflare_sdk/cluster/awload.py +++ b/src/codeflare_sdk/cluster/awload.py @@ -24,7 +24,7 @@ from kubernetes import client, config from ..utils.kube_api_helpers import _kube_api_error_handling -from .auth import KubeConfigFileAuthentication, TokenAuthentication +from .auth import config_check, api_config_handler class AWManager: @@ -58,10 +58,8 @@ def submit(self) -> None: Attempts to create the AppWrapper custom resource using the yaml file """ try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi( - TokenAuthentication.api_config_handler() - ) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) api_instance.create_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -85,10 +83,8 @@ def remove(self) -> None: return try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi( - TokenAuthentication.api_config_handler() - ) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index faee8e6d8..b56139498 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -23,7 +23,7 @@ from ray.job_submission import JobSubmissionClient -from .auth import KubeConfigFileAuthentication, TokenAuthentication +from .auth import config_check, api_config_handler from ..utils import pretty_print from ..utils.generate_yaml import generate_appwrapper from ..utils.kube_api_helpers import _kube_api_error_handling @@ -69,7 +69,11 @@ def create_app_wrapper(self): if self.config.namespace is None: self.config.namespace = get_current_namespace() - if type(self.config.namespace) is not str: + if self.config.namespace is None: + print( + "Unable to find current namespace please specify with namespace=" + ) + elif type(self.config.namespace) is not str: raise TypeError( f"Namespace {self.config.namespace} is of type {type(self.config.namespace)}. Check your Kubernetes Authentication." ) @@ -115,10 +119,8 @@ def up(self): """ namespace = self.config.namespace try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi( - TokenAuthentication.api_config_handler() - ) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) with open(self.app_wrapper_yaml) as f: aw = yaml.load(f, Loader=yaml.FullLoader) api_instance.create_namespaced_custom_object( @@ -138,10 +140,8 @@ def down(self): """ namespace = self.config.namespace try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi( - TokenAuthentication.api_config_handler() - ) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) api_instance.delete_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -252,10 +252,8 @@ def cluster_dashboard_uri(self) -> str: Returns a string containing the cluster's dashboard URI. """ try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi( - TokenAuthentication.api_config_handler() - ) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", version="v1", @@ -383,8 +381,7 @@ def list_all_queued(namespace: str, print_to_console: bool = True): def get_current_namespace(): # pragma: no cover - namespace_error = "Unable to find current namespace please specify with namespace=" - if TokenAuthentication.api_config_handler() != None: + if api_config_handler() != None: if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"): try: file = open( @@ -393,21 +390,20 @@ def get_current_namespace(): # pragma: no cover active_context = file.readline().strip("\n") return active_context except Exception as e: - return namespace_error + print("Unable to find current namespace") + return None else: - return namespace_error + print("Unable to find current namespace") + return None else: try: - # KubeConfigFileAuthentication.config_check() - _, active_context = config.list_kube_config_contexts( - KubeConfigFileAuthentication.config_check() - ) + _, active_context = config.list_kube_config_contexts(config_check()) except Exception as e: return _kube_api_error_handling(e) try: return active_context["context"]["namespace"] except KeyError: - return namespace_error + return None def get_cluster(cluster_name: str, namespace: str = "default"): @@ -446,8 +442,8 @@ def _get_ingress_domain(): def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -465,8 +461,8 @@ def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]: def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) rcs = api_instance.list_namespaced_custom_object( group="ray.io", version="v1alpha1", @@ -485,8 +481,8 @@ def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: def _get_ray_clusters(namespace="default") -> List[RayCluster]: list_of_clusters = [] try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) rcs = api_instance.list_namespaced_custom_object( group="ray.io", version="v1alpha1", @@ -507,8 +503,8 @@ def _get_app_wrappers( list_of_app_wrappers = [] try: - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) aws = api_instance.list_namespaced_custom_object( group="mcad.ibm.com", version="v1beta1", @@ -534,8 +530,8 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]: else: status = RayClusterStatus.UNKNOWN - KubeConfigFileAuthentication.config_check() - api_instance = client.CustomObjectsApi(TokenAuthentication.api_config_handler()) + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) routes = api_instance.list_namespaced_custom_object( group="route.openshift.io", version="v1", diff --git a/src/codeflare_sdk/utils/generate_cert.py b/src/codeflare_sdk/utils/generate_cert.py index 0e28f6fae..04b04d3e0 100644 --- a/src/codeflare_sdk/utils/generate_cert.py +++ b/src/codeflare_sdk/utils/generate_cert.py @@ -19,7 +19,7 @@ from cryptography import x509 from cryptography.x509.oid import NameOID import datetime -from ..cluster.auth import KubeConfigFileAuthentication, TokenAuthentication +from ..cluster.auth import config_check, api_config_handler from kubernetes import client, config @@ -83,8 +83,8 @@ def generate_tls_cert(cluster_name, namespace, days=30): # Similar to: # oc get secret ca-secret- -o template='{{index .data "ca.key"}}' # oc get secret ca-secret- -o template='{{index .data "ca.crt"}}'|base64 -d > ${TLSDIR}/ca.crt - KubeConfigFileAuthentication.config_check() - v1 = client.CoreV1Api(TokenAuthentication.api_config_handler()) + config_check() + v1 = client.CoreV1Api(api_config_handler()) secret = v1.read_namespaced_secret(f"ca-secret-{cluster_name}", namespace).data ca_cert = secret.get("ca.crt") ca_key = secret.get("ca.key") From 39834522dfe1409ed1ee88c8b0d9ee61b0018c59 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Mon, 17 Jul 2023 12:44:55 +0100 Subject: [PATCH 09/16] Changed error message for cluster configuration --- src/codeflare_sdk/cluster/cluster.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index b56139498..ff92bfcf0 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -70,9 +70,7 @@ def create_app_wrapper(self): if self.config.namespace is None: self.config.namespace = get_current_namespace() if self.config.namespace is None: - print( - "Unable to find current namespace please specify with namespace=" - ) + print("Please specify with namespace=") elif type(self.config.namespace) is not str: raise TypeError( f"Namespace {self.config.namespace} is of type {type(self.config.namespace)}. Check your Kubernetes Authentication." From 6e93729937bdf2548225a14e2915fbddbf54556a Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Tue, 18 Jul 2023 11:48:29 +0100 Subject: [PATCH 10/16] Removed default values for token + server --- src/codeflare_sdk/cluster/auth.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 5888592fa..82b04ecae 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -20,7 +20,6 @@ """ import abc -import os from kubernetes import client, config global api_client @@ -74,8 +73,8 @@ class TokenAuthentication(Authentication): def __init__( self, - token: str = None, - server: str = None, + token: str, + server: str, skip_tls: bool = False, ca_cert_path: str = "/etc/pki/tls/certs/ca-bundle.crt", ): From 56e56e18358336b543bdc163de54f1e7a596b637 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Tue, 18 Jul 2023 14:50:12 +0100 Subject: [PATCH 11/16] Added check for correct credentials --- src/codeflare_sdk/cluster/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 82b04ecae..b1531a81a 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -106,10 +106,12 @@ def login(self) -> str: else: configuration.verify_ssl = False api_client = client.ApiClient(configuration) + client.AuthenticationApi(api_client).get_api_group() config_path = None return "Logged into %s" % self.server - except client.ApiException as exception: - return exception + except client.ApiException: + api_client = None + print("Authentication Error please provide the correct token + server") def logout(self) -> str: """ From 4d77046d9526debaeada70b2e1347f55e6e2574c Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Wed, 19 Jul 2023 10:35:25 +0100 Subject: [PATCH 12/16] Changed how using certs works with certifi.where --- src/codeflare_sdk/cluster/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index b1531a81a..1400d2918 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -76,7 +76,7 @@ def __init__( token: str, server: str, skip_tls: bool = False, - ca_cert_path: str = "/etc/pki/tls/certs/ca-bundle.crt", + ca_cert_path: str = None, ): """ Initialize a TokenAuthentication object that requires a value for `token`, the API Token @@ -101,7 +101,9 @@ def login(self) -> str: configuration.api_key_prefix["authorization"] = "Bearer" configuration.host = self.server configuration.api_key["authorization"] = self.token - if self.skip_tls == False: + if self.skip_tls == False and self.ca_cert_path == None: + configuration.verify_ssl = True + elif self.skip_tls == False: configuration.ssl_ca_cert = self.ca_cert_path else: configuration.verify_ssl = False From 453d4f7f1c1353345f7d09ceb492b2e6f6f7202d Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Wed, 19 Jul 2023 17:37:00 +0100 Subject: [PATCH 13/16] Added unit tests for new authentication methods --- tests/unit_test.py | 109 +++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 73 deletions(-) diff --git a/tests/unit_test.py b/tests/unit_test.py index 57d606e49..75a43fc8f 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -21,7 +21,7 @@ parent = Path(__file__).resolve().parents[1] sys.path.append(str(parent) + "/src") -from kubernetes import client +from kubernetes import client, config from codeflare_sdk.cluster.awload import AWManager from codeflare_sdk.cluster.cluster import ( Cluster, @@ -35,8 +35,8 @@ ) from codeflare_sdk.cluster.auth import ( TokenAuthentication, - PasswordUserAuthentication, Authentication, + KubeConfigFileAuthentication ) from codeflare_sdk.utils.pretty_print import ( print_no_resources_found, @@ -65,7 +65,6 @@ ) import openshift -from openshift import OpenShiftPythonException from openshift.selector import Selector import ray from torchx.specs import AppDryRunInfo, AppDef @@ -89,27 +88,8 @@ def att_side_effect(self): return self.high_level_operation -def att_side_effect_tls(self): - if "--insecure-skip-tls-verify" in self.high_level_operation[1]: - return self.high_level_operation - else: - raise OpenShiftPythonException( - "The server uses a certificate signed by unknown authority" - ) - - def test_token_auth_creation(): try: - token_auth = TokenAuthentication() - assert token_auth.token == None - assert token_auth.server == None - assert token_auth.skip_tls == False - - token_auth = TokenAuthentication("token") - assert token_auth.token == "token" - assert token_auth.server == None - assert token_auth.skip_tls == False - token_auth = TokenAuthentication("token", "server") assert token_auth.token == "token" assert token_auth.server == "server" @@ -130,79 +110,62 @@ def test_token_auth_creation(): assert token_auth.server == "server" assert token_auth.skip_tls == True + token_auth = TokenAuthentication(token="token", server="server", skip_tls=False) + assert token_auth.token == "token" + assert token_auth.server == "server" + assert token_auth.skip_tls == False + + token_auth = TokenAuthentication(token="token", server="server", skip_tls=False, ca_cert_path="path/to/cert") + assert token_auth.token == "token" + assert token_auth.server == "server" + assert token_auth.skip_tls == False + assert token_auth.ca_cert_path == "path/to/cert" + except Exception: assert 0 == 1 def test_token_auth_login_logout(mocker): - mocker.patch("openshift.invoke", side_effect=arg_side_effect) - mock_res = mocker.patch.object(openshift.Result, "out") - mock_res.side_effect = lambda: att_side_effect(fake_res) + mocker.patch.object(client, 'ApiClient') token_auth = TokenAuthentication(token="testtoken", server="testserver:6443") assert token_auth.login() == ( - "login", - ["--token=testtoken", "--server=testserver:6443"], + "Logged into testserver:6443" ) assert token_auth.logout() == ( - "logout", - ["--token=testtoken", "--server=testserver:6443"], + "Successfully logged out of testserver:6443" ) def test_token_auth_login_tls(mocker): - mocker.patch("openshift.invoke", side_effect=arg_side_effect) - mock_res = mocker.patch.object(openshift.Result, "out") - mock_res.side_effect = lambda: att_side_effect_tls(fake_res) - - # FIXME - Pytest mocker not allowing caught exception - # token_auth = TokenAuthentication(token="testtoken", server="testserver") - # assert token_auth.login() == "Error: certificate auth failure, please set `skip_tls=True` in TokenAuthentication" + mocker.patch.object(client, 'ApiClient') token_auth = TokenAuthentication( token="testtoken", server="testserver:6443", skip_tls=True ) assert token_auth.login() == ( - "login", - ["--token=testtoken", "--server=testserver:6443", "--insecure-skip-tls-verify"], + "Logged into testserver:6443" + ) + token_auth = TokenAuthentication( + token="testtoken", server="testserver:6443", skip_tls=False + ) + assert token_auth.login() == ( + "Logged into testserver:6443" + ) + token_auth = TokenAuthentication( + token="testtoken", server="testserver:6443", skip_tls=False, ca_cert_path="path/to/cert" + ) + assert token_auth.login() == ( + "Logged into testserver:6443" ) +def test_load_kube_config(mocker): + kube_config_auth = KubeConfigFileAuthentication() + kube_config_auth.kube_config_path = '/path/to/your/config' + mocker.patch.object(config, 'load_kube_config') -def test_passwd_auth_creation(): - try: - passwd_auth = PasswordUserAuthentication() - assert passwd_auth.username == None - assert passwd_auth.password == None - - passwd_auth = PasswordUserAuthentication("user") - assert passwd_auth.username == "user" - assert passwd_auth.password == None - - passwd_auth = PasswordUserAuthentication("user", "passwd") - assert passwd_auth.username == "user" - assert passwd_auth.password == "passwd" - - passwd_auth = PasswordUserAuthentication("user", password="passwd") - assert passwd_auth.username == "user" - assert passwd_auth.password == "passwd" - - passwd_auth = PasswordUserAuthentication(username="user", password="passwd") - assert passwd_auth.username == "user" - assert passwd_auth.password == "passwd" - - except Exception: - assert 0 == 1 - - -def test_passwd_auth_login_logout(mocker): - mocker.patch("openshift.invoke", side_effect=arg_side_effect) - mocker.patch("openshift.login", side_effect=arg_side_effect) - mock_res = mocker.patch.object(openshift.Result, "out") - mock_res.side_effect = lambda: att_side_effect(fake_res) - - token_auth = PasswordUserAuthentication(username="user", password="passwd") - assert token_auth.login() == ("user", "passwd") - assert token_auth.logout() == ("logout",) + response = kube_config_auth.load_kube_config() + assert response == "Loaded user config file at path %s"%kube_config_auth.kube_config_path def test_auth_coverage(): From ef7d1807f0ce8c73796494730b5a3044b3090fa2 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Wed, 19 Jul 2023 17:42:10 +0100 Subject: [PATCH 14/16] Fixed formatting and updated .gitignore to include test created files --- .gitignore | 2 ++ tests/unit_test.py | 45 ++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index eef1052fe..3e6302016 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ Pipfile.lock poetry.lock .venv* build/ +tls-cluster-namespace +quicktest.yaml \ No newline at end of file diff --git a/tests/unit_test.py b/tests/unit_test.py index 75a43fc8f..30bf02311 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -36,7 +36,7 @@ from codeflare_sdk.cluster.auth import ( TokenAuthentication, Authentication, - KubeConfigFileAuthentication + KubeConfigFileAuthentication, ) from codeflare_sdk.utils.pretty_print import ( print_no_resources_found, @@ -115,7 +115,9 @@ def test_token_auth_creation(): assert token_auth.server == "server" assert token_auth.skip_tls == False - token_auth = TokenAuthentication(token="token", server="server", skip_tls=False, ca_cert_path="path/to/cert") + token_auth = TokenAuthentication( + token="token", server="server", skip_tls=False, ca_cert_path="path/to/cert" + ) assert token_auth.token == "token" assert token_auth.server == "server" assert token_auth.skip_tls == False @@ -126,46 +128,43 @@ def test_token_auth_creation(): def test_token_auth_login_logout(mocker): - mocker.patch.object(client, 'ApiClient') + mocker.patch.object(client, "ApiClient") token_auth = TokenAuthentication(token="testtoken", server="testserver:6443") - assert token_auth.login() == ( - "Logged into testserver:6443" - ) - assert token_auth.logout() == ( - "Successfully logged out of testserver:6443" - ) + assert token_auth.login() == ("Logged into testserver:6443") + assert token_auth.logout() == ("Successfully logged out of testserver:6443") def test_token_auth_login_tls(mocker): - mocker.patch.object(client, 'ApiClient') + mocker.patch.object(client, "ApiClient") token_auth = TokenAuthentication( token="testtoken", server="testserver:6443", skip_tls=True ) - assert token_auth.login() == ( - "Logged into testserver:6443" - ) + assert token_auth.login() == ("Logged into testserver:6443") token_auth = TokenAuthentication( token="testtoken", server="testserver:6443", skip_tls=False ) - assert token_auth.login() == ( - "Logged into testserver:6443" - ) + assert token_auth.login() == ("Logged into testserver:6443") token_auth = TokenAuthentication( - token="testtoken", server="testserver:6443", skip_tls=False, ca_cert_path="path/to/cert" - ) - assert token_auth.login() == ( - "Logged into testserver:6443" + token="testtoken", + server="testserver:6443", + skip_tls=False, + ca_cert_path="path/to/cert", ) + assert token_auth.login() == ("Logged into testserver:6443") + def test_load_kube_config(mocker): kube_config_auth = KubeConfigFileAuthentication() - kube_config_auth.kube_config_path = '/path/to/your/config' - mocker.patch.object(config, 'load_kube_config') + kube_config_auth.kube_config_path = "/path/to/your/config" + mocker.patch.object(config, "load_kube_config") response = kube_config_auth.load_kube_config() - assert response == "Loaded user config file at path %s"%kube_config_auth.kube_config_path + assert ( + response + == "Loaded user config file at path %s" % kube_config_auth.kube_config_path + ) def test_auth_coverage(): From 339953b615e5c0a3390d120e4a3548520c0a263d Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Wed, 19 Jul 2023 17:45:09 +0100 Subject: [PATCH 15/16] Fixed .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3e6302016..fbb31b2b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ poetry.lock .venv* build/ tls-cluster-namespace -quicktest.yaml \ No newline at end of file +quicktest.yaml From 91b57a758fea15de48c6f373cc17e5c9998a3873 Mon Sep 17 00:00:00 2001 From: Bobbins228 Date: Thu, 20 Jul 2023 10:27:02 +0100 Subject: [PATCH 16/16] Updated unit authentication tests --- src/codeflare_sdk/cluster/auth.py | 4 ++-- tests/unit_test.py | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/codeflare_sdk/cluster/auth.py b/src/codeflare_sdk/cluster/auth.py index 1400d2918..85db3d61d 100644 --- a/src/codeflare_sdk/cluster/auth.py +++ b/src/codeflare_sdk/cluster/auth.py @@ -111,7 +111,7 @@ def login(self) -> str: client.AuthenticationApi(api_client).get_api_group() config_path = None return "Logged into %s" % self.server - except client.ApiException: + except client.ApiException: # pragma: no cover api_client = None print("Authentication Error please provide the correct token + server") @@ -148,7 +148,7 @@ def load_kube_config(self): api_client = None config.load_kube_config(config_path) response = "Loaded user config file at path %s" % self.kube_config_path - except config.ConfigException: + except config.ConfigException: # pragma: no cover config_path = None raise Exception("Please specify a config file path") return response diff --git a/tests/unit_test.py b/tests/unit_test.py index 30bf02311..21c1adf24 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -90,30 +90,23 @@ def att_side_effect(self): def test_token_auth_creation(): try: - token_auth = TokenAuthentication("token", "server") - assert token_auth.token == "token" - assert token_auth.server == "server" - assert token_auth.skip_tls == False - - token_auth = TokenAuthentication("token", server="server") - assert token_auth.token == "token" - assert token_auth.server == "server" - assert token_auth.skip_tls == False - token_auth = TokenAuthentication(token="token", server="server") assert token_auth.token == "token" assert token_auth.server == "server" assert token_auth.skip_tls == False + assert token_auth.ca_cert_path == None token_auth = TokenAuthentication(token="token", server="server", skip_tls=True) assert token_auth.token == "token" assert token_auth.server == "server" assert token_auth.skip_tls == True + assert token_auth.ca_cert_path == None token_auth = TokenAuthentication(token="token", server="server", skip_tls=False) assert token_auth.token == "token" assert token_auth.server == "server" assert token_auth.skip_tls == False + assert token_auth.ca_cert_path == None token_auth = TokenAuthentication( token="token", server="server", skip_tls=False, ca_cert_path="path/to/cert" @@ -130,7 +123,9 @@ def test_token_auth_creation(): def test_token_auth_login_logout(mocker): mocker.patch.object(client, "ApiClient") - token_auth = TokenAuthentication(token="testtoken", server="testserver:6443") + token_auth = TokenAuthentication( + token="testtoken", server="testserver:6443", skip_tls=False, ca_cert_path=None + ) assert token_auth.login() == ("Logged into testserver:6443") assert token_auth.logout() == ("Successfully logged out of testserver:6443") @@ -139,11 +134,11 @@ def test_token_auth_login_tls(mocker): mocker.patch.object(client, "ApiClient") token_auth = TokenAuthentication( - token="testtoken", server="testserver:6443", skip_tls=True + token="testtoken", server="testserver:6443", skip_tls=True, ca_cert_path=None ) assert token_auth.login() == ("Logged into testserver:6443") token_auth = TokenAuthentication( - token="testtoken", server="testserver:6443", skip_tls=False + token="testtoken", server="testserver:6443", skip_tls=False, ca_cert_path=None ) assert token_auth.login() == ("Logged into testserver:6443") token_auth = TokenAuthentication( @@ -156,11 +151,12 @@ def test_token_auth_login_tls(mocker): def test_load_kube_config(mocker): - kube_config_auth = KubeConfigFileAuthentication() - kube_config_auth.kube_config_path = "/path/to/your/config" mocker.patch.object(config, "load_kube_config") - + kube_config_auth = KubeConfigFileAuthentication( + kube_config_path="/path/to/your/config" + ) response = kube_config_auth.load_kube_config() + assert ( response == "Loaded user config file at path %s" % kube_config_auth.kube_config_path