488 lines
12 KiB
Python
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}
|
|
"""
|