Compare commits

..

No commits in common. "eee694c9152ba4c0e7722d7e1b26afe1f018895f" and "43b4e1d3729d00ab81e4a3797145754e880284e7" have entirely different histories.

140 changed files with 4188 additions and 5869 deletions

View file

@ -48,7 +48,6 @@ Violations cause state drift, which causes future applies to break or silently r
- **Tier 0 details**: Decrypt priority: Vault Transit (primary) → age key fallback. Encrypt: both Vault Transit + age recipients. Scripts: `scripts/state-sync {encrypt|decrypt|commit} [stack]`.
- **Adding operator**: Generate age key (`age-keygen`), add pubkey to `.sops.yaml`, run `sops updatekeys` on Tier 0 `.enc` files. For Tier 1, only Vault access is needed.
- **Migration script**: `scripts/migrate-state-to-pg` (one-shot, idempotent) migrates Tier 1 stacks from local to PG.
- **Adopting existing resources**: use HCL `import {}` blocks (TF 1.5+), not `terraform import` CLI. Commit stanza → plan-to-zero → apply → delete stanza. Canonical reason: reviewable in PR, plan-safe, idempotent, tier-agnostic. Full rules + per-provider ID formats in `AGENTS.md` → "Adopting Existing Resources".
## Secrets Management — Vault KV
- **Vault is the sole source of truth** for secrets.

View file

@ -1,26 +1,22 @@
---
name: payslip-extractor
description: "Extract structured UK payslip fields from already-extracted text (preferred) or a base64 PDF (fallback) into strict JSON."
description: "Extract structured UK payslip fields from a base64-encoded PDF into strict JSON."
model: haiku
allowedTools:
- Bash
- Read
---
You are a headless payslip-field extractor. You receive a prompt containing a UK payslip (either as pre-extracted text or as a base64-encoded PDF) plus a target JSON schema, and you produce exactly one JSON object that matches the schema.
You are a headless payslip-field extractor. You receive a prompt containing a base64-encoded UK payslip PDF plus a target JSON schema, and you produce exactly one JSON object that matches the schema.
## Your single job
Given a prompt that contains EITHER:
- A line `PAYSLIP_TEXT:` followed by already-extracted text (preferred path — use it directly, skip to Step 3).
- OR a line `PDF_BASE64:` followed by a base64 blob (fallback path — decode then extract text first).
Given a prompt that contains:
- A line of the form `PDF_BASE64: <base64-blob>`
- A JSON schema describing the target fields
Produce EXACTLY ONE JSON object on stdout matching the schema. No prose. No markdown fences. No preamble. No trailing commentary. The final message content must be a single valid JSON object and nothing else.
## Fast path: PAYSLIP_TEXT is present
If the prompt contains `PAYSLIP_TEXT:`, the caller has already run `pdftotext -layout`. Skip Steps 1-2 entirely — the text is already in your context. Go straight to Step 3.
## Processing steps
### Step 1. Extract and decode the base64 PDF

View file

