Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
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
4 changes: 2 additions & 2 deletions docs/source/getting-started/configuration/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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'
```
```
4 changes: 2 additions & 2 deletions docs/source/getting-started/usage/setup-distributed-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
patterns and implementation examples, see [Examples](./examples.md).
12 changes: 6 additions & 6 deletions packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand Down
33 changes: 22 additions & 11 deletions packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -309,55 +312,63 @@ 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)
save_exporter_mock.reset_mock()

# 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()
Expand Down
38 changes: 34 additions & 4 deletions packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import partial
from typing import Literal, Optional

import asyncclick as click
Expand All @@ -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:
Expand All @@ -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"
Expand Down
Loading