Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 99c4cc5

Browse files
authored
Merge pull request #476 from jumpstarter-dev/backport-label-options
[Backport release-0.6] Update labels option syntax on admin cli to match other places
2 parents a06eca4 + b1bd805 commit 99c4cc5

5 files changed

Lines changed: 66 additions & 25 deletions

File tree

docs/source/getting-started/configuration/authentication.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ jwt:
190190
service account name with "dex:" as configured in the claim mappings.:
191191

192192
```shell
193-
$ jmp admin create exporter test-exporter \
193+
$ jmp admin create exporter test-exporter --label foo=bar \
194194
--insecure-tls-config \
195195
--oidc-username dex:system:serviceaccount:default:test-service-account
196196
```
@@ -293,4 +293,4 @@ jwt:
293293
message: 'username cannot used reserved system: prefix'
294294
- expression: "user.groups.all(group, !group.startsWith('system:'))"
295295
message: 'groups cannot used reserved system: prefix'
296-
```
296+
```

docs/source/getting-started/usage/setup-distributed-mode.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Run this command to create an exporter named `example-distributed` and save the
4040
configuration locally:
4141

4242
```shell
43-
$ jmp admin create exporter example-distributed --save --insecure-tls-config
43+
$ jmp admin create exporter example-distributed --label foo=bar --save --insecure-tls-config
4444
```
4545

4646
After creating the exporter, find the new configuration file at
@@ -114,4 +114,4 @@ $ exit
114114

115115
Once you have your exporter shell running, you can start using Jumpstarter
116116
commands to interact with your hardware. To learn more about common workflow
117-
patterns and implementation examples, see [Examples](./examples.md).
117+
patterns and implementation examples, see [Examples](./examples.md).

packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def print_created_client(client: V1Alpha1Client, output: OutputType):
7474
default=None,
7575
)
7676
@opt_namespace
77-
@opt_labels
77+
@opt_labels()
7878
@opt_kubeconfig
7979
@opt_context
8080
@opt_insecure_tls_config
@@ -87,7 +87,7 @@ async def create_client(
8787
context: Optional[str],
8888
insecure_tls_config: bool,
8989
namespace: str,
90-
labels: list[(str, str)],
90+
labels: dict[str, str],
9191
save: bool,
9292
allow: Optional[str],
9393
unsafe: bool,
@@ -103,7 +103,7 @@ async def create_client(
103103
if output is None:
104104
# Only print status if is not JSON/YAML
105105
click.echo(f"Creating client '{name}' in namespace '{namespace}'")
106-
created_client = await api.create_client(name, dict(labels), oidc_username)
106+
created_client = await api.create_client(name, labels, oidc_username)
107107
# Save the client config
108108
if save or out is not None or nointeractive is False and click.confirm("Save client configuration?"):
109109
if output is None:
@@ -159,7 +159,7 @@ def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType):
159159
default=None,
160160
)
161161
@opt_namespace
162-
@opt_labels
162+
@opt_labels(required=True)
163163
@opt_kubeconfig
164164
@opt_context
165165
@opt_insecure_tls_config
@@ -172,7 +172,7 @@ async def create_exporter(
172172
context: Optional[str],
173173
insecure_tls_config: bool,
174174
namespace: str,
175-
labels: list[(str, str)],
175+
labels: dict[str, str],
176176
save: bool,
177177
out: Optional[str],
178178
oidc_username: str | None,
@@ -185,7 +185,7 @@ async def create_exporter(
185185
async with ExportersV1Alpha1Api(namespace, kubeconfig, context) as api:
186186
if output is None:
187187
click.echo(f"Creating exporter '{name}' in namespace '{namespace}'")
188-
created_exporter = await api.create_exporter(name, dict(labels), oidc_username)
188+
created_exporter = await api.create_exporter(name, labels, oidc_username)
189189
# Save the client config
190190
if save or out is not None or nointeractive is False and click.confirm("Save exporter configuration?"):
191191
if output is None:

packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ async def test_create_exporter(
279279
runner = CliRunner()
280280

281281
# Don't save exporter config
282-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME], input="n\n")
282+
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar"], input="n\n")
283+
print(result.output)
283284
assert result.exit_code == 0
284285
assert "Creating exporter" in result.output
285286
assert EXPORTER_NAME in result.output
@@ -290,7 +291,9 @@ async def test_create_exporter(
290291
# Insecure TLS config is returned
291292
_get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG
292293
# Save with prompts accept insecure = Y, save = Y
293-
result = await runner.invoke(create, ["exporter", "--insecure-tls-config", EXPORTER_NAME], input="Y\nY\n")
294+
result = await runner.invoke(
295+
create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="Y\nY\n"
296+
)
294297
assert result.exit_code == 0
295298
assert "Exporter configuration successfully saved" in result.output
296299
save_exporter_mock.assert_called_once_with(INSECURE_TLS_EXPORTER_CONFIG, None)
@@ -299,7 +302,7 @@ async def test_create_exporter(
299302
_get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG
300303
# Save with prompts accept no interactive
301304
result = await runner.invoke(
302-
create, ["exporter", "--insecure-tls-config", "--nointeractive", "--save", EXPORTER_NAME]
305+
create, ["exporter", "--insecure-tls-config", "--nointeractive", "--save", EXPORTER_NAME, "--label", "foo=bar"]
303306
)
304307
assert result.exit_code == 0
305308
assert "Exporter configuration successfully saved" in result.output
@@ -309,55 +312,63 @@ async def test_create_exporter(
309312
# Insecure TLS config is returned
310313
_get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG
311314
# Save with prompts accept insecure = N
312-
result = await runner.invoke(create, ["exporter", "--insecure-tls-config", EXPORTER_NAME], input="n\n")
315+
result = await runner.invoke(
316+
create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="n\n"
317+
)
313318
assert result.exit_code == 1
314319
assert "Aborted" in result.output
315320

316321
# Save with prompts
317-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME], input="Y\n")
322+
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar"], input="Y\n")
318323
assert result.exit_code == 0
319324
assert "Exporter configuration successfully saved" in result.output
320325
save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, None)
321326
save_exporter_mock.reset_mock()
322327

323328
# Save with arguments
324-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--save"])
329+
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--save"])
325330
assert result.exit_code == 0
326331
assert "Exporter configuration successfully saved" in result.output
327332
save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, None)
328333
save_exporter_mock.reset_mock()
329334

