Skip to content

Commit fa88f67

Browse files
carsonmhMaxusmusti
authored andcommitted
CLI Authentication (#252)
* Add: create_api_client_config helper function for the SDK * Add: login function for CLI * Change: change options and help for login function * Create: logout function * Test: add unit tests for login and logout functions * add: additional error handling and change layout slightly * test: add unit test for load_auth * change: make tls skip false by default * add: make authentication go into .codeflare * test: add unit tests for checking validity of auth file and split login/logout tests
1 parent 2f835e0 commit fa88f67

File tree

6 files changed

+213
-14
lines changed

6 files changed

+213
-14
lines changed

Diff for: src/codeflare_sdk/cli/cli_utils.py

+47
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import ast
22
import click
3+
from kubernetes import client, config
4+
import pickle
5+
import os
6+
7+
from codeflare_sdk.cluster.auth import _create_api_client_config
8+
from codeflare_sdk.utils.kube_api_helpers import _kube_api_error_handling
9+
import codeflare_sdk.cluster.auth as sdk_auth
310

411

512
class PythonLiteralOption(click.Option):
@@ -10,3 +17,43 @@ def type_cast_value(self, ctx, value):
1017
return ast.literal_eval(value)
1118
except:
1219
raise click.BadParameter(value)
20+
21+
22+
class AuthenticationConfig:
23+
"""
24+
Authentication configuration that will be stored in a file once
25+
the user logs in using `codeflare login`
26+
"""
27+
28+
def __init__(
29+
self,
30+
token: str,
31+
server: str,
32+
skip_tls: bool,
33+
ca_cert_path: str,
34+
):
35+
self.api_client_config = _create_api_client_config(
36+
token, server, skip_tls, ca_cert_path
37+
)
38+
self.server = server
39+
self.token = token
40+
41+
def create_client(self):
42+
return client.ApiClient(self.api_client_config)
43+
44+
45+
def load_auth():
46+
"""
47+
Loads AuthenticationConfiguration and stores it in global variables
48+
which can be used by the SDK for authentication
49+
"""
50+
try:
51+
auth_file_path = os.path.expanduser("~/.codeflare/auth")
52+
with open(auth_file_path, "rb") as file:
53+
auth = pickle.load(file)
54+
sdk_auth.api_client = auth.create_client()
55+
return auth
56+
except (IOError, EOFError):
57+
click.echo("No authentication found, trying default kubeconfig")
58+
except client.ApiException:
59+
click.echo("Invalid authentication, trying default kubeconfig")

Diff for: src/codeflare_sdk/cli/codeflare_cli.py

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands"))
66

77

8+
class CodeflareContext:
9+
def __init__(self, codeflare_path):
10+
self.codeflare_path = codeflare_path
11+
12+
813
class CodeflareCLI(click.MultiCommand):
914
def list_commands(self, ctx):
1015
rv = []
@@ -26,9 +31,18 @@ def get_command(self, ctx, name):
2631
return
2732

2833

34+
def initialize_cli(ctx):
35+
# Make .codeflare folder
36+
codeflare_folder = os.path.expanduser("~/.codeflare")
37+
if not os.path.exists(codeflare_folder):
38+
os.makedirs(codeflare_folder)
39+
ctx.obj = CodeflareContext(codeflare_folder)
40+
41+
2942
@click.command(cls=CodeflareCLI)
3043
@click.pass_context
3144
def cli(ctx):
45+
initialize_cli(ctx) # Ran on every command
3246
pass
3347

3448

Diff for: src/codeflare_sdk/cli/commands/login.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import click
2+
import pickle
3+
from kubernetes import client
4+
import os
5+
6+
from codeflare_sdk.cluster.auth import TokenAuthentication
7+
from codeflare_sdk.cli.cli_utils import AuthenticationConfig
8+
import codeflare_sdk.cluster.auth as sdk_auth
9+
10+
11+
@click.command()
12+
@click.pass_context
13+
@click.option("--server", "-s", type=str, required=True, help="Cluster API address")
14+
@click.option("--token", "-t", type=str, required=True, help="Authentication token")
15+
@click.option(
16+
"--insecure-skip-tls-verify",
17+
type=bool,
18+
help="If true, server's certificate won't be checked for validity",
19+
default=False,
20+
)
21+
@click.option(
22+
"--certificate-authority",
23+
type=str,
24+
help="Path to cert file for certificate authority",
25+
)
26+
def cli(ctx, server, token, insecure_skip_tls_verify, certificate_authority):
27+
"""
28+
Login to your Kubernetes cluster and save login for subsequent use
29+
"""
30+
auth = TokenAuthentication(
31+
token, server, insecure_skip_tls_verify, certificate_authority
32+
)
33+
auth.login()
34+
if not sdk_auth.api_client: # TokenAuthentication failed
35+
return
36+
37+
auth_config = AuthenticationConfig(
38+
token,
39+
server,
40+
insecure_skip_tls_verify,
41+
certificate_authority,
42+
)
43+
auth_file_path = ctx.obj.codeflare_path + "/auth"
44+
with open(auth_file_path, "wb") as file:
45+
pickle.dump(auth_config, file)
46+
click.echo(f"Logged into '{server}'")

Diff for: src/codeflare_sdk/cli/commands/logout.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import click
2+
import os
3+
import pickle
4+
5+
6+
@click.command()
7+
@click.pass_context
8+
def cli(ctx):
9+
"""
10+
Log out of current Kubernetes cluster
11+
"""
12+
try:
13+
auth_file_path = ctx.obj.codeflare_path + "/auth"
14+
with open(auth_file_path, "rb") as file:
15+
auth = pickle.load(file)
16+
os.remove(auth_file_path)
17+
click.echo(f"Successfully logged out of '{auth.server}'")
18+
except:
19+
click.echo("Not logged in")

Diff for: src/codeflare_sdk/cluster/auth.py

+24-11
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,11 @@ def login(self) -> str:
9797
global config_path
9898
global api_client
9999
try:
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
108-
else:
109-
configuration.verify_ssl = False
110-
api_client = client.ApiClient(configuration)
100+
api_client = client.ApiClient(
101+
_create_api_client_config(
102+
self.token, self.server, self.skip_tls, self.ca_cert_path
103+
)
104+
)
111105
client.AuthenticationApi(api_client).get_api_group()
112106
config_path = None
113107
return "Logged into %s" % self.server
@@ -154,6 +148,25 @@ def load_kube_config(self):
154148
return response
155149

156150

151+
def _create_api_client_config(
152+
token: str, server: str, skip_tls: bool = False, ca_cert_path: str = None
153+
):
154+
"""
155+
Creates Kubernetes client configuration given necessary parameters
156+
"""
157+
configuration = client.Configuration()
158+
configuration.api_key_prefix["authorization"] = "Bearer"
159+
configuration.host = server
160+
configuration.api_key["authorization"] = token
161+
if skip_tls == False and ca_cert_path == None:
162+
configuration.verify_ssl = True
163+
elif skip_tls == False:
164+
configuration.ssl_ca_cert = ca_cert_path
165+
else:
166+
configuration.verify_ssl = False
167+
return configuration
168+
169+
157170
def config_check() -> str:
158171
"""
159172
Function for loading the config file at the default config location ~/.kube/config if the user has not

Diff for: tests/unit_test.py

+63-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import re
2020
from click.testing import CliRunner
21+
import pickle
2122

2223
parent = Path(__file__).resolve().parents[1]
2324
sys.path.append(str(parent) + "/src")
@@ -65,6 +66,8 @@
6566
export_env,
6667
)
6768
from codeflare_sdk.cli.codeflare_cli import cli
69+
from codeflare_sdk.cli.cli_utils import load_auth
70+
import codeflare_sdk.cluster.auth as sdk_auth
6871

6972
import openshift
7073
from openshift.selector import Selector
@@ -108,6 +111,65 @@ def test_cluster_definition_cli():
108111
)
109112

110113

114+
def test_login_cli(mocker):
115+
runner = CliRunner()
116+
mocker.patch.object(client, "ApiClient")
117+
k8s_login_command = """
118+
login
119+
--server=testserver:6443
120+
--token=testtoken
121+
"""
122+
login_result = runner.invoke(cli, k8s_login_command)
123+
assert login_result.output == "Logged into 'testserver:6443'\n"
124+
try:
125+
auth_file_path = os.path.expanduser("~/.codeflare/auth")
126+
with open(auth_file_path, "rb") as file:
127+
auth = pickle.load(file)
128+
except:
129+
assert 0 == 1
130+
assert auth.server == "testserver:6443"
131+
assert auth.token == "testtoken"
132+
assert auth.api_client_config.api_key["authorization"] == "testtoken"
133+
assert auth.api_client_config.verify_ssl
134+
assert auth.api_client_config.host == "testserver:6443"
135+
136+
137+
def test_login_tls_cli(mocker):
138+
runner = CliRunner()
139+
mocker.patch.object(client, "ApiClient")
140+
k8s_tls_login_command = """
141+
login
142+
--server=testserver:6443
143+
--token=testtoken
144+
--insecure-skip-tls-verify=False
145+
"""
146+
k8s_skip_tls_login_command = """
147+
login
148+
--server=testserver:6443
149+
--token=testtoken
150+
--insecure-skip-tls-verify=True
151+
"""
152+
tls_result = runner.invoke(cli, k8s_tls_login_command)
153+
skip_tls_result = runner.invoke(cli, k8s_skip_tls_login_command)
154+
assert (
155+
tls_result.output == skip_tls_result.output == "Logged into 'testserver:6443'\n"
156+
)
157+
158+
159+
def test_logout_cli(mocker):
160+
runner = CliRunner()
161+
mocker.patch.object(client, "ApiClient")
162+
k8s_logout_command = "logout"
163+
logout_result = runner.invoke(cli, k8s_logout_command)
164+
assert logout_result.output == "Successfully logged out of 'testserver:6443'\n"
165+
assert not os.path.exists(os.path.expanduser("~/.codeflare/auth"))
166+
167+
168+
def test_load_auth():
169+
load_auth()
170+
assert sdk_auth.api_client is not None
171+
172+
111173
# For mocking openshift client results
112174
fake_res = openshift.Result("fake")
113175

@@ -2254,12 +2316,10 @@ def test_cleanup():
22542316
os.remove("unit-test-default-cluster.yaml")
22552317
os.remove("test.yaml")
22562318
os.remove("raytest2.yaml")
2257-
<<<<<<< HEAD
22582319
os.remove("quicktest.yaml")
22592320
os.remove("tls-cluster-namespace/ca.crt")
22602321
os.remove("tls-cluster-namespace/tls.crt")
22612322
os.remove("tls-cluster-namespace/tls.key")
22622323
os.rmdir("tls-cluster-namespace")
2263-
=======
22642324
os.remove("cli-test-cluster.yaml")
2265-
>>>>>>> 3195eb1 (CLI Layout and Create RayCluster function (#227))
2325+
os.removedirs(os.path.expanduser("~/.codeflare"))

0 commit comments

Comments
 (0)