infra/stacks/reverse-proxy/modules/reverse_proxy/main.tf
Viktor Barzin 8b43692af0 [infra] Suppress Goldilocks vpa-update-mode label drift on all namespaces [ci skip]
## Context

Wave 3B-continued: the Goldilocks VPA dashboard (stacks/vpa) runs a Kyverno
ClusterPolicy `goldilocks-vpa-auto-mode` that mutates every namespace with
`metadata.labels["goldilocks.fairwinds.com/vpa-update-mode"] = "off"`. This
is intentional — Terraform owns container resource limits, and Goldilocks
should only provide recommendations, never auto-update. The label is how
Goldilocks decides per-namespace whether to run its VPA in `off` mode.

Effect on Terraform: every `kubernetes_namespace` resource shows the label
as pending-removal (`-> null`) on every `scripts/tg plan`. Dawarich survey
2026-04-18 confirmed the drift. Cluster-side count: 88 namespaces carry the
label (`kubectl get ns -o json | jq ... | wc -l`). Every TF-managed namespace
is affected.

This commit brings the intentional admission drift under the same
`# KYVERNO_LIFECYCLE_V1` discoverability marker introduced in c9d221d5 for
the ndots dns_config pattern. The marker now stands generically for any
Kyverno admission-webhook drift suppression; the inline comment records
which specific policy stamps which specific field so future grep audits
show why each suppression exists.

## This change

107 `.tf` files touched — every stack's `resource "kubernetes_namespace"`
resource gets:

```hcl
lifecycle {
  # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace
  ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]]
}
```

Injection was done with a brace-depth-tracking Python pass (`/tmp/add_goldilocks_ignore.py`):
match `^resource "kubernetes_namespace" ` → track `{` / `}` until the
outermost closing brace → insert the lifecycle block before the closing
brace. The script is idempotent (skips any file that already mentions
`goldilocks.fairwinds.com/vpa-update-mode`) so re-running is safe.

Vault stack picked up 2 namespaces in the same file (k8s-users produces
one, plus a second explicit ns) — confirmed via file diff (+8 lines).

## What is NOT in this change

- `stacks/trading-bot/main.tf` — entire file is `/* … */` commented out
  (paused 2026-04-06 per user decision). Reverted after the script ran.
- `stacks/_template/main.tf.example` — per-stack skeleton, intentionally
  minimal. User keeps it that way. Not touched by the script (file
  has no real `resource "kubernetes_namespace"` — only a placeholder
  comment).
- `.terraform/` copies (e.g. `stacks/metallb/.terraform/modules/...`) —
  gitignored, won't commit; the live path was edited.
- `terraform fmt` cleanup of adjacent pre-existing alignment issues in
  authentik, freedify, hermes-agent, nvidia, vault, meshcentral. Reverted
  to keep the commit scoped to the Goldilocks sweep. Those files will
  need a separate fmt-only commit or will be cleaned up on next real
  apply to that stack.

## Verification

Dawarich (one of the hundred-plus touched stacks) showed the pattern
before and after:

```
$ cd stacks/dawarich && ../../scripts/tg plan

Before:
  Plan: 0 to add, 2 to change, 0 to destroy.
   # kubernetes_namespace.dawarich will be updated in-place
     (goldilocks.fairwinds.com/vpa-update-mode -> null)
   # module.tls_secret.kubernetes_secret.tls_secret will be updated in-place
     (Kyverno generate.* labels — fixed in 8d94688d)

After:
  No changes. Your infrastructure matches the configuration.
```

Injection count check:
```
$ rg -c 'KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode' stacks/ | awk -F: '{s+=$2} END {print s}'
108
```

## Reproduce locally
1. `git pull`
2. Pick any stack: `cd stacks/<name> && ../../scripts/tg plan`
3. Expect: no drift on the namespace's goldilocks.fairwinds.com/vpa-update-mode label.

Closes: code-dwx

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:15:27 +00:00

426 lines
14 KiB
HCL

