diff --git a/.gitignore b/.gitignore index 2e3cc57..ebf0b61 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ secrets.auto.tfvars inventory/group_vars/all/serguzim.net.yml inventory/group_vars/all/opentofu.yml inventory/group_vars/all/all_services.yml + +infrastructure.svg diff --git a/Makefile b/Makefile index e40dffd..fd5d543 100644 --- a/Makefile +++ b/Makefile @@ -52,3 +52,10 @@ all: $(MAKE) dns @printf "\n=====\n\n" ansible-playbook ./playbooks/serguzim.net.yml -t $(TAGS) + +visualize: + tofu output --json \ + | jq 'with_entries(.value |= .value)' \ + | ./visualize.py \ + | d2 - infrastructure.svg + diff --git a/shell.nix b/shell.nix index 781e61d..ab65f96 100644 --- a/shell.nix +++ b/shell.nix @@ -3,6 +3,7 @@ mkShell { nativeBuildInputs = [ ansible ansible-lint + d2 dnscontrol opentofu ]; diff --git a/visualize.py b/visualize.py new file mode 100755 index 0000000..9287d03 --- /dev/null +++ b/visualize.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +import json +import sys + +icon_overrides = { + "acme_dns": "lets-encrypt", + "extra_services": None, + "faas": None, + "forgejo_runner": "forgejo", + "healthcheck": "healthchecks", + "mailcowdockerized": "mailcow", + "reitanlage_oranienburg": "grav", + "tandoor": "tandoor-recipes", + "teamspeak_fallback": None, + "tinytinyrss": "tiny-tiny-rss", + "wiki_js": "wiki-js", + "woodpecker": None, +} + +template_host = """ +'serguzim.net'.{host}: {{ + label: {host} +}} +""" + +template_service = """ +{key}: {{ + label: {label} + label.near: top-left + icon: https://cdn.jsdelivr.net/gh/selfhst/icons/webp/{icon}.webp + icon.near: top-right +}} +""" + +default_d2 = """ +vars: { + d2-config: { + layout-engine: elk + # Terminal theme code + theme-id: 101 + } +} + +external: { + scaleway: { + s3 + } + + restic: { + icon: https://cdn.jsdelivr.net/gh/selfhst/icons/webp/restic.webp + } +} +""" + +def service_key(svc, data): + return f"'serguzim.net'.{data['host']}.{svc}" + +def service_key_find(svc_name, services): + for svc, data in services.items(): + if svc == svc_name: + return service_key(svc, data) + return None + +def parse_hosts(hosts): + result = [] + for host in hosts.keys(): + result.append(template_host.format(host=host)) + return result + +def parse_services(services): + result = [] + + postgresql_key = service_key_find("postgresql", services) + authentik_key = service_key_find("authentik", services) + + result.append(f"{postgresql_key}.grid-columns: 3") + result.append(f"{postgresql_key}.grid-gap: 0") + result.append(f"{authentik_key}.grid-columns: 3") + result.append(f"{authentik_key}.grid-gap: 0") + + for svc, data in services.items(): + svc_key = service_key(svc, data) + + domains = [] + if data.get("dns"): + domains = [] + for dns in data["dns"]: + domain = "" + if dns.get("target") != "@": + domain += f"{dns["target"]}." + domain += dns['domain'] + domains.append(f"- {domain}") + + result.append(template_service.format( + key=svc_key, + label="\\n".join([svc] + domains), + icon=icon_overrides.get(svc, svc) or "docker", + )) + + for backup in data.get("backup") or []: + result.append(f'({svc_key} -> external.restic.{data['host']}).style.stroke: "#0f0"') + + if data.get("database"): + result.append(f"({svc_key} -> {postgresql_key}).style.stroke: '#00f'") + result.append(f"{postgresql_key}.{svc}") + if data.get("auth"): + result.append(f"({svc_key} -> {authentik_key}).style.stroke: '#FD4B2D'") + result.append(f"{authentik_key}.{svc}") + if data.get("s3"): + result.append(f"({svc_key} -> external.scaleway.s3).style.stroke: '#bbb'") + result.append(f"external.scaleway.s3.{svc}") + return result + +if __name__ == '__main__': + hosts = [] + services = [] + + data = json.loads(sys.stdin.read()) + + hosts = parse_hosts(data["hosts"]) + services = parse_services(data["services"]) + + print("\n".join( + [default_d2] + + hosts + + services))