@ -42,15 +42,10 @@ steps:
-d "{\"role\":\"ci\",\"jwt\":\"$SA_TOKEN\"}" | jq -r .auth.client_token)
# ── Run terraform plan on all stacks ──
# Emits two timestamps per drifted stack so the Pushgateway/Prometheus
# side can compute drift-age-hours via `time() - drift_stack_first_seen`.
- |
DRIFTED=""
CLEAN=0
ERRORS=""
NOW=$(date +%s)
# Metrics accumulator — written once per stack, then pushed as a batch.
METRICS=""
for stack_dir in stacks/*/; do
stack=$(basename "$stack_dir")
@ -61,50 +56,12 @@ steps:
EXIT=$?
case $EXIT in
0)
echo "OK (no changes)"
CLEAN=$((CLEAN + 1))
# drift_stack_state=0 means clean; age-hours irrelevant so we
# still push 0 so per-stack gauges don't go stale.
METRICS="${METRICS}drift_stack_state{stack=\"$stack\"} 0\n"
METRICS="${METRICS}drift_stack_age_hours{stack=\"$stack\"} 0\n"
;;
1)
echo "ERROR"
ERRORS="$ERRORS $stack"
METRICS="${METRICS}drift_stack_state{stack=\"$stack\"} 2\n"
;;
2)
echo "DRIFT DETECTED"
DRIFTED="$DRIFTED $stack"
# Fetch first-seen timestamp from Pushgateway (preserve across runs).
FIRST_SEEN=$(curl -s "http://prometheus-prometheus-pushgateway.monitoring:9091/metrics" \
| awk -v s="$stack" '$1 == "drift_stack_first_seen{stack=\""s"\"}" {print $2; exit}')
if [ -z "$FIRST_SEEN" ] || [ "$FIRST_SEEN" = "0" ]; then
FIRST_SEEN="$NOW"
fi
AGE_HOURS=$(( (NOW - FIRST_SEEN) / 3600 ))
METRICS="${METRICS}drift_stack_state{stack=\"$stack\"} 1\n"
METRICS="${METRICS}drift_stack_first_seen{stack=\"$stack\"} $FIRST_SEEN\n"
METRICS="${METRICS}drift_stack_age_hours{stack=\"$stack\"} $AGE_HOURS\n"
;;
0) echo "OK (no changes)"; CLEAN=$((CLEAN + 1)) ;;
1) echo "ERROR"; ERRORS="$ERRORS $stack" ;;
2) echo "DRIFT DETECTED"; DRIFTED="$DRIFTED $stack" ;;
esac
done
# Summary counters — single gauge per run.
DRIFT_COUNT=$(echo "$DRIFTED" | wc -w)
ERROR_COUNT=$(echo "$ERRORS" | wc -w)
METRICS="${METRICS}drift_stack_count $DRIFT_COUNT\n"
METRICS="${METRICS}drift_error_count $ERROR_COUNT\n"
METRICS="${METRICS}drift_clean_count $CLEAN\n"
METRICS="${METRICS}drift_detection_last_run_timestamp $NOW\n"
# ── Push to Pushgateway ──
# One batched push keeps the run atomic: either all metrics land or none.
printf "%b" "$METRICS" | curl -s --data-binary @- \
http://prometheus-prometheus-pushgateway.monitoring:9091/metrics/job/drift-detection \
|| echo "(pushgateway unavailable, metrics lost for this run)"
echo ""
echo "=== Drift Detection Summary ==="
echo "Clean: $CLEAN stacks"

View file

@ -15,49 +15,6 @@
- **Health check**: `bash scripts/cluster_healthcheck.sh --quiet`
- **Plan all**: `cd stacks && terragrunt run --all --non-interactive -- plan`
## Adopting Existing Resources — Use `import {}` Blocks, Not the CLI
When bringing a live cluster/Vault/Cloudflare resource under Terraform management, use an HCL `import {}` block (Terraform 1.5+). Do **NOT** use `terraform import` on the CLI for anything landing in this repo — the CLI path leaves no audit trail and makes multi-operator adoption fragile.
**Canonical workflow:**
1. Write the `resource` block that matches the live object.
2. In the same stack, add an `import {}` stanza naming the target and the provider-specific ID:
```hcl
import {
to = helm_release.kured
id = "kured/kured" # Helm ID format: <namespace>/<release-name>
}
resource "helm_release" "kured" {
name = "kured"
namespace = "kured"
repository = "https://kubereboot.github.io/charts/"
chart = "kured"
version = "5.7.0"
# ... values matching the live release
}
```
3. `scripts/tg plan` — every change it proposes is real divergence between HCL and live state. Iterate on values until the plan is **0 changes**.
4. `scripts/tg apply` — the import runs alongside whatever zero-change apply you have. If your plan is 0 changes, this commits only the state-ownership transfer.
5. After the apply lands cleanly, **delete the `import {}` block** in a follow-up commit. The resource is now fully TF-owned and the stanza would be a no-op that clutters diffs.
**Why `import {}` and not `terraform import`:**
- Reviewable in PRs before any state mutation. The CLI path is an out-of-band action nobody sees.
- Plan-safe: the `import` plan step shows the exact object being adopted. Mistyped IDs or the wrong resource address are caught before apply, not after.
- Survives state backend changes (Tier 0 SOPS vs Tier 1 PG) transparently — both work identically from the operator's perspective because both use `scripts/tg`.
- Re-runnable: if the apply fails partway through, the `import {}` block is idempotent. The CLI path's state mutation is not.
**Finding the provider-specific ID:** each provider has its own convention.
| Resource | ID format | Example |
|---|---|---|
| `helm_release` | `<namespace>/<release-name>` | `kured/kured` |
| `kubernetes_manifest` | `{"apiVersion":"...","kind":"...","metadata":{"namespace":"...","name":"..."}}` | (pass as HCL object literal) |
| `kubernetes_<kind>_v1` | `<namespace>/<name>` for namespaced, `<name>` for cluster-scoped | `kube-system/coredns` |
| `authentik_provider_proxy` | provider UUID | `0eecac07-97c7-443c-...` |
| `cloudflare_record` | `<zone-id>/<record-id>` | `abc123/def456` |
## Secrets Management (SOPS)
- **`config.tfvars`** — plaintext config (hostnames, IPs, DNS records, public keys)
- **`secrets.sops.json`** — SOPS-encrypted secrets (passwords, tokens, SSH keys, API keys)

View file

@ -1,185 +0,0 @@
# Beads Auto-Dispatch Runbook
Users can hand work to the headless `beads-task-runner` agent by assigning a
bead to the sentinel user `agent`. Two CronJobs in the `beads-server`
namespace drive the pipeline:
- **`beads-dispatcher`** — every 2 min: picks up the highest-priority
`assignee=agent`/`status=open` bead with non-empty acceptance criteria,
claims it by flipping to `in_progress`, and POSTs it to BeadBoard's
`/api/agent-dispatch`. BeadBoard forwards to `claude-agent-service` with
the existing bearer-token flow.
- **`beads-reaper`** — every 10 min: flips any `assignee=agent` +
`status=in_progress` bead whose `updated_at` is older than 30 min to
`status=blocked` with an explanatory note. Catches pod crashes mid-run.
The manual BeadBoard Dispatch button continues to work in parallel.
## Flow diagram
```
user: bd assign <id> agent
Dolt @ dolt.beads-server.svc:3306 ◄──── every 2 min ────┐
│ │
▼ │
CronJob: beads-dispatcher │
1. GET beadboard/api/agent-status (busy?) │
2. bd query 'assignee=agent AND status=open' │
3. bd update -s in_progress (claim) │
4. POST beadboard/api/agent-dispatch │
5. bd note "dispatched: job=…" │
│ │
▼ │
claude-agent-service /execute │
beads-task-runner agent runs; notes/closes bead │
│ │
▼ │
done ──► next tick picks up the next bead ───────────────┘
CronJob: beads-reaper (every 10 min)
for bead (assignee=agent, status=in_progress, updated_at > 30 min):
bd note "reaper: no progress for Nm — blocking"
bd update -s blocked
```
## Usage
### Hand a bead to the agent
```
bd create "Title" \
-d "Full context — files, services, error messages. Any agent with no prior context must be able to execute this." \
--acceptance "Concrete, verifiable criteria" \
-p 2
bd assign <new-id> agent
```
**Acceptance criteria is required.** Beads without it are skipped by the
dispatcher and stay in `open` forever. This is intentional — the
`beads-task-runner` agent expects clear done conditions.
### Take a bead back (unassign)
```
bd assign <id> ""
```
If the bead is already `in_progress`, also reset it:
```
bd update <id> -s open
```
### Pause auto-dispatch
```
cd infra/stacks/beads-server
scripts/tg apply -var=beads_dispatcher_enabled=false
```
This sets `spec.suspend: true` on both CronJobs. Existing running jobs
continue; no new ticks fire. Re-enable by re-applying with
`beads_dispatcher_enabled=true` (the default). Manual BeadBoard Dispatch
remains available while paused.
### Read the logs
```
# Recent dispatcher runs
kubectl -n beads-server get jobs --selector=job-name --sort-by=.metadata.creationTimestamp | grep beads-dispatcher | tail
kubectl -n beads-server logs job/<dispatcher-job-name>
# Tail the underlying agent once a bead dispatches
kubectl -n claude-agent logs -l app=claude-agent-service -f
# Inspect reaper decisions
kubectl -n beads-server get jobs | grep beads-reaper | tail
kubectl -n beads-server logs job/<reaper-job-name>
```
### Inspect a specific bead's dispatch history
```
bd show <id> --json | jq '{status, assignee, notes, updated_at}'
```
Both the dispatcher and reaper write dated notes (`auto-dispatcher claimed
at…`, `dispatched: job=…`, `reaper: no progress for…`) so the audit trail
lives on the bead itself.
## Reaper semantics — when a bead becomes `blocked`
The reaper flips a bead to `blocked` if:
- `assignee = agent`, AND
- `status = in_progress`, AND
- `updated_at` is more than **30 minutes** in the past.
Every `bd note` bumps `updated_at`, so a well-behaved `beads-task-runner`
agent never trips the reaper — it notes progress as it works. A `blocked`
bead is a signal that:
- the agent pod crashed mid-run (`kubectl -n claude-agent delete pod` test),
- the job hit its 15-minute budget timeout inside `claude-agent-service`
without notes (rare — the agent usually notes failure before exiting),
- `claude-agent-service` was restarted during the run (in-memory job state
is lost; see [known risks](#known-risks)).
Recovery: read the reaper note, reopen manually if appropriate:
```
bd update <id> -s open
bd assign <id> agent # re-arm for next dispatcher tick
```
## Design choices
- **Sentinel assignee `agent`** — free-form, no Beads schema change. Any bd
client can set it (`bd assign <id> agent`).
- **Sequential dispatch** — matches `claude-agent-service`'s single-slot
`asyncio.Lock`. With a 2-min poll cadence and ~5-min average run,
throughput is ~12 beads/hour. Parallelism is a separate plan.
- **Fixed agent (`beads-task-runner`)** — read-only rails, matches BeadBoard's
manual Dispatch button. Broader-privilege agents stay manual.
- **CronJob (not in-service polling, not n8n)** — matches existing infra
pattern (OpenClaw task-processor, certbot-renewal, backups), TF-managed,
easy to pause.
- **ConfigMap-mounted `metadata.json`** — declarative TF rather than reusing
the image-seeded file. The CronJob's init step copies it into `/tmp/.beads/`
because `bd` may touch the parent directory and ConfigMap mounts are
read-only.
## Known risks
- **In-memory job state in `claude-agent-service`** — if the pod restarts
mid-run, the job record is lost. The reaper catches this after 30 min.
Persistent job store is deferred.
- **Prompt injection via bead fields** — a malicious bead description could
try to steer the agent. The `beads-task-runner` rails + token budget +
timeout are the defense. Identical exposure as the manual Dispatch button.
- **Image tag drift**`claude_agent_service_image_tag` in
`stacks/beads-server/main.tf` mirrors `local.image_tag` in
`stacks/claude-agent-service/main.tf`. Bump both when the image rebuilds,
or the dispatcher/reaper will run on an older layer. (They only need
`bd`, `curl`, `jq` — stable across rebuilds — so the drift is low-risk.)
- **`bd` JSON schema changes** — the reaper's `jq` reads `.id` and
`.updated_at`. If a future `bd` upgrade renames these, the reaper breaks
silently (no reaping, no alert). `BD_VERSION` is pinned in the image
Dockerfile.
## Verification after change
```
# Both CronJobs exist with the right schedule / SUSPEND state
kubectl -n beads-server get cronjob
# End-to-end smoke test
bd create "auto-dispatch smoke test" \
-d "Read /etc/hostname inside the agent sandbox and close." \
--acceptance "bd note includes 'hostname=' and bead is closed."
bd assign <new-id> agent
# within 2 min:
bd show <new-id> --json | jq '.notes'
# → contains 'auto-dispatcher claimed' + 'dispatched: job=<uuid>'
```

View file

@ -18,8 +18,4 @@ resource "kubernetes_secret" "tls_secret" {
"tls.key" = var.tls_key == "" ? file("${path.root}/secrets/privkey.pem") : var.tls_key
}
type = "kubernetes.io/tls"
lifecycle {
# KYVERNO_LIFECYCLE_V1: the sync-tls-secret policy stamps generate.kyverno.io/* + app.kubernetes.io/managed-by labels on this generated Secret
ignore_changes = [metadata[0].labels]
}
}

View file

@ -116,10 +116,6 @@ resource "kubernetes_deployment" "actualbudget" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "actualbudget" {
@ -218,10 +214,6 @@ resource "kubernetes_deployment" "actualbudget-http-api" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "actualbudget-http-api" {
@ -312,8 +304,4 @@ resource "kubernetes_cron_job_v1" "bank-sync" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -59,10 +59,6 @@ resource "kubernetes_namespace" "actualbudget" {
tier = local.tiers.edge
}
}
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" {

View file

@ -90,10 +90,6 @@ resource "kubernetes_namespace" "affine" {
tier = local.tiers.aux
}
}
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" {
@ -323,10 +319,6 @@ resource "kubernetes_deployment" "affine" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "affine" {

View file

@ -31,10 +31,6 @@ resource "kubernetes_namespace" "authentik" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "kubernetes_resource_quota" "authentik" {

View file

@ -115,10 +115,6 @@ resource "kubernetes_deployment" "pgbouncer" {
}
}
depends_on = [kubernetes_secret.pgbouncer_auth]
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
# --- 4 Service ---

View file

@ -3,25 +3,10 @@ variable "tls_secret_name" {
sensitive = true
}
# Temporary default until GHA pipeline publishes the first 8-char SHA tag.
variable "beadboard_image_tag" {
type = string
default = "17a38e43"
}
# Mirrors `local.image_tag` in stacks/claude-agent-service/main.tf keep in
# sync when the claude-agent-service image is rebuilt. Reused here because the
# dispatcher + reaper CronJobs only need bd, curl, and jq, which that image
# already ships.
variable "claude_agent_service_image_tag" {
type = string
default = "0c24c9b6"
}
# Kill switch for auto-dispatch. When false, both CronJobs are suspended. The
# manual BeadBoard Dispatch button keeps working either way.
variable "beads_dispatcher_enabled" {
type = bool
default = true
default = "latest"
}
resource "kubernetes_namespace" "beads" {
@ -31,10 +16,6 @@ resource "kubernetes_namespace" "beads" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_persistent_volume_claim" "dolt_data" {
@ -687,274 +668,3 @@ module "beadboard_ingress" {
"gethomepage.dev/pod-selector" = ""
}
}
# Beads auto-dispatch (dispatcher + reaper CronJobs)
#
# Flow:
# user: bd assign <id> agent
# > CronJob: beads-dispatcher (every 2 min)
# 1. GET BeadBoard /api/agent-status skip if claude-agent-service busy
# 2. bd query 'assignee=agent AND status=open' pick highest priority
# 3. bd update -s in_progress (claim; next tick won't re-pick)
# 4. POST BeadBoard /api/agent-dispatch reuses prompt-build + bearer flow
# 5. bd note "dispatched: job=<id>" (or rollback + note on failure)
#
# CronJob: beads-reaper (every 10 min)
# for bead (assignee=agent, status=in_progress, updated_at > 30m):
# bd update -s blocked + bd note (recover from pod crashes mid-run)
#
# The claude-agent-service image ships bd + jq + curl no separate image built.
resource "kubernetes_config_map" "beads_metadata" {
metadata {
name = "beads-metadata"
namespace = kubernetes_namespace.beads.metadata[0].name
}
data = {
"metadata.json" = jsonencode({
database = "dolt"
backend = "dolt"
dolt_mode = "server"
dolt_server_host = "${kubernetes_service.dolt.metadata[0].name}.${kubernetes_namespace.beads.metadata[0].name}.svc.cluster.local"
dolt_server_port = 3306
dolt_server_user = "beads"
dolt_database = "code"
project_id = "a8f8bae7-ce65-4145-a5db-a13d11d297da"
})
}
}
locals {
claude_agent_service_image = "registry.viktorbarzin.me/claude-agent-service:${var.claude_agent_service_image_tag}"
beadboard_internal_url = "http://${kubernetes_service.beadboard.metadata[0].name}.${kubernetes_namespace.beads.metadata[0].name}.svc.cluster.local"
beads_script_prelude = <<-EOT
set -euo pipefail
# bd with Dolt server mode needs metadata.json in a directory it can walk.
# ConfigMap mounts are read-only copy to a writable location before use.
mkdir -p /tmp/.beads
cp /etc/beads-metadata/metadata.json /tmp/.beads/metadata.json
EOT
}
resource "kubernetes_cron_job_v1" "beads_dispatcher" {
metadata {
name = "beads-dispatcher"
namespace = kubernetes_namespace.beads.metadata[0].name
}
spec {
schedule = "*/2 * * * *"
concurrency_policy = "Forbid"
successful_jobs_history_limit = 3
failed_jobs_history_limit = 3
starting_deadline_seconds = 60
suspend = !var.beads_dispatcher_enabled
job_template {
metadata {}
spec {
backoff_limit = 0
ttl_seconds_after_finished = 600
template {
metadata {
labels = {
app = "beads-dispatcher"
}
}
spec {
restart_policy = "Never"
image_pull_secrets {
name = "registry-credentials"
}
container {
name = "dispatcher"
image = local.claude_agent_service_image
command = ["/bin/sh", "-c", <<-EOT
${local.beads_script_prelude}
BUSY=$(curl -sf "$${BEADBOARD_URL}/api/agent-status" | jq -r '.busy // false')
if [ "$BUSY" != "false" ]; then
echo "claude-agent-service is busy — skipping tick"
exit 0
fi
BEAD=$(bd --db /tmp/.beads query 'assignee=agent AND status=open' --json \
| jq -r '[.[] | select(.acceptance_criteria and (.acceptance_criteria | length) > 0)]
| sort_by(.priority, .updated_at)[0].id // empty')
if [ -z "$BEAD" ]; then
echo "no eligible beads (assignee=agent, status=open, has acceptance_criteria)"
exit 0
fi
echo "picked bead: $BEAD"
bd --db /tmp/.beads update "$BEAD" -s in_progress
bd --db /tmp/.beads note "$BEAD" "auto-dispatcher claimed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
RESP=$(curl -sS -w '\n%%{http_code}' -X POST \
-H 'Content-Type: application/json' \
-d "{\"taskId\":\"$BEAD\"}" \
"$${BEADBOARD_URL}/api/agent-dispatch")
CODE=$(printf '%s' "$RESP" | tail -n1)
BODY=$(printf '%s' "$RESP" | sed '$d')
if [ "$CODE" = "200" ]; then
JOB_ID=$(printf '%s' "$BODY" | jq -r '.job_id // "unknown"')
bd --db /tmp/.beads note "$BEAD" "dispatched: job=$JOB_ID"
echo "dispatched $BEAD as job $JOB_ID"
else
# Roll the claim back so the next tick can retry.
bd --db /tmp/.beads update "$BEAD" -s open
bd --db /tmp/.beads note "$BEAD" "dispatch failed HTTP $CODE: $BODY"
echo "dispatch FAILED for $BEAD: HTTP $CODE — $BODY" >&2
exit 1
fi
EOT
]
env {
name = "BEADBOARD_URL"
value = local.beadboard_internal_url
}
env {
name = "API_BEARER_TOKEN"
value_from {
secret_key_ref {
name = "beadboard-agent-service"
key = "api_bearer_token"
}
}
}
env {
name = "BEADS_ACTOR"
value = "beads-dispatcher"
}
env {
name = "HOME"
value = "/tmp"
}
volume_mount {
name = "beads-metadata"
mount_path = "/etc/beads-metadata"
read_only = true
}
resources {
requests = {
cpu = "50m"
memory = "128Mi"
}
limits = {
memory = "256Mi"
}
}
}
volume {
name = "beads-metadata"
config_map {
name = kubernetes_config_map.beads_metadata.metadata[0].name
}
}
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_cron_job_v1" "beads_reaper" {
metadata {
name = "beads-reaper"
namespace = kubernetes_namespace.beads.metadata[0].name
}
spec {
schedule = "*/10 * * * *"
concurrency_policy = "Forbid"
successful_jobs_history_limit = 3
failed_jobs_history_limit = 3
starting_deadline_seconds = 60
suspend = !var.beads_dispatcher_enabled
job_template {
metadata {}
spec {
backoff_limit = 0
ttl_seconds_after_finished = 600
template {
metadata {
labels = {
app = "beads-reaper"
}
}
spec {
restart_policy = "Never"
image_pull_secrets {
name = "registry-credentials"
}
container {
name = "reaper"
image = local.claude_agent_service_image
command = ["/bin/sh", "-c", <<-EOT
${local.beads_script_prelude}
THRESHOLD_MIN=30
NOW=$(date -u +%s)
bd --db /tmp/.beads query 'assignee=agent AND status=in_progress' --json \
| jq -c '.[]' \
| while read -r BEAD_JSON; do
ID=$(printf '%s' "$BEAD_JSON" | jq -r '.id')
LAST_UPDATE=$(printf '%s' "$BEAD_JSON" | jq -r '.updated_at')
# Alpine's busybox date lacks GNU -d; parse ISO-8601 with python3.
LAST_TS=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$LAST_UPDATE'.replace('Z','+00:00')).timestamp()))")
AGE_MIN=$(( (NOW - LAST_TS) / 60 ))
if [ "$AGE_MIN" -gt "$THRESHOLD_MIN" ]; then
bd --db /tmp/.beads note "$ID" "reaper: no progress for $${AGE_MIN}m (threshold $${THRESHOLD_MIN}m) — blocking"
bd --db /tmp/.beads update "$ID" -s blocked
echo "REAPED $ID (stale $${AGE_MIN}m)"
else
echo "keeping $ID (age $${AGE_MIN}m < $${THRESHOLD_MIN}m)"
fi
done
EOT
]
env {
name = "BEADS_ACTOR"
value = "beads-reaper"
}
env {
name = "HOME"
value = "/tmp"
}
volume_mount {
name = "beads-metadata"
mount_path = "/etc/beads-metadata"
read_only = true
}
resources {
requests = {
cpu = "50m"
memory = "128Mi"
}
limits = {
memory = "256Mi"
}
}
}
volume {
name = "beads-metadata"
config_map {
name = kubernetes_config_map.beads_metadata.metadata[0].name
}
}
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "website" {
tier = local.tiers.aux
}
}
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" {
@ -75,10 +71,6 @@ resource "kubernetes_deployment" "blog" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "blog" {

View file

@ -14,10 +14,6 @@ resource "kubernetes_namespace" "broker_sync" {
tier = local.tiers.aux
}
}
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"]]
}
}
# Secrets for all providers. Seeded in Vault at `secret/broker-sync`:
@ -126,10 +122,6 @@ resource "kubernetes_cron_job_v1" "version_probe" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Trading212 steady-state daily sync. Phase 1 deliverable.
@ -226,10 +218,6 @@ resource "kubernetes_cron_job_v1" "trading212" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# IMAP ingest InvestEngine + Schwab email parsers, one combined pod.
@ -355,10 +343,6 @@ resource "kubernetes_cron_job_v1" "imap" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# CSV drop-folder processor Scottish Widows, Fidelity quarterly, Freetrade, etc.
@ -447,10 +431,6 @@ resource "kubernetes_cron_job_v1" "csv_drop" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Monthly HMRC FX reconciliation rewrites last-month activities with official
@ -539,10 +519,6 @@ resource "kubernetes_cron_job_v1" "fx_reconcile" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Backup: snapshot sync.db / fx.db / csv-archive into NFS daily, keep 30 days.
@ -620,10 +596,6 @@ resource "kubernetes_cron_job_v1" "backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# -----------------------------------------------------------------------------

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "changedetection" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -186,10 +182,6 @@ resource "kubernetes_deployment" "changedetection" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "changedetection" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "city-guesser" {
tier = local.tiers.aux
}
}
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" {
@ -66,10 +62,6 @@ resource "kubernetes_deployment" "city-guesser" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "city-guesser" {

View file

@ -11,7 +11,7 @@ data "vault_kv_secret_v2" "viktor_secrets" {
locals {
namespace = "claude-agent"
image = "registry.viktorbarzin.me/claude-agent-service"
image_tag = "0c24c9b6"
image_tag = "382d6b14"
labels = {
app = "claude-agent-service"
}
@ -28,10 +28,6 @@ resource "kubernetes_namespace" "claude_agent" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
# --- Secrets ---
@ -590,8 +586,4 @@ resource "kubernetes_cron_job_v1" "claude_oauth_expiry_monitor" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -20,10 +20,6 @@ resource "kubernetes_namespace" "claude-memory" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -242,8 +238,7 @@ resource "kubernetes_deployment" "claude-memory" {
lifecycle {
# DRIFT_WORKAROUND: CI pipeline owns image tag (kubectl set image from Woodpecker/GHA). Reviewed 2026-04-18.
ignore_changes = [
spec[0].template[0].spec[0].container[0].image,
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
spec[0].template[0].spec[0].container[0].image
]
}
}

View file

@ -9,10 +9,6 @@ resource "kubernetes_namespace" "cloudflared" {
tier = var.tier
}
}
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"]]
}
}
variable "tier" { type = string }
@ -93,10 +89,6 @@ resource "kubernetes_deployment" "cloudflared" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_pod_disruption_budget_v1" "cloudflared" {

View file

@ -10,10 +10,6 @@ resource "kubernetes_namespace" "cnpg_system" {
tier = var.tier
}
}
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"]]
}
}
# -----------------------------------------------------------------------------

View file

@ -54,10 +54,6 @@ resource "kubernetes_namespace" "coturn" {
tier = local.tiers.edge
}
}
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" {
@ -193,10 +189,6 @@ resource "kubernetes_deployment" "coturn" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
# LoadBalancer service with MetalLB exposes STUN/TURN signaling + relay ports

View file

@ -31,10 +31,6 @@ resource "kubernetes_namespace" "crowdsec" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "kubernetes_config_map" "crowdsec_custom_scenarios" {
@ -237,10 +233,6 @@ resource "kubernetes_deployment" "crowdsec-web" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "crowdsec-web" {
@ -366,10 +358,6 @@ resource "kubernetes_cron_job_v1" "crowdsec_blocklist_import" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Service account for the blocklist import job (needs kubectl exec permissions)

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "cyberchef" {
tier = local.tiers.aux
}
}
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" {
@ -76,10 +72,6 @@ resource "kubernetes_deployment" "cyberchef" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "cyberchef" {

View file

@ -18,10 +18,6 @@ resource "kubernetes_namespace" "dashy" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_config_map" "config" {
@ -99,10 +95,6 @@ resource "kubernetes_deployment" "dashy" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "dashy" {

View file

@ -88,7 +88,6 @@ resource "kubernetes_deployment" "dawarich" {
}
}
spec {
termination_grace_period_seconds = 60
container {
image = "freikin/dawarich:${var.image_version}"
@ -201,132 +200,81 @@ resource "kubernetes_deployment" "dawarich" {
}
}
}
container {
image = "freikin/dawarich:${var.image_version}"
name = "dawarich-sidekiq"
command = ["sidekiq-entrypoint.sh"]
args = ["bundle exec sidekiq"]
env {
name = "REDIS_URL"
value = "redis://${var.redis_host}:6379"
}
env {
name = "DATABASE_HOST"
value = var.postgresql_host
}
env {
name = "DATABASE_USERNAME"
value = "dawarich"
}
env {
name = "DATABASE_PASSWORD"
value_from {
secret_key_ref {
name = "dawarich-secrets"
key = "db_password"
}
}
}
env {
name = "DATABASE_NAME"
value = "dawarich"
}
env {
name = "MIN_MINUTES_SPENT_IN_CITY"
value = "60"
}
env {
name = "TIME_ZONE"
value = "Europe/London"
}
env {
name = "DISTANCE_UNIT"
value = "km"
}
env {
name = "BACKGROUND_PROCESSING_CONCURRENCY"
value = "2"
}
env {
name = "ENABLE_TELEMETRY"
value = "true"
}
env {
name = "APPLICATION_HOSTS"
value = "dawarich.viktorbarzin.me"
}
# Prometheus exporter disabled until a standalone `prometheus_exporter`
# server sidecar is added see follow-up bead. The client middleware
# pushes over TCP to PROMETHEUS_EXPORTER_HOST:PORT, it does not start
# a listener itself. Keeping ENABLED=false silences the reconnect
# log spam (~2/sec) from PrometheusExporter::Client.
env {
name = "PROMETHEUS_EXPORTER_ENABLED"
value = "false"
}
env {
name = "RAILS_ENV"
value = "production"
}
env {
name = "SECRET_KEY_BASE"
value_from {
secret_key_ref {
name = "dawarich-secrets"
key = "secret_key_base"
}
}
}
env {
name = "RAILS_LOG_TO_STDOUT"
value = "true"
}
env {
name = "SELF_HOSTED"
value = "true"
}
env {
name = "GEOAPIFY_API_KEY"
value_from {
secret_key_ref {
name = "dawarich-secrets"
key = "geoapify_api_key"
}
}
}
resources {
requests = {
cpu = "50m"
memory = "768Mi"
}
limits = {
memory = "1Gi"
}
}
liveness_probe {
exec {
command = ["/bin/sh", "-c", "pgrep -f 'bundle exec sidekiq' >/dev/null"]
}
initial_delay_seconds = 90
period_seconds = 30
timeout_seconds = 5
failure_threshold = 3
}
readiness_probe {
exec {
command = ["/bin/sh", "-c", "pgrep -f 'bundle exec sidekiq' >/dev/null"]
}
initial_delay_seconds = 30
period_seconds = 15
timeout_seconds = 5
}
}
# container {
# image = "freikin/dawarich:${var.image_version}"
# name = "dawarich-sidekiq"
# command = ["sidekiq-entrypoint.sh"]
# args = ["bundle exec sidekiq"]
# env {
# name = "REDIS_URL"
# value = "redis://redis.redis.svc.cluster.local:6379"
# }
# env {
# name = "DATABASE_HOST"
# value = "postgresql.dbaas"
# }
# env {
# name = "DATABASE_USERNAME"
# value = "dawarich"
# }
# env {
# name = "DATABASE_PASSWORD"
# value = data.vault_kv_secret_v2.secrets.data["db_password"]
# }
# env {
# name = "DATABASE_NAME"
# value = "dawarich"
# }
# env {
# name = "MIN_MINUTES_SPENT_IN_CITY"
# value = "60"
# }
# env {
# name = "BACKGROUND_PROCESSING_CONCURRENCY"
# value = "10"
# }
# env {
# name = "ENABLE_TELEMETRY"
# value = "true"
# }
# env {
# name = "APPLICATION_HOST"
# value = "dawarich.viktorbarzin.me"
# }
# # env {
# # name = "PROMETHEUS_EXPORTER_ENABLED"
# # value = "false"
# # }
# # env {
# # name = "PROMETHEUS_EXPORTER_HOST"
# # value = "dawarich.dawarich"
# # }
# # env {
# # name = "PHOTON_API_HOST"
# # value = "photon.dawarich:2322"
# # # value = "photon.komoot.io"
# # }
# # env {
# # name = "PHOTON_API_USE_HTTPS"
# # value = "false"
# # }
# env {
# name = "GEOAPIFY_API_KEY"
# value = data.vault_kv_secret_v2.secrets.data["geoapify_api_key"]
# }
# env {
# name = "SELF_HOSTED"
# value = "true"
# }
# # volume_mount {
# # name = "watched"
# # mount_path = "/var/app/tmp/imports/watched"
# # }
# }
}
}
}
lifecycle {
ignore_changes = [spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
}
}
@ -446,71 +394,3 @@ module "ingress" {
"gethomepage.dev/pod-selector" = ""
}
}
# Paired with DawarichIngestionStale alert in monitoring/prometheus_chart_values.tpl.
resource "kubernetes_cron_job_v1" "ingestion_freshness_monitor" {
metadata {
name = "ingestion-freshness-monitor"
namespace = kubernetes_namespace.dawarich.metadata[0].name
}
spec {
concurrency_policy = "Forbid"
failed_jobs_history_limit = 3
schedule = "30 6 * * *"
starting_deadline_seconds = 300
successful_jobs_history_limit = 1
job_template {
metadata {}
spec {
backoff_limit = 2
ttl_seconds_after_finished = 3600
template {
metadata {}
spec {
restart_policy = "OnFailure"
container {
name = "ingestion-freshness-monitor"
image = "docker.io/library/postgres:16-alpine"
env {
name = "PGPASSWORD"
value_from {
secret_key_ref {
name = "dawarich-secrets"
key = "db_password"
}
}
}
command = ["/bin/sh", "-c", <<-EOT
set -eu
apk add --no-cache curl >/dev/null 2>&1 || true
TS=$(PGPASSWORD=$PGPASSWORD psql -h ${var.postgresql_host} -U dawarich -d dawarich -t -A -c \
"SELECT COALESCE(EXTRACT(epoch FROM MAX(created_at))::bigint, 0) FROM points WHERE user_id = 1;")
NOW=$(date +%s)
if [ -z "$TS" ] || [ "$TS" = "0" ]; then
echo "ERROR: no points found for user_id=1"
exit 1
fi
AGE_H=$(( (NOW - TS) / 3600 ))
echo "last_point_ts=$TS now=$NOW age_hours=$AGE_H"
curl -sf --data-binary @- "http://prometheus-prometheus-pushgateway.monitoring:9091/metrics/job/dawarich-ingestion-freshness/user/viktor" <<METRICS
# TYPE dawarich_last_point_ingested_timestamp gauge
dawarich_last_point_ingested_timestamp $TS
# TYPE dawarich_ingestion_monitor_last_push_timestamp gauge
dawarich_ingestion_monitor_last_push_timestamp $NOW
METRICS
EOT
]
}
}
}
}
}
}
lifecycle {
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
}
}

