"""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)