# Reverse proxy for things in my infra that are
# outside of K8S but would be nice to use the Nginx-ingress
variable "tls_secret_name" {}
variable "truenas_homepage_token" {}
variable "pfsense_homepage_token" {}
variable "haos_homepage_token" {
type = string
default = ""
sensitive = true
}
resource "kubernetes_namespace" "reverse-proxy" {
metadata {
name = "reverse-proxy"
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace
ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]]
}
}
module "tls_secret" {
source = "../../../../modules/kubernetes/setup_tls_secret"
namespace = "reverse-proxy"
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
}
# https://pfsense.viktorbarzin.me/
module "pfsense" {
source = "./factory"
dns_type = "proxied"
name = "pfsense"
external_name = "pfsense.viktorbarzin.lan"
tls_secret_name = var.tls_secret_name
port = 443
backend_protocol = "HTTPS"
extra_annotations = {
"gethomepage.dev/enabled" : "true"
"gethomepage.dev/description" : "Cluster Firewall"
"gethomepage.dev/group" : "Identity & Security"
"gethomepage.dev/icon" : "pfsense.png"
"gethomepage.dev/name" : "pFsense"
"gethomepage.dev/widget.type" : "pfsense"
"gethomepage.dev/widget.version" : "2"
"gethomepage.dev/widget.url" : "https://10.0.20.1"
"gethomepage.dev/widget.username" : "admin"
"gethomepage.dev/widget.password" : var.pfsense_homepage_token
"gethomepage.dev/widget.fields" = "[\"load\", \"memory\", \"temp\", \"disk\"]"
"gethomepage.dev/widget.wan" = "vtnet0"
}
depends_on = [kubernetes_namespace.reverse-proxy]
}
# https://nas.viktorbarzin.me/
module "nas" {
source = "./factory"
dns_type = "proxied"
name = "nas"
external_name = "nas.viktorbarzin.lan"
port = 5001
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
max_body_size = "0m"
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "Synology NAS"
"gethomepage.dev/description" = "Network storage"
"gethomepage.dev/icon" = "synology.png"
"gethomepage.dev/group" = "Infrastructure"
"gethomepage.dev/pod-selector" = ""
}
}
# https://files.viktorbarzin.me/
module "nas-files" {
source = "./factory"
dns_type = "non-proxied"
name = "files"
external_name = "nas.viktorbarzin.lan"
port = 5001
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
protected = false # allow anyone to download files
ingress_path = ["/sharing", "/scripts", "/webman", "/wfmlogindialog.js", "/fsdownload"]
max_body_size = "0m"
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}
# https://idrac.viktorbarzin.me/
module "idrac" {
source = "./factory"
dns_type = "proxied"
name = "idrac"
external_name = "idrac.viktorbarzin.lan"
port = 443
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
strip_auth_headers = true
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "iDRAC"
"gethomepage.dev/description" = "Server management"
"gethomepage.dev/icon" = "dell.png"
"gethomepage.dev/group" = "Infrastructure"
"gethomepage.dev/pod-selector" = ""
}
depends_on = [kubernetes_namespace.reverse-proxy]
}
# Can either listen on https or http; can't do both :/
# TODO: Not working yet
module "tp-link-gateway" {
source = "./factory"
dns_type = "proxied"
name = "gw"
external_name = "gw.viktorbarzin.lan"
port = 443
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
depends_on = [kubernetes_namespace.reverse-proxy]
protected = true
strip_auth_headers = true
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}
# https://truenas.viktorbarzin.me/
module "truenas" {
source = "./factory"
dns_type = "proxied"
name = "truenas"
external_name = "truenas.viktorbarzin.lan"
port = 80
tls_secret_name = var.tls_secret_name
max_body_size = "0m"
extra_annotations = {
"gethomepage.dev/enabled" : "true"
"gethomepage.dev/description" : "TrueNAS"
"gethomepage.dev/group" : "Infrastructure"
"gethomepage.dev/icon" : "truenas.png"
"gethomepage.dev/name" : "TrueNAS"
"gethomepage.dev/widget.type" : "truenas"
"gethomepage.dev/widget.url" : "https://truenas.viktorbarzin.lan"
"gethomepage.dev/widget.key" : var.truenas_homepage_token
# "gethomepage.dev/widget.enablePools" : "true"
# "gethomepage.dev/pod-selector" : ""
}
depends_on = [kubernetes_namespace.reverse-proxy]
}
# https://r730.viktorbarzin.me/
module "r730" {
source = "./factory"
name = "r730"
external_name = "r730.viktorbarzin.lan"
port = 443
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "R730"
"gethomepage.dev/description" = "Dell PowerEdge server"
"gethomepage.dev/icon" = "dell.png"
"gethomepage.dev/group" = "Infrastructure"
"gethomepage.dev/pod-selector" = ""
}
}
# https://proxmox.viktorbarzin.me/
module "proxmox" {
source = "./factory"
dns_type = "proxied"
name = "proxmox"
external_name = "proxmox.viktorbarzin.lan"
port = 8006
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
max_body_size = "0" # unlimited
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "Proxmox"
"gethomepage.dev/description" = "Hypervisor"
"gethomepage.dev/icon" = "proxmox.png"
"gethomepage.dev/group" = "Infrastructure"
"gethomepage.dev/pod-selector" = ""
}
}
# https://docker.viktorbarzin.me/ (registry web UI)
module "docker-registry-ui" {
source = "./factory"
dns_type = "proxied"
name = "docker"
external_name = "docker-registry.viktorbarzin.lan"
port = 8080
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
# Override middleware chain to remove rate-limit; the UI fires many API calls to list repos/tags
"traefik.ingress.kubernetes.io/router.middlewares" = "traefik-csp-headers@kubernetescrd,traefik-crowdsec@kubernetescrd,traefik-authentik-forward-auth@kubernetescrd"
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "Docker Registry"
"gethomepage.dev/description" = "Container registry"
"gethomepage.dev/icon" = "docker.png"
"gethomepage.dev/group" = "Infrastructure"
"gethomepage.dev/pod-selector" = ""
}
}
# https://registry.viktorbarzin.me/ (Docker CLI push/pull endpoint)
module "docker-registry-cli" {
source = "./factory"
dns_type = "non-proxied"
name = "registry"
external_name = "docker-registry.viktorbarzin.lan"
port = 5050
backend_protocol = "HTTPS"
tls_secret_name = var.tls_secret_name
protected = false # Docker CLI uses htpasswd, NOT Authentik
max_body_size = "0" # unlimited - Docker layers can be large
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
# Skip rate-limit (Docker push/pull generates many rapid requests)
# Keep CrowdSec for L7 protection
"traefik.ingress.kubernetes.io/router.middlewares" = "traefik-csp-headers@kubernetescrd,traefik-crowdsec@kubernetescrd"
"gethomepage.dev/enabled" = "false"
}
}
# https://valchedrym.viktorbarzin.me/
module "valchedrym" {
source = "./factory"
dns_type = "proxied"
name = "valchedrym"
external_name = "valchedrym.viktorbarzin.lan"
tls_secret_name = var.tls_secret_name
port = 80
backend_protocol = "HTTP"
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}
# https://ip150.viktorbarzin.me/
# Server has funky behaviour based on headers; works on some browrsers not others...
# module "valchedrym-ip150" {
# source = "./factory"
# name = "ip150"
# # external_name = "valchedrym.ddns.net"
# external_name = "192.168.0.10"
# port = 80
# backend_protocol = "HTTP"
# use_proxy_protocol = false
# tls_secret_name = var.tls_secret_name
# protected = false
# depends_on = [kubernetes_namespace.reverse-proxy]
# }
# https://mladost3.viktorbarzin.me/
module "mladost3" {
source = "./factory"
name = "mladost3"
external_name = "mladost3.ddns.net"
port = 8080
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}
# # https://server-switch.viktorbarzin.me/
# module "server-switch" {
# source = "./factory"
# name = "server-switch"
# external_name = "server-switch.viktorbarzin.lan"
# port = 80
# tls_secret_name = var.tls_secret_name
# depends_on = [kubernetes_namespace.reverse-proxy]
# }
# https://ha-sofia.viktorbarzin.me/
resource "kubernetes_manifest" "ha_sofia_rate_limit" {
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "Middleware"
metadata = {
name = "ha-sofia-rate-limit"
namespace = "reverse-proxy"
}
spec = {
rateLimit = {
average = 100
burst = 200
}
}
}
}
module "ha-sofia" {
source = "./factory"
dns_type = "non-proxied"
name = "ha-sofia"
external_name = "ha-sofia.viktorbarzin.lan"
port = 8123
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
protected = false
skip_global_rate_limit = true
extra_middlewares = [
"reverse-proxy-ha-sofia-rate-limit@kubernetescrd",
]
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "Home Assistant Sofia"
"gethomepage.dev/description" = "Smart home hub"
"gethomepage.dev/icon" = "home-assistant.png"
"gethomepage.dev/group" = "Smart Home"
"gethomepage.dev/pod-selector" = ""
}
}
# https://music-assistant.viktorbarzin.me/
module "music-assistant" {
source = "./factory"
dns_type = "non-proxied"
name = "music-assistant"
external_name = "ha-sofia.viktorbarzin.lan"
port = 8095
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
protected = false
skip_global_rate_limit = true
extra_middlewares = [
"reverse-proxy-ha-sofia-rate-limit@kubernetescrd",
]
}
# https://ha-london.viktorbarzin.me/
module "ha-london" {
source = "./factory"
dns_type = "non-proxied"
name = "ha-london"
external_name = "ha-london.viktorbarzin.lan"
port = 8123
tls_secret_name = var.tls_secret_name
depends_on = [kubernetes_namespace.reverse-proxy]
protected = false
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "Home Assistant London"
"gethomepage.dev/description" = "Smart home hub"
"gethomepage.dev/icon" = "home-assistant.png"
"gethomepage.dev/group" = "Smart Home"
"gethomepage.dev/pod-selector" = ""
}
}
# https://london.viktorbarzin.me/
module "london" {
source = "./factory"
dns_type = "proxied"
name = "london"
external_name = "openwrt-london.viktorbarzin.lan"
port = 443
tls_secret_name = var.tls_secret_name
backend_protocol = "HTTPS"
protected = true
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = {
"gethomepage.dev/enabled" : "false"
"gethomepage.dev/description" : "OpenWRT London"
# gethomepage.dev/group: Media
"gethomepage.dev/icon" : "openwrt.png"
"gethomepage.dev/name" : "OpenWRT London"
"gethomepage.dev/widget.type" : "openwrt"
"gethomepage.dev/widget.url" : "https://100.64.0.14"
# "gethomepage.dev/widget.token" = var.homepage_token
"gethomepage.dev/widget.username" : "homepage"
"gethomepage.dev/widget.password" : "" # add later as Flint2's openwrt is a little odd
"gethomepage.dev/pod-selector" : ""
}
}
module "pi-lights" {
source = "./factory"
dns_type = "proxied"
name = "pi"
external_name = "ha-london.viktorbarzin.lan"
port = 5000
tls_secret_name = var.tls_secret_name
protected = true
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}
# module "ups" { # .NET app doesn't work well behind host
# source = "./factory"
# name = "ups"
# external_name = "ups.viktorbarzin.lan"
# backend_protocol = "HTTPS"
# port = 443
# tls_secret_name = var.tls_secret_name
# # protected = true
# protected = false
# depends_on = [kubernetes_namespace.reverse-proxy]
# extra_annotations = {
# "nginx.ingress.kubernetes.io/upstream-vhost" : "",
# # "nginx.ingress.kubernetes.io/proxy-set-header" : "Host: <>",
# }
# }
module "mbp14" {
source = "./factory"
dns_type = "proxied"
name = "mbp14"
external_name = "mbp14.viktorbarzin.lan"
port = 4020
tls_secret_name = var.tls_secret_name
protected = true
depends_on = [kubernetes_namespace.reverse-proxy]
extra_annotations = { "gethomepage.dev/enabled" = "false" }
}