From b1bd805ad8d97521ab662241206c6b18c41294e7 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 9 May 2025 15:01:34 -0400 Subject: [PATCH] Update labels option syntax on admin cli to match other places (cherry picked from commit 5d3ba48651a064619f4a24b7c3164133ed8f1611) --- .../configuration/authentication.md | 4 +- .../usage/setup-distributed-mode.md | 4 +- .../jumpstarter_cli_admin/create.py | 12 +++--- .../jumpstarter_cli_admin/create_test.py | 33 ++++++++++------ .../jumpstarter_cli_common/opt.py | 38 +++++++++++++++++-- 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/docs/source/getting-started/configuration/authentication.md b/docs/source/getting-started/configuration/authentication.md index e5ad13aaf..5db527ad5 100644 --- a/docs/source/getting-started/configuration/authentication.md +++ b/docs/source/getting-started/configuration/authentication.md @@ -190,7 +190,7 @@ jwt: service account name with "dex:" as configured in the claim mappings.: ```shell -$ jmp admin create exporter test-exporter \ +$ jmp admin create exporter test-exporter --label foo=bar \ --insecure-tls-config \ --oidc-username dex:system:serviceaccount:default:test-service-account ``` @@ -293,4 +293,4 @@ jwt: message: 'username cannot used reserved system: prefix' - expression: "user.groups.all(group, !group.startsWith('system:'))" message: 'groups cannot used reserved system: prefix' -``` \ No newline at end of file +``` diff --git a/docs/source/getting-started/usage/setup-distributed-mode.md b/docs/source/getting-started/usage/setup-distributed-mode.md index 36d420378..688ff7f0a 100644 --- a/docs/source/getting-started/usage/setup-distributed-mode.md +++ b/docs/source/getting-started/usage/setup-distributed-mode.md @@ -40,7 +40,7 @@ Run this command to create an exporter named `example-distributed` and save the configuration locally: ```shell -$ jmp admin create exporter example-distributed --save --insecure-tls-config +$ jmp admin create exporter example-distributed --label foo=bar --save --insecure-tls-config ``` After creating the exporter, find the new configuration file at @@ -114,4 +114,4 @@ $ exit Once you have your exporter shell running, you can start using Jumpstarter commands to interact with your hardware. To learn more about common workflow -patterns and implementation examples, see [Examples](./examples.md). \ No newline at end of file +patterns and implementation examples, see [Examples](./examples.md). diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index e00e6f510..2282104e7 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -74,7 +74,7 @@ def print_created_client(client: V1Alpha1Client, output: OutputType): default=None, ) @opt_namespace -@opt_labels +@opt_labels() @opt_kubeconfig @opt_context @opt_insecure_tls_config @@ -87,7 +87,7 @@ async def create_client( context: Optional[str], insecure_tls_config: bool, namespace: str, - labels: list[(str, str)], + labels: dict[str, str], save: bool, allow: Optional[str], unsafe: bool, @@ -103,7 +103,7 @@ async def create_client( if output is None: # Only print status if is not JSON/YAML click.echo(f"Creating client '{name}' in namespace '{namespace}'") - created_client = await api.create_client(name, dict(labels), oidc_username) + created_client = await api.create_client(name, labels, oidc_username) # Save the client config if save or out is not None or nointeractive is False and click.confirm("Save client configuration?"): if output is None: @@ -159,7 +159,7 @@ def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType): default=None, ) @opt_namespace -@opt_labels +@opt_labels(required=True) @opt_kubeconfig @opt_context @opt_insecure_tls_config @@ -172,7 +172,7 @@ async def create_exporter( context: Optional[str], insecure_tls_config: bool, namespace: str, - labels: list[(str, str)], + labels: dict[str, str], save: bool, out: Optional[str], oidc_username: str | None, @@ -185,7 +185,7 @@ async def create_exporter( async with ExportersV1Alpha1Api(namespace, kubeconfig, context) as api: if output is None: click.echo(f"Creating exporter '{name}' in namespace '{namespace}'") - created_exporter = await api.create_exporter(name, dict(labels), oidc_username) + created_exporter = await api.create_exporter(name, labels, oidc_username) # Save the client config if save or out is not None or nointeractive is False and click.confirm("Save exporter configuration?"): if output is None: diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py index 47f270379..b1e0edc0c 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py @@ -279,7 +279,8 @@ async def test_create_exporter( runner = CliRunner() # Don't save exporter config - result = await runner.invoke(create, ["exporter", EXPORTER_NAME], input="n\n") + result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar"], input="n\n") + print(result.output) assert result.exit_code == 0 assert "Creating exporter" in result.output assert EXPORTER_NAME in result.output @@ -290,7 +291,9 @@ async def test_create_exporter( # Insecure TLS config is returned _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept insecure = Y, save = Y - result = await runner.invoke(create, ["exporter", "--insecure-tls-config", EXPORTER_NAME], input="Y\nY\n") + result = await runner.invoke( + create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="Y\nY\n" + ) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output save_exporter_mock.assert_called_once_with(INSECURE_TLS_EXPORTER_CONFIG, None) @@ -299,7 +302,7 @@ async def test_create_exporter( _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept no interactive result = await runner.invoke( - create, ["exporter", "--insecure-tls-config", "--nointeractive", "--save", EXPORTER_NAME] + create, ["exporter", "--insecure-tls-config", "--nointeractive", "--save", EXPORTER_NAME, "--label", "foo=bar"] ) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output @@ -309,19 +312,21 @@ async def test_create_exporter( # Insecure TLS config is returned _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept insecure = N - result = await runner.invoke(create, ["exporter", "--insecure-tls-config", EXPORTER_NAME], input="n\n") + result = await runner.invoke( + create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="n\n" + ) assert result.exit_code == 1 assert "Aborted" in result.output # Save with prompts - result = await runner.invoke(create, ["exporter", EXPORTER_NAME], input="Y\n") + result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar"], input="Y\n") assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, None) save_exporter_mock.reset_mock() # Save with arguments - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--save"]) + result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--save"]) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, None) @@ -329,35 +334,41 @@ async def test_create_exporter( # Save with arguments and custom path out = f"/tmp/{EXPORTER_NAME}.yaml" - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--out", out]) + result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--out", out]) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, str(Path(out).resolve())) save_exporter_mock.reset_mock() # Save with nointeractive - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive"]) + result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive"]) assert result.exit_code == 0 assert "Creating exporter" in result.output save_exporter_mock.assert_not_called() save_exporter_mock.reset_mock() # Save with JSON output - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "json"]) + result = await runner.invoke( + create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "json"] + ) assert result.exit_code == 0 assert result.output == EXPORTER_JSON save_exporter_mock.assert_not_called() save_exporter_mock.reset_mock() # Save with YAML output - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "yaml"]) + result = await runner.invoke( + create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "yaml"] + ) assert result.exit_code == 0 assert result.output == EXPORTER_YAML save_exporter_mock.assert_not_called() save_exporter_mock.reset_mock() # Save with name output - result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "name"]) + result = await runner.invoke( + create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "name"] + ) assert result.exit_code == 0 assert result.output == f"exporter.jumpstarter.dev/{EXPORTER_NAME}\n" save_exporter_mock.assert_not_called() diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py index 50186ff4b..3126b75d4 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py @@ -1,3 +1,4 @@ +from functools import partial from typing import Literal, Optional import asyncclick as click @@ -17,12 +18,40 @@ opt_namespace = click.option("-n", "--namespace", type=str, help="Kubernetes namespace to use", default="default") -opt_labels = click.option("-l", "--label", "labels", type=(str, str), multiple=True, help="Labels") -opt_insecure_tls_config = click.option("--insecure-tls-config", "insecure_tls_config", is_flag=True, default=False, - help="Disable endpoint TLS verification. This is insecure and should only be used for testing purposes") +def _opt_labels_callback(ctx, param, value): + labels = {} -def confirm_insecure_tls(insecure_tls_config:bool, nointeractive: bool): + for label in value: + k, sep, v = label.partition("=") + if sep == "": + raise click.BadParameter("Invalid label '{}', should be formatted as 'key=value'".format(k)) + labels[k] = v + + return labels + + +opt_labels = partial( + click.option, + "-l", + "--label", + "labels", + type=str, + multiple=True, + help="Labels to set on resource, can be set multiple times", + callback=_opt_labels_callback, +) + +opt_insecure_tls_config = click.option( + "--insecure-tls-config", + "insecure_tls_config", + is_flag=True, + default=False, + help="Disable endpoint TLS verification. This is insecure and should only be used for testing purposes", +) + + +def confirm_insecure_tls(insecure_tls_config: bool, nointeractive: bool): """Confirm if insecure TLS config is enabled and user wants to continue. Args: @@ -37,6 +66,7 @@ def confirm_insecure_tls(insecure_tls_config:bool, nointeractive: bool): click.echo("Aborting.") raise click.Abort() + class OutputMode(str): JSON = "json" YAML = "yaml"