Compare commits
22 commits
43b4e1d372
...
eee694c915
| Author | SHA1 | Date | |
|---|---|---|---|
| eee694c915 | |||
|
|
b28c76e371 | ||
|
|
124a756351 | ||
|
|
1a7f68fe5b | ||
|
|
01955916b2 | ||
|
|
10fd88aec5 | ||
|
|
9e5d7cd825 | ||
|
|
402fd1fbac | ||
|
|
345ba2182f | ||
|
|
e2516b07a3 | ||
|
|
01a718e17b | ||
|
|
327ce215b9 | ||
|
|
8b43692af0 | ||
|
|
e612baac15 | ||
|
|
8a99be1194 | ||
|
|
2b8bb849c0 | ||
|
|
8d94688dde | ||
|
|
f79e3c563e | ||
|
|
6cf3575ed9 | ||
|
|
30fa411bf7 | ||
|
|
61e94c21fe | ||
|
|
c75beaac6c |
140 changed files with 5870 additions and 4189 deletions
|
|
@ -48,6 +48,7 @@ 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.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
---
|
||||
name: payslip-extractor
|
||||
description: "Extract structured UK payslip fields from a base64-encoded PDF into strict JSON."
|
||||
description: "Extract structured UK payslip fields from already-extracted text (preferred) or a base64 PDF (fallback) into strict JSON."
|
||||
model: haiku
|
||||
allowedTools:
|
||||
- Bash
|
||||
- Read
|
||||
---
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Your single job
|
||||
|
||||
Given a prompt that contains:
|
||||
- A line of the form `PDF_BASE64: <base64-blob>`
|
||||
- A JSON schema describing the target fields
|
||||
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).
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -42,10 +42,15 @@ 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")
|
||||
|
|
@ -56,12 +61,50 @@ steps:
|
|||
EXIT=$?
|
||||
|
||||
case $EXIT in
|
||||
0) echo "OK (no changes)"; CLEAN=$((CLEAN + 1)) ;;
|
||||
1) echo "ERROR"; ERRORS="$ERRORS $stack" ;;
|
||||
2) echo "DRIFT DETECTED"; DRIFTED="$DRIFTED $stack" ;;
|
||||
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"
|
||||
;;
|
||||
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"
|
||||
|
|
|
|||
43
AGENTS.md
43
AGENTS.md
|
|
@ -15,6 +15,49 @@
|
|||
- **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)
|
||||
|
|
|
|||
185
docs/runbooks/beads-auto-dispatch.md
Normal file
185
docs/runbooks/beads-auto-dispatch.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# 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>'
|
||||
```
|
||||
|
|
@ -18,4 +18,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ 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" {
|
||||
|
|
@ -214,6 +218,10 @@ 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" {
|
||||
|
|
@ -304,4 +312,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ 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" {
|
||||
|
|
@ -319,6 +323,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ 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 ---
|
||||
|
|
|
|||
|
|
@ -3,10 +3,25 @@ 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 = "latest"
|
||||
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
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace" "beads" {
|
||||
|
|
@ -16,6 +31,10 @@ 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" {
|
||||
|
|
@ -668,3 +687,274 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -71,6 +75,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ 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`:
|
||||
|
|
@ -122,6 +126,10 @@ 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.
|
||||
|
|
@ -218,6 +226,10 @@ 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.
|
||||
|
|
@ -343,6 +355,10 @@ 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.
|
||||
|
|
@ -431,6 +447,10 @@ 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
|
||||
|
|
@ -519,6 +539,10 @@ 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.
|
||||
|
|
@ -596,6 +620,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
@ -182,6 +186,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -62,6 +66,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ data "vault_kv_secret_v2" "viktor_secrets" {
|
|||
locals {
|
||||
namespace = "claude-agent"
|
||||
image = "registry.viktorbarzin.me/claude-agent-service"
|
||||
image_tag = "382d6b14"
|
||||
image_tag = "0c24c9b6"
|
||||
labels = {
|
||||
app = "claude-agent-service"
|
||||
}
|
||||
|
|
@ -28,6 +28,10 @@ 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 ---
|
||||
|
|
@ -586,4 +590,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ 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" {
|
||||
|
|
@ -238,7 +242,8 @@ 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].container[0].image,
|
||||
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ 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 }
|
||||
|
||||
|
|
@ -89,6 +93,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ 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"]]
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ 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" {
|
||||
|
|
@ -189,6 +193,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ 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" {
|
||||
|
|
@ -233,6 +237,10 @@ 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" {
|
||||
|
|
@ -358,6 +366,10 @@ 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)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
@ -72,6 +76,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ 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" {
|
||||
|
|
@ -95,6 +99,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ resource "kubernetes_deployment" "dawarich" {
|
|||
}
|
||||
}
|
||||
spec {
|
||||
termination_grace_period_seconds = 60
|
||||
|
||||
container {
|
||||
image = "freikin/dawarich:${var.image_version}"
|
||||
|
|
@ -200,81 +201,132 @@ 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://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"
|
||||
# # }
|
||||
# }
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -394,3 +446,71 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ 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
|
||||
|
|
@ -83,301 +87,6 @@ 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
|
||||
|
|
@ -747,6 +456,10 @@ 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)
|
||||
|
|
@ -842,6 +555,10 @@ 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" {
|
||||
|
|
@ -1047,6 +764,10 @@ 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" {
|
||||
|
|
@ -1574,6 +1295,10 @@ 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 {
|
||||
|
|
@ -1682,6 +1407,10 @@ 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)
|
||||
|
|
@ -1789,4 +1518,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ 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"]]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -115,6 +119,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -313,6 +321,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -399,6 +411,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -69,6 +73,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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"]]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -112,6 +116,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ 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" {
|
||||
|
|
@ -137,6 +141,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -130,6 +134,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ 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" {
|
||||
|
|
@ -184,6 +188,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ 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" {
|
||||
|
|
@ -219,6 +223,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ 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" {
|
||||
|
|
@ -322,6 +326,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -160,6 +164,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ 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" {
|
||||
|
|
@ -245,6 +249,10 @@ 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 {
|
||||
|
|
@ -482,6 +490,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -141,6 +145,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ 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" {
|
||||
|
|
@ -113,6 +117,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ 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" {
|
||||
|
|
@ -778,6 +782,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -188,6 +188,10 @@ 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
|
||||
|
|
@ -242,6 +246,10 @@ 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
|
||||
|
|
@ -277,6 +285,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ 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
|
||||
|
|
@ -87,4 +91,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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"
|
||||
|
|
@ -52,6 +56,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ 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"]]
|
||||
}
|
||||
}
|
||||
# }
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -92,6 +96,10 @@ 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" {
|
||||
|
|
@ -172,6 +180,10 @@ 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" {
|
||||
|
|
|
|||
252
stacks/kured/main.tf
Normal file
252
stacks/kured/main.tf
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
# 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]
|
||||
}
|
||||
}
|
||||
1
stacks/kured/secrets
Symbolic link
1
stacks/kured/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/kured/terragrunt.hcl
Normal file
8
stacks/kured/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "platform" {
|
||||
config_path = "../platform"
|
||||
skip_outputs = true
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ 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" {
|
||||
|
|
@ -197,6 +201,10 @@ 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 {
|
||||
|
|
|
|||
198
stacks/local-path/main.tf
Normal file
198
stacks/local-path/main.tf
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
stacks/local-path/secrets
Symbolic link
1
stacks/local-path/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/local-path/terragrunt.hcl
Normal file
8
stacks/local-path/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "platform" {
|
||||
config_path = "../platform"
|
||||
skip_outputs = true
|
||||
}
|
||||
|
|
@ -14,6 +14,26 @@ 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"
|
||||
|
|
@ -25,6 +45,10 @@ 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" {
|
||||
|
|
@ -93,7 +117,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" = format("%s%s", var.postfix_account_aliases, file("${path.module}/extra/aliases.txt"))
|
||||
"postfix-virtual.cf" = local.postfix_virtual
|
||||
|
||||
KeyTable = "mail._domainkey.viktorbarzin.me viktorbarzin.me:mail:/etc/opendkim/keys/viktorbarzin.me-mail.key\n"
|
||||
SigningTable = "*@viktorbarzin.me mail._domainkey.viktorbarzin.me\n"
|
||||
|
|
@ -598,15 +622,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 3 min
|
||||
# Step 2: Wait for delivery, retry IMAP up to 5 min (15 x 20s)
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
found = False
|
||||
for attempt in range(9):
|
||||
for attempt in range(15):
|
||||
time.sleep(20)
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_HOST, 993, ssl_context=ctx)
|
||||
imap = imaplib.IMAP4_SSL(IMAP_HOST, 993, ssl_context=ctx, timeout=10)
|
||||
imap.login(IMAP_USER, IMAP_PASS)
|
||||
imap.select("INBOX")
|
||||
_, msg_ids = imap.search(None, "SUBJECT", marker)
|
||||
|
|
@ -700,5 +724,9 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -231,6 +231,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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)
|
||||
|
|
@ -192,6 +196,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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" {
|
||||
|
|
@ -231,6 +235,10 @@ EOT
|
|||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
|
||||
ignore_changes = [spec[0].template[0].spec[0].dns_config]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -257,14 +265,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"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ 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]
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ 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" {
|
||||
|
|
@ -88,6 +92,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
@ -211,6 +219,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -1787,6 +1787,30 @@ 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
|
||||
|
|
@ -1939,6 +1963,38 @@ 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'
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ 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" {
|
||||
|
|
@ -277,6 +281,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
@ -198,6 +202,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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" {
|
||||
|
|
@ -196,6 +200,10 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -66,6 +70,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ 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" {
|
||||
|
|
@ -463,6 +467,10 @@ 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" {
|
||||
|
|
@ -533,4 +541,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ 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" {
|
||||
|
|
@ -83,6 +87,7 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ 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" {
|
||||
|
|
@ -151,6 +155,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ 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)
|
||||
|
|
@ -177,6 +181,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ 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" {
|
||||
|
|
@ -220,6 +224,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ 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" {
|
||||
|
|
@ -602,6 +606,10 @@ 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" {
|
||||
|
|
@ -803,6 +811,10 @@ 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" {
|
||||
|
|
@ -940,6 +952,10 @@ 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 ---
|
||||
|
|
@ -1032,6 +1048,10 @@ 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) ---
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ 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" {
|
||||
|
|
@ -108,6 +112,10 @@ 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" {
|
||||
|
|
@ -189,6 +197,10 @@ 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" {
|
||||
|
|
@ -274,6 +286,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ 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" {
|
||||
|
|
@ -177,6 +181,10 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ 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" {
|
||||
|
|
@ -198,6 +202,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ 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.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ 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" {
|
||||
|
|
@ -368,6 +372,10 @@ 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
|
||||
|
|
@ -564,6 +572,10 @@ 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
|
||||
|
|
@ -724,4 +736,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
@ -83,6 +87,7 @@ 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 {
|
||||
|
|
@ -308,6 +313,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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" {
|
||||
|
|
@ -174,6 +178,10 @@ 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)
|
||||
|
|
@ -293,4 +301,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ 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" {
|
||||
|
|
@ -101,6 +105,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ 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" {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ 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" {
|
||||
|
|
@ -154,7 +158,8 @@ resource "kubernetes_deployment" "realestate-crawler-ui" {
|
|||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
spec[0].template[0].spec[0].container[0].image
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -300,7 +305,8 @@ resource "kubernetes_deployment" "realestate-crawler-api" {
|
|||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
spec[0].template[0].spec[0].container[0].image
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -463,6 +469,10 @@ 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" {
|
||||
|
|
@ -570,4 +580,8 @@ 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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ 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" {
|
||||
|
|
@ -236,6 +240,10 @@ 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.
|
||||
|
|
@ -368,4 +376,8 @@ 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
Loading…
Add table
Add a link
Reference in a new issue