Files
cli/sunbeam/gitea.py
Sienna Meridian Satterwhite cdc109d728 feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00

206 lines
7.3 KiB
Python

"""Gitea bootstrap — registry trust, admin setup, org creation."""
import base64
import json
import subprocess
import time
from sunbeam.kube import kube, kube_out
from sunbeam.output import step, ok, warn
LIMA_VM = "sunbeam"
GITEA_ADMIN_USER = "gitea_admin"
GITEA_ADMIN_EMAIL = "gitea@local.domain"
K8S_CTX = ["--context=sunbeam"]
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 _run(cmd, *, check=True, input=None, capture=False, cwd=None):
text = not isinstance(input, bytes)
return subprocess.run(cmd, check=check, text=text, input=input,
capture_output=capture, cwd=cwd)
def _kube_ok(*args):
return subprocess.run(
["kubectl", *K8S_CTX, *args], capture_output=True
).returncode == 0
def setup_lima_vm_registry(domain: str, gitea_admin_pass: str = ""):
"""Install mkcert root CA in the Lima VM and configure k3s to auth with Gitea.
Restarts k3s if either configuration changes so pods don't fight TLS errors
or get unauthenticated pulls on the first deploy.
"""
step("Configuring Lima VM registry trust...")
changed = False
# Install mkcert root CA so containerd trusts our wildcard TLS cert
caroot = _capture_out(["mkcert", "-CAROOT"])
if caroot:
from pathlib import Path
ca_pem = Path(caroot) / "rootCA.pem"
if ca_pem.exists():
already = subprocess.run(
["limactl", "shell", LIMA_VM, "test", "-f",
"/usr/local/share/ca-certificates/mkcert-root.crt"],
capture_output=True,
).returncode == 0
if not already:
_run(["limactl", "copy", str(ca_pem),
f"{LIMA_VM}:/tmp/mkcert-root.pem"])
_run(["limactl", "shell", LIMA_VM, "sudo", "cp",
"/tmp/mkcert-root.pem",
"/usr/local/share/ca-certificates/mkcert-root.crt"])
_run(["limactl", "shell", LIMA_VM, "sudo",
"update-ca-certificates"])
ok("mkcert CA installed in VM.")
changed = True
else:
ok("mkcert CA already installed.")
# Write k3s registries.yaml (auth for Gitea container registry)
registry_host = f"src.{domain}"
want = (
f'configs:\n'
f' "{registry_host}":\n'
f' auth:\n'
f' username: "{GITEA_ADMIN_USER}"\n'
f' password: "{gitea_admin_pass}"\n'
)
existing = _capture_out(["limactl", "shell", LIMA_VM,
"sudo", "cat",
"/etc/rancher/k3s/registries.yaml"])
if existing.strip() != want.strip():
subprocess.run(
["limactl", "shell", LIMA_VM, "sudo", "tee",
"/etc/rancher/k3s/registries.yaml"],
input=want, text=True, capture_output=True,
)
ok(f"Registry config written for {registry_host}.")
changed = True
else:
ok("Registry config up to date.")
if changed:
ok("Restarting k3s to apply changes...")
subprocess.run(
["limactl", "shell", LIMA_VM, "sudo", "systemctl", "restart",
"k3s"],
capture_output=True,
)
# Wait for API server to come back
for _ in range(40):
if _kube_ok("get", "nodes"):
break
time.sleep(3)
# Extra settle time -- pods take a moment to start terminating/restarting
time.sleep(15)
ok("k3s restarted.")
def cmd_bootstrap(domain: str = "", gitea_admin_pass: str = ""):
"""Ensure Gitea admin has a known password and create the studio/internal orgs."""
if not domain:
from sunbeam.kube import get_lima_ip
ip = get_lima_ip()
domain = f"{ip}.sslip.io"
if not gitea_admin_pass:
b64 = kube_out("-n", "devtools", "get", "secret",
"gitea-admin-credentials",
"-o=jsonpath={.data.password}")
if b64:
gitea_admin_pass = base64.b64decode(b64).decode()
step("Bootstrapping Gitea...")
# Wait for a Running + Ready Gitea pod
pod = ""
for _ in range(60):
candidate = kube_out(
"-n", "devtools", "get", "pods",
"-l=app.kubernetes.io/name=gitea",
"--field-selector=status.phase=Running",
"-o=jsonpath={.items[0].metadata.name}",
)
if candidate:
ready = kube_out("-n", "devtools", "get", "pod", candidate,
"-o=jsonpath={.status.containerStatuses[0].ready}")
if ready == "true":
pod = candidate
break
time.sleep(3)
if not pod:
warn("Gitea pod not ready after 3 min -- skipping bootstrap.")
return
def gitea_exec(*args):
return subprocess.run(
["kubectl", *K8S_CTX, "-n", "devtools", "exec", pod, "-c",
"gitea", "--"] + list(args),
capture_output=True, text=True,
)
# Ensure admin has the generated password
r = gitea_exec("gitea", "admin", "user", "change-password",
"--username", GITEA_ADMIN_USER, "--password",
gitea_admin_pass)
if r.returncode == 0 or "password" in (r.stdout + r.stderr).lower():
ok(f"Admin '{GITEA_ADMIN_USER}' password set.")
else:
warn(f"change-password: {r.stderr.strip()}")
# Clear must_change_password via Postgres
pg_pod = kube_out("-n", "data", "get", "pods",
"-l=cnpg.io/cluster=postgres,role=primary",
"-o=jsonpath={.items[0].metadata.name}")
if pg_pod:
kube("exec", "-n", "data", pg_pod, "-c", "postgres", "--",
"psql", "-U", "postgres", "-d", "gitea_db", "-c",
f'UPDATE "user" SET must_change_password = false'
f" WHERE lower_name = '{GITEA_ADMIN_USER.lower()}';",
check=False)
ok("Cleared must-change-password flag.")
else:
warn("Postgres pod not found -- must-change-password may block API "
"calls.")
def api(method, path, data=None):
args = [
"curl", "-s", "-X", method,
f"http://localhost:3000/api/v1{path}",
"-H", "Content-Type: application/json",
"-u", f"{GITEA_ADMIN_USER}:{gitea_admin_pass}",
]
if data:
args += ["-d", json.dumps(data)]
r = gitea_exec(*args)
try:
return json.loads(r.stdout)
except json.JSONDecodeError:
return {}
for org_name, visibility, desc in [
("studio", "public", "Public source code"),
("internal", "private", "Internal tools and services"),
]:
result = api("POST", "/orgs", {
"username": org_name,
"visibility": visibility,
"description": desc,
})
if "id" in result:
ok(f"Created org '{org_name}'.")
elif "already" in result.get("message", "").lower():
ok(f"Org '{org_name}' already exists.")
else:
warn(f"Org '{org_name}': {result.get('message', result)}")
ok(f"Gitea ready -- https://src.{domain} ({GITEA_ADMIN_USER} / <from "
f"openbao>)")