feat(cli): meet build/seed support, production kube tunnel, gitea OIDC bootstrap

- secrets.py: seed secret/meet (django-secret-key, application-jwt-secret-key)
- images.py: add sunbeam build meet (meet-backend + meet-frontend from source)
- kube.py: production SSH tunnel support, domain discovery from cluster, cmd_bao
- gitea.py: configure Hydra as OIDC auth source; mark admin account as private
- services.py: minor VSO sync status and services list fixes
- users.py: add cmd_user_enable
This commit is contained in:
2026-03-06 12:05:10 +00:00
parent c759f2c014
commit 2569978f47
6 changed files with 750 additions and 206 deletions

View File

@@ -1,9 +1,56 @@
"""Kubernetes interface — kubectl/kustomize wrappers, domain substitution, target parsing."""
import subprocess
import time
from contextlib import contextmanager
from pathlib import Path
from sunbeam.tools import run_tool, CACHE_DIR
from sunbeam.output import die
from sunbeam.output import die, ok
# Active kubectl context. Set once at startup via set_context().
# Defaults to "sunbeam" (Lima VM) for local dev.
_context: str = "sunbeam"
# SSH host for production tunnel. Set alongside context for production env.
_ssh_host: str = ""
_tunnel_proc: subprocess.Popen | None = None
def set_context(ctx: str, ssh_host: str = "") -> None:
global _context, _ssh_host
_context = ctx
_ssh_host = ssh_host
def context_arg() -> str:
"""Return '--context=<active>' for use in subprocess command lists."""
return f"--context={_context}"
def ensure_tunnel() -> None:
"""Open SSH tunnel to localhost:16443 → remote:6443 for production if needed."""
global _tunnel_proc
if not _ssh_host:
return
import socket
try:
with socket.create_connection(("127.0.0.1", 16443), timeout=0.5):
return # already open
except (ConnectionRefusedError, TimeoutError, OSError):
pass
ok(f"Opening SSH tunnel to {_ssh_host}...")
_tunnel_proc = subprocess.Popen(
["ssh", "-p", "2222", "-L", "16443:127.0.0.1:6443", "-N", "-o", "ExitOnForwardFailure=yes",
"-o", "StrictHostKeyChecking=no", _ssh_host],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
for _ in range(10):
try:
with socket.create_connection(("127.0.0.1", 16443), timeout=0.5):
return
except (ConnectionRefusedError, TimeoutError, OSError):
time.sleep(0.5)
die(f"SSH tunnel to {_ssh_host} did not open in time")
def parse_target(s: str | None) -> tuple[str | None, str | None]:
@@ -42,23 +89,26 @@ def get_lima_ip() -> str:
def kube(*args, input=None, check=True) -> subprocess.CompletedProcess:
"""Run kubectl with --context=sunbeam."""
"""Run kubectl against the active context, opening SSH tunnel if needed."""
ensure_tunnel()
text = not isinstance(input, bytes)
return run_tool("kubectl", "--context=sunbeam", *args,
return run_tool("kubectl", context_arg(), *args,
input=input, text=text, check=check,
capture_output=False)
def kube_out(*args) -> str:
"""Run kubectl and return stdout (empty string on failure)."""
r = run_tool("kubectl", "--context=sunbeam", *args,
ensure_tunnel()
r = run_tool("kubectl", context_arg(), *args,
capture_output=True, text=True, check=False)
return r.stdout.strip() if r.returncode == 0 else ""
def kube_ok(*args) -> bool:
"""Return True if kubectl command exits 0."""
r = run_tool("kubectl", "--context=sunbeam", *args,
ensure_tunnel()
r = run_tool("kubectl", context_arg(), *args,
capture_output=True, check=False)
return r.returncode == 0
@@ -95,7 +145,7 @@ def create_secret(ns: str, name: str, **literals) -> None:
def kube_exec(ns: str, pod: str, *cmd: str, container: str | None = None) -> tuple[int, str]:
"""Run a command inside a pod. Returns (returncode, stdout)."""
args = ["kubectl", "--context=sunbeam", "exec", "-n", ns, pod]
args = ["kubectl", context_arg(), "exec", "-n", ns, pod]
if container:
args += ["-c", container]
args += ["--", *cmd]
@@ -106,22 +156,42 @@ def kube_exec(ns: str, pod: str, *cmd: str, container: str | None = None) -> tup
def get_domain() -> str:
"""Discover the active domain from cluster state.
Reads a known substituted configmap value; falls back to the Lima VM IP.
Tries multiple reliable anchors; falls back to the Lima VM IP for local dev.
"""
raw = kube_out("get", "configmap", "lasuite-oidc-provider", "-n", "lasuite",
"-o=jsonpath={.data.OIDC_OP_JWKS_ENDPOINT}")
if raw and "https://auth." in raw:
# e.g. "https://auth.192.168.105.2.sslip.io/.well-known/jwks.json"
return raw.split("https://auth.")[1].split("/")[0]
import base64
# 1. Gitea inline-config secret: server section contains DOMAIN=src.<domain>
# Works in both local and production because DOMAIN_SUFFIX is substituted
# into gitea-values.yaml at apply time.
raw = kube_out("get", "secret", "gitea-inline-config", "-n", "devtools",
"-o=jsonpath={.data.server}", "--ignore-not-found")
if raw:
try:
server_ini = base64.b64decode(raw).decode()
for line in server_ini.splitlines():
if line.startswith("DOMAIN=src."):
# e.g. "DOMAIN=src.sunbeam.pt"
return line.split("DOMAIN=src.", 1)[1].strip()
except Exception:
pass
# 2. Fallback: lasuite-oidc-provider configmap (works if La Suite is deployed)
raw2 = kube_out("get", "configmap", "lasuite-oidc-provider", "-n", "lasuite",
"-o=jsonpath={.data.OIDC_OP_JWKS_ENDPOINT}", "--ignore-not-found")
if raw2 and "https://auth." in raw2:
return raw2.split("https://auth.")[1].split("/")[0]
# 3. Local dev fallback
ip = get_lima_ip()
return f"{ip}.sslip.io"
def cmd_k8s(kubectl_args: list[str]) -> int:
"""Transparent kubectl --context=sunbeam passthrough. Returns kubectl's exit code."""
"""Transparent kubectl passthrough for the active context."""
ensure_tunnel()
from sunbeam.tools import ensure_tool
bin_path = ensure_tool("kubectl")
r = subprocess.run([str(bin_path), "--context=sunbeam", *kubectl_args])
r = subprocess.run([str(bin_path), context_arg(), *kubectl_args])
return r.returncode
@@ -149,19 +219,21 @@ def cmd_bao(bao_args: list[str]) -> int:
cmd_str = "VAULT_TOKEN=" + root_token + " bao " + " ".join(bao_args)
r = subprocess.run(
["kubectl", "--context=sunbeam", "-n", "data", "exec", ob_pod,
["kubectl", context_arg(), "-n", "data", "exec", ob_pod,
"-c", "openbao", "--", "sh", "-c", cmd_str]
)
return r.returncode
def kustomize_build(overlay: Path, domain: str) -> str:
"""Run kustomize build --enable-helm and apply domain substitution."""
def kustomize_build(overlay: Path, domain: str, email: str = "") -> str:
"""Run kustomize build --enable-helm and apply domain/email substitution."""
r = run_tool(
"kustomize", "build", "--enable-helm", str(overlay),
capture_output=True, text=True, check=True,
)
text = r.stdout
text = domain_replace(text, domain)
if email:
text = text.replace("ACME_EMAIL", email)
text = text.replace("\n annotations: null", "")
return text