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

Commit c119d30

Browse files
authored
Merge pull request #338 from jumpstarter-dev/client-svc-lease
kubectl style cli
2 parents 1f68b0d + fa18287 commit c119d30

File tree

16 files changed

+427
-188
lines changed

16 files changed

+427
-188
lines changed

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
V1Alpha1ExporterStatus,
1616
V1Alpha1Lease,
1717
V1Alpha1LeaseList,
18+
V1Alpha1LeaseSelector,
1819
V1Alpha1LeaseSpec,
1920
V1Alpha1LeaseStatus,
2021
)
@@ -837,7 +838,7 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock
837838
spec=V1Alpha1LeaseSpec(
838839
client=V1ObjectReference(name="test_client"),
839840
duration="5m",
840-
selector={"hardware": "rpi4"},
841+
selector=V1Alpha1LeaseSelector(match_labels={"hardware": "rpi4"}),
841842
),
842843
)
843844

@@ -868,7 +869,7 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock
868869
spec=V1Alpha1LeaseSpec(
869870
client=V1ObjectReference(name="test_client"),
870871
duration="1h",
871-
selector={},
872+
selector=V1Alpha1LeaseSelector(match_labels={}),
872873
),
873874
)
874875

@@ -885,7 +886,9 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock
885886
"name": "test_client"
886887
},
887888
"duration": "1h",
888-
"selector": {}
889+
"selector": {
890+
"matchLabels": {}
891+
}
889892
},
890893
"status": {
891894
"beginTime": "2024-01-01T21:00:00Z",
@@ -918,7 +921,8 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock
918921
client:
919922
name: test_client
920923
duration: 1h
921-
selector: {}
924+
selector:
925+
matchLabels: {}
922926
status:
923927
beginTime: '2024-01-01T21:00:00Z'
924928
conditions:
@@ -1022,7 +1026,9 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
10221026
},
10231027
"duration": "5m",
10241028
"selector": {
1025-
"hardware": "rpi4"
1029+
"matchLabels": {
1030+
"hardware": "rpi4"
1031+
}
10261032
}
10271033
},
10281034
"status": {
@@ -1057,7 +1063,9 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
10571063
"name": "test_client"
10581064
},
10591065
"duration": "1h",
1060-
"selector": {}
1066+
"selector": {
1067+
"matchLabels": {}
1068+
}
10611069
},
10621070
"status": {
10631071
"beginTime": "2024-01-01T21:00:00Z",
@@ -1096,7 +1104,8 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
10961104
name: test_client
10971105
duration: 5m
10981106
selector:
1099-
hardware: rpi4
1107+
matchLabels:
1108+
hardware: rpi4
11001109
status:
11011110
beginTime: '2024-01-01T21:00:00Z'
11021111
conditions:
@@ -1120,7 +1129,8 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
11201129
client:
11211130
name: test_client
11221131
duration: 1h
1123-
selector: {}
1132+
selector:
1133+
matchLabels: {}
11241134
status:
11251135
beginTime: '2024-01-01T21:00:00Z'
11261136
conditions:

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ def get_reason(lease: V1Alpha1Lease):
131131
return "Expired"
132132
else:
133133
return "Complete"
134+
else:
135+
return reason
134136

135137

136138
def make_lease_row(lease: V1Alpha1Lease):
137139
selectors = []
138-
for label in lease.spec.selector:
139-
selectors.append(f"{label}:{str(lease.spec.selector[label])}")
140+
for label in lease.spec.selector.match_labels:
141+
selectors.append(f"{label}:{str(lease.spec.selector.match_labels[label])}")
140142
return {
141143
"NAME": lease.metadata.name,
142144
"CLIENT": lease.spec.client.name if lease.spec.client is not None else "",

packages/jumpstarter-cli-client/jumpstarter_cli_client/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
from jumpstarter_cli_common import AliasedGroup, opt_log_level, version
77
from jumpstarter_cli_common.exceptions import handle_exceptions
88

9-
from .client_exporter import list_client_exporters
10-
from .client_lease import client_lease
119
from .client_login import client_login
1210
from .client_shell import client_shell
1311
from .config import config
12+
from .create import create
13+
from .delete import delete
14+
from .get import get
15+
from .update import update
1416
from jumpstarter.common.utils import env
1517

1618

@@ -38,9 +40,11 @@ def cli():
3840
sys.exit(1)
3941

4042

41-
client.add_command(list_client_exporters)
4243
client.add_command(config)
43-
client.add_command(client_lease)
44+
client.add_command(create)
45+
client.add_command(get)
46+
client.add_command(delete)
47+
client.add_command(update)
4448
client.add_command(client_login)
4549
client.add_command(client_shell)
4650
client.add_command(version)

packages/jumpstarter-cli-client/jumpstarter_cli_client/client_exporter.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

packages/jumpstarter-cli-client/jumpstarter_cli_client/client_lease.py

Lines changed: 0 additions & 96 deletions
This file was deleted.

packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,24 @@
33
import asyncclick as click
44
from jumpstarter_cli_common.exceptions import handle_exceptions
55

6+
from .common import opt_config, opt_selector_simple, selector_to_labels
67
from jumpstarter.common import MetadataFilter
78
from jumpstarter.common.utils import launch_shell
8-
from jumpstarter.config import (
9-
ClientConfigV1Alpha1,
10-
UserConfigV1Alpha1,
11-
)
129

1310

1411
@click.command("shell", short_help="Spawns a shell connecting to a leased remote exporter")
15-
@click.argument("name", type=str, default="")
16-
@click.option("-l", "--label", "labels", type=(str, str), multiple=True)
1712
@click.option("-n", "--lease", "lease_name", type=str)
13+
@opt_config
14+
@opt_selector_simple
1815
@handle_exceptions
19-
def client_shell(name: str, labels, lease_name):
16+
def client_shell(config, selector: str, lease_name):
2017
"""Spawns a shell connecting to a leased remote exporter"""
21-
if name:
22-
config = ClientConfigV1Alpha1.load(name)
23-
else:
24-
config = UserConfigV1Alpha1.load_or_create().config.current_client
25-
if not config:
26-
raise click.BadParameter(
27-
"no client specified, and no default client set: specify a client name, or use jmp client config use",
28-
param_hint="name",
29-
)
3018

3119
exit_code = 0
32-
with config.lease(metadata_filter=MetadataFilter(labels=dict(labels)), lease_name=lease_name) as lease:
20+
21+
with config.lease(
22+
metadata_filter=MetadataFilter(labels=selector_to_labels(selector)), lease_name=lease_name
23+
) as lease:
3324
with lease.serve_unix() as path:
3425
with lease.monitor():
3526
exit_code = launch_shell(path, "remote", config.drivers.allow, config.drivers.unsafe)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import timedelta
2+
3+
import asyncclick as click
4+
from pydantic import TypeAdapter
5+
6+
from jumpstarter.config import (
7+
ClientConfigV1Alpha1,
8+
UserConfigV1Alpha1,
9+
)
10+
11+
opt_selector = click.option(
12+
"-l",
13+
"--selector",
14+
help="Selector (label query) to filter on, supports '=', '==', and '!=' (e.g. -l key1=value1,key2=value2)."
15+
" Matching objects must satisfy all of the specified label constraints.",
16+
)
17+
18+
opt_selector_simple = click.option(
19+
"-l",
20+
"--selector",
21+
help="Selector (label query) to filter on, only supports '=', (e.g. -l key1=value1,key2=value2)."
22+
" Matching objects must satisfy all of the specified label constraints.",
23+
required=True,
24+
)
25+
26+
27+
def selector_to_labels(selector: str):
28+
# TODO: support complex selectors (e.g. !=)
29+
return dict([term.split("=") for term in selector.split(",")])
30+
31+
32+
class ClientParamType(click.ParamType):
33+
name = "client"
34+
35+
def convert(self, value, param, ctx):
36+
if isinstance(value, ClientConfigV1Alpha1):
37+
return value
38+
39+
if isinstance(value, bool): # hack to allow loading the default config
40+
config = UserConfigV1Alpha1.load_or_create().config.current_client
41+
if config is None:
42+
self.fail("no client config specified, and no default client config set", param, ctx)
43+
return config
44+
else:
45+
return ClientConfigV1Alpha1.load(value)
46+
47+
48+
class DurationParamType(click.ParamType):
49+
name = "duration"
50+
51+
def convert(self, value, param, ctx):
52+
if isinstance(value, timedelta):
53+
return value
54+
55+
try:
56+
return TypeAdapter(timedelta).validate_python(value)
57+
except ValueError:
58+
self.fail(f"{value!r} is not a valid duration", param, ctx)
59+
60+
61+
DURATION = DurationParamType()
62+
CLIENT = ClientParamType()
63+
64+
opt_config = click.option("--client", "config", type=CLIENT, default=False, help="Name of client config")

0 commit comments

Comments
 (0)