View file

@ -37,10 +37,6 @@ resource "kubernetes_namespace" "dbaas" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
# Override Kyverno tier-1-cluster LimitRange (max 4Gi) to allow MySQL 6Gi limit
@ -87,6 +83,301 @@ module "tls_secret" {
tls_secret_name = var.tls_secret_name
}
#### MYSQL InnoDB Cluster via MySQL Operator
#
# 3 MySQL servers with Group Replication + 1 MySQL Router for auto-failover.
# Operator installed in mysql-operator namespace (toleration for control-plane).
# Init containers are slow (~20 min each) due to mysqlsh plugin loading.
resource "kubernetes_namespace" "mysql_operator" {
metadata {
name = "mysql-operator"
labels = {
tier = "1-cluster"
}
}
}
resource "helm_release" "mysql_operator" {
namespace = kubernetes_namespace.mysql_operator.metadata[0].name
create_namespace = false
name = "mysql-operator"
timeout = 300
repository = "https://mysql.github.io/mysql-operator/"
chart = "mysql-operator"
version = "2.2.7"
# NOTE: The mysql-operator chart (2.2.7) does NOT expose a resources values key.
# The resources block below is ignored by the chart. Without explicit resources
# on the deployment, the LimitRange default (256Mi) applies silently.
# Fix: kubectl patch deployment mysql-operator -n mysql-operator --type=json \
# -p='[{"op":"replace","path":"/spec/template/spec/containers/0/resources","value":{"requests":{"cpu":"100m","memory":"256Mi"},"limits":{"memory":"512Mi"}}}]'
values = [yamlencode({
resources = {
requests = {
cpu = "100m"
memory = "256Mi"
}
limits = {
memory = "512Mi"
}
}
})]
}
# The mysql-sidecar ClusterRole created by the Helm chart is missing
# namespace and CRD list/watch permissions needed by the kopf framework
# in the sidecar container. Without these, the sidecar enters degraded
# mode and never completes InnoDB cluster join operations.
resource "kubernetes_cluster_role" "mysql_sidecar_extra" {
metadata {
name = "mysql-sidecar-extra"
}
rule {
api_groups = [""]
resources = ["namespaces"]
verbs = ["list", "watch"]
}
rule {
api_groups = ["apiextensions.k8s.io"]
resources = ["customresourcedefinitions"]
verbs = ["list", "watch"]
}
}
resource "kubernetes_cluster_role_binding" "mysql_sidecar_extra" {
metadata {
name = "mysql-sidecar-extra"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.mysql_sidecar_extra.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = "mysql-cluster-sa"
namespace = kubernetes_namespace.dbaas.metadata[0].name
}
}
# ConfigMap for MySQL extra config mounted as subPath over 99-extra.cnf
# This is the only reliable way to persist innodb_doublewrite=OFF because:
# - spec.mycnf only applies on initial cluster creation
# - The operator's initconf container overwrites 99-extra.cnf on every pod start
# - SET PERSIST doesn't support innodb_doublewrite (static variable)
resource "kubernetes_config_map" "mysql_extra_cnf" {
metadata {
name = "mysql-extra-cnf"
namespace = kubernetes_namespace.dbaas.metadata[0].name
}
data = {
"99-extra.cnf" = <<-EOT
[mysqld]
innodb_doublewrite=OFF
EOT
}
}
resource "helm_release" "mysql_cluster" {
namespace = kubernetes_namespace.dbaas.metadata[0].name
create_namespace = false
name = "mysql-cluster"
timeout = 900
repository = "https://mysql.github.io/mysql-operator/"
chart = "mysql-innodbcluster"
version = "2.2.7"
values = [yamlencode({
serverInstances = 1
routerInstances = 1
serverVersion = "8.4.4"
credentials = {
root = {
user = "root"
password = var.dbaas_root_password
host = "%"
}
}
tls = {
useSelfSigned = true
}
datadirVolumeClaimTemplate = {
storageClassName = "proxmox-lvm-encrypted"
metadata = {
annotations = {
"resize.topolvm.io/threshold" = "80%"
"resize.topolvm.io/increase" = "20%"
"resize.topolvm.io/storage_limit" = "100Gi"
}
}
resources = {
requests = {
storage = "30Gi"
}
}
}
serverConfig = {
mycnf = <<-EOT
[mysqld]
skip-name-resolve
mysql-native-password=ON
# Auto-recovery after crashes: rejoin group without manual intervention
group_replication_autorejoin_tries=2016
group_replication_exit_state_action=OFFLINE_MODE
group_replication_member_expel_timeout=30
group_replication_unreachable_majority_timeout=60
group_replication_start_on_boot=ON
# Cap XCom cache to prevent unbounded growth (default 1GB causes OOM)
group_replication_message_cache_size=134217728
# Reduce log buffer (16MB sufficient for this workload, was 64MB)
innodb_log_buffer_size=16777216
# Limit connections (peak usage ~40, no need for 151)
max_connections=80
# --- Disk write reduction (HDD/LVM thin) ---
# Flush redo log once per second, not per commit. Up to 1s data loss on MySQL crash,
# but group replication provides redundancy across 3 nodes.
innodb_flush_log_at_trx_commit=0
# OS decides when to flush binlog (not per commit)
sync_binlog=0
# HDD-tuned I/O capacity (default 200/2000 is for SSD)
innodb_io_capacity=100
innodb_io_capacity_max=200
# 1GB redo log capacity larger log means less frequent checkpoint flushes
innodb_redo_log_capacity=1073741824
# 1GB buffer pool
innodb_buffer_pool_size=1073741824
# Disable doublewrite halves write amplification. Safe with group replication
# (crashed node can re-clone from healthy replica rather than relying on local recovery)
innodb_doublewrite=OFF
# Flush neighbors on HDD (coalesce adjacent dirty pages into single I/O)
innodb_flush_neighbors=1
# Reduce page cleaner aggressiveness
innodb_lru_scan_depth=256
innodb_page_cleaners=1
# Reduce adaptive flushing let dirty pages accumulate longer before background flush
innodb_adaptive_flushing_lwm=10
innodb_max_dirty_pages_pct=90
innodb_max_dirty_pages_pct_lwm=10
EOT
}
# Top-level resources apply to SIDECAR container
# VPA shows sidecar needs only 248Mi target / 334Mi upper bound
# Setting to 350Mi (was 2Gi/4Gi - 17× over-provisioned)
resources = {
requests = {
cpu = "250m"
memory = "350Mi"
}
limits = {
memory = "350Mi"
}
}
podSpec = {
affinity = {
nodeAffinity = {
requiredDuringSchedulingIgnoredDuringExecution = {
nodeSelectorTerms = [{
matchExpressions = [{
key = "kubernetes.io/hostname"
operator = "NotIn"
values = ["k8s-node1"]
}]
}]
}
}
podAntiAffinity = {
preferredDuringSchedulingIgnoredDuringExecution = [{
weight = 100
podAffinityTerm = {
labelSelector = {
matchLabels = {
"component" = "mysqld"
}
}
topologyKey = "kubernetes.io/hostname"
}
}]
}
}
# Container-specific resources for MYSQL container
# VPA shows 2.98Gi target / 5.26Gi upper bound
# Current usage ~1.8Gi peak. Reducing limit from 4Gi to 3Gi
containers = [
{
name = "mysql"
resources = {
requests = {
memory = "2Gi"
cpu = "250m"
}
limits = {
memory = "3Gi"
}
}
},
{
# MySQL operator sidecar (kopf Python control loop)
# VPA upper bound: 334Mi. Was 6Gi limit 17× over-provisioned.
name = "sidecar"
resources = {
requests = {
memory = "350Mi"
cpu = "50m"
}
limits = {
memory = "512Mi"
}
}
}
]
initContainers = [
{
name = "fixdatadir"
resources = {
requests = { memory = "64Mi", cpu = "25m" }
limits = { memory = "64Mi" }
}
},
{
name = "initconf"
resources = {
requests = { memory = "256Mi", cpu = "50m" }
limits = { memory = "256Mi" }
}
},
{
name = "initmysql"
resources = {
requests = { memory = "512Mi", cpu = "250m" }
limits = { memory = "512Mi" }
}
}
]
}
# MySQL Router - explicitly set resources (chart does not expose router.resources)
# VPA shows 100Mi upper bound, setting to 128Mi
# Note: This requires manual kubectl patch after helm release:
# kubectl patch deployment mysql-cluster-router -n dbaas --type=json -p='[
# {"op": "replace", "path": "/spec/template/spec/containers/0/resources",
# "value": {"requests": {"cpu": "25m", "memory": "128Mi"}, "limits": {"memory": "128Mi"}}}]'
# TODO: migrate to mysql-operator fork or wait for upstream router.resources support
})]
depends_on = [helm_release.mysql_operator]
}
#### MYSQL Standalone (migration target)
#
# Standalone MySQL without Group Replication. Eliminates ~95 GB/day of GR
@ -456,10 +747,6 @@ resource "kubernetes_cron_job_v1" "mysql-backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Per-database MySQL backups (enables single-database restore without affecting others)
@ -555,10 +842,6 @@ resource "kubernetes_cron_job_v1" "mysql-backup-per-db" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# resource "kubernetes_persistent_volume" "mysql" {
@ -764,10 +1047,6 @@ resource "kubernetes_deployment" "phpmyadmin" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "phpmyadmin" {
@ -1295,10 +1574,6 @@ resource "kubernetes_deployment" "pgadmin" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "pgadmin" {
metadata {
@ -1407,10 +1682,6 @@ resource "kubernetes_cron_job_v1" "postgresql-backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Per-database PostgreSQL backups (enables single-database restore without affecting others)
@ -1518,8 +1789,4 @@ resource "kubernetes_cron_job_v1" "postgresql-backup-per-db" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -7,10 +7,6 @@ resource "kubernetes_namespace" "descheduler" {
tier = local.tiers.cluster
}
}
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"]]
}
}
resource "kubernetes_cluster_role" "descheduler" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "diun" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {

View file

@ -19,10 +19,6 @@ resource "kubernetes_namespace" "ebook2audiobook" {
tier = local.tiers.gpu
}
}
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"]]
}
}
@ -119,10 +115,6 @@ resource "kubernetes_deployment" "ebook2audiobook" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
@ -321,10 +313,6 @@ resource "kubernetes_deployment" "audiblez" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
@ -411,10 +399,6 @@ resource "kubernetes_deployment" "audiblez-web" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "audiblez-web" {

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "ebooks" {
tier = local.tiers.edge
}
}
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"]]
}
}
# ExternalSecrets for all three sources

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "echo" {
tier = local.tiers.edge
}
}
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" {
@ -73,10 +69,6 @@ resource "kubernetes_deployment" "echo" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "echo" {

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "excalidraw" {
tier = local.tiers.aux
}
}
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"]]
}
}
@ -116,10 +112,6 @@ resource "kubernetes_deployment" "excalidraw" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "draw" {

View file

@ -5,10 +5,6 @@ resource "kubernetes_namespace" "external_secrets" {
tier = local.tiers.cluster
}
}
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"]]
}
}
resource "helm_release" "external_secrets" {

View file

@ -14,10 +14,6 @@ resource "kubernetes_namespace" "f1-stream" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -141,10 +137,6 @@ resource "kubernetes_deployment" "f1-stream" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "foolery" {
tier = local.tiers.aux
}
}
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" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "forgejo" {
tier = local.tiers.edge
}
}
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" {
@ -134,10 +130,6 @@ resource "kubernetes_deployment" "forgejo" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "forgejo" {

View file

@ -57,10 +57,6 @@ resource "kubernetes_namespace" "freedify" {
tier = local.tiers.aux
}
}
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" {

