Files
krkn/CI/tests_v2/lib/namespace.py
Darshan Jain a14d3955a6 feat(ci): add pytest-based CI test framework v2 with ephemeral namespace isolation (#1172) (#1171)
* feat: add pytest-based CI test framework v2 with ephemeral namespace isolation

Signed-off-by: ddjain <darjain@redhat.com>

* feat(ci): add tests_v2 pytest functional test framework

Signed-off-by: ddjain <darjain@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: improve naming convention

Signed-off-by: ddjain <darjain@redhat.com>

* improve local setup script.

Signed-off-by: ddjain <darjain@redhat.com>

* added CI job for v2 test

Signed-off-by: ddjain <darjain@redhat.com>

* disabled broken test

Signed-off-by: ddjain <darjain@redhat.com>

* improved CI pipeline execution time

Signed-off-by: ddjain <darjain@redhat.com>

* chore: remove unwanted/generated files from PR

Signed-off-by: ddjain <darjain@redhat.com>

* clean up gitignore file

Signed-off-by: ddjain <darjain@redhat.com>

* fix copilot comments

Signed-off-by: ddjain <darjain@redhat.com>

* fixed copilot suggestion

Signed-off-by: ddjain <darjain@redhat.com>

* uncommented out test upload stage

Signed-off-by: ddjain <darjain@redhat.com>

* exclude CI/tests_v2 from test coverage reporting

Signed-off-by: ddjain <darjain@redhat.com>

* uploading style.css to fix broken report artifacts

Signed-off-by: ddjain <darjain@redhat.com>

* added openshift supported labels in namespace creatation api

Signed-off-by: ddjain <darjain@redhat.com>

---------

Signed-off-by: ddjain <darjain@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-06 08:44:07 -05:00

115 lines
3.8 KiB
Python

"""
Namespace lifecycle fixtures for CI/tests_v2: create, delete, stale cleanup.
"""
import logging
import os
import time
import uuid
from datetime import datetime
import pytest
from kubernetes import client
from kubernetes.client.rest import ApiException
logger = logging.getLogger(__name__)
STALE_NS_AGE_MINUTES = 30
def _namespace_age_minutes(metadata) -> float:
"""Return age of namespace in minutes from its creation_timestamp."""
if not metadata or not metadata.creation_timestamp:
return 0.0
created = metadata.creation_timestamp
if hasattr(created, "timestamp"):
created_ts = created.timestamp()
else:
try:
dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
created_ts = dt.timestamp()
except Exception:
return 0.0
return (time.time() - created_ts) / 60.0
def _wait_for_namespace_gone(k8s_core, name: str, timeout: int = 60):
"""Poll until the namespace no longer exists."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
k8s_core.read_namespace(name=name)
except ApiException as e:
if e.status == 404:
return
raise
time.sleep(1)
raise TimeoutError(f"Namespace {name} did not disappear within {timeout}s")
@pytest.fixture(scope="function")
def test_namespace(request, k8s_core):
"""
Create an ephemeral namespace for the test. Deleted after the test unless
--keep-ns-on-fail is set and the test failed.
"""
name = f"krkn-test-{uuid.uuid4().hex[:8]}"
ns = client.V1Namespace(
metadata=client.V1ObjectMeta(
name=name,
labels={
"pod-security.kubernetes.io/audit": "privileged",
"pod-security.kubernetes.io/enforce": "privileged",
"pod-security.kubernetes.io/enforce-version": "v1.24",
"pod-security.kubernetes.io/warn": "privileged",
"security.openshift.io/scc.podSecurityLabelSync": "false",
},
)
)
k8s_core.create_namespace(body=ns)
logger.info("Created test namespace: %s", name)
yield name
keep_on_fail = request.config.getoption("--keep-ns-on-fail", False)
rep_call = getattr(request.node, "rep_call", None)
failed = rep_call is not None and rep_call.failed
if keep_on_fail and failed:
logger.info("[keep-ns-on-fail] Keeping namespace %s for debugging", name)
return
try:
k8s_core.delete_namespace(
name=name,
body=client.V1DeleteOptions(propagation_policy="Background"),
)
logger.debug("Scheduled background deletion for namespace: %s", name)
except Exception as e:
logger.warning("Failed to delete namespace %s: %s", name, e)
@pytest.fixture(scope="session", autouse=True)
def _cleanup_stale_namespaces(k8s_core):
"""Delete krkn-test-* namespaces older than STALE_NS_AGE_MINUTES at session start."""
if os.environ.get("PYTEST_XDIST_WORKER"):
return
try:
namespaces = k8s_core.list_namespace()
except Exception as e:
logger.warning("Could not list namespaces for stale cleanup: %s", e)
return
for ns in namespaces.items or []:
name = ns.metadata.name if ns.metadata else ""
if not name.startswith("krkn-test-"):
continue
if _namespace_age_minutes(ns.metadata) <= STALE_NS_AGE_MINUTES:
continue
try:
logger.warning("Deleting stale namespace: %s", name)
k8s_core.delete_namespace(
name=name,
body=client.V1DeleteOptions(propagation_policy="Background"),
)
except Exception as e:
logger.warning("Failed to delete stale namespace %s: %s", name, e)