From e6420c7b3601992a42c58d673714f668142fda08 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 22 Feb 2026 14:38:14 +0000 Subject: [PATCH] [ci skip] Move Terraform modules into stack directories Move all 88 service modules (66 individual + 22 platform) from modules/kubernetes// into their corresponding stack directories: - Service stacks: stacks//module/ - Platform stack: stacks/platform/modules// This collocates module source code with its Terragrunt definition. Only shared utility modules remain in modules/kubernetes/: ingress_factory, setup_tls_secret, dockerhub_secret, oauth-proxy. All cross-references to shared modules updated to use correct relative paths. Verified with terragrunt run --all -- plan: 0 adds, 0 destroys across all 68 stacks. --- stacks/actualbudget/.terraform.lock.hcl | 59 + stacks/actualbudget/backend.tf | 6 + stacks/actualbudget/main.tf | 8 +- .../actualbudget/module}/factory/main.tf | 2 +- .../actualbudget/module}/main.tf | 2 +- stacks/actualbudget/providers.tf | 15 + stacks/actualbudget/secrets | 1 + stacks/affine/.terraform.lock.hcl | 40 + stacks/affine/backend.tf | 6 + stacks/affine/main.tf | 10 +- .../affine => stacks/affine/module}/main.tf | 4 +- stacks/affine/providers.tf | 15 + stacks/affine/secrets | 1 + stacks/audiobookshelf/.terraform.lock.hcl | 40 + stacks/audiobookshelf/backend.tf | 6 + stacks/audiobookshelf/main.tf | 6 +- .../audiobookshelf/module}/main.tf | 4 +- stacks/audiobookshelf/providers.tf | 15 + stacks/audiobookshelf/secrets | 1 + stacks/blog/.terraform.lock.hcl | 40 + stacks/blog/backend.tf | 6 + stacks/blog/main.tf | 6 +- .../blog => stacks/blog/module}/main.tf | 8 +- stacks/blog/providers.tf | 15 + stacks/blog/secrets | 1 + stacks/calibre/.terraform.lock.hcl | 40 + stacks/calibre/backend.tf | 6 + stacks/calibre/main.tf | 10 +- .../calibre => stacks/calibre/module}/main.tf | 6 +- stacks/calibre/providers.tf | 15 + stacks/calibre/secrets | 1 + stacks/changedetection/.terraform.lock.hcl | 40 + stacks/changedetection/backend.tf | 6 + stacks/changedetection/main.tf | 6 +- .../changedetection/module}/main.tf | 4 +- stacks/changedetection/providers.tf | 15 + stacks/changedetection/secrets | 1 + stacks/city-guesser/.terraform.lock.hcl | 40 + stacks/city-guesser/backend.tf | 6 + stacks/city-guesser/main.tf | 6 +- stacks/city-guesser/module/main.tf | 154 + stacks/city-guesser/providers.tf | 15 + stacks/city-guesser/secrets | 1 + stacks/coturn/.terraform.lock.hcl | 40 + stacks/coturn/backend.tf | 6 + stacks/coturn/main.tf | 10 +- .../coturn => stacks/coturn/module}/main.tf | 2 +- stacks/coturn/providers.tf | 15 + stacks/coturn/secrets | 1 + stacks/cyberchef/.terraform.lock.hcl | 40 + stacks/cyberchef/backend.tf | 6 + stacks/cyberchef/main.tf | 6 +- .../cyberchef/module}/main.tf | 4 +- stacks/cyberchef/providers.tf | 15 + stacks/cyberchef/secrets | 1 + stacks/dashy/.terraform.lock.hcl | 40 + stacks/dashy/backend.tf | 6 + stacks/dashy/main.tf | 6 +- .../dashy => stacks/dashy/module}/conf.yml | 0 .../dashy => stacks/dashy/module}/main.tf | 4 +- stacks/dashy/providers.tf | 15 + stacks/dashy/secrets | 1 + stacks/dawarich/.terraform.lock.hcl | 40 + stacks/dawarich/backend.tf | 6 + stacks/dawarich/main.tf | 10 +- .../dawarich/module}/main.tf | 4 +- stacks/dawarich/providers.tf | 15 + stacks/dawarich/secrets | 1 + stacks/descheduler/.terraform.lock.hcl | 40 + stacks/descheduler/backend.tf | 6 + stacks/descheduler/main.tf | 2 +- .../descheduler/module}/main.tf | 0 .../descheduler/module}/values.yaml | 0 stacks/descheduler/providers.tf | 15 + stacks/descheduler/secrets | 1 + stacks/diun/.terraform.lock.hcl | 40 + stacks/diun/backend.tf | 6 + stacks/diun/main.tf | 10 +- .../diun => stacks/diun/module}/main.tf | 2 +- stacks/diun/providers.tf | 15 + stacks/diun/secrets | 1 + stacks/drone/.terraform.lock.hcl | 40 + stacks/drone/backend.tf | 6 + stacks/drone/main.tf | 20 +- .../drone => stacks/drone/module}/main.tf | 8 +- stacks/drone/providers.tf | 15 + stacks/drone/secrets | 1 + stacks/ebook2audiobook/.terraform.lock.hcl | 40 + stacks/ebook2audiobook/backend.tf | 6 + stacks/ebook2audiobook/main.tf | 6 +- .../ebook2audiobook/module}/audiblez-web | 0 .../ebook2audiobook/module}/main.tf | 6 +- stacks/ebook2audiobook/providers.tf | 15 + stacks/ebook2audiobook/secrets | 1 + stacks/echo/.terraform.lock.hcl | 40 + stacks/echo/backend.tf | 6 + stacks/echo/main.tf | 6 +- .../echo => stacks/echo/module}/main.tf | 4 +- stacks/echo/providers.tf | 15 + stacks/echo/secrets | 1 + stacks/excalidraw/.terraform.lock.hcl | 40 + stacks/excalidraw/backend.tf | 6 + stacks/excalidraw/main.tf | 6 +- .../excalidraw/module}/main.tf | 4 +- .../excalidraw/module}/project/.gitignore | 0 .../excalidraw/module}/project/Dockerfile | 0 .../excalidraw/module}/project/README.md | 0 .../excalidraw/module}/project/go.mod | 0 .../excalidraw/module}/project/main.go | 0 .../module}/project/static/editor.html | 0 stacks/excalidraw/providers.tf | 15 + stacks/excalidraw/secrets | 1 + stacks/f1-stream/.terraform.lock.hcl | 40 + stacks/f1-stream/backend.tf | 6 + stacks/f1-stream/main.tf | 10 +- ...-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK | 3 + .../f1-stream/module}/files/.dockerignore | 0 .../module/files/.planning/PROJECT.md | 78 + .../module/files/.planning/REQUIREMENTS.md | 115 + .../module/files/.planning/ROADMAP.md | 106 + .../f1-stream/module/files/.planning/STATE.md | 93 + .../files/.planning/codebase/ARCHITECTURE.md | 191 + .../files/.planning/codebase/CONCERNS.md | 232 + .../files/.planning/codebase/CONVENTIONS.md | 159 + .../files/.planning/codebase/INTEGRATIONS.md | 121 + .../module/files/.planning/codebase/STACK.md | 109 + .../files/.planning/codebase/STRUCTURE.md | 202 + .../files/.planning/codebase/TESTING.md | 256 + .../module/files/.planning/config.json | 12 + .../01-scraper-validation/01-01-PLAN.md | 237 + .../01-scraper-validation/01-01-SUMMARY.md | 107 + .../01-scraper-validation/01-RESEARCH.md | 599 + .../01-scraper-validation/01-VERIFICATION.md | 200 + .../02-01-PLAN.md | 225 + .../02-01-SUMMARY.md | 106 + .../02-02-PLAN.md | 197 + .../02-02-SUMMARY.md | 101 + .../02-RESEARCH.md | 801 + .../02-VERIFICATION.md | 123 + .../03-auto-publish-pipeline/03-01-PLAN.md | 186 + .../03-auto-publish-pipeline/03-01-SUMMARY.md | 105 + .../03-VERIFICATION.md | 115 + .../04-01-PLAN.md | 183 + .../04-01-SUMMARY.md | 114 + .../04-02-PLAN.md | 218 + .../04-02-SUMMARY.md | 107 + .../04-VERIFICATION.md | 108 + .../05-sandbox-proxy-hardening/05-01-PLAN.md | 147 + .../05-01-SUMMARY.md | 108 + .../05-sandbox-proxy-hardening/05-02-PLAN.md | 240 + .../05-02-SUMMARY.md | 102 + .../05-VERIFICATION.md | 170 + .../f1-stream/module}/files/Dockerfile | 0 .../f1-stream/module}/files/go.mod | 0 .../f1-stream/module}/files/go.sum | 0 .../f1-stream/module}/files/index.html | 0 .../module}/files/internal/auth/auth.go | 0 .../module}/files/internal/auth/context.go | 0 .../files/internal/extractor/browser.go | 0 .../files/internal/extractor/capture.go | 0 .../files/internal/extractor/session.go | 0 .../files/internal/extractor/webrtc.go | 0 .../files/internal/healthcheck/healthcheck.go | 0 .../files/internal/hlsproxy/hlsproxy.go | 0 .../module}/files/internal/models/models.go | 0 .../internal/playerconfig/playerconfig.go | 0 .../module}/files/internal/proxy/proxy.go | 0 .../module}/files/internal/scraper/reddit.go | 0 .../module}/files/internal/scraper/scraper.go | 0 .../files/internal/scraper/validate.go | 0 .../files/internal/scraper/validate_test.go | 0 .../files/internal/server/middleware.go | 0 .../module}/files/internal/server/server.go | 0 .../module}/files/internal/store/health.go | 0 .../module}/files/internal/store/scraped.go | 0 .../module}/files/internal/store/sessions.go | 0 .../module}/files/internal/store/store.go | 0 .../module}/files/internal/store/streams.go | 0 .../module}/files/internal/store/users.go | 0 .../f1-stream/module}/files/main.go | 0 .../files/node_modules/.package-lock.json | 6 + .../f1-stream/module/files/package-lock.json | 6 + stacks/f1-stream/module/files/package.json | 1 + .../f1-stream/module}/files/redeploy.sh | 0 .../module}/files/static/css/custom.css | 0 .../module}/files/static/css/pico.min.css | 0 .../f1-stream/module}/files/static/index.html | 0 .../f1-stream/module}/files/static/js/app.js | 0 .../f1-stream/module}/files/static/js/auth.js | 0 .../module}/files/static/js/player.js | 0 .../module}/files/static/js/streams.js | 0 .../module}/files/static/js/utils.js | 0 .../f1-stream/module}/main.tf | 4 +- stacks/f1-stream/providers.tf | 15 + stacks/f1-stream/secrets | 1 + stacks/forgejo/.terraform.lock.hcl | 40 + stacks/forgejo/backend.tf | 6 + stacks/forgejo/main.tf | 6 +- .../forgejo => stacks/forgejo/module}/main.tf | 4 +- stacks/forgejo/providers.tf | 15 + stacks/forgejo/secrets | 1 + stacks/freedify/.terraform.lock.hcl | 40 + stacks/freedify/backend.tf | 6 + stacks/freedify/main.tf | 8 +- .../freedify/module}/factory/main.tf | 2 +- .../freedify/module}/main.tf | 2 +- stacks/freedify/providers.tf | 15 + stacks/freedify/secrets | 1 + stacks/freshrss/.terraform.lock.hcl | 40 + stacks/freshrss/backend.tf | 6 + stacks/freshrss/main.tf | 6 +- .../freshrss/module}/main.tf | 4 +- stacks/freshrss/providers.tf | 15 + stacks/freshrss/secrets | 1 + stacks/frigate/.terraform.lock.hcl | 40 + stacks/frigate/backend.tf | 6 + stacks/frigate/main.tf | 6 +- .../frigate => stacks/frigate/module}/main.tf | 6 +- stacks/frigate/providers.tf | 15 + stacks/frigate/secrets | 1 + stacks/grampsweb/.terraform.lock.hcl | 59 + stacks/grampsweb/backend.tf | 6 + stacks/grampsweb/main.tf | 8 +- .../grampsweb/module}/main.tf | 4 +- stacks/grampsweb/providers.tf | 15 + stacks/grampsweb/secrets | 1 + stacks/hackmd/.terraform.lock.hcl | 40 + stacks/hackmd/backend.tf | 6 + stacks/hackmd/main.tf | 8 +- .../hackmd => stacks/hackmd/module}/main.tf | 4 +- stacks/hackmd/providers.tf | 15 + stacks/hackmd/secrets | 1 + stacks/health/.terraform.lock.hcl | 40 + stacks/health/backend.tf | 6 + stacks/health/main.tf | 10 +- .../health => stacks/health/module}/main.tf | 4 +- stacks/health/providers.tf | 15 + stacks/health/secrets | 1 + stacks/homepage/.terraform.lock.hcl | 40 + stacks/homepage/backend.tf | 6 + stacks/homepage/main.tf | 6 +- .../homepage/module}/main.tf | 2 +- .../homepage/module}/values.yaml | 0 stacks/homepage/providers.tf | 15 + stacks/homepage/secrets | 1 + stacks/immich/.terraform.lock.hcl | 40 + stacks/immich/backend.tf | 6 + stacks/immich/main.tf | 12 +- .../immich/module}/chart_values.tpl | 0 .../immich => stacks/immich/module}/frame.tf | 2 +- .../immich => stacks/immich/module}/main.tf | 6 +- stacks/immich/providers.tf | 15 + stacks/immich/secrets | 1 + stacks/infra/backend.tf | 6 + stacks/infra/providers.tf | 25 + stacks/isponsorblocktv/.terraform.lock.hcl | 40 + stacks/isponsorblocktv/backend.tf | 6 + stacks/isponsorblocktv/main.tf | 4 +- .../isponsorblocktv/module}/main.tf | 0 stacks/isponsorblocktv/providers.tf | 15 + stacks/isponsorblocktv/secrets | 1 + stacks/jsoncrack/.terraform.lock.hcl | 40 + stacks/jsoncrack/backend.tf | 6 + stacks/jsoncrack/main.tf | 6 +- .../jsoncrack/module}/main.tf | 4 +- stacks/jsoncrack/providers.tf | 15 + stacks/jsoncrack/secrets | 1 + stacks/k8s-dashboard/.terraform.lock.hcl | 59 + stacks/k8s-dashboard/backend.tf | 6 + stacks/k8s-dashboard/main.tf | 2 +- .../k8s-dashboard/module}/main.tf | 4 +- stacks/k8s-dashboard/providers.tf | 15 + stacks/k8s-dashboard/secrets | 1 + stacks/kms/.terraform.lock.hcl | 40 + stacks/kms/backend.tf | 6 + stacks/kms/main.tf | 6 +- .../kms => stacks/kms/module}/main.tf | 4 +- .../kms => stacks/kms/module}/variables.tf | 0 stacks/kms/providers.tf | 15 + stacks/kms/secrets | 1 + stacks/linkwarden/.terraform.lock.hcl | 59 + stacks/linkwarden/backend.tf | 6 + stacks/linkwarden/main.tf | 2 +- .../linkwarden/module}/main.tf | 4 +- stacks/linkwarden/providers.tf | 15 + stacks/linkwarden/secrets | 1 + stacks/matrix/.terraform.lock.hcl | 40 + stacks/matrix/backend.tf | 6 + stacks/matrix/main.tf | 2 +- .../matrix => stacks/matrix/module}/main.tf | 4 +- stacks/matrix/providers.tf | 15 + stacks/matrix/secrets | 1 + stacks/meshcentral/.terraform.lock.hcl | 40 + stacks/meshcentral/backend.tf | 6 + stacks/meshcentral/main.tf | 2 +- .../meshcentral/module}/main.tf | 4 +- stacks/meshcentral/providers.tf | 15 + stacks/meshcentral/secrets | 1 + stacks/n8n/.terraform.lock.hcl | 40 + stacks/n8n/backend.tf | 6 + stacks/n8n/main.tf | 2 +- .../n8n => stacks/n8n/module}/main.tf | 4 +- stacks/n8n/providers.tf | 15 + stacks/n8n/secrets | 1 + stacks/navidrome/.terraform.lock.hcl | 40 + stacks/navidrome/backend.tf | 6 + stacks/navidrome/main.tf | 2 +- .../navidrome/module}/main.tf | 4 +- stacks/navidrome/providers.tf | 15 + stacks/navidrome/secrets | 1 + stacks/netbox/.terraform.lock.hcl | 59 + stacks/netbox/backend.tf | 6 + stacks/netbox/main.tf | 2 +- .../netbox => stacks/netbox/module}/main.tf | 4 +- stacks/netbox/providers.tf | 15 + stacks/netbox/secrets | 1 + stacks/networking-toolbox/.terraform.lock.hcl | 40 + stacks/networking-toolbox/backend.tf | 6 + stacks/networking-toolbox/main.tf | 2 +- .../networking-toolbox/module}/main.tf | 4 +- stacks/networking-toolbox/providers.tf | 15 + stacks/networking-toolbox/secrets | 1 + stacks/nextcloud/.terraform.lock.hcl | 40 + stacks/nextcloud/backend.tf | 6 + stacks/nextcloud/main.tf | 2 +- .../nextcloud/module}/chart_values.yaml | 0 .../nextcloud/module}/main.tf | 6 +- stacks/nextcloud/providers.tf | 15 + stacks/nextcloud/secrets | 1 + stacks/ntfy/.terraform.lock.hcl | 40 + stacks/ntfy/backend.tf | 6 + stacks/ntfy/main.tf | 2 +- .../ntfy => stacks/ntfy/module}/main.tf | 4 +- stacks/ntfy/providers.tf | 15 + stacks/ntfy/secrets | 1 + stacks/ollama/.terraform.lock.hcl | 40 + stacks/ollama/backend.tf | 6 + stacks/ollama/main.tf | 2 +- .../ollama => stacks/ollama/module}/main.tf | 8 +- .../ollama/module}/values.yaml | 0 stacks/ollama/providers.tf | 15 + stacks/ollama/secrets | 1 + stacks/onlyoffice/.terraform.lock.hcl | 40 + stacks/onlyoffice/backend.tf | 6 + stacks/onlyoffice/main.tf | 2 +- .../onlyoffice/module}/main.tf | 4 +- stacks/onlyoffice/providers.tf | 15 + stacks/onlyoffice/secrets | 1 + stacks/openclaw/.terraform.lock.hcl | 59 + stacks/openclaw/backend.tf | 6 + stacks/openclaw/main.tf | 2 +- .../openclaw/module}/main.tf | 4 +- stacks/openclaw/providers.tf | 15 + stacks/openclaw/secrets | 1 + stacks/osm_routing/.terraform.lock.hcl | 40 + stacks/osm_routing/backend.tf | 6 + stacks/osm_routing/main.tf | 2 +- .../osm_routing/module}/main.tf | 0 stacks/osm_routing/providers.tf | 15 + stacks/osm_routing/secrets | 1 + stacks/owntracks/.terraform.lock.hcl | 40 + stacks/owntracks/backend.tf | 6 + stacks/owntracks/main.tf | 2 +- .../owntracks/module}/main.tf | 4 +- stacks/owntracks/providers.tf | 15 + stacks/owntracks/secrets | 1 + stacks/paperless-ngx/.terraform.lock.hcl | 40 + stacks/paperless-ngx/backend.tf | 6 + stacks/paperless-ngx/main.tf | 2 +- .../paperless-ngx/module}/main.tf | 4 +- stacks/paperless-ngx/providers.tf | 15 + stacks/paperless-ngx/secrets | 1 + stacks/platform/.terraform.lock.hcl | 82 + stacks/platform/backend.tf | 6 + stacks/platform/main.tf | 44 +- .../platform/modules}/authentik/main.tf | 6 +- .../platform/modules}/authentik/pgbouncer.ini | 0 .../platform/modules}/authentik/pgbouncer.tf | 0 .../platform/modules}/authentik/userlist.txt | 0 .../platform/modules}/authentik/values.yaml | 0 .../modules}/cloudflared/cloudflare.tf | 0 .../platform/modules}/cloudflared/main.tf | 2 +- .../crowdsec/crowdsec-ingress-bouncer.yaml | 0 .../platform/modules}/crowdsec/main.tf | 4 +- .../platform/modules}/crowdsec/values.yaml | 0 .../platform/modules}/dbaas/chart_values.tpl | 0 .../platform/modules}/dbaas/cluster.yaml | 0 stacks/platform/modules/dbaas/main.tf | 916 + .../modules}/dbaas/mysql_chart_values.yaml | 0 .../dbaas/postgres/postgres_Dockerfile | 0 .../platform/modules}/dbaas/versions.tf | 0 stacks/platform/modules/headscale/main.tf | 254 + .../modules}/infra-maintenance/main.tf | 0 .../modules}/k8s-portal/files/.gitignore | 0 .../platform/modules}/k8s-portal/files/.npmrc | 0 .../modules}/k8s-portal/files/Dockerfile | 0 .../modules}/k8s-portal/files/README.md | 0 .../k8s-portal/files/package-lock.json | 0 .../modules}/k8s-portal/files/package.json | 0 .../modules}/k8s-portal/files/src/app.d.ts | 0 .../modules}/k8s-portal/files/src/app.html | 0 .../files/src/lib/assets/favicon.svg | 0 .../k8s-portal/files/src/lib/index.ts | 0 .../files/src/routes/+layout.svelte | 0 .../files/src/routes/+page.server.ts | 0 .../k8s-portal/files/src/routes/+page.svelte | 0 .../files/src/routes/download/+server.ts | 0 .../files/src/routes/setup/+page.svelte | 0 .../files/src/routes/setup/script/+server.ts | 0 .../k8s-portal/files/static/robots.txt | 0 .../k8s-portal/files/svelte.config.js | 0 .../modules}/k8s-portal/files/tsconfig.json | 0 .../modules}/k8s-portal/files/vite.config.ts | 0 .../platform/modules}/k8s-portal/main.tf | 6 +- .../platform/modules}/kyverno/main.tf | 0 .../modules}/kyverno/resource-governance.tf | 0 .../modules}/mailserver/extra/aliases.txt | 0 .../platform/modules}/mailserver/main.tf | 2 +- .../modules}/mailserver/roundcubemail.tf | 2 +- .../platform/modules}/mailserver/variables.tf | 0 .../platform/modules}/metallb/main.tf | 0 .../platform/modules}/metrics-server/main.tf | 2 +- .../modules}/metrics-server/values.yaml | 0 .../platform/modules}/monitoring/Dockerfile | 0 .../platform/modules}/monitoring/alloy.yaml | 0 .../monitoring/dashboards/api_server.json | 0 .../monitoring/dashboards/cluster_health.json | 0 .../monitoring/dashboards/core_dns.json | 0 .../modules}/monitoring/dashboards/idrac.json | 0 .../monitoring/dashboards/k8s-audit.json | 0 .../dashboards/kube-state-metrics.json | 0 .../modules}/monitoring/dashboards/loki.json | 0 .../monitoring/dashboards/nginx_ingress.json | 0 .../dashboards/node_exporter_full.json | 0 .../modules}/monitoring/dashboards/nodes.json | 0 .../monitoring/dashboards/nvidia.json | 0 .../modules}/monitoring/dashboards/pods.json | 0 .../dashboards/proxmox_node_exporter.json | 0 .../dashboards/realestate-crawler.json | 976 + .../monitoring/dashboards/registry.json | 0 .../monitoring/dashboards/technitium-dns.json | 0 .../dashboards/ups-prometheus-metrics.yml | 0 .../modules}/monitoring/dashboards/ups.json | 0 .../platform/modules}/monitoring/grafana.tf | 0 .../monitoring/grafana_chart_values.yaml | 0 .../platform/modules}/monitoring/idrac.tf | 2 +- .../monitoring/k8s-monitoring-values.yaml | 0 .../platform/modules}/monitoring/loki.tf | 0 .../platform/modules}/monitoring/loki.yaml | 0 .../platform/modules}/monitoring/main.tf | 2 +- .../modules}/monitoring/prometheus.tf | 0 .../monitoring/prometheus_chart_values.tpl | 0 .../prometheus_snmp_chart_values.yaml | 80939 ++++++++++++++++ .../modules}/monitoring/pve_exporter.tf | 0 .../monitoring/server-power-cycle/main.py | 0 .../monitoring/server-power-cycle/main.sh | 0 .../modules}/monitoring/snmp_exporter.tf | 2 +- .../modules}/monitoring/ups_snmp_values.yaml | 0 .../platform/modules}/nvidia/Dockerfile | 0 .../platform/modules}/nvidia/main.tf | 4 +- .../platform/modules}/nvidia/values.yaml | 0 .../platform/modules}/rbac/apiserver-oidc.tf | 0 .../platform/modules}/rbac/audit-policy.tf | 0 .../platform/modules}/rbac/main.tf | 0 .../platform/modules}/redis/main.tf | 4 +- .../modules}/reverse_proxy/factory/main.tf | 0 .../platform/modules}/reverse_proxy/main.tf | 2 +- .../platform/modules}/technitium/main.tf | 6 +- .../platform/modules}/traefik/main.tf | 4 +- .../platform/modules}/traefik/middleware.tf | 0 .../platform/modules}/uptime-kuma/main.tf | 4 +- .../platform/modules}/vaultwarden/main.tf | 4 +- .../modules}/wireguard/extra/clients.conf | 0 .../modules}/wireguard/extra/last_ip.txt | 0 stacks/platform/modules/wireguard/main.tf | 227 + .../platform/modules}/xray/main.tf | 2 +- .../modules}/xray/xray_config.json.tpl | 0 stacks/platform/providers.tf | 15 + stacks/plotting-book/.terraform.lock.hcl | 40 + stacks/plotting-book/backend.tf | 6 + stacks/plotting-book/main.tf | 2 +- .../plotting-book/module}/main.tf | 4 +- stacks/plotting-book/providers.tf | 15 + stacks/plotting-book/secrets | 1 + stacks/privatebin/.terraform.lock.hcl | 40 + stacks/privatebin/backend.tf | 6 + stacks/privatebin/main.tf | 2 +- .../privatebin/module}/main.tf | 4 +- stacks/privatebin/providers.tf | 15 + stacks/privatebin/secrets | 1 + .../real-estate-crawler/.terraform.lock.hcl | 40 + stacks/real-estate-crawler/backend.tf | 6 + stacks/real-estate-crawler/main.tf | 2 +- .../real-estate-crawler/module}/main.tf | 6 +- stacks/real-estate-crawler/providers.tf | 15 + stacks/real-estate-crawler/secrets | 1 + stacks/reloader/.terraform.lock.hcl | 40 + stacks/reloader/backend.tf | 6 + stacks/reloader/main.tf | 2 +- .../reloader/module}/main.tf | 0 stacks/reloader/providers.tf | 15 + stacks/reloader/secrets | 1 + stacks/resume/.terraform.lock.hcl | 40 + stacks/resume/backend.tf | 6 + stacks/resume/main.tf | 2 +- .../resume => stacks/resume/module}/main.tf | 4 +- stacks/resume/providers.tf | 15 + stacks/resume/secrets | 1 + stacks/rybbit/.terraform.lock.hcl | 59 + stacks/rybbit/backend.tf | 6 + stacks/rybbit/main.tf | 2 +- .../rybbit => stacks/rybbit/module}/main.tf | 6 +- stacks/rybbit/providers.tf | 15 + stacks/rybbit/secrets | 1 + stacks/send/.terraform.lock.hcl | 40 + stacks/send/backend.tf | 6 + stacks/send/main.tf | 2 +- .../send => stacks/send/module}/main.tf | 4 +- stacks/send/providers.tf | 15 + stacks/send/secrets | 1 + stacks/servarr/.terraform.lock.hcl | 59 + stacks/servarr/backend.tf | 6 + stacks/servarr/main.tf | 2 +- .../servarr/module}/aiostreams/main.tf | 2 +- .../servarr/module}/flaresolverr/main.tf | 2 +- .../servarr/module}/lidarr/main.tf | 4 +- .../servarr/module}/listenarr/main.tf | 2 +- .../servarr => stacks/servarr/module}/main.tf | 2 +- .../servarr/module}/prowlarr/main.tf | 2 +- .../servarr/module}/qbittorrent/main.tf | 2 +- .../servarr/module}/readarr/main.tf | 4 +- .../servarr/module}/soulseek/main.tf | 2 +- stacks/servarr/providers.tf | 15 + stacks/servarr/secrets | 1 + stacks/shadowsocks/.terraform.lock.hcl | 40 + stacks/shadowsocks/backend.tf | 6 + stacks/shadowsocks/main.tf | 2 +- .../shadowsocks/module}/main.tf | 0 .../module}/shadowsocks_chart_values.tpl | 0 stacks/shadowsocks/providers.tf | 15 + stacks/shadowsocks/secrets | 1 + stacks/speedtest/.terraform.lock.hcl | 59 + stacks/speedtest/backend.tf | 6 + stacks/speedtest/main.tf | 2 +- .../speedtest/module}/main.tf | 4 +- stacks/speedtest/providers.tf | 15 + stacks/speedtest/secrets | 1 + stacks/stirling-pdf/.terraform.lock.hcl | 40 + stacks/stirling-pdf/backend.tf | 6 + stacks/stirling-pdf/main.tf | 2 +- .../stirling-pdf/module}/main.tf | 4 +- stacks/stirling-pdf/providers.tf | 15 + stacks/stirling-pdf/secrets | 1 + stacks/tandoor/.terraform.lock.hcl | 59 + stacks/tandoor/backend.tf | 6 + stacks/tandoor/main.tf | 2 +- .../tandoor => stacks/tandoor/module}/main.tf | 4 +- stacks/tandoor/providers.tf | 15 + stacks/tandoor/secrets | 1 + stacks/tor-proxy/.terraform.lock.hcl | 40 + stacks/tor-proxy/backend.tf | 6 + stacks/tor-proxy/main.tf | 2 +- .../tor-proxy/module}/main.tf | 2 +- stacks/tor-proxy/providers.tf | 15 + stacks/tor-proxy/secrets | 1 + stacks/travel_blog/.terraform.lock.hcl | 40 + stacks/travel_blog/backend.tf | 6 + stacks/travel_blog/main.tf | 2 +- .../travel_blog/module}/main.tf | 6 +- stacks/travel_blog/providers.tf | 15 + stacks/travel_blog/secrets | 1 + stacks/tuya-bridge/.terraform.lock.hcl | 40 + stacks/tuya-bridge/backend.tf | 6 + stacks/tuya-bridge/main.tf | 2 +- .../tuya-bridge/module}/main.tf | 4 +- stacks/tuya-bridge/providers.tf | 15 + stacks/tuya-bridge/secrets | 1 + stacks/url/.terraform.lock.hcl | 40 + stacks/url/backend.tf | 6 + stacks/url/main.tf | 2 +- stacks/url/module/main.tf | 294 + .../url/module}/versions.tf | 0 stacks/url/providers.tf | 15 + stacks/url/secrets | 1 + stacks/wealthfolio/.terraform.lock.hcl | 59 + stacks/wealthfolio/backend.tf | 6 + stacks/wealthfolio/main.tf | 2 +- .../wealthfolio/module}/main.tf | 4 +- stacks/wealthfolio/providers.tf | 15 + stacks/wealthfolio/secrets | 1 + stacks/webhook_handler/.terraform.lock.hcl | 40 + stacks/webhook_handler/backend.tf | 6 + stacks/webhook_handler/main.tf | 2 +- .../webhook_handler/module}/main.tf | 4 +- stacks/webhook_handler/providers.tf | 15 + stacks/webhook_handler/secrets | 1 + stacks/whisper/.terraform.lock.hcl | 40 + stacks/whisper/backend.tf | 6 + stacks/whisper/main.tf | 2 +- .../whisper => stacks/whisper/module}/main.tf | 2 +- stacks/whisper/providers.tf | 15 + stacks/whisper/secrets | 1 + stacks/ytdlp/.terraform.lock.hcl | 40 + stacks/ytdlp/backend.tf | 6 + stacks/ytdlp/main.tf | 2 +- .../ytdlp/module}/main.tf | 6 +- .../ytdlp/module}/yt-highlights/Dockerfile | 0 .../module}/yt-highlights/app/__init__.py | 0 .../ytdlp/module}/yt-highlights/app/main.py | 0 .../yt-highlights/app/static/index.html | 0 .../module}/yt-highlights/requirements.txt | 0 stacks/ytdlp/providers.tf | 15 + stacks/ytdlp/secrets | 1 + 613 files changed, 94827 insertions(+), 339 deletions(-) create mode 100644 stacks/actualbudget/.terraform.lock.hcl create mode 100644 stacks/actualbudget/backend.tf rename {modules/kubernetes/actualbudget => stacks/actualbudget/module}/factory/main.tf (98%) rename {modules/kubernetes/actualbudget => stacks/actualbudget/module}/main.tf (96%) create mode 100644 stacks/actualbudget/providers.tf create mode 120000 stacks/actualbudget/secrets create mode 100644 stacks/affine/.terraform.lock.hcl create mode 100644 stacks/affine/backend.tf rename {modules/kubernetes/affine => stacks/affine/module}/main.tf (97%) create mode 100644 stacks/affine/providers.tf create mode 120000 stacks/affine/secrets create mode 100644 stacks/audiobookshelf/.terraform.lock.hcl create mode 100644 stacks/audiobookshelf/backend.tf rename {modules/kubernetes/audiobookshelf => stacks/audiobookshelf/module}/main.tf (95%) create mode 100644 stacks/audiobookshelf/providers.tf create mode 120000 stacks/audiobookshelf/secrets create mode 100644 stacks/blog/.terraform.lock.hcl create mode 100644 stacks/blog/backend.tf rename {modules/kubernetes/blog => stacks/blog/module}/main.tf (91%) create mode 100644 stacks/blog/providers.tf create mode 120000 stacks/blog/secrets create mode 100644 stacks/calibre/.terraform.lock.hcl create mode 100644 stacks/calibre/backend.tf rename {modules/kubernetes/calibre => stacks/calibre/module}/main.tf (97%) create mode 100644 stacks/calibre/providers.tf create mode 120000 stacks/calibre/secrets create mode 100644 stacks/changedetection/.terraform.lock.hcl create mode 100644 stacks/changedetection/backend.tf rename {modules/kubernetes/changedetection => stacks/changedetection/module}/main.tf (95%) create mode 100644 stacks/changedetection/providers.tf create mode 120000 stacks/changedetection/secrets create mode 100644 stacks/city-guesser/.terraform.lock.hcl create mode 100644 stacks/city-guesser/backend.tf create mode 100644 stacks/city-guesser/module/main.tf create mode 100644 stacks/city-guesser/providers.tf create mode 120000 stacks/city-guesser/secrets create mode 100644 stacks/coturn/.terraform.lock.hcl create mode 100644 stacks/coturn/backend.tf rename {modules/kubernetes/coturn => stacks/coturn/module}/main.tf (98%) create mode 100644 stacks/coturn/providers.tf create mode 120000 stacks/coturn/secrets create mode 100644 stacks/cyberchef/.terraform.lock.hcl create mode 100644 stacks/cyberchef/backend.tf rename {modules/kubernetes/cyberchef => stacks/cyberchef/module}/main.tf (92%) create mode 100644 stacks/cyberchef/providers.tf create mode 120000 stacks/cyberchef/secrets create mode 100644 stacks/dashy/.terraform.lock.hcl create mode 100644 stacks/dashy/backend.tf rename {modules/kubernetes/dashy => stacks/dashy/module}/conf.yml (100%) rename {modules/kubernetes/dashy => stacks/dashy/module}/main.tf (94%) create mode 100644 stacks/dashy/providers.tf create mode 120000 stacks/dashy/secrets create mode 100644 stacks/dawarich/.terraform.lock.hcl create mode 100644 stacks/dawarich/backend.tf rename {modules/kubernetes/dawarich => stacks/dawarich/module}/main.tf (98%) create mode 100644 stacks/dawarich/providers.tf create mode 120000 stacks/dawarich/secrets create mode 100644 stacks/descheduler/.terraform.lock.hcl create mode 100644 stacks/descheduler/backend.tf rename {modules/kubernetes/descheduler => stacks/descheduler/module}/main.tf (100%) rename {modules/kubernetes/descheduler => stacks/descheduler/module}/values.yaml (100%) create mode 100644 stacks/descheduler/providers.tf create mode 120000 stacks/descheduler/secrets create mode 100644 stacks/diun/.terraform.lock.hcl create mode 100644 stacks/diun/backend.tf rename {modules/kubernetes/diun => stacks/diun/module}/main.tf (98%) create mode 100644 stacks/diun/providers.tf create mode 120000 stacks/diun/secrets create mode 100644 stacks/drone/.terraform.lock.hcl create mode 100644 stacks/drone/backend.tf rename {modules/kubernetes/drone => stacks/drone/module}/main.tf (97%) create mode 100644 stacks/drone/providers.tf create mode 120000 stacks/drone/secrets create mode 100644 stacks/ebook2audiobook/.terraform.lock.hcl create mode 100644 stacks/ebook2audiobook/backend.tf rename {modules/kubernetes/ebook2audiobook => stacks/ebook2audiobook/module}/audiblez-web (100%) rename {modules/kubernetes/ebook2audiobook => stacks/ebook2audiobook/module}/main.tf (97%) create mode 100644 stacks/ebook2audiobook/providers.tf create mode 120000 stacks/ebook2audiobook/secrets create mode 100644 stacks/echo/.terraform.lock.hcl create mode 100644 stacks/echo/backend.tf rename {modules/kubernetes/echo => stacks/echo/module}/main.tf (91%) create mode 100644 stacks/echo/providers.tf create mode 120000 stacks/echo/secrets create mode 100644 stacks/excalidraw/.terraform.lock.hcl create mode 100644 stacks/excalidraw/backend.tf rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/main.tf (94%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/.gitignore (100%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/Dockerfile (100%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/README.md (100%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/go.mod (100%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/main.go (100%) rename {modules/kubernetes/excalidraw => stacks/excalidraw/module}/project/static/editor.html (100%) create mode 100644 stacks/excalidraw/providers.tf create mode 120000 stacks/excalidraw/secrets create mode 100644 stacks/f1-stream/.terraform.lock.hcl create mode 100644 stacks/f1-stream/backend.tf create mode 100644 stacks/f1-stream/module/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/.dockerignore (100%) create mode 100644 stacks/f1-stream/module/files/.planning/PROJECT.md create mode 100644 stacks/f1-stream/module/files/.planning/REQUIREMENTS.md create mode 100644 stacks/f1-stream/module/files/.planning/ROADMAP.md create mode 100644 stacks/f1-stream/module/files/.planning/STATE.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/ARCHITECTURE.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/CONCERNS.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/CONVENTIONS.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/INTEGRATIONS.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/STACK.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/STRUCTURE.md create mode 100644 stacks/f1-stream/module/files/.planning/codebase/TESTING.md create mode 100644 stacks/f1-stream/module/files/.planning/config.json create mode 100644 stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-RESEARCH.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-VERIFICATION.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-01-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-01-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-02-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-02-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-RESEARCH.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/02-health-check-infrastructure/02-VERIFICATION.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/03-auto-publish-pipeline/03-01-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/03-auto-publish-pipeline/03-01-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/03-auto-publish-pipeline/03-VERIFICATION.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-01-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-01-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-02-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-02-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-VERIFICATION.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-01-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-01-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-02-PLAN.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-02-SUMMARY.md create mode 100644 stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-VERIFICATION.md rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/Dockerfile (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/go.mod (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/go.sum (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/index.html (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/auth/auth.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/auth/context.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/extractor/browser.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/extractor/capture.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/extractor/session.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/extractor/webrtc.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/healthcheck/healthcheck.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/hlsproxy/hlsproxy.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/models/models.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/playerconfig/playerconfig.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/proxy/proxy.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/scraper/reddit.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/scraper/scraper.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/scraper/validate.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/scraper/validate_test.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/server/middleware.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/server/server.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/health.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/scraped.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/sessions.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/store.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/streams.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/internal/store/users.go (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/main.go (100%) create mode 100644 stacks/f1-stream/module/files/node_modules/.package-lock.json create mode 100644 stacks/f1-stream/module/files/package-lock.json create mode 100644 stacks/f1-stream/module/files/package.json rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/redeploy.sh (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/css/custom.css (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/css/pico.min.css (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/index.html (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/js/app.js (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/js/auth.js (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/js/player.js (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/js/streams.js (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/files/static/js/utils.js (100%) rename {modules/kubernetes/f1-stream => stacks/f1-stream/module}/main.tf (95%) create mode 100644 stacks/f1-stream/providers.tf create mode 120000 stacks/f1-stream/secrets create mode 100644 stacks/forgejo/.terraform.lock.hcl create mode 100644 stacks/forgejo/backend.tf rename {modules/kubernetes/forgejo => stacks/forgejo/module}/main.tf (93%) create mode 100644 stacks/forgejo/providers.tf create mode 120000 stacks/forgejo/secrets create mode 100644 stacks/freedify/.terraform.lock.hcl create mode 100644 stacks/freedify/backend.tf rename {modules/kubernetes/freedify => stacks/freedify/module}/factory/main.tf (97%) rename {modules/kubernetes/freedify => stacks/freedify/module}/main.tf (96%) create mode 100644 stacks/freedify/providers.tf create mode 120000 stacks/freedify/secrets create mode 100644 stacks/freshrss/.terraform.lock.hcl create mode 100644 stacks/freshrss/backend.tf rename {modules/kubernetes/freshrss => stacks/freshrss/module}/main.tf (94%) create mode 100644 stacks/freshrss/providers.tf create mode 120000 stacks/freshrss/secrets create mode 100644 stacks/frigate/.terraform.lock.hcl create mode 100644 stacks/frigate/backend.tf rename {modules/kubernetes/frigate => stacks/frigate/module}/main.tf (95%) create mode 100644 stacks/frigate/providers.tf create mode 120000 stacks/frigate/secrets create mode 100644 stacks/grampsweb/.terraform.lock.hcl create mode 100644 stacks/grampsweb/backend.tf rename {modules/kubernetes/grampsweb => stacks/grampsweb/module}/main.tf (97%) create mode 100644 stacks/grampsweb/providers.tf create mode 120000 stacks/grampsweb/secrets create mode 100644 stacks/hackmd/.terraform.lock.hcl create mode 100644 stacks/hackmd/backend.tf rename {modules/kubernetes/hackmd => stacks/hackmd/module}/main.tf (96%) create mode 100644 stacks/hackmd/providers.tf create mode 120000 stacks/hackmd/secrets create mode 100644 stacks/health/.terraform.lock.hcl create mode 100644 stacks/health/backend.tf rename {modules/kubernetes/health => stacks/health/module}/main.tf (95%) create mode 100644 stacks/health/providers.tf create mode 120000 stacks/health/secrets create mode 100644 stacks/homepage/.terraform.lock.hcl create mode 100644 stacks/homepage/backend.tf rename {modules/kubernetes/homepage => stacks/homepage/module}/main.tf (91%) rename {modules/kubernetes/homepage => stacks/homepage/module}/values.yaml (100%) create mode 100644 stacks/homepage/providers.tf create mode 120000 stacks/homepage/secrets create mode 100644 stacks/immich/.terraform.lock.hcl create mode 100644 stacks/immich/backend.tf rename {modules/kubernetes/immich => stacks/immich/module}/chart_values.tpl (100%) rename {modules/kubernetes/immich => stacks/immich/module}/frame.tf (97%) rename {modules/kubernetes/immich => stacks/immich/module}/main.tf (98%) create mode 100644 stacks/immich/providers.tf create mode 120000 stacks/immich/secrets create mode 100644 stacks/infra/backend.tf create mode 100644 stacks/infra/providers.tf create mode 100644 stacks/isponsorblocktv/.terraform.lock.hcl create mode 100644 stacks/isponsorblocktv/backend.tf rename {modules/kubernetes/isponsorblocktv => stacks/isponsorblocktv/module}/main.tf (100%) create mode 100644 stacks/isponsorblocktv/providers.tf create mode 120000 stacks/isponsorblocktv/secrets create mode 100644 stacks/jsoncrack/.terraform.lock.hcl create mode 100644 stacks/jsoncrack/backend.tf rename {modules/kubernetes/jsoncrack => stacks/jsoncrack/module}/main.tf (92%) create mode 100644 stacks/jsoncrack/providers.tf create mode 120000 stacks/jsoncrack/secrets create mode 100644 stacks/k8s-dashboard/.terraform.lock.hcl create mode 100644 stacks/k8s-dashboard/backend.tf rename {modules/kubernetes/k8s-dashboard => stacks/k8s-dashboard/module}/main.tf (98%) create mode 100644 stacks/k8s-dashboard/providers.tf create mode 120000 stacks/k8s-dashboard/secrets create mode 100644 stacks/kms/.terraform.lock.hcl create mode 100644 stacks/kms/backend.tf rename {modules/kubernetes/kms => stacks/kms/module}/main.tf (96%) rename {modules/kubernetes/kms => stacks/kms/module}/variables.tf (100%) create mode 100644 stacks/kms/providers.tf create mode 120000 stacks/kms/secrets create mode 100644 stacks/linkwarden/.terraform.lock.hcl create mode 100644 stacks/linkwarden/backend.tf rename {modules/kubernetes/linkwarden => stacks/linkwarden/module}/main.tf (95%) create mode 100644 stacks/linkwarden/providers.tf create mode 120000 stacks/linkwarden/secrets create mode 100644 stacks/matrix/.terraform.lock.hcl create mode 100644 stacks/matrix/backend.tf rename {modules/kubernetes/matrix => stacks/matrix/module}/main.tf (93%) create mode 100644 stacks/matrix/providers.tf create mode 120000 stacks/matrix/secrets create mode 100644 stacks/meshcentral/.terraform.lock.hcl create mode 100644 stacks/meshcentral/backend.tf rename {modules/kubernetes/meshcentral => stacks/meshcentral/module}/main.tf (96%) create mode 100644 stacks/meshcentral/providers.tf create mode 120000 stacks/meshcentral/secrets create mode 100644 stacks/n8n/.terraform.lock.hcl create mode 100644 stacks/n8n/backend.tf rename {modules/kubernetes/n8n => stacks/n8n/module}/main.tf (95%) create mode 100644 stacks/n8n/providers.tf create mode 120000 stacks/n8n/secrets create mode 100644 stacks/navidrome/.terraform.lock.hcl create mode 100644 stacks/navidrome/backend.tf rename {modules/kubernetes/navidrome => stacks/navidrome/module}/main.tf (94%) create mode 100644 stacks/navidrome/providers.tf create mode 120000 stacks/navidrome/secrets create mode 100644 stacks/netbox/.terraform.lock.hcl create mode 100644 stacks/netbox/backend.tf rename {modules/kubernetes/netbox => stacks/netbox/module}/main.tf (96%) create mode 100644 stacks/netbox/providers.tf create mode 120000 stacks/netbox/secrets create mode 100644 stacks/networking-toolbox/.terraform.lock.hcl create mode 100644 stacks/networking-toolbox/backend.tf rename {modules/kubernetes/networking-toolbox => stacks/networking-toolbox/module}/main.tf (92%) create mode 100644 stacks/networking-toolbox/providers.tf create mode 120000 stacks/networking-toolbox/secrets create mode 100644 stacks/nextcloud/.terraform.lock.hcl create mode 100644 stacks/nextcloud/backend.tf rename {modules/kubernetes/nextcloud => stacks/nextcloud/module}/chart_values.yaml (100%) rename {modules/kubernetes/nextcloud => stacks/nextcloud/module}/main.tf (97%) create mode 100644 stacks/nextcloud/providers.tf create mode 120000 stacks/nextcloud/secrets create mode 100644 stacks/ntfy/.terraform.lock.hcl create mode 100644 stacks/ntfy/backend.tf rename {modules/kubernetes/ntfy => stacks/ntfy/module}/main.tf (95%) create mode 100644 stacks/ntfy/providers.tf create mode 120000 stacks/ntfy/secrets create mode 100644 stacks/ollama/.terraform.lock.hcl create mode 100644 stacks/ollama/backend.tf rename {modules/kubernetes/ollama => stacks/ollama/module}/main.tf (96%) rename {modules/kubernetes/ollama => stacks/ollama/module}/values.yaml (100%) create mode 100644 stacks/ollama/providers.tf create mode 120000 stacks/ollama/secrets create mode 100644 stacks/onlyoffice/.terraform.lock.hcl create mode 100644 stacks/onlyoffice/backend.tf rename {modules/kubernetes/onlyoffice => stacks/onlyoffice/module}/main.tf (95%) create mode 100644 stacks/onlyoffice/providers.tf create mode 120000 stacks/onlyoffice/secrets create mode 100644 stacks/openclaw/.terraform.lock.hcl create mode 100644 stacks/openclaw/backend.tf rename {modules/kubernetes/openclaw => stacks/openclaw/module}/main.tf (99%) create mode 100644 stacks/openclaw/providers.tf create mode 120000 stacks/openclaw/secrets create mode 100644 stacks/osm_routing/.terraform.lock.hcl create mode 100644 stacks/osm_routing/backend.tf rename {modules/kubernetes/osm-routing => stacks/osm_routing/module}/main.tf (100%) create mode 100644 stacks/osm_routing/providers.tf create mode 120000 stacks/osm_routing/secrets create mode 100644 stacks/owntracks/.terraform.lock.hcl create mode 100644 stacks/owntracks/backend.tf rename {modules/kubernetes/owntracks => stacks/owntracks/module}/main.tf (96%) create mode 100644 stacks/owntracks/providers.tf create mode 120000 stacks/owntracks/secrets create mode 100644 stacks/paperless-ngx/.terraform.lock.hcl create mode 100644 stacks/paperless-ngx/backend.tf rename {modules/kubernetes/paperless-ngx => stacks/paperless-ngx/module}/main.tf (97%) create mode 100644 stacks/paperless-ngx/providers.tf create mode 120000 stacks/paperless-ngx/secrets create mode 100644 stacks/platform/.terraform.lock.hcl create mode 100644 stacks/platform/backend.tf rename {modules/kubernetes => stacks/platform/modules}/authentik/main.tf (89%) rename {modules/kubernetes => stacks/platform/modules}/authentik/pgbouncer.ini (100%) rename {modules/kubernetes => stacks/platform/modules}/authentik/pgbouncer.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/authentik/userlist.txt (100%) rename {modules/kubernetes => stacks/platform/modules}/authentik/values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/cloudflared/cloudflare.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/cloudflared/main.tf (96%) rename {modules/kubernetes => stacks/platform/modules}/crowdsec/crowdsec-ingress-bouncer.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/crowdsec/main.tf (98%) rename {modules/kubernetes => stacks/platform/modules}/crowdsec/values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/dbaas/chart_values.tpl (100%) rename {modules/kubernetes => stacks/platform/modules}/dbaas/cluster.yaml (100%) create mode 100644 stacks/platform/modules/dbaas/main.tf rename {modules/kubernetes => stacks/platform/modules}/dbaas/mysql_chart_values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/dbaas/postgres/postgres_Dockerfile (100%) rename {modules/kubernetes => stacks/platform/modules}/dbaas/versions.tf (100%) create mode 100644 stacks/platform/modules/headscale/main.tf rename {modules/kubernetes => stacks/platform/modules}/infra-maintenance/main.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/.gitignore (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/.npmrc (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/Dockerfile (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/README.md (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/package-lock.json (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/package.json (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/app.d.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/app.html (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/lib/assets/favicon.svg (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/lib/index.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/+layout.svelte (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/+page.server.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/+page.svelte (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/download/+server.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/setup/+page.svelte (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/src/routes/setup/script/+server.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/static/robots.txt (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/svelte.config.js (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/tsconfig.json (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/files/vite.config.ts (100%) rename {modules/kubernetes => stacks/platform/modules}/k8s-portal/main.tf (92%) rename {modules/kubernetes => stacks/platform/modules}/kyverno/main.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/kyverno/resource-governance.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/mailserver/extra/aliases.txt (100%) rename {modules/kubernetes => stacks/platform/modules}/mailserver/main.tf (99%) rename {modules/kubernetes => stacks/platform/modules}/mailserver/roundcubemail.tf (98%) rename {modules/kubernetes => stacks/platform/modules}/mailserver/variables.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/metallb/main.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/metrics-server/main.tf (91%) rename {modules/kubernetes => stacks/platform/modules}/metrics-server/values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/Dockerfile (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/alloy.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/api_server.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/cluster_health.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/core_dns.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/idrac.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/k8s-audit.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/kube-state-metrics.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/loki.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/nginx_ingress.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/node_exporter_full.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/nodes.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/nvidia.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/pods.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/proxmox_node_exporter.json (100%) create mode 100644 stacks/platform/modules/monitoring/dashboards/realestate-crawler.json rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/registry.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/technitium-dns.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/ups-prometheus-metrics.yml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/dashboards/ups.json (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/grafana.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/grafana_chart_values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/idrac.tf (97%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/k8s-monitoring-values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/loki.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/loki.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/main.tf (98%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/prometheus.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/prometheus_chart_values.tpl (100%) create mode 100644 stacks/platform/modules/monitoring/prometheus_snmp_chart_values.yaml rename {modules/kubernetes => stacks/platform/modules}/monitoring/pve_exporter.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/server-power-cycle/main.py (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/server-power-cycle/main.sh (100%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/snmp_exporter.tf (97%) rename {modules/kubernetes => stacks/platform/modules}/monitoring/ups_snmp_values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/nvidia/Dockerfile (100%) rename {modules/kubernetes => stacks/platform/modules}/nvidia/main.tf (99%) rename {modules/kubernetes => stacks/platform/modules}/nvidia/values.yaml (100%) rename {modules/kubernetes => stacks/platform/modules}/rbac/apiserver-oidc.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/rbac/audit-policy.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/rbac/main.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/redis/main.tf (93%) rename {modules/kubernetes => stacks/platform/modules}/reverse_proxy/factory/main.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/reverse_proxy/main.tf (99%) rename {modules/kubernetes => stacks/platform/modules}/technitium/main.tf (97%) rename {modules/kubernetes => stacks/platform/modules}/traefik/main.tf (97%) rename {modules/kubernetes => stacks/platform/modules}/traefik/middleware.tf (100%) rename {modules/kubernetes => stacks/platform/modules}/uptime-kuma/main.tf (97%) rename {modules/kubernetes => stacks/platform/modules}/vaultwarden/main.tf (95%) rename {modules/kubernetes => stacks/platform/modules}/wireguard/extra/clients.conf (100%) rename {modules/kubernetes => stacks/platform/modules}/wireguard/extra/last_ip.txt (100%) create mode 100644 stacks/platform/modules/wireguard/main.tf rename {modules/kubernetes => stacks/platform/modules}/xray/main.tf (98%) rename {modules/kubernetes => stacks/platform/modules}/xray/xray_config.json.tpl (100%) create mode 100644 stacks/platform/providers.tf create mode 100644 stacks/plotting-book/.terraform.lock.hcl create mode 100644 stacks/plotting-book/backend.tf rename {modules/kubernetes/plotting-book => stacks/plotting-book/module}/main.tf (94%) create mode 100644 stacks/plotting-book/providers.tf create mode 120000 stacks/plotting-book/secrets create mode 100644 stacks/privatebin/.terraform.lock.hcl create mode 100644 stacks/privatebin/backend.tf rename {modules/kubernetes/privatebin => stacks/privatebin/module}/main.tf (93%) create mode 100644 stacks/privatebin/providers.tf create mode 120000 stacks/privatebin/secrets create mode 100644 stacks/real-estate-crawler/.terraform.lock.hcl create mode 100644 stacks/real-estate-crawler/backend.tf rename {modules/kubernetes/real-estate-crawler => stacks/real-estate-crawler/module}/main.tf (98%) create mode 100644 stacks/real-estate-crawler/providers.tf create mode 120000 stacks/real-estate-crawler/secrets create mode 100644 stacks/reloader/.terraform.lock.hcl create mode 100644 stacks/reloader/backend.tf rename {modules/kubernetes/reloader => stacks/reloader/module}/main.tf (100%) create mode 100644 stacks/reloader/providers.tf create mode 120000 stacks/reloader/secrets create mode 100644 stacks/resume/.terraform.lock.hcl create mode 100644 stacks/resume/backend.tf rename {modules/kubernetes/resume => stacks/resume/module}/main.tf (97%) create mode 100644 stacks/resume/providers.tf create mode 120000 stacks/resume/secrets create mode 100644 stacks/rybbit/.terraform.lock.hcl create mode 100644 stacks/rybbit/backend.tf rename {modules/kubernetes/rybbit => stacks/rybbit/module}/main.tf (96%) create mode 100644 stacks/rybbit/providers.tf create mode 120000 stacks/rybbit/secrets create mode 100644 stacks/send/.terraform.lock.hcl create mode 100644 stacks/send/backend.tf rename {modules/kubernetes/send => stacks/send/module}/main.tf (94%) create mode 100644 stacks/send/providers.tf create mode 120000 stacks/send/secrets create mode 100644 stacks/servarr/.terraform.lock.hcl create mode 100644 stacks/servarr/backend.tf rename {modules/kubernetes/servarr => stacks/servarr/module}/aiostreams/main.tf (96%) rename {modules/kubernetes/servarr => stacks/servarr/module}/flaresolverr/main.tf (95%) rename {modules/kubernetes/servarr => stacks/servarr/module}/lidarr/main.tf (95%) rename {modules/kubernetes/servarr => stacks/servarr/module}/listenarr/main.tf (95%) rename {modules/kubernetes/servarr => stacks/servarr/module}/main.tf (95%) rename {modules/kubernetes/servarr => stacks/servarr/module}/prowlarr/main.tf (96%) rename {modules/kubernetes/servarr => stacks/servarr/module}/qbittorrent/main.tf (97%) rename {modules/kubernetes/servarr => stacks/servarr/module}/readarr/main.tf (94%) rename {modules/kubernetes/servarr => stacks/servarr/module}/soulseek/main.tf (96%) create mode 100644 stacks/servarr/providers.tf create mode 120000 stacks/servarr/secrets create mode 100644 stacks/shadowsocks/.terraform.lock.hcl create mode 100644 stacks/shadowsocks/backend.tf rename {modules/kubernetes/shadowsocks => stacks/shadowsocks/module}/main.tf (100%) rename {modules/kubernetes/shadowsocks => stacks/shadowsocks/module}/shadowsocks_chart_values.tpl (100%) create mode 100644 stacks/shadowsocks/providers.tf create mode 120000 stacks/shadowsocks/secrets create mode 100644 stacks/speedtest/.terraform.lock.hcl create mode 100644 stacks/speedtest/backend.tf rename {modules/kubernetes/speedtest => stacks/speedtest/module}/main.tf (96%) create mode 100644 stacks/speedtest/providers.tf create mode 120000 stacks/speedtest/secrets create mode 100644 stacks/stirling-pdf/.terraform.lock.hcl create mode 100644 stacks/stirling-pdf/backend.tf rename {modules/kubernetes/stirling-pdf => stacks/stirling-pdf/module}/main.tf (93%) create mode 100644 stacks/stirling-pdf/providers.tf create mode 120000 stacks/stirling-pdf/secrets create mode 100644 stacks/tandoor/.terraform.lock.hcl create mode 100644 stacks/tandoor/backend.tf rename {modules/kubernetes/tandoor => stacks/tandoor/module}/main.tf (96%) create mode 100644 stacks/tandoor/providers.tf create mode 120000 stacks/tandoor/secrets create mode 100644 stacks/tor-proxy/.terraform.lock.hcl create mode 100644 stacks/tor-proxy/backend.tf rename {modules/kubernetes/tor-proxy => stacks/tor-proxy/module}/main.tf (97%) create mode 100644 stacks/tor-proxy/providers.tf create mode 120000 stacks/tor-proxy/secrets create mode 100644 stacks/travel_blog/.terraform.lock.hcl create mode 100644 stacks/travel_blog/backend.tf rename {modules/kubernetes/travel_blog => stacks/travel_blog/module}/main.tf (91%) create mode 100644 stacks/travel_blog/providers.tf create mode 120000 stacks/travel_blog/secrets create mode 100644 stacks/tuya-bridge/.terraform.lock.hcl create mode 100644 stacks/tuya-bridge/backend.tf rename {modules/kubernetes/tuya-bridge => stacks/tuya-bridge/module}/main.tf (94%) create mode 100644 stacks/tuya-bridge/providers.tf create mode 120000 stacks/tuya-bridge/secrets create mode 100644 stacks/url/.terraform.lock.hcl create mode 100644 stacks/url/backend.tf create mode 100644 stacks/url/module/main.tf rename {modules/kubernetes/url-shortener => stacks/url/module}/versions.tf (100%) create mode 100644 stacks/url/providers.tf create mode 120000 stacks/url/secrets create mode 100644 stacks/wealthfolio/.terraform.lock.hcl create mode 100644 stacks/wealthfolio/backend.tf rename {modules/kubernetes/wealthfolio => stacks/wealthfolio/module}/main.tf (95%) create mode 100644 stacks/wealthfolio/providers.tf create mode 120000 stacks/wealthfolio/secrets create mode 100644 stacks/webhook_handler/.terraform.lock.hcl create mode 100644 stacks/webhook_handler/backend.tf rename {modules/kubernetes/webhook_handler => stacks/webhook_handler/module}/main.tf (97%) create mode 100644 stacks/webhook_handler/providers.tf create mode 120000 stacks/webhook_handler/secrets create mode 100644 stacks/whisper/.terraform.lock.hcl create mode 100644 stacks/whisper/backend.tf rename {modules/kubernetes/whisper => stacks/whisper/module}/main.tf (98%) create mode 100644 stacks/whisper/providers.tf create mode 120000 stacks/whisper/secrets create mode 100644 stacks/ytdlp/.terraform.lock.hcl create mode 100644 stacks/ytdlp/backend.tf rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/main.tf (97%) rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/yt-highlights/Dockerfile (100%) rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/yt-highlights/app/__init__.py (100%) rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/yt-highlights/app/main.py (100%) rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/yt-highlights/app/static/index.html (100%) rename {modules/kubernetes/youtube_dl => stacks/ytdlp/module}/yt-highlights/requirements.txt (100%) create mode 100644 stacks/ytdlp/providers.tf create mode 120000 stacks/ytdlp/secrets diff --git a/stacks/actualbudget/.terraform.lock.hcl b/stacks/actualbudget/.terraform.lock.hcl new file mode 100644 index 00000000..afde2320 --- /dev/null +++ b/stacks/actualbudget/.terraform.lock.hcl @@ -0,0 +1,59 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} diff --git a/stacks/actualbudget/backend.tf b/stacks/actualbudget/backend.tf new file mode 100644 index 00000000..07725d7d --- /dev/null +++ b/stacks/actualbudget/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/actualbudget/terraform.tfstate" + } +} diff --git a/stacks/actualbudget/main.tf b/stacks/actualbudget/main.tf index 2bb2d58c..ddba7250 100644 --- a/stacks/actualbudget/main.tf +++ b/stacks/actualbudget/main.tf @@ -12,8 +12,8 @@ locals { } module "actualbudget" { - source = "../../modules/kubernetes/actualbudget" - tls_secret_name = var.tls_secret_name - tier = local.tiers.edge - credentials = var.actualbudget_credentials + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.edge + credentials = var.actualbudget_credentials } diff --git a/modules/kubernetes/actualbudget/factory/main.tf b/stacks/actualbudget/module/factory/main.tf similarity index 98% rename from modules/kubernetes/actualbudget/factory/main.tf rename to stacks/actualbudget/module/factory/main.tf index d76a7f94..e173d3fb 100644 --- a/modules/kubernetes/actualbudget/factory/main.tf +++ b/stacks/actualbudget/module/factory/main.tf @@ -89,7 +89,7 @@ resource "kubernetes_service" "actualbudget" { } module "ingress" { - source = "../../ingress_factory" + source = "../../../../modules/kubernetes/ingress_factory" namespace = "actualbudget" name = "budget-${var.name}" tls_secret_name = var.tls_secret_name diff --git a/modules/kubernetes/actualbudget/main.tf b/stacks/actualbudget/module/main.tf similarity index 96% rename from modules/kubernetes/actualbudget/main.tf rename to stacks/actualbudget/module/main.tf index e9f374c5..eaa4ebd4 100644 --- a/modules/kubernetes/actualbudget/main.tf +++ b/stacks/actualbudget/module/main.tf @@ -20,7 +20,7 @@ resource "kubernetes_namespace" "actualbudget" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.actualbudget.metadata[0].name tls_secret_name = var.tls_secret_name } diff --git a/stacks/actualbudget/providers.tf b/stacks/actualbudget/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/actualbudget/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/actualbudget/secrets b/stacks/actualbudget/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/actualbudget/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/affine/.terraform.lock.hcl b/stacks/affine/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/affine/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/affine/backend.tf b/stacks/affine/backend.tf new file mode 100644 index 00000000..973f9c04 --- /dev/null +++ b/stacks/affine/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/affine/terraform.tfstate" + } +} diff --git a/stacks/affine/main.tf b/stacks/affine/main.tf index 52137488..0043e2a5 100644 --- a/stacks/affine/main.tf +++ b/stacks/affine/main.tf @@ -13,9 +13,9 @@ locals { } module "affine" { - source = "../../modules/kubernetes/affine" - tls_secret_name = var.tls_secret_name - postgresql_password = var.affine_postgresql_password - smtp_password = var.mailserver_accounts["info@viktorbarzin.me"] - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + postgresql_password = var.affine_postgresql_password + smtp_password = var.mailserver_accounts["info@viktorbarzin.me"] + tier = local.tiers.aux } diff --git a/modules/kubernetes/affine/main.tf b/stacks/affine/module/main.tf similarity index 97% rename from modules/kubernetes/affine/main.tf rename to stacks/affine/module/main.tf index b1a8d4fc..8d73ec85 100644 --- a/modules/kubernetes/affine/main.tf +++ b/stacks/affine/module/main.tf @@ -13,7 +13,7 @@ resource "kubernetes_namespace" "affine" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.affine.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -209,7 +209,7 @@ resource "kubernetes_service" "affine" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.affine.metadata[0].name name = "affine" tls_secret_name = var.tls_secret_name diff --git a/stacks/affine/providers.tf b/stacks/affine/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/affine/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/affine/secrets b/stacks/affine/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/affine/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/audiobookshelf/.terraform.lock.hcl b/stacks/audiobookshelf/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/audiobookshelf/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/audiobookshelf/backend.tf b/stacks/audiobookshelf/backend.tf new file mode 100644 index 00000000..486ccbea --- /dev/null +++ b/stacks/audiobookshelf/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/audiobookshelf/terraform.tfstate" + } +} diff --git a/stacks/audiobookshelf/main.tf b/stacks/audiobookshelf/main.tf index a7930049..d09071b9 100644 --- a/stacks/audiobookshelf/main.tf +++ b/stacks/audiobookshelf/main.tf @@ -11,7 +11,7 @@ locals { } module "audiobookshelf" { - source = "../../modules/kubernetes/audiobookshelf" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/audiobookshelf/main.tf b/stacks/audiobookshelf/module/main.tf similarity index 95% rename from modules/kubernetes/audiobookshelf/main.tf rename to stacks/audiobookshelf/module/main.tf index a23fb489..db3280aa 100644 --- a/modules/kubernetes/audiobookshelf/main.tf +++ b/stacks/audiobookshelf/module/main.tf @@ -12,7 +12,7 @@ resource "kubernetes_namespace" "audiobookshelf" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.audiobookshelf.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -126,7 +126,7 @@ resource "kubernetes_service" "audiobookshelf" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.audiobookshelf.metadata[0].name name = "audiobookshelf" tls_secret_name = var.tls_secret_name diff --git a/stacks/audiobookshelf/providers.tf b/stacks/audiobookshelf/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/audiobookshelf/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/audiobookshelf/secrets b/stacks/audiobookshelf/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/audiobookshelf/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/blog/.terraform.lock.hcl b/stacks/blog/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/blog/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/blog/backend.tf b/stacks/blog/backend.tf new file mode 100644 index 00000000..e91c1b32 --- /dev/null +++ b/stacks/blog/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/blog/terraform.tfstate" + } +} diff --git a/stacks/blog/main.tf b/stacks/blog/main.tf index 15aefede..6a0ec217 100644 --- a/stacks/blog/main.tf +++ b/stacks/blog/main.tf @@ -11,7 +11,7 @@ locals { } module "blog" { - source = "../../modules/kubernetes/blog" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/blog/main.tf b/stacks/blog/module/main.tf similarity index 91% rename from modules/kubernetes/blog/main.tf rename to stacks/blog/module/main.tf index eb02b035..e01b2d10 100644 --- a/modules/kubernetes/blog/main.tf +++ b/stacks/blog/module/main.tf @@ -13,13 +13,13 @@ resource "kubernetes_namespace" "website" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.website.metadata[0].name tls_secret_name = var.tls_secret_name } # module "dockerhub_creds" { -# source = "../dockerhub_secret" +# source = "../../../modules/kubernetes/dockerhub_secret" # namespace = kubernetes_namespace.website.metadata[0].name # password = var.dockerhub_password # } @@ -110,7 +110,7 @@ resource "kubernetes_service" "blog" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.website.metadata[0].name name = "blog" service_name = "blog" @@ -120,7 +120,7 @@ module "ingress" { } module "ingress-www" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.website.metadata[0].name name = "blog-www" service_name = "blog" diff --git a/stacks/blog/providers.tf b/stacks/blog/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/blog/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/blog/secrets b/stacks/blog/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/blog/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/calibre/.terraform.lock.hcl b/stacks/calibre/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/calibre/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/calibre/backend.tf b/stacks/calibre/backend.tf new file mode 100644 index 00000000..93431ff5 --- /dev/null +++ b/stacks/calibre/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/calibre/terraform.tfstate" + } +} diff --git a/stacks/calibre/main.tf b/stacks/calibre/main.tf index 4170c94d..8e2434ec 100644 --- a/stacks/calibre/main.tf +++ b/stacks/calibre/main.tf @@ -12,9 +12,9 @@ locals { } module "calibre" { - source = "../../modules/kubernetes/calibre" - tls_secret_name = var.tls_secret_name - homepage_username = var.homepage_credentials["calibre-web"]["username"] - homepage_password = var.homepage_credentials["calibre-web"]["password"] - tier = local.tiers.edge + source = "./module" + tls_secret_name = var.tls_secret_name + homepage_username = var.homepage_credentials["calibre-web"]["username"] + homepage_password = var.homepage_credentials["calibre-web"]["password"] + tier = local.tiers.edge } diff --git a/modules/kubernetes/calibre/main.tf b/stacks/calibre/module/main.tf similarity index 97% rename from modules/kubernetes/calibre/main.tf rename to stacks/calibre/module/main.tf index a2ac3ff1..fe444ed0 100644 --- a/modules/kubernetes/calibre/main.tf +++ b/stacks/calibre/module/main.tf @@ -20,7 +20,7 @@ resource "kubernetes_namespace" "calibre" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.calibre.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -222,7 +222,7 @@ resource "kubernetes_service" "calibre" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.calibre.metadata[0].name name = "calibre" tls_secret_name = var.tls_secret_name @@ -325,7 +325,7 @@ resource "kubernetes_service" "annas-archive-stacks" { } module "stacks-ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.calibre.metadata[0].name name = "stacks" service_name = "annas-archive-stacks" diff --git a/stacks/calibre/providers.tf b/stacks/calibre/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/calibre/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/calibre/secrets b/stacks/calibre/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/calibre/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/changedetection/.terraform.lock.hcl b/stacks/changedetection/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/changedetection/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/changedetection/backend.tf b/stacks/changedetection/backend.tf new file mode 100644 index 00000000..a0c7d0a9 --- /dev/null +++ b/stacks/changedetection/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/changedetection/terraform.tfstate" + } +} diff --git a/stacks/changedetection/main.tf b/stacks/changedetection/main.tf index c6996e46..0be18513 100644 --- a/stacks/changedetection/main.tf +++ b/stacks/changedetection/main.tf @@ -11,7 +11,7 @@ locals { } module "changedetection" { - source = "../../modules/kubernetes/changedetection" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/changedetection/main.tf b/stacks/changedetection/module/main.tf similarity index 95% rename from modules/kubernetes/changedetection/main.tf rename to stacks/changedetection/module/main.tf index e7dca791..b5c99704 100644 --- a/modules/kubernetes/changedetection/main.tf +++ b/stacks/changedetection/module/main.tf @@ -12,7 +12,7 @@ resource "kubernetes_namespace" "changedetection" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.changedetection.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -124,7 +124,7 @@ resource "kubernetes_service" "changedetection" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.changedetection.metadata[0].name name = "changedetection" tls_secret_name = var.tls_secret_name diff --git a/stacks/changedetection/providers.tf b/stacks/changedetection/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/changedetection/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/changedetection/secrets b/stacks/changedetection/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/changedetection/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/city-guesser/.terraform.lock.hcl b/stacks/city-guesser/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/city-guesser/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/city-guesser/backend.tf b/stacks/city-guesser/backend.tf new file mode 100644 index 00000000..92290eb7 --- /dev/null +++ b/stacks/city-guesser/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/city-guesser/terraform.tfstate" + } +} diff --git a/stacks/city-guesser/main.tf b/stacks/city-guesser/main.tf index 70e5d965..422497b4 100644 --- a/stacks/city-guesser/main.tf +++ b/stacks/city-guesser/main.tf @@ -11,7 +11,7 @@ locals { } module "city-guesser" { - source = "../../modules/kubernetes/city-guesser" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/stacks/city-guesser/module/main.tf b/stacks/city-guesser/module/main.tf new file mode 100644 index 00000000..b1ece574 --- /dev/null +++ b/stacks/city-guesser/module/main.tf @@ -0,0 +1,154 @@ +variable "tls_secret_name" {} +variable "tier" { type = string } + +resource "kubernetes_namespace" "city-guesser" { + metadata { + name = "city-guesser" + labels = { + "istio-injection" : "disabled" + tier = var.tier + } + } +} + +module "tls_secret" { + source = "../../../modules/kubernetes/setup_tls_secret" + namespace = "city-guesser" + tls_secret_name = var.tls_secret_name +} + +resource "kubernetes_deployment" "city-guesser" { + metadata { + name = "city-guesser" + namespace = "city-guesser" + labels = { + run = "city-guesser" + tier = var.tier + } + } + spec { + replicas = 1 + selector { + match_labels = { + run = "city-guesser" + } + } + template { + metadata { + labels = { + run = "city-guesser" + } + } + spec { + container { + image = "viktorbarzin/city-guesser:latest" + name = "city-guesser" + resources { + limits = { + cpu = "0.5" + memory = "512Mi" + } + requests = { + cpu = "250m" + memory = "50Mi" + } + } + port { + container_port = 80 + } + } + } + } + } +} + +resource "kubernetes_service" "city-guesser" { + metadata { + name = "city-guesser" + namespace = "city-guesser" + labels = { + "run" = "city-guesser" + } + } + + spec { + selector = { + run = "city-guesser" + } + port { + name = "http" + port = "80" + target_port = "80" + } + } +} +# resource "kubernetes_service" "city-guesser-oauth" { +# metadata { +# name = "city-guesser-oauth" +# namespace = "city-guesser" +# labels = { +# "run" = "city-guesser-oauth" +# } +# } + +# spec { +# type = "ExternalName" +# external_name = "oauth-proxy.oauth.svc.cluster.local" + +# # port { +# # name = "tcp" +# # port = "80" +# # target_port = "80" +# # } +# } +# } + +module "ingress" { + source = "../../../modules/kubernetes/ingress_factory" + namespace = "city-guesser" + name = "city-guesser" + tls_secret_name = var.tls_secret_name + protected = true +} + +# resource "kubernetes_ingress_v1" "city-guesser-oauth" { +# metadata { +# name = "city-guesser-ingress-oauth" +# namespace = "city-guesser" +# annotations = { +# "kubernetes.io/ingress.class" = "nginx" +# } +# } + +# spec { +# tls { +# hosts = ["city-guesser.viktorbarzin.me"] +# secret_name = var.tls_secret_name +# } +# rule { +# host = "city-guesser.viktorbarzin.me" +# http { +# path { +# path = "/oauth2" +# backend { +# service_name = "city-guesser-oauth" +# service_port = "80" +# } +# } +# } +# } +# } +# } + + +# module "oauth" { +# source = "../../../modules/kubernetes/oauth-proxy" +# # oauth_client_id = "3d8ce4bf7b893899d967" +# # oauth_client_secret = "REDACTED_OAUTH_SECRET" +# client_id = "3d8ce4bf7b893899d967" +# client_secret = "REDACTED_OAUTH_SECRET" +# namespace = "city-guesser" +# host = "city-guesser.viktorbarzin.me" +# tls_secret_name = var.tls_secret_name +# svc_name = "city-guesser-oauth" +# } diff --git a/stacks/city-guesser/providers.tf b/stacks/city-guesser/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/city-guesser/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/city-guesser/secrets b/stacks/city-guesser/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/city-guesser/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/coturn/.terraform.lock.hcl b/stacks/coturn/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/coturn/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/coturn/backend.tf b/stacks/coturn/backend.tf new file mode 100644 index 00000000..a160e22c --- /dev/null +++ b/stacks/coturn/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/coturn/terraform.tfstate" + } +} diff --git a/stacks/coturn/main.tf b/stacks/coturn/main.tf index 38be5240..d1ceadff 100644 --- a/stacks/coturn/main.tf +++ b/stacks/coturn/main.tf @@ -13,9 +13,9 @@ locals { } module "coturn" { - source = "../../modules/kubernetes/coturn" - tls_secret_name = var.tls_secret_name - tier = local.tiers.edge - turn_secret = var.coturn_turn_secret - public_ip = var.public_ip + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.edge + turn_secret = var.coturn_turn_secret + public_ip = var.public_ip } diff --git a/modules/kubernetes/coturn/main.tf b/stacks/coturn/module/main.tf similarity index 98% rename from modules/kubernetes/coturn/main.tf rename to stacks/coturn/module/main.tf index c580a7a5..abbd5ecf 100644 --- a/modules/kubernetes/coturn/main.tf +++ b/stacks/coturn/module/main.tf @@ -21,7 +21,7 @@ resource "kubernetes_namespace" "coturn" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.coturn.metadata[0].name tls_secret_name = var.tls_secret_name } diff --git a/stacks/coturn/providers.tf b/stacks/coturn/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/coturn/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/coturn/secrets b/stacks/coturn/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/coturn/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/cyberchef/.terraform.lock.hcl b/stacks/cyberchef/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/cyberchef/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/cyberchef/backend.tf b/stacks/cyberchef/backend.tf new file mode 100644 index 00000000..87e2ee96 --- /dev/null +++ b/stacks/cyberchef/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/cyberchef/terraform.tfstate" + } +} diff --git a/stacks/cyberchef/main.tf b/stacks/cyberchef/main.tf index a331e2fa..ad338c05 100644 --- a/stacks/cyberchef/main.tf +++ b/stacks/cyberchef/main.tf @@ -11,7 +11,7 @@ locals { } module "cyberchef" { - source = "../../modules/kubernetes/cyberchef" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/cyberchef/main.tf b/stacks/cyberchef/module/main.tf similarity index 92% rename from modules/kubernetes/cyberchef/main.tf rename to stacks/cyberchef/module/main.tf index 5f707e29..e0c5970b 100644 --- a/modules/kubernetes/cyberchef/main.tf +++ b/stacks/cyberchef/module/main.tf @@ -10,7 +10,7 @@ resource "kubernetes_namespace" "cyberchef" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.cyberchef.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -80,7 +80,7 @@ resource "kubernetes_service" "cyberchef" { module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.cyberchef.metadata[0].name name = "cc" tls_secret_name = var.tls_secret_name diff --git a/stacks/cyberchef/providers.tf b/stacks/cyberchef/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/cyberchef/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/cyberchef/secrets b/stacks/cyberchef/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/cyberchef/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/dashy/.terraform.lock.hcl b/stacks/dashy/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/dashy/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/dashy/backend.tf b/stacks/dashy/backend.tf new file mode 100644 index 00000000..b1f5cb0b --- /dev/null +++ b/stacks/dashy/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/dashy/terraform.tfstate" + } +} diff --git a/stacks/dashy/main.tf b/stacks/dashy/main.tf index ea12817f..40b7eb84 100644 --- a/stacks/dashy/main.tf +++ b/stacks/dashy/main.tf @@ -11,7 +11,7 @@ locals { } module "dashy" { - source = "../../modules/kubernetes/dashy" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/dashy/conf.yml b/stacks/dashy/module/conf.yml similarity index 100% rename from modules/kubernetes/dashy/conf.yml rename to stacks/dashy/module/conf.yml diff --git a/modules/kubernetes/dashy/main.tf b/stacks/dashy/module/main.tf similarity index 94% rename from modules/kubernetes/dashy/main.tf rename to stacks/dashy/module/main.tf index 6f1b8e35..f4100cac 100644 --- a/modules/kubernetes/dashy/main.tf +++ b/stacks/dashy/module/main.tf @@ -3,7 +3,7 @@ variable "tls_secret_name" {} variable "tier" { type = string } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.dashy.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -117,7 +117,7 @@ resource "kubernetes_service" "dashy" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.dashy.metadata[0].name name = "dashy" tls_secret_name = var.tls_secret_name diff --git a/stacks/dashy/providers.tf b/stacks/dashy/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/dashy/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/dashy/secrets b/stacks/dashy/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/dashy/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/dawarich/.terraform.lock.hcl b/stacks/dawarich/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/dawarich/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/dawarich/backend.tf b/stacks/dawarich/backend.tf new file mode 100644 index 00000000..6e6a11de --- /dev/null +++ b/stacks/dawarich/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/dawarich/terraform.tfstate" + } +} diff --git a/stacks/dawarich/main.tf b/stacks/dawarich/main.tf index 7124910b..d4b9bc5e 100644 --- a/stacks/dawarich/main.tf +++ b/stacks/dawarich/main.tf @@ -13,9 +13,9 @@ locals { } module "dawarich" { - source = "../../modules/kubernetes/dawarich" - tls_secret_name = var.tls_secret_name - database_password = var.dawarich_database_password - geoapify_api_key = var.geoapify_api_key - tier = local.tiers.edge + source = "./module" + tls_secret_name = var.tls_secret_name + database_password = var.dawarich_database_password + geoapify_api_key = var.geoapify_api_key + tier = local.tiers.edge } diff --git a/modules/kubernetes/dawarich/main.tf b/stacks/dawarich/module/main.tf similarity index 98% rename from modules/kubernetes/dawarich/main.tf rename to stacks/dawarich/module/main.tf index 527bcd55..6b9ce1fd 100644 --- a/modules/kubernetes/dawarich/main.tf +++ b/stacks/dawarich/module/main.tf @@ -18,7 +18,7 @@ resource "kubernetes_namespace" "dawarich" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.dawarich.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -317,7 +317,7 @@ resource "kubernetes_service" "dawarich" { # } # } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.dawarich.metadata[0].name name = "dawarich" tls_secret_name = var.tls_secret_name diff --git a/stacks/dawarich/providers.tf b/stacks/dawarich/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/dawarich/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/dawarich/secrets b/stacks/dawarich/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/dawarich/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/descheduler/.terraform.lock.hcl b/stacks/descheduler/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/descheduler/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/descheduler/backend.tf b/stacks/descheduler/backend.tf new file mode 100644 index 00000000..ea7708a6 --- /dev/null +++ b/stacks/descheduler/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/descheduler/terraform.tfstate" + } +} diff --git a/stacks/descheduler/main.tf b/stacks/descheduler/main.tf index 2a761e02..a56cbbcf 100644 --- a/stacks/descheduler/main.tf +++ b/stacks/descheduler/main.tf @@ -1,3 +1,3 @@ module "descheduler" { - source = "../../modules/kubernetes/descheduler" + source = "./module" } diff --git a/modules/kubernetes/descheduler/main.tf b/stacks/descheduler/module/main.tf similarity index 100% rename from modules/kubernetes/descheduler/main.tf rename to stacks/descheduler/module/main.tf diff --git a/modules/kubernetes/descheduler/values.yaml b/stacks/descheduler/module/values.yaml similarity index 100% rename from modules/kubernetes/descheduler/values.yaml rename to stacks/descheduler/module/values.yaml diff --git a/stacks/descheduler/providers.tf b/stacks/descheduler/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/descheduler/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/descheduler/secrets b/stacks/descheduler/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/descheduler/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/diun/.terraform.lock.hcl b/stacks/diun/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/diun/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/diun/backend.tf b/stacks/diun/backend.tf new file mode 100644 index 00000000..57f03703 --- /dev/null +++ b/stacks/diun/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/diun/terraform.tfstate" + } +} diff --git a/stacks/diun/main.tf b/stacks/diun/main.tf index 941d3fd3..332bf1b4 100644 --- a/stacks/diun/main.tf +++ b/stacks/diun/main.tf @@ -13,9 +13,9 @@ locals { } module "diun" { - source = "../../modules/kubernetes/diun" - tls_secret_name = var.tls_secret_name - diun_nfty_token = var.diun_nfty_token - diun_slack_url = var.diun_slack_url - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + diun_nfty_token = var.diun_nfty_token + diun_slack_url = var.diun_slack_url + tier = local.tiers.aux } diff --git a/modules/kubernetes/diun/main.tf b/stacks/diun/module/main.tf similarity index 98% rename from modules/kubernetes/diun/main.tf rename to stacks/diun/module/main.tf index 5109495b..ed7809f0 100644 --- a/modules/kubernetes/diun/main.tf +++ b/stacks/diun/module/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "diun" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.diun.metadata[0].name tls_secret_name = var.tls_secret_name } diff --git a/stacks/diun/providers.tf b/stacks/diun/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/diun/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/diun/secrets b/stacks/diun/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/diun/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/drone/.terraform.lock.hcl b/stacks/drone/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/drone/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/drone/backend.tf b/stacks/drone/backend.tf new file mode 100644 index 00000000..49bde38f --- /dev/null +++ b/stacks/drone/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/drone/terraform.tfstate" + } +} diff --git a/stacks/drone/main.tf b/stacks/drone/main.tf index 84372a98..7a2cb194 100644 --- a/stacks/drone/main.tf +++ b/stacks/drone/main.tf @@ -15,14 +15,14 @@ locals { } module "drone" { - source = "../../modules/kubernetes/drone" - tls_secret_name = var.tls_secret_name - git_crypt_key_base64 = filebase64("${path.root}/../../.git/git-crypt/keys/default") - github_client_id = var.drone_github_client_id - github_client_secret = var.drone_github_client_secret - rpc_secret = var.drone_rpc_secret - webhook_secret = var.drone_webhook_secret - server_host = "drone.viktorbarzin.me" - server_proto = "https" - tier = local.tiers.edge + source = "./module" + tls_secret_name = var.tls_secret_name + git_crypt_key_base64 = filebase64("${path.root}/../../.git/git-crypt/keys/default") + github_client_id = var.drone_github_client_id + github_client_secret = var.drone_github_client_secret + rpc_secret = var.drone_rpc_secret + webhook_secret = var.drone_webhook_secret + server_host = "drone.viktorbarzin.me" + server_proto = "https" + tier = local.tiers.edge } diff --git a/modules/kubernetes/drone/main.tf b/stacks/drone/module/main.tf similarity index 97% rename from modules/kubernetes/drone/main.tf rename to stacks/drone/module/main.tf index 07e99d0e..9a84de24 100644 --- a/modules/kubernetes/drone/main.tf +++ b/stacks/drone/module/main.tf @@ -42,7 +42,7 @@ resource "kubernetes_resource_quota" "drone" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.drone.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -191,7 +191,7 @@ resource "kubernetes_service" "drone" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.drone.metadata[0].name name = "drone" tls_secret_name = var.tls_secret_name @@ -314,6 +314,10 @@ resource "kubernetes_deployment" "drone_runner" { name = "DRONE_DEBUG" value = "true" } + env { + name = "DRONE_IMAGES_CLONE" + value = "alpine/git:latest" + } } } } diff --git a/stacks/drone/providers.tf b/stacks/drone/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/drone/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/drone/secrets b/stacks/drone/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/drone/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/ebook2audiobook/.terraform.lock.hcl b/stacks/ebook2audiobook/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/ebook2audiobook/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/ebook2audiobook/backend.tf b/stacks/ebook2audiobook/backend.tf new file mode 100644 index 00000000..6457a661 --- /dev/null +++ b/stacks/ebook2audiobook/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/ebook2audiobook/terraform.tfstate" + } +} diff --git a/stacks/ebook2audiobook/main.tf b/stacks/ebook2audiobook/main.tf index 354a39a4..ee3c57db 100644 --- a/stacks/ebook2audiobook/main.tf +++ b/stacks/ebook2audiobook/main.tf @@ -11,7 +11,7 @@ locals { } module "ebook2audiobook" { - source = "../../modules/kubernetes/ebook2audiobook" - tls_secret_name = var.tls_secret_name - tier = local.tiers.gpu + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.gpu } diff --git a/modules/kubernetes/ebook2audiobook/audiblez-web b/stacks/ebook2audiobook/module/audiblez-web similarity index 100% rename from modules/kubernetes/ebook2audiobook/audiblez-web rename to stacks/ebook2audiobook/module/audiblez-web diff --git a/modules/kubernetes/ebook2audiobook/main.tf b/stacks/ebook2audiobook/module/main.tf similarity index 97% rename from modules/kubernetes/ebook2audiobook/main.tf rename to stacks/ebook2audiobook/module/main.tf index 991dd93b..8f0e609f 100644 --- a/modules/kubernetes/ebook2audiobook/main.tf +++ b/stacks/ebook2audiobook/module/main.tf @@ -3,7 +3,7 @@ variable "tls_secret_name" {} variable "tier" { type = string } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.ebook2audiobook.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -223,7 +223,7 @@ resource "kubernetes_service" "ebook2audiobook" { module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.ebook2audiobook.metadata[0].name name = "ebook2audiobook" tls_secret_name = var.tls_secret_name @@ -399,7 +399,7 @@ resource "kubernetes_service" "audiblez-web" { } module "audiblez-web-ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.ebook2audiobook.metadata[0].name name = "audiblez-web" host = "audiblez" diff --git a/stacks/ebook2audiobook/providers.tf b/stacks/ebook2audiobook/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/ebook2audiobook/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/ebook2audiobook/secrets b/stacks/ebook2audiobook/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/ebook2audiobook/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/echo/.terraform.lock.hcl b/stacks/echo/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/echo/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/echo/backend.tf b/stacks/echo/backend.tf new file mode 100644 index 00000000..b0d2db2b --- /dev/null +++ b/stacks/echo/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/echo/terraform.tfstate" + } +} diff --git a/stacks/echo/main.tf b/stacks/echo/main.tf index 9aab5baf..946c6230 100644 --- a/stacks/echo/main.tf +++ b/stacks/echo/main.tf @@ -11,7 +11,7 @@ locals { } module "echo" { - source = "../../modules/kubernetes/echo" - tls_secret_name = var.tls_secret_name - tier = local.tiers.edge + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.edge } diff --git a/modules/kubernetes/echo/main.tf b/stacks/echo/module/main.tf similarity index 91% rename from modules/kubernetes/echo/main.tf rename to stacks/echo/module/main.tf index 1eacfb41..e13c4e09 100644 --- a/modules/kubernetes/echo/main.tf +++ b/stacks/echo/module/main.tf @@ -12,7 +12,7 @@ resource "kubernetes_namespace" "echo" { } module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.echo.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -77,7 +77,7 @@ resource "kubernetes_service" "echo" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.echo.metadata[0].name name = "echo" tls_secret_name = var.tls_secret_name diff --git a/stacks/echo/providers.tf b/stacks/echo/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/echo/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/echo/secrets b/stacks/echo/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/echo/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/excalidraw/.terraform.lock.hcl b/stacks/excalidraw/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/excalidraw/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/excalidraw/backend.tf b/stacks/excalidraw/backend.tf new file mode 100644 index 00000000..b9a78a68 --- /dev/null +++ b/stacks/excalidraw/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/excalidraw/terraform.tfstate" + } +} diff --git a/stacks/excalidraw/main.tf b/stacks/excalidraw/main.tf index d2e4fcbc..cfbf6fb8 100644 --- a/stacks/excalidraw/main.tf +++ b/stacks/excalidraw/main.tf @@ -11,7 +11,7 @@ locals { } module "excalidraw" { - source = "../../modules/kubernetes/excalidraw" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux } diff --git a/modules/kubernetes/excalidraw/main.tf b/stacks/excalidraw/module/main.tf similarity index 94% rename from modules/kubernetes/excalidraw/main.tf rename to stacks/excalidraw/module/main.tf index c120e7fc..4c410692 100644 --- a/modules/kubernetes/excalidraw/main.tf +++ b/stacks/excalidraw/module/main.tf @@ -13,7 +13,7 @@ resource "kubernetes_namespace" "excalidraw" { module "tls_secret" { - source = "../setup_tls_secret" + source = "../../../modules/kubernetes/setup_tls_secret" namespace = kubernetes_namespace.excalidraw.metadata[0].name tls_secret_name = var.tls_secret_name } @@ -99,7 +99,7 @@ resource "kubernetes_service" "draw" { } module "ingress" { - source = "../ingress_factory" + source = "../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.excalidraw.metadata[0].name name = "draw" tls_secret_name = var.tls_secret_name diff --git a/modules/kubernetes/excalidraw/project/.gitignore b/stacks/excalidraw/module/project/.gitignore similarity index 100% rename from modules/kubernetes/excalidraw/project/.gitignore rename to stacks/excalidraw/module/project/.gitignore diff --git a/modules/kubernetes/excalidraw/project/Dockerfile b/stacks/excalidraw/module/project/Dockerfile similarity index 100% rename from modules/kubernetes/excalidraw/project/Dockerfile rename to stacks/excalidraw/module/project/Dockerfile diff --git a/modules/kubernetes/excalidraw/project/README.md b/stacks/excalidraw/module/project/README.md similarity index 100% rename from modules/kubernetes/excalidraw/project/README.md rename to stacks/excalidraw/module/project/README.md diff --git a/modules/kubernetes/excalidraw/project/go.mod b/stacks/excalidraw/module/project/go.mod similarity index 100% rename from modules/kubernetes/excalidraw/project/go.mod rename to stacks/excalidraw/module/project/go.mod diff --git a/modules/kubernetes/excalidraw/project/main.go b/stacks/excalidraw/module/project/main.go similarity index 100% rename from modules/kubernetes/excalidraw/project/main.go rename to stacks/excalidraw/module/project/main.go diff --git a/modules/kubernetes/excalidraw/project/static/editor.html b/stacks/excalidraw/module/project/static/editor.html similarity index 100% rename from modules/kubernetes/excalidraw/project/static/editor.html rename to stacks/excalidraw/module/project/static/editor.html diff --git a/stacks/excalidraw/providers.tf b/stacks/excalidraw/providers.tf new file mode 100644 index 00000000..516f9fed --- /dev/null +++ b/stacks/excalidraw/providers.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +variable "kube_config_path" { + type = string + default = "~/.kube/config" +} + +provider "kubernetes" { + config_path = var.kube_config_path +} + +provider "helm" { + kubernetes = { + config_path = var.kube_config_path + } +} diff --git a/stacks/excalidraw/secrets b/stacks/excalidraw/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/excalidraw/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/f1-stream/.terraform.lock.hcl b/stacks/f1-stream/.terraform.lock.hcl new file mode 100644 index 00000000..1e5d8b27 --- /dev/null +++ b/stacks/f1-stream/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/helm" { + version = "3.1.1" + hashes = [ + "h1:47CqNwkxctJtL/N/JuEj+8QMg8mRNI/NWeKO5/ydfZU=", + "zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275", + "zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a", + "zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29", + "zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104", + "zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990", + "zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34", + "zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8", + "zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1", + "zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b", + "zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903", + "zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.0.1" + hashes = [ + "h1:P0c8knzZnouTNFIRij8IS7+pqd0OKaFDYX0j4GRsiqo=", + "zh:02d55b0b2238fd17ffa12d5464593864e80f402b90b31f6e1bd02249b9727281", + "zh:20b93a51bfeed82682b3c12f09bac3031f5bdb4977c47c97a042e4df4fb2f9ba", + "zh:6e14486ecfaee38c09ccf33d4fdaf791409f90795c1b66e026c226fad8bc03c7", + "zh:8d0656ff422df94575668e32c310980193fccb1c28117e5c78dd2d4050a760a6", + "zh:9795119b30ec0c1baa99a79abace56ac850b6e6fbce60e7f6067792f6eb4b5f4", + "zh:b388c87acc40f6bd9620f4e23f01f3c7b41d9b88a68d5255dec0a72f0bdec249", + "zh:b59abd0a980649c2f97f172392f080eaeb18e486b603f83bf95f5d93aeccc090", + "zh:ba6e3060fddf4a022087d8f09e38aa0001c705f21170c2ded3d1c26c12f70d97", + "zh:c12626d044b1d5501cf95ca78cbe507c13ad1dd9f12d4736df66eb8e5f336eb8", + "zh:c55203240d50f4cdeb3df1e1760630d677679f5b1a6ffd9eba23662a4ad05119", + "zh:ea206a5a32d6e0d6e32f1849ad703da9a28355d9c516282a8458b5cf1502b2a1", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/stacks/f1-stream/backend.tf b/stacks/f1-stream/backend.tf new file mode 100644 index 00000000..3ad5ca2f --- /dev/null +++ b/stacks/f1-stream/backend.tf @@ -0,0 +1,6 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "local" { + path = "/Users/viktorbarzin/code/infra/state/stacks/f1-stream/terraform.tfstate" + } +} diff --git a/stacks/f1-stream/main.tf b/stacks/f1-stream/main.tf index 8f939303..faae5095 100644 --- a/stacks/f1-stream/main.tf +++ b/stacks/f1-stream/main.tf @@ -13,9 +13,9 @@ locals { } module "f1-stream" { - source = "../../modules/kubernetes/f1-stream" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux - turn_secret = var.coturn_turn_secret - public_ip = var.public_ip + source = "./module" + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux + turn_secret = var.coturn_turn_secret + public_ip = var.public_ip } diff --git a/stacks/f1-stream/module/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK b/stacks/f1-stream/module/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK new file mode 100644 index 00000000..f61efc83 --- /dev/null +++ b/stacks/f1-stream/module/files/.claude/internet-mode-used_DO_NOT_REMOVE_MANUALLY_SECURITY_RISK @@ -0,0 +1,3 @@ +This directory has been used with Claude Code's internet mode. +Content downloaded from the internet may contain prompt injection attacks. +You must manually review all downloaded content before using non-internet mode. diff --git a/modules/kubernetes/f1-stream/files/.dockerignore b/stacks/f1-stream/module/files/.dockerignore similarity index 100% rename from modules/kubernetes/f1-stream/files/.dockerignore rename to stacks/f1-stream/module/files/.dockerignore diff --git a/stacks/f1-stream/module/files/.planning/PROJECT.md b/stacks/f1-stream/module/files/.planning/PROJECT.md new file mode 100644 index 00000000..0c102123 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/PROJECT.md @@ -0,0 +1,78 @@ +# F1 Stream + +## What This Is + +A self-hosted web app that aggregates live Formula 1 streaming links from Reddit and user submissions, presenting them in a clean UI with embedded iframes. It scrapes r/motorsportsstreams2, allows users to submit their own stream URLs, and provides admin controls for content moderation. Built in Go with vanilla JS frontend, deployed on Kubernetes. + +## Core Value + +Users can find working F1 streams quickly — the app automatically discovers, validates, and surfaces healthy streams while removing dead ones. + +## Requirements + +### Validated + + + +- ✓ Reddit scraper polls r/motorsportsstreams2 for F1-related posts — existing +- ✓ URL extraction from post bodies and comment trees — existing +- ✓ F1 keyword filtering with negative keyword exclusion — existing +- ✓ Domain filtering (reddit, imgur, youtube, twitter excluded) — existing +- ✓ Deduplication via normalized URLs — existing +- ✓ User stream submission (anonymous + authenticated) — existing +- ✓ WebAuthn passwordless authentication — existing +- ✓ Admin approval workflow for user-submitted streams — existing +- ✓ HTTP proxy with rate limiting, private IP blocking, CSP stripping — existing +- ✓ Static frontend with iframe-based stream viewing — existing +- ✓ Default seed streams on first run — existing +- ✓ Stale link cleanup (24h) — existing +- ✓ Client-side health sort (reorder by reachability) — existing + +### Active + + + +- [ ] Scraper validates extracted URLs look like actual streams (video/player content), not random links +- [ ] Server-side health checker runs every 5 minutes against all known streams +- [ ] Health check: HTTP reachability check first, then proxy-fetch to detect video/player markers +- [ ] Configurable health check timeout +- [ ] Streams marked unhealthy after 5 consecutive check failures get hidden from public page +- [ ] Unhealthy streams retried on each check cycle — restored if they recover +- [ ] Scraped streams that pass health checks auto-published to main streams page +- [ ] Dead streams dynamically removed from the page without manual intervention +- [ ] Health status persisted (failure count, last check time, healthy/unhealthy state) + +### Out of Scope + +- Database migration (SQLite/PostgreSQL) — file-based storage is fine for this scope +- Multiple subreddit sources — stick with r/motorsportsstreams2 for now +- Real-time WebSocket push of stream status — polling is sufficient +- Mobile app — web-only +- OAuth/social login — WebAuthn is sufficient + +## Context + +- The app runs on a personal Kubernetes cluster, deployed via Terraform +- Single-user / small-group usage — performance at scale is not a concern +- The existing client-side `sortStreamsByHealth` does a basic `no-cors` fetch but can't inspect content; server-side checks via the proxy can do deeper validation +- Reddit's public JSON API requires no auth but rate-limits aggressively; the scraper already handles 429s with backoff +- Stream sites frequently go down, change URLs, or get taken down — health checking is essential for a good UX + +## Constraints + +- **Tech stack**: Go backend, vanilla JS frontend — no new frameworks or dependencies unless strictly necessary +- **Storage**: File-based JSON — no database +- **Deployment**: Docker container on Kubernetes, single replica +- **Reddit API**: Public JSON endpoints, must respect rate limits (1 req/sec delay already in place) + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| Server-side health checks over client-side only | Client can't inspect response content (CORS); server proxy can detect video markers | — Pending | +| 5 consecutive failures before hiding | Avoids flapping — streams that are temporarily down aren't immediately removed | — Pending | +| Auto-publish scraped streams that pass health | Reduces manual admin work; the health check is the quality gate | — Pending | +| Health check every 5 minutes | Balances freshness vs. load — streams don't change status that frequently | — Pending | + +--- +*Last updated: 2026-02-17 after initialization* diff --git a/stacks/f1-stream/module/files/.planning/REQUIREMENTS.md b/stacks/f1-stream/module/files/.planning/REQUIREMENTS.md new file mode 100644 index 00000000..c50a1f71 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/REQUIREMENTS.md @@ -0,0 +1,115 @@ +# Requirements: F1 Stream + +**Defined:** 2026-02-17 +**Core Value:** Users can find working F1 streams quickly — the app automatically discovers, validates, and surfaces healthy streams while removing dead ones. + +## v1 Requirements + +Requirements for initial release. Each maps to roadmap phases. + +### Scraper Validation + +- [ ] **SCRP-01**: Scraper filters Reddit posts by F1 keywords before extracting URLs (existing behavior, preserve) +- [ ] **SCRP-02**: Scraper validates each extracted URL by proxy-fetching it and checking for video/player content markers (video tags, HLS/DASH manifests, common player libraries) +- [ ] **SCRP-03**: URLs that don't look like streams (no video markers detected) are discarded before saving +- [ ] **SCRP-04**: Validation has a configurable timeout (default 10s) to avoid blocking on slow sites + +### Health Checking + +- [ ] **HLTH-01**: Background health checker service runs every 5 minutes against all known streams (scraped + user-submitted) +- [ ] **HLTH-02**: Health check performs HTTP reachability check first (does the URL respond with 2xx?) +- [ ] **HLTH-03**: If HTTP check passes, health checker proxy-fetches the page and checks for video/player content markers +- [ ] **HLTH-04**: Health check has a configurable timeout per check (default 10s) +- [ ] **HLTH-05**: Each stream tracks consecutive failure count, last check time, and healthy/unhealthy status in persisted state +- [ ] **HLTH-06**: Stream marked unhealthy after 5 consecutive health check failures +- [ ] **HLTH-07**: Unhealthy streams hidden from public streams page (`GET /api/streams/public`) +- [ ] **HLTH-08**: Unhealthy streams continue to be checked — restored to healthy if they recover (failure count resets) +- [ ] **HLTH-09**: Health check interval configurable via `HEALTH_CHECK_INTERVAL` env var (default 5m) + +### Auto-publish Pipeline + +- [ ] **AUTO-01**: Scraped streams that pass both scraper validation and initial health check are auto-published to the main streams page +- [ ] **AUTO-02**: Dead streams (unhealthy after 5 failures) are dynamically removed from the public page without admin intervention +- [ ] **AUTO-03**: Auto-published streams are distinguishable from user-submitted streams in the data model (source field) + +### Secure Embedding + +- [ ] **EMBED-01**: Proxy fetches stream page and attempts to extract direct video source URL (HLS .m3u8, DASH .mpd, direct MP4/WebM, or embedded video player source) +- [ ] **EMBED-02**: When direct video source is found, render it in a minimal HTML5 video player on the app's own page (no third-party page loaded) +- [ ] **EMBED-03**: When direct extraction fails, fall back to rendering the full proxied page in a shadow DOM sandbox +- [ ] **EMBED-04**: Shadow DOM sandbox blocks `window.open`, `window.top` navigation, popup creation, and `alert`/`confirm`/`prompt` +- [ ] **EMBED-05**: Shadow DOM sandbox prevents access to parent page cookies and localStorage +- [ ] **EMBED-06**: Proxy strips known ad/tracker scripts and domains from proxied content before serving +- [ ] **EMBED-07**: Proxy rewrites relative URLs in proxied content to route through the proxy (so sub-resources load correctly) +- [ ] **EMBED-08**: All proxied content served with strict CSP headers scoped to the sandbox context + +## v2 Requirements + +Deferred to future release. Tracked but not in current roadmap. + +### Enhanced Sources + +- **SRC-01**: Support scraping from additional subreddits or Discord channels +- **SRC-02**: User-reported stream quality ratings + +### UI Enhancements + +- **UI-01**: Real-time WebSocket push of stream health status changes +- **UI-02**: Stream quality indicator (resolution, bitrate if detectable) +- **UI-03**: Stream viewer count or popularity metric + +### Security Hardening + +- **SEC-01**: Ad-blocker filter list integration (uBlock Origin lists) +- **SEC-02**: JavaScript AST analysis for malicious patterns before allowing execution + +## Out of Scope + +| Feature | Reason | +|---------|--------| +| Database migration (SQLite/PostgreSQL) | File-based storage is sufficient for current scale | +| Multiple replica deployment | Single-user/small-group usage, single replica is fine | +| Mobile app | Web-only, responsive design sufficient | +| OAuth/social login | WebAuthn already works | +| Full browser automation (Puppeteer/Playwright) | Too heavy for stream validation; HTTP-based checks are sufficient | +| Video transcoding/re-streaming | Out of scope — we link to or proxy existing streams | + +## Traceability + +Which phases cover which requirements. Updated during roadmap creation. + +| Requirement | Phase | Status | +|-------------|-------|--------| +| SCRP-01 | Phase 1 | Pending | +| SCRP-02 | Phase 1 | Pending | +| SCRP-03 | Phase 1 | Pending | +| SCRP-04 | Phase 1 | Pending | +| HLTH-01 | Phase 2 | Pending | +| HLTH-02 | Phase 2 | Pending | +| HLTH-03 | Phase 2 | Pending | +| HLTH-04 | Phase 2 | Pending | +| HLTH-05 | Phase 2 | Pending | +| HLTH-06 | Phase 2 | Pending | +| HLTH-07 | Phase 2 | Pending | +| HLTH-08 | Phase 2 | Pending | +| HLTH-09 | Phase 2 | Pending | +| AUTO-01 | Phase 3 | Pending | +| AUTO-02 | Phase 3 | Pending | +| AUTO-03 | Phase 3 | Pending | +| EMBED-01 | Phase 4 | Pending | +| EMBED-02 | Phase 4 | Pending | +| EMBED-03 | Phase 5 | Pending | +| EMBED-04 | Phase 5 | Pending | +| EMBED-05 | Phase 5 | Pending | +| EMBED-06 | Phase 5 | Pending | +| EMBED-07 | Phase 5 | Pending | +| EMBED-08 | Phase 5 | Pending | + +**Coverage:** +- v1 requirements: 24 total +- Mapped to phases: 24 +- Unmapped: 0 + +--- +*Requirements defined: 2026-02-17* +*Last updated: 2026-02-17 after roadmap creation* diff --git a/stacks/f1-stream/module/files/.planning/ROADMAP.md b/stacks/f1-stream/module/files/.planning/ROADMAP.md new file mode 100644 index 00000000..d79f7d42 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/ROADMAP.md @@ -0,0 +1,106 @@ +# Roadmap: F1 Stream + +## Overview + +This roadmap delivers server-side stream quality assurance and secure viewing. First, the scraper learns to validate that extracted URLs actually contain video content. Then a background health checker continuously monitors all streams. These combine into an auto-publish pipeline that surfaces good streams and hides dead ones without admin intervention. Finally, secure embedding replaces raw iframes with native video playback where possible and a hardened sandbox fallback for everything else. + +## Phases + +**Phase Numbering:** +- Integer phases (1, 2, 3): Planned milestone work +- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) + +Decimal phases appear between their surrounding integers in numeric order. + +- [ ] **Phase 1: Scraper Validation** - Scraper validates extracted URLs contain video/player content before saving +- [ ] **Phase 2: Health Check Infrastructure** - Background service continuously monitors stream health and persists status +- [ ] **Phase 3: Auto-publish Pipeline** - Healthy scraped streams auto-publish; dead streams auto-hide +- [ ] **Phase 4: Video Extraction and Native Playback** - Extract direct video sources and play them in a native HTML5 player +- [ ] **Phase 5: Sandbox and Proxy Hardening** - Fallback rendering in a sandboxed shadow DOM with ad stripping and strict CSP + +## Phase Details + +### Phase 1: Scraper Validation +**Goal**: Scraped URLs are verified to contain actual video/player content before being stored, eliminating junk links at the source +**Depends on**: Nothing (extends existing scraper) +**Requirements**: SCRP-01, SCRP-02, SCRP-03, SCRP-04 +**Success Criteria** (what must be TRUE): + 1. Scraper still discovers F1-related posts from Reddit using keyword filtering (existing behavior preserved) + 2. Each extracted URL is proxy-fetched and inspected for video/player content markers (video tags, HLS/DASH manifests, player libraries) + 3. URLs without video content markers are discarded and do not appear in scraped.json + 4. Validation respects a configurable timeout so slow sites do not block the scrape cycle +**Plans**: 1 plan + +Plans: +- [ ] 01-01-PLAN.md — Add URL validation with video marker detection to scraper pipeline + +### Phase 2: Health Check Infrastructure +**Goal**: All known streams are continuously monitored for health, with status persisted and unhealthy streams hidden from users +**Depends on**: Phase 1 (reuses content validation logic from scraper validation) +**Requirements**: HLTH-01, HLTH-02, HLTH-03, HLTH-04, HLTH-05, HLTH-06, HLTH-07, HLTH-08, HLTH-09 +**Success Criteria** (what must be TRUE): + 1. A background service checks every known stream (scraped and user-submitted) on a regular interval that defaults to 5 minutes and is configurable via environment variable + 2. Each check performs HTTP reachability first, then proxy-fetches the page to verify video/player content markers + 3. Each stream's health state (consecutive failure count, last check time, healthy/unhealthy flag) is persisted across restarts + 4. A stream is hidden from the public streams page after 5 consecutive check failures, and restored if it later passes a check + 5. Health check timeout per stream is configurable +**Plans**: 2 plans + +Plans: +- [ ] 02-01-PLAN.md — HealthState model, store persistence, export HasVideoContent, create HealthChecker service +- [ ] 02-02-PLAN.md — Wire health checker in main.go, filter unhealthy streams from public API + +### Phase 3: Auto-publish Pipeline +**Goal**: Scraped streams that pass validation and health checks appear on the public page automatically; dead streams disappear without admin action +**Depends on**: Phase 1, Phase 2 +**Requirements**: AUTO-01, AUTO-02, AUTO-03 +**Success Criteria** (what must be TRUE): + 1. A scraped stream that passes scraper validation and its first health check is visible on the public streams page without any admin approval + 2. A stream marked unhealthy (5 consecutive failures) is no longer visible on the public page, with no admin intervention required + 3. Auto-published streams are distinguishable from user-submitted streams in the data model (source field tracks origin) +**Plans**: 1 plan + +Plans: +- [ ] 03-01-PLAN.md — Add Source field to Stream model, create PublishScrapedStream, wire scraper auto-publish + +### Phase 4: Video Extraction and Native Playback +**Goal**: When a stream URL contains an extractable video source, users watch it in a clean native HTML5 player instead of loading the third-party page +**Depends on**: Nothing (independent of phases 1-3; can be built in parallel but ordered here for delivery focus) +**Requirements**: EMBED-01, EMBED-02 +**Success Criteria** (what must be TRUE): + 1. The proxy can extract direct video source URLs (HLS .m3u8, DASH .mpd, direct MP4/WebM, or embedded player source attributes) from a stream page + 2. When a direct video source is found, the user sees a minimal HTML5 video player on the app's own page playing the stream without loading the original third-party page +**Plans**: 2 plans + +Plans: +- [ ] 04-01-PLAN.md — Backend video source extractor package and API endpoint +- [ ] 04-02-PLAN.md — Frontend native HTML5 video player with HLS.js and iframe fallback + +### Phase 5: Sandbox and Proxy Hardening +**Goal**: When direct video extraction fails, the proxied page is rendered safely in a sandbox that blocks popups, ads, and access to the parent page +**Depends on**: Phase 4 (this is the fallback path when extraction fails) +**Requirements**: EMBED-03, EMBED-04, EMBED-05, EMBED-06, EMBED-07, EMBED-08 +**Success Criteria** (what must be TRUE): + 1. When direct video extraction fails, the full proxied page renders inside a shadow DOM sandbox on the app's page + 2. The sandbox blocks window.open, top-frame navigation, popup creation, and alert/confirm/prompt dialogs + 3. The sandbox prevents the proxied content from accessing parent page cookies and localStorage + 4. Known ad/tracker scripts and domains are stripped from proxied content before serving, and relative URLs are rewritten to route through the proxy + 5. All proxied content is served with strict CSP headers scoped to the sandbox context +**Plans**: 2 plans + +Plans: +- [ ] 05-01-PLAN.md — Backend proxy hardening: HTML sanitizer with ad/tracker stripping, URL rewriting, and CSP headers +- [ ] 05-02-PLAN.md — Frontend shadow DOM sandbox replacing iframe fallback with API overrides + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 + +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. Scraper Validation | 0/1 | Planned | - | +| 2. Health Check Infrastructure | 0/2 | Planned | - | +| 3. Auto-publish Pipeline | 0/1 | Planned | - | +| 4. Video Extraction and Native Playback | 0/2 | Planned | - | +| 5. Sandbox and Proxy Hardening | 0/2 | Not started | - | diff --git a/stacks/f1-stream/module/files/.planning/STATE.md b/stacks/f1-stream/module/files/.planning/STATE.md new file mode 100644 index 00000000..043af6e1 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/STATE.md @@ -0,0 +1,93 @@ +# Project State + +## Project Reference + +See: .planning/PROJECT.md (updated 2026-02-17) + +**Core value:** Users can find working F1 streams quickly -- the app automatically discovers, validates, and surfaces healthy streams while removing dead ones. +**Current focus:** Phase 5: Sandbox Proxy Hardening (complete) + +## Current Position + +Phase: 5 of 5 (Sandbox Proxy Hardening) +Plan: 2 of 2 in current phase (complete) +Status: ALL PHASES COMPLETE -- project finished +Last activity: 2026-02-17 -- Completed 05-02 frontend shadow DOM sandbox + +Progress: [██████████] 100% + +## Performance Metrics + +**Velocity:** +- Total plans completed: 8 +- Average duration: 2.1min +- Total execution time: 0.28 hours + +**By Phase:** + +| Phase | Plans | Total | Avg/Plan | +|-------|-------|-------|----------| +| 01-scraper-validation | 1 | 3min | 3min | +| 02-health-check-infrastructure | 2 | 4min | 2min | +| 03-auto-publish-pipeline | 1 | 2min | 2min | +| 04-video-extraction-native-playback | 2 | 5min | 2.5min | +| 05-sandbox-proxy-hardening | 2 | 3min | 1.5min | + +**Recent Trend:** +- Last 5 plans: 2min, 3min, 2min, 2min, 1min +- Trend: Stable + +*Updated after each plan completion* + +## Accumulated Context + +### Decisions + +Decisions are logged in PROJECT.md Key Decisions table. +Recent decisions affecting current work: + +- Server-side health checks chosen over client-only (client can't inspect CORS responses) +- 5 consecutive failures threshold to avoid flapping +- Auto-publish for scraped streams that pass health check (health check is the quality gate) +- 5-minute health check interval (freshness vs load balance) +- String matching over DOM parsing for video detection (DOM reserved for Phase 4) +- 2MB body limit for HTML inspection to prevent memory issues +- 3 redirect limit to avoid infinite redirect chains on stream sites +- HealthMap reads file without lock to avoid deadlock from cross-lock scenarios +- Single HasVideoContent call covers both reachability and content checks +- Orphaned health state entries pruned each cycle to prevent unbounded file growth +- URLs not in health map assumed healthy to prevent new streams disappearing before first check +- HealthMap called within streamsMu/scrapedMu read locks safely via lock-free file read +- Source field uses string values (user/system/scraped) for readability over int enum +- PublishScrapedStream deduplicates by exact URL match; normalized matching stays in scraper layer +- Auto-publish iterates all validated links each cycle; deduplication makes repeat calls no-ops +- DOM parsing with golang.org/x/net/html for structured video source extraction (Phase 4) +- Dual extraction strategy: DOM walking + regex script parsing for maximum video URL coverage +- Priority ordering HLS > DASH > MP4 > WebM for frontend source selection +- 5-minute cache on extract endpoint to reduce upstream load +- Empty sources array (not error) when no video found to distinguish from fetch failures +- HLS.js loaded from jsDelivr CDN to avoid bundling complexity +- Extraction runs async after card render -- progressive enhancement with shadow DOM sandbox fallback +- DASH sources fall back to shadow DOM sandbox (dash.js too heavy for current scope) +- Silent console.log on extraction failure -- no user-facing errors for extraction issues +- 50+ ad/tracker domains in blocklist with parent-domain walk-up matching +- Inline scripts kept for video players; blocked scripts removed by domain +- CSP allows img/media/connect broadly since video sources come from arbitrary origins +- Non-HTML sub-resources proxied as-is with CSP headers +- Closed shadow DOM mode prevents external JS from accessing shadow root +- Script element created via createElement for execution in shadow DOM (innerHTML scripts don't execute) +- Direct link fallback when sandbox proxy fetch fails rather than broken state + +### Pending Todos + +None yet. + +### Blockers/Concerns + +None yet. + +## Session Continuity + +Last session: 2026-02-17 +Stopped at: Completed 05-02-PLAN.md (frontend shadow DOM sandbox) -- ALL PHASES COMPLETE +Resume file: None diff --git a/stacks/f1-stream/module/files/.planning/codebase/ARCHITECTURE.md b/stacks/f1-stream/module/files/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 00000000..76ebca6b --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,191 @@ +# Architecture + +**Analysis Date:** 2026-02-17 + +## Pattern Overview + +**Overall:** Layered monolithic service with clear separation between HTTP API layer, business logic, and persistent storage layer. + +**Key Characteristics:** +- Single Go binary serving both API and static frontend +- File-based JSON persistence (no database) +- Modular internal packages for distinct concerns +- WebAuthn-based passwordless authentication +- Background scraper for content aggregation +- Rate-limited proxy service + +## Layers + +**HTTP Handler Layer:** +- Purpose: Accept and route HTTP requests, apply middleware, respond to clients +- Location: `internal/server/` +- Contains: Route registration, handler functions, middleware chains +- Depends on: Auth, Store, Proxy, Scraper packages +- Used by: HTTP clients (browser, mobile) + +**Authentication & Authorization Layer:** +- Purpose: Manage user registration, login, sessions, and permission checks +- Location: `internal/auth/` +- Contains: WebAuthn ceremony implementations, session management, context helpers +- Depends on: Store, go-webauthn library +- Used by: Server middleware and handlers + +**Business Logic Layer:** +- Purpose: Core domain operations (stream management, scraping, proxying) +- Location: `internal/scraper/`, `internal/proxy/` +- Contains: Scraper service (Reddit polling), Proxy service (content fetching with rate limiting) +- Depends on: Store for persistence +- Used by: Server, main entry point for orchestration + +**Data Model Layer:** +- Purpose: Define domain types and interfaces +- Location: `internal/models/models.go` +- Contains: `User`, `Stream`, `ScrapedLink`, `Session` types +- Depends on: External WebAuthn library for credential types +- Used by: All layers + +**Persistence Layer:** +- Purpose: Provide file-based storage abstraction +- Location: `internal/store/` +- Contains: JSON read/write helpers, file-based storage per entity type (streams, users, sessions, scraped links) +- Depends on: Models, filesystem +- Used by: All business logic layers + +## Data Flow + +**Stream Submission Flow:** + +1. Client submits stream URL and title via `POST /api/streams` +2. Server handler validates URL format and length +3. Optional: If authenticated user, stream marked as unpublished; if anonymous, marked as published +4. Stream stored via `Store.AddStream()` which reads current `streams.json`, appends new stream, writes atomically +5. Response returned with stream metadata + +**Authentication Flow (WebAuthn):** + +1. User initiates registration with `POST /api/auth/register/begin` sending username +2. Server validates username format, checks uniqueness, creates temporary user +3. Server generates WebAuthn registration options via go-webauthn library +4. Server stores session data in memory with 5-minute expiry +5. Client performs attestation ceremony, sends credential via `POST /api/auth/register/finish?username=...` +6. Server retrieves in-memory session, validates with go-webauthn +7. Credential appended to user in `users.json` +8. Session token created in `sessions.json`, set as HttpOnly cookie + +**Scraper Flow:** + +1. Scraper runs on timer (default 15 minutes) or on manual trigger +2. Calls `scrapeReddit()` to poll r/motorsportsstreams2 new posts +3. Extracts URLs using regex, filters by F1-related keywords +4. Merges with existing `scraped.json`, deduplicating by normalized URL +5. Writes updated list atomically +6. Stale entries cleaned up, active ones returned via `GET /api/scraped` + +**Proxy Flow:** + +1. Client requests `GET /proxy?url=https://...` +2. Server validates URL scheme (must be HTTPS), length, and target is not private IP +3. Applies rate limiting via token bucket per client IP +4. Fetches URL with timeout, limits response body to 5MB +5. Injects `` tag into HTML response for relative URL resolution +6. Strips X-Frame-Options and CSP headers to allow iframe embedding +7. Returns modified content + +**Admin Approval Flow:** + +1. Anonymous streams created with `Published: false` +2. Admin views all streams via `GET /api/admin/streams` +3. Admin toggles publication status via `PUT /api/streams/{id}/publish` +4. Published streams visible in `GET /api/streams/public` + +**State Management:** + +- **User Sessions:** In-memory WebAuthn ceremony sessions (5-minute TTL), persistent sessions in `sessions.json` with configurable TTL +- **Streams:** Fully loaded into memory from `streams.json` on each read/write, entire file rewritten atomically +- **Scraped Links:** Similar full-file pattern, deduplicated during scrape merge +- **Users:** Fully loaded per query, updated atomically per write +- **Cleanup:** Hourly cleanup of expired sessions via background goroutine + +## Key Abstractions + +**Store Interface (implicit):** +- Purpose: Encapsulate all file-based persistence operations +- Examples: `store.AddStream()`, `store.GetUserByName()`, `store.CreateSession()` +- Pattern: Each entity type has dedicated file; reads are lock-protected; writes are atomic (temp-file-then-rename) + +**Auth Middleware Chain:** +- Purpose: Extract and validate user from session cookie, inject into request context +- Examples: `AuthMiddleware()`, `RequireAuth()`, `RequireAdmin()` +- Pattern: Composable handler functions that wrap next handler + +**Scraper Service:** +- Purpose: Periodically fetch and aggregate content from external sources +- Examples: Background goroutine running on interval, triggered scrape +- Pattern: Mutex-protected scrape operations to prevent concurrent executions + +**Proxy Handler:** +- Purpose: Fetch external content safely with rate limiting and framing bypass +- Examples: URL validation, private IP blocking, rate limiting per IP, HTML base tag injection +- Pattern: Implements `http.Handler` interface, maintains per-IP token bucket state + +## Entry Points + +**HTTP Server (`main.go`):** +- Location: `main.go` +- Triggers: Process start +- Responsibilities: Initialize all services, configure routes, handle graceful shutdown on SIGTERM/SIGINT + +**Handler Routes (`internal/server/server.go`):** +- Location: `internal/server/server.go:registerRoutes()` +- Pattern: All routes defined in single function, middleware applied uniformly +- Public endpoints: Health, public streams, public scraped links +- Authenticated endpoints: Personal streams, submit stream, delete stream +- Admin endpoints: All streams, toggle publish, trigger scrape + +**Background Services:** +- Scraper: Started in goroutine at startup via `scraper.Run(ctx)` +- Session cleanup: Goroutine with hourly ticker +- Proxy rate-limit cleanup: Goroutine with 10-minute ticker + +## Error Handling + +**Strategy:** Error strings returned in JSON responses with appropriate HTTP status codes. Panics caught and logged by recovery middleware. + +**Patterns:** +- Validation errors: `400 Bad Request` +- Authentication failures: `401 Unauthorized` +- Permission denied: `403 Forbidden` +- Resource not found: `404 Not Found` +- Duplicate entries: `409 Conflict` +- Server errors: `500 Internal Server Error` +- Rate limit exceeded: `429 Too Many Requests` + +Errors include descriptive messages: `{"error":"username must be 3-30 chars, alphanumeric or underscore"}` + +## Cross-Cutting Concerns + +**Logging:** stdlib log package +- Request logging: Method, path, remote address via `LoggingMiddleware` +- Scraper logging: Intervals, timing, link counts +- Proxy logging: Fetch errors +- All goes to stdout + +**Validation:** +- Username: 3-30 chars, alphanumeric + underscore +- URLs: Must be HTTP(S), max 2048 chars, proxy-only supports HTTPS +- HTML escaping on stream titles to prevent injection + +**Authentication:** +- WebAuthn for registration/login (passwordless) +- Session tokens as HttpOnly, Secure, SameSite=Strict cookies +- Configurable session TTL (default 720 hours) +- First registered user becomes admin unless ADMIN_USERNAME env var set + +**CORS/Origin Check:** +- Origin header validated on mutation requests (POST, PUT, DELETE) +- Allowed origins configurable via WEBAUTHN_ORIGIN env var (comma-separated) +- CSRF protection via origin validation + +--- + +*Architecture analysis: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/CONCERNS.md b/stacks/f1-stream/module/files/.planning/codebase/CONCERNS.md new file mode 100644 index 00000000..d295032e --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/CONCERNS.md @@ -0,0 +1,232 @@ +# Codebase Concerns + +**Analysis Date:** 2026-02-17 + +## Tech Debt + +**File-based JSON storage as primary data persistence:** +- Issue: All data (users, streams, sessions, scraped links) are stored as JSON files on disk with file-level locking. This is a fundamental scalability constraint. +- Files: `internal/store/store.go`, `internal/store/streams.go`, `internal/store/sessions.go`, `internal/store/users.go`, `internal/store/scraped.go` +- Impact: + - Non-atomic multi-file operations (e.g., DeleteStream reads all streams, filters, writes back). Race conditions possible if two deletes happen simultaneously. + - Entire file loaded into memory for any operation, even reads. With thousands of streams/sessions, this becomes slow and memory-inefficient. + - Sessions file grows unbounded until manual cleanup (CleanExpiredSessions runs hourly). Could cause memory/disk pressure. + - No transaction support, no rollback capability on failure. +- Fix approach: Migrate to a proper database (SQLite for simplicity, PostgreSQL for production). Keep JSON file for backup/export purposes only. + +**In-memory WebAuthn ceremony session storage with no cleanup guarantee:** +- Issue: Registration and login ceremony session data stored in `Auth.regSessions` and `Auth.loginSessions` maps. Cleanup relies on goroutines that may not execute if server crashes. +- Files: `internal/auth/auth.go` (lines 27-29, 107-117, 230-239) +- Impact: + - Memory leak on server restarts: orphaned sessions never cleaned up. + - No recovery mechanism if goroutine misses cleanup window. + - Session hijacking if an attacker can predict/guess the cleanup timing. +- Fix approach: Either move ceremony sessions to persistent store or use a time.AfterFunc with guaranteed cleanup (still risky). Better: use signed JWTs for ceremony state instead of server-side storage. + +**Scraper loads entire scraped links list into memory on every scrape:** +- Issue: `Scraper.scrape()` loads all existing links, filters and deduplicates them, then rewrites entire file. +- Files: `internal/scraper/scraper.go` (lines 46-92) +- Impact: With thousands of links, each 15-minute scrape cycle causes a large memory spike and full file rewrite. Inefficient deduplication logic (O(n) map lookups on every new link). +- Fix approach: With database migration, use INSERT OR IGNORE / upsert patterns. For now, batch process links in chunks and use database indexes for deduplication. + +**No input validation on URL lengths beyond basic checks:** +- Issue: URL length limited to 2048 chars in two places (`internal/server/server.go` line 153, `internal/proxy/proxy.go` line 72), but no validation of URL structure beyond "starts with http/https" and HTTPS-only in proxy. +- Files: `internal/server/server.go` (lines 146-160), `internal/proxy/proxy.go` (lines 54-80) +- Impact: Malformed URLs could bypass checks and cause unexpected behavior in downstream systems. User submission streams could contain typos/malware links. +- Fix approach: Use a proper URL parsing library with validation. Whitelist domains for stream submissions. Consider regex validation for known stream site patterns. + +**Hardcoded default streams in main.go:** +- Issue: Default stream URLs are hardcoded and point to external streaming sites that may become unavailable, redirect, or change terms of service. +- Files: `main.go` (lines 100-123) +- Impact: If any of these URLs break, users get broken default content. Sites could shut down or get legal takedown notices. Application appears to endorse/support these sites. +- Fix approach: Move to configuration file. Make seeding optional. Add stream validation/health checks before serving. Consider removing entirely if this is a liability concern. + +**Proxy strips CSP headers without replacement:** +- Issue: `internal/proxy/proxy.go` deliberately strips `X-Frame-Options` and CSP headers (line 123) to allow iframe-based proxying. No security headers added back. +- Files: `internal/proxy/proxy.go` (lines 121-125) +- Impact: Proxied content loses all origin security protections. Could allow downstream attacks to run XSS, clickjacking, etc. in the proxy context. Injected `` tag doesn't prevent all attacks. +- Fix approach: Add back a strict CSP policy scoped to the proxy origin. Implement iframe sandbox attributes. Add additional security headers (X-Content-Type-Options: nosniff, etc.). + +## Security Considerations + +**Authentication ceremony session fixation vulnerability:** +- Risk: Username used as session key for WebAuthn ceremonies (`Auth.BeginRegistration`, `Auth.BeginLogin`). Attacker could start ceremony for victim's account, then victim continues from attacker's session state. +- Files: `internal/auth/auth.go` (lines 107-108, 230-231) +- Current mitigation: None. Ceremony session stored in-memory and deleted after 5 minutes, but no CSRF token or state validation. +- Recommendations: Use cryptographically random state tokens for ceremony sessions instead of username. Store state in secure HTTP-only cookies or database. Validate state on finish. + +**Rate limiting per-IP but no account lockout for failed authentication:** +- Risk: Brute force attacks on specific usernames are possible. Attacker can try many passwords (using different IPs) against a single account without consequence. +- Files: `internal/proxy/proxy.go` implements rate limiting (per-IP token bucket), but no equivalent exists for auth endpoints (`internal/auth/auth.go`). +- Current mitigation: WebAuthn makes guessing harder (passkeys), but early attack surface (BeginLogin endpoint) has no protection. Leaked user list could enable targeted attacks. +- Recommendations: Add per-username failure tracking. Lock account after N failed attempts. Add exponential backoff. Require captcha after threshold. + +**CORS Origin validation incomplete:** +- Risk: `OriginCheck` middleware in `internal/server/middleware.go` (lines 71-93) only checks on non-GET requests. GET requests can still trigger state-changing operations (e.g., visiting a crafted link that proxies through the app). +- Files: `internal/server/middleware.go` (lines 74) +- Current mitigation: Proxy request uses query param, but no SameSite cookie attribute on proxy endpoint (only on session cookie). +- Recommendations: Require Origin header on all mutation requests. Consider using POST for scrape trigger. Add X-CSRF-Token validation. + +**Admin user initialization has race condition:** +- Risk: First user to register becomes admin if `ADMIN_USERNAME` not set. Two concurrent registration requests could both see 0 users and both become admin. +- Files: `internal/auth/auth.go` (lines 83-91) +- Current mitigation: Relies on file-level locking in store operations, but store operations are done after the check (line 121), not atomic. +- Recommendations: Move first-user-is-admin logic into CreateUser transaction, or seed admin during initialization phase before accepting requests. + +**Session token stored in http-only cookie but not marked Secure in non-HTTPS:** +- Risk: Cookie marked `Secure: r.TLS != nil` (line 187, 300). In development or non-HTTPS deployments, session token sent over plaintext HTTP. +- Files: `internal/auth/auth.go` (lines 187, 300) +- Current mitigation: None for non-HTTPS. Relies on deployment to enforce HTTPS. +- Recommendations: Always set Secure=true. Force HTTPS in production via HSTS header. Log warning if TLS is nil. + +**Proxy does not validate Content-Type before injecting `` tag:** +- Risk: Non-HTML responses (PDFs, images, binaries) could be corrupted by injecting `` tag. Base64 encoded binary data could break. +- Files: `internal/proxy/proxy.go` (lines 104-119) +- Current mitigation: 5MB body size limit, but no content-type validation. +- Recommendations: Check Content-Type header before modification. Skip injection for non-HTML types. Use proper HTML parsing (e.g., golang.org/x/net/html) instead of string manipulation. + +## Performance Bottlenecks + +**Scraper Reddit parsing with inefficient comment recursion:** +- Problem: `walkComments` in `internal/scraper/reddit.go` (lines 245-260) recursively walks comment trees using JSON unmarshaling in each recursion level. Could cause O(n^2) behavior on deep comment threads. +- Files: `internal/scraper/reddit.go` (lines 245-260, 132-142) +- Cause: Each comment reply is unmarshaled separately. For a thread with 1000 nested replies, this could create 1000 unmarshaling operations. +- Improvement path: Pre-flatten comment tree or use iterative traversal instead of recursion. Cache unmarshaled comments during initial fetch. + +**O(n) lookups on every store operation:** +- Problem: All store methods (GetUserByName, GetUserByID, FindStream by ID) iterate through entire in-memory list. +- Files: `internal/store/users.go` (lines 21-49), `internal/store/streams.go` (lines 12-52) +- Cause: File-based storage forces full-file loads. Even with caching, no indexing. +- Improvement path: With database migration, use indexed lookups. For now, maintain in-process cache with invalidation on updates. + +**Rate limiter token bucket not garbage collected properly:** +- Problem: Buckets for old IPs are deleted every 10 minutes (bucketCleanup), but inactive users' buckets accumulate until cleanup cycle. +- Files: `internal/proxy/proxy.go` (lines 170-181) +- Cause: Cleanup is reactive, not triggered on write. High-traffic scenarios could have thousands of stale buckets in memory. +- Improvement path: Use sync.Map for lock-free reads. Implement heap-based cleanup timer per bucket instead of global interval. + +**Entire streams/sessions list rewritten on every add/delete:** +- Problem: Adding one stream requires reading all streams, appending, and rewriting entire file. Deleting a session does the same. +- Files: `internal/store/streams.go` (lines 54-78, 80-103), `internal/store/sessions.go` (lines 22-44, 61-81) +- Cause: Atomic write pattern (writeJSON uses temp-file-then-rename), but forces full serialization. +- Improvement path: Migrate to database with transaction support. Implement write-ahead logging if staying with files. + +## Fragile Areas + +**Proxy string-based HTML manipulation is fragile:** +- Files: `internal/proxy/proxy.go` (lines 107-119) +- Why fragile: Uses string.Index to find `` and `` tags with string.ToLower comparisons. Cases like `` would be missed. Malformed HTML (missing closing tags, nested structures) could place `` tag in wrong location. +- Safe modification: Use golang.org/x/net/html parser. Insert `` into head node properly. Handle edge cases (no head, multiple heads, xhtml). +- Test coverage: No tests for proxy HTML injection logic. Edge cases untested. + +**Auth ceremony cleanup relies on goroutines:** +- Files: `internal/auth/auth.go` (lines 112-117, 234-239) +- Why fragile: If goroutine is blocked or delayed, cleanup doesn't happen. No guarantee cleanup runs at correct time. Server crash loses all in-flight ceremonies. +- Safe modification: Use context deadlines instead of sleep timers. Implement cleanup on FinishRegistration/FinishLogin regardless of goroutine. Store ceremonies in database with TTL. +- Test coverage: No tests for ceremony timeout behavior. Hard to test goroutine cleanup timing. + +**DeleteStream and related operations use string.Contains for error classification:** +- Files: `internal/server/server.go` (lines 196-203) +- Why fragile: Error messages must contain specific strings ("not authorized", "not found") for proper HTTP status mapping. Changing error text breaks error handling. +- Safe modification: Use error types (custom errors or error wrapping with errors.Is/As). Map error types to status codes centrally. +- Test coverage: No tests for error status code mapping. + +**Scraper is single-threaded with mutex but TriggerScrape starts new goroutine:** +- Files: `internal/scraper/scraper.go` (lines 42-44, 46-92) +- Why fragile: Calling TriggerScrape while scrape() is running (locked) will queue a second scrape. If scrapes take >15 minutes, queue grows. No bounds on concurrent scrapes. +- Safe modification: Use atomic flag to prevent concurrent scrapes. Queue only one pending scrape. Timeout long-running scrapes. +- Test coverage: No tests for concurrent scrape behavior or queue limits. + +**Admin check depends on user count atomicity:** +- Files: `internal/auth/auth.go` (lines 83-91) +- Why fragile: Check user count, then create user are separate operations. Two concurrent registrations both see count=0, both get admin. Later operation fails due to username uniqueness check, but by then both claimed to be admin. +- Safe modification: Move atomicity into CreateUser. Use database transaction. +- Test coverage: No concurrency tests for admin initialization. + +## Scaling Limits + +**All data files live on single filesystem:** +- Current capacity: Depends on disk size. Assuming 1GB available, JSON files with generous spacing could hold ~100k streams, users, or sessions before performance degrades. +- Limit: At 10k active users with 5 sessions each (50k sessions), sessions.json alone is >50MB uncompressed. Each read loads entire file. +- Scaling path: Migrate to database. Use SQLite for single-node, PostgreSQL for distributed. Implement sharding for sessions by user_id. + +**In-memory rate limit buckets per IP:** +- Current capacity: ~100k unique IPs can be tracked before memory pressure (each bucket ~48 bytes). +- Limit: Behind a proxy/load balancer, all traffic appears from proxy IP, making per-IP limiting useless. Map grows indefinitely per proxy. +- Scaling path: Move rate limiting to reverse proxy/load balancer layer (nginx, Envoy). Or, extract real IP from X-Forwarded-For more carefully (currently does this, but assumes trust). + +**Scraper single-threaded, only hits one subreddit:** +- Current capacity: 25 posts per run * 15-min interval = 100 posts/hour. Each post processes comments once. Total throughput ~1000-5000 URLs/hour depending on post depth. +- Limit: If stream demand increases or multiple subreddits need scraping, single scraper becomes bottleneck. No parallelism. +- Scaling path: Implement scraper pool. Scrape multiple subreddits in parallel. Move scraper to separate service. Implement distributed job queue. + +**WebAuthn session storage grows unbounded until server restart:** +- Current capacity: Each ceremony session is ~1KB. 1000 concurrent registrations = 1MB. 100k in-flight = 100MB. +- Limit: Memory exhaustion if registrations are started but not finished (or attacker starts many ceremonies). +- Scaling path: Use database for ceremony sessions. Implement hard timeout (e.g., 5 min) enforced by scheduled cleanup task. Set max concurrent ceremonies. + +## Dependencies at Risk + +**go-webauthn/webauthn v0.15.0:** +- Risk: Security library. May have vulnerabilities. Check for updates regularly. +- Impact: Passkey authentication could be compromised if library has bugs. +- Migration plan: Keep updated. Monitor GitHub releases. Test updates before deploying. + +**Hardcoded subreddit URL (reddit.com API):** +- Risk: Reddit API could change, add authentication requirements, or shut down /r/motorsportsstreams2 community. +- Impact: Scraper stops working entirely. No fallback stream sources. +- Migration plan: Implement abstraction for stream sources. Support multiple scraper backends (Reddit, Discord, Twitter, etc.). Add health checks for scraper endpoints. + +## Test Coverage Gaps + +**No tests for HTTP error handling:** +- What's not tested: Error status code mapping, error response formatting, error logging. +- Files: `internal/server/server.go` (all handlers), `internal/auth/auth.go` (all endpoints) +- Risk: Error responses could be inconsistent or leaky (exposing internal details). Status codes could be wrong. +- Priority: High + +**No tests for concurrent store operations:** +- What's not tested: Race conditions in add/delete/update. Concurrent reads while write in progress. +- Files: `internal/store/streams.go`, `internal/store/sessions.go`, `internal/store/users.go` +- Risk: Data corruption or loss under load. Auth bypass if race condition allows duplicate users. +- Priority: High + +**No tests for WebAuthn ceremony timeouts:** +- What's not tested: Behavior when ceremony session expires. Cleanup of orphaned sessions. +- Files: `internal/auth/auth.go` (BeginRegistration, FinishRegistration, BeginLogin, FinishLogin) +- Risk: Session fixation, orphaned memory, unexpected behavior on retry. +- Priority: Medium + +**No tests for proxy HTML injection:** +- What's not tested: Edge cases (malformed HTML, no head tag, nested structures). Security implications (XSS prevention, CSP). +- Files: `internal/proxy/proxy.go` (ServeHTTP) +- Risk: Injected tags could be placed incorrectly. Proxied content could break. Security headers could be ineffective. +- Priority: Medium + +**No tests for rate limiter token bucket algorithm:** +- What's not tested: Burst capacity behavior, refill rate, edge cases (high request volume, time skew). +- Files: `internal/proxy/proxy.go` (allowRequest, cleanBuckets) +- Risk: Rate limiting could be too strict or too lenient. Cleanup could fail to run. +- Priority: Medium + +**No tests for admin initialization logic:** +- What's not tested: First user gets admin flag. Edge cases with concurrent registrations. Behavior when ADMIN_USERNAME is set. +- Files: `internal/auth/auth.go` (BeginRegistration, lines 83-91) +- Risk: Non-admin user gets admin flag (privilege escalation). Two admins created unexpectedly. +- Priority: High + +**No integration tests for full auth flow:** +- What's not tested: Complete registration + login + logout cycle. Error recovery. Session expiration. +- Files: All of `internal/auth/auth.go` and `internal/server/server.go` auth endpoints. +- Risk: Subtle bugs in ceremony sequencing. Auth logic could break without being detected. +- Priority: High + +**No tests for scraper Reddit parsing:** +- What's not tested: Comment tree recursion. URL extraction. F1 keyword matching. Deduplication logic. +- Files: `internal/scraper/reddit.go` +- Risk: Scraper could miss streams, extract bad URLs, or fail on unexpected Reddit response format. +- Priority: Medium + +--- + +*Concerns audit: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/CONVENTIONS.md b/stacks/f1-stream/module/files/.planning/codebase/CONVENTIONS.md new file mode 100644 index 00000000..3a4f4e04 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,159 @@ +# Coding Conventions + +**Analysis Date:** 2026-02-17 + +## Naming Patterns + +**Files:** +- Go packages: lowercase, single word when possible (e.g., `auth`, `store`, `proxy`) +- Go files: lowercase with descriptive names (e.g., `server.go`, `middleware.go`, `reddit.go`) +- JSON files: snake_case (e.g., `users.json`, `sessions.json`, `scraped_links.json`) +- JavaScript files: camelCase (e.g., `app.js`, `auth.js`, `streams.js`) + +**Functions and Methods:** +- Go: PascalCase for exported functions (e.g., `New`, `BeginRegistration`, `ServeHTTP`) +- Go: camelCase for unexported functions (e.g., `randomID`, `isF1Post`, `normalizeURL`) +- JavaScript: camelCase for all functions (e.g., `showToast`, `switchTab`, `doRegister`) + +**Variables and Fields:** +- Go: camelCase for local variables (e.g., `streams`, `userID`, `sessionTTL`) +- Go: PascalCase for exported struct fields (e.g., `ID`, `Username`, `IsAdmin`) +- Go: prefixed mutex pattern: `resourceMu` for mutex protecting resource (e.g., `streamsMu`, `usersMu`, `sessionsMu`) +- JavaScript: camelCase for all variables (e.g., `currentUser`, `beginResp`, `container`) + +**Types and Constants:** +- Go: PascalCase for exported types (e.g., `Server`, `Auth`, `Store`, `User`) +- Go: camelCase for unexported types (e.g., `contextKey`, `bucket`, `redditListing`) +- Go: SCREAMING_SNAKE_CASE for constants (e.g., `maxBodySize`, `rateLimit`, `bucketCleanup`) + +**Interfaces:** +- Go context keys use private types with exported constants (e.g., `type contextKey string; const userKey contextKey = "user"`) + +## Code Style + +**Formatting:** +- Language: Go (no automated formatter config detected, using standard gofmt conventions) +- Import organization: Standard library → local packages (separated by blank line) +- File layout: Package declaration → Imports → Constants/Variables → Types → Functions + +**Linting:** +- No eslint or golangci-yml configuration found +- Go code follows idiomatic Go conventions: error checking, defer cleanup, interface composition + +## Import Organization + +**Go Order:** +1. Standard library imports (context, encoding/json, fmt, log, etc.) +2. Blank line +3. Local f1-stream packages (internal/auth, internal/models, etc.) +4. Blank line +5. External third-party packages (github.com/...) + +**Example from `internal/auth/auth.go`:** +```go +import ( + "crypto/rand" + "encoding/json" + "fmt" + "log" + "net/http" + "regexp" + "sync" + "time" + + "f1-stream/internal/models" + "f1-stream/internal/store" + + "github.com/go-webauthn/webauthn/webauthn" +) +``` + +**JavaScript:** +- No explicit import organization (vanilla JavaScript, no modules) +- HTML file loads scripts in order: utils → app → auth/streams + +## Error Handling + +**Patterns:** +- Go: Explicit error return as second value (e.g., `err := operation(); if err != nil { return err }`) +- Go: Wrapping errors with context: `fmt.Errorf("operation failed: %w", err)` +- Go: String matching on error messages for classification (see `internal/server/server.go` line 196-205) +- Go: Logging errors with `log.Printf()` for non-critical failures, `log.Fatalf()` for startup errors +- Go: HTTP errors returned via `http.Error(w, message, statusCode)` for API endpoints +- JavaScript: Try-catch blocks for async operations, error fields in UI (e.g., `errEl.textContent = err.error || 'Operation failed'`) + +**HTTP Error Responses:** +- Standard JSON format: `{"error":"description"}` +- Success responses vary by endpoint (JSON arrays, `{"ok":true}`, encoded objects via `json.NewEncoder`) + +## Logging + +**Framework:** `log` package (standard library) + +**Patterns:** +- Informational: `log.Printf("message with %v context", value)` +- Errors: `log.Printf("operation failed: %v", err)` +- Startup: `log.Fatalf("critical: %v", err)` for initialization failures +- Component prefixes: `log.Printf("scraper: action description")` + +**Example from `internal/scraper/scraper.go`:** +```go +log.Printf("scraper: starting scrape") +log.Printf("scraper: error after %v: %v", time.Since(start).Round(time.Millisecond), err) +log.Printf("scraper: done in %v, added %d new links (total: %d)", time.Since(start).Round(time.Millisecond), added, len(existing)) +``` + +## Comments + +**When to Comment:** +- Explain WHY, not WHAT (code shows what, comments explain reasoning) +- Used for non-obvious logic or security concerns +- Example from `internal/proxy/proxy.go` line 123: `// Explicitly do NOT copy X-Frame-Options or CSP` +- Example from `internal/auth/auth.go` line 120: `// Store user temporarily - will be committed on finish` + +**Patterns:** +- Short inline comments before complex sections +- Package-level comments before exported types explaining purpose +- Security/business logic gets explained + +## Function Design + +**Size:** Functions keep complexity low, typically 20-50 lines; larger operations split across helpers + +**Parameters:** +- Receiver methods use pointer receivers: `func (s *Store) GetSession(token string) ...` +- Constructor pattern returns initialized type and error: `func New(...) (*Type, error)` +- HTTP handlers follow signature: `func(w http.ResponseWriter, r *http.Request)` + +**Return Values:** +- Errors always returned as last value: `(result, error)` +- Multiple return values when needed: `(*Type, error)` or `([]Type, error)` +- HTTP handlers write directly to ResponseWriter, return via `http.Error()` or direct writes +- Query methods return nil for "not found" rather than error (see `internal/store/users.go` line 33) + +## Module Design + +**Exports:** +- Exported names start with capital letter (e.g., `New`, `User`, `Server`) +- Unexported helpers start with lowercase +- Types exported when they're part of public API +- Helper functions (e.g., `isF1Post`, `normalizeURL`) kept unexported + +**Barrel Files:** Not used; single concerns per file + +**Package Organization:** +- `internal/auth/`: Authentication and WebAuthn implementation +- `internal/store/`: Data persistence (users.go, streams.go, sessions.go, scraped.go, store.go) +- `internal/server/`: HTTP routing and middleware +- `internal/scraper/`: Reddit scraping logic +- `internal/proxy/`: HTTP proxy with rate limiting +- `internal/models/`: Type definitions only + +**Struct Composition:** +- `Server` struct holds dependencies injected at construction (line 15-21 in `internal/server/server.go`) +- Methods extend functionality through receiver pattern +- No inheritance, composition via embedded types used sparingly + +--- + +*Convention analysis: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/INTEGRATIONS.md b/stacks/f1-stream/module/files/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 00000000..23ba1d1f --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,121 @@ +# External Integrations + +**Analysis Date:** 2026-02-17 + +## APIs & External Services + +**Reddit API:** +- Service: Reddit public JSON API (no authentication required) +- What it's used for: Scraping F1 stream links from r/motorsportsstreams2 subreddit + - Fetches 25 most recent posts: `https://www.reddit.com/r/motorsportsstreams2/new.json?limit=25` + - Extracts URLs from post titles and comment bodies + - Filters by F1-related keywords + - Runs on configurable interval (default 15 minutes) +- Implementation: `internal/scraper/reddit.go` +- Authentication: None - public API endpoint + +**HTTP Proxy Service:** +- Service: Internal HTTP proxy for accessing external streams +- What it's used for: Fetching and proxying external stream pages while enforcing security policies + - Rate limiting: 30 requests/minute per IP with 5-request burst capacity + - URL validation: Only HTTPS URLs allowed, 2048-character limit + - Private IP blocking: Blocks requests to loopback, private, and link-local addresses + - Content transformation: Injects `` tag for relative URL resolution + - Strips X-Frame-Options and CSP headers to allow iframe embedding +- Implementation: `internal/proxy/proxy.go` +- Endpoint: `GET /proxy?url=[url]` + +## Data Storage + +**File Storage:** +- Type: Local filesystem (JSON files) +- Location: Configurable via `DATA_DIR` environment variable (default: `/data`) +- Persistence mechanism: + - Atomic writes using temp-file-then-rename pattern + - No database server required + - Files stored in flat structure: + - `streams.json` - User-submitted and scraped stream links + - `users.json` - User accounts with WebAuthn credentials + - `sessions.json` - Active user sessions + - `scraped.json` - Reddit-scraped links +- Client: Go `encoding/json` standard library with sync.RWMutex for thread-safe access + +**Caching:** +- Type: None - file-based storage only +- Session cleanup: Automatic garbage collection every 1 hour + +## Authentication & Identity + +**Auth Provider:** +- Type: Custom WebAuthn/FIDO2 implementation +- Library: github.com/go-webauthn/webauthn v0.15.0 +- Implementation details: + - Passwordless authentication using WebAuthn standard + - Registration ceremony: `POST /api/auth/register/begin` → `POST /api/auth/register/finish` + - Login ceremony: `POST /api/auth/login/begin` → `POST /api/auth/login/finish` + - Session tokens stored in HTTP-only, SameSite-strict cookies + - In-memory ceremony data storage with 5-minute expiration + - Manual admin assignment via `ADMIN_USERNAME` env var + - First user automatically becomes admin if no `ADMIN_USERNAME` set +- Files: `internal/auth/auth.go`, `internal/auth/context.go` + +## Monitoring & Observability + +**Error Tracking:** +- Type: None - no external error tracking service +- Implementation: Standard Go logging with `log` package + +**Logs:** +- Format: Standard Go log output (stdout) +- Level: Info and error messages +- No centralized logging, no external integration + +## CI/CD & Deployment + +**Hosting:** +- Platform: Kubernetes (Terraform module at `infra/modules/kubernetes/f1-stream/`) +- Deployment method: Container image + +**CI Pipeline:** +- Type: Not detected in this codebase +- Build method: Dockerfile multi-stage build + - Builder: golang:1.23-alpine with `go mod download` + - Runtime: alpine:3.20 with minimal dependencies + +## Environment Configuration + +**Required env vars (with defaults):** +- `LISTEN_ADDR` - Server listen address (default: `:8080`) +- `DATA_DIR` - Data storage directory (default: `/data`) +- `SCRAPE_INTERVAL` - Reddit scraper frequency (default: 15m) +- `SESSION_TTL` - Session expiration (default: 720h) +- `PROXY_TIMEOUT` - Proxy request timeout (default: 10s) +- `WEBAUTHN_RPID` - Relying party ID (default: `localhost`) +- `WEBAUTHN_ORIGIN` - Origin URL list, comma-separated (default: `http://localhost:8080`) +- `WEBAUTHN_DISPLAY_NAME` - UI display name (default: `F1 Stream`) +- `ADMIN_USERNAME` - Optional: pre-set admin username (no default) + +**Secrets location:** +- No secrets required - uses WebAuthn credentials stored locally +- CORS origin validation via `WEBAUTHN_ORIGIN` env var + +## Webhooks & Callbacks + +**Incoming:** +- None detected + +**Outgoing:** +- None detected + +## Stream Link Sources + +**Default Stream URLs (hardcoded in main.go):** +1. `https://wearechecking.live/streams-pages/motorsports` - WeAreChecking Motorsports +2. `https://vipleague.im/formula-1-schedule-streaming-links` - VIPLeague F1 +3. `https://www.vipbox.lc/` - VIPBox +4. `https://f1box.me/` - F1Box +5. `https://1stream.vip/formula-1-streams/` - 1Stream F1 + +--- + +*Integration audit: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/STACK.md b/stacks/f1-stream/module/files/.planning/codebase/STACK.md new file mode 100644 index 00000000..cc9a0371 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/STACK.md @@ -0,0 +1,109 @@ +# Technology Stack + +**Analysis Date:** 2026-02-17 + +## Languages + +**Primary:** +- Go 1.24.1 - Backend application and main server logic + +**Secondary:** +- HTML/CSS/JavaScript - Frontend UI + +## Runtime + +**Environment:** +- Go runtime (compiled binary) + +**Container Runtime:** +- Docker/Alpine Linux (3.20) - Production deployment target +- Multi-stage Dockerfile with golang:1.23-alpine builder + +**Package Manager:** +- Go modules (go.mod/go.sum) + +## Frameworks + +**Core:** +- Standard Go `net/http` - HTTP server and routing + - Native http.ServeMux for route handling (Go 1.22+ pattern routing) + - Native http.FileServer for static file serving + - Native http.Handler interface for middleware + +**Authentication:** +- github.com/go-webauthn/webauthn v0.15.0 - WebAuthn/FIDO2 authentication + - Handles registration and login ceremonies + - Supports multiple credential types + +**Frontend:** +- HTML5 - Markup +- CSS - Styling (Pico CSS framework for minimal styling) +- Vanilla JavaScript - Client-side interactivity (no framework detected) + +## Key Dependencies + +**Critical:** +- github.com/go-webauthn/webauthn v0.15.0 - Passwordless authentication via WebAuthn + - Includes transitive dependencies: + - github.com/go-webauthn/x v0.1.26 - WebAuthn extension support + - github.com/golang-jwt/jwt/v5 v5.3.0 - JWT token handling + - github.com/google/go-tpm v0.9.6 - TPM support for credentials + - github.com/fxamacker/cbor/v2 v2.9.0 - CBOR encoding/decoding + - github.com/go-viper/mapstructure/v2 v2.4.0 - Configuration mapping + - github.com/google/uuid v1.6.0 - UUID generation + - golang.org/x/crypto v0.43.0 - Cryptographic primitives + - golang.org/x/sys v0.37.0 - System-level primitives + +**Infrastructure:** +- None detected (no external databases, queues, or third-party services in go.mod) +- File-based storage only + +## Configuration + +**Environment Variables:** +- `LISTEN_ADDR` - Server listen address (default: `:8080`) +- `DATA_DIR` - Data storage directory (default: `/data`) +- `SCRAPE_INTERVAL` - Reddit scraper interval (default: 15 minutes) +- `ADMIN_USERNAME` - Admin account username (optional) +- `SESSION_TTL` - Session expiration time (default: 720 hours) +- `PROXY_TIMEOUT` - HTTP proxy request timeout (default: 10 seconds) +- `WEBAUTHN_RPID` - WebAuthn relying party ID (default: `localhost`) +- `WEBAUTHN_ORIGIN` - WebAuthn origin URL (default: `http://localhost:8080`) +- `WEBAUTHN_DISPLAY_NAME` - WebAuthn display name (default: `F1 Stream`) + +**Build:** +- `Dockerfile` - Multi-stage Docker build + - Builder stage: golang:1.23-alpine with CGO_ENABLED=0 + - Runtime stage: alpine:3.20 with ca-certificates + - Exposes port 8080 + +## Platform Requirements + +**Development:** +- Go 1.24.1 or compatible +- Unix-like shell (bash/zsh) for build scripts +- Optional: Docker for containerized development + +**Production:** +- Kubernetes cluster (Terraform module structure suggests K8s deployment) +- Persistent volume for `/data` directory +- Port 8080 exposed for HTTP traffic +- ca-certificates for HTTPS proxying + +## Storage + +**Data Persistence:** +- File-based JSON storage in `DATA_DIR` +- Files: `streams.json`, `users.json`, `sessions.json`, `scraped.json` +- Atomic writes using temp-file-then-rename pattern (`writeJSON` function in `internal/store/store.go`) + +## External Data Sources + +**Reddit API:** +- URL: `https://www.reddit.com/r/motorsportsstreams2/new.json?limit=25` +- No authentication required (public subreddit) +- Used for scraping F1 stream links + +--- + +*Stack analysis: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/STRUCTURE.md b/stacks/f1-stream/module/files/.planning/codebase/STRUCTURE.md new file mode 100644 index 00000000..db27252e --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/STRUCTURE.md @@ -0,0 +1,202 @@ +# Codebase Structure + +**Analysis Date:** 2026-02-17 + +## Directory Layout + +``` +f1-stream/ +├── main.go # Entry point, service initialization, signal handling +├── go.mod # Go module definition +├── go.sum # Dependency lock file +├── Dockerfile # Container image definition +├── redeploy.sh # Kubernetes redeployment script +├── index.html # HTML template served at root +├── internal/ # Private Go packages +│ ├── auth/ # WebAuthn authentication and session management +│ ├── models/ # Domain data types +│ ├── server/ # HTTP handlers, routes, middleware +│ ├── store/ # File-based persistence layer +│ ├── scraper/ # Reddit content scraper +│ └── proxy/ # HTTP proxy with rate limiting +├── static/ # Frontend assets served to clients +│ ├── index.html # Main SPA template +│ ├── css/ # Stylesheets +│ └── js/ # Client-side JavaScript modules +└── .planning/ # Planning/documentation directory + └── codebase/ # Architecture analysis documents +``` + +## Directory Purposes + +**Root Level:** +- Purpose: Service configuration and entry point +- Contains: Go module, main executable, Docker configuration, shell scripts +- Key files: `main.go` (service bootstrap), `go.mod` (dependencies) + +**`internal/`:** +- Purpose: Private packages (not importable by external code) +- Contains: All business logic, separated by concern +- Key pattern: Each subdirectory is a distinct Go package with clear responsibility + +**`internal/auth/`:** +- Purpose: User authentication, session management, context helpers +- Contains: WebAuthn ceremony handlers, session token management, user-in-context utilities +- Key files: + - `auth.go`: Registration/login handlers, ceremony session storage, credential validation + - `context.go`: Request context helpers for passing user data between middleware and handlers + +**`internal/models/`:** +- Purpose: Domain model definitions +- Contains: User, Stream, ScrapedLink, Session type definitions +- Key files: `models.go` (all types, includes WebAuthn interface implementations) + +**`internal/server/`:** +- Purpose: HTTP API and routing layer +- Contains: Handler functions, route registration, middleware implementations +- Key files: + - `server.go`: Server struct, route registration, API handlers (streams, admin, public endpoints) + - `middleware.go`: LoggingMiddleware, RecoveryMiddleware, AuthMiddleware, RequireAuth, RequireAdmin, OriginCheck + +**`internal/store/`:** +- Purpose: Persistent storage abstraction over file system +- Contains: JSON file operations, per-entity storage methods, atomic write patterns +- Key files: + - `store.go`: Store struct, directory initialization, JSON helper functions (readJSON, writeJSON) + - `streams.go`: Stream CRUD operations, publish toggle, seeding + - `users.go`: User lookup, credential updates, admin count + - `sessions.go`: Session creation, validation, expiry cleanup + - `scraped.go`: Scraped link persistence, active link filtering + +**`internal/scraper/`:** +- Purpose: Background content aggregation +- Contains: Interval-based scraper, Reddit-specific scraper logic +- Key files: + - `scraper.go`: Scraper service, interval-based run loop, manual trigger mechanism, deduplication logic + - `reddit.go`: Reddit API polling, F1 keyword filtering, URL extraction (not included in sample reads but referenced) + +**`internal/proxy/`:** +- Purpose: HTTP content fetching with security controls and rate limiting +- Contains: Rate limiter, private IP validation, response modification +- Key files: `proxy.go` (implements http.Handler, rate limiting, content fetching, base tag injection) + +**`static/`:** +- Purpose: Frontend assets served to browser +- Contains: HTML template and client-side code +- Key files: + - `index.html`: SPA HTML template (includes script tags loading js/) + - `js/app.js`: Toast notifications, dialog system, tab switching, initialization + - `js/auth.js`: Registration/login UI, WebAuthn client ceremony + - `js/streams.js`: Stream display, filtering, admin operations + - `js/utils.js`: Shared utilities (HTML escaping) + - `css/`: Stylesheets for app UI + +## Key File Locations + +**Entry Points:** +- `main.go`: Service initialization, dependency injection, signal handling, goroutine startup + +**Configuration:** +- Environment variables read in `main.go` (LISTEN_ADDR, DATA_DIR, SCRAPE_INTERVAL, etc.) +- WebAuthn config passed to `auth.New()` +- `.env` files not tracked (see .gitignore) + +**Core Logic:** +- Request routing: `internal/server/server.go:registerRoutes()` +- Auth logic: `internal/auth/auth.go` +- Data storage: `internal/store/store.go` and per-entity files +- Scraping: `internal/scraper/scraper.go` +- Proxying: `internal/proxy/proxy.go` + +**Testing:** +- No test files present in codebase (see TESTING.md concerns section) + +## Naming Conventions + +**Files:** +- Go source files: lowercase with underscores (e.g., `auth.go`, `middleware.go`) +- JavaScript files: lowercase with hyphens or underscores (e.g., `app.js`, `auth.js`) +- JSON data files: lowercase (e.g., `streams.json`, `users.json`, `sessions.json`) + +**Directories:** +- Go packages: lowercase, single word preferred (e.g., `auth`, `store`, `models`) +- Frontend assets: plural nouns (e.g., `static`, `css`, `js`) + +**Functions:** +- Go: CamelCase (exported), camelCase (unexported) +- JavaScript: camelCase throughout (e.g., `loadPublicStreams()`, `showToast()`) + +**Types:** +- Go structs: CamelCase (e.g., `User`, `Stream`, `Store`, `Auth`) +- Methods: CamelCase (e.g., `BeginLogin()`, `AddStream()`) + +**Variables:** +- Go: camelCase (e.g., `listenAddr`, `dataDir`, `adminUsername`) +- JavaScript: camelCase (e.g., `container`, `userID`, `sessionToken`) + +## Where to Add New Code + +**New Feature (e.g., new stream filter):** +- Primary code: Add handler in `internal/server/server.go`, register route in `registerRoutes()` +- Store operations: Add method to appropriate file in `internal/store/` (likely `streams.go`) +- Frontend: Add UI in `static/` and API call in `static/js/streams.js` or new module +- Models: Extend types in `internal/models/models.go` if new fields needed + +**New Authentication Method:** +- Core implementation: New file in `internal/auth/` (e.g., `oauth.go`) +- Handlers: Add methods following WebAuthn pattern (BeginXxx, FinishXxx) +- Routes: Register in `registerRoutes()` +- Frontend: Add form/button in `static/js/auth.js` + +**New Background Service (e.g., content validator):** +- Implementation: New file in `internal/` or new package `internal/validator/` +- Integration: Initialize in `main()` alongside `scraper.New()` +- Lifecycle: Use context pattern from scraper's `Run(ctx)` method +- Storage: Use existing `Store` instance + +**Utilities/Helpers:** +- Shared by Go packages: Add to package where most useful, or create new `internal/util/` package +- Shared by frontend: Add to `static/js/utils.js` or create new module +- Shared helpers pattern: Functions not tied to single package, used across multiple + +## Special Directories + +**`internal/`:** +- Purpose: Enforce package privacy (cannot be imported by external code) +- Generated: No +- Committed: Yes + +**`static/`:** +- Purpose: Served directly to clients via `http.FileServer` +- Generated: No (hand-written frontend) +- Committed: Yes + +**`.planning/codebase/`:** +- Purpose: Architecture documentation for development guidance +- Generated: No (manually created by mapping process) +- Committed: Yes + +**Data Directory (runtime):** +- Purpose: Persistent JSON files (streams.json, users.json, sessions.json, scraped.json) +- Location: Specified by DATA_DIR env var (default `/data`) +- Generated: Yes (created on first run) +- Committed: No (varies per deployment environment) + +## Import Patterns + +**Go Package Imports:** +- Standard library first: `import ("context" "fmt" "log")` +- Internal packages second: `import ("f1-stream/internal/auth" "f1-stream/internal/store")` +- External third-party last: `import ("github.com/go-webauthn/webauthn/webauthn")` + +**Cross-Package Dependencies:** +- Server depends on: Auth, Store, Proxy, Scraper, Models +- Auth depends on: Store, Models +- Scraper depends on: Store, Models +- Proxy depends on: none (standalone service) +- Store depends on: Models +- Models depends on: external WebAuthn library only + +--- + +*Structure analysis: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/codebase/TESTING.md b/stacks/f1-stream/module/files/.planning/codebase/TESTING.md new file mode 100644 index 00000000..8e381269 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/codebase/TESTING.md @@ -0,0 +1,256 @@ +# Testing Patterns + +**Analysis Date:** 2026-02-17 + +## Test Framework + +**Status:** No testing infrastructure present + +**Runner:** Not detected + +**Assertion Library:** Not applicable + +**Run Commands:** Not applicable + +## Test File Organization + +**Current State:** Zero test files found + +After scanning the codebase: +- No `*_test.go` files in `internal/` packages +- No `*.test.js` or `*.spec.js` files in static assets +- No test configuration files (jest.config.js, vitest.config.ts, etc.) +- No test runners in go.mod dependencies + +## Test Coverage + +**Requirements:** Not enforced; no test infrastructure + +**Current Coverage:** 0% - no tests exist + +## Test Types Present in Codebase + +### Unit Test Candidates (Not Currently Tested) + +**`internal/models/models.go`:** +- User and Stream model struct definitions +- WebAuthn interface implementations (lines 18-21) + +**`internal/auth/auth.go`:** +- Username validation via regex `usernameRe` (line 19) +- Registration/login ceremony steps +- Session creation and token generation +- Admin user detection logic (lines 83-91) + +**`internal/store/*.go` (all files):** +- JSON read/write operations with file locking +- User lookup and stream operations +- Session creation, validation, and cleanup +- Scraped link filtering and deduplication + +**`internal/scraper/reddit.go`:** +- F1 post detection: `isF1Post()` function (line 272-285) +- URL normalization: `normalizeURL()` function (line 262-270) +- URL extraction: `extractURLs()` function (line 210-243) +- Comment walking: `walkComments()` function (line 245-260) +- Keyword matching logic (lines 29-45) +- Retry logic with backoff (lines 183-208) + +**`internal/proxy/proxy.go`:** +- Rate limiting with token bucket algorithm (lines 145-168) +- Private host detection: `isPrivateHost()` function (line 128-143) +- Client IP extraction: `clientAddr()` function (line 184-191) +- Bucket cleanup mechanism (lines 170-182) + +**`internal/server/middleware.go`:** +- Auth middleware context injection +- Authorization checks (RequireAuth, RequireAdmin) +- Origin validation for CSRF protection +- Panic recovery middleware + +### Integration Test Candidates (Not Currently Tested) + +**Authentication Flow:** +- Begin registration → finish registration → session creation +- Begin login → finish login → session creation +- Session validation and expiration +- WebAuthn ceremony with mock credentials + +**Stream Management:** +- Add stream → save to JSON → retrieve +- Delete stream with authorization checks +- Toggle publish status +- Filter streams by visibility/ownership + +**Scraping Pipeline:** +- Fetch Reddit listing +- Extract F1 posts +- Walk comments recursively +- Deduplicate URLs +- Merge with existing links + +### E2E Test Candidates (Not Currently Tested) + +**HTTP Endpoints:** +- Full registration flow (POST /api/auth/register/begin, /api/auth/register/finish) +- Full login flow (POST /api/auth/login/begin, /api/auth/login/finish) +- Stream CRUD operations +- Public stream viewing +- Scrape triggering and result retrieval + +## Critical Untested Paths + +**High Risk - Security:** +- Authentication middleware context injection (`internal/server/middleware.go` lines 33-43) +- Admin authorization checks (line 62) +- CSRF origin validation (line 78-88) +- Private address filtering in proxy (line 77-80 in `internal/proxy/proxy.go`) +- Rate limiting enforcement (line 62-65 in `internal/proxy/proxy.go`) + +**High Risk - Data Integrity:** +- Concurrent access to store files via mutex protection (no verification that race conditions are prevented) +- JSON read/write atomicity with temp files (lines 41-52 in `internal/store/store.go`) +- Session expiration cleanup (lines 83-98 in `internal/store/sessions.go`) +- Stream deduplication during scraping (lines 65-85 in `internal/scraper/scraper.go`) + +**Medium Risk - Business Logic:** +- F1 post detection with negative keywords (lines 272-285 in `internal/scraper/reddit.go`) +- URL normalization for deduplication (line 262-270) +- Retry logic with rate limit backoff (line 183-208) + +## What Needs Testing + +### Unit Test Suggestions + +```go +// Example: Test username validation +func TestUsernameValidation(t *testing.T) { + tests := []struct { + username string + valid bool + }{ + {"valid123", true}, + {"valid_name", true}, + {"ab", false}, // too short + {"invalid-char", false}, // invalid character + {"", false}, // empty + } + // usernameRe.MatchString(username) for each test case +} + +// Example: Test F1 post detection +func TestIsF1Post(t *testing.T) { + tests := []struct { + title string + expected bool + }{ + {"F1 GP Race - Monaco", true}, + {"Formula 1 Practice", true}, + {"Help with F1 key binding", false}, // negative keyword + {"Random post about cars", false}, + } + // isF1Post(title) for each test case +} + +// Example: Test URL normalization +func TestNormalizeURL(t *testing.T) { + // Check that different URL formats normalize to same string + // Check case-insensitivity and trailing slash handling +} + +// Example: Test rate limiting +func TestRateLimiting(t *testing.T) { + p := New(10 * time.Second) + ip := "192.168.1.1" + + // First burst allowed + for i := 0; i < 5; i++ { + if !p.allowRequest(ip) { + t.Fail() + } + } + + // Burst exhausted + if p.allowRequest(ip) { + t.Fail() + } + + // Wait and verify replenishment + time.Sleep(10 * time.Second) + if !p.allowRequest(ip) { + t.Fail() + } +} +``` + +### Integration Test Suggestions + +```go +// Example: Test store operations with concurrency +func TestConcurrentStreamOperations(t *testing.T) { + st, _ := store.New(t.TempDir()) + + // Concurrent adds from multiple goroutines + // Verify no data corruption + // Verify final count is correct +} + +// Example: Test scraper deduplication +func TestScraperDeduplication(t *testing.T) { + // Create scraper with test store + // Mock Reddit response with duplicate URLs + // Verify only unique URLs are stored + // Verify normalization works (http vs https, trailing slashes) +} + +// Example: Test auth middleware +func TestAuthMiddleware(t *testing.T) { + st, _ := store.New(t.TempDir()) + auth, _ := auth.New(st, ...) + + // Create test token + // Make request with session cookie + // Verify user injected into context +} +``` + +## Recommended Testing Strategy + +1. **Phase 1 - Unit Tests (Highest Priority):** + - Validation functions (username regex, F1 keywords) + - String utilities (URL normalization, truncate) + - Rate limiting algorithm + - Private host detection + +2. **Phase 2 - Integration Tests:** + - Store operations with concurrency (verify mutex protection) + - Scraper pipeline (Reddit fetch → parse → deduplicate → save) + - Auth ceremony flow with mock WebAuthn + - Stream CRUD with permission checks + +3. **Phase 3 - E2E Tests:** + - Full HTTP request flows + - Middleware chain validation + - Session management across endpoints + +## Testing Patterns to Establish + +**Once framework chosen (Go: testing or testify):** + +- Use `t.TempDir()` for store tests to avoid file conflicts +- Mock HTTP responses for scraper tests +- Use `net/http/httptest` for handler testing +- Mock WebAuthn responses for auth tests +- Table-driven tests for validation logic +- Parallel test execution with `-race` flag for concurrency detection + +**Coverage gaps to close:** +- All error paths in store operations +- Session expiration edge cases +- Concurrent access scenarios +- HTTP header validation +- CORS/origin validation + +--- + +*Testing analysis: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/config.json b/stacks/f1-stream/module/files/.planning/config.json new file mode 100644 index 00000000..758c1fd8 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/config.json @@ -0,0 +1,12 @@ +{ + "mode": "yolo", + "depth": "standard", + "parallelization": true, + "commit_docs": true, + "model_profile": "quality", + "workflow": { + "research": true, + "plan_check": true, + "verifier": true + } +} diff --git a/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-PLAN.md b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-PLAN.md new file mode 100644 index 00000000..ec92eb20 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-PLAN.md @@ -0,0 +1,237 @@ +--- +phase: 01-scraper-validation +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - internal/scraper/validate.go + - internal/scraper/validate_test.go + - internal/scraper/scraper.go + - main.go +autonomous: true +requirements: + - SCRP-01 + - SCRP-02 + - SCRP-03 + - SCRP-04 + +must_haves: + truths: + - "Scraper still discovers F1-related posts using keyword filtering (existing isF1Post behavior unchanged)" + - "Each newly scraped URL is fetched and inspected for video/player content markers before being saved to scraped_links.json" + - "URLs without video content markers are discarded and do not appear in scraped_links.json" + - "Validation uses a configurable timeout (SCRAPER_VALIDATE_TIMEOUT env var, default 10s) that prevents slow sites from blocking the scrape cycle" + artifacts: + - path: "internal/scraper/validate.go" + provides: "URL validation logic with video marker detection" + contains: "func validateLinks" + - path: "internal/scraper/validate_test.go" + provides: "Unit tests for marker detection and content type checks" + contains: "func TestContainsVideoMarkers" + - path: "internal/scraper/scraper.go" + provides: "Updated Scraper struct with validateTimeout field and validation call in scrape()" + contains: "validateTimeout" + - path: "main.go" + provides: "SCRAPER_VALIDATE_TIMEOUT env var configuration" + contains: "SCRAPER_VALIDATE_TIMEOUT" + key_links: + - from: "internal/scraper/scraper.go" + to: "internal/scraper/validate.go" + via: "validateLinks call in scrape() between URL extraction and merge" + pattern: "validateLinks\\(links" + - from: "main.go" + to: "internal/scraper/scraper.go" + via: "scraper.New() call with validateTimeout parameter" + pattern: "scraper\\.New\\(st.*validateTimeout" +--- + + +Add URL validation to the scraper pipeline so that each extracted URL is proxy-fetched and inspected for video/player content markers before being saved. URLs without video markers are discarded at the source. + +Purpose: Eliminate junk links (blog posts, news articles, social media) from scraped results so users only see actual stream pages. +Output: Working validation step integrated into scraper pipeline, with unit tests and configurable timeout. + + + +@/Users/viktorbarzin/.claude/get-shit-done/workflows/execute-plan.md +@/Users/viktorbarzin/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-scraper-validation/01-RESEARCH.md + +@internal/scraper/scraper.go +@internal/scraper/reddit.go +@internal/models/models.go +@main.go + + + + + + Task 1: Create validate.go with video marker detection + internal/scraper/validate.go + +Create `internal/scraper/validate.go` in package `scraper` with the following: + +1. Define `videoMarkers` string slice (case-insensitive markers checked against lowercased HTML body): + - HTML5: `= 3`). + - Iterate over links. For each, call `hasVideoContent(client, link.URL)`. If true, keep the link. If false, log: `scraper: discarded %s (no video markers)` using `truncate(link.URL, 60)`. + - Return the filtered slice. + +5. Implement `hasVideoContent(client *http.Client, rawURL string) bool`: + - Create GET request with `User-Agent` set to the existing `userAgent` constant from `reddit.go`. + - Execute request. On error, log and return false. + - Check status code: if < 200 or >= 400, return false. + - Check Content-Type header (lowercased) against `videoContentTypes` -- if match, return true (it's a direct video file). + - If Content-Type is not `text/html` or `application/xhtml`, return false (not a video file, not an HTML page to inspect). + - Read body with `io.LimitReader` (2MB limit). + - Return `containsVideoMarkers(strings.ToLower(string(body)))`. + +6. Implement `containsVideoMarkers(loweredBody string) bool`: + - Iterate over `videoMarkers`, return true on first `strings.Contains` match. + +7. Implement `isDirectVideoContentType(ct string) bool`: + - Lowercase ct, iterate `videoContentTypes`, return true on first `strings.Contains` match. + +Imports needed: `io`, `log`, `net/http`, `strings`, `time`, and `f1-stream/internal/models`. + +Do NOT use `golang.org/x/net/html` (reserved for Phase 4). This is detection, not extraction -- string matching is sufficient. + + +Run `cd /Users/viktorbarzin/code/infra/modules/kubernetes/f1-stream/files && go build ./...` -- must compile without errors. + + +`validate.go` exists with `validateLinks`, `hasVideoContent`, `containsVideoMarkers`, `isDirectVideoContentType` functions. The file compiles as part of the `scraper` package. + + + + + Task 2: Wire validation into scraper pipeline and add config + internal/scraper/scraper.go, main.go + +**In `internal/scraper/scraper.go`:** + +1. Add `validateTimeout time.Duration` field to the `Scraper` struct. + +2. Update `New()` signature to accept `validateTimeout` parameter: + ```go + func New(s *store.Store, interval time.Duration, validateTimeout time.Duration) *Scraper { + return &Scraper{store: s, interval: interval, validateTimeout: validateTimeout} + } + ``` + +3. In `scrape()` method, add validation step between the `scrapeReddit()` return and the merge-with-existing logic. Insert AFTER the line `log.Printf("scraper: reddit scrape completed in %v, got %d links", ...)` and BEFORE the line `existing, err := s.store.LoadScrapedLinks()`: + + ```go + // Validate links - only keep those with video content markers + if len(links) > 0 { + validated := validateLinks(links, s.validateTimeout) + log.Printf("scraper: validated %d/%d links as streams", len(validated), len(links)) + links = validated + } + ``` + + This preserves SCRP-01: existing keyword filtering in `scrapeReddit()` via `isF1Post()` runs first, then validation filters the results. + +**In `main.go`:** + +1. Add `validateTimeout` env var read after the existing `scrapeInterval` line: + ```go + validateTimeout := envDuration("SCRAPER_VALIDATE_TIMEOUT", 10*time.Second) + ``` + +2. Update the `scraper.New()` call to pass the new parameter: + ```go + sc := scraper.New(st, scrapeInterval, validateTimeout) + ``` + +Both changes are minimal and follow the existing configuration pattern used for `SCRAPE_INTERVAL`, `PROXY_TIMEOUT`, etc. + + +Run `cd /Users/viktorbarzin/code/infra/modules/kubernetes/f1-stream/files && go build ./...` -- must compile without errors. Then run `go vet ./...` -- no issues. + + +`Scraper` struct has `validateTimeout` field. `New()` accepts 3 parameters. `scrape()` calls `validateLinks` between extraction and merge. `main.go` reads `SCRAPER_VALIDATE_TIMEOUT` env var (default 10s) and passes it to `scraper.New()`. + + + + + Task 3: Add unit tests for validation functions + internal/scraper/validate_test.go + +Create `internal/scraper/validate_test.go` in package `scraper` with the following test functions: + +**`TestContainsVideoMarkers`** - table-driven test covering: +- Positive cases (should return true): + - ` + +Run `cd /Users/viktorbarzin/code/infra/modules/kubernetes/f1-stream/files && go test ./internal/scraper/ -v -run "TestContainsVideoMarkers|TestIsDirectVideoContentType"` -- all tests pass. + + +`validate_test.go` exists with `TestContainsVideoMarkers` (8+ positive, 4+ negative cases) and `TestIsDirectVideoContentType` (6+ positive, 5+ negative cases). All tests pass. + + + + + + +1. `go build ./...` compiles without errors +2. `go vet ./...` reports no issues +3. `go test ./internal/scraper/ -v` -- all tests pass +4. Verify `validate.go` contains the `videoMarkers` slice with at least 15 markers +5. Verify `scraper.go:scrape()` calls `validateLinks` between `scrapeReddit()` return and `LoadScrapedLinks()` +6. Verify `main.go` reads `SCRAPER_VALIDATE_TIMEOUT` with default `10*time.Second` +7. Verify the existing `isF1Post` keyword filtering in `scrapeReddit()` is untouched (SCRP-01) + + + +- The scraper pipeline compiles and all tests pass +- `validateLinks` is called in `scrape()` after URL extraction but before merge, filtering out URLs without video markers +- The validation timeout is configurable via `SCRAPER_VALIDATE_TIMEOUT` env var (default 10s) +- Existing F1 keyword filtering behavior is preserved unchanged +- No new external dependencies are introduced (stdlib only) + + + +After completion, create `.planning/phases/01-scraper-validation/01-01-SUMMARY.md` + diff --git a/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-SUMMARY.md b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-SUMMARY.md new file mode 100644 index 00000000..bd4b7d41 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-01-SUMMARY.md @@ -0,0 +1,107 @@ +--- +phase: 01-scraper-validation +plan: 01 +subsystem: scraper +tags: [go, http, video-detection, content-validation, streaming] + +# Dependency graph +requires: [] +provides: + - "URL validation pipeline with video marker detection (validateLinks)" + - "Configurable validation timeout via SCRAPER_VALIDATE_TIMEOUT env var" + - "Video content type and HTML marker detection functions" +affects: [02-health-checks, 04-link-extraction] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Pipeline filter pattern: scrapeReddit -> validateLinks -> merge" + - "String-match video detection (no DOM parsing) for Phase 1 speed" + - "2MB body limit for HTML inspection to prevent memory issues" + +key-files: + created: + - internal/scraper/validate.go + - internal/scraper/validate_test.go + modified: + - internal/scraper/scraper.go + - main.go + +key-decisions: + - "String matching over DOM parsing for video detection (DOM reserved for Phase 4)" + - "2MB body limit to prevent memory issues on large pages" + - "3 redirect limit to avoid infinite redirect chains" + +patterns-established: + - "Pipeline filter: validate scraped links before merge into store" + - "Env var config pattern: envDuration for timeout configuration" + +requirements-completed: [SCRP-01, SCRP-02, SCRP-03, SCRP-04] + +# Metrics +duration: 3min +completed: 2026-02-17 +--- + +# Phase 1 Plan 1: Scraper Validation Summary + +**URL validation pipeline with 18 video/player markers filtering scraped links before store merge, configurable via SCRAPER_VALIDATE_TIMEOUT** + +## Performance + +- **Duration:** 3 min +- **Started:** 2026-02-17T20:49:16Z +- **Completed:** 2026-02-17T20:51:54Z +- **Tasks:** 3 +- **Files modified:** 4 + +## Accomplishments +- Created validate.go with 18 video/player markers covering HTML5, HLS, DASH, and 10+ player libraries +- Wired validateLinks into scrape() pipeline between URL extraction and store merge +- Added SCRAPER_VALIDATE_TIMEOUT env var (default 10s) following existing config patterns +- Added 25 unit tests (10 positive + 4 negative marker tests, 6 positive + 5 negative content type tests) + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Create validate.go with video marker detection** - `adeb478` (feat) +2. **Task 2: Wire validation into scraper pipeline and add config** - `22d29db` (feat) +3. **Task 3: Add unit tests for validation functions** - `6c5cc02` (test) + +## Files Created/Modified +- `internal/scraper/validate.go` - URL validation with video marker detection (validateLinks, hasVideoContent, containsVideoMarkers, isDirectVideoContentType) +- `internal/scraper/validate_test.go` - Table-driven unit tests for marker detection and content type checks (25 cases) +- `internal/scraper/scraper.go` - Added validateTimeout field and validateLinks call in scrape() +- `main.go` - Added SCRAPER_VALIDATE_TIMEOUT env var read (default 10s) + +## Decisions Made +- Used string matching (not DOM parsing) for video detection -- DOM parsing reserved for Phase 4 link extraction +- Set 2MB body read limit to prevent memory issues on large streaming pages +- Limited redirects to 3 to avoid infinite redirect chains on sketchy stream sites +- Validation runs sequentially (not concurrent) to avoid overwhelming target sites + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +None. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- Validation pipeline is integrated and tested, ready for health check layer (Phase 2) +- The validateLinks function provides the filtering foundation that health checks will build upon +- No blockers or concerns + +## Self-Check: PASSED + +All 5 files verified present. All 3 task commits verified in git log. + +--- +*Phase: 01-scraper-validation* +*Completed: 2026-02-17* diff --git a/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-RESEARCH.md b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-RESEARCH.md new file mode 100644 index 00000000..dd8f043f --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/01-scraper-validation/01-RESEARCH.md @@ -0,0 +1,599 @@ +# Phase 1: Scraper Validation - Research + +**Researched:** 2026-02-17 +**Domain:** HTTP content fetching and HTML video/player content detection in Go +**Confidence:** HIGH + +## Summary + +Phase 1 adds a validation step to the existing Reddit scraper pipeline. Currently, the scraper extracts ALL URLs from F1-related Reddit posts and saves them to `scraped_links.json` without verifying whether they point to actual stream pages. The validation step will proxy-fetch each extracted URL (reusing the existing proxy's HTTP client pattern) and inspect the HTML response for video/player content markers before saving. + +The implementation is straightforward because the codebase already has all the infrastructure needed: HTTP fetching with timeouts (used in both `internal/scraper/reddit.go` and `internal/proxy/proxy.go`), URL validation, and the scraper pipeline with deduplication. The new code is a validation function inserted between URL extraction and saving, operating on the same `[]models.ScrapedLink` type. + +**Primary recommendation:** Add a `validateStreamURL` function in a new file `internal/scraper/validate.go` that uses string-based content matching (not full HTML parsing) to detect video markers, with `golang.org/x/net/html` reserved for Phase 4 (video extraction). Keep it simple: fetch the page, lowercase the body, check for known patterns. This avoids adding a dependency for Phase 1 while Phase 2 will reuse the same validation logic for health checks. + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| SCRP-01 | Scraper filters Reddit posts by F1 keywords before extracting URLs (existing behavior, preserve) | Existing `isF1Post()` function in `reddit.go` lines 272-285 handles this. No changes needed -- just ensure the validation step is added AFTER URL extraction, not replacing the keyword filter. | +| SCRP-02 | Scraper validates each extracted URL by proxy-fetching it and checking for video/player content markers | New `validateStreamURL()` function fetches URL with configurable timeout, reads response body, checks for video content markers (see "Video Content Markers" section below for complete list). Reuse existing HTTP client pattern from `reddit.go:88`. | +| SCRP-03 | URLs that don't look like streams (no video markers detected) are discarded before saving | Filter applied in `scraper.go:scrape()` between URL extraction (line 57) and merge/save (line 60). Only URLs passing validation are included in the `links` slice passed to the merge step. | +| SCRP-04 | Validation has a configurable timeout (default 10s) to avoid blocking on slow sites | Add `SCRAPER_VALIDATE_TIMEOUT` environment variable read in `main.go`, passed to `scraper.New()`. Use `context.WithTimeout` on per-URL fetch to enforce deadline. Default 10 seconds. | + + +## Standard Stack + +### Core + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| `net/http` | stdlib | HTTP client for fetching URLs | Already used throughout codebase (`reddit.go`, `proxy.go`). No external dependency needed. | +| `strings` | stdlib | Case-insensitive string matching for content markers | Already used extensively. `strings.Contains` on lowercased body is the simplest approach for marker detection. | +| `regexp` | stdlib | Pattern matching for HLS/DASH URLs in page source | Already used in `reddit.go` for URL extraction. Needed for matching `.m3u8` and `.mpd` URL patterns in HTML content. | +| `context` | stdlib | Timeout enforcement per URL validation | Already used in scraper (`scraper.go:Run`). `context.WithTimeout` provides per-request deadline. | +| `io` | stdlib | `io.LimitReader` for response body size limiting | Already used in `proxy.go` and `reddit.go` for body size limits. | + +### Supporting + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| `golang.org/x/net/html` | latest | Full HTML DOM parsing | NOT needed for Phase 1. Reserve for Phase 4 (video source extraction). String matching is sufficient for detection. | +| `sync` | stdlib | WaitGroup for parallel validation | If parallel validation is desired. But sequential is simpler and respects rate limits of target sites. | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| String matching on body | `golang.org/x/net/html` DOM parsing | DOM parsing is more accurate but adds a dependency and complexity. For Phase 1 (detection, not extraction), string matching is sufficient. Phase 4 needs DOM parsing for actual source extraction. | +| Sequential URL validation | `sync.WaitGroup` parallel validation | Parallel is faster but risks triggering rate limits on target sites and complicates error handling. Sequential with timeout is simpler and predictable. | +| Custom HTTP client | Reuse proxy's `*http.Client` | The proxy client has redirect limits and timeout already configured. But the scraper should have its own client with validation-specific timeout. Keep them independent. | + +**Installation:** +```bash +# No new dependencies needed for Phase 1. All stdlib. +# golang.org/x/net/html deferred to Phase 4. +``` + +## Architecture Patterns + +### Where Validation Fits in the Pipeline + +``` +Current flow: + scrapeReddit() -> []models.ScrapedLink -> merge with existing -> save + +New flow: + scrapeReddit() -> []models.ScrapedLink -> validateLinks() -> []models.ScrapedLink -> merge with existing -> save +``` + +The validation step is a filter function that takes a slice of scraped links and returns only those that pass validation. This keeps the existing pipeline intact and makes the validation step independently testable. + +### Recommended File Structure + +``` +internal/scraper/ + scraper.go # Orchestrator (existing, add validateTimeout field + call validateLinks) + reddit.go # Reddit API scraping (existing, no changes) + validate.go # NEW: validateStreamURL(), validateLinks(), content marker definitions +``` + +### Pattern 1: Validation as a Filter Function + +**What:** A pure filter function that takes `[]models.ScrapedLink` and returns the subset that pass validation. +**When to use:** When adding a validation/filter step to an existing pipeline. +**Example:** + +```go +// internal/scraper/validate.go + +// validateLinks filters links to only those with video content markers. +// Each URL is fetched with the given timeout and inspected for markers. +func validateLinks(links []models.ScrapedLink, timeout time.Duration) []models.ScrapedLink { + client := &http.Client{Timeout: timeout} + var valid []models.ScrapedLink + for _, link := range links { + if hasVideoContent(client, link.URL) { + valid = append(valid, link) + } else { + log.Printf("scraper: discarded %s (no video markers)", truncate(link.URL, 60)) + } + } + return valid +} + +// hasVideoContent fetches a URL and checks for video/player content markers. +func hasVideoContent(client *http.Client, rawURL string) bool { + req, err := http.NewRequest("GET", rawURL, nil) + if err != nil { + return false + } + req.Header.Set("User-Agent", userAgent) // reuse existing constant + + resp, err := client.Do(req) + if err != nil { + log.Printf("scraper: validate fetch error for %s: %v", truncate(rawURL, 60), err) + return false + } + defer resp.Body.Close() + + // Only inspect HTML responses + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/html") && !strings.Contains(ct, "application/xhtml") { + // Could be a direct video file (.m3u8, .mpd, .mp4) which is valid + if isDirectVideoContentType(ct) { + return true + } + return false + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) // 2MB limit for validation + if err != nil { + return false + } + + return containsVideoMarkers(strings.ToLower(string(body))) +} +``` + +### Pattern 2: Configuration Via Struct Field + +**What:** Pass validation timeout through the existing `Scraper` struct, configured from `main.go` env vars. +**When to use:** Following existing codebase pattern where all config flows through `main.go` -> constructor. +**Example:** + +```go +// internal/scraper/scraper.go +type Scraper struct { + store *store.Store + interval time.Duration + validateTimeout time.Duration // NEW + mu sync.Mutex +} + +func New(s *store.Store, interval time.Duration, validateTimeout time.Duration) *Scraper { + return &Scraper{store: s, interval: interval, validateTimeout: validateTimeout} +} + +// In main.go: +validateTimeout := envDuration("SCRAPER_VALIDATE_TIMEOUT", 10*time.Second) +sc := scraper.New(st, scrapeInterval, validateTimeout) +``` + +### Pattern 3: Integration Point in scrape() + +**What:** Call validateLinks between scrapeReddit return and merge step. +**When to use:** Minimal change to existing scrape flow. +**Example:** + +```go +// In scraper.go:scrape() - between lines 57 and 60 +links, err := scrapeReddit() +if err != nil { + // ... existing error handling +} +log.Printf("scraper: reddit scrape completed in %v, got %d links", time.Since(start).Round(time.Millisecond), len(links)) + +// NEW: validate links before merging +if len(links) > 0 { + validated := validateLinks(links, s.validateTimeout) + log.Printf("scraper: validated %d/%d links as streams", len(validated), len(links)) + links = validated +} + +// Continue with existing merge logic... +``` + +### Anti-Patterns to Avoid + +- **Fetching URLs inside the Reddit API loop:** Validation should happen after all URLs are collected from Reddit, not interleaved with Reddit API calls. This keeps the Reddit API calls fast and avoids mixing rate-limit concerns. +- **Using the proxy's HTTP handler for internal validation:** The proxy (`internal/proxy/proxy.go`) is designed as an HTTP handler for client-facing requests with IP-based rate limiting. The scraper should use its own HTTP client without rate limiting since it is a trusted internal caller. +- **Modifying the ScrapedLink model to track validation state:** For Phase 1, validation is a binary filter (pass or discard). Adding validation metadata to the model is premature and adds complexity to the store layer. If needed in Phase 2 for health checking, it can be added then. +- **Full HTML DOM parsing for detection:** Using `golang.org/x/net/html` to parse the full DOM tree just to detect presence of video tags is overkill. String matching on lowercased HTML body is sufficient for detection. DOM parsing is needed in Phase 4 for actual source URL extraction. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| HTTP fetching with timeout | Custom TCP client | `net/http.Client` with `Timeout` field | stdlib handles redirects, TLS, timeouts, connection pooling | +| HTML content inspection | Full DOM parser | `strings.Contains` on lowercased body | Detection (yes/no) does not need structural parsing; string matching is faster and simpler | +| URL scheme validation | Manual string prefix check | `net/url.Parse` + scheme check | Already used in codebase; handles edge cases | +| Concurrent timeout enforcement | Manual goroutine + channel | `context.WithTimeout` + `http.NewRequestWithContext` | stdlib integration; cancels in-flight requests properly | + +**Key insight:** Phase 1 is a detection problem (does this page look like a stream?), not an extraction problem (what is the stream URL?). Detection can be done with string matching. Extraction (Phase 4) needs DOM parsing. + +## Video Content Markers + +### HIGH confidence markers (any one of these strongly indicates a stream page) + +**HTML Tags:** +- `` tags or player references -- those are injected by JS after page load. +**Why it happens:** HTTP fetching returns raw HTML; no JavaScript execution. +**How to avoid:** This is an accepted limitation per the requirements doc ("Full browser automation (Puppeteer/Playwright)" is Out of Scope). The marker list includes JavaScript library references (e.g., `hls.js`, `video.js`) which ARE present in the raw HTML even before execution. Most streaming sites include their player library in ``, true}, + {"video.js library", ``, true}, + {"jwplayer", `
`, true}, + {"no markers", `

