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:
@@ -1,5 +1,6 @@
|
||||
"""Image mirroring — patch amd64-only images + push to Gitea registry."""
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -200,6 +201,43 @@ def _run(cmd, *, check=True, input=None, capture=False, cwd=None):
|
||||
capture_output=capture, cwd=cwd)
|
||||
|
||||
|
||||
def _seed_and_push(image: str, admin_pass: str):
|
||||
"""Pre-seed a locally-built Docker image into k3s containerd, then push
|
||||
to the Gitea registry via 'ctr images push' inside the Lima VM.
|
||||
|
||||
This avoids 'docker push' entirely — the Lima k3s VM's containerd already
|
||||
trusts the mkcert CA (used for image pulls from Gitea), so ctr push works
|
||||
where docker push would hit a TLS cert verification error on the Mac.
|
||||
"""
|
||||
ok("Pre-seeding image into k3s containerd...")
|
||||
save = subprocess.Popen(["docker", "save", image], stdout=subprocess.PIPE)
|
||||
ctr = subprocess.run(
|
||||
["limactl", "shell", LIMA_VM, "--",
|
||||
"sudo", "ctr", "-n", "k8s.io", "images", "import", "-"],
|
||||
stdin=save.stdout,
|
||||
capture_output=True,
|
||||
)
|
||||
save.stdout.close()
|
||||
save.wait()
|
||||
if ctr.returncode != 0:
|
||||
warn(f"containerd import failed:\n{ctr.stderr.decode().strip()}")
|
||||
else:
|
||||
ok("Image pre-seeded.")
|
||||
|
||||
ok("Pushing to Gitea registry (via ctr in Lima VM)...")
|
||||
push = subprocess.run(
|
||||
["limactl", "shell", LIMA_VM, "--",
|
||||
"sudo", "ctr", "-n", "k8s.io", "images", "push",
|
||||
"--user", f"{GITEA_ADMIN_USER}:{admin_pass}", image],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if push.returncode != 0:
|
||||
warn(f"ctr push failed (image is pre-seeded; cluster will work without push):\n"
|
||||
f"{push.stderr.strip()}")
|
||||
else:
|
||||
ok(f"Pushed {image}")
|
||||
|
||||
|
||||
def cmd_mirror(domain: str = "", gitea_admin_pass: str = ""):
|
||||
"""Patch amd64-only images with an arm64 alias and push to Gitea registry."""
|
||||
if not domain:
|
||||
@@ -271,21 +309,86 @@ def _trust_registry_in_docker_vm(registry: str):
|
||||
ok(f"mkcert CA installed in Docker VM for {registry}.")
|
||||
|
||||
|
||||
def cmd_build(what: str):
|
||||
"""Build and push an image. Supports 'proxy', 'integration', and 'kratos-admin'."""
|
||||
def cmd_build(what: str, push: bool = False, deploy: bool = False):
|
||||
"""Build an image. Pass push=True to push, deploy=True to also apply + rollout."""
|
||||
if what == "proxy":
|
||||
_build_proxy()
|
||||
_build_proxy(push=push, deploy=deploy)
|
||||
elif what == "integration":
|
||||
_build_integration()
|
||||
_build_integration(push=push, deploy=deploy)
|
||||
elif what == "kratos-admin":
|
||||
_build_kratos_admin()
|
||||
_build_kratos_admin(push=push, deploy=deploy)
|
||||
elif what == "meet":
|
||||
_build_meet(push=push, deploy=deploy)
|
||||
elif what == "docs-frontend":
|
||||
_build_la_suite_frontend(
|
||||
app="docs-frontend",
|
||||
repo_dir=Path(__file__).resolve().parents[2] / "docs",
|
||||
workspace_rel="src/frontend",
|
||||
app_rel="src/frontend/apps/impress",
|
||||
dockerfile_rel="src/frontend/Dockerfile",
|
||||
image_name="impress-frontend",
|
||||
deployment="docs-frontend",
|
||||
namespace="lasuite",
|
||||
push=push,
|
||||
deploy=deploy,
|
||||
)
|
||||
elif what == "people-frontend":
|
||||
_build_la_suite_frontend(
|
||||
app="people-frontend",
|
||||
repo_dir=Path(__file__).resolve().parents[2] / "people",
|
||||
workspace_rel="src/frontend",
|
||||
app_rel="src/frontend/apps/desk",
|
||||
dockerfile_rel="src/frontend/Dockerfile",
|
||||
image_name="people-frontend",
|
||||
deployment="people-frontend",
|
||||
namespace="lasuite",
|
||||
push=push,
|
||||
deploy=deploy,
|
||||
)
|
||||
else:
|
||||
die(f"Unknown build target: {what}")
|
||||
|
||||
|
||||
def _build_proxy():
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
|
||||
def _seed_image_production(image: str, ssh_host: str, admin_pass: str):
|
||||
"""Build linux/amd64 image, pipe into production containerd via SSH, then push to Gitea."""
|
||||
ok("Importing image into production containerd via SSH pipe...")
|
||||
save = subprocess.Popen(["docker", "save", image], stdout=subprocess.PIPE)
|
||||
import_cmd = f"sudo ctr -n k8s.io images import -"
|
||||
ctr = subprocess.run(
|
||||
["ssh", "-p", "2222", "-o", "StrictHostKeyChecking=no", ssh_host, import_cmd],
|
||||
stdin=save.stdout,
|
||||
capture_output=True,
|
||||
)
|
||||
save.stdout.close()
|
||||
save.wait()
|
||||
if ctr.returncode != 0:
|
||||
warn(f"containerd import failed:\n{ctr.stderr.decode().strip()}")
|
||||
return False
|
||||
ok("Image imported into production containerd.")
|
||||
|
||||
ok("Pushing image to Gitea registry (via ctr on production server)...")
|
||||
push = subprocess.run(
|
||||
["ssh", "-p", "2222", "-o", "StrictHostKeyChecking=no", ssh_host,
|
||||
f"sudo ctr -n k8s.io images push --user {GITEA_ADMIN_USER}:{admin_pass} {image}"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if push.returncode != 0:
|
||||
warn(f"ctr push failed (image is pre-seeded; cluster will start):\n{push.stderr.strip()}")
|
||||
else:
|
||||
ok(f"Pushed {image} to Gitea registry.")
|
||||
return True
|
||||
|
||||
|
||||
def _build_proxy(push: bool = False, deploy: bool = False):
|
||||
from sunbeam import kube as _kube
|
||||
is_prod = bool(_kube._ssh_host)
|
||||
|
||||
if is_prod:
|
||||
domain = os.environ.get("SUNBEAM_DOMAIN", "sunbeam.pt")
|
||||
else:
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
|
||||
b64 = kube_out("-n", "devtools", "get", "secret",
|
||||
"gitea-admin-credentials", "-o=jsonpath={.data.password}")
|
||||
@@ -302,73 +405,110 @@ def _build_proxy():
|
||||
die(f"Proxy source not found at {proxy_dir}")
|
||||
|
||||
registry = f"src.{domain}"
|
||||
image = f"{registry}/studio/sunbeam-proxy:latest"
|
||||
image = f"{registry}/studio/proxy:latest"
|
||||
|
||||
step(f"Building sunbeam-proxy -> {image} ...")
|
||||
|
||||
# Ensure the Lima Docker VM trusts our mkcert CA for this registry.
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
|
||||
# Authenticate Docker with Gitea before the build so --push succeeds.
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
ok("Building image (linux/arm64, push)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/arm64",
|
||||
"--push",
|
||||
"-t", image,
|
||||
str(proxy_dir)])
|
||||
|
||||
ok(f"Pushed {image}")
|
||||
|
||||
# On single-node clusters, pre-seed the image directly into k3s containerd.
|
||||
# This breaks the circular dependency: when the proxy restarts, Pingora goes
|
||||
# down before the new pod starts, making the Gitea registry (behind Pingora)
|
||||
# unreachable for the image pull. By importing into containerd first,
|
||||
# imagePullPolicy: IfNotPresent means k8s never needs to contact the registry.
|
||||
nodes = kube_out("get", "nodes", "-o=jsonpath={.items[*].metadata.name}").split()
|
||||
if len(nodes) == 1:
|
||||
ok("Single-node cluster: pre-seeding image into k3s containerd...")
|
||||
save = subprocess.Popen(
|
||||
["docker", "save", image],
|
||||
stdout=subprocess.PIPE,
|
||||
if is_prod:
|
||||
# Production (x86_64 server): cross-compile on the Mac arm64 host using
|
||||
# x86_64-linux-musl-gcc (brew install filosottile/musl-cross/musl-cross),
|
||||
# then package the pre-built static binary into a minimal Docker image.
|
||||
# This avoids QEMU x86_64 emulation which crashes rustc (SIGSEGV).
|
||||
musl_gcc = shutil.which("x86_64-linux-musl-gcc")
|
||||
if not musl_gcc:
|
||||
die(
|
||||
"x86_64-linux-musl-gcc not found.\n"
|
||||
"Install: brew install filosottile/musl-cross/musl-cross"
|
||||
)
|
||||
ok("Cross-compiling sunbeam-proxy for x86_64-musl (native, no QEMU)...")
|
||||
import os as _os
|
||||
env = dict(_os.environ)
|
||||
env["CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER"] = musl_gcc
|
||||
env["CC_x86_64_unknown_linux_musl"] = musl_gcc
|
||||
env["RUSTFLAGS"] = "-C target-feature=+crt-static"
|
||||
r = subprocess.run(
|
||||
["cargo", "build", "--release", "--target", "x86_64-unknown-linux-musl"],
|
||||
cwd=str(proxy_dir),
|
||||
env=env,
|
||||
)
|
||||
ctr = subprocess.run(
|
||||
["limactl", "shell", LIMA_VM, "--",
|
||||
"sudo", "ctr", "-n", "k8s.io", "images", "import", "-"],
|
||||
stdin=save.stdout,
|
||||
capture_output=True,
|
||||
if r.returncode != 0:
|
||||
die("cargo build failed.")
|
||||
binary = proxy_dir / "target" / "x86_64-unknown-linux-musl" / "release" / "sunbeam-proxy"
|
||||
|
||||
# Download tini static binary for amd64 if not cached
|
||||
import tempfile, urllib.request
|
||||
tmpdir = Path(tempfile.mkdtemp(prefix="proxy-pkg-"))
|
||||
tini_path = tmpdir / "tini"
|
||||
ok("Downloading tini-static-amd64...")
|
||||
urllib.request.urlretrieve(
|
||||
"https://github.com/krallin/tini/releases/download/v0.19.0/tini-static-amd64",
|
||||
str(tini_path),
|
||||
)
|
||||
save.stdout.close()
|
||||
save.wait()
|
||||
if ctr.returncode != 0:
|
||||
warn(f"containerd import failed (will fall back to registry pull):\n"
|
||||
f"{ctr.stderr.decode().strip()}")
|
||||
else:
|
||||
ok("Image pre-seeded.")
|
||||
tini_path.chmod(0o755)
|
||||
shutil.copy(str(binary), str(tmpdir / "sunbeam-proxy"))
|
||||
(tmpdir / "Dockerfile").write_text(
|
||||
"FROM cgr.dev/chainguard/static:latest\n"
|
||||
"COPY tini /tini\n"
|
||||
"COPY sunbeam-proxy /usr/local/bin/sunbeam-proxy\n"
|
||||
"EXPOSE 80 443\n"
|
||||
'ENTRYPOINT ["/tini", "--", "/usr/local/bin/sunbeam-proxy"]\n'
|
||||
)
|
||||
ok("Packaging into Docker image (linux/amd64, pre-built binary)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/amd64",
|
||||
"--provenance=false",
|
||||
"--load",
|
||||
"-t", image,
|
||||
str(tmpdir)])
|
||||
shutil.rmtree(str(tmpdir), ignore_errors=True)
|
||||
if push:
|
||||
_seed_image_production(image, _kube._ssh_host, admin_pass)
|
||||
else:
|
||||
# Local Lima dev: build linux/arm64 natively.
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
|
||||
# Apply manifests so the Deployment spec reflects the Gitea image ref.
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply()
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["limactl", "shell", LIMA_DOCKER_VM, "--",
|
||||
"docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
# Roll the pingora pod.
|
||||
ok("Rolling pingora deployment...")
|
||||
kube("rollout", "restart", "deployment/pingora", "-n", "ingress")
|
||||
kube("rollout", "status", "deployment/pingora", "-n", "ingress",
|
||||
"--timeout=120s")
|
||||
ok("Pingora redeployed.")
|
||||
ok("Building image (linux/arm64)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/arm64",
|
||||
"--provenance=false",
|
||||
"--load",
|
||||
"-t", image,
|
||||
str(proxy_dir)])
|
||||
|
||||
if push:
|
||||
ok("Pushing image...")
|
||||
_run(["docker", "push", image])
|
||||
_seed_and_push(image, admin_pass)
|
||||
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply(env="production" if is_prod else "local", domain=domain)
|
||||
ok("Rolling pingora deployment...")
|
||||
kube("rollout", "restart", "deployment/pingora", "-n", "ingress")
|
||||
kube("rollout", "status", "deployment/pingora", "-n", "ingress",
|
||||
"--timeout=120s")
|
||||
ok("Pingora redeployed.")
|
||||
|
||||
|
||||
def _build_integration():
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
def _build_integration(push: bool = False, deploy: bool = False):
|
||||
from sunbeam import kube as _kube
|
||||
is_prod = bool(_kube._ssh_host)
|
||||
|
||||
if is_prod:
|
||||
domain = os.environ.get("SUNBEAM_DOMAIN", "sunbeam.pt")
|
||||
else:
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
|
||||
b64 = kube_out("-n", "devtools", "get", "secret",
|
||||
"gitea-admin-credentials", "-o=jsonpath={.data.password}")
|
||||
@@ -397,31 +537,21 @@ def _build_integration():
|
||||
|
||||
step(f"Building integration -> {image} ...")
|
||||
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
platform = "linux/amd64" if is_prod else "linux/arm64"
|
||||
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
ok("Building image (linux/arm64, push)...")
|
||||
# --file points to integration-service/Dockerfile; context is sunbeam/ root.
|
||||
# Docker resolves .dockerignore relative to the build context root, but since
|
||||
# --file is outside the context root we provide it explicitly via env or flag.
|
||||
# Workaround: copy .dockerignore to sunbeam/ root temporarily, then remove.
|
||||
# Copy .dockerignore to context root temporarily if needed.
|
||||
root_ignore = sunbeam_dir / ".dockerignore"
|
||||
copied_ignore = False
|
||||
if not root_ignore.exists():
|
||||
if not root_ignore.exists() and dockerignore.exists():
|
||||
shutil.copy(str(dockerignore), str(root_ignore))
|
||||
copied_ignore = True
|
||||
try:
|
||||
ok(f"Building image ({platform})...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/arm64",
|
||||
"--push",
|
||||
"--platform", platform,
|
||||
"--provenance=false",
|
||||
"--load",
|
||||
"-f", str(dockerfile),
|
||||
"-t", image,
|
||||
str(sunbeam_dir)])
|
||||
@@ -429,43 +559,166 @@ def _build_integration():
|
||||
if copied_ignore and root_ignore.exists():
|
||||
root_ignore.unlink()
|
||||
|
||||
ok(f"Pushed {image}")
|
||||
|
||||
# Pre-seed into k3s containerd (same pattern as other custom images).
|
||||
nodes = kube_out("get", "nodes", "-o=jsonpath={.items[*].metadata.name}").split()
|
||||
if len(nodes) == 1:
|
||||
ok("Single-node cluster: pre-seeding image into k3s containerd...")
|
||||
save = subprocess.Popen(
|
||||
["docker", "save", image],
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
ctr = subprocess.run(
|
||||
["limactl", "shell", LIMA_VM, "--",
|
||||
"sudo", "ctr", "-n", "k8s.io", "images", "import", "-"],
|
||||
stdin=save.stdout,
|
||||
capture_output=True,
|
||||
)
|
||||
save.stdout.close()
|
||||
save.wait()
|
||||
if ctr.returncode != 0:
|
||||
warn(f"containerd import failed (will fall back to registry pull):\n"
|
||||
f"{ctr.stderr.decode().strip()}")
|
||||
if push:
|
||||
if is_prod:
|
||||
_seed_image_production(image, _kube._ssh_host, admin_pass)
|
||||
else:
|
||||
ok("Image pre-seeded.")
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["limactl", "shell", LIMA_DOCKER_VM, "--",
|
||||
"docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
_seed_and_push(image, admin_pass)
|
||||
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply()
|
||||
|
||||
ok("Rolling integration deployment...")
|
||||
kube("rollout", "restart", "deployment/integration", "-n", "lasuite")
|
||||
kube("rollout", "status", "deployment/integration", "-n", "lasuite",
|
||||
"--timeout=120s")
|
||||
ok("Integration redeployed.")
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply(env="production" if is_prod else "local", domain=domain)
|
||||
ok("Rolling integration deployment...")
|
||||
kube("rollout", "restart", "deployment/integration", "-n", "lasuite")
|
||||
kube("rollout", "status", "deployment/integration", "-n", "lasuite",
|
||||
"--timeout=120s")
|
||||
ok("Integration redeployed.")
|
||||
|
||||
|
||||
def _build_kratos_admin():
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
def _build_kratos_admin(push: bool = False, deploy: bool = False):
|
||||
from sunbeam import kube as _kube
|
||||
|
||||
is_prod = bool(_kube._ssh_host)
|
||||
|
||||
b64 = kube_out("-n", "devtools", "get", "secret",
|
||||
"gitea-admin-credentials", "-o=jsonpath={.data.password}")
|
||||
if not b64:
|
||||
die("gitea-admin-credentials secret not found -- run seed first.")
|
||||
admin_pass = base64.b64decode(b64).decode()
|
||||
|
||||
# kratos-admin source
|
||||
kratos_admin_dir = Path(__file__).resolve().parents[2] / "kratos-admin"
|
||||
if not kratos_admin_dir.is_dir():
|
||||
die(f"kratos-admin source not found at {kratos_admin_dir}")
|
||||
|
||||
if is_prod:
|
||||
domain = os.environ.get("SUNBEAM_DOMAIN", "sunbeam.pt")
|
||||
registry = f"src.{domain}"
|
||||
image = f"{registry}/studio/kratos-admin-ui:latest"
|
||||
ssh_host = _kube._ssh_host
|
||||
|
||||
step(f"Building kratos-admin-ui (linux/amd64, native cross-compile) -> {image} ...")
|
||||
|
||||
if not shutil.which("deno"):
|
||||
die("deno not found — install Deno: https://deno.land/")
|
||||
if not shutil.which("npm"):
|
||||
die("npm not found — install Node.js")
|
||||
|
||||
ok("Building UI assets (npm run build)...")
|
||||
_run(["npm", "run", "build"], cwd=str(kratos_admin_dir / "ui"))
|
||||
|
||||
ok("Cross-compiling Deno binary for x86_64-linux-gnu...")
|
||||
_run([
|
||||
"deno", "compile",
|
||||
"--target", "x86_64-unknown-linux-gnu",
|
||||
"--allow-net", "--allow-read", "--allow-env",
|
||||
"--include", "ui/dist",
|
||||
"-o", "kratos-admin-x86_64",
|
||||
"main.ts",
|
||||
], cwd=str(kratos_admin_dir))
|
||||
|
||||
bin_path = kratos_admin_dir / "kratos-admin-x86_64"
|
||||
if not bin_path.exists():
|
||||
die("Deno cross-compilation produced no binary")
|
||||
|
||||
# Build minimal Docker image
|
||||
pkg_dir = Path("/tmp/kratos-admin-pkg")
|
||||
pkg_dir.mkdir(exist_ok=True)
|
||||
import shutil as _sh
|
||||
_sh.copy2(str(bin_path), str(pkg_dir / "kratos-admin"))
|
||||
# Copy ui/dist for serveStatic (binary has it embedded but keep external copy for fallback)
|
||||
(pkg_dir / "dockerfile").write_text(
|
||||
"FROM gcr.io/distroless/cc-debian12:nonroot\n"
|
||||
"WORKDIR /app\n"
|
||||
"COPY kratos-admin ./\n"
|
||||
"EXPOSE 3000\n"
|
||||
'ENTRYPOINT ["/app/kratos-admin"]\n'
|
||||
)
|
||||
|
||||
ok("Building Docker image...")
|
||||
_run([
|
||||
"docker", "buildx", "build",
|
||||
"--platform", "linux/amd64",
|
||||
"--provenance=false",
|
||||
"--load",
|
||||
"-f", str(pkg_dir / "dockerfile"),
|
||||
"-t", image,
|
||||
str(pkg_dir),
|
||||
])
|
||||
|
||||
if push:
|
||||
_seed_image_production(image, ssh_host, admin_pass)
|
||||
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply(env="production", domain=domain)
|
||||
|
||||
else:
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
registry = f"src.{domain}"
|
||||
image = f"{registry}/studio/kratos-admin-ui:latest"
|
||||
|
||||
if not shutil.which("docker"):
|
||||
die("docker not found -- is the Lima docker VM running?")
|
||||
|
||||
step(f"Building kratos-admin-ui -> {image} ...")
|
||||
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["limactl", "shell", LIMA_DOCKER_VM, "--",
|
||||
"docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
ok("Building image (linux/arm64)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/arm64",
|
||||
"--provenance=false",
|
||||
"--load",
|
||||
"-t", image,
|
||||
str(kratos_admin_dir)])
|
||||
|
||||
if push:
|
||||
_seed_and_push(image, admin_pass)
|
||||
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply()
|
||||
|
||||
if deploy:
|
||||
ok("Rolling kratos-admin-ui deployment...")
|
||||
kube("rollout", "restart", "deployment/kratos-admin-ui", "-n", "ory")
|
||||
kube("rollout", "status", "deployment/kratos-admin-ui", "-n", "ory",
|
||||
"--timeout=120s")
|
||||
ok("kratos-admin-ui redeployed.")
|
||||
|
||||
|
||||
def _build_meet(push: bool = False, deploy: bool = False):
|
||||
"""Build meet-backend and meet-frontend images from source."""
|
||||
from sunbeam import kube as _kube
|
||||
is_prod = bool(_kube._ssh_host)
|
||||
|
||||
if is_prod:
|
||||
domain = os.environ.get("SUNBEAM_DOMAIN", "sunbeam.pt")
|
||||
else:
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
|
||||
b64 = kube_out("-n", "devtools", "get", "secret",
|
||||
"gitea-admin-credentials", "-o=jsonpath={.data.password}")
|
||||
@@ -476,62 +729,162 @@ def _build_kratos_admin():
|
||||
if not shutil.which("docker"):
|
||||
die("docker not found -- is the Lima docker VM running?")
|
||||
|
||||
# kratos-admin source
|
||||
kratos_admin_dir = Path(__file__).resolve().parents[2] / "kratos-admin"
|
||||
if not kratos_admin_dir.is_dir():
|
||||
die(f"kratos-admin source not found at {kratos_admin_dir}")
|
||||
meet_dir = Path(__file__).resolve().parents[2] / "meet"
|
||||
if not meet_dir.is_dir():
|
||||
die(f"meet source not found at {meet_dir}")
|
||||
|
||||
registry = f"src.{domain}"
|
||||
image = f"{registry}/studio/kratos-admin-ui:latest"
|
||||
backend_image = f"{registry}/studio/meet-backend:latest"
|
||||
frontend_image = f"{registry}/studio/meet-frontend:latest"
|
||||
platform = "linux/amd64" if is_prod else "linux/arm64"
|
||||
|
||||
step(f"Building kratos-admin-ui -> {image} ...")
|
||||
if not is_prod:
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["limactl", "shell", LIMA_DOCKER_VM, "--",
|
||||
"docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
step(f"Building meet-backend -> {backend_image} ...")
|
||||
ok(f"Building image ({platform}, backend-production target)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", platform,
|
||||
"--provenance=false",
|
||||
"--target", "backend-production",
|
||||
"--load",
|
||||
"-t", backend_image,
|
||||
str(meet_dir)])
|
||||
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
if push:
|
||||
if is_prod:
|
||||
_seed_image_production(backend_image, _kube._ssh_host, admin_pass)
|
||||
else:
|
||||
_seed_and_push(backend_image, admin_pass)
|
||||
|
||||
ok("Building image (linux/arm64, push)...")
|
||||
step(f"Building meet-frontend -> {frontend_image} ...")
|
||||
frontend_dockerfile = meet_dir / "src" / "frontend" / "Dockerfile"
|
||||
if not frontend_dockerfile.exists():
|
||||
die(f"meet frontend Dockerfile not found at {frontend_dockerfile}")
|
||||
|
||||
ok(f"Building image ({platform}, frontend-production target)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", platform,
|
||||
"--provenance=false",
|
||||
"--target", "frontend-production",
|
||||
"--build-arg", "VITE_API_BASE_URL=",
|
||||
"--load",
|
||||
"-f", str(frontend_dockerfile),
|
||||
"-t", frontend_image,
|
||||
str(meet_dir)])
|
||||
|
||||
if push:
|
||||
if is_prod:
|
||||
_seed_image_production(frontend_image, _kube._ssh_host, admin_pass)
|
||||
else:
|
||||
_seed_and_push(frontend_image, admin_pass)
|
||||
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply(env="production" if is_prod else "local", domain=domain)
|
||||
for deployment in ("meet-backend", "meet-celery-worker", "meet-frontend"):
|
||||
ok(f"Rolling {deployment} deployment...")
|
||||
kube("rollout", "restart", f"deployment/{deployment}", "-n", "lasuite")
|
||||
for deployment in ("meet-backend", "meet-celery-worker", "meet-frontend"):
|
||||
kube("rollout", "status", f"deployment/{deployment}", "-n", "lasuite",
|
||||
"--timeout=180s")
|
||||
ok("Meet redeployed.")
|
||||
|
||||
|
||||
def _build_la_suite_frontend(
|
||||
app: str,
|
||||
repo_dir: Path,
|
||||
workspace_rel: str,
|
||||
app_rel: str,
|
||||
dockerfile_rel: str,
|
||||
image_name: str,
|
||||
deployment: str,
|
||||
namespace: str,
|
||||
push: bool = False,
|
||||
deploy: bool = False,
|
||||
):
|
||||
"""Build a La Suite frontend image from source and push to the Gitea registry.
|
||||
|
||||
Steps:
|
||||
1. yarn install in the workspace root — updates yarn.lock for new packages.
|
||||
2. yarn build-theme in the app dir — regenerates cunningham token CSS/TS.
|
||||
3. docker buildx build --target frontend-production → push.
|
||||
4. Pre-seed into k3s containerd.
|
||||
5. sunbeam apply + rollout restart.
|
||||
"""
|
||||
if not shutil.which("yarn"):
|
||||
die("yarn not found on PATH — install Node.js + yarn first (nvm use 22).")
|
||||
if not shutil.which("docker"):
|
||||
die("docker not found — is the Lima docker VM running?")
|
||||
|
||||
ip = get_lima_ip()
|
||||
domain = f"{ip}.sslip.io"
|
||||
|
||||
b64 = kube_out("-n", "devtools", "get", "secret",
|
||||
"gitea-admin-credentials", "-o=jsonpath={.data.password}")
|
||||
if not b64:
|
||||
die("gitea-admin-credentials secret not found — run seed first.")
|
||||
admin_pass = base64.b64decode(b64).decode()
|
||||
|
||||
workspace_dir = repo_dir / workspace_rel
|
||||
app_dir = repo_dir / app_rel
|
||||
dockerfile = repo_dir / dockerfile_rel
|
||||
|
||||
if not repo_dir.is_dir():
|
||||
die(f"{app} source not found at {repo_dir}")
|
||||
if not dockerfile.exists():
|
||||
die(f"Dockerfile not found at {dockerfile}")
|
||||
|
||||
registry = f"src.{domain}"
|
||||
image = f"{registry}/studio/{image_name}:latest"
|
||||
|
||||
step(f"Building {app} -> {image} ...")
|
||||
|
||||
ok("Updating yarn.lock (yarn install in workspace)...")
|
||||
_run(["yarn", "install"], cwd=str(workspace_dir))
|
||||
|
||||
ok("Regenerating cunningham design tokens (yarn build-theme)...")
|
||||
_run(["yarn", "build-theme"], cwd=str(app_dir))
|
||||
|
||||
if push:
|
||||
_trust_registry_in_docker_vm(registry)
|
||||
ok("Logging in to Gitea registry...")
|
||||
r = subprocess.run(
|
||||
["limactl", "shell", LIMA_DOCKER_VM, "--",
|
||||
"docker", "login", registry,
|
||||
"--username", GITEA_ADMIN_USER, "--password-stdin"],
|
||||
input=admin_pass, text=True, capture_output=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
die(f"docker login failed:\n{r.stderr.strip()}")
|
||||
|
||||
ok("Building image (linux/arm64, frontend-production target)...")
|
||||
_run(["docker", "buildx", "build",
|
||||
"--platform", "linux/arm64",
|
||||
"--push",
|
||||
"--provenance=false",
|
||||
"--target", "frontend-production",
|
||||
"--load",
|
||||
"-f", str(dockerfile),
|
||||
"-t", image,
|
||||
str(kratos_admin_dir)])
|
||||
str(repo_dir)])
|
||||
|
||||
ok(f"Pushed {image}")
|
||||
if push:
|
||||
_seed_and_push(image, admin_pass)
|
||||
|
||||
# Pre-seed into k3s containerd (same pattern as proxy)
|
||||
nodes = kube_out("get", "nodes", "-o=jsonpath={.items[*].metadata.name}").split()
|
||||
if len(nodes) == 1:
|
||||
ok("Single-node cluster: pre-seeding image into k3s containerd...")
|
||||
save = subprocess.Popen(
|
||||
["docker", "save", image],
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
ctr = subprocess.run(
|
||||
["limactl", "shell", LIMA_VM, "--",
|
||||
"sudo", "ctr", "-n", "k8s.io", "images", "import", "-"],
|
||||
stdin=save.stdout,
|
||||
capture_output=True,
|
||||
)
|
||||
save.stdout.close()
|
||||
save.wait()
|
||||
if ctr.returncode != 0:
|
||||
warn(f"containerd import failed:\n{ctr.stderr.decode().strip()}")
|
||||
else:
|
||||
ok("Image pre-seeded.")
|
||||
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply()
|
||||
|
||||
ok("Rolling kratos-admin-ui deployment...")
|
||||
kube("rollout", "restart", "deployment/kratos-admin-ui", "-n", "ory")
|
||||
kube("rollout", "status", "deployment/kratos-admin-ui", "-n", "ory",
|
||||
"--timeout=120s")
|
||||
ok("kratos-admin-ui redeployed.")
|
||||
if deploy:
|
||||
from sunbeam.manifests import cmd_apply
|
||||
cmd_apply()
|
||||
ok(f"Rolling {deployment} deployment...")
|
||||
kube("rollout", "restart", f"deployment/{deployment}", "-n", namespace)
|
||||
kube("rollout", "status", f"deployment/{deployment}", "-n", namespace,
|
||||
"--timeout=180s")
|
||||
ok(f"{deployment} redeployed.")
|
||||
|
||||
Reference in New Issue
Block a user