From 842f760d4d2744c392616b2e2f65903848ff633c Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Sun, 24 Sep 2023 11:23:27 +0100 Subject: [PATCH] Create and Delete objects from files (#166) --- examples/kubectl-ng/kubectl_ng/_create.py | 50 +++++++++++++++ examples/kubectl-ng/kubectl_ng/_delete.py | 62 +++++++++++++++++++ examples/kubectl-ng/kubectl_ng/cli.py | 4 ++ .../resources/simple/nginx_pod_service.yaml | 27 ++++++++ .../kubectl_ng/tests/test_create_delete.py | 34 ++++++++++ kr8s/_objects.py | 23 +++++-- kr8s/objects.py | 6 +- 7 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 examples/kubectl-ng/kubectl_ng/_create.py create mode 100644 examples/kubectl-ng/kubectl_ng/_delete.py create mode 100644 examples/kubectl-ng/kubectl_ng/tests/resources/simple/nginx_pod_service.yaml create mode 100644 examples/kubectl-ng/kubectl_ng/tests/test_create_delete.py diff --git a/examples/kubectl-ng/kubectl_ng/_create.py b/examples/kubectl-ng/kubectl_ng/_create.py new file mode 100644 index 00000000..b11e500d --- /dev/null +++ b/examples/kubectl-ng/kubectl_ng/_create.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023, Dask Developers, NVIDIA +# SPDX-License-Identifier: BSD 3-Clause License + +import typer +from rich.console import Console + +import kr8s.asyncio +from kr8s.asyncio.objects import objects_from_files + +console = Console() + + +# Missing Options +# TODO --allow-missing-template-keys='true' +# TODO --dry-run='none' +# TODO --edit='false' +# TODO --field-manager='' +# TODO -k, --kustomize='' +# TODO -o, --output='' +# TODO --raw='false' +# TODO -R, --recursive='false' +# TODO --save-config='false' +# TODO -l, --selector='' +# TODO --show-managed-fields='false' +# TODO --template='' +# TODO --validate='true' +# TODO --windows-line-endings='false' + + +async def create( + filename: str = typer.Option( + "", + "--filename", + "-f", + help="Filename, directory, or URL to files identifying the resources to create", + ), +): + api = await kr8s.asyncio.api() + try: + objs = await objects_from_files(filename, api) + except Exception as e: + console.print(f"[red]Error loading objects from {filename}[/red]: {e}") + raise typer.Exit(1) + for obj in objs: + try: + await obj.create() + except Exception as e: + console.print(f"[red]Error creating {obj}[/red]: {e}") + raise typer.Exit(1) + console.print(f'[green]{obj.singular} "{obj}" created [/green]') diff --git a/examples/kubectl-ng/kubectl_ng/_delete.py b/examples/kubectl-ng/kubectl_ng/_delete.py new file mode 100644 index 00000000..18dae041 --- /dev/null +++ b/examples/kubectl-ng/kubectl_ng/_delete.py @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023, Dask Developers, NVIDIA +# SPDX-License-Identifier: BSD 3-Clause License + +import anyio +import typer +from rich.console import Console + +import kr8s.asyncio +from kr8s.asyncio.objects import objects_from_files + +console = Console() + + +# Missing Options +# TODO --all=false +# TODO -A, --all-namespaces=false +# TODO --cascade='background' +# TODO --dry-run='none' +# TODO --field-selector='' +# TODO --force=false +# TODO --grace-period=-1 +# TODO --ignore-not-found=false +# TODO -k, --kustomize='' +# TODO --now='false' +# TODO -o, --output='' +# TODO --raw='false' +# TODO -R, --recursive='false' +# TODO -l, --selector='' +# TODO --timeout='0s' +# TODO --wait='true' + + +async def delete( + filename: str = typer.Option( + "", + "--filename", + "-f", + help="Filename, directory, or URL to files identifying the resources to delete", + ), + wait: bool = typer.Option( + True, + "--wait", + help="If true, wait for resources to be gone before returning. This waits for finalizers.", + ), +): + api = await kr8s.asyncio.api() + try: + objs = await objects_from_files(filename, api) + except Exception as e: + console.print(f"[red]Error loading objects from {filename}[/red]: {e}") + raise typer.Exit(1) + for obj in objs: + try: + await obj.delete() + except Exception as e: + console.print(f"[red]Error deleting {obj}[/red]: {e}") + raise typer.Exit(1) + console.print(f'[green]{obj.singular} "{obj}" deleted [/green]') + async with anyio.create_task_group() as tg: + for obj in objs: + if wait: + tg.start_soon(obj.wait, "delete") diff --git a/examples/kubectl-ng/kubectl_ng/cli.py b/examples/kubectl-ng/kubectl_ng/cli.py index cc3cd03f..c152fd1e 100644 --- a/examples/kubectl-ng/kubectl_ng/cli.py +++ b/examples/kubectl-ng/kubectl_ng/cli.py @@ -6,6 +6,8 @@ import typer from ._api_resources import api_resources +from ._create import create +from ._delete import delete from ._get import get from ._version import version from ._wait import wait @@ -27,6 +29,8 @@ def register(app, func): app = typer.Typer() register(app, api_resources) +register(app, create) +register(app, delete) register(app, get) register(app, version) register(app, wait) diff --git a/examples/kubectl-ng/kubectl_ng/tests/resources/simple/nginx_pod_service.yaml b/examples/kubectl-ng/kubectl_ng/tests/resources/simple/nginx_pod_service.yaml new file mode 100644 index 00000000..51b23d7b --- /dev/null +++ b/examples/kubectl-ng/kubectl_ng/tests/resources/simple/nginx_pod_service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx + labels: + app.kubernetes.io/name: proxy +spec: + containers: + - name: nginx + image: nginx:stable + ports: + - containerPort: 80 + name: http-web-svc + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx-service +spec: + selector: + app.kubernetes.io/name: proxy + ports: + - name: name-of-service-port + protocol: TCP + port: 80 + targetPort: http-web-svc diff --git a/examples/kubectl-ng/kubectl_ng/tests/test_create_delete.py b/examples/kubectl-ng/kubectl_ng/tests/test_create_delete.py new file mode 100644 index 00000000..e797a4f1 --- /dev/null +++ b/examples/kubectl-ng/kubectl_ng/tests/test_create_delete.py @@ -0,0 +1,34 @@ +import pathlib + +from kubectl_ng.cli import app +from typer.testing import CliRunner + +from kr8s.objects import objects_from_files + +runner = CliRunner() + +HERE = pathlib.Path(__file__).parent.absolute() + + +def test_create_and_delete(): + spec = str(HERE / "resources" / "simple" / "nginx_pod_service.yaml") + + objs = objects_from_files(spec) + for obj in objs: + assert not obj.exists() + + result = runner.invoke(app, ["create", "-f", spec]) + assert result.exit_code == 0 + for obj in objs: + assert obj.name in result.stdout + + for obj in objs: + assert obj.exists() + + result = runner.invoke(app, ["delete", "-f", spec]) + assert result.exit_code == 0 + for obj in objs: + assert obj.name in result.stdout + + for obj in objs: + assert not obj.exists() diff --git a/kr8s/_objects.py b/kr8s/_objects.py index b116ed98..a2e24029 100644 --- a/kr8s/_objects.py +++ b/kr8s/_objects.py @@ -1212,12 +1212,14 @@ def new_class( def object_from_spec( - spec: dict, api: Api = None, allow_unknown_type: bool = False + spec: dict, api: Api = None, allow_unknown_type: bool = False, _asyncio: bool = True ) -> APIObject: """Create an APIObject from a Kubernetes resource spec. Args: spec: A Kubernetes resource spec. + allow_unknown_type: Whether to allow unknown resource types. + _asyncio: Whether to use asyncio or not. Returns: A corresponding APIObject subclass instance. @@ -1226,7 +1228,7 @@ def object_from_spec( ValueError: If the resource kind or API version is not supported. """ try: - cls = get_class(spec["kind"], spec["apiVersion"]) + cls = get_class(spec["kind"], spec["apiVersion"], _asyncio=_asyncio) except KeyError: if allow_unknown_type: cls = new_class(spec["kind"], spec["apiVersion"]) @@ -1236,12 +1238,15 @@ def object_from_spec( async def object_from_name_type( - name: str, namespace: str = None, api: Api = None + name: str, namespace: str = None, api: Api = None, _asyncio: bool = True ) -> APIObject: """Create an APIObject from a Kubernetes resource name. Args: name: A Kubernetes resource name. + namespace: The namespace of the resource. + api: An optional API instance to use. + _asyncio: Whether to use asyncio or not. Returns: A corresponding APIObject subclass instance. @@ -1258,12 +1263,15 @@ async def object_from_name_type( else: kind = resource_type version = None - cls = get_class(kind, version) + cls = get_class(kind, version, _asyncio=_asyncio) return await cls.get(name, namespace=namespace, api=api) async def objects_from_files( - path: Union[str, pathlib.Path], api: Api = None, recursive: bool = False + path: Union[str, pathlib.Path], + api: Api = None, + recursive: bool = False, + _asyncio: bool = True, ) -> List[APIObject]: """Create APIObjects from Kubernetes resource files. @@ -1271,6 +1279,7 @@ async def objects_from_files( path: A path to a Kubernetes resource file or directory of resource files. api: An optional API instance to use. recursive: Whether to recursively search for resource files in subdirectories. + _asyncio: Whether to use asyncio or not. Returns: A list of APIObject subclass instances. @@ -1290,6 +1299,8 @@ async def objects_from_files( for doc in yaml.safe_load_all(f): if doc is not None: objects.append( - object_from_spec(doc, api=api, allow_unknown_type=True) + object_from_spec( + doc, api=api, allow_unknown_type=True, _asyncio=_asyncio + ) ) return objects diff --git a/kr8s/objects.py b/kr8s/objects.py index 1ecd0e1c..648b1277 100644 --- a/kr8s/objects.py +++ b/kr8s/objects.py @@ -1,3 +1,5 @@ +from functools import partial + from ._io import run_sync, sync from ._objects import ( APIObject as _APIObject, @@ -328,5 +330,5 @@ class Table(_Table): _asyncio = False -object_from_name_type = run_sync(_object_from_name_type) -objects_from_files = run_sync(_objects_from_files) +object_from_name_type = run_sync(partial(_object_from_name_type, _asyncio=False)) +objects_from_files = run_sync(partial(_objects_from_files, _asyncio=False))