330335
# Save with arguments and custom path
331336
out = f"/tmp/{EXPORTER_NAME}.yaml"
332-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--out", out])
337+
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--out", out])
333338
assert result.exit_code == 0
334339
assert "Exporter configuration successfully saved" in result.output
335340
save_exporter_mock.assert_called_once_with(EXPORTER_CONFIG, str(Path(out).resolve()))
336341
save_exporter_mock.reset_mock()
337342

338343
# Save with nointeractive
339-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive"])
344+
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive"])
340345
assert result.exit_code == 0
341346
assert "Creating exporter" in result.output
342347
save_exporter_mock.assert_not_called()
343348
save_exporter_mock.reset_mock()
344349

345350
# Save with JSON output
346-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "json"])
351+
result = await runner.invoke(
352+
create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "json"]
353+
)
347354
assert result.exit_code == 0
348355
assert result.output == EXPORTER_JSON
349356
save_exporter_mock.assert_not_called()
350357
save_exporter_mock.reset_mock()
351358

352359
# Save with YAML output
353-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "yaml"])
360+
result = await runner.invoke(
361+
create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "yaml"]
362+
)
354363
assert result.exit_code == 0
355364
assert result.output == EXPORTER_YAML
356365
save_exporter_mock.assert_not_called()
357366
save_exporter_mock.reset_mock()
358367

359368
# Save with name output
360-
result = await runner.invoke(create, ["exporter", EXPORTER_NAME, "--nointeractive", "--output", "name"])
369+
result = await runner.invoke(
370+
create, ["exporter", EXPORTER_NAME, "--label", "foo=bar", "--nointeractive", "--output", "name"]
371+
)
361372
assert result.exit_code == 0
362373
assert result.output == f"exporter.jumpstarter.dev/{EXPORTER_NAME}\n"
363374
save_exporter_mock.assert_not_called()

packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
from typing import Literal, Optional
23

34
import asyncclick as click
@@ -17,12 +18,40 @@
1718

1819
opt_namespace = click.option("-n", "--namespace", type=str, help="Kubernetes namespace to use", default="default")
1920

20-
opt_labels = click.option("-l", "--label", "labels", type=(str, str), multiple=True, help="Labels")
2121

22-
opt_insecure_tls_config = click.option("--insecure-tls-config", "insecure_tls_config", is_flag=True, default=False,
23-
help="Disable endpoint TLS verification. This is insecure and should only be used for testing purposes")
22+
def _opt_labels_callback(ctx, param, value):
23+
labels = {}
2424

25-
def confirm_insecure_tls(insecure_tls_config:bool, nointeractive: bool):
25+
for label in value:
26+
k, sep, v = label.partition("=")
27+
if sep == "":
28+
raise click.BadParameter("Invalid label '{}', should be formatted as 'key=value'".format(k))
29+
labels[k] = v
30+
31+
return labels
32+
33+
34+
opt_labels = partial(
35+
click.option,
36+
"-l",
37+
"--label",
38+
"labels",
39+
type=str,
40+
multiple=True,
41+
help="Labels to set on resource, can be set multiple times",
42+
callback=_opt_labels_callback,
43+
)
44+
45+
opt_insecure_tls_config = click.option(
46+
"--insecure-tls-config",
47+
"insecure_tls_config",
48+
is_flag=True,
49+
default=False,
50+
help="Disable endpoint TLS verification. This is insecure and should only be used for testing purposes",
51+
)
52+
53+
54+
def confirm_insecure_tls(insecure_tls_config: bool, nointeractive: bool):
2655
"""Confirm if insecure TLS config is enabled and user wants to continue.
2756
2857
Args:
@@ -37,6 +66,7 @@ def confirm_insecure_tls(insecure_tls_config:bool, nointeractive: bool):
3766
click.echo("Aborting.")
3867
raise click.Abort()
3968

69+
4070
class OutputMode(str):
4171
JSON = "json"
4272
YAML = "yaml"

0 commit comments

Comments
 (0)