Release v0.1.16
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user