diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py index 69ebbfc24..fa7521d12 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py @@ -15,6 +15,7 @@ V1Alpha1ExporterStatus, V1Alpha1Lease, V1Alpha1LeaseList, + V1Alpha1LeaseSelector, V1Alpha1LeaseSpec, V1Alpha1LeaseStatus, ) @@ -837,7 +838,7 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock spec=V1Alpha1LeaseSpec( client=V1ObjectReference(name="test_client"), duration="5m", - selector={"hardware": "rpi4"}, + selector=V1Alpha1LeaseSelector(match_labels={"hardware": "rpi4"}), ), ) @@ -868,7 +869,7 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock spec=V1Alpha1LeaseSpec( client=V1ObjectReference(name="test_client"), duration="1h", - selector={}, + selector=V1Alpha1LeaseSelector(match_labels={}), ), ) @@ -885,7 +886,9 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock "name": "test_client" }, "duration": "1h", - "selector": {} + "selector": { + "matchLabels": {} + } }, "status": { "beginTime": "2024-01-01T21:00:00Z", @@ -918,7 +921,8 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock client: name: test_client duration: 1h - selector: {} + selector: + matchLabels: {} status: beginTime: '2024-01-01T21:00:00Z' conditions: @@ -1022,7 +1026,9 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock): }, "duration": "5m", "selector": { - "hardware": "rpi4" + "matchLabels": { + "hardware": "rpi4" + } } }, "status": { @@ -1057,7 +1063,9 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock): "name": "test_client" }, "duration": "1h", - "selector": {} + "selector": { + "matchLabels": {} + } }, "status": { "beginTime": "2024-01-01T21:00:00Z", @@ -1096,7 +1104,8 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock): name: test_client duration: 5m selector: - hardware: rpi4 + matchLabels: + hardware: rpi4 status: beginTime: '2024-01-01T21:00:00Z' conditions: @@ -1120,7 +1129,8 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock): client: name: test_client duration: 1h - selector: {} + selector: + matchLabels: {} status: beginTime: '2024-01-01T21:00:00Z' conditions: diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index 3d42dad12..fc0a4cb38 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -131,12 +131,14 @@ def get_reason(lease: V1Alpha1Lease): return "Expired" else: return "Complete" + else: + return reason def make_lease_row(lease: V1Alpha1Lease): selectors = [] - for label in lease.spec.selector: - selectors.append(f"{label}:{str(lease.spec.selector[label])}") + for label in lease.spec.selector.match_labels: + selectors.append(f"{label}:{str(lease.spec.selector.match_labels[label])}") return { "NAME": lease.metadata.name, "CLIENT": lease.spec.client.name if lease.spec.client is not None else "", diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/__init__.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/__init__.py index 0c8fef955..79ea4d292 100644 --- a/packages/jumpstarter-cli-client/jumpstarter_cli_client/__init__.py +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/__init__.py @@ -6,11 +6,13 @@ from jumpstarter_cli_common import AliasedGroup, opt_log_level, version from jumpstarter_cli_common.exceptions import handle_exceptions -from .client_exporter import list_client_exporters -from .client_lease import client_lease from .client_login import client_login from .client_shell import client_shell from .config import config +from .create import create +from .delete import delete +from .get import get +from .update import update from jumpstarter.common.utils import env @@ -38,9 +40,11 @@ def cli(): sys.exit(1) -client.add_command(list_client_exporters) client.add_command(config) -client.add_command(client_lease) +client.add_command(create) +client.add_command(get) +client.add_command(delete) +client.add_command(update) client.add_command(client_login) client.add_command(client_shell) client.add_command(version) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_exporter.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_exporter.py deleted file mode 100644 index 632b6d6d7..000000000 --- a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_exporter.py +++ /dev/null @@ -1,46 +0,0 @@ -import asyncclick as click -from jumpstarter_cli_common import OutputMode, OutputType, make_table, opt_labels, opt_output_all -from jumpstarter_cli_common.exceptions import handle_exceptions - -from jumpstarter.config import ( - ClientConfigV1Alpha1, - UserConfigV1Alpha1, -) - - -@click.command("list-exporters", short_help="List available exporters.") -@click.argument("name", type=str, default="") -@opt_labels -@opt_output_all -@handle_exceptions -def list_client_exporters(name: str | None, labels: list[(str, str)], output: OutputType): - if name: - config = ClientConfigV1Alpha1.load(name) - else: - config = UserConfigV1Alpha1.load_or_create().config.current_client - if not config: - raise click.BadParameter( - "no client specified, and no default client set: specify a client name, or use jmp client config use", - param_hint="name", - ) - - exporters = config.list_exporters(filter=",".join("{}={}".format(i[0], i[1]) for i in labels)) - - if output == OutputMode.JSON: - click.echo(exporters.dump_json()) - elif output == OutputMode.YAML: - click.echo(exporters.dump_yaml()) - elif output == OutputMode.NAME: - for exporter in exporters.exporters: - click.echo(exporter.name) - else: - columns = ["NAME", "LABELS"] - - def make_row(exporter): - return { - "NAME": exporter.name, - "LABELS": ",".join(("{}={}".format(i[0], i[1]) for i in exporter.labels.items())), - } - - rows = list(map(make_row, exporters.exporters)) - click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_lease.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_lease.py deleted file mode 100644 index 033ccdeee..000000000 --- a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_lease.py +++ /dev/null @@ -1,96 +0,0 @@ -import asyncclick as click -from jumpstarter_cli_common import AliasedGroup -from jumpstarter_cli_common.exceptions import handle_exceptions - -from jumpstarter.common import MetadataFilter -from jumpstarter.config import ( - ClientConfigV1Alpha1, - UserConfigV1Alpha1, -) - - -@click.group(name="lease", cls=AliasedGroup, short_help="") -def client_lease(): - """Manage leases held by the current client""" - pass - - -@client_lease.command("list") -@click.argument("name", type=str, default="") -@handle_exceptions -def lease_list(name): - if name: - config = ClientConfigV1Alpha1.load(name) - else: - config = UserConfigV1Alpha1.load_or_create().config.current_client - if not config: - raise ValueError("no client specified") - - for lease in config.list_leases(): - print(lease) - - -@client_lease.command("release") -@click.argument("name", type=str, default="") -@click.option("-l", "--lease", "lease", type=str, default="") -@click.option("--all", "all_leases", is_flag=True) -@handle_exceptions -def lease_release(name, lease, all_leases): - if name: - config = ClientConfigV1Alpha1.load(name) - else: - config = UserConfigV1Alpha1.load_or_create().config.current_client - if not config: - raise click.BadParameter( - "no client specified, and no default client set: specify a client name, or use jmp client config use", - param_hint="name", - ) - - if all_leases: - for lease in config.list_leases(): - config.release_lease(lease) - else: - if not lease: - raise click.BadParameter( - "no lease specified, provide one or use --all to release all leases", param_hint="lease" - ) - config.release_lease(lease) - - -@client_lease.command("request") -@click.option("-l", "--label", "labels", type=(str, str), multiple=True) -@click.argument("name", type=str, default="") -@handle_exceptions -def lease_request(name, labels): - """Request an exporter lease from the jumpstarter controller. - - The result of this command will be a lease ID that can be used to - connect to the remote exporter. - - This is useful for multi-step workflows where you want to hold a lease - for a specific exporter while performing multiple operations, or for - CI environments where one step will request the lease and other steps - will perform operations on the leased exporter. - - Example: - - .. code-block:: bash - - $ JMP_LEASE=$(jmp lease request -l label match) - $ jmp shell - $$ j --help - $$ exit - $ jmp lease release -l "${JMP_LEASE}" - - """ - if name: - config = ClientConfigV1Alpha1.load(name) - else: - config = UserConfigV1Alpha1.load_or_create().config.current_client - if not config: - raise click.BadParameter( - "no client specified, and no default client set: specify a client name, or use jmp client config use", - param_hint="name", - ) - lease = config.request_lease(metadata_filter=MetadataFilter(labels=dict(labels))) - print(lease.name) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py index 1ef884920..5d0168f73 100644 --- a/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py @@ -3,33 +3,24 @@ import asyncclick as click from jumpstarter_cli_common.exceptions import handle_exceptions +from .common import opt_config, opt_selector_simple, selector_to_labels from jumpstarter.common import MetadataFilter from jumpstarter.common.utils import launch_shell -from jumpstarter.config import ( - ClientConfigV1Alpha1, - UserConfigV1Alpha1, -) @click.command("shell", short_help="Spawns a shell connecting to a leased remote exporter") -@click.argument("name", type=str, default="") -@click.option("-l", "--label", "labels", type=(str, str), multiple=True) @click.option("-n", "--lease", "lease_name", type=str) +@opt_config +@opt_selector_simple @handle_exceptions -def client_shell(name: str, labels, lease_name): +def client_shell(config, selector: str, lease_name): """Spawns a shell connecting to a leased remote exporter""" - if name: - config = ClientConfigV1Alpha1.load(name) - else: - config = UserConfigV1Alpha1.load_or_create().config.current_client - if not config: - raise click.BadParameter( - "no client specified, and no default client set: specify a client name, or use jmp client config use", - param_hint="name", - ) exit_code = 0 - with config.lease(metadata_filter=MetadataFilter(labels=dict(labels)), lease_name=lease_name) as lease: + + with config.lease( + metadata_filter=MetadataFilter(labels=selector_to_labels(selector)), lease_name=lease_name + ) as lease: with lease.serve_unix() as path: with lease.monitor(): exit_code = launch_shell(path, "remote", config.drivers.allow, config.drivers.unsafe) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/common.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/common.py new file mode 100644 index 000000000..665f2fe84 --- /dev/null +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/common.py @@ -0,0 +1,64 @@ +from datetime import timedelta + +import asyncclick as click +from pydantic import TypeAdapter + +from jumpstarter.config import ( + ClientConfigV1Alpha1, + UserConfigV1Alpha1, +) + +opt_selector = click.option( + "-l", + "--selector", + help="Selector (label query) to filter on, supports '=', '==', and '!=' (e.g. -l key1=value1,key2=value2)." + " Matching objects must satisfy all of the specified label constraints.", +) + +opt_selector_simple = click.option( + "-l", + "--selector", + help="Selector (label query) to filter on, only supports '=', (e.g. -l key1=value1,key2=value2)." + " Matching objects must satisfy all of the specified label constraints.", + required=True, +) + + +def selector_to_labels(selector: str): + # TODO: support complex selectors (e.g. !=) + return dict([term.split("=") for term in selector.split(",")]) + + +class ClientParamType(click.ParamType): + name = "client" + + def convert(self, value, param, ctx): + if isinstance(value, ClientConfigV1Alpha1): + return value + + if isinstance(value, bool): # hack to allow loading the default config + config = UserConfigV1Alpha1.load_or_create().config.current_client + if config is None: + self.fail("no client config specified, and no default client config set", param, ctx) + return config + else: + return ClientConfigV1Alpha1.load(value) + + +class DurationParamType(click.ParamType): + name = "duration" + + def convert(self, value, param, ctx): + if isinstance(value, timedelta): + return value + + try: + return TypeAdapter(timedelta).validate_python(value) + except ValueError: + self.fail(f"{value!r} is not a valid duration", param, ctx) + + +DURATION = DurationParamType() +CLIENT = ClientParamType() + +opt_config = click.option("--client", "config", type=CLIENT, default=False, help="Name of client config") diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/create.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/create.py new file mode 100644 index 000000000..a6cc2e315 --- /dev/null +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/create.py @@ -0,0 +1,74 @@ +from datetime import timedelta + +import asyncclick as click +from jumpstarter_cli_common import ( + OutputMode, + OutputType, + make_table, + opt_output_all, +) +from jumpstarter_cli_common.exceptions import handle_exceptions + +from .common import DURATION, opt_config, opt_selector_simple + + +@click.group() +def create(): + """ + Create a resource + """ + + +@create.command(name="lease") +@opt_config +@opt_selector_simple +@click.option("--duration", "duration", type=DURATION, required=True) +@opt_output_all +@handle_exceptions +async def create_lease(config, selector: str, duration: timedelta, output: OutputType): + """ + Create a lease + + Request an exporter lease from the jumpstarter controller. + + The result of this command will be a lease ID that can be used to + connect to the remote exporter. + + This is useful for multi-step workflows where you want to hold a lease + for a specific exporter while performing multiple operations, or for + CI environments where one step will request the lease and other steps + will perform operations on the leased exporter. + + Example: + + .. code-block:: bash + + $ JMP_LEASE=$(jmp client create lease -l foo=bar --duration 1d --output name) + $ jmp shell + $$ j --help + $$ exit + $ jmp client delete lease "${JMP_LEASE}" + + """ + + lease = config.create_lease(selector=selector, duration=duration) + + match output: + case OutputMode.JSON: + click.echo(lease.dump_json()) + case OutputMode.YAML: + click.echo(lease.dump_yaml()) + case OutputMode.NAME: + click.echo(lease.name) + case _: + columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "SELECTOR": lease.selector, + "DURATION": str(lease.duration.total_seconds()), + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/delete.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/delete.py new file mode 100644 index 000000000..063e24532 --- /dev/null +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/delete.py @@ -0,0 +1,50 @@ +import asyncclick as click +from jumpstarter_cli_common import OutputMode, OutputType, opt_output_name_only +from jumpstarter_cli_common.exceptions import handle_exceptions + +from .common import opt_config, opt_selector + + +@click.group() +def delete(): + """ + Delete resources + """ + + +@delete.command(name="leases") +@opt_config +@click.argument("name", required=False) +@opt_selector +@click.option("--all", "all", is_flag=True) +@opt_output_name_only +@handle_exceptions +def delete_leases(config, name: str, selector: str | None, all: bool, output: OutputType): + """ + Delete leases + """ + + names = [] + + if name is not None: + names.append(name) + elif selector: + leases = config.list_leases(filter=selector) + for lease in leases.leases: + if lease.client == config.metadata.name: + names.append(lease.name) + elif all: + leases = config.list_leases(filter=None) + for lease in leases.leases: + if lease.client == config.metadata.name: + names.append(lease.name) + else: + raise click.ClickException("One of NAME, --selector or --all must be specified") + + for name in names: + config.delete_lease(name=name) + match output: + case OutputMode.NAME: + click.echo(name) + case _: + click.echo('lease "{}" deleted'.format(name)) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/get.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/get.py new file mode 100644 index 000000000..4cd27596f --- /dev/null +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/get.py @@ -0,0 +1,77 @@ +import asyncclick as click +from jumpstarter_cli_common import OutputMode, OutputType, make_table, opt_output_all +from jumpstarter_cli_common.exceptions import handle_exceptions + +from .common import opt_config, opt_selector + + +@click.group() +def get(): + """ + Display one or many resources + """ + + +@get.command(name="exporters") +@opt_config +@opt_selector +@opt_output_all +@handle_exceptions +def get_exporters(config, selector: str | None, output: OutputType): + """ + Display one or many exporters + """ + + exporters = config.list_exporters(filter=selector) + + match output: + case OutputMode.JSON: + click.echo(exporters.dump_json()) + case OutputMode.YAML: + click.echo(exporters.dump_yaml()) + case OutputMode.NAME: + for exporter in exporters.exporters: + click.echo(exporter.name) + case _: + columns = ["NAME", "LABELS"] + rows = [ + { + "NAME": exporter.name, + "LABELS": ",".join(("{}={}".format(i[0], i[1]) for i in exporter.labels.items())), + } + for exporter in exporters.exporters + ] + click.echo(make_table(columns, rows)) + + +@get.command(name="leases") +@opt_config +@opt_selector +@opt_output_all +@handle_exceptions +def get_leases(config, selector: str | None, output: OutputType): + """ + Display one or many leases + """ + + leases = config.list_leases(filter=selector) + + match output: + case OutputMode.JSON: + click.echo(leases.dump_json()) + case OutputMode.YAML: + click.echo(leases.dump_yaml()) + case OutputMode.NAME: + for lease in leases.leases: + click.echo(lease.name) + case _: + columns = ["NAME", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + for lease in leases.leases + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/update.py b/packages/jumpstarter-cli-client/jumpstarter_cli_client/update.py new file mode 100644 index 000000000..9b899d268 --- /dev/null +++ b/packages/jumpstarter-cli-client/jumpstarter_cli_client/update.py @@ -0,0 +1,48 @@ +from datetime import timedelta + +import asyncclick as click +from jumpstarter_cli_common import OutputMode, OutputType, make_table, opt_output_all +from jumpstarter_cli_common.exceptions import handle_exceptions + +from .common import DURATION, opt_config + + +@click.group() +def update(): + """ + Update a resource + """ + + +@update.command(name="lease") +@opt_config +@click.argument("name") +@click.option("--duration", "duration", type=DURATION, required=True) +@opt_output_all +@handle_exceptions +async def update_lease(config, name: str, duration: timedelta, output: OutputType): + """ + Update a lease + """ + + lease = config.update_lease(name, duration) + + match output: + case OutputMode.JSON: + click.echo(lease.dump_json()) + case OutputMode.YAML: + click.echo(lease.dump_yaml()) + case OutputMode.NAME: + click.echo(lease.name) + case _: + columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "SELECTOR": lease.selector, + "DURATION": str(lease.duration.total_seconds()), + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py index fd2a9a85f..07b91b3ad 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py @@ -1,3 +1,5 @@ +from functools import wraps + import asyncclick as click from jumpstarter.common.exceptions import JumpstarterException @@ -11,6 +13,7 @@ def format_message(self) -> str: def async_handle_exceptions(func): """Decorator to handle exceptions in async functions.""" + @wraps(func) async def wrapped(*args, **kwargs): try: return await func(*args, **kwargs) @@ -27,6 +30,7 @@ async def wrapped(*args, **kwargs): def handle_exceptions(func): """Decorator to handle exceptions in blocking functions.""" + @wraps(func) def wrapped(*args, **kwargs): try: return func(*args, **kwargs) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py index 2febbd246..6a9bf1b76 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py @@ -7,7 +7,14 @@ V1Alpha1ExporterStatus, ) from .install import get_ip_address, helm_installed, install_helm_chart -from .leases import LeasesV1Alpha1Api, V1Alpha1Lease, V1Alpha1LeaseList, V1Alpha1LeaseSpec, V1Alpha1LeaseStatus +from .leases import ( + LeasesV1Alpha1Api, + V1Alpha1Lease, + V1Alpha1LeaseList, + V1Alpha1LeaseSelector, + V1Alpha1LeaseSpec, + V1Alpha1LeaseStatus, +) from .list import V1Alpha1List __all__ = [ @@ -24,6 +31,7 @@ "V1Alpha1Lease", "V1Alpha1LeaseStatus", "V1Alpha1LeaseList", + "V1Alpha1LeaseSelector", "V1Alpha1LeaseSpec", "V1Alpha1List", "get_ip_address", diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py index f57b9fe40..67361684c 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py @@ -17,10 +17,14 @@ class V1Alpha1LeaseStatus(JsonBaseModel): exporter: Optional[SerializeV1ObjectReference] +class V1Alpha1LeaseSelector(JsonBaseModel): + match_labels: dict[str, str] = Field(alias="matchLabels") + + class V1Alpha1LeaseSpec(JsonBaseModel): client: SerializeV1ObjectReference duration: Optional[str] - selector: dict[str, str] + selector: V1Alpha1LeaseSelector class V1Alpha1Lease(JsonBaseModel): @@ -38,7 +42,6 @@ def from_dict(dict: dict): metadata=V1ObjectMeta( creation_timestamp=dict["metadata"]["creationTimestamp"], generation=dict["metadata"]["generation"], - labels=dict["metadata"]["labels"], managed_fields=dict["metadata"]["managedFields"], name=dict["metadata"]["name"], namespace=dict["metadata"]["namespace"], @@ -69,7 +72,7 @@ def from_dict(dict: dict): if "clientRef" in dict["spec"] else None, duration=dict["spec"]["duration"] if "duration" in dict["spec"] else None, - selector=dict["spec"]["selector"], + selector=V1Alpha1LeaseSelector(match_labels=dict["spec"]["selector"]["matchLabels"]), ), ) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py index 456359e54..ef523d38c 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py @@ -1,6 +1,6 @@ from kubernetes_asyncio.client.models import V1Condition, V1ObjectMeta, V1ObjectReference -from jumpstarter_kubernetes import V1Alpha1Lease, V1Alpha1LeaseSpec, V1Alpha1LeaseStatus +from jumpstarter_kubernetes import V1Alpha1Lease, V1Alpha1LeaseSelector, V1Alpha1LeaseSpec, V1Alpha1LeaseStatus TEST_LEASE = V1Alpha1Lease( api_version="jumpstarter.dev/v1alpha1", @@ -16,7 +16,7 @@ spec=V1Alpha1LeaseSpec( client=V1ObjectReference(name="test-client"), duration="1h", - selector={"test": "label", "another": "something"}, + selector=V1Alpha1LeaseSelector(match_labels={"test": "label", "another": "something"}), ), status=V1Alpha1LeaseStatus( begin_time="2021-10-01T00:00:00Z", @@ -53,8 +53,10 @@ def test_lease_dump_json(): }, "duration": "1h", "selector": { - "test": "label", - "another": "something" + "matchLabels": { + "test": "label", + "another": "something" + } } }, "status": { @@ -96,8 +98,9 @@ def test_lease_dump_yaml(): name: test-client duration: 1h selector: - another: something - test: label + matchLabels: + another: something + test: label status: beginTime: '2021-10-01T00:00:00Z' conditions: diff --git a/packages/jumpstarter/jumpstarter/config/client.py b/packages/jumpstarter/jumpstarter/config/client.py index 6e0346339..715038daf 100644 --- a/packages/jumpstarter/jumpstarter/config/client.py +++ b/packages/jumpstarter/jumpstarter/config/client.py @@ -1,5 +1,6 @@ import os from contextlib import asynccontextmanager, contextmanager +from datetime import timedelta from pathlib import Path from typing import ClassVar, Literal, Optional, Self @@ -83,9 +84,28 @@ def request_lease(self, metadata_filter: MetadataFilter): with start_blocking_portal() as portal: return portal.call(self.request_lease_async, metadata_filter, portal) - def list_leases(self): + def list_leases(self, filter: str): with start_blocking_portal() as portal: - return portal.call(self.list_leases_async) + return portal.call(self.list_leases_async, filter) + + def create_lease( + self, + selector: str, + duration: timedelta, + ): + with start_blocking_portal() as portal: + return portal.call(self.create_lease_async, selector, duration) + + def delete_lease( + self, + name: str, + ): + with start_blocking_portal() as portal: + return portal.call(self.delete_lease_async, name) + + def update_lease(self, name, duration: timedelta): + with start_blocking_portal() as portal: + return portal.call(self.update_lease_async, name, duration) def release_lease(self, name): with start_blocking_portal() as portal: @@ -106,6 +126,25 @@ async def list_exporters_async( with translate_grpc_exceptions(): return await svc.ListExporters(page_size=page_size, page_token=page_token, filter=filter) + async def create_lease_async( + self, + selector: str, + duration: timedelta, + ): + svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) + with translate_grpc_exceptions(): + return await svc.CreateLease( + selector=selector, + duration=duration, + ) + + async def delete_lease_async(self, name: str): + svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) + with translate_grpc_exceptions(): + await svc.DeleteLease( + name=name, + ) + async def request_lease_async( self, metadata_filter: MetadataFilter, @@ -128,11 +167,15 @@ async def request_lease_async( with translate_grpc_exceptions(): return await lease.request_async() - async def list_leases_async(self): + async def list_leases_async(self, filter: str): + svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) + with translate_grpc_exceptions(): + return await svc.ListLeases(filter=filter) + + async def update_lease_async(self, name, duration: timedelta): svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) with translate_grpc_exceptions(): - result = await svc.ListLeases() - return [lease.name for lease in result.leases] + return await svc.UpdateLease(name=name, duration=duration) async def release_lease_async(self, name): svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)