scripts: rename local-up.py → sunbeam.py; add Gitea bootstrap + registry mirroring
- Rename local-up.py → sunbeam.py; update docstring and argparser description
- Add setup_lima_vm_registry(): installs mkcert root CA into Lima VM system trust
store and writes k3s registries.yaml (Gitea auth); restarts k3s if changed
- Add bootstrap_gitea(): waits for pod Running+Ready, sets admin password via
gitea CLI, clears must_change_password via Postgres UPDATE (Gitea enforces
this flag at API level regardless of auth method), creates studio/internal orgs
- Add mirror_amd64_images(): pulls amd64-only images, patches OCI index with an
arm64 alias pointing at the same manifest (Rosetta runs it transparently),
imports patched image into k3s containerd, pushes to Gitea container registry
- Add AMD64_ONLY_IMAGES list (currently: lasuite/people-{backend,frontend})
- Add --gitea partial flag: registry trust + Gitea bootstrap + mirror
- Add --status flag: pod health table across all managed namespaces
- Fix create_secret to use --field-manager=sunbeam so kustomize apply (manager
kubectl) never wipes data fields written by the seed script
- Add people-frontend to SERVICES_TO_RESTART (was missing)
This commit is contained in:
@@ -1,551 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
local-up.py — Sunbeam local dev stack lifecycle manager.
|
|
||||||
|
|
||||||
Idempotent: safe to run from any state (fresh Mac, existing VM, partial deploy).
|
|
||||||
Consolidates local-up.sh + local-seed-secrets.sh into one place.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
./scripts/local-up.py # full stack bring-up
|
|
||||||
./scripts/local-up.py --seed # re-seed secrets only (e.g. after adding a service)
|
|
||||||
./scripts/local-up.py --apply # re-apply manifests only (e.g. after a config change)
|
|
||||||
./scripts/local-up.py --restart # restart services only
|
|
||||||
|
|
||||||
Requires: limactl mkcert kubectl kustomize linkerd jq yq
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ── Paths ─────────────────────────────────────────────────────────────────────
|
|
||||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
||||||
REPO_ROOT = SCRIPT_DIR.parent
|
|
||||||
SECRETS_DIR = REPO_ROOT / "secrets" / "local"
|
|
||||||
|
|
||||||
# ── Config ────────────────────────────────────────────────────────────────────
|
|
||||||
LIMA_VM = "sunbeam"
|
|
||||||
K8S_CTX = ["--context=sunbeam"]
|
|
||||||
|
|
||||||
# Deterministic local-dev credentials (not for production)
|
|
||||||
DB_PASSWORD = "localdev"
|
|
||||||
S3_ACCESS_KEY = "minioadmin"
|
|
||||||
S3_SECRET_KEY = "minioadmin"
|
|
||||||
HYDRA_SYSTEM_SECRET = "local-hydra-system-secret-at-least-16"
|
|
||||||
HYDRA_COOKIE_SECRET = "local-hydra-cookie-secret-at-least-16"
|
|
||||||
HYDRA_PAIRWISE_SALT = "local-hydra-pairwise-salt-value-1"
|
|
||||||
LIVEKIT_API_KEY = "devkey"
|
|
||||||
LIVEKIT_API_SECRET = "secret-placeholder"
|
|
||||||
PEOPLE_DJANGO_SECRET = "local-dev-people-django-secret-key-not-for-production"
|
|
||||||
|
|
||||||
REQUIRED_TOOLS = ["limactl", "mkcert", "kubectl", "kustomize", "linkerd", "jq", "yq"]
|
|
||||||
|
|
||||||
PG_USERS = [
|
|
||||||
"kratos", "hydra", "gitea", "hive",
|
|
||||||
"docs", "meet", "drive", "messages", "conversations",
|
|
||||||
"people", "find",
|
|
||||||
]
|
|
||||||
|
|
||||||
SERVICES_TO_RESTART = [
|
|
||||||
("ory", "hydra"),
|
|
||||||
("ory", "kratos"),
|
|
||||||
("ory", "login-ui"),
|
|
||||||
("devtools", "gitea"),
|
|
||||||
("storage", "seaweedfs-filer"),
|
|
||||||
("lasuite", "hive"),
|
|
||||||
("lasuite", "people-backend"),
|
|
||||||
("lasuite", "people-celery-worker"),
|
|
||||||
("lasuite", "people-celery-beat"),
|
|
||||||
("media", "livekit-server"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# ── Output ────────────────────────────────────────────────────────────────────
|
|
||||||
def step(msg: str) -> None:
|
|
||||||
print(f"\n==> {msg}", flush=True)
|
|
||||||
|
|
||||||
def ok(msg: str) -> None:
|
|
||||||
print(f" {msg}", flush=True)
|
|
||||||
|
|
||||||
def warn(msg: str) -> None:
|
|
||||||
print(f" WARN: {msg}", file=sys.stderr, flush=True)
|
|
||||||
|
|
||||||
def die(msg: str) -> None:
|
|
||||||
print(f"\nERROR: {msg}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# ── Subprocess helpers ────────────────────────────────────────────────────────
|
|
||||||
def run(cmd: list, *, check: bool = True, input: str | None = None,
|
|
||||||
capture: bool = False, cwd: Path | None = None) -> subprocess.CompletedProcess:
|
|
||||||
return subprocess.run(
|
|
||||||
cmd, check=check, text=True, input=input,
|
|
||||||
capture_output=capture, cwd=cwd,
|
|
||||||
)
|
|
||||||
|
|
||||||
def capture(cmd: list, *, default: str = "") -> str:
|
|
||||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
return r.stdout.strip() if r.returncode == 0 else default
|
|
||||||
|
|
||||||
def succeeds(cmd: list) -> bool:
|
|
||||||
return subprocess.run(cmd, capture_output=True).returncode == 0
|
|
||||||
|
|
||||||
# ── kubectl wrappers ──────────────────────────────────────────────────────────
|
|
||||||
def kube(*args, input: str | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
|
||||||
return run(["kubectl", *K8S_CTX, *args], input=input, check=check)
|
|
||||||
|
|
||||||
def kube_out(*args) -> str:
|
|
||||||
return capture(["kubectl", *K8S_CTX, *args])
|
|
||||||
|
|
||||||
def kube_ok(*args) -> bool:
|
|
||||||
return succeeds(["kubectl", *K8S_CTX, *args])
|
|
||||||
|
|
||||||
def kube_apply(manifest: str, *, server_side: bool = True) -> None:
|
|
||||||
args = ["apply", "-f", "-"]
|
|
||||||
if server_side:
|
|
||||||
args += ["--server-side", "--force-conflicts"]
|
|
||||||
kube(*args, input=manifest)
|
|
||||||
|
|
||||||
def ns_exists(ns: str) -> bool:
|
|
||||||
return kube_ok("get", "namespace", ns)
|
|
||||||
|
|
||||||
def ensure_ns(ns: str) -> None:
|
|
||||||
manifest = kube_out("create", "namespace", ns, "--dry-run=client", "-o=yaml")
|
|
||||||
if manifest:
|
|
||||||
kube_apply(manifest)
|
|
||||||
|
|
||||||
def create_secret(ns: str, name: str, **literals) -> None:
|
|
||||||
"""Create or update a generic secret idempotently."""
|
|
||||||
args = ["create", "secret", "generic", name, f"-n={ns}"]
|
|
||||||
for k, v in literals.items():
|
|
||||||
args.append(f"--from-literal={k}={v}")
|
|
||||||
args += ["--dry-run=client", "-o=yaml"]
|
|
||||||
manifest = kube_out(*args)
|
|
||||||
if manifest:
|
|
||||||
kube("apply", "--server-side", "-f", "-", input=manifest)
|
|
||||||
|
|
||||||
# ── 1. Prerequisites ──────────────────────────────────────────────────────────
|
|
||||||
def check_prerequisites() -> None:
|
|
||||||
step("Checking prerequisites...")
|
|
||||||
missing = [t for t in REQUIRED_TOOLS if not shutil.which(t)]
|
|
||||||
if missing:
|
|
||||||
die(f"missing tools: {', '.join(missing)}\nInstall: brew install {' '.join(missing)}")
|
|
||||||
ok("All tools present.")
|
|
||||||
|
|
||||||
# ── 2. Lima VM ────────────────────────────────────────────────────────────────
|
|
||||||
def ensure_lima_vm() -> None:
|
|
||||||
step("Lima VM...")
|
|
||||||
status = _lima_status()
|
|
||||||
if status == "none":
|
|
||||||
ok("Creating 'sunbeam' (k3s 6 CPU / 12 GB / 60 GB)...")
|
|
||||||
run(["limactl", "start",
|
|
||||||
"--name=sunbeam", "template:k3s",
|
|
||||||
"--memory=12", "--cpus=6", "--disk=60",
|
|
||||||
"--vm-type=vz", "--mount-type=virtiofs"])
|
|
||||||
elif status == "Running":
|
|
||||||
ok("Already running.")
|
|
||||||
else:
|
|
||||||
ok(f"Starting (current status: {status})...")
|
|
||||||
run(["limactl", "start", LIMA_VM])
|
|
||||||
|
|
||||||
def _lima_status() -> str:
|
|
||||||
"""Return the Lima VM status, handling both JSON-array and NDJSON output."""
|
|
||||||
raw = capture(["limactl", "list", "--json"])
|
|
||||||
if not raw:
|
|
||||||
return "none"
|
|
||||||
vms: list = []
|
|
||||||
# Try JSON array first
|
|
||||||
try:
|
|
||||||
parsed = json.loads(raw)
|
|
||||||
vms = parsed if isinstance(parsed, list) else [parsed]
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# Fall back to NDJSON (one object per line)
|
|
||||||
for line in raw.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
vms.append(json.loads(line))
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
for vm in vms:
|
|
||||||
if vm.get("name") == LIMA_VM:
|
|
||||||
return vm.get("status", "unknown")
|
|
||||||
return "none"
|
|
||||||
|
|
||||||
# ── 3. Kubeconfig ─────────────────────────────────────────────────────────────
|
|
||||||
def merge_kubeconfig() -> None:
|
|
||||||
step("Merging kubeconfig...")
|
|
||||||
lima_kube = Path.home() / f".lima/{LIMA_VM}/copied-from-guest/kubeconfig.yaml"
|
|
||||||
if not lima_kube.exists():
|
|
||||||
die(f"Lima kubeconfig not found: {lima_kube}")
|
|
||||||
|
|
||||||
tmp = Path("/tmp/sunbeam-kube")
|
|
||||||
tmp.mkdir(exist_ok=True)
|
|
||||||
try:
|
|
||||||
for query, filename in [
|
|
||||||
(".clusters[0].cluster.certificate-authority-data", "ca.crt"),
|
|
||||||
(".users[0].user.client-certificate-data", "client.crt"),
|
|
||||||
(".users[0].user.client-key-data", "client.key"),
|
|
||||||
]:
|
|
||||||
b64 = capture(["yq", query, str(lima_kube)])
|
|
||||||
(tmp / filename).write_bytes(base64.b64decode(b64))
|
|
||||||
|
|
||||||
run(["kubectl", "config", "set-cluster", LIMA_VM,
|
|
||||||
"--server=https://127.0.0.1:6443",
|
|
||||||
f"--certificate-authority={tmp}/ca.crt", "--embed-certs=true"])
|
|
||||||
run(["kubectl", "config", "set-credentials", f"{LIMA_VM}-admin",
|
|
||||||
f"--client-certificate={tmp}/client.crt",
|
|
||||||
f"--client-key={tmp}/client.key", "--embed-certs=true"])
|
|
||||||
run(["kubectl", "config", "set-context", LIMA_VM,
|
|
||||||
f"--cluster={LIMA_VM}", f"--user={LIMA_VM}-admin"])
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
|
||||||
ok("Context 'sunbeam' ready.")
|
|
||||||
|
|
||||||
# ── 4. Traefik ────────────────────────────────────────────────────────────────
|
|
||||||
def disable_traefik() -> None:
|
|
||||||
step("Traefik...")
|
|
||||||
if kube_ok("get", "helmchart", "traefik", "-n", "kube-system"):
|
|
||||||
ok("Removing (replaced by Pingora)...")
|
|
||||||
kube("delete", "helmchart", "traefik", "traefik-crd",
|
|
||||||
"-n", "kube-system", check=False)
|
|
||||||
subprocess.run(
|
|
||||||
["limactl", "shell", LIMA_VM,
|
|
||||||
"sudo", "rm", "-f",
|
|
||||||
"/var/lib/rancher/k3s/server/manifests/traefik.yaml"],
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
ok("Done.")
|
|
||||||
|
|
||||||
# ── 5. cert-manager ───────────────────────────────────────────────────────────
|
|
||||||
def ensure_cert_manager() -> None:
|
|
||||||
step("cert-manager...")
|
|
||||||
if ns_exists("cert-manager"):
|
|
||||||
ok("Already installed.")
|
|
||||||
return
|
|
||||||
ok("Installing...")
|
|
||||||
kube("apply", "-f",
|
|
||||||
"https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml")
|
|
||||||
for dep in ["cert-manager", "cert-manager-webhook", "cert-manager-cainjector"]:
|
|
||||||
kube("rollout", "status", f"deployment/{dep}",
|
|
||||||
"-n", "cert-manager", "--timeout=120s")
|
|
||||||
ok("Installed.")
|
|
||||||
|
|
||||||
# ── 6. Linkerd ────────────────────────────────────────────────────────────────
|
|
||||||
def ensure_linkerd() -> None:
|
|
||||||
step("Linkerd...")
|
|
||||||
if ns_exists("linkerd"):
|
|
||||||
ok("Already installed.")
|
|
||||||
return
|
|
||||||
ok("Installing Gateway API CRDs...")
|
|
||||||
kube("apply", "--server-side", "-f",
|
|
||||||
"https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml")
|
|
||||||
ok("Installing Linkerd CRDs...")
|
|
||||||
crds = capture(["linkerd", "install", "--crds"])
|
|
||||||
kube_apply(crds)
|
|
||||||
ok("Installing Linkerd control plane...")
|
|
||||||
cp = capture(["linkerd", "install"])
|
|
||||||
kube_apply(cp)
|
|
||||||
for dep in ["linkerd-identity", "linkerd-destination", "linkerd-proxy-injector"]:
|
|
||||||
kube("rollout", "status", f"deployment/{dep}",
|
|
||||||
"-n", "linkerd", "--timeout=120s")
|
|
||||||
ok("Installed.")
|
|
||||||
|
|
||||||
# ── 7. TLS certificate ────────────────────────────────────────────────────────
|
|
||||||
def get_lima_ip() -> str:
|
|
||||||
raw = capture(["limactl", "shell", LIMA_VM,
|
|
||||||
"ip", "-4", "addr", "show", "eth1"])
|
|
||||||
for line in raw.splitlines():
|
|
||||||
if "inet " in line:
|
|
||||||
return line.strip().split()[1].split("/")[0]
|
|
||||||
# Fallback: first non-loopback IP
|
|
||||||
return capture(["limactl", "shell", LIMA_VM, "hostname", "-I"]).split()[0]
|
|
||||||
|
|
||||||
def ensure_tls_cert() -> str:
|
|
||||||
step("TLS certificate...")
|
|
||||||
ip = get_lima_ip()
|
|
||||||
domain = f"{ip}.sslip.io"
|
|
||||||
cert = SECRETS_DIR / "tls.crt"
|
|
||||||
if cert.exists():
|
|
||||||
ok(f"Cert exists. Domain: {domain}")
|
|
||||||
return domain
|
|
||||||
ok(f"Generating wildcard cert for *.{domain}...")
|
|
||||||
SECRETS_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
run(["mkcert", f"*.{domain}"], cwd=SECRETS_DIR)
|
|
||||||
for src, dst in [
|
|
||||||
(f"_wildcard.{domain}.pem", "tls.crt"),
|
|
||||||
(f"_wildcard.{domain}-key.pem", "tls.key"),
|
|
||||||
]:
|
|
||||||
(SECRETS_DIR / src).rename(SECRETS_DIR / dst)
|
|
||||||
ok(f"Cert generated. Domain: {domain}")
|
|
||||||
return domain
|
|
||||||
|
|
||||||
# ── 8. TLS secret ─────────────────────────────────────────────────────────────
|
|
||||||
def ensure_tls_secret(domain: str) -> None:
|
|
||||||
step("TLS secret...")
|
|
||||||
ensure_ns("ingress")
|
|
||||||
manifest = kube_out(
|
|
||||||
"create", "secret", "tls", "pingora-tls",
|
|
||||||
f"--cert={SECRETS_DIR}/tls.crt",
|
|
||||||
f"--key={SECRETS_DIR}/tls.key",
|
|
||||||
"-n", "ingress",
|
|
||||||
"--dry-run=client", "-o=yaml",
|
|
||||||
)
|
|
||||||
if manifest:
|
|
||||||
kube_apply(manifest)
|
|
||||||
ok("Done.")
|
|
||||||
|
|
||||||
# ── 9. Apply manifests ────────────────────────────────────────────────────────
|
|
||||||
def apply_manifests(domain: str) -> None:
|
|
||||||
step(f"Applying manifests (domain: {domain})...")
|
|
||||||
r = run(
|
|
||||||
["kustomize", "build", "--enable-helm", "overlays/local/"],
|
|
||||||
capture=True, cwd=REPO_ROOT,
|
|
||||||
)
|
|
||||||
manifests = r.stdout.replace("DOMAIN_SUFFIX", domain)
|
|
||||||
kube("apply", "--server-side", "--force-conflicts", "-f", "-", input=manifests)
|
|
||||||
ok("Applied.")
|
|
||||||
|
|
||||||
# ── 10. Seed secrets ──────────────────────────────────────────────────────────
|
|
||||||
def seed_secrets() -> None:
|
|
||||||
step("Seeding secrets...")
|
|
||||||
|
|
||||||
# ── Wait for postgres ─────────────────────────────────────────────────────
|
|
||||||
ok("Waiting for postgres cluster...")
|
|
||||||
pg_pod = ""
|
|
||||||
for _ in range(60):
|
|
||||||
phase = kube_out("-n", "data", "get", "cluster", "postgres",
|
|
||||||
"-o=jsonpath={.status.phase}")
|
|
||||||
if phase == "Cluster in healthy state":
|
|
||||||
pg_pod = kube_out("-n", "data", "get", "pods",
|
|
||||||
"-l=cnpg.io/cluster=postgres,role=primary",
|
|
||||||
"-o=jsonpath={.items[0].metadata.name}")
|
|
||||||
ok(f"Postgres ready ({pg_pod}).")
|
|
||||||
break
|
|
||||||
time.sleep(5)
|
|
||||||
else:
|
|
||||||
warn("Postgres not ready after 5 min — continuing anyway.")
|
|
||||||
|
|
||||||
# ── Set postgres passwords ────────────────────────────────────────────────
|
|
||||||
if pg_pod:
|
|
||||||
ok("Setting postgres user passwords...")
|
|
||||||
for user in PG_USERS:
|
|
||||||
kube("exec", "-n", "data", pg_pod, "-c", "postgres", "--",
|
|
||||||
"psql", "-U", "postgres", "-c",
|
|
||||||
f"ALTER USER {user} WITH PASSWORD '{DB_PASSWORD}';",
|
|
||||||
check=False)
|
|
||||||
|
|
||||||
# ── K8s secrets ───────────────────────────────────────────────────────────
|
|
||||||
ok("Creating K8s secrets...")
|
|
||||||
|
|
||||||
# Ory
|
|
||||||
ensure_ns("ory")
|
|
||||||
create_secret("ory", "hydra",
|
|
||||||
dsn=(f"postgresql://hydra:{DB_PASSWORD}@"
|
|
||||||
"postgres-rw.data.svc.cluster.local:5432/hydra_db?sslmode=disable"),
|
|
||||||
secretsSystem=HYDRA_SYSTEM_SECRET,
|
|
||||||
secretsCookie=HYDRA_COOKIE_SECRET,
|
|
||||||
**{"pairwise-salt": HYDRA_PAIRWISE_SALT},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Devtools
|
|
||||||
ensure_ns("devtools")
|
|
||||||
create_secret("devtools", "gitea-db-credentials", password=DB_PASSWORD)
|
|
||||||
create_secret("devtools", "gitea-s3-credentials",
|
|
||||||
**{"access-key": S3_ACCESS_KEY, "secret-key": S3_SECRET_KEY})
|
|
||||||
|
|
||||||
# Storage
|
|
||||||
ensure_ns("storage")
|
|
||||||
create_secret("storage", "seaweedfs-s3-credentials",
|
|
||||||
S3_ACCESS_KEY=S3_ACCESS_KEY, S3_SECRET_KEY=S3_SECRET_KEY)
|
|
||||||
|
|
||||||
# La Suite
|
|
||||||
ensure_ns("lasuite")
|
|
||||||
create_secret("lasuite", "seaweedfs-s3-credentials",
|
|
||||||
S3_ACCESS_KEY=S3_ACCESS_KEY, S3_SECRET_KEY=S3_SECRET_KEY)
|
|
||||||
create_secret("lasuite", "hive-db-url",
|
|
||||||
url=(f"postgresql://hive:{DB_PASSWORD}@"
|
|
||||||
"postgres-rw.data.svc.cluster.local:5432/hive_db"))
|
|
||||||
create_secret("lasuite", "hive-oidc",
|
|
||||||
**{"client-id": "hive-local", "client-secret": "hive-local-secret"})
|
|
||||||
create_secret("lasuite", "people-db-credentials", password=DB_PASSWORD)
|
|
||||||
create_secret("lasuite", "people-django-secret",
|
|
||||||
DJANGO_SECRET_KEY=PEOPLE_DJANGO_SECRET)
|
|
||||||
|
|
||||||
# Media
|
|
||||||
ensure_ns("media")
|
|
||||||
|
|
||||||
# ── OpenBao ───────────────────────────────────────────────────────────────
|
|
||||||
_seed_openbao()
|
|
||||||
|
|
||||||
ok("All secrets seeded.")
|
|
||||||
|
|
||||||
def _seed_openbao() -> None:
|
|
||||||
ob_pod = kube_out(
|
|
||||||
"-n", "data", "get", "pods",
|
|
||||||
"-l=app.kubernetes.io/name=openbao,component=server",
|
|
||||||
"-o=jsonpath={.items[0].metadata.name}",
|
|
||||||
)
|
|
||||||
if not ob_pod:
|
|
||||||
ok("OpenBao pod not found — skipping.")
|
|
||||||
return
|
|
||||||
|
|
||||||
ok(f"OpenBao ({ob_pod})...")
|
|
||||||
# Wait for pod to be Running (won't be Ready until unsealed)
|
|
||||||
kube("wait", "-n", "data", f"pod/{ob_pod}",
|
|
||||||
"--for=jsonpath={.status.phase}=Running", "--timeout=120s", check=False)
|
|
||||||
|
|
||||||
def bao(cmd: str) -> str:
|
|
||||||
r = kube_out("-n", "data", "exec", ob_pod, "-c", "openbao",
|
|
||||||
"--", "sh", "-c", cmd)
|
|
||||||
return r
|
|
||||||
|
|
||||||
status_json = bao("bao status -format=json 2>/dev/null || echo '{}'")
|
|
||||||
try:
|
|
||||||
status = json.loads(status_json)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
status = {}
|
|
||||||
|
|
||||||
unseal_key = ""
|
|
||||||
root_token = ""
|
|
||||||
|
|
||||||
if not status.get("initialized"):
|
|
||||||
ok("Initializing OpenBao...")
|
|
||||||
init_json = bao("bao operator init -key-shares=1 -key-threshold=1 -format=json 2>/dev/null")
|
|
||||||
try:
|
|
||||||
init = json.loads(init_json)
|
|
||||||
unseal_key = init["unseal_keys_b64"][0]
|
|
||||||
root_token = init["root_token"]
|
|
||||||
create_secret("data", "openbao-keys",
|
|
||||||
key=unseal_key, **{"root-token": root_token})
|
|
||||||
ok("Initialized — keys stored in secret/openbao-keys.")
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
warn(f"OpenBao init parse failed: {e}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
ok("Already initialized.")
|
|
||||||
unseal_key = kube_out("-n", "data", "get", "secret", "openbao-keys",
|
|
||||||
"-o=jsonpath={.data.key}")
|
|
||||||
if unseal_key:
|
|
||||||
unseal_key = base64.b64decode(unseal_key).decode()
|
|
||||||
root_token = kube_out("-n", "data", "get", "secret", "openbao-keys",
|
|
||||||
"-o=jsonpath={.data.root-token}")
|
|
||||||
if root_token:
|
|
||||||
root_token = base64.b64decode(root_token).decode()
|
|
||||||
|
|
||||||
# Unseal if sealed
|
|
||||||
try:
|
|
||||||
sealed = json.loads(bao("bao status -format=json 2>/dev/null || echo '{}'")).get("sealed", False)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
sealed = False
|
|
||||||
|
|
||||||
if sealed and unseal_key:
|
|
||||||
ok("Unsealing...")
|
|
||||||
bao(f"bao operator unseal '{unseal_key}' 2>/dev/null")
|
|
||||||
|
|
||||||
if root_token:
|
|
||||||
ok("Seeding KV...")
|
|
||||||
pg_rw = "postgres-rw.data.svc.cluster.local:5432"
|
|
||||||
bao(f"""
|
|
||||||
BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' sh -c '
|
|
||||||
bao secrets enable -path=secret -version=2 kv 2>/dev/null || true
|
|
||||||
bao kv put secret/postgres password="{DB_PASSWORD}"
|
|
||||||
bao kv put secret/hydra db-password="{DB_PASSWORD}" system-secret="{HYDRA_SYSTEM_SECRET}" cookie-secret="{HYDRA_COOKIE_SECRET}" pairwise-salt="{HYDRA_PAIRWISE_SALT}"
|
|
||||||
bao kv put secret/kratos db-password="{DB_PASSWORD}"
|
|
||||||
bao kv put secret/gitea db-password="{DB_PASSWORD}" s3-access-key="{S3_ACCESS_KEY}" s3-secret-key="{S3_SECRET_KEY}"
|
|
||||||
bao kv put secret/seaweedfs access-key="{S3_ACCESS_KEY}" secret-key="{S3_SECRET_KEY}"
|
|
||||||
bao kv put secret/hive db-url="postgresql://hive:{DB_PASSWORD}@{pg_rw}/hive_db" oidc-client-id="hive-local" oidc-client-secret="hive-local-secret"
|
|
||||||
bao kv put secret/livekit api-key="{LIVEKIT_API_KEY}" api-secret="{LIVEKIT_API_SECRET}"
|
|
||||||
bao kv put secret/people db-password="{DB_PASSWORD}" django-secret-key="{PEOPLE_DJANGO_SECRET}"
|
|
||||||
'
|
|
||||||
""")
|
|
||||||
|
|
||||||
# ── 11. Restart services ──────────────────────────────────────────────────────
|
|
||||||
def restart_services() -> None:
|
|
||||||
step("Restarting services waiting for secrets...")
|
|
||||||
for ns, dep in SERVICES_TO_RESTART:
|
|
||||||
kube("-n", ns, "rollout", "restart", f"deployment/{dep}", check=False)
|
|
||||||
ok("Done.")
|
|
||||||
|
|
||||||
# ── 12. Wait for core ─────────────────────────────────────────────────────────
|
|
||||||
def wait_for_core() -> None:
|
|
||||||
step("Waiting for core services...")
|
|
||||||
for ns, dep in [("data", "valkey"), ("ory", "kratos"), ("ory", "hydra")]:
|
|
||||||
kube("rollout", "status", f"deployment/{dep}",
|
|
||||||
"-n", ns, "--timeout=120s", check=False)
|
|
||||||
ok("Core services ready.")
|
|
||||||
|
|
||||||
# ── 13. Print URLs ────────────────────────────────────────────────────────────
|
|
||||||
def print_urls(domain: str) -> None:
|
|
||||||
print(f"\n{'─'*60}")
|
|
||||||
print(f" Stack is up. Domain: {domain}")
|
|
||||||
print(f"{'─'*60}")
|
|
||||||
services = [
|
|
||||||
("Auth", f"https://auth.{domain}/"),
|
|
||||||
("Docs", f"https://docs.{domain}/"),
|
|
||||||
("Meet", f"https://meet.{domain}/"),
|
|
||||||
("Drive", f"https://drive.{domain}/"),
|
|
||||||
("Chat", f"https://chat.{domain}/"),
|
|
||||||
("Mail", f"https://mail.{domain}/"),
|
|
||||||
("People", f"https://people.{domain}/"),
|
|
||||||
("Gitea", f"https://src.{domain}/"),
|
|
||||||
]
|
|
||||||
for name, url in services:
|
|
||||||
print(f" {name:<10} {url}")
|
|
||||||
print()
|
|
||||||
print(" OpenBao UI:")
|
|
||||||
print(f" kubectl --context=sunbeam -n data port-forward svc/openbao 8200:8200")
|
|
||||||
print(f" http://localhost:8200")
|
|
||||||
token_cmd = "kubectl --context=sunbeam -n data get secret openbao-keys -o jsonpath='{.data.root-token}' | base64 -d"
|
|
||||||
print(f" token: {token_cmd}")
|
|
||||||
print(f"{'─'*60}\n")
|
|
||||||
|
|
||||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="Sunbeam local dev stack manager")
|
|
||||||
parser.add_argument("--seed", action="store_true", help="Re-seed secrets only")
|
|
||||||
parser.add_argument("--apply", action="store_true", help="Re-apply manifests only")
|
|
||||||
parser.add_argument("--restart", action="store_true", help="Restart services only")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
check_prerequisites()
|
|
||||||
|
|
||||||
if args.seed:
|
|
||||||
seed_secrets()
|
|
||||||
restart_services()
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.restart:
|
|
||||||
restart_services()
|
|
||||||
return
|
|
||||||
|
|
||||||
domain = get_lima_ip()
|
|
||||||
domain = f"{domain}.sslip.io"
|
|
||||||
|
|
||||||
if args.apply:
|
|
||||||
apply_manifests(domain)
|
|
||||||
restart_services()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Full bring-up
|
|
||||||
ensure_lima_vm()
|
|
||||||
merge_kubeconfig()
|
|
||||||
disable_traefik()
|
|
||||||
ensure_cert_manager()
|
|
||||||
ensure_linkerd()
|
|
||||||
domain = ensure_tls_cert() # also computes domain from current IP
|
|
||||||
ensure_tls_secret(domain)
|
|
||||||
apply_manifests(domain)
|
|
||||||
seed_secrets()
|
|
||||||
restart_services()
|
|
||||||
wait_for_core()
|
|
||||||
print_urls(domain)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
1027
scripts/sunbeam.py
Executable file
1027
scripts/sunbeam.py
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user