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

Commit 1e807be

Browse files
support creating and updating lease's begin_time
this allows to create scheduled leases that will start at the requested time. At begin_time the lease will start acquisition that may be delayed or fail if the selected exporter is not available, following the lease acquisition timeout setting.
1 parent 6c37015 commit 1e807be

5 files changed

Lines changed: 91 additions & 27 deletions

File tree

packages/jumpstarter-cli/jumpstarter_cli/common.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import timedelta
1+
from datetime import datetime, timedelta
22
from functools import partial
33

44
import click
@@ -44,3 +44,31 @@ def convert(self, value, param, ctx):
4444
See https://docs.rs/speedate/latest/speedate/ for details
4545
""",
4646
)
47+
48+
49+
class DateTimeParamType(click.ParamType):
50+
name = "datetime"
51+
52+
def convert(self, value, param, ctx):
53+
if isinstance(value, datetime):
54+
return value
55+
56+
try:
57+
return TypeAdapter(datetime).validate_python(value)
58+
except ValueError:
59+
self.fail(f"{value!r} is not a valid datetime", param, ctx)
60+
61+
62+
DATETIME = DateTimeParamType()
63+
64+
opt_begin_time = click.option(
65+
"--begin-time",
66+
"begin_time",
67+
type=DATETIME,
68+
default=None,
69+
help="""
70+
Begin time for the lease in ISO 8601 format (e.g., 2024-01-01T12:00:00 or 2024-01-01T12:00:00Z).
71+
If not specified, the lease tries to be acquired immediately. The lease duration always starts
72+
at the actual time of acquisition.
73+
""",
74+
)

packages/jumpstarter-cli/jumpstarter_cli/create.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from datetime import timedelta
1+
from datetime import datetime, timedelta
22

33
import click
44
from jumpstarter_cli_common.config import opt_config
55
from jumpstarter_cli_common.exceptions import handle_exceptions_with_reauthentication
66
from jumpstarter_cli_common.opt import OutputType, opt_output_all
77
from jumpstarter_cli_common.print import model_print
88

9-
from .common import opt_duration_partial, opt_selector
9+
from .common import opt_begin_time, opt_duration_partial, opt_selector
1010
from .login import relogin_client
1111

1212

@@ -21,9 +21,10 @@ def create():
2121
@opt_config(exporter=False)
2222
@opt_selector
2323
@opt_duration_partial(required=True)
24+
@opt_begin_time
2425
@opt_output_all
2526
@handle_exceptions_with_reauthentication(relogin_client)
26-
def create_lease(config, selector: str, duration: timedelta, output: OutputType):
27+
def create_lease(config, selector: str, duration: timedelta, begin_time: datetime | None, output: OutputType):
2728
"""
2829
Create a lease
2930
@@ -49,6 +50,6 @@ def create_lease(config, selector: str, duration: timedelta, output: OutputType)
4950
5051
"""
5152

52-
lease = config.create_lease(selector=selector, duration=duration)
53+
lease = config.create_lease(selector=selector, duration=duration, begin_time=begin_time)
5354

5455
model_print(lease, output)

packages/jumpstarter-cli/jumpstarter_cli/update.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from datetime import timedelta
1+
from datetime import datetime, timedelta
22

33
import click
44
from jumpstarter_cli_common.config import opt_config
55
from jumpstarter_cli_common.exceptions import handle_exceptions_with_reauthentication
66
from jumpstarter_cli_common.opt import OutputType, opt_output_all
77
from jumpstarter_cli_common.print import model_print
88

9-
from .common import opt_duration_partial
9+
from .common import opt_begin_time, opt_duration_partial
1010
from .login import relogin_client
1111

1212

@@ -20,14 +20,22 @@ def update():
2020
@update.command(name="lease")
2121
@opt_config(exporter=False)
2222
@click.argument("name")
23-
@opt_duration_partial(required=True)
23+
@opt_duration_partial(required=False)
24+
@opt_begin_time
2425
@opt_output_all
2526
@handle_exceptions_with_reauthentication(relogin_client)
26-
def update_lease(config, name: str, duration: timedelta, output: OutputType):
27+
def update_lease(config, name: str, duration: timedelta | None, begin_time: datetime | None, output: OutputType):
2728
"""
2829
Update a lease
30+
31+
Update the duration and/or begin time of an existing lease.
32+
At least one of --duration or --begin-time must be specified.
33+
Updating the begin time of an already active lease is not allowed.
2934
"""
3035

31-
lease = config.update_lease(name, duration)
36+
if duration is None and begin_time is None:
37+
raise click.UsageError("At least one of --duration or --begin-time must be specified")
38+
39+
lease = config.update_lease(name, duration=duration, begin_time=begin_time)
3240

3341
model_print(lease, output)

packages/jumpstarter/jumpstarter/client/grpc.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from types import SimpleNamespace
88
from typing import Any
99

10-
from google.protobuf import duration_pb2, field_mask_pb2, json_format
10+
from google.protobuf import duration_pb2, field_mask_pb2, json_format, timestamp_pb2
1111
from grpc import ChannelConnectivity
1212
from grpc.aio import Channel
1313
from jumpstarter_protocol import client_pb2, client_pb2_grpc, jumpstarter_pb2_grpc, kubernetes_pb2, router_pb2_grpc
@@ -346,18 +346,26 @@ async def CreateLease(
346346
*,
347347
selector: str,
348348
duration: timedelta,
349+
begin_time: datetime | None = None,
349350
):
350351
duration_pb = duration_pb2.Duration()
351352
duration_pb.FromTimedelta(duration)
352353

354+
lease_pb = client_pb2.Lease(
355+
duration=duration_pb,
356+
selector=selector,
357+
)
358+
359+
if begin_time:
360+
timestamp_pb = timestamp_pb2.Timestamp()
361+
timestamp_pb.FromDatetime(begin_time)
362+
lease_pb.begin_time.CopyFrom(timestamp_pb)
363+
353364
with translate_grpc_exceptions():
354365
lease = await self.stub.CreateLease(
355366
client_pb2.CreateLeaseRequest(
356367
parent="namespaces/{}".format(self.namespace),
357-
lease=client_pb2.Lease(
358-
duration=duration_pb,
359-
selector=selector,
360-
),
368+
lease=lease_pb,
361369
)
362370
)
363371
return Lease.from_protobuf(lease)
@@ -366,21 +374,37 @@ async def UpdateLease(
366374
self,
367375
*,
368376
name: str,
369-
duration: timedelta,
377+
duration: timedelta | None = None,
378+
begin_time: datetime | None = None,
370379
):
371-
duration_pb = duration_pb2.Duration()
372-
duration_pb.FromTimedelta(duration)
380+
lease_pb = client_pb2.Lease(
381+
name="namespaces/{}/leases/{}".format(self.namespace, name),
382+
)
383+
384+
update_fields = []
385+
386+
if duration is not None:
387+
duration_pb = duration_pb2.Duration()
388+
duration_pb.FromTimedelta(duration)
389+
lease_pb.duration.CopyFrom(duration_pb)
390+
update_fields.append("duration")
391+
392+
if begin_time is not None:
393+
timestamp_pb = timestamp_pb2.Timestamp()
394+
timestamp_pb.FromDatetime(begin_time)
395+
lease_pb.begin_time.CopyFrom(timestamp_pb)
396+
update_fields.append("begin_time")
397+
398+
if not update_fields:
399+
raise ValueError("At least one of duration or begin_time must be provided")
373400

374401
update_mask = field_mask_pb2.FieldMask()
375-
update_mask.FromJsonString("duration")
402+
update_mask.FromJsonString(",".join(update_fields))
376403

377404
with translate_grpc_exceptions():
378405
lease = await self.stub.UpdateLease(
379406
client_pb2.UpdateLeaseRequest(
380-
lease=client_pb2.Lease(
381-
name="namespaces/{}/leases/{}".format(self.namespace, name),
382-
duration=duration_pb,
383-
),
407+
lease=lease_pb,
384408
update_mask=update_mask,
385409
)
386410
)

packages/jumpstarter/jumpstarter/config/client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
import os
55
from contextlib import asynccontextmanager, contextmanager
6-
from datetime import timedelta
6+
from datetime import datetime, timedelta
77
from functools import wraps
88
from pathlib import Path
99
from typing import Annotated, ClassVar, Literal, Optional, Self
@@ -200,11 +200,13 @@ async def create_lease(
200200
self,
201201
selector: str,
202202
duration: timedelta,
203+
begin_time: datetime | None = None,
203204
):
204205
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
205206
return await svc.CreateLease(
206207
selector=selector,
207208
duration=duration,
209+
begin_time=begin_time,
208210
)
209211

210212
@_blocking_compat
@@ -237,11 +239,12 @@ async def list_leases(
237239
@_handle_connection_error
238240
async def update_lease(
239241
self,
240-
name,
241-
duration: timedelta,
242+
name: str,
243+
duration: timedelta | None = None,
244+
begin_time: datetime | None = None,
242245
):
243246
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
244-
return await svc.UpdateLease(name=name, duration=duration)
247+
return await svc.UpdateLease(name=name, duration=duration, begin_time=begin_time)
245248

246249
@asynccontextmanager
247250
async def lease_async(

0 commit comments

Comments
 (0)