diff --git a/pyisolate/operator/__init__.py b/pyisolate/operator/__init__.py index 5dcd775..0b00287 100644 --- a/pyisolate/operator/__init__.py +++ b/pyisolate/operator/__init__.py @@ -1,4 +1,5 @@ """Kubernetes operator for PyIsolate sandboxes.""" + from __future__ import annotations import logging @@ -15,8 +16,19 @@ def run_operator(namespace: str = "default") -> None: - """Start the operator watch loop.""" - from kubernetes import client, config, watch # type: ignore + """Start the operator watch loop. + + Requires the optional ``kubernetes`` client, which is not a core dependency. + Install it with ``pip install pyisolate[operator]``. + """ + try: + from kubernetes import client, config, watch # type: ignore + except ImportError as exc: + raise RuntimeError( + "the PyIsolate Kubernetes operator requires the 'kubernetes' " + "package, which is not installed. Install it with " + "`pip install pyisolate[operator]`." + ) from exc config.load_incluster_config() api = client.CustomObjectsApi() diff --git a/pyproject.toml b/pyproject.toml index 80f4dcd..17dc55a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ pyisolate-doctor = "pyisolate.doctor:main" [project.optional-dependencies] pqcrypto = ["pqcrypto"] +operator = ["kubernetes"] release = [ "build", "twine", diff --git a/tests/test_operator.py b/tests/test_operator.py index 573349d..cf01965 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -3,6 +3,8 @@ import types from pathlib import Path +import pytest + def load_operator(): pkg = types.ModuleType("pyisolate") @@ -34,6 +36,18 @@ def test_operator_module(): assert hasattr(op, "scale_sandboxes") +def test_run_operator_without_kubernetes_gives_actionable_error(): + try: + import kubernetes # noqa: F401 + + pytest.skip("kubernetes is installed; missing-dependency path not exercised") + except ImportError: + pass + op = load_operator() + with pytest.raises(RuntimeError, match=r"pyisolate\[operator\]"): + op.run_operator() + + def test_operator_handles_relisted_added_and_modified(monkeypatch): op = load_operator() events = [