From 40866c6b41348ef5359a2489641f33869600165f Mon Sep 17 00:00:00 2001 From: Romain Demeure Date: Tue, 12 Aug 2025 16:15:02 +0200 Subject: [PATCH 1/2] :sparkles: feat: add autocompletion on argument cluster-name and project-name --- oks_cli/cache.py | 10 ++++----- oks_cli/cluster.py | 46 +++++++++++++++++++------------------- oks_cli/main.py | 6 ++--- oks_cli/project.py | 22 +++++++++---------- oks_cli/utils.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 42 deletions(-) diff --git a/oks_cli/cache.py b/oks_cli/cache.py index a5b539f..f8e7702 100644 --- a/oks_cli/cache.py +++ b/oks_cli/cache.py @@ -1,11 +1,11 @@ import click -from .utils import clear_cache, find_project_id_by_name, find_cluster_id_by_name, get_all_cache, get_expiration_date, ctx_update, login_profile, profile_completer +from .utils import clear_cache, find_project_id_by_name, find_cluster_id_by_name, get_all_cache, get_expiration_date, ctx_update, login_profile, profile_completer, cluster_completer, project_completer import prettytable # DEFINE THE CACHE COMMAND GROUP @click.group(help="Cache related commands.") -@click.option('--project-name', '-p', required = False, help="Project Name") -@click.option('--cluster-name', '-c', required = False, help="Cluster Name") +@click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer) @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context def cache(ctx, project_name, cluster_name, profile): @@ -20,8 +20,8 @@ def delete_cache(force): clear_cache() @cache.command('kubeconfigs', help="List cached kubeconfigs") -@click.option('--project-name', '-p', required=False, help="Project Name") -@click.option('--cluster-name', '-c', required=False, help="Cluster Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--plain', is_flag=True, help="Plain table format") @click.option('--msword', is_flag=True, help="Microsoft Word table format") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) diff --git a/oks_cli/cluster.py b/oks_cli/cluster.py index b078a31..06cce76 100644 --- a/oks_cli/cluster.py +++ b/oks_cli/cluster.py @@ -12,7 +12,7 @@ import prettytable import logging -from .utils import do_request, print_output, find_project_id_by_name, find_cluster_id_by_name, get_cache, save_cache, detect_and_parse_input, verify_certificate, shell_completions, transform_tuple, profile_list, login_profile, cluster_create_in_background, ctx_update, set_cluster_id, get_cluster_id, get_project_id, get_template, get_cluster_name, format_changed_row, is_interesting_status, profile_completer +from .utils import cluster_completer, do_request, print_output, find_project_id_by_name, find_cluster_id_by_name, get_cache, save_cache, detect_and_parse_input, verify_certificate, shell_completions, transform_tuple, profile_list, login_profile, cluster_create_in_background, ctx_update, set_cluster_id, get_cluster_id, get_project_id, get_template, get_cluster_name, format_changed_row, is_interesting_status, profile_completer, project_completer from .profile import add_profile from .project import project_create, project_login @@ -20,9 +20,9 @@ # DEFINE THE CLUSTER GROUP @click.group(help="Cluster related commands.") @click.option('--project', 'project_name', required = False, help="Project Name") -@click.option('--project-name', '-p', required = False, help="Project Name") +@click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required = False, help="Cluster Name") -@click.option('--cluster-name', '-c', required = False, help="Cluster Name") +@click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer) @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context def cluster(ctx, project_name, cluster_name, profile): @@ -31,7 +31,7 @@ def cluster(ctx, project_name, cluster_name, profile): # LOGIN ON CLUSTER @cluster.command('login', help="Set a default cluster") -@click.option('--cluster-name', '-c', required=False, help="Name of cluster") +@click.option('--cluster-name', '-c', required=False, help="Name of cluster", shell_complete=cluster_completer) @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context def cluster_login(ctx, cluster_name, profile): @@ -70,9 +70,9 @@ def cluster_logout(ctx, profile): # LIST CLUSTERS @cluster.command('list', help="List all clusters") -@click.option('--project-name', '-p', required=False, help="Project Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required = False, help="Cluster Name") -@click.option('--cluster-name', '-c', required = False, help="Cluster Name") +@click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--deleted', is_flag=True, help="List deleted clusters") @click.option('--plain', is_flag=True, help="Plain table format") @click.option('--msword', is_flag=True, help="Microsoft Word table format") @@ -215,9 +215,9 @@ def format_row(cluster): # GET CLUSTER BY NAME @cluster.command('get', help="Get a cluster by name") -@click.option('--project-name', '-p', required = False, help="Project Name") -@click.option('--name', 'cluster_name', required=False, help="Cluster Name") -@click.option('--cluster-name', '-c', required=False, help="Cluster Name") +@click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer) +@click.option('--name', 'cluster_name', required=False, help="Cluster Name", shell_complete=cluster_completer) +@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context @@ -311,8 +311,8 @@ def _create_cluster(project_name, cluster_config, output): # CLUSTER CREATE BY NAME @cluster.command('create', help="Create a new cluster") -@click.option('--project-name', '-p', required=False, help="Project Name") -@click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--description', help="Description of the cluster") @click.option('--admin', help="Admin Whitelist") @click.option('--version', shell_complete=shell_completions, help="Kubernetes version") @@ -411,9 +411,9 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin, # UPDATE CLUSTER @cluster.command('update', help="Update a cluster by name") -@click.option('--project-name', '-p', required=False, help="Project name") +@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required=False, help="Cluster name") -@click.option('--cluster-name', '-c', required=False, help="Cluster name") +@click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer) @click.option('--description', help="Description of the cluster") @click.option('--admin', help="Admin Whitelist") @click.option('--version', shell_complete=shell_completions, help="Kubernetes version") @@ -499,9 +499,9 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin, # UPGRADE CLUSTER @cluster.command('upgrade', help="Upgrade a cluster by name") -@click.option('--project-name', '-p', required=False, help="Project name") +@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required=False, help="Cluster name") -@click.option('--cluster-name', '-c', required=False, help="Cluster name") +@click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--force', is_flag=True, help="Force upgrade") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @@ -521,9 +521,9 @@ def cluster_update_command(ctx, project_name, cluster_name, output, force, profi # DELETE CLUSTER BY NAME @cluster.command('delete', help="Delete a cluster by name") -@click.option('--project-name', '-p', required=False, help="Project name") +@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required=False, help="Cluster name") -@click.option('--cluster-name', '-c', required=False, help="Cluster name") +@click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--dry-run', is_flag=True, help="Run without any action") @click.option('--force', is_flag=True, help="Force deletion without confirmation") @@ -553,9 +553,9 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for # GET KUBECONFIG @cluster.command('kubeconfig', help="Fetch the kubeconfig for a cluster") -@click.option('--project-name', '-p', required=False, help="Project Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('--name', 'cluster_name', required=False, help="Cluster name") -@click.option('--cluster-name', '-c', required=False, help="Cluster Name") +@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--print-path', is_flag=True, help="Print path to saved kubeconfig") @click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig") @click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)") @@ -663,8 +663,8 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None): @cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True}) -@click.option('--project-name', '-p', required=False, help="Project Name") -@click.option('--cluster-name', '-c', required=False, help="Cluster Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--user', type=click.STRING, help="User") @click.option('--group', type=click.STRING, help="Group") @click.argument("args", nargs=-1, type=click.UNPROCESSED) @@ -682,8 +682,8 @@ def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, @click.group(help="nodepool related commands.") -@click.option('--project-name', '-p', required=False, help="Project Name") -@click.option('--cluster-name', '-c', required=False, help="Cluster Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer) @click.option('--user', type=click.STRING, help="User") @click.option('--group', type=click.STRING, help="Group") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) diff --git a/oks_cli/main.py b/oks_cli/main.py index ecf7d5b..d3ba8dd 100755 --- a/oks_cli/main.py +++ b/oks_cli/main.py @@ -9,13 +9,13 @@ from .cache import cache from .quotas import quotas -from .utils import ctx_update, login_profile, install_completions, profile_completer +from .utils import ctx_update, login_profile, install_completions, profile_completer, cluster_completer, project_completer # Main CLI entry point @click.group(invoke_without_command=True) @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer) -@click.option('--project-name', '-p', required = False, help="Project Name") -@click.option('--cluster-name', '-c', required = False, help="Cluster Name") +@click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer) +@click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer) @click.option('-v', '--verbose', count=True) @click.pass_context def cli(ctx, project_name, cluster_name, profile, verbose): diff --git a/oks_cli/project.py b/oks_cli/project.py index c50de62..204f99d 100644 --- a/oks_cli/project.py +++ b/oks_cli/project.py @@ -6,12 +6,12 @@ import prettytable import os -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 +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 # DEIFNE THE PROJECT COMMAND GROUP @click.group(help="Project related commands.") @click.option('--project', 'project_name', required = False, help="Project Name") -@click.option('--project-name', '-p', 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 project(ctx, project_name, profile): @@ -20,7 +20,7 @@ def project(ctx, project_name, profile): # LOGIN ON PROJECT @project.command('login', help="Set a default project by name") -@click.option('--project-name', '-p', required=False, help="Name of project", type=click.STRING) +@click.option('--project-name', '-p', required=False, help="Name of project", type=click.STRING, shell_complete=project_completer) @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context def project_login(ctx, project_name, profile): @@ -58,7 +58,7 @@ def project_logout(ctx, profile): # LIST PROJECTS @project.command('list', help="List all projects") -@click.option('--project-name', '-p', help="Name of project", type=click.STRING) +@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer) @click.option('--deleted', is_flag=True, help="List deleted projects") @click.option('--plain', is_flag=True, help="Plain table format") @click.option('--msword', is_flag=True, help="Microsoft Word table format") @@ -193,7 +193,7 @@ def format_row(project): # CREATE PROJECT BY NAME @project.command('create', help="Create a new project") -@click.option('--project-name', '-p', help="Name of the project") +@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer) @click.option('--description', help="Description of the project") @click.option('--cidr', help='CIDR for the project') @click.option('--quirk', multiple=True, help="Quirk") @@ -254,7 +254,7 @@ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_ap # GET PROJECT BY NAME @project.command('get', help="Get default project or the project by name") -@click.option('--project-name', '-p', help="Name of the project") +@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context @@ -270,7 +270,7 @@ def project_get(ctx, project_name, output, profile): # DELETE PROJECT BY NAME @project.command('delete', help="Delete a project by name") -@click.option('--project-name', '-p', required=False, help="Project Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--dry-run', is_flag=True, help="Run without any action") @click.option('--force', is_flag=True, help="Force deletion without confirmation") @@ -302,7 +302,7 @@ def project_delete_command(ctx, project_name, output, dry_run, force, profile): # UPDATE PROJECT BY NAME @project.command('update', help="Update a project by name") -@click.option('--project-name', '-p', required=False, help="Project Name") +@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer) @click.option('--description', help="Description of the project") @click.option('--quirk', multiple=True, help="Quirk") @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'") @@ -350,7 +350,7 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_ # GET PROJECT QUOTAS BY PROJECT NAME @project.command('quotas', help="Get project quotas") -@click.option('--project-name', '-p', help="Name of the project") +@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context @@ -378,7 +378,7 @@ def project_get_quotas(ctx, project_name, output, profile): # GET PROJECT SNAPSHOTS BY PROJECT NAME @project.command('snapshots', help="Get project snapshots") -@click.option('--project-name', '-p', help="Name of the project") +@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer) @click.pass_context @@ -394,7 +394,7 @@ def project_get(ctx, project_name, output, profile): # GET PUBLIC IPS BY PROJECT NAME @project.command('publicips', help="Get project public ips") -@click.option('--project-name', '-p', help="Name of the project") +@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer) @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json") @click.option('--profile', help="Configuration profile to use") @click.pass_context diff --git a/oks_cli/utils.py b/oks_cli/utils.py index fd068f3..11004f8 100644 --- a/oks_cli/utils.py +++ b/oks_cli/utils.py @@ -370,6 +370,61 @@ def profile_completer(ctx, param, incomplete): profiles = get_profiles() return [CompletionItem(p) for p in profiles if p.startswith(incomplete)] +def cluster_completer(ctx, param, incomplete): + profile = ( + ctx.params.get("profile") + or getattr(ctx.parent, "params", {}).get("profile") + or getattr(getattr(ctx.parent, "parent", None), "params", {}).get("profile") + or "default" + ) + + try: + if profile: + login_profile(profile) + except Exception: + return [] + + try: + project_id = get_project_id() + except NameError: + project_id = None + + params = {} + if project_id: + params["project_id"] = project_id + + try: + data = do_request("GET", "clusters", params=params) + except Exception: + return [] + + cluster_names = [c["name"] for c in data] + matches = [n for n in cluster_names if n.startswith(incomplete)] if incomplete else cluster_names + return [CompletionItem(n) for n in matches] + +def project_completer(ctx, param, incomplete): + profile = ( + ctx.params.get("profile") + or getattr(ctx.parent, "params", {}).get("profile") + or getattr(getattr(ctx.parent, "parent", None), "params", {}).get("profile") + or "default" + ) + + try: + if profile: + login_profile(profile) + except Exception: + return [] + + try: + data = do_request("GET", "projects") + except Exception: + return [] + + project_names = [p["name"] for p in data] + matches = [n for n in project_names if n.startswith(incomplete)] if incomplete else project_names + return [CompletionItem(n) for n in matches] + def set_profile(name, obj: dict): """Add or update a profile in the profiles file.""" _, PROFILE_FILE = get_config_path() From 8bc1cef37993d282480c0031ab596a5d5e6cd49b Mon Sep 17 00:00:00 2001 From: Romain Demeure Date: Wed, 13 Aug 2025 13:54:59 +0200 Subject: [PATCH 2/2] :sparkles: feat: add autocompletion on argument cluster-name and project-name --- oks_cli/utils.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/oks_cli/utils.py b/oks_cli/utils.py index 11004f8..8981cc6 100644 --- a/oks_cli/utils.py +++ b/oks_cli/utils.py @@ -384,10 +384,20 @@ def cluster_completer(ctx, param, incomplete): except Exception: return [] + project_name = ctx.params.get("project_name") or getattr(ctx.parent, "params", {}).get("project_name") + + project_id = None try: - project_id = get_project_id() - except NameError: - project_id = None + projects = do_request("GET", "projects") + if project_name: + for p in projects: + if p["name"] == project_name: + project_id = p["id"] + break + else: + project_id = get_project_id() + except Exception: + return [] params = {} if project_id: