184 lines
6.2 KiB
Python
184 lines
6.2 KiB
Python
"""Reconcile OpenSandbox Pod UIDs to narrowly scoped SPIRE entries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from aiohttp import ClientResponseError
|
|
|
|
from .sandbox_kubernetes import SandboxKubernetesClient, allocated_pod_name
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
KUBERNETES_API = os.environ.get("KUBERNETES_API", "https://kubernetes.default.svc")
|
|
KUBERNETES_TOKEN_FILE = Path(
|
|
os.environ.get(
|
|
"KUBERNETES_TOKEN_FILE",
|
|
"/var/run/secrets/kubernetes.io/serviceaccount/token",
|
|
)
|
|
)
|
|
KUBERNETES_CA_FILE = Path(
|
|
os.environ.get(
|
|
"KUBERNETES_CA_FILE",
|
|
"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
|
|
)
|
|
)
|
|
NAMESPACE = os.environ.get("OPENSANDBOX_NAMESPACE", "opensandbox")
|
|
SPIFFE_TRUST_DOMAIN = os.environ.get("SPIFFE_TRUST_DOMAIN", "ddupan.top")
|
|
SPIRE_CLUSTER_NAME = os.environ.get("SPIRE_CLUSTER_NAME", "sandbox-kata")
|
|
SPIRE_CLASS_NAME = os.environ.get("SPIRE_CLASS_NAME", "spire-mgmt-spire")
|
|
RUNNER_UID = int(os.environ.get("RUNNER_UID", "2000"))
|
|
RECONCILE_INTERVAL = int(os.environ.get("RECONCILE_INTERVAL", "2"))
|
|
|
|
|
|
def entry_name(sandbox_id: str) -> str:
|
|
suffix = hashlib.sha256(sandbox_id.encode()).hexdigest()[:12]
|
|
return f"gitea-ci-{suffix}"
|
|
|
|
|
|
def sandbox_name(document: dict[str, object]) -> str | None:
|
|
metadata = document.get("metadata")
|
|
value = metadata.get("name") if isinstance(metadata, dict) else None
|
|
return value if isinstance(value, str) and value else None
|
|
|
|
|
|
def is_runner_sandbox(document: dict[str, object]) -> bool:
|
|
metadata = document.get("metadata")
|
|
labels = metadata.get("labels") if isinstance(metadata, dict) else None
|
|
return isinstance(labels, dict) and labels.get("ci.ddupan.top/runner") == "true"
|
|
|
|
|
|
def task_environment(document: dict[str, object]) -> dict[str, str]:
|
|
spec = document.get("spec")
|
|
task = spec.get("taskTemplate") if isinstance(spec, dict) else None
|
|
task_spec = task.get("spec") if isinstance(task, dict) else None
|
|
process = task_spec.get("process") if isinstance(task_spec, dict) else None
|
|
values = process.get("env") if isinstance(process, dict) else None
|
|
result: dict[str, str] = {}
|
|
if not isinstance(values, list):
|
|
return result
|
|
for item in values:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
name, value = item.get("name"), item.get("value")
|
|
if isinstance(name, str) and isinstance(value, str):
|
|
result[name] = value
|
|
return result
|
|
|
|
|
|
def identity_entry(
|
|
*, sandbox_id: str, pod_uid: str, spiffe_id: str
|
|
) -> dict[str, object]:
|
|
expected_prefix = f"spiffe://{SPIFFE_TRUST_DOMAIN}/ci/"
|
|
if not spiffe_id.startswith(expected_prefix):
|
|
raise ValueError("runner SPIFFE ID is outside the CI namespace")
|
|
return {
|
|
"apiVersion": "spire.spiffe.io/v1alpha1",
|
|
"kind": "ClusterStaticEntry",
|
|
"metadata": {
|
|
"name": entry_name(sandbox_id),
|
|
"labels": {
|
|
"app.kubernetes.io/name": "gitea-dynamic-runner",
|
|
"app.kubernetes.io/component": "opensandbox-identity",
|
|
"ci.ddupan.top/sandbox-id": sandbox_id,
|
|
},
|
|
},
|
|
"spec": {
|
|
"className": SPIRE_CLASS_NAME,
|
|
"parentID": (
|
|
f"spiffe://{SPIFFE_TRUST_DOMAIN}/spire/agent/k8s_psat/"
|
|
f"{SPIRE_CLUSTER_NAME}/pod/{pod_uid}"
|
|
),
|
|
"spiffeID": spiffe_id,
|
|
"selectors": [f"unix:uid:{RUNNER_UID}"],
|
|
},
|
|
}
|
|
|
|
|
|
async def reconcile(client: SandboxKubernetesClient) -> None:
|
|
sandboxes = await client.list_batchsandboxes()
|
|
live_names = {
|
|
name
|
|
for document in sandboxes
|
|
if is_runner_sandbox(document) and (name := sandbox_name(document))
|
|
}
|
|
entries = await client.list_entries()
|
|
existing = {
|
|
name
|
|
for document in entries
|
|
if (name := sandbox_name(document)) is not None
|
|
}
|
|
|
|
for document in sandboxes:
|
|
if not is_runner_sandbox(document):
|
|
continue
|
|
name = sandbox_name(document)
|
|
pod_name = allocated_pod_name(document)
|
|
spiffe_id = task_environment(document).get("CI_SPIFFE_ID")
|
|
if not name or not pod_name or not spiffe_id or entry_name(name) in existing:
|
|
continue
|
|
pod = await client.get_pod(pod_name)
|
|
metadata = pod.get("metadata") if isinstance(pod, dict) else None
|
|
pod_uid = metadata.get("uid") if isinstance(metadata, dict) else None
|
|
if not isinstance(pod_uid, str) or not pod_uid:
|
|
continue
|
|
try:
|
|
await client.create_entry(
|
|
identity_entry(
|
|
sandbox_id=name,
|
|
pod_uid=pod_uid,
|
|
spiffe_id=spiffe_id,
|
|
)
|
|
)
|
|
except ClientResponseError as error:
|
|
if error.status != 409:
|
|
raise
|
|
LOG.info(
|
|
"SPIRE entry ready sandbox=%s pod=%s pod_uid=%s spiffe_id=%s",
|
|
name,
|
|
pod_name,
|
|
pod_uid,
|
|
spiffe_id,
|
|
)
|
|
|
|
for document in entries:
|
|
metadata = document.get("metadata")
|
|
labels = metadata.get("labels") if isinstance(metadata, dict) else None
|
|
sandbox_id = labels.get("ci.ddupan.top/sandbox-id") if isinstance(labels, dict) else None
|
|
name = metadata.get("name") if isinstance(metadata, dict) else None
|
|
if (
|
|
isinstance(name, str)
|
|
and isinstance(sandbox_id, str)
|
|
and sandbox_id not in live_names
|
|
):
|
|
await client.delete_entry(name)
|
|
LOG.info("removed stale SPIRE entry=%s sandbox=%s", name, sandbox_id)
|
|
|
|
|
|
async def main() -> None:
|
|
async with SandboxKubernetesClient(
|
|
api_url=KUBERNETES_API,
|
|
token_file=KUBERNETES_TOKEN_FILE,
|
|
ca_file=KUBERNETES_CA_FILE,
|
|
namespace=NAMESPACE,
|
|
) as client:
|
|
while True:
|
|
try:
|
|
await reconcile(client)
|
|
except Exception:
|
|
LOG.exception("OpenSandbox identity reconcile failed")
|
|
await asyncio.sleep(RECONCILE_INTERVAL)
|
|
|
|
|
|
def cli() -> None:
|
|
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))
|
|
asyncio.run(main())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|