Files
soleprint/soleprint/ctrl/k8s/templates.py
2026-09-12 02:44:40 -03:00

488 lines
12 KiB
Python

"""String templates for kind-cluster manifests.
Each function returns a YAML/shell blob. Keep plain strings — no jinja2
dep, matches the init/core.py convention.
"""
from __future__ import annotations
# ─── Base manifests ─────────────────────────────────────────────────
def namespace(*, room: str) -> str:
return f"""\
apiVersion: v1
kind: Namespace
metadata:
name: {room}
labels:
soleprint-room: "{room}"
"""
def configmap(*, room: str) -> str:
return f"""\
apiVersion: v1
kind: ConfigMap
metadata:
name: {room}-env
namespace: {room}
data:
DEPLOYMENT_NAME: "{room}"
POSTGRES_DB: "fixture"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "localdev123"
ARTERY_EXTERNAL_URL: "/artery"
ATLAS_EXTERNAL_URL: "/atlas"
STATION_EXTERNAL_URL: "/station"
AUTH_BYPASS: "true"
"""
def postgres(*, room: str) -> str:
return f"""\
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pgdata
namespace: {room}
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: {room}
labels: {{app: db}}
spec:
replicas: 1
selector: {{matchLabels: {{app: db}}}}
template:
metadata:
labels: {{app: db}}
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- configMapRef: {{name: {room}-env}}
ports:
- containerPort: 5432
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: [sh, -c, "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
periodSeconds: 5
volumes:
- name: pgdata
persistentVolumeClaim: {{claimName: pgdata}}
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: {room}
spec:
selector: {{app: db}}
ports:
- port: 5432
targetPort: 5432
"""
def backend(*, room: str, managed_name: str) -> str:
return f"""\
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: {room}
labels: {{app: backend}}
spec:
replicas: 1
selector: {{matchLabels: {{app: backend}}}}
template:
metadata:
labels: {{app: backend}}
spec:
containers:
- name: backend
image: {room}-backend:dev
imagePullPolicy: IfNotPresent
env:
- {{name: DB_HOST, value: db}}
- {{name: DB_PORT, value: "5432"}}
- {{name: SEED_ON_START, value: "true"}}
envFrom:
- configMapRef: {{name: {room}-env}}
ports:
- containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: {room}
spec:
selector: {{app: backend}}
ports:
- port: 8000
targetPort: 8000
"""
def frontend(*, room: str, managed_name: str) -> str:
return f"""\
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: {room}
labels: {{app: frontend}}
spec:
replicas: 1
selector: {{matchLabels: {{app: frontend}}}}
template:
metadata:
labels: {{app: frontend}}
spec:
containers:
- name: frontend
image: {room}-frontend:dev
imagePullPolicy: IfNotPresent
env:
- {{name: VITE_API_URL, value: ""}}
ports:
- containerPort: 5173
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: {room}
spec:
selector: {{app: frontend}}
ports:
- port: 5173
targetPort: 5173
"""
def link(*, room: str) -> str:
return f"""\
apiVersion: apps/v1
kind: Deployment
metadata:
name: link
namespace: {room}
labels: {{app: link}}
spec:
replicas: 1
selector: {{matchLabels: {{app: link}}}}
template:
metadata:
labels: {{app: link}}
spec:
containers:
- name: link
image: {room}-link:dev
imagePullPolicy: IfNotPresent
env:
- {{name: DB_HOST, value: db}}
- {{name: DB_PORT, value: "5432"}}
envFrom:
- configMapRef: {{name: {room}-env}}
ports:
- containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
name: link
namespace: {room}
spec:
selector: {{app: link}}
ports:
- port: 8000
targetPort: 8000
"""
def soleprint(*, room: str) -> str:
return f"""\
apiVersion: apps/v1
kind: Deployment
metadata:
name: soleprint
namespace: {room}
labels: {{app: soleprint}}
spec:
replicas: 1
selector: {{matchLabels: {{app: soleprint}}}}
template:
metadata:
labels: {{app: soleprint}}
spec:
containers:
- name: soleprint
image: {room}-soleprint:dev
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef: {{name: {room}-env}}
ports:
- containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
name: soleprint
namespace: {room}
spec:
selector: {{app: soleprint}}
ports:
- port: 8000
targetPort: 8000
"""
def gateway(*, room: str, nodeport: int) -> str:
return f"""\
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
namespace: {room}
labels: {{app: gateway, soleprint-room: "{room}"}}
spec:
replicas: 1
selector: {{matchLabels: {{app: gateway}}}}
template:
metadata:
labels: {{app: gateway, soleprint-room: "{room}"}}
spec:
containers:
- name: envoy
image: envoyproxy/envoy:v1.30-latest
ports:
- containerPort: 10000
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
volumes:
- name: envoy-config
configMap:
name: envoy-config
---
apiVersion: v1
kind: Service
metadata:
name: gateway
namespace: {room}
spec:
type: NodePort
selector: {{app: gateway}}
ports:
- port: 80
targetPort: 10000
nodePort: {nodeport}
"""
def envoy(*, room: str, has_backend: bool, has_frontend: bool) -> str:
"""Envoy config with /spr → soleprint, /api → backend, / → frontend."""
routes = [' - match: {prefix: "/spr/"}\n route: {cluster: soleprint, prefix_rewrite: "/"}']
clusters = [_cluster("soleprint", "soleprint", 8000)]
if has_backend:
routes.append(' - match: {prefix: "/api/"}\n route: {cluster: backend}')
clusters.append(_cluster("backend", "backend", 8000))
if has_frontend:
routes.append(' - match: {prefix: "/"}\n route: {cluster: frontend}')
clusters.append(_cluster("frontend", "frontend", 5173))
else:
# Fall back to soleprint as default if no frontend
routes.append(' - match: {prefix: "/"}\n route: {cluster: soleprint}')
routes_yaml = "\n".join(routes)
clusters_yaml = "\n".join(clusters)
envoy_yaml = f"""\
admin:
address:
socket_address: {{address: 0.0.0.0, port_value: 9901}}
static_resources:
listeners:
- name: main
address:
socket_address: {{address: 0.0.0.0, port_value: 10000}}
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
upgrade_configs: [{{upgrade_type: websocket}}]
route_config:
name: main
virtual_hosts:
- name: default
domains: ["*"]
routes:
{_indent(routes_yaml, 14)}
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
{clusters_yaml}
"""
return f"""\
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-config
namespace: {room}
data:
envoy.yaml: |
{_indent(envoy_yaml, 4)}
"""
def _cluster(name: str, host: str, port: int) -> str:
return f"""\
- name: {name}
type: STRICT_DNS
connect_timeout: 1s
load_assignment:
cluster_name: {name}
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: {{address: {host}, port_value: {port}}}"""
def _indent(text: str, spaces: int) -> str:
pad = " " * spaces
return "\n".join(pad + line if line else line for line in text.splitlines())
def kustomization_base(*, resources: list[str]) -> str:
# envoy.yaml is itself a ConfigMap manifest, so it belongs in resources.
res = "\n".join(f" - {r}" for r in resources + ["envoy.yaml"])
return f"""\
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
{res}
"""
def kustomization_dev(*, room: str) -> str:
return f"""\
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: {room}
resources:
- ../../base
"""
# ─── Lifecycle scripts ──────────────────────────────────────────────
# These target the shared `spr` kind cluster (created by `make cluster up`
# at the repo root, which builds it with rig). Each room owns a namespace inside that cluster.
def k8s_up_sh(*, room: str, cluster: str, nodeport: int) -> str:
return f"""\
#!/bin/bash
# Apply the "{room}" room into the shared `{cluster}` kind cluster.
# (Run 'make cluster up' at the repo root first if the cluster doesn't exist.)
set -e
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
K8S_DIR="$SCRIPT_DIR/k8s"
if ! kind get clusters 2>/dev/null | grep -q '^{cluster}$'; then
echo "Kind cluster '{cluster}' not found — run 'make cluster up' from the repo root first."
exit 1
fi
CTX="kind-{cluster}"
echo "Loading images for room '{room}' into '{cluster}'..."
"$SCRIPT_DIR/k8s-load.sh"
echo "Applying manifests (namespace={room})..."
kubectl --context "$CTX" apply -k "$K8S_DIR/overlays/dev"
echo
echo "Done."
echo " Gateway: http://localhost:{nodeport}/"
echo " (add `127.0.0.1 {room}.spr.local.ar` to /etc/hosts for Host routing)"
"""
def k8s_down_sh(*, room: str, cluster: str) -> str:
return f"""\
#!/bin/bash
# Remove the "{room}" namespace from the shared `{cluster}` cluster.
# Leaves the cluster itself running (use 'make cluster down' at the repo root to drop everything).
set -e
CTX="kind-{cluster}"
if ! kind get clusters 2>/dev/null | grep -q '^{cluster}$'; then
echo "Kind cluster '{cluster}' not running — nothing to do."
exit 0
fi
if kubectl --context "$CTX" get namespace {room} >/dev/null 2>&1; then
kubectl --context "$CTX" delete namespace {room}
else
echo "Namespace '{room}' not found in '{cluster}'."
fi
"""
def k8s_load_sh(
*, room: str, cluster: str, managed_name: str,
has_managed: bool, has_frontend: bool, has_link: bool,
) -> str:
steps = [f'echo "Building soleprint image…"',
f'docker build -t {room}-soleprint:dev "$ROOT_DIR/soleprint"']
if has_managed:
steps += [f'echo "Building backend image…"',
f'docker build -t {room}-backend:dev "$ROOT_DIR/{managed_name}/backend"']
if has_frontend:
steps += [f'echo "Building frontend image…"',
f'docker build -t {room}-frontend:dev "$ROOT_DIR/{managed_name}/frontend"']
if has_link:
steps += [f'echo "Building link image…"',
f'docker build -t {room}-link:dev "$ROOT_DIR/link"']
loads = [f'kind load docker-image {room}-soleprint:dev --name {cluster}']
if has_managed:
loads.append(f'kind load docker-image {room}-backend:dev --name {cluster}')
if has_frontend:
loads.append(f'kind load docker-image {room}-frontend:dev --name {cluster}')
if has_link:
loads.append(f'kind load docker-image {room}-link:dev --name {cluster}')
body = "\n".join(steps + [""] + [f'echo "Loading into `{cluster}` kind cluster…"'] + loads)
return f"""\
#!/bin/bash
# Build all images for room "{room}" and load them into the shared `{cluster}` cluster.
set -e
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
{body}
"""