Skip to content

Updated authentication for Kubernetes #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Pipfile.lock
poetry.lock
.venv*
build/
tls-cluster-namespace
quicktest.yaml
151 changes: 103 additions & 48 deletions src/codeflare_sdk/cluster/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
"""

import abc
import openshift as oc
from openshift import OpenShiftPythonException
from kubernetes import client, config

global api_client
api_client = None
global config_path
config_path = None


class Authentication(metaclass=abc.ABCMeta):
Expand All @@ -43,80 +47,131 @@ 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 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
`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.
"""

def __init__(self, token: str = None, server: str = None, skip_tls: bool = False):
def __init__(
self,
token: str,
server: str,
skip_tls: bool = False,
ca_cert_path: str = None,
):
"""
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

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 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`.
"""
args = [f"--token={self.token}", f"--server={self.server}"]
if self.skip_tls:
args.append("--insecure-skip-tls-verify")
global config_path
global api_client
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)
configuration = client.Configuration()
configuration.api_key_prefix["authorization"] = "Bearer"
configuration.host = self.server
configuration.api_key["authorization"] = self.token
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:
return error_msg
return response.out()
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: # pragma: no cover
api_client = None
print("Authentication Error please provide the correct token + server")

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()
global config_path
config_path = None
global api_client
api_client = None
return "Successfully logged out of %s" % self.server


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.
A class that defines the necessary methods for passing a user's own Kubernetes config file.
Specifically this class defines the `load_kube_config()` and `config_check()` 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:
def load_kube_config(self):
"""
This function is used to login to an OpenShift cluster using the user's `username` and `password`.
Function for loading a user's own predefined Kubernetes config file.
"""
response = oc.login(self.username, self.password)
return response.out()
global config_path
global api_client
try:
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(config_path)
response = "Loaded user config file at path %s" % self.kube_config_path
except config.ConfigException: # pragma: no cover
config_path = None
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 logout(self) -> str:
"""
This function is used to logout of an OpenShift cluster.
"""
response = oc.invoke("logout")
return response.out()

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
9 changes: 5 additions & 4 deletions src/codeflare_sdk/cluster/awload.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from kubernetes import client, config
from ..utils.kube_api_helpers import _kube_api_error_handling
from .auth import config_check, api_config_handler


class AWManager:
Expand Down Expand Up @@ -57,8 +58,8 @@ def submit(self) -> None:
Attempts to create the AppWrapper custom resource using the yaml file
"""
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
api_instance.create_namespaced_custom_object(
group="mcad.ibm.com",
version="v1beta1",
Expand All @@ -82,8 +83,8 @@ def remove(self) -> None:
return

try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
api_instance.delete_namespaced_custom_object(
group="mcad.ibm.com",
version="v1beta1",
Expand Down
71 changes: 44 additions & 27 deletions src/codeflare_sdk/cluster/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from ray.job_submission import JobSubmissionClient

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
Expand All @@ -35,8 +36,8 @@
RayClusterStatus,
)
from kubernetes import client, config

import yaml
import os


class Cluster:
Expand Down Expand Up @@ -68,7 +69,9 @@ 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("Please specify with namespace=<your_current_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."
)
Expand Down Expand Up @@ -114,8 +117,8 @@ def up(self):
"""
namespace = self.config.namespace
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
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(
Expand All @@ -135,8 +138,8 @@ def down(self):
"""
namespace = self.config.namespace
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
api_instance.delete_namespaced_custom_object(
group="mcad.ibm.com",
version="v1beta1",
Expand Down Expand Up @@ -247,8 +250,8 @@ def cluster_dashboard_uri(self) -> str:
Returns a string containing the cluster's dashboard URI.
"""
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
routes = api_instance.list_namespaced_custom_object(
group="route.openshift.io",
version="v1",
Expand Down Expand Up @@ -376,15 +379,29 @@ def list_all_queued(namespace: str, print_to_console: bool = True):


def get_current_namespace(): # pragma: no cover
try:
config.load_kube_config()
_, active_context = config.list_kube_config_contexts()
except Exception as e:
return _kube_api_error_handling(e)
try:
return active_context["context"]["namespace"]
except KeyError:
return "default"
if 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:
print("Unable to find current namespace")
return None
else:
print("Unable to find current namespace")
return None
else:
try:
_, 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 None


def get_cluster(cluster_name: str, namespace: str = "default"):
Expand Down Expand Up @@ -423,8 +440,8 @@ def _get_ingress_domain():

def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]:
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
aws = api_instance.list_namespaced_custom_object(
group="mcad.ibm.com",
version="v1beta1",
Expand All @@ -442,8 +459,8 @@ def _app_wrapper_status(name, namespace="default") -> Optional[AppWrapper]:

def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]:
try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
rcs = api_instance.list_namespaced_custom_object(
group="ray.io",
version="v1alpha1",
Expand All @@ -462,8 +479,8 @@ 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()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
rcs = api_instance.list_namespaced_custom_object(
group="ray.io",
version="v1alpha1",
Expand All @@ -484,8 +501,8 @@ def _get_app_wrappers(
list_of_app_wrappers = []

try:
config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
aws = api_instance.list_namespaced_custom_object(
group="mcad.ibm.com",
version="v1beta1",
Expand All @@ -511,8 +528,8 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]:
else:
status = RayClusterStatus.UNKNOWN

config.load_kube_config()
api_instance = client.CustomObjectsApi()
config_check()
api_instance = client.CustomObjectsApi(api_config_handler())
routes = api_instance.list_namespaced_custom_object(
group="route.openshift.io",
version="v1",
Expand Down
Loading