View file

@ -10,10 +10,6 @@ resource "kubernetes_namespace" "immich" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -188,10 +184,6 @@ resource "kubernetes_deployment" "freshrss" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "freshrss" {

View file

@ -15,10 +15,6 @@ resource "kubernetes_namespace" "frigate" {
# "istio-injection" : "enabled"
# }
}
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" {
@ -223,10 +219,6 @@ for name, det in stats.get('detectors', {}).items():
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "frigate" {

View file

@ -53,10 +53,6 @@ resource "kubernetes_namespace" "grampsweb" {
tier = local.tiers.aux
}
}
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" {
@ -326,10 +322,6 @@ resource "kubernetes_deployment" "grampsweb" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "grampsweb" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "hackmd" {
tier = local.tiers.edge
}
}
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" {
@ -164,10 +160,6 @@ resource "kubernetes_deployment" "hackmd" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "hackmd" {

View file

@ -28,10 +28,6 @@ resource "kubernetes_namespace" "headscale" {
tier = var.tier
}
}
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" {
@ -249,10 +245,6 @@ resource "kubernetes_deployment" "headscale" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "headscale" {
metadata {
@ -490,10 +482,6 @@ resource "kubernetes_cron_job_v1" "headscale_backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Grafana dashboard

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "health" {
tier = local.tiers.aux
}
}
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" {
@ -145,10 +141,6 @@ resource "kubernetes_deployment" "health" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "health" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "hermes_agent" {
tier = local.tiers.aux
}
}
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" {