Hello world

`, false}, + {"reddit link page", `Click here`, false}, + {"blog post", `
F1 race results...
`, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := containsVideoMarkers(tt.body) + if result != tt.expected { + t.Errorf("containsVideoMarkers(%q) = %v, want %v", truncate(tt.body, 40), result, tt.expected) + } + }) + } +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Save all URLs from F1 posts | Will validate each URL before saving | Phase 1 (now) | Eliminates junk links at the source | +| No content inspection | String-based marker detection | Phase 1 (now) | Simple, fast, no external dependencies | +| Static marker list | Static marker list (sufficient for now) | - | May need updating as new players emerge; easily extensible | + +**Why not use browser automation:** +The REQUIREMENTS.md explicitly marks "Full browser automation (Puppeteer/Playwright)" as Out of Scope. HTTP-based checks with string matching catch the majority of stream pages because player library ` +``` + +**2. Update `streamCard()` in streams.js:** + +The current `streamCard()` function renders an iframe immediately. Change the approach: + +a. The card initially renders with a loading state placeholder instead of an iframe: +```html +
+
+
+
+
+
+
+ ${escapeHtml(stream.title)} +
+ ${externalBtn} + ${deleteBtn} +
+
+
+``` + +b. After cards are rendered in the DOM (after `grid.innerHTML = ...`), call a new function `tryExtractVideos(streams)` that attempts extraction for each stream. + +**3. Create `async function tryExtractVideos(streams)`:** + +For each stream, call `tryExtractVideo(stream)` concurrently using `Promise.allSettled`. + +**4. Create `async function tryExtractVideo(stream)`:** + +- Fetch `GET /api/streams/${stream.id}/extract` +- If response is OK and `sources` array is non-empty: + - Pick the best source: prefer "hls" type, then "dash", then "mp4"/"webm" + - Call `renderNativePlayer(stream.id, source)` to replace the loading placeholder +- If response fails or sources is empty: + - Call `renderIframeFallback(stream.id, stream.url)` to show the existing iframe + +**5. Create `function renderNativePlayer(streamId, source)`:** + +Get the wrapper element `player-wrap-${streamId}`. + +For HLS sources (source.type === "hls"): +```javascript +const video = document.createElement('video'); +video.controls = true; +video.autoplay = false; +video.style.width = '100%'; +video.style.height = '100%'; +video.setAttribute('playsinline', ''); + +if (Hls.isSupported()) { + const hls = new Hls(); + hls.loadSource(source.url); + hls.attachMedia(video); +} else if (video.canPlayType('application/vnd.apple.mpegurl')) { + // Native HLS support (Safari) + video.src = source.url; +} +``` + +For MP4/WebM sources: +```javascript +const video = document.createElement('video'); +video.controls = true; +video.autoplay = false; +video.style.width = '100%'; +video.style.height = '100%'; +video.setAttribute('playsinline', ''); +video.src = source.url; +``` + +For DASH sources (skip for now, just fall back to iframe — DASH requires dash.js which is heavier): +- Call `renderIframeFallback(streamId, stream.url)` instead. + +Replace the loading overlay content with the video element. Remove the spinner. Add a "loaded" class to the loading overlay. + +**6. Create `function renderIframeFallback(streamId, streamURL)`:** + +Get the wrapper element and replace its innerHTML with the existing iframe markup: +```javascript +const wrap = document.getElementById(`player-wrap-${streamId}`); +if (!wrap) return; +wrap.innerHTML = ` +
+
+
+ +`; +``` + +**7. Update `loadPublicStreams()` and `loadMyStreams()`:** + +After `grid.innerHTML = streams.map(...)`, add `tryExtractVideos(streams)` call. The existing `sortStreamsByHealth` call should remain after it. + +**Important:** Do NOT block the initial render. The cards show immediately with loading spinners. Extraction runs asynchronously and replaces the spinner with either a native player or an iframe as results come in. + +**Error handling:** If the fetch to `/api/streams/{id}/extract` throws (network error, timeout), silently fall back to iframe. Log to console but do not show user-facing errors for extraction failures — the iframe fallback is the safety net. + + +1. Open `static/index.html` in a text editor and confirm the HLS.js script tag is present before other JS scripts. +2. Open `static/js/streams.js` and confirm: + - `streamCard()` no longer renders an iframe directly + - `tryExtractVideos()` and `tryExtractVideo()` functions exist + - `renderNativePlayer()` and `renderIframeFallback()` functions exist + - `loadPublicStreams()` calls `tryExtractVideos()` +3. Run `cd /Users/viktorbarzin/code/infra/modules/kubernetes/f1-stream/files && go build ./...` to confirm the Go project still compiles (no Go changes, but verify no accidental edits). + + +- HLS.js loaded via CDN in index.html +- Stream cards attempt extraction first, render native HTML5 video player for HLS/MP4/WebM sources +- Iframe fallback works when extraction fails or returns no sources +- No visual regression for streams without extractable video sources + + + + + + +1. `static/index.html` includes HLS.js CDN script tag +2. `static/js/streams.js` has extraction-first card rendering logic +3. `streamCard()` renders placeholder, not iframe, as initial state +4. `tryExtractVideo()` calls `/api/streams/{id}/extract` and handles success/failure +5. `renderNativePlayer()` creates HTML5 ` + + +- Streams with extractable video sources render in a native HTML5 video player with controls +- HLS streams play via hls.js (or native HLS on Safari) +- Streams without extractable sources fall back to iframe rendering (existing behavior preserved) +- No user-facing errors when extraction fails — silent fallback to iframe +- Video player has play, pause, volume, and fullscreen controls + + + +After completion, create `.planning/phases/04-video-extraction-native-playback/04-02-SUMMARY.md` + diff --git a/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-02-SUMMARY.md b/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-02-SUMMARY.md new file mode 100644 index 00000000..40927caf --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-02-SUMMARY.md @@ -0,0 +1,107 @@ +--- +phase: 04-video-extraction-native-playback +plan: 02 +subsystem: ui +tags: [hls-js, html5-video, native-playback, video-extraction, frontend] + +# Dependency graph +requires: + - phase: 04-video-extraction-native-playback + provides: Video source extractor API endpoint (GET /api/streams/{id}/extract) +provides: + - Extraction-first stream card rendering with native HTML5 video player + - HLS.js integration for .m3u8 playback in non-Safari browsers + - Iframe fallback for streams without extractable video sources +affects: [05-polish-monitoring, frontend, streaming-experience] + +# Tech tracking +tech-stack: + added: [hls.js@1 (CDN)] + patterns: [extraction-first rendering, progressive enhancement with fallback, priority-based source selection] + +key-files: + created: [] + modified: [static/index.html, static/js/streams.js] + +key-decisions: + - "HLS.js loaded from CDN (jsdelivr) to avoid bundling complexity" + - "Extraction runs async after card render -- loading spinner shows immediately, player replaces it" + - "DASH sources fall back to iframe (dash.js too heavy for current scope)" + - "pickBestSource priority: HLS > DASH > MP4 > WebM matches backend ordering" + - "Silent console.log on extraction failure -- no user-facing errors for extraction issues" + +patterns-established: + - "Progressive enhancement pattern: render placeholder, attempt extraction, upgrade to native or fall back to iframe" + - "Promise.allSettled for concurrent extraction across all stream cards" + +requirements-completed: [EMBED-02] + +# Metrics +duration: 2min +completed: 2026-02-17 +--- + +# Phase 04 Plan 02: Frontend Native Video Playback Summary + +**Extraction-first stream card rendering with HLS.js integration and iframe fallback for native HTML5 video playback** + +## Performance + +- **Duration:** 2 min +- **Started:** 2026-02-17T21:48:20Z +- **Completed:** 2026-02-17T21:50:03Z +- **Tasks:** 1 +- **Files modified:** 2 + +## Accomplishments +- Stream cards now attempt video extraction before falling back to iframe +- Native HTML5 video player renders for HLS, MP4, and WebM sources with standard controls +- HLS.js handles .m3u8 streams in non-Safari browsers; Safari uses native HLS support +- Iframe fallback preserves existing behavior for streams without extractable sources +- Loading spinners provide visual feedback during async extraction + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Add HLS.js and update streamCard for native video playback** - `2a40af9` (feat) + +**Plan metadata:** (pending final docs commit) + +## Files Created/Modified +- `static/index.html` - Added HLS.js CDN script tag before other JS scripts +- `static/js/streams.js` - Extraction-first rendering: streamCard renders placeholder, tryExtractVideos/tryExtractVideo call extract API, renderNativePlayer creates HTML5 video element, renderIframeFallback preserves existing iframe approach + +## Decisions Made +- HLS.js loaded from jsDelivr CDN rather than self-hosted -- avoids build tooling while keeping the library current +- DASH sources intentionally fall back to iframe -- dash.js is heavier and DASH is lower priority than HLS +- Extraction errors logged to console only -- user sees iframe fallback seamlessly, no error UI needed +- pickBestSource uses same priority ordering (HLS > DASH > MP4 > WebM) established in backend extractor + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +- Go module cache sandbox permission error during build verification; resolved with temporary GOPATH (same workaround as 04-01, environment constraint only) + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- Phase 04 (Video Extraction & Native Playback) is now complete +- Streams with extractable video sources play in native HTML5 player +- Streams without extractable sources continue to work via iframe fallback +- Ready for Phase 05 (Polish & Monitoring) if planned + +--- +*Phase: 04-video-extraction-native-playback* +*Completed: 2026-02-17* + +## Self-Check: PASSED + +- FOUND: static/index.html +- FOUND: static/js/streams.js +- FOUND: 04-02-SUMMARY.md +- FOUND: commit 2a40af9 diff --git a/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-VERIFICATION.md b/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-VERIFICATION.md new file mode 100644 index 00000000..27156391 --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/04-video-extraction-native-playback/04-VERIFICATION.md @@ -0,0 +1,108 @@ +--- +phase: 04-video-extraction-native-playback +verified: 2026-02-17T22:00:00Z +status: passed +score: 11/11 must-haves verified +re_verification: false +--- + +# Phase 4: Video Extraction and Native Playback Verification Report + +**Phase Goal:** When a stream URL contains an extractable video source, users watch it in a clean native HTML5 player instead of loading the third-party page + +**Verified:** 2026-02-17T22:00:00Z + +**Status:** passed + +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | GET /api/streams/{id}/extract returns a JSON response with extracted video source URL and type when the stream page contains a video source | ✓ VERIFIED | Endpoint exists in server.go:83, handler at server.go:242-295 returns JSON with sources array and stream_id | +| 2 | Extractor can find HLS .m3u8 URLs from video/source src attributes and script tag contents | ✓ VERIFIED | DOM extraction at extractor.go:166-192, regex patterns at extractor.go:196,201, matches .m3u8 in video/source/iframe src | +| 3 | Extractor can find DASH .mpd URLs from video/source src attributes and script tag contents | ✓ VERIFIED | DOM extraction at extractor.go:166-192, regex patterns at extractor.go:197,202, matches .mpd in video/source/iframe src | +| 4 | Extractor can find direct MP4/WebM URLs from video/source src attributes | ✓ VERIFIED | DOM extraction at extractor.go:166-192, regex patterns at extractor.go:198-199, matches .mp4/.webm in video/source/iframe src | +| 5 | Extractor can find video URLs from jwplayer, video.js, and hls.js setup calls in script tags | ✓ VERIFIED | Regex patterns at extractor.go:200-202 for JWPlayer (file:), hls.js/video.js (src:=), script extraction at extractor.go:206-223 | +| 6 | GET /api/streams/{id}/extract returns empty result (not error) when no video source is found | ✓ VERIFIED | Returns []VideoSource{} at extractor.go:66,306 with nil error when no sources found | +| 7 | When a stream has an extractable HLS source, the user sees a native video player on the app page instead of an iframe loading the third-party site | ✓ VERIFIED | tryExtractVideo at streams.js:172-189 fetches extract endpoint, renderNativePlayer at streams.js:200-228 creates HTML5 video element | +| 8 | When a stream has an extractable MP4/WebM source, the user sees a native HTML5 video element playing the stream | ✓ VERIFIED | renderNativePlayer at streams.js:200-228 handles mp4/webm by setting video.src directly (line 223) | +| 9 | When extraction fails or returns no sources, the user sees the existing iframe fallback (no regression) | ✓ VERIFIED | tryExtractVideo calls renderIframeFallback at streams.js:188 on error/empty sources, renderIframeFallback at streams.js:230-246 creates iframe element | +| 10 | HLS streams play using hls.js library when the browser does not support HLS natively | ✓ VERIFIED | HLS.js loaded from CDN at index.html:162, renderNativePlayer checks Hls.isSupported() at streams.js:212, creates new Hls() at streams.js:213-215, Safari native fallback at streams.js:216-217 | +| 11 | The video player has standard controls (play, pause, volume, fullscreen) | ✓ VERIFIED | video.controls = true at streams.js:205, HTML5 video element provides standard browser controls including play, pause, volume, fullscreen | + +**Score:** 11/11 truths verified + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `internal/extractor/extractor.go` | Video source URL extraction from HTML pages | ✓ VERIFIED | 326 lines, exports Extract function and VideoSource type, implements DOM parsing with golang.org/x/net/html and regex extraction from script tags | +| `internal/server/server.go` | API endpoint for video extraction | ✓ VERIFIED | Route registered at line 83, handler at lines 242-295, calls extractor.Extract, returns JSON with sources array and Cache-Control headers | +| `static/index.html` | HLS.js library script tag | ✓ VERIFIED | HLS.js CDN script tag at line 162 before other JS scripts | +| `static/js/streams.js` | Updated streamCard with native video player support | ✓ VERIFIED | 398 lines total, includes tryExtractVideos (168-170), tryExtractVideo (172-189), renderNativePlayer (200-228), renderIframeFallback (230-246), pickBestSource (191-198) | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|----|--------|---------| +| internal/server/server.go | internal/extractor/extractor.go | handler calls extractor.Extract | ✓ WIRED | Import at server.go:13, call at server.go:282 with client and streamURL parameters | +| internal/server/server.go | internal/store | handler looks up stream by ID | ✓ WIRED | Import at server.go:16, LoadStreams() call at server.go:246, iterates to find stream by ID at lines 255-264 | +| static/js/streams.js | /api/streams/{id}/extract | fetch call to extract endpoint | ✓ WIRED | fetch call at streams.js:174 with template literal for stream ID, response parsed as JSON at line 176 | +| static/js/streams.js | Hls | HLS.js library for .m3u8 playback | ✓ WIRED | Hls.isSupported() check at streams.js:212, new Hls() instantiation at streams.js:213, loadSource/attachMedia at streams.js:214-215 | + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|------------|-------------|--------|----------| +| EMBED-01 | 04-01-PLAN.md | Proxy fetches stream page and attempts to extract direct video source URL (HLS .m3u8, DASH .mpd, direct MP4/WebM, or embedded video player source) | ✓ SATISFIED | Extractor package at internal/extractor/extractor.go implements all extraction types: HLS (lines 107,117,147,196,201,229), DASH (lines 109,120,148,197,202,235), MP4 (lines 111,149,198), WebM (lines 113,150,199), JWPlayer/video.js/hls.js patterns (lines 200-202). Server-side extraction via handleExtractVideo at server.go:242-295 | +| EMBED-02 | 04-02-PLAN.md | When direct video source is found, render it in a minimal HTML5 video player on the app's own page (no third-party page loaded) | ✓ SATISFIED | renderNativePlayer at streams.js:200-228 creates HTML5 video element with controls=true (line 205), integrates HLS.js for .m3u8 sources (lines 212-215), sets src directly for mp4/webm (line 223), replaces loading placeholder in player-wrap div preventing third-party page load | + +### Anti-Patterns Found + +None detected. + +Scanned files: +- `internal/extractor/extractor.go`: No TODO/FIXME/placeholder comments, no empty implementations, substantive extraction logic +- `internal/server/server.go`: Handler implementation complete, proper error handling, cache headers set +- `static/js/streams.js`: No TODO/FIXME/placeholder comments, complete extraction-first rendering logic +- `static/index.html`: HLS.js script tag present, no issues + +### Human Verification Required + +None. All verification completed programmatically. + +### Phase Summary + +Phase 4 successfully delivers video extraction and native playback capability. The backend extractor can identify HLS, DASH, MP4, and WebM sources from both DOM elements and script tags. The frontend attempts extraction first and upgrades to a native HTML5 video player when sources are found, falling back to iframe rendering when extraction fails or returns no results. + +**Key accomplishments:** +- Server-side video source extraction with multiple strategies (DOM parsing + regex script extraction) +- Native HTML5 video player with HLS.js integration for .m3u8 streams +- Progressive enhancement pattern: render placeholder, attempt extraction, upgrade or fallback +- No breaking changes to existing iframe fallback behavior +- 5-minute cache on extraction endpoint to reduce upstream load +- Priority-based source selection (HLS > DASH > MP4 > WebM) + +**Technical quality:** +- All artifacts exist, substantive, and properly wired +- No anti-patterns detected +- Consistent error handling with silent fallback to iframe +- User-Agent and timeout patterns match existing proxy/scraper conventions +- Proper use of golang.org/x/net/html for DOM parsing +- HLS.js loaded from CDN with browser capability detection + +**Requirements fulfilled:** +- EMBED-01: Video source extraction from stream pages ✓ +- EMBED-02: Native HTML5 video player rendering ✓ + +Phase goal achieved. Users can now watch streams with extractable video sources in a clean native player without loading third-party pages. + +--- + +_Verified: 2026-02-17T22:00:00Z_ + +_Verifier: Claude (gsd-verifier)_ diff --git a/stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-01-PLAN.md b/stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-01-PLAN.md new file mode 100644 index 00000000..de31a10d --- /dev/null +++ b/stacks/f1-stream/module/files/.planning/phases/05-sandbox-proxy-hardening/05-01-PLAN.md @@ -0,0 +1,147 @@ +--- +phase: 05-sandbox-proxy-hardening +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: [internal/proxy/sanitize.go, internal/proxy/blocklist.go, internal/proxy/proxy.go, internal/server/server.go] +autonomous: true +requirements: [EMBED-06, EMBED-07, EMBED-08] + +must_haves: + truths: + - "Proxied HTML content has known ad/tracker scripts and domains stripped before serving" + - "Relative URLs in proxied content are rewritten to route through the proxy" + - "All proxied content is served with strict CSP headers scoped to the sandbox context" + artifacts: + - path: "internal/proxy/sanitize.go" + provides: "HTML sanitizer that strips ads, rewrites URLs, and adds CSP" + contains: "func Sanitize" + - path: "internal/proxy/blocklist.go" + provides: "Ad/tracker domain blocklist for script filtering" + contains: "blockedDomains" + - path: "internal/proxy/proxy.go" + provides: "New /proxy/sandbox endpoint serving sanitized content" + contains: "ServeSandbox" + - path: "internal/server/server.go" + provides: "Route registration for sandbox proxy endpoint" + contains: "proxy/sandbox" + key_links: + - from: "internal/server/server.go" + to: "internal/proxy/proxy.go" + via: "route registration for /proxy/sandbox" + pattern: "proxy/sandbox.*ServeSandbox" + - from: "internal/proxy/proxy.go" + to: "internal/proxy/sanitize.go" + via: "ServeSandbox calls Sanitize" + pattern: "Sanitize\\(" +--- + + +Backend proxy hardening: sanitize proxied HTML content by stripping ad/tracker scripts, rewriting relative URLs through the proxy, and serving with strict CSP headers. + +Purpose: When the frontend shadow DOM sandbox fetches proxied page content, the backend must deliver clean HTML free of ad scripts with working sub-resources and strict content security policy. + +Output: A new `/proxy/sandbox` endpoint that serves sanitized HTML for shadow DOM injection. + + + +@/Users/viktorbarzin/.claude/get-shit-done/workflows/execute-plan.md +@/Users/viktorbarzin/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + +@internal/proxy/proxy.go +@internal/server/server.go +@internal/extractor/extractor.go + + + + + + Task 1: Create HTML sanitizer with ad/tracker stripping and URL rewriting + internal/proxy/sanitize.go, internal/proxy/blocklist.go + +Create `internal/proxy/blocklist.go`: +- Define a `var blockedDomains = map[string]bool{...}` containing well-known ad/tracker domains. Include at least 30-40 entries covering: doubleclick.net, googlesyndication.com, googleadservices.com, google-analytics.com, facebook.net, connect.facebook.net, adservice.google.com, pagead2.googlesyndication.com, cdn.taboola.com, cdn.outbrain.com, ads.yahoo.com, amazon-adsystem.com, adsrvr.org, criteo.com, quantserve.com, scorecardresearch.com, serving-sys.com, rubiconproject.com, pubmatic.com, moatads.com, chartbeat.com, newrelic.com, nr-data.net, hotjar.com, mixpanel.com, segment.com, amplitude.com, popads.net, popcash.net, propellerads.com, adsterra.com, exoclick.com, juicyads.com, trafficjunky.com, etc. +- Define `func IsBlockedDomain(host string) bool` that checks the hostname and its parent domains against the blocklist. For example, if host is "ads.example.com" and "example.com" is blocked, return true. Walk up the domain labels. + +Create `internal/proxy/sanitize.go`: +- Import `golang.org/x/net/html` (already in go.mod). +- Define `func Sanitize(doc *html.Node, baseURL *url.URL, proxyPrefix string) string` that: + 1. Walks the DOM tree and removes `