- Add matrix to MANAGED_NS and tuwunel to restart/build targets
- Add post-apply hooks for matrix namespace:
- _patch_tuwunel_oauth2_redirect: reads client_id from hydra-maester
Secret and patches OAuth2Client redirectUris dynamically
- _inject_opensearch_model_id: reads model_id from ingest pipeline
and writes to ConfigMap for tuwunel deployment env var injection
- Add post-apply hook for data namespace:
- _ensure_opensearch_ml: idempotently registers/deploys all-mpnet-base-v2
(768-dim) model, creates ingest + hybrid search pipelines
- Add tuwunel secrets to OpenBao seed (OIDC, TURN, registration token)
- Refactor secret seeding to only write dirty paths (avoid VSO churn)
- Add ACME email fallback from config when not provided via CLI flag
237 lines
7.5 KiB
Python
237 lines
7.5 KiB
Python
"""Service management — status, logs, restart."""
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import sunbeam.kube as _kube_mod
|
|
from sunbeam.kube import kube, kube_out, parse_target
|
|
from sunbeam.tools import ensure_tool
|
|
from sunbeam.output import step, ok, warn, die
|
|
|
|
MANAGED_NS = ["data", "devtools", "ingress", "lasuite", "matrix", "media", "ory",
|
|
"storage", "vault-secrets-operator"]
|
|
|
|
SERVICES_TO_RESTART = [
|
|
("ory", "hydra"),
|
|
("ory", "kratos"),
|
|
("ory", "login-ui"),
|
|
("devtools", "gitea"),
|
|
("storage", "seaweedfs-filer"),
|
|
("lasuite", "hive"),
|
|
("lasuite", "people-backend"),
|
|
("lasuite", "people-frontend"),
|
|
("lasuite", "people-celery-worker"),
|
|
("lasuite", "people-celery-beat"),
|
|
("matrix", "tuwunel"),
|
|
("media", "livekit-server"),
|
|
]
|
|
|
|
|
|
def _k8s_ctx():
|
|
"""Return the kubectl --context flag matching the active environment."""
|
|
return [_kube_mod.context_arg()]
|
|
|
|
|
|
def _capture_out(cmd, *, default=""):
|
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
return r.stdout.strip() if r.returncode == 0 else default
|
|
|
|
|
|
def _vso_sync_status():
|
|
"""Print VSO VaultStaticSecret and VaultDynamicSecret sync health.
|
|
|
|
VSS synced = status.secretMAC is non-empty.
|
|
VDS synced = status.lastRenewalTime is non-zero.
|
|
"""
|
|
step("VSO secret sync status...")
|
|
all_ok = True
|
|
|
|
# VaultStaticSecrets: synced when secretMAC is populated
|
|
vss_raw = _capture_out([
|
|
"kubectl", *_k8s_ctx(), "get", "vaultstaticsecret", "-A", "--no-headers",
|
|
"-o=custom-columns="
|
|
"NS:.metadata.namespace,NAME:.metadata.name,MAC:.status.secretMAC",
|
|
])
|
|
cur_ns = None
|
|
for line in sorted(vss_raw.splitlines()):
|
|
cols = line.split()
|
|
if len(cols) < 2:
|
|
continue
|
|
ns, name = cols[0], cols[1]
|
|
mac = cols[2] if len(cols) > 2 else ""
|
|
synced = bool(mac and mac != "<none>")
|
|
if not synced:
|
|
all_ok = False
|
|
icon = "\u2713" if synced else "\u2717"
|
|
if ns != cur_ns:
|
|
print(f" {ns} (VSS):")
|
|
cur_ns = ns
|
|
print(f" {icon} {name}")
|
|
|
|
# VaultDynamicSecrets: synced when lastRenewalTime is non-zero
|
|
vds_raw = _capture_out([
|
|
"kubectl", *_k8s_ctx(), "get", "vaultdynamicsecret", "-A", "--no-headers",
|
|
"-o=custom-columns="
|
|
"NS:.metadata.namespace,NAME:.metadata.name,RENEWED:.status.lastRenewalTime",
|
|
])
|
|
cur_ns = None
|
|
for line in sorted(vds_raw.splitlines()):
|
|
cols = line.split()
|
|
if len(cols) < 2:
|
|
continue
|
|
ns, name = cols[0], cols[1]
|
|
renewed = cols[2] if len(cols) > 2 else "0"
|
|
synced = renewed not in ("", "0", "<none>")
|
|
if not synced:
|
|
all_ok = False
|
|
icon = "\u2713" if synced else "\u2717"
|
|
if ns != cur_ns:
|
|
print(f" {ns} (VDS):")
|
|
cur_ns = ns
|
|
print(f" {icon} {name}")
|
|
|
|
print()
|
|
if all_ok:
|
|
ok("All VSO secrets synced.")
|
|
else:
|
|
warn("Some VSO secrets are not synced.")
|
|
|
|
|
|
def cmd_status(target: str | None):
|
|
"""Show pod health, optionally filtered by namespace or namespace/service."""
|
|
step("Pod health across all namespaces...")
|
|
|
|
ns_set = set(MANAGED_NS)
|
|
|
|
if target is None:
|
|
# All pods across managed namespaces
|
|
raw = _capture_out([
|
|
"kubectl", *_k8s_ctx(),
|
|
"get", "pods",
|
|
"--field-selector=metadata.namespace!= kube-system",
|
|
"-A", "--no-headers",
|
|
])
|
|
pods = []
|
|
for line in raw.splitlines():
|
|
cols = line.split()
|
|
if len(cols) < 4:
|
|
continue
|
|
ns = cols[0]
|
|
if ns not in ns_set:
|
|
continue
|
|
pods.append(cols)
|
|
else:
|
|
ns, name = parse_target(target)
|
|
if name:
|
|
# Specific service: namespace/service
|
|
raw = _capture_out([
|
|
"kubectl", *_k8s_ctx(),
|
|
"get", "pods", "-n", ns, "-l", f"app={name}", "--no-headers",
|
|
])
|
|
pods = []
|
|
for line in raw.splitlines():
|
|
cols = line.split()
|
|
if len(cols) < 3:
|
|
continue
|
|
# Prepend namespace since -n output doesn't include it
|
|
pods.append([ns] + cols)
|
|
else:
|
|
# Namespace only
|
|
raw = _capture_out([
|
|
"kubectl", *_k8s_ctx(),
|
|
"get", "pods", "-n", ns, "--no-headers",
|
|
])
|
|
pods = []
|
|
for line in raw.splitlines():
|
|
cols = line.split()
|
|
if len(cols) < 3:
|
|
continue
|
|
pods.append([ns] + cols)
|
|
|
|
if not pods:
|
|
warn("No pods found in managed namespaces.")
|
|
return
|
|
|
|
all_ok = True
|
|
cur_ns = None
|
|
icon_map = {"Running": "\u2713", "Completed": "\u2713", "Succeeded": "\u2713",
|
|
"Pending": "\u25cb", "Failed": "\u2717", "Unknown": "?"}
|
|
for cols in sorted(pods, key=lambda c: (c[0], c[1])):
|
|
ns, name, ready, status = cols[0], cols[1], cols[2], cols[3]
|
|
if ns != cur_ns:
|
|
print(f" {ns}:")
|
|
cur_ns = ns
|
|
icon = icon_map.get(status, "?")
|
|
unhealthy = status not in ("Running", "Completed", "Succeeded")
|
|
# Only check ready ratio for Running pods — Completed/Succeeded pods
|
|
# legitimately report 0/N containers ready.
|
|
if not unhealthy and status == "Running" and "/" in ready:
|
|
r, t = ready.split("/")
|
|
unhealthy = r != t
|
|
if unhealthy:
|
|
all_ok = False
|
|
print(f" {icon} {name:<50} {ready:<6} {status}")
|
|
|
|
print()
|
|
if all_ok:
|
|
ok("All pods healthy.")
|
|
else:
|
|
warn("Some pods are not ready.")
|
|
|
|
_vso_sync_status()
|
|
|
|
|
|
def cmd_logs(target: str, follow: bool):
|
|
"""Stream logs for a service. Target must include service name (e.g. ory/kratos)."""
|
|
ns, name = parse_target(target)
|
|
if not name:
|
|
die("Logs require a service name, e.g. 'ory/kratos'.")
|
|
|
|
_kube_mod.ensure_tunnel()
|
|
kubectl = str(ensure_tool("kubectl"))
|
|
cmd = [kubectl, _kube_mod.context_arg(), "-n", ns, "logs",
|
|
"-l", f"app={name}", "--tail=100"]
|
|
if follow:
|
|
cmd.append("--follow")
|
|
|
|
proc = subprocess.Popen(cmd)
|
|
proc.wait()
|
|
|
|
|
|
def cmd_get(target: str, output: str = "yaml"):
|
|
"""Print raw kubectl get output for a pod or resource (ns/name).
|
|
|
|
Usage: sunbeam get vault-secrets-operator/vault-secrets-operator-test
|
|
sunbeam get ory/kratos-abc -o json
|
|
"""
|
|
ns, name = parse_target(target)
|
|
if not ns or not name:
|
|
die("get requires namespace/name, e.g. 'sunbeam get ory/kratos-abc'")
|
|
# Try pod first, fall back to any resource type if caller passes kind/ns/name
|
|
result = kube_out("get", "pod", name, "-n", ns, f"-o={output}")
|
|
if not result:
|
|
die(f"Pod {ns}/{name} not found.")
|
|
print(result)
|
|
|
|
|
|
def cmd_restart(target: str | None):
|
|
"""Restart deployments. None=all, 'ory'=namespace, 'ory/kratos'=specific."""
|
|
step("Restarting services...")
|
|
|
|
if target is None:
|
|
matched = SERVICES_TO_RESTART
|
|
else:
|
|
ns, name = parse_target(target)
|
|
if name:
|
|
matched = [(n, d) for n, d in SERVICES_TO_RESTART if n == ns and d == name]
|
|
else:
|
|
matched = [(n, d) for n, d in SERVICES_TO_RESTART if n == ns]
|
|
|
|
if not matched:
|
|
warn(f"No matching services for target: {target}")
|
|
return
|
|
|
|
for ns, dep in matched:
|
|
kube("-n", ns, "rollout", "restart", f"deployment/{dep}", check=False)
|
|
ok("Done.")
|