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

Commit 36015dd

Browse files
authored
Merge pull request #365 from jumpstarter-dev/duration
Allow specifying duration in jmp client shell command
2 parents d4c2c76 + 3926324 commit 36015dd

7 files changed

Lines changed: 101 additions & 107 deletions

File tree

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import sys
2+
from datetime import timedelta
23

34
import asyncclick as click
45
from jumpstarter_cli_common.exceptions import handle_exceptions
56

6-
from .common import opt_config, opt_selector_simple
7+
from .common import opt_config, opt_duration_partial, opt_selector_simple
78
from jumpstarter.common.utils import launch_shell
89

910

1011
@click.command("shell", short_help="Spawns a shell connecting to a leased remote exporter")
1112
@click.option("-n", "--lease", "lease_name", type=str)
1213
@opt_config
1314
@opt_selector_simple
15+
@opt_duration_partial(default=timedelta(minutes=30), show_default="00:30:00")
1416
@handle_exceptions
15-
def client_shell(config, selector: str, lease_name):
17+
def client_shell(config, selector: str, duration: timedelta, lease_name):
1618
"""Spawns a shell connecting to a leased remote exporter"""
1719

1820
exit_code = 0
1921

20-
with config.lease(selector=selector, lease_name=lease_name) as lease:
22+
with config.lease(selector=selector, lease_name=lease_name, duration=duration) as lease:
2123
with lease.serve_unix() as path:
2224
with lease.monitor():
2325
exit_code = launch_shell(path, "remote", config.drivers.allow, config.drivers.unsafe)

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import timedelta
2+
from functools import partial
23

34
import asyncclick as click
45
from pydantic import TypeAdapter
@@ -57,3 +58,20 @@ def convert(self, value, param, ctx):
5758
CLIENT = ClientParamType()
5859

