#!/usr/bin/env python3 """Generate Terragrunt service stack files for all app-level services.""" import os import textwrap REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Each service: (module_name, source_dir, [(arg_name, var_expr), ...], tier) # var_expr is what goes on the right side of = in the module call. # If var_expr starts with "var.", it's a variable passthrough and we declare the variable. # If it's a literal string, we inline it. # Special: "LOCAL_TIER" means we use local.tiers. SERVICES = [ ("blog", "blog", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("descheduler", "descheduler", []), ("f1-stream", "f1-stream", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("turn_secret", "var.coturn_turn_secret"), ("public_ip", "var.public_ip"), ]), ("coturn", "coturn", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ("turn_secret", "var.coturn_turn_secret"), ("public_ip", "var.public_ip"), ]), ("hackmd", "hackmd", [ ("hackmd_db_password", "var.hackmd_db_password"), ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ]), ("kms", "kms", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("k8s-dashboard", "k8s-dashboard", [ ("tier", "LOCAL_TIER:cluster"), ("tls_secret_name", "var.tls_secret_name"), ("client_certificate_secret_name", "var.client_certificate_secret_name"), ]), ("privatebin", "privatebin", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ]), ("reloader", "reloader", [ ("tier", "LOCAL_TIER:aux"), ]), ("shadowsocks", "shadowsocks", [ ("password", "var.shadowsocks_password"), ("tier", "LOCAL_TIER:edge"), ]), ("city-guesser", "city-guesser", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("echo", "echo", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ]), ("url", "url-shortener", [ ("tls_secret_name", "var.tls_secret_name"), ("geolite_license_key", "var.url_shortener_geolite_license_key"), ("api_key", "var.url_shortener_api_key"), ("mysql_password", "var.url_shortener_mysql_password"), ("tier", "LOCAL_TIER:aux"), ]), ("webhook_handler", "webhook_handler", [ ("tls_secret_name", "var.tls_secret_name"), ("webhook_secret", "var.webhook_handler_secret"), ("fb_verify_token", "var.webhook_handler_fb_verify_token"), ("fb_page_token", "var.webhook_handler_fb_page_token"), ("fb_app_secret", "var.webhook_handler_fb_app_secret"), ("git_user", "var.webhook_handler_git_user"), ("git_token", "var.webhook_handler_git_token"), ("ssh_key", "var.webhook_handler_ssh_key"), ("tier", "LOCAL_TIER:aux"), ]), ("excalidraw", "excalidraw", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("travel_blog", "travel_blog", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("dashy", "dashy", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("send", "send", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("ytdlp", "youtube_dl", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("openrouter_api_key", "var.openrouter_api_key"), ("slack_bot_token", "var.slack_bot_token"), ("slack_channel", "var.slack_channel"), ]), ("immich", "immich", [ ("tls_secret_name", "var.tls_secret_name"), ("postgresql_password", "var.immich_postgresql_password"), ("frame_api_key", "var.immich_frame_api_key"), ("homepage_token", 'var.homepage_credentials["immich"]["token"]'), ("tier", "LOCAL_TIER:gpu"), ]), ("resume", "resume", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("database_url", "var.resume_database_url"), ("auth_secret", "var.resume_auth_secret"), ("smtp_password", 'var.mailserver_accounts["info@viktorbarzin.me"]'), ]), ("frigate", "frigate", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:gpu"), ]), ("paperless-ngx", "paperless-ngx", [ ("tls_secret_name", "var.tls_secret_name"), ("db_password", "var.paperless_db_password"), ("homepage_username", 'var.homepage_credentials["paperless-ngx"]["username"]'), ("homepage_password", 'var.homepage_credentials["paperless-ngx"]["password"]'), ("tier", "LOCAL_TIER:edge"), ]), ("jsoncrack", "jsoncrack", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("servarr", "servarr", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("aiostreams_database_connection_string", "var.aiostreams_database_connection_string"), ]), ("ollama", "ollama", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:gpu"), ("ollama_api_credentials", "var.ollama_api_credentials"), ]), ("ntfy", "ntfy", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("cyberchef", "cyberchef", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("diun", "diun", [ ("tls_secret_name", "var.tls_secret_name"), ("diun_nfty_token", "var.diun_nfty_token"), ("diun_slack_url", "var.diun_slack_url"), ("tier", "LOCAL_TIER:aux"), ]), ("meshcentral", "meshcentral", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("netbox", "netbox", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("nextcloud", "nextcloud", [ ("tls_secret_name", "var.tls_secret_name"), ("db_password", "var.nextcloud_db_password"), ("tier", "LOCAL_TIER:edge"), ]), ("homepage", "homepage", [ ("tier", "LOCAL_TIER:aux"), ("tls_secret_name", "var.tls_secret_name"), ]), ("matrix", "matrix", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("linkwarden", "linkwarden", [ ("tls_secret_name", "var.tls_secret_name"), ("postgresql_password", "var.linkwarden_postgresql_password"), ("authentik_client_id", "var.linkwarden_authentik_client_id"), ("authentik_client_secret", "var.linkwarden_authentik_client_secret"), ("tier", "LOCAL_TIER:aux"), ]), ("actualbudget", "actualbudget", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ("credentials", "var.actualbudget_credentials"), ]), ("owntracks", "owntracks", [ ("tls_secret_name", "var.tls_secret_name"), ("owntracks_credentials", "var.owntracks_credentials"), ("tier", "LOCAL_TIER:aux"), ]), ("dawarich", "dawarich", [ ("tls_secret_name", "var.tls_secret_name"), ("database_password", "var.dawarich_database_password"), ("geoapify_api_key", "var.geoapify_api_key"), ("tier", "LOCAL_TIER:edge"), ]), ("changedetection", "changedetection", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("tandoor", "tandoor", [ ("tls_secret_name", "var.tls_secret_name"), ("tandoor_database_password", "var.tandoor_database_password"), ("tandoor_email_password", "var.tandoor_email_password"), ("tier", "LOCAL_TIER:aux"), ]), ("n8n", "n8n", [ ("tls_secret_name", "var.tls_secret_name"), ("postgresql_password", "var.n8n_postgresql_password"), ("tier", "LOCAL_TIER:aux"), ]), ("real-estate-crawler", "real-estate-crawler", [ ("tls_secret_name", "var.tls_secret_name"), ("db_password", "var.realestate_crawler_db_password"), ("notification_settings", "var.realestate_crawler_notification_settings"), ("tier", "LOCAL_TIER:aux"), ]), ("osm_routing", "osm-routing", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("tor-proxy", "tor-proxy", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("onlyoffice", "onlyoffice", [ ("tls_secret_name", "var.tls_secret_name"), ("db_password", "var.onlyoffice_db_password"), ("jwt_token", "var.onlyoffice_jwt_token"), ("tier", "LOCAL_TIER:edge"), ]), ("forgejo", "forgejo", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:edge"), ]), ("freshrss", "freshrss", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("navidrome", "navidrome", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("networking-toolbox", "networking-toolbox", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("tuya-bridge", "tuya-bridge", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:cluster"), ("tiny_tuya_api_key", "var.tiny_tuya_api_key"), ("tiny_tuya_api_secret", "var.tiny_tuya_api_secret"), ("tiny_tuya_service_secret", "var.tiny_tuya_service_secret"), ("slack_url", "var.tiny_tuya_slack_url"), ]), ("stirling-pdf", "stirling-pdf", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("isponsorblocktv", "isponsorblocktv", [ ("tier", "LOCAL_TIER:edge"), ]), ("ebook2audiobook", "ebook2audiobook", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:gpu"), ]), ("rybbit", "rybbit", [ ("tls_secret_name", "var.tls_secret_name"), ("clickhouse_password", "var.clickhouse_password"), ("postgres_password", "var.clickhouse_postgres_password"), ("tier", "LOCAL_TIER:aux"), ]), ("wealthfolio", "wealthfolio", [ ("tls_secret_name", "var.tls_secret_name"), ("wealthfolio_password_hash", "var.wealthfolio_password_hash"), ("tier", "LOCAL_TIER:aux"), ]), ("speedtest", "speedtest", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("db_password", "var.speedtest_db_password"), ]), ("freedify", "freedify", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ("additional_credentials", "var.freedify_credentials"), ]), ("affine", "affine", [ ("tls_secret_name", "var.tls_secret_name"), ("postgresql_password", "var.affine_postgresql_password"), ("smtp_password", 'var.mailserver_accounts["info@viktorbarzin.me"]'), ("tier", "LOCAL_TIER:aux"), ]), ("plotting-book", "plotting-book", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:aux"), ]), ("health", "health", [ ("tls_secret_name", "var.tls_secret_name"), ("postgresql_password", "var.health_postgresql_password"), ("secret_key", "var.health_secret_key"), ("tier", "LOCAL_TIER:aux"), ]), ("whisper", "whisper", [ ("tls_secret_name", "var.tls_secret_name"), ("tier", "LOCAL_TIER:gpu"), ]), ("grampsweb", "grampsweb", [ ("tls_secret_name", "var.tls_secret_name"), ("smtp_password", 'var.mailserver_accounts["info@viktorbarzin.me"]'), ("tier", "LOCAL_TIER:aux"), ]), ("openclaw", "openclaw", [ ("tls_secret_name", "var.tls_secret_name"), ("ssh_key", "var.openclaw_ssh_key"), ("skill_secrets", "var.openclaw_skill_secrets"), ("gemini_api_key", "var.gemini_api_key"), ("llama_api_key", "var.llama_api_key"), ("brave_api_key", "var.brave_api_key"), ("modal_api_key", "var.modal_api_key"), ("tier", "LOCAL_TIER:aux"), ]), ] # Variable type overrides (var_name -> type declaration) VAR_TYPES = { "tls_secret_name": "string", "client_certificate_secret_name": "string", "public_ip": "string", "hackmd_db_password": "string", "shadowsocks_password": "string", "openrouter_api_key": "string", "slack_bot_token": "string", "slack_channel": "string", "ollama_api_credentials": "string", "clickhouse_password": "string", "clickhouse_postgres_password": "string", "wealthfolio_password_hash": "string", "speedtest_db_password": "string", "affine_postgresql_password": "string", "health_postgresql_password": "string", "health_secret_key": "string", "gemini_api_key": "string", "llama_api_key": "string", "brave_api_key": "string", "modal_api_key": "string", "coturn_turn_secret": "string", "onlyoffice_db_password": "string", "onlyoffice_jwt_token": "string", "resume_database_url": "string", "resume_auth_secret": "string", "nextcloud_db_password": "string", "paperless_db_password": "string", "diun_nfty_token": "string", "diun_slack_url": "string", "dawarich_database_password": "string", "geoapify_api_key": "string", "tandoor_database_password": "string", "tandoor_email_password": "string", "n8n_postgresql_password": "string", "realestate_crawler_db_password": "string", "immich_postgresql_password": "string", "immich_frame_api_key": "string", "linkwarden_postgresql_password": "string", "linkwarden_authentik_client_id": "string", "linkwarden_authentik_client_secret": "string", "aiostreams_database_connection_string": "string", "tiny_tuya_api_key": "string", "tiny_tuya_api_secret": "string", "tiny_tuya_service_secret": "string", "tiny_tuya_slack_url": "string", "url_shortener_geolite_license_key": "string", "url_shortener_api_key": "string", "url_shortener_mysql_password": "string", "webhook_handler_secret": "string", "webhook_handler_fb_verify_token": "string", "webhook_handler_fb_page_token": "string", "webhook_handler_fb_app_secret": "string", "webhook_handler_git_user": "string", "webhook_handler_git_token": "string", "webhook_handler_ssh_key": "string", "openclaw_ssh_key": "string", "openclaw_skill_secrets": "map(string)", "actualbudget_credentials": "map(any)", "freedify_credentials": "map(any)", "realestate_crawler_notification_settings": "map(string)", "homepage_credentials": "map(any)", "mailserver_accounts": "map(any)", "owntracks_credentials": "string", } TERRAGRUNT_HCL = """\ include "root" { path = find_in_parent_folders() } dependency "platform" { config_path = "../platform" skip_outputs = true } """ TIERS_BLOCK = """\ locals { tiers = { core = "0-core" cluster = "1-cluster" gpu = "2-gpu" edge = "3-edge" aux = "4-aux" } } """ def extract_var_name(expr): """Extract variable name from var.xxx or var.xxx["yyy"]["zzz"].""" if not expr.startswith("var."): return None # Get the base variable name (before any indexing) name = expr[4:] bracket = name.find("[") if bracket != -1: name = name[:bracket] return name def gen_main_tf(mod_name, source_dir, args): """Generate main.tf content for a service stack.""" lines = [] # Collect variables needed vars_needed = {} needs_tiers = False for arg_name, var_expr in args: if var_expr.startswith("LOCAL_TIER:"): needs_tiers = True continue vname = extract_var_name(var_expr) if vname and vname not in vars_needed: vtype = VAR_TYPES.get(vname, None) vars_needed[vname] = vtype # Variable declarations for vname, vtype in vars_needed.items(): if vtype: lines.append(f'variable "{vname}" {{ type = {vtype} }}') else: lines.append(f'variable "{vname}" {{}}') if vars_needed: lines.append("") # Tiers block if needed if needs_tiers: lines.append(TIERS_BLOCK) # Module call lines.append(f'module "{mod_name}" {{') lines.append(f' source = "../../modules/kubernetes/{source_dir}"') for arg_name, var_expr in args: if var_expr.startswith("LOCAL_TIER:"): tier = var_expr.split(":")[1] val = f"local.tiers.{tier}" else: val = var_expr # Pad for alignment lines.append(f" {arg_name:30s} = {val}") lines.append("}") lines.append("") return "\n".join(lines) def main(): stacks_dir = os.path.join(REPO_ROOT, "stacks") for mod_name, source_dir, args in SERVICES: # Use source_dir as the stack directory name for consistency # But some modules have different names than source dirs # Use the module name for the stack dir stack_dir = os.path.join(stacks_dir, mod_name) os.makedirs(stack_dir, exist_ok=True) # terragrunt.hcl tg_path = os.path.join(stack_dir, "terragrunt.hcl") with open(tg_path, "w") as f: f.write(TERRAGRUNT_HCL) # main.tf main_path = os.path.join(stack_dir, "main.tf") with open(main_path, "w") as f: f.write(gen_main_tf(mod_name, source_dir, args)) # secrets symlink secrets_link = os.path.join(stack_dir, "secrets") if not os.path.exists(secrets_link): os.symlink("../../secrets", secrets_link) print(f" Created stacks/{mod_name}/") print(f"\nGenerated {len(SERVICES)} service stacks") if __name__ == "__main__": main()