From b361e0af251130efd97919ae324fa80d3a6a5080 Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Mon, 9 Mar 2026 11:16:18 +0100 Subject: [PATCH 01/10] =?UTF-8?q?=20=E2=9C=A8=20feat:=20init=20eim=20users?= =?UTF-8?q?=20management=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/main.py | 2 ++ oks_cli/user.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ oks_cli/utils.py | 6 ++++ 3 files changed, 91 insertions(+) create mode 100644 oks_cli/user.py diff --git a/oks_cli/main.py b/oks_cli/main.py index ee88881..5a1c780 100755 --- a/oks_cli/main.py +++ b/oks_cli/main.py @@ -9,6 +9,7 @@ from .cache import cache from .quotas import quotas from .netpeering import netpeering +from .user import user from .utils import ctx_update, install_completions, profile_completer, cluster_completer, project_completer @@ -62,6 +63,7 @@ def cli(ctx, project_name, cluster_name, profile, verbose): cli.add_command(cache) cli.add_command(quotas) cli.add_command(netpeering) +cli.add_command(user) def recursive_help(cmd, parent=None): """Recursively prints help for all commands and subcommands.""" diff --git a/oks_cli/user.py b/oks_cli/user.py new file mode 100644 index 0000000..7530588 --- /dev/null +++ b/oks_cli/user.py @@ -0,0 +1,83 @@ +import click +import time +import datetime +import dateutil.parser +import human_readable +import prettytable +import os +from prettytable import TableStyle +from nacl.public import PrivateKey, SealedBox +from nacl.encoding import Base64Encoder + +from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, \ + detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, \ + format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer, \ + format_row, apply_set_fields + +# DEIFNE THE USER COMMAND GROUP +@click.group(help="EIM users related commands.") +@click.option('--project', 'project_name', required = False, help="Project Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) +@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) +@click.pass_context +def user(ctx, project_name, profile): + """Group of commands related to project management.""" + ctx_update(ctx, project_name, None, profile) + +# LIST USERS +@user.command('list', help="List EIM users") +@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) +@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") +@click.option('--profile', help="Configuration profile to use") +@click.pass_context +def user_list(ctx, project_name, output, profile): + """List users""" + project_name, _, profile = ctx_update(ctx, project_name, None, profile) + login_profile(profile) + + project_id = get_project_id() + + + data = do_request("GET", f'projects/{project_id}/eim_users') + + if output: + print_output(data, output) + return + + +@user.command('create', help="Create a new cluster") +@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) +@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") +@click.option('--profile', help="Configuration profile to use") +@click.option('--user', required=True, help="OKS User type") +@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)") +@click.option('--nacl', is_flag=True, help="Use public key encryption on wire") +def user_create(ctx, project_name, output, profile, user, ttl, nacl): + """List projects with filtering, formatting, and live watch capabilities.""" + project_name, _, profile = ctx_update(ctx, project_name, None, profile) + login_profile(profile) + + project_id = get_project_id() + + params = { + "user": user + } + if ttl: + params["ttl"] = ttl + + if nacl: + ephemeral = PrivateKey.generate() + unsealbox = SealedBox(ephemeral) + + headers = { + 'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii') + } + raw_data = do_request("POST", f'project/{project_id}/kubeconfig', params = params, headers = headers)['data']['kubeconfig'] + + + if output: + print_output(data, output) + return + + # print_output(cluster_tdatamplate, output) + diff --git a/oks_cli/utils.py b/oks_cli/utils.py index 9590096..1b089d1 100644 --- a/oks_cli/utils.py +++ b/oks_cli/utils.py @@ -79,6 +79,12 @@ def find_response_object(data): return response["IP"] elif key == "Nets": return response["Nets"] + elif key == "EimUsers": + return response["EimUsers"] + elif key == "EimUser": + return response["EimUser"] + elif key == "Data": + return response["Data"] raise click.ClickException("The API response format is incorrect.") From de044c034b7c53c2568749f2309c4288ec2bff43 Mon Sep 17 00:00:00 2001 From: Romain Demeure Date: Tue, 10 Mar 2026 09:36:22 +0100 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20feat:=20improve=20user=5Flist?= =?UTF-8?q?=20and=20user=5Fcreate=20and=20add=20=20user=5Fdelete=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 75 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 7530588..a022005 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -4,6 +4,7 @@ import dateutil.parser import human_readable import prettytable +import json import os from prettytable import TableStyle from nacl.public import PrivateKey, SealedBox @@ -43,17 +44,34 @@ def user_list(ctx, project_name, output, profile): if output: print_output(data, output) return + + field_names = ["USER", "EMAIL", "USER ID", "CREATED"] + + table = prettytable.PrettyTable() + table.field_names = field_names + + for user in data: + row = [ + user.get("UserName"), + user.get("UserEmail"), + user.get("UserId"), + user.get("CreationDate"), + ] + table.add_row(row) + + click.echo(table) @user.command('create', help="Create a new cluster") @click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) -@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") +@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use") @click.option('--user', required=True, help="OKS User type") @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)") @click.option('--nacl', is_flag=True, help="Use public key encryption on wire") +@click.pass_context def user_create(ctx, project_name, output, profile, user, ttl, nacl): - """List projects with filtering, formatting, and live watch capabilities.""" + """Create a new EIM user.""" project_name, _, profile = ctx_update(ctx, project_name, None, profile) login_profile(profile) @@ -72,12 +90,53 @@ def user_create(ctx, project_name, output, profile, user, ttl, nacl): headers = { 'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii') } - raw_data = do_request("POST", f'project/{project_id}/kubeconfig', params = params, headers = headers)['data']['kubeconfig'] - - if output: - print_output(data, output) - return + raw_data = do_request( + "POST", + f'projects/{project_id}/eim_users', + params=params, + headers=headers + ) + + decrypted = unsealbox.decrypt( + raw_data.encode('ascii'), + encoder=Base64Encoder + ).decode('ascii') + + data = json.loads(decrypted) + + else: + data = do_request( + "POST", + f'projects/{project_id}/eim_users', + params=params + ) + + print_output(data, output) + +# DELETE USER +@user.command('delete', help="Delete an EIM user") +@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer) +@click.option('--user-name', '--name', '-u', required=True, help="User name") +@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format") +@click.option('--dry-run', is_flag=True, help="Run without any action") +@click.option('--force', is_flag=True, help="Force deletion without confirmation") +@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) +@click.pass_context +def user_delete(ctx, project_name, user_name, output, dry_run, force, profile): + """CLI command to delete an EIM user.""" - # print_output(cluster_tdatamplate, output) + project_name, _, profile = ctx_update(ctx, project_name, None, profile) + login_profile(profile) + + project_id = find_project_id_by_name(project_name) + + if dry_run: + message = {"message": f"Dry run: The user '{user_name}' would be deleted."} + print_output(message, output) + return + + if force or click.confirm(f"Are you sure you want to delete the user '{user_name}'?", abort=True): + data = do_request("DELETE", f"projects/{project_id}/eim_users/{user_name}") + print_output(data, output) From 726e670524dc4cf07628c31b8f0e91acac94295b Mon Sep 17 00:00:00 2001 From: Romain Demeure Date: Tue, 10 Mar 2026 11:06:22 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat:=20display=20access=20ke?= =?UTF-8?q?y,=20state=20and=20expiration=20date=20on=20command=20user=20li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index a022005..df12c5c 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -17,7 +17,6 @@ # DEIFNE THE USER COMMAND GROUP @click.group(help="EIM users related commands.") -@click.option('--project', 'project_name', required = False, help="Project Name") @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context @@ -27,13 +26,12 @@ def user(ctx, project_name, profile): # LIST USERS @user.command('list', help="List EIM users") -@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) -@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") +@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use") @click.pass_context -def user_list(ctx, project_name, output, profile): +def user_list(ctx, output, profile): """List users""" - project_name, _, profile = ctx_update(ctx, project_name, None, profile) + _, _, profile = ctx_update(ctx, None, None, profile) login_profile(profile) project_id = get_project_id() @@ -44,17 +42,18 @@ def user_list(ctx, project_name, output, profile): if output: print_output(data, output) return - - field_names = ["USER", "EMAIL", "USER ID", "CREATED"] - + field_names = ["USER", "ACCESS KEY ID", "STATE", "EXPIRATION DATE", "CREATED"] table = prettytable.PrettyTable() table.field_names = field_names for user in data: + access_keys = user.get("AccessKeys", []) + access_key = access_keys[0] if access_keys else {} row = [ user.get("UserName"), - user.get("UserEmail"), - user.get("UserId"), + access_key.get("AccessKeyId", "N/A"), + access_key.get("State", "N/A"), + access_key.get("ExpirationDate", "N/A"), user.get("CreationDate"), ] table.add_row(row) From ae28cd4d20f67114d4cfe493334625e5944bd0b6 Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Tue, 10 Mar 2026 13:30:27 +0100 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20command=20op?= =?UTF-8?q?tions=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index df12c5c..349ea72 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -27,11 +27,12 @@ def user(ctx, project_name, profile): # LIST USERS @user.command('list', help="List EIM users") @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('--profile', help="Configuration profile to use") @click.pass_context -def user_list(ctx, output, profile): +def user_list(ctx, output, project_name, profile): """List users""" - _, _, profile = ctx_update(ctx, None, None, profile) + project_name, _, profile = ctx_update(ctx, project_name, None, profile) login_profile(profile) project_id = get_project_id() @@ -65,7 +66,7 @@ def user_list(ctx, output, profile): @click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use") -@click.option('--user', required=True, help="OKS User type") +@click.option('--user', '-u', required=True, help="OKS User type") @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)") @click.option('--nacl', is_flag=True, help="Use public key encryption on wire") @click.pass_context @@ -116,7 +117,7 @@ def user_create(ctx, project_name, output, profile, user, ttl, nacl): # DELETE USER @user.command('delete', help="Delete an EIM user") @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer) -@click.option('--user-name', '--name', '-u', required=True, help="User name") +@click.option('--user', '-u', required=True, help="User name") @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format") @click.option('--dry-run', is_flag=True, help="Run without any action") @click.option('--force', is_flag=True, help="Force deletion without confirmation") From 559db64d663caac4c4f8918382317cf5ab549355 Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Tue, 10 Mar 2026 13:52:59 +0100 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20feat:=20format=20user=20list?= =?UTF-8?q?=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 349ea72..0701951 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -1,6 +1,6 @@ import click import time -import datetime +from datetime import datetime import dateutil.parser import human_readable import prettytable @@ -37,25 +37,37 @@ def user_list(ctx, output, project_name, profile): project_id = get_project_id() - data = do_request("GET", f'projects/{project_id}/eim_users') if output: print_output(data, output) return - field_names = ["USER", "ACCESS KEY ID", "STATE", "EXPIRATION DATE", "CREATED"] + + field_names = ["USER", "ACCESS KEY", "STATE", "CREATED", "EXPIRATION DATE"] table = prettytable.PrettyTable() table.field_names = field_names for user in data: access_keys = user.get("AccessKeys", []) access_key = access_keys[0] if access_keys else {} + + + state = access_key.get("State", "N/A") + if state == 'ACTIVE': + state = click.style(state, fg='green') + elif state == "INACTIVE": + state = click.style(state, fg='red') + + created_at = dateutil.parser.parse(user.get("CreationDate")) + exp_at = dateutil.parser.parse(access_key.get("ExpirationDate")) + now = datetime.now(tz=created_at.tzinfo) + row = [ user.get("UserName"), access_key.get("AccessKeyId", "N/A"), - access_key.get("State", "N/A"), - access_key.get("ExpirationDate", "N/A"), - user.get("CreationDate"), + state, + human_readable.date_time(now - created_at), + human_readable.date_time(now - exp_at) ] table.add_row(row) @@ -123,7 +135,7 @@ def user_create(ctx, project_name, output, profile, user, ttl, nacl): @click.option('--force', is_flag=True, help="Force deletion without confirmation") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context -def user_delete(ctx, project_name, user_name, output, dry_run, force, profile): +def user_delete(ctx, project_name, user, output, dry_run, force, profile): """CLI command to delete an EIM user.""" project_name, _, profile = ctx_update(ctx, project_name, None, profile) @@ -132,11 +144,11 @@ def user_delete(ctx, project_name, user_name, output, dry_run, force, profile): project_id = find_project_id_by_name(project_name) if dry_run: - message = {"message": f"Dry run: The user '{user_name}' would be deleted."} + message = {"message": f"Dry run: The user '{user}' would be deleted."} print_output(message, output) return - if force or click.confirm(f"Are you sure you want to delete the user '{user_name}'?", abort=True): - data = do_request("DELETE", f"projects/{project_id}/eim_users/{user_name}") + if force or click.confirm(f"Are you sure you want to delete the user '{user}'?", abort=True): + data = do_request("DELETE", f"projects/{project_id}/eim_users/{user}") print_output(data, output) From 2f122ac431da24fbf233cd18d7d3fc0e84edc33f Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Tue, 10 Mar 2026 14:19:40 +0100 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=85=20test:=20pytests=20for=20user?= =?UTF-8?q?=20management=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 4 ++-- tests/test_user.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/test_user.py diff --git a/oks_cli/user.py b/oks_cli/user.py index 0701951..635cfec 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -35,7 +35,7 @@ def user_list(ctx, output, project_name, profile): project_name, _, profile = ctx_update(ctx, project_name, None, profile) login_profile(profile) - project_id = get_project_id() + project_id = find_project_id_by_name(project_name) data = do_request("GET", f'projects/{project_id}/eim_users') @@ -87,7 +87,7 @@ def user_create(ctx, project_name, output, profile, user, ttl, nacl): project_name, _, profile = ctx_update(ctx, project_name, None, profile) login_profile(profile) - project_id = get_project_id() + project_id = find_project_id_by_name(project_name) params = { "user": user diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..e0dcb71 --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,56 @@ +from click.testing import CliRunner +from oks_cli.main import cli +from unittest.mock import patch, MagicMock + +@patch("oks_cli.utils.requests.request") +def test_user_list_command(mock_request, add_default_profile): + mock_request.side_effect = [ + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}), + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "EimUsers": []}) + ] + + runner = CliRunner() + result = runner.invoke(cli, ["user", "list", "-p", "test"]) + assert result.exit_code == 0 + assert 'USER | ACCESS KEY' in result.output + +@patch("oks_cli.utils.requests.request") +def test_user_create_command(mock_request, add_default_profile): + mock_request.side_effect = [ + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}), + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "EimUser": { + "UserName": "OKSAuditor", + "CreationDate": "2026-03-04T12:31:58.000+0000", + "UserId": "BlaBla", + "UserEmail": "bla@email.local", + "LastModificationDate": "2026-03-04T12:31:58.000+0000", + "Path": "/", + "AccessKeys": [ + { + "State": "ACTIVE", + "AccessKeyId": "AK", + "CreationDate": "2026-03-04T12:31:59.841+0000", + "ExpirationDate": "2026-03-11T12:31:59.297+0000", + "SecretKey": "SK", + "LastModificationDate": "2026-03-04T12:31:59.841+0000" + } + ] + }}) + ] + + runner = CliRunner() + result = runner.invoke(cli, ["user", "create", "-p", "test", "-u", "OKSAuditor", "--ttl", "1w"]) + assert result.exit_code == 0 + assert 'bla@email.local' in result.output + +@patch("oks_cli.utils.requests.request") +def test_user_delete_command(mock_request, add_default_profile): + mock_request.side_effect = [ + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}), + MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Details": "User has been deleted." }) + ] + + runner = CliRunner() + result = runner.invoke(cli, ["user", "delete", "-p", "test", "-u", "OKSAuditor", "--force"]) + assert result.exit_code == 0 + assert 'User has been deleted.' in result.output \ No newline at end of file From fb857b435e10ef0ad0bd7e3255a8988f6563d6e7 Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Tue, 10 Mar 2026 15:24:10 +0100 Subject: [PATCH 07/10] format error response for encrypted data --- oks_cli/user.py | 21 +++++++++++++-------- oks_cli/utils.py | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 635cfec..85fb86f 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -1,19 +1,14 @@ import click -import time from datetime import datetime import dateutil.parser import human_readable import prettytable import json -import os -from prettytable import TableStyle + from nacl.public import PrivateKey, SealedBox from nacl.encoding import Base64Encoder -from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, \ - detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, \ - format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer, \ - format_row, apply_set_fields +from .utils import do_request, print_output, find_project_id_by_name, ctx_update, login_profile, profile_completer, project_completer, JSONClickException # DEIFNE THE USER COMMAND GROUP @click.group(help="EIM users related commands.") @@ -111,12 +106,22 @@ def user_create(ctx, project_name, output, profile, user, ttl, nacl): ) decrypted = unsealbox.decrypt( - raw_data.encode('ascii'), + raw_data.get("Data").encode('ascii'), encoder=Base64Encoder ).decode('ascii') data = json.loads(decrypted) + # format decrypted errors the same way as the api errors. + if "Errors" in data: + response_context = raw_data.get("ResponseContext") + errors = [] + for error in data.get("Errors", []): + error["Code"] = str(data.get("Code")) + errors.append(error) + + raise JSONClickException(json.dumps({"Errors": errors,"ResponseContext": response_context}, separators=(",", ":"))) + else: data = do_request( "POST", diff --git a/oks_cli/utils.py b/oks_cli/utils.py index 1b089d1..625968f 100644 --- a/oks_cli/utils.py +++ b/oks_cli/utils.py @@ -84,7 +84,7 @@ def find_response_object(data): elif key == "EimUser": return response["EimUser"] elif key == "Data": - return response["Data"] + return response raise click.ClickException("The API response format is incorrect.") From 7c6765bb57dce0bfd64f7db6668d50a197110c9b Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Thu, 12 Mar 2026 18:04:54 +0100 Subject: [PATCH 08/10] fix: cli throws a traceback in case of missing ak/sk --- oks_cli/user.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 85fb86f..92dc4f8 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -53,17 +53,27 @@ def user_list(ctx, output, project_name, profile): elif state == "INACTIVE": state = click.style(state, fg='red') - created_at = dateutil.parser.parse(user.get("CreationDate")) - exp_at = dateutil.parser.parse(access_key.get("ExpirationDate")) - now = datetime.now(tz=created_at.tzinfo) row = [ user.get("UserName"), access_key.get("AccessKeyId", "N/A"), - state, - human_readable.date_time(now - created_at), - human_readable.date_time(now - exp_at) + state ] + + if "CreationDate" in user: + created_at = dateutil.parser.parse(user.get("CreationDate")) + now = datetime.now(tz=created_at.tzinfo) + row.append(human_readable.date_time(now - created_at)) + else: + row.append("N/A") + + if "ExpirationDate" in access_key: + exp_at = dateutil.parser.parse(access_key.get("ExpirationDate")) + now = datetime.now(tz=exp_at.tzinfo) + row.append(human_readable.date_time(now - exp_at)) + else: + row.append("N/A") + table.add_row(row) click.echo(table) From 11c9d673599b85a5be44f99bcef49d3e588a1504 Mon Sep 17 00:00:00 2001 From: Yurii Kasper Date: Fri, 13 Mar 2026 15:33:52 +0100 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=A8=20feat:=20get=20creation=20date?= =?UTF-8?q?=20from=20accesskey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 92dc4f8..56bce85 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -60,8 +60,8 @@ def user_list(ctx, output, project_name, profile): state ] - if "CreationDate" in user: - created_at = dateutil.parser.parse(user.get("CreationDate")) + if "CreationDate" in access_key: + created_at = dateutil.parser.parse(access_key.get("CreationDate")) now = datetime.now(tz=created_at.tzinfo) row.append(human_readable.date_time(now - created_at)) else: From bd6f1e8d96b9ea9fc5c9310ed559dfeafc48c17f Mon Sep 17 00:00:00 2001 From: Romain Demeure Date: Fri, 20 Mar 2026 14:40:54 +0100 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20improve=20help=20co?= =?UTF-8?q?mmand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oks_cli/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oks_cli/user.py b/oks_cli/user.py index 56bce85..ae67ef3 100644 --- a/oks_cli/user.py +++ b/oks_cli/user.py @@ -79,12 +79,12 @@ def user_list(ctx, output, project_name, profile): click.echo(table) -@user.command('create', help="Create a new cluster") +@user.command('create', help="Create a new EIM user") @click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use") @click.option('--user', '-u', required=True, help="OKS User type") -@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)") +@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w), by default is 1w") @click.option('--nacl', is_flag=True, help="Use public key encryption on wire") @click.pass_context def user_create(ctx, project_name, output, profile, user, ttl, nacl):