50c2fe2a1e
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
Complete working build of the system learn-python/ teaches. 164 tests, mypy --strict clean, domain coverage 99%.
134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
"""The real thing: a real helm, against a real cluster, installing a real chart.
|
|
|
|
Everything else in this suite runs against a FakeProvisioner, which is what makes the
|
|
worker tests take milliseconds. That trade has one cost, and this file is the payment: the
|
|
fake proves the *worker* is correct, and cannot prove the *adapter* is. Argv order, the
|
|
`--wait` flag, chart resolution, RBAC, whether `upgrade --install` is genuinely idempotent
|
|
against a live release — none of it is exercised by a fake that returns None.
|
|
|
|
So one test does it for real, exactly once. It is marked `e2e` and excluded from `make test`
|
|
and from every pre-commit run; CI runs `-m e2e` in a job that has a cluster.
|
|
|
|
kind create cluster --name svcforge
|
|
uv run pytest -m e2e -q
|
|
|
|
It skips (does not fail) with no cluster, because a laptop without kubectl is not a
|
|
regression.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import shutil
|
|
import subprocess
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from svcforge_core.adapters.helm import HelmProvisioner
|
|
from svcforge_core.domain.models import CatalogEntry, SizeSpec
|
|
|
|
pytestmark = [pytest.mark.e2e, pytest.mark.slow]
|
|
|
|
NAMESPACE = "svcforge-e2e"
|
|
|
|
# A chart with no dependencies, no PVCs and no image pulls worth waiting on. The point is
|
|
# to exercise the adapter, not to wait four minutes for Elasticsearch.
|
|
ENTRY = CatalogEntry(
|
|
service_type="podinfo",
|
|
chart="oci://ghcr.io/stefanprodan/charts/podinfo",
|
|
chart_version="6.7.1",
|
|
sizes={"small": SizeSpec(replicas=1, resources={})},
|
|
)
|
|
|
|
|
|
def _cluster_reachable() -> bool:
|
|
if not shutil.which("helm") or not shutil.which("kubectl"):
|
|
return False
|
|
out = subprocess.run(
|
|
["kubectl", "cluster-info"],
|
|
capture_output=True,
|
|
timeout=15,
|
|
)
|
|
return out.returncode == 0
|
|
|
|
|
|
requires_cluster = pytest.mark.skipif(
|
|
not _cluster_reachable(),
|
|
reason="no reachable cluster: `kind create cluster --name svcforge`",
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def namespace() -> str:
|
|
subprocess.run(
|
|
["kubectl", "create", "namespace", NAMESPACE],
|
|
capture_output=True,
|
|
check=False, # already exists is fine — the whole system is idempotent or it is broken
|
|
)
|
|
return NAMESPACE
|
|
|
|
|
|
@requires_cluster
|
|
async def test_install_is_idempotent_against_a_real_cluster(namespace: str) -> None:
|
|
"""Install twice. Get one release. This is the claim the fake cannot make for us."""
|
|
release = f"e2e-podinfo-{uuid.uuid4().hex[:8]}"
|
|
prov = HelmProvisioner(timeout_s=300)
|
|
|
|
try:
|
|
await prov.install(release=release, ns=namespace, entry=ENTRY, values={"replicaCount": 1})
|
|
|
|
releases = [r for r in await prov.list_releases() if r.name == release]
|
|
assert len(releases) == 1, f"expected exactly one release, got {releases}"
|
|
|
|
# The redelivery, for real: same task, same deterministic release name, run again.
|
|
# `helm upgrade --install` must converge, not duplicate and not error.
|
|
await prov.install(release=release, ns=namespace, entry=ENTRY, values={"replicaCount": 1})
|
|
|
|
releases = [r for r in await prov.list_releases() if r.name == release]
|
|
assert len(releases) == 1, "second install created a second release: not idempotent"
|
|
|
|
# --wait means the DB writing `ready` is telling the truth.
|
|
out = subprocess.run(
|
|
[
|
|
"kubectl",
|
|
"get",
|
|
"deploy",
|
|
"-n",
|
|
namespace,
|
|
"-l",
|
|
f"app.kubernetes.io/instance={release}",
|
|
"-o",
|
|
"jsonpath={.items[*].status.readyReplicas}",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
assert out.stdout.strip() == "1", f"--wait returned before the pod was ready: {out.stdout!r}"
|
|
finally:
|
|
await prov.uninstall(release=release, ns=namespace)
|
|
|
|
|
|
@requires_cluster
|
|
async def test_uninstall_of_a_missing_release_is_not_an_error(namespace: str) -> None:
|
|
"""The desired state — no release — is already true. That is success, not failure."""
|
|
prov = HelmProvisioner(timeout_s=120)
|
|
await prov.uninstall(release=f"never-existed-{uuid.uuid4().hex[:8]}", ns=namespace)
|
|
|
|
|
|
@requires_cluster
|
|
async def test_real_helm_timeout_leaves_no_helm_behind(namespace: str) -> None:
|
|
"""A deadline that does not kill helm is a deadline that lets helm keep mutating."""
|
|
prov = HelmProvisioner(timeout_s=1) # cannot possibly finish
|
|
release = f"e2e-timeout-{uuid.uuid4().hex[:8]}"
|
|
|
|
with pytest.raises((TimeoutError, Exception)):
|
|
await prov.install(release=release, ns=namespace, entry=ENTRY, values={})
|
|
|
|
await asyncio.sleep(0.5)
|
|
out = subprocess.run(["pgrep", "-f", f"helm.*{release}"], capture_output=True, text=True)
|
|
assert out.stdout.strip() == "", "helm survived its own timeout and is still touching the cluster"
|
|
|
|
await prov.uninstall(release=release, ns=namespace)
|