5960
opt_config = click.option("--client", "config", type=CLIENT, default=False, help="Name of client config")
61+
opt_duration_partial = partial(
62+
click.option,
63+
"--duration",
64+
"duration",
65+
type=DURATION,
66+
help="""
67+
Accepted duration formats:
68+
69+
\b
70+
PnYnMnDTnHnMnS - ISO 8601 duration format
71+
HH:MM:SS - time in hours, minutes, seconds
72+
D days, HH:MM:SS - time prefixed by X days
73+
D d, HH:MM:SS - time prefixed by X d
74+
75+
See https://docs.rs/speedate/latest/speedate/ for details
76+
""",
77+
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010
from jumpstarter_cli_common.exceptions import handle_exceptions
1111

12-
from .common import DURATION, opt_config, opt_selector_simple
12+
from .common import opt_config, opt_duration_partial, opt_selector_simple
1313

1414

1515
@click.group()
@@ -22,7 +22,7 @@ def create():
2222
@create.command(name="lease")
2323
@opt_config
2424
@opt_selector_simple
25-
@click.option("--duration", "duration", type=DURATION, required=True)
25+
@opt_duration_partial(required=True)
2626
@opt_output_all
2727
@handle_exceptions
2828
async def create_lease(config, selector: str, duration: timedelta, output: OutputType):

packages/jumpstarter/jumpstarter/client/grpc.py

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from jumpstarter_protocol import client_pb2, client_pb2_grpc, kubernetes_pb2
1010
from pydantic import BaseModel, ConfigDict, Field, field_serializer
1111

12+
from jumpstarter.common.grpc import translate_grpc_exceptions
13+
1214

1315
def parse_identifier(identifier: str, kind: str) -> (str, str):
1416
segments = identifier.split("/")
@@ -143,11 +145,12 @@ def __post_init__(self):
143145
self.stub = client_pb2_grpc.ClientServiceStub(channel=self.channel)
144146

145147
async def GetExporter(self, *, name: str):
146-
exporter = await self.stub.GetExporter(
147-
client_pb2.GetExporterRequest(
148-
name="namespaces/{}/exporters/{}".format(self.namespace, name),
148+
with translate_grpc_exceptions():
149+
exporter = await self.stub.GetExporter(
150+
client_pb2.GetExporterRequest(
151+
name="namespaces/{}/exporters/{}".format(self.namespace, name),
152+
)
149153
)
150-
)
151154
return Exporter.from_protobuf(exporter)
152155

153156
async def ListExporters(
@@ -157,22 +160,24 @@ async def ListExporters(
157160
page_token: str | None = None,
158161
filter: str | None = None,
159162
):
160-
exporters = await self.stub.ListExporters(
161-
client_pb2.ListExportersRequest(
162-
parent="namespaces/{}".format(self.namespace),
163-
page_size=page_size,
164-
page_token=page_token,
165-
filter=filter,
163+
with translate_grpc_exceptions():
164+
exporters = await self.stub.ListExporters(
165+
client_pb2.ListExportersRequest(
166+
parent="namespaces/{}".format(self.namespace),
167+
page_size=page_size,
168+
page_token=page_token,
169+
filter=filter,
170+
)
166171
)
167-
)
168172
return ExporterList.from_protobuf(exporters)
169173

170174
async def GetLease(self, *, name: str):
171-
lease = await self.stub.GetLease(
172-
client_pb2.GetLeaseRequest(
173-
name="namespaces/{}/leases/{}".format(self.namespace, name),
175+
with translate_grpc_exceptions():
176+
lease = await self.stub.GetLease(
177+
client_pb2.GetLeaseRequest(
178+
name="namespaces/{}/leases/{}".format(self.namespace, name),
179+
)
174180
)
175-
)
176181
return Lease.from_protobuf(lease)
177182

178183
async def ListLeases(
@@ -182,14 +187,15 @@ async def ListLeases(
182187
page_token: str | None = None,
183188
filter: str | None = None,
184189
):
185-
leases = await self.stub.ListLeases(
186-
client_pb2.ListLeasesRequest(
187-
parent="namespaces/{}".format(self.namespace),
188-
page_size=page_size,
189-
page_token=page_token,
190-
filter=filter,
190+
with translate_grpc_exceptions():
191+
leases = await self.stub.ListLeases(
192+
client_pb2.ListLeasesRequest(
193+
parent="namespaces/{}".format(self.namespace),
194+
page_size=page_size,
195+
page_token=page_token,
196+
filter=filter,
197+
)
191198
)
192-
)
193199
return LeaseList.from_protobuf(leases)
194200

195201
async def CreateLease(
@@ -201,15 +207,16 @@ async def CreateLease(
201207
duration_pb = duration_pb2.Duration()
202208
duration_pb.FromTimedelta(duration)
203209

204-
lease = await self.stub.CreateLease(
205-
client_pb2.CreateLeaseRequest(
206-
parent="namespaces/{}".format(self.namespace),
207-
lease=client_pb2.Lease(
208-
duration=duration_pb,
209-
selector=selector,
210-
),
210+
with translate_grpc_exceptions():
211+
lease = await self.stub.CreateLease(
212+
client_pb2.CreateLeaseRequest(
213+
parent="namespaces/{}".format(self.namespace),
214+
lease=client_pb2.Lease(
215+
duration=duration_pb,
216+
selector=selector,
217+
),
218+
)
211219
)
212-
)
213220
return Lease.from_protobuf(lease)
214221

215222
async def UpdateLease(
@@ -224,20 +231,22 @@ async def UpdateLease(
224231
update_mask = field_mask_pb2.FieldMask()
225232
update_mask.FromJsonString("duration")
226233

227-
lease = await self.stub.UpdateLease(
228-
client_pb2.UpdateLeaseRequest(
229-
lease=client_pb2.Lease(
230-
name="namespaces/{}/leases/{}".format(self.namespace, name),
231-
duration=duration_pb,
232-
),
233-
update_mask=update_mask,
234+
with translate_grpc_exceptions():
235+
lease = await self.stub.UpdateLease(
236+
client_pb2.UpdateLeaseRequest(
237+
lease=client_pb2.Lease(
238+
name="namespaces/{}/leases/{}".format(self.namespace, name),
239+
duration=duration_pb,
240+
),
241+
update_mask=update_mask,
242+
)
234243
)
235-
)
236244
return Lease.from_protobuf(lease)
237245

238246
async def DeleteLease(self, *, name: str):
239-
await self.stub.DeleteLease(
240-
client_pb2.DeleteLeaseRequest(
241-
name="namespaces/{}/leases/{}".format(self.namespace, name),
247+
with translate_grpc_exceptions():
248+
await self.stub.DeleteLease(
249+
client_pb2.DeleteLeaseRequest(
250+
name="namespaces/{}/leases/{}".format(self.namespace, name),
251+
)
242252
)
243-
)

packages/jumpstarter/jumpstarter/client/lease.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@dataclass(kw_only=True)
3131
class Lease(AbstractContextManager, AbstractAsyncContextManager):
3232
channel: Channel
33-
timeout: int = 1800
33+
duration: timedelta
3434
selector: str
3535
portal: BlockingPortal
3636
namespace: str
@@ -51,17 +51,15 @@ def __post_init__(self):
5151
self.manager = self.portal.wrap_async_context_manager(self)
5252

5353
async def _create(self):
54-
duration = timedelta(seconds=self.timeout)
55-
56-
logger.debug("Creating lease request for selector %s for duration %s", self.selector, duration)
54+
logger.debug("Creating lease request for selector %s for duration %s", self.selector, self.duration)
5755
with translate_grpc_exceptions():
5856
self.name = (
5957
await self.svc.CreateLease(
6058
selector=self.selector,
61-
duration=timedelta(seconds=self.timeout),
59+
duration=self.duration,
6260
)
6361
).name
64-
logger.info("Created lease request for selector %s for duration %s", self.selector, duration)
62+
logger.info("Created lease request for selector %s for duration %s", self.selector, self.duration)
6563

6664
async def get(self):
6765
with translate_grpc_exceptions():

packages/jumpstarter/jumpstarter/config/client.py

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .tls import TLSConfigV1Alpha1
1616
from jumpstarter.client.grpc import ClientService
1717
from jumpstarter.common.exceptions import FileNotFoundError
18-
from jumpstarter.common.grpc import aio_secure_channel, ssl_channel_credentials, translate_grpc_exceptions
18+
from jumpstarter.common.grpc import aio_secure_channel, ssl_channel_credentials
1919

2020

2121
def _allow_from_env():
@@ -61,9 +61,14 @@ async def channel(self):
6161
return aio_secure_channel(self.endpoint, credentials, self.grpcOptions)
6262

6363
@contextmanager
64-
def lease(self, selector: str | None = None, lease_name: str | None = None):
64+
def lease(
65+
self,
66+
selector: str | None = None,
67+
lease_name: str | None = None,
68+
duration: timedelta = timedelta(minutes=30),
69+
):
6570
with start_blocking_portal() as portal:
66-
with portal.wrap_async_context_manager(self.lease_async(selector, lease_name, portal)) as lease:
71+
with portal.wrap_async_context_manager(self.lease_async(selector, lease_name, duration, portal)) as lease:
6772
yield lease
6873

6974
def get_exporter(self, name: str):
@@ -79,10 +84,6 @@ def list_exporters(
7984
with start_blocking_portal() as portal:
8085
return portal.call(self.list_exporters_async, page_size, page_token, filter)
8186

82-
def request_lease(self, selector: str):
83-
with start_blocking_portal() as portal:
84-
return portal.call(self.request_lease_async, selector, portal)
85-
8687
def list_leases(self, filter: str):
8788
with start_blocking_portal() as portal:
8889
return portal.call(self.list_leases_async, filter)
@@ -106,14 +107,9 @@ def update_lease(self, name, duration: timedelta):
106107
with start_blocking_portal() as portal:
107108
return portal.call(self.update_lease_async, name, duration)
108109

109-
def release_lease(self, name):
110-
with start_blocking_portal() as portal:
111-
portal.call(self.release_lease_async, name)
112-
113110
async def get_exporter_async(self, name: str):
114111
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
115-
with translate_grpc_exceptions():
116-
return await svc.GetExporter(name=name)
112+
return await svc.GetExporter(name=name)
117113

118114
async def list_exporters_async(
119115
self,
@@ -122,70 +118,39 @@ async def list_exporters_async(
122118
filter: str | None = None,
123119
):
124120
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
125-
with translate_grpc_exceptions():
126-
return await svc.ListExporters(page_size=page_size, page_token=page_token, filter=filter)
121+
return await svc.ListExporters(page_size=page_size, page_token=page_token, filter=filter)
127122

128123
async def create_lease_async(
129124
self,
130125
selector: str,
131126
duration: timedelta,
132127
):
133128
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
134-
with translate_grpc_exceptions():
135-
return await svc.CreateLease(
136-
selector=selector,
137-
duration=duration,
138-
)
129+
return await svc.CreateLease(
130+
selector=selector,
131+
duration=duration,
132+
)
139133

140134
async def delete_lease_async(self, name: str):
141135
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
142-
with translate_grpc_exceptions():
143-
await svc.DeleteLease(
144-
name=name,
145-
)
146-
147-
async def request_lease_async(
148-
self,
149-
selector: str,
150-
portal: BlockingPortal,
151-
):
152-
# dynamically import to avoid circular imports
153-
from jumpstarter.client import Lease
154-
155-
lease = Lease(
156-
channel=await self.channel(),
157-
namespace=self.metadata.namespace,
158-
name=None,
159-
selector=selector,
160-
portal=portal,
161-
allow=self.drivers.allow,
162-
unsafe=self.drivers.unsafe,
163-
tls_config=self.tls,
164-
grpc_options=self.grpcOptions,
136+
await svc.DeleteLease(
137+
name=name,
165138
)
166-
with translate_grpc_exceptions():
167-
return await lease.request_async()
168139

169140
async def list_leases_async(self, filter: str):
170141
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
171-
with translate_grpc_exceptions():
172-
return await svc.ListLeases(filter=filter)
142+
return await svc.ListLeases(filter=filter)
173143

174144
async def update_lease_async(self, name, duration: timedelta):
175145
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
176-
with translate_grpc_exceptions():
177-
return await svc.UpdateLease(name=name, duration=duration)
178-
179-
async def release_lease_async(self, name):
180-
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
181-
with translate_grpc_exceptions():
182-
await svc.DeleteLease(name=name)
146+
return await svc.UpdateLease(name=name, duration=duration)
183147

184148
@asynccontextmanager
185149
async def lease_async(
186150
self,
187151
selector: str,
188152
lease_name: str | None,
153+
duration: timedelta,
189154
portal: BlockingPortal,
190155
):
191156
from jumpstarter.client import Lease
@@ -200,6 +165,7 @@ async def lease_async(
200165
namespace=self.metadata.namespace,
201166
name=lease_name,
202167
selector=selector,
168+
duration=duration,
203169
portal=portal,
204170
allow=self.drivers.allow,
205171
unsafe=self.drivers.unsafe,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ locale = "en-us"
7171

7272
[tool.typos.default.extend-words]
7373
ser = "ser"
74+
Pn = "Pn"
7475

7576
[tool.coverage.run]
7677
omit = ["conftest.py", "test_*.py", "*_test.py", "*_pb2.py", "*_pb2_grpc.py"]

0 commit comments

Comments
 (0)