View file

@ -18,10 +18,6 @@ resource "kubernetes_namespace" "homepage" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "helm_release" "homepage" {
@ -117,10 +113,6 @@ resource "kubernetes_deployment" "cache_proxy" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "cache_proxy" {

View file

@ -95,10 +95,6 @@ resource "kubernetes_deployment" "immich-frame" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -100,10 +100,6 @@ resource "kubernetes_namespace" "immich" {
tier = local.tiers.gpu
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -782,10 +778,6 @@ resource "kubernetes_cron_job_v1" "postgresql-backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# POWER TOOLS

View file

@ -188,10 +188,6 @@ resource "kubernetes_cron_job_v1" "backup-etcd" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Weekly etcd defragmentation prevents fragmentation buildup that causes slow requests
@ -246,10 +242,6 @@ resource "kubernetes_cron_job_v1" "defrag-etcd" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Clean up evicted/failed pods cluster-wide daily
@ -285,10 +277,6 @@ resource "kubernetes_cron_job_v1" "cleanup-failed-pods" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service_account" "cleanup_sa" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "insta2spotify" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {

View file

@ -8,10 +8,6 @@ resource "kubernetes_namespace" "isponsorblocktv" {
tier = local.tiers.edge
}
}
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"]]
}
}
# Before running, setup config using
# docker run --rm -it -v ./youtube:/app/data -e TERM=$TERM -e COLORTERM=$COLORTERM ghcr.io/dmunozv04/isponsorblocktv --setup
@ -91,8 +87,4 @@ resource "kubernetes_deployment" "isponsorblocktv-vermont" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "jsoncrack" {
tier = local.tiers.aux
}
}
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"
@ -56,10 +52,6 @@ resource "kubernetes_deployment" "jsoncrack" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "jsoncrack" {

View file

@ -34,10 +34,6 @@ resource "kubernetes_namespace" "k8s-dashboard" {
tier = local.tiers.cluster
}
}
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"]]
}
}
# }

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "k8s_portal" {
tier = var.tier
}
}
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" {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "kms" {
tier = local.tiers.aux
}
}
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" {
@ -96,10 +92,6 @@ resource "kubernetes_deployment" "kms-web-page" {
}
}
depends_on = [kubernetes_config_map.kms-web-page]
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "kms-web-page" {
@ -180,10 +172,6 @@ resource "kubernetes_deployment" "windows_kms" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "windows_kms" {

View file

@ -1,252 +0,0 @@
# kured Kubernetes Reboot Daemon
#
# Auto-reboots nodes when /var/run/reboot-required exists on the host (set by
# unattended-upgrades). The reboot process is gated by a custom sentinel file
# (kured-sentinel-gate DaemonSet below) so reboots only happen when:
# - all nodes Ready
# - all calico-node pods Running
# - no node has transitioned Ready in the last 30 minutes (cool-down)
#
# History:
# - 2026-03 post-mortem (memory 390): 26h cluster outage triggered by kured
# rebooting nodes while containerd's overlayfs snapshotter was corrupted.
# Remediation included the sentinel gate and a tight reboot window
# (Mon-Fri 02:00-06:00 London).
# - 2026-04-18: adopted into Terraform (Wave 5a). Previously helm-installed
# manually + kubectl-applied sentinel gate.
resource "kubernetes_namespace" "kured" {
metadata {
name = "kured"
labels = {
"istio-injection" = "disabled"
tier = local.tiers.cluster
}
}
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"]]
}
}
# -----------------------------------------------------------------------------
# kured Helm release
# -----------------------------------------------------------------------------
resource "helm_release" "kured" {
namespace = kubernetes_namespace.kured.metadata[0].name
create_namespace = false
name = "kured"
chart = "kured"
repository = "https://kubereboot.github.io/charts/"
version = "5.11.0"
values = [yamlencode({
configuration = {
period = "1h0m0s"
timeZone = "Europe/London"
startTime = "02:00"
endTime = "06:00"
rebootDays = ["mo", "tu", "we", "th", "fr"]
rebootSentinel = "/sentinel/gated-reboot-required"
notifyUrl = data.vault_kv_secret_v2.secrets.data["slack_kured_webhook"]
}
reboot_days = "mon,tue,wed,thu,fri"
window_end = "06:00"
window_start = "22:00"
service = {
annotations = {
"prometheus.io/scrape" = "true"
"prometheus.io/port" = "8080"
"prometheus.io/path" = "/metrics"
}
}
})]
}
data "vault_kv_secret_v2" "secrets" {
mount = "secret"
name = "kured"
}
# -----------------------------------------------------------------------------
# kured-sentinel-gate
#
# Runs a DaemonSet that creates /var/run/gated-reboot-required ONLY when all
# safety preconditions are met (see script). kured's rebootSentinel points at
# this file, so reboots are effectively blocked unless every check passes.
# -----------------------------------------------------------------------------
resource "kubernetes_service_account" "kured_sentinel_gate" {
metadata {
name = "kured-sentinel-gate"
namespace = kubernetes_namespace.kured.metadata[0].name
}
automount_service_account_token = false
}
resource "kubernetes_cluster_role" "kured_sentinel_gate" {
metadata {
name = "kured-sentinel-gate"
}
rule {
api_groups = [""]
resources = ["nodes"]
verbs = ["list"]
}
rule {
api_groups = [""]
resources = ["pods"]
verbs = ["list"]
}
}
resource "kubernetes_cluster_role_binding" "kured_sentinel_gate" {
metadata {
name = "kured-sentinel-gate"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.kured_sentinel_gate.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.kured_sentinel_gate.metadata[0].name
namespace = kubernetes_namespace.kured.metadata[0].name
}
}
resource "kubernetes_daemon_set_v1" "kured_sentinel_gate" {
metadata {
name = "kured-sentinel-gate"
namespace = kubernetes_namespace.kured.metadata[0].name
labels = {
app = "kured-sentinel-gate"
tier = local.tiers.cluster
}
}
spec {
selector {
match_labels = {
app = "kured-sentinel-gate"
}
}
template {
metadata {
labels = {
app = "kured-sentinel-gate"
}
}
spec {
service_account_name = kubernetes_service_account.kured_sentinel_gate.metadata[0].name
automount_service_account_token = false
enable_service_links = false
toleration {
effect = "NoSchedule"
key = "node-role.kubernetes.io/control-plane"
operator = "Equal"
}
toleration {
effect = "NoSchedule"
key = "node-role.kubernetes.io/master"
operator = "Equal"
}
container {
name = "gate"
image = "bitnami/kubectl:latest"
image_pull_policy = "Always"
command = [
"/bin/bash",
"-c",
<<-EOT
while true; do
echo "[$(date)] Checking reboot gate conditions..."
# Check 1: Does the host need a reboot?
if [ ! -f /host/var-run/reboot-required ]; then
echo " No reboot required on this host"
rm -f /host/var-run/gated-reboot-required
sleep 300
continue
fi
echo " Host has /var/run/reboot-required"
# Check 2: Are ALL nodes Ready?
NOT_READY=$(kubectl get nodes --no-headers | grep -v ' Ready' | wc -l | tr -d ' ')
if [ "$NOT_READY" -gt 0 ]; then
echo " BLOCKED: $NOT_READY node(s) not Ready"
rm -f /host/var-run/gated-reboot-required
sleep 300
continue
fi
echo " All nodes Ready"
# Check 3: Are ALL calico-node pods Running?
CALICO_NOT_RUNNING=$(kubectl get pods -n calico-system -l k8s-app=calico-node --no-headers 2>/dev/null | grep -v Running | wc -l | tr -d ' ')
if [ "$CALICO_NOT_RUNNING" -gt 0 ]; then
echo " BLOCKED: $CALICO_NOT_RUNNING calico-node pod(s) not Running"
rm -f /host/var-run/gated-reboot-required
sleep 300
continue
fi
echo " All calico-node pods Running"
# Check 4: No node rebooted in last 30 minutes (cool-down)
RECENT_REBOOT=0
while IFS= read -r transition_time; do
if [ -n "$transition_time" ]; then
transition_epoch=$(date -d "$transition_time" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$transition_time" +%s 2>/dev/null)
now_epoch=$(date +%s)
diff=$(( now_epoch - transition_epoch ))
if [ "$diff" -lt 1800 ]; then
RECENT_REBOOT=1
break
fi
fi
done < <(kubectl get nodes -o jsonpath='{range .items[*]}{range .status.conditions[?(@.type=="Ready")]}{.lastTransitionTime}{"\n"}{end}{end}')
if [ "$RECENT_REBOOT" -eq 1 ]; then
echo " BLOCKED: A node transitioned Ready within the last 30 minutes (cool-down)"
rm -f /host/var-run/gated-reboot-required
sleep 300
continue
fi
echo " No recent node reboots (30m cool-down clear)"
# All checks passed create gated sentinel
echo " ALL CHECKS PASSED — creating /var/run/gated-reboot-required"
touch /host/var-run/gated-reboot-required
sleep 300
done
EOT
]
resources {
requests = {
cpu = "10m"
memory = "32Mi"
}
limits = {
memory = "64Mi"
}
}
volume_mount {
name = "var-run"
mount_path = "/host/var-run"
}
}
volume {
name = "var-run"
host_path {
path = "/var/run"
type = "Directory"
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -1 +0,0 @@
../../secrets

View file

@ -1,8 +0,0 @@
include "root" {
path = find_in_parent_folders()
}
dependency "platform" {
config_path = "../platform"
skip_outputs = true
}

View file

@ -6,10 +6,6 @@ resource "kubernetes_namespace" "kyverno" {
"istio-injection" : "disabled"
}
}
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"]]
}
}
resource "helm_release" "kyverno" {

View file

@ -21,10 +21,6 @@ resource "kubernetes_namespace" "linkwarden" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -201,10 +197,6 @@ resource "kubernetes_deployment" "linkwarden" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "linkwarden" {
metadata {

View file

@ -1,198 +0,0 @@
# local-path-provisioner
#
# Rancher's local-path provisioner backs PVCs with node-local
# /opt/local-path-provisioner directories. Currently serves as the default
# StorageClass. Deployed via raw kubectl apply 55d ago; adopted into TF
# (Wave 5c) on 2026-04-18.
#
# Upstream: https://github.com/rancher/local-path-provisioner
# Version pinned to rancher/local-path-provisioner:v0.0.31
resource "kubernetes_namespace" "local_path_storage" {
metadata {
name = "local-path-storage"
}
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"]]
}
}
resource "kubernetes_service_account" "local_path_provisioner" {
metadata {
name = "local-path-provisioner-service-account"
namespace = kubernetes_namespace.local_path_storage.metadata[0].name
}
automount_service_account_token = false
}
resource "kubernetes_cluster_role" "local_path_provisioner" {
metadata {
name = "local-path-provisioner-role"
}
rule {
api_groups = [""]
resources = ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"]
verbs = ["get", "list", "watch"]
}
rule {
api_groups = [""]
resources = ["persistentvolumes"]
verbs = ["get", "list", "watch", "create", "patch", "update", "delete"]
}
rule {
api_groups = [""]
resources = ["events"]
verbs = ["create", "patch"]
}
rule {
api_groups = ["storage.k8s.io"]
resources = ["storageclasses"]
verbs = ["get", "list", "watch"]
}
}
resource "kubernetes_cluster_role_binding" "local_path_provisioner" {
metadata {
name = "local-path-provisioner-bind"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.local_path_provisioner.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.local_path_provisioner.metadata[0].name
namespace = kubernetes_namespace.local_path_storage.metadata[0].name
}
}
resource "kubernetes_config_map" "local_path_config" {
metadata {
name = "local-path-config"
namespace = kubernetes_namespace.local_path_storage.metadata[0].name
}
data = {
"config.json" = jsonencode({
nodePathMap = [{
node = "DEFAULT_PATH_FOR_NON_LISTED_NODES"
paths = ["/opt/local-path-provisioner"]
}]
})
"helperPod.yaml" = <<-EOT
apiVersion: v1
kind: Pod
metadata:
name: helper-pod
spec:
priorityClassName: system-node-critical
tolerations:
- key: node.kubernetes.io/disk-pressure
operator: Exists
effect: NoSchedule
containers:
- name: helper-pod
image: busybox
imagePullPolicy: IfNotPresent
EOT
"setup" = <<-EOT
#!/bin/sh
set -eu
mkdir -m 0777 -p "$VOL_DIR"
EOT
"teardown" = <<-EOT
#!/bin/sh
set -eu
rm -rf "$VOL_DIR"
EOT
}
}
resource "kubernetes_storage_class_v1" "local_path" {
metadata {
name = "local-path"
annotations = {
"storageclass.kubernetes.io/is-default-class" = "true"
}
}
storage_provisioner = "rancher.io/local-path"
reclaim_policy = "Delete"
volume_binding_mode = "WaitForFirstConsumer"
allow_volume_expansion = false
}
resource "kubernetes_deployment" "local_path_provisioner" {
metadata {
name = "local-path-provisioner"
namespace = kubernetes_namespace.local_path_storage.metadata[0].name
labels = {
tier = "default"
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "local-path-provisioner"
}
}
template {
metadata {
labels = {
app = "local-path-provisioner"
}
}
spec {
service_account_name = kubernetes_service_account.local_path_provisioner.metadata[0].name
automount_service_account_token = false
enable_service_links = false
container {
name = "local-path-provisioner"
image = "rancher/local-path-provisioner:v0.0.31"
image_pull_policy = "IfNotPresent"
command = [
"local-path-provisioner",
"--debug",
"start",
"--config",
"/etc/config/config.json",
]
env {
name = "POD_NAMESPACE"
value_from {
field_ref {
field_path = "metadata.namespace"
}
}
}
env {
name = "CONFIG_MOUNT_PATH"
value = "/etc/config/"
}
volume_mount {
name = "config-volume"
mount_path = "/etc/config/"
}
}
volume {
name = "config-volume"
config_map {
name = kubernetes_config_map.local_path_config.metadata[0].name
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -1 +0,0 @@
../../secrets

View file

@ -1,8 +0,0 @@
include "root" {
path = find_in_parent_folders()
}
dependency "platform" {
config_path = "../platform"
skip_outputs = true
}

View file

@ -14,26 +14,6 @@ variable "email_monitor_imap_password" {
sensitive = true
}
# Build the virtual-alias map, dropping aliases where BOTH the source and
# target are real mailboxes in var.mailserver_accounts (and are different).
# Without this filter, docker-mailserver emits two passwd-file userdb lines
# for the source address its own mailbox home plus the alias target's home
# and Dovecot logs 'exists more than once' on every auth lookup. Aliases
# that forward to external addresses (gmail etc.) or to self are safe.
locals {
_account_set = keys(var.mailserver_accounts)
_virtual_lines = split("\n", format("%s%s", var.postfix_account_aliases, file("${path.module}/extra/aliases.txt")))
postfix_virtual = join("\n", [
for line in local._virtual_lines : line
if !(
length(split(" ", line)) == 2 &&
contains(local._account_set, split(" ", line)[0]) &&
contains(local._account_set, split(" ", line)[1]) &&
split(" ", line)[0] != split(" ", line)[1]
)
])
}
resource "kubernetes_namespace" "mailserver" {
metadata {
name = "mailserver"
@ -45,10 +25,6 @@ resource "kubernetes_namespace" "mailserver" {
# "istio-injection" : "enabled"
# }
}
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" {
@ -117,7 +93,7 @@ resource "kubernetes_config_map" "mailserver_config" {
# Actual mail settings
"postfix-accounts.cf" = join("\n", [for user, pass in var.mailserver_accounts : "${user}|${bcrypt(pass, 6)}"])
"postfix-main.cf" = var.postfix_cf
"postfix-virtual.cf" = local.postfix_virtual
"postfix-virtual.cf" = format("%s%s", var.postfix_account_aliases, file("${path.module}/extra/aliases.txt"))
KeyTable = "mail._domainkey.viktorbarzin.me viktorbarzin.me:mail:/etc/opendkim/keys/viktorbarzin.me-mail.key\n"
SigningTable = "*@viktorbarzin.me mail._domainkey.viktorbarzin.me\n"
@ -622,15 +598,15 @@ try:
resp.raise_for_status()
print(f"Sent test email via Brevo: {resp.status_code} marker={marker}")
# Step 2: Wait for delivery, retry IMAP up to 5 min (15 x 20s)
# Step 2: Wait for delivery, retry IMAP up to 3 min
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
found = False
for attempt in range(15):
for attempt in range(9):
time.sleep(20)
try:
imap = imaplib.IMAP4_SSL(IMAP_HOST, 993, ssl_context=ctx, timeout=10)
imap = imaplib.IMAP4_SSL(IMAP_HOST, 993, ssl_context=ctx)
imap.login(IMAP_USER, IMAP_PASS)
imap.select("INBOX")
_, msg_ids = imap.search(None, "SUBJECT", marker)
@ -724,9 +700,5 @@ sys.exit(0 if success else 1)
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -231,10 +231,6 @@ resource "kubernetes_deployment" "roundcubemail" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "roundcubemail" {

View file

@ -23,18 +23,6 @@ smtpd_tls_loglevel = 1
smtpd_client_connection_rate_limit = 10
smtpd_client_message_rate_limit = 30
anvil_rate_time_unit = 60s
# Disable the postscreen decision cache. The default (btree) driver
# requires an exclusive file lock for every access, and with postscreen
# re-spawning per connection (master.cf: maxproc=1) that produces thousands
# of 'unable to get exclusive lock' fatals per day stalling SMTP
# acceptance and starving inbound delivery. lmdb would avoid the lock but
# isn't compiled into docker-mailserver 15.0.0's Postfix build
# (postconf -m no lmdb). Proxy:btree is unsafe because postscreen does
# its own locking. An empty value disables the cache entirely legitimate
# clients pay the greet/bare-newline re-check on every new TCP session,
# which is trivial at our volume (~100 deliveries/day).
postscreen_cache_map =
EOT
}

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "matrix" {
tier = local.tiers.aux
}
}
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"]]
}
}
# DB credentials from Vault database engine (rotated every 24h)
@ -196,10 +192,6 @@ resource "kubernetes_deployment" "matrix" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "matrix" {

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "meshcentral" {
tier = local.tiers.aux
}
}
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" {
@ -235,10 +231,6 @@ EOT
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
@ -265,14 +257,14 @@ resource "kubernetes_service" "meshcentral" {
}
module "ingress" {
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.meshcentral.metadata[0].name
name = "meshcentral"
tls_secret_name = var.tls_secret_name
port = 80
protected = true
anti_ai_scraping = false
source = "../../modules/kubernetes/ingress_factory"
dns_type = "proxied"
namespace = kubernetes_namespace.meshcentral.metadata[0].name
name = "meshcentral"
tls_secret_name = var.tls_secret_name
port = 80
protected = true
anti_ai_scraping = false
extra_annotations = {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = "MeshCentral"

View file

@ -7,10 +7,6 @@ resource "kubernetes_namespace" "metallb" {
app = "metallb"
}
}
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"]]
}
}
resource "helm_release" "metallb" {

View file

@ -8,10 +8,6 @@ resource "kubernetes_namespace" "metrics-server" {
tier = var.tier
}
}
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" {

View file

@ -50,10 +50,6 @@ resource "kubernetes_deployment" "goflow2" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "goflow2" {

View file

@ -91,10 +91,6 @@ resource "kubernetes_deployment" "idrac-redfish" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "idrac-redfish-exporter" {

View file

@ -100,10 +100,6 @@ resource "kubernetes_daemon_set_v1" "sysctl-inotify" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
*/

View file

@ -39,10 +39,6 @@ resource "kubernetes_namespace" "monitoring" {
"resource-governance/custom-quota" = "true"
}
}
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" {
@ -92,10 +88,6 @@ resource "kubernetes_cron_job_v1" "monitor_prom" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# -----------------------------------------------------------------------------
@ -219,10 +211,6 @@ resource "kubernetes_cron_job_v1" "dns_anomaly_monitor" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Expose Pushgateway via NodePort so the PVE host can push LVM snapshot metrics

View file

@ -1787,30 +1787,6 @@ serverFiles:
severity: warning
annotations:
summary: "Privatebin has no available replicas"
- alert: DawarichIngestionStale
expr: (time() - dawarich_last_point_ingested_timestamp{user="viktor"}) > 172800
for: 15m
labels:
severity: warning
annotations:
summary: "Dawarich: no points from viktor in >2 days"
description: "The iOS Dawarich app likely stopped sending location points. Open the app, verify it's running, and check background location permissions. Server-side is healthy when this alert fires — the issue is client-side."
- alert: DawarichIngestionMonitorStale
expr: (time() - dawarich_ingestion_monitor_last_push_timestamp{user="viktor"}) > 129600
for: 15m
labels:
severity: warning
annotations:
summary: "Dawarich ingestion freshness monitor hasn't pushed in >36h"
description: "CronJob ingestion-freshness-monitor in dawarich ns isn't running or failing. Check `kubectl -n dawarich get cronjob ingestion-freshness-monitor` and recent Job logs."
- alert: DawarichIngestionMonitorNeverRun
expr: absent(dawarich_ingestion_monitor_last_push_timestamp{user="viktor"})
for: 2h
labels:
severity: warning
annotations:
summary: "Dawarich ingestion freshness monitor has never pushed"
description: "Expected `dawarich_ingestion_monitor_last_push_timestamp` to appear once the daily CronJob runs. Check the CronJob in dawarich namespace."
- name: "Network Traffic (GoFlow2)"
rules:
- alert: GoFlow2Down
@ -1963,38 +1939,6 @@ serverFiles:
severity: warning
annotations:
summary: "Authentik outpost restarted {{ $value | printf \"%.0f\" }} times in 30m — check for OOM or crash loop"
- name: Infrastructure Drift
# Metrics pushed by .woodpecker/drift-detection.yml after each cron run.
# See Wave 7 of the state-drift consolidation plan.
rules:
- alert: DriftDetectionStale
# Drift detection pipeline hasn't reported in 26h. Either the cron
# didn't fire, or the job is failing before the push step.
expr: time() - max(drift_detection_last_run_timestamp) > 26 * 3600
for: 30m
labels:
severity: warning
annotations:
summary: "Drift detection hasn't reported in {{ $value | humanizeDuration }} — check Woodpecker pipeline 'drift-detection'"
- alert: DriftUnaddressed
# Any stack drifted for >72h without being reconciled. Either apply
# to bring config in line, or update HCL to match desired state.
expr: max(drift_stack_age_hours) > 72
for: 1h
labels:
severity: warning
annotations:
summary: "A stack has been drifted for {{ $value | printf \"%.0f\" }}h — run scripts/tg plan across stacks to identify and reconcile"
- alert: DriftStacksMany
# More than 10 stacks drifting simultaneously usually means a
# systemic issue (cluster upgrade, new admission controller,
# provider version bump) rather than individual misconfigurations.
expr: drift_stack_count > 10
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $value | printf \"%.0f\" }} stacks drifting — likely a systemic cause (new admission webhook, provider upgrade). Check the most recent drift-detection run in Woodpecker."
extraScrapeConfigs: |
- job_name: 'proxmox-host'

View file

@ -86,10 +86,6 @@ resource "kubernetes_deployment" "pve_exporter" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "proxmox-exporter" {

View file

@ -90,10 +90,6 @@ resource "kubernetes_deployment" "snmp-exporter" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "snmp-exporter" {

View file

@ -18,10 +18,6 @@ resource "kubernetes_namespace" "n8n" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -281,10 +277,6 @@ resource "kubernetes_deployment" "n8n" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "n8n" {

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "navidrome" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -202,10 +198,6 @@ resource "kubernetes_deployment" "navidrome" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "navidrome" {

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "netbox" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -200,10 +196,6 @@ resource "kubernetes_deployment" "netbox" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "netbox" {
metadata {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "networking-toolbox" {
tier = local.tiers.aux
}
}
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" {
@ -70,10 +66,6 @@ resource "kubernetes_deployment" "networking-toolbox" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "networking-toolbox" {

View file

@ -32,10 +32,6 @@ resource "kubernetes_namespace" "nextcloud" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -467,10 +463,6 @@ resource "kubernetes_cron_job_v1" "nextcloud_watchdog" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_cron_job_v1" "nextcloud-backup" {
@ -541,8 +533,4 @@ resource "kubernetes_cron_job_v1" "nextcloud-backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -8,10 +8,6 @@ resource "kubernetes_namespace" "nfs_csi" {
tier = var.tier
}
}
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"]]
}
}
resource "helm_release" "nfs_csi_driver" {

View file

@ -38,10 +38,6 @@ resource "kubernetes_namespace" "novelapp" {
tier = local.tiers.aux
}
}
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" {
@ -87,7 +83,6 @@ resource "kubernetes_deployment" "novelapp" {
# DRIFT_WORKAROUND: CI pipeline owns image tag (kubectl set image from Woodpecker/GHA). Reviewed 2026-04-18.
ignore_changes = [
spec[0].template[0].spec[0].container[0].image,
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
]
}
spec {

View file

@ -12,10 +12,6 @@ resource "kubernetes_namespace" "ntfy" {
tier = local.tiers.aux
}
}
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" {
@ -155,10 +151,6 @@ resource "kubernetes_deployment" "ntfy" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "ntfy" {

View file

@ -13,14 +13,10 @@ resource "kubernetes_namespace" "nvidia" {
labels = {
"istio-injection" : "disabled"
tier = var.tier
"resource-governance/custom-quota" = "true"
"resource-governance/custom-quota" = "true"
"resource-governance/custom-limitrange" = "true"
}
}
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"]]
}
}
# Custom LimitRange overrides Kyverno tier-2-gpu default (1Gi per container)
@ -181,10 +177,6 @@ resource "kubernetes_deployment" "nvidia-exporter" {
}
}
depends_on = [helm_release.nvidia-gpu-operator]
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "nvidia-exporter" {

View file

@ -16,10 +16,6 @@ resource "kubernetes_namespace" "onlyoffice" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -224,10 +220,6 @@ resource "kubernetes_deployment" "onlyoffice-document-server" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "onlyoffice" {

View file

@ -23,10 +23,6 @@ resource "kubernetes_namespace" "openclaw" {
"resource-governance/custom-quota" = "true"
}
}
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" {
@ -606,10 +602,6 @@ resource "kubernetes_deployment" "openclaw" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "openclaw" {
@ -811,10 +803,6 @@ resource "kubernetes_deployment" "task_webhook" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "task_webhook" {
@ -952,10 +940,6 @@ resource "kubernetes_cron_job_v1" "cluster_healthcheck" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# --- CronJob: Task processor polls Forgejo issues and triggers OpenClaw ---
@ -1048,10 +1032,6 @@ resource "kubernetes_cron_job_v1" "task_processor" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# --- OpenLobster: Multi-user Telegram AI assistant (trial) ---

View file

@ -14,10 +14,6 @@ resource "kubernetes_namespace" "osm-routing" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "kubernetes_resource_quota_v1" "osm_routing" {
@ -112,10 +108,6 @@ resource "kubernetes_deployment" "osrm-foot" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "osrm-foot" {
@ -197,10 +189,6 @@ resource "kubernetes_deployment" "osrm-bicycle" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "osrm-bicycle" {
@ -286,10 +274,6 @@ resource "kubernetes_deployment" "otp" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "otp" {

View file

@ -52,10 +52,6 @@ resource "kubernetes_namespace" "owntracks" {
tier = local.tiers.aux
}
}
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" {
@ -181,10 +177,6 @@ resource "kubernetes_deployment" "owntracks" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -26,10 +26,6 @@ resource "kubernetes_namespace" "paperless-ngx" {
# "istio-injection" : "enabled"
# }
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -202,10 +198,6 @@ resource "kubernetes_deployment" "paperless-ngx" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "paperless-ngx" {

View file

@ -22,10 +22,6 @@ resource "kubernetes_namespace" "payslip_ingest" {
"istio-injection" = "disabled"
}
}
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"]]
}
}
# App secrets sourced from multiple Vault KV keys.

View file

@ -20,10 +20,6 @@ resource "kubernetes_namespace" "phpipam" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -372,10 +368,6 @@ resource "kubernetes_cron_job_v1" "phpipam_dns_sync" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# CronJob: Import devices from pfSense (Kea DHCP leases + ARP table) into phpIPAM
@ -572,10 +564,6 @@ PYEOF
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# CronJob: Import devices from remote sites (London + Valchedrym) via SSH
@ -736,8 +724,4 @@ PYEOF
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "plotting-book" {
tier = local.tiers.aux
}
}
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"]]
}
}
resource "kubernetes_manifest" "external_secret" {
@ -87,7 +83,6 @@ resource "kubernetes_deployment" "plotting-book" {
# DRIFT_WORKAROUND: CI pipeline owns image tag (kubectl set image from Woodpecker/GHA). Reviewed 2026-04-18.
ignore_changes = [
spec[0].template[0].spec[0].container[0].image,
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
]
}
spec {
@ -313,10 +308,6 @@ resource "kubernetes_cron_job_v1" "plotting_book_backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}
# Sealed Secrets encrypted secrets safe to commit to git

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "poison_fountain" {
tier = local.tiers.cluster
}
}
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" {
@ -178,10 +174,6 @@ resource "kubernetes_deployment" "poison_fountain" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
# Internal service (for ForwardAuth from Traefik)
@ -301,8 +293,4 @@ resource "kubernetes_cron_job_v1" "poison_fetcher" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -11,10 +11,6 @@ resource "kubernetes_namespace" "priority-pass" {
tier = local.tiers.aux
}
}
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" {

View file

@ -13,10 +13,6 @@ resource "kubernetes_namespace" "privatebin" {
tier = local.tiers.edge
}
}
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" {
@ -105,10 +101,6 @@ resource "kubernetes_deployment" "privatebin" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "privatebin" {

View file

@ -6,10 +6,6 @@ resource "kubernetes_namespace" "proxmox_csi" {
"resource-governance/custom-quota" = "true"
}
}
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"]]
}
}
resource "helm_release" "proxmox_csi" {

View file

@ -7,10 +7,6 @@ resource "kubernetes_namespace" "pvc_autoresizer" {
tier = var.tier
}
}
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"]]
}
}
resource "helm_release" "pvc_autoresizer" {

View file

@ -90,10 +90,6 @@ resource "kubernetes_namespace" "realestate-crawler" {
tier = local.tiers.aux
}
}
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" {
@ -158,8 +154,7 @@ resource "kubernetes_deployment" "realestate-crawler-ui" {
}
lifecycle {
ignore_changes = [
spec[0].template[0].spec[0].container[0].image,
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
spec[0].template[0].spec[0].container[0].image
]
}
}
@ -305,8 +300,7 @@ resource "kubernetes_deployment" "realestate-crawler-api" {
}
lifecycle {
ignore_changes = [
spec[0].template[0].spec[0].container[0].image,
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
spec[0].template[0].spec[0].container[0].image
]
}
}
@ -469,10 +463,6 @@ resource "kubernetes_deployment" "realestate-crawler-celery" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
resource "kubernetes_service" "realestate-crawler-celery-metrics" {
@ -580,8 +570,4 @@ resource "kubernetes_deployment" "realestate-crawler-celery-beat" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}

View file

@ -9,10 +9,6 @@ resource "kubernetes_namespace" "redis" {
tier = var.tier
}
}
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" {
@ -240,10 +236,6 @@ resource "kubernetes_deployment" "haproxy" {
}
depends_on = [helm_release.redis]
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].template[0].spec[0].dns_config]
}
}
# Dedicated service for HAProxy master-only routing.
@ -376,8 +368,4 @@ resource "kubernetes_cron_job_v1" "redis-backup" {
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
}

Some files were not shown because too many files have changed in this diff Show more