Files
govoplan/tools/lab/govoplan_lab/render.py
T
zemion 3ca068f76a
Dependency Audit / dependency-audit (push) Failing after 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m18s
Developer Meta-package Release / publish-package (push) Successful in 11s
Release v0.1.16
2026-08-05 19:52:32 +02:00

312 lines
10 KiB
Python

"""Deterministic configuration rendering for the Kubernetes VM lab."""
from __future__ import annotations
import base64
import json
from pathlib import Path
from typing import Mapping
from .config import LabConfig, LabNode
def render_cloud_init(config: LabConfig, node: LabNode, public_key: str) -> str:
packages = ["ca-certificates", "curl", "qemu-guest-agent"]
if node.role == "state":
packages.extend(["docker.io", "docker-compose-v2", "openssl"])
package_lines = "\n".join(f" - {item}" for item in packages)
return f"""#cloud-config
hostname: {node.name}
manage_etc_hosts: true
package_update: true
package_upgrade: false
packages:
{package_lines}
users:
- default
- name: {config.ssh_user}
groups: [adm, sudo]
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock_passwd: true
ssh_authorized_keys:
- {json.dumps(public_key.strip())}
ssh_pwauth: false
disable_root: true
runcmd:
- [systemctl, enable, --now, qemu-guest-agent]
- [sh, -c, "test ! -e /usr/bin/docker || systemctl enable --now docker"]
final_message: "GovOPlaN lab node is ready"
"""
def render_network_config(config: LabConfig, node: LabNode) -> str:
dns = ", ".join(str(item) for item in config.network.dns_servers)
return f"""version: 2
ethernets:
primary:
match:
macaddress: {node.mac_address}
set-name: eth0
addresses:
- {node.address}/{config.network.prefix_length}
routes:
- to: default
via: {config.network.gateway}
nameservers:
addresses: [{dns}]
"""
def render_meta_data(config: LabConfig, node: LabNode) -> str:
return f"instance-id: {config.name}-{node.name}\nlocal-hostname: {node.name}\n"
def render_k3s_config(
config: LabConfig,
node: LabNode,
*,
cluster_token: str,
) -> str:
lines = [
f'node-name: "{node.name}"',
f'node-ip: "{node.address}"',
f'token: "{cluster_token}"',
]
if node.role == "control":
if node == config.primary_control:
lines.append("cluster-init: true")
else:
lines.append(f'server: "https://{config.primary_control.address}:6443"')
lines.extend(
[
'write-kubeconfig-mode: "0600"',
"secrets-encryption: true",
"tls-san:",
f' - "{config.primary_control.address}"',
"node-taint:",
' - "node-role.kubernetes.io/control-plane=true:NoSchedule"',
]
)
elif node.role == "worker":
lines.extend(
[
f'server: "https://{config.primary_control.address}:6443"',
"node-label:",
' - "govoplan.add-ideas.de/runtime=true"',
f' - "topology.govoplan.add-ideas.de/failure-domain={node.failure_domain}"',
]
)
else:
raise ValueError("state nodes do not receive K3s configuration")
return "\n".join(lines) + "\n"
def render_registry_config(username: str, password: str) -> str:
if not username and not password:
return ""
if not username or not password:
raise ValueError("registry username and password must be supplied together")
return (
'mirrors:\n "git.add-ideas.de":\n'
' endpoint:\n - "https://git.add-ideas.de"\n'
'configs:\n "git.add-ideas.de":\n auth:\n'
f" username: {json.dumps(username)}\n"
f" password: {json.dumps(password)}\n"
)
def render_garage_config() -> str:
return """metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
[admin]
api_bind_addr = "[::]:3903"
"""
def render_caddyfile() -> str:
return """:9443 {
tls /etc/caddy/tls/server.crt /etc/caddy/tls/server.key
reverse_proxy garage:3900
}
"""
def render_state_compose(images: Mapping[str, str]) -> str:
required = {"postgres", "redis", "garage", "managed_ingress", "test_mail"}
missing = sorted(required - set(images))
if missing:
raise ValueError("release manifest is missing state images: " + ", ".join(missing))
value = {
"name": "govoplan-lab-state",
"services": {
"postgres": {
"image": images["postgres"],
"restart": "unless-stopped",
"environment": {
"POSTGRES_DB": "${POSTGRES_DB}",
"POSTGRES_USER": "${POSTGRES_USER}",
"POSTGRES_PASSWORD": "${POSTGRES_PASSWORD}",
},
"ports": ["5432:5432"],
"healthcheck": {
"test": [
"CMD-SHELL",
'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"',
],
"interval": "5s",
"timeout": "3s",
"retries": 30,
},
"volumes": ["postgres-data:/var/lib/postgresql/data"],
"networks": ["internal"],
},
"redis": {
"image": images["redis"],
"restart": "unless-stopped",
"command": [
"sh",
"-ec",
'exec redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"',
],
"environment": {"REDIS_PASSWORD": "${REDIS_PASSWORD}"},
"ports": ["6379:6379"],
"healthcheck": {
"test": [
"CMD-SHELL",
'redis-cli -a "$${REDIS_PASSWORD}" --no-auth-warning ping',
],
"interval": "5s",
"timeout": "3s",
"retries": 30,
},
"volumes": ["redis-data:/data"],
"networks": ["internal"],
},
"garage": {
"image": images["garage"],
"restart": "unless-stopped",
"command": ["/garage", "server", "--single-node", "--default-bucket"],
"environment": {
"GARAGE_DEFAULT_ACCESS_KEY": "${GARAGE_DEFAULT_ACCESS_KEY}",
"GARAGE_DEFAULT_SECRET_KEY": "${GARAGE_DEFAULT_SECRET_KEY}",
"GARAGE_DEFAULT_BUCKET": "${GARAGE_DEFAULT_BUCKET}",
"GARAGE_RPC_SECRET": "${GARAGE_RPC_SECRET}",
"GARAGE_ADMIN_TOKEN": "${GARAGE_ADMIN_TOKEN}",
"GARAGE_METRICS_TOKEN": "${GARAGE_METRICS_TOKEN}",
},
"healthcheck": {
"test": ["CMD", "/garage", "status"],
"interval": "10s",
"timeout": "5s",
"retries": 30,
"start_period": "15s",
},
"security_opt": ["no-new-privileges:true"],
"volumes": [
"./garage.toml:/etc/garage.toml:ro",
"garage-meta:/var/lib/garage/meta",
"garage-data:/var/lib/garage/data",
],
"networks": ["internal"],
},
"s3-tls": {
"image": images["managed_ingress"],
"restart": "unless-stopped",
"depends_on": {"garage": {"condition": "service_healthy"}},
"ports": ["9443:9443"],
"volumes": [
"./Caddyfile:/etc/caddy/Caddyfile:ro",
"./tls:/etc/caddy/tls:ro",
],
"security_opt": ["no-new-privileges:true"],
"networks": ["internal"],
},
"test-mail": {
"image": images["test_mail"],
"restart": "unless-stopped",
"environment": {
"GREENMAIL_OPTS": (
"-Dgreenmail.setup.test.smtp -Dgreenmail.setup.test.imap "
"-Dgreenmail.hostname=0.0.0.0"
)
},
"ports": ["3025:3025", "3143:3143"],
"networks": ["internal"],
},
},
"volumes": {
"postgres-data": {},
"redis-data": {},
"garage-meta": {},
"garage-data": {},
},
"networks": {"internal": {"driver": "bridge"}},
}
return json.dumps(value, indent=2, sort_keys=True) + "\n"
def render_state_environment(values: Mapping[str, str]) -> str:
return "".join(f"{key}={_env_quote(value)}\n" for key, value in sorted(values.items()))
def render_secret_manifest(
*, namespace: str, name: str, values: Mapping[str, bytes | str]
) -> bytes:
encoded = {
key: base64.b64encode(value.encode() if isinstance(value, str) else value).decode()
for key, value in sorted(values.items())
}
payload = {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {"name": name, "namespace": namespace},
"type": "Opaque",
"data": encoded,
}
return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode()
def render_tls_secret_manifest(
*, namespace: str, name: str, certificate: bytes, private_key: bytes
) -> bytes:
payload = json.loads(
render_secret_manifest(
namespace=namespace,
name=name,
values={"tls.crt": certificate, "tls.key": private_key},
)
)
payload["type"] = "kubernetes.io/tls"
return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode()
def render_hosts(config: LabConfig) -> str:
return (
f"{config.primary_control.address} {config.public_host}\n"
f"{config.state_node.address} {config.s3_host}\n"
)
def _env_quote(value: str) -> str:
return json.dumps(value, ensure_ascii=True)
def write_private(path: Path, value: str | bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
data = value.encode() if isinstance(value, str) else value
path.write_bytes(data)
path.chmod(0o600)