From 7501ea286bb569f2e1b7daa8800aa8d277bd3824 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Mon, 8 Jun 2026 09:26:21 +0000 Subject: [PATCH] tripit: wire planner subsystem (merged trip-planner) secrets + Slack webhook ingress - ExternalSecret gains SLACK_SIGNING_SECRET / TREK_USER / TREK_PASSWORD / CLAUDE_AGENT_TOKEN (SLACK_BOT_TOKEN reused from nudges). - New auth=none ingress carve-out /api/planner/slack (Slack v0 signature-gated, same pattern as the calendar + emails-confirm carve-outs). - Remove the superseded standalone stacks/trip-planner (merged into tripit per the "future travel logic goes in tripit" policy). Co-Authored-By: Claude Opus 4.8 --- stacks/trip-planner/main.tf | 275 ----------------------------- stacks/trip-planner/secrets | 1 - stacks/trip-planner/terragrunt.hcl | 23 --- stacks/tripit/main.tf | 25 +++ 4 files changed, 25 insertions(+), 299 deletions(-) delete mode 100644 stacks/trip-planner/main.tf delete mode 120000 stacks/trip-planner/secrets delete mode 100644 stacks/trip-planner/terragrunt.hcl diff --git a/stacks/trip-planner/main.tf b/stacks/trip-planner/main.tf deleted file mode 100644 index db283013..00000000 --- a/stacks/trip-planner/main.tf +++ /dev/null @@ -1,275 +0,0 @@ -variable "image_tag" { - type = string - default = "latest" - description = "trip-planner image tag. Use 8-char git SHA in CI." -} - -variable "postgresql_host" { type = string } - -variable "tls_secret_name" { - type = string - sensitive = true -} - -locals { - namespace = "trip-planner" - image = "forgejo.viktorbarzin.me/viktor/trip-planner:${var.image_tag}" - labels = { - app = "trip-planner" - } -} - -resource "kubernetes_namespace" "trip_planner" { - metadata { - name = local.namespace - labels = { - tier = local.tiers.aux - "istio-injection" = "disabled" - # Opt into Keel auto-update (inject-keel-annotations ClusterPolicy). - "keel.sh/enrolled" = "true" - } - } - lifecycle { - # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label. - ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]] - } -} - -# App secrets — seed these in Vault before applying: -# secret/trip-planner -# SLACK_BOT_TOKEN — Slack bot OAuth token (xoxb-…) -# SLACK_SIGNING_SECRET — Slack app signing secret (v0 HMAC) for request verification -# TREK_API_URL — TREK instance base URL -# TREK_API_KEY — TREK API key -# -# Schema in CNPG: `trip_planner` (alembic creates tables on first migrate). -# DB user: created via Vault database engine — see static-creds/pg-trip-planner. -resource "kubernetes_manifest" "external_secret" { - manifest = { - apiVersion = "external-secrets.io/v1beta1" - kind = "ExternalSecret" - metadata = { - name = "trip-planner-secrets" - namespace = local.namespace - } - spec = { - refreshInterval = "15m" - secretStoreRef = { - name = "vault-kv" - kind = "ClusterSecretStore" - } - target = { - name = "trip-planner-secrets" - template = { - metadata = { - annotations = { - "reloader.stakater.com/match" = "true" - } - } - } - } - dataFrom = [{ - extract = { - key = "trip-planner" - } - }] - } - } - depends_on = [kubernetes_namespace.trip_planner] -} - -# DB credentials from Vault database engine (7-day rotation). -# Builds the asyncpg DSN consumed by the FastAPI app as DB_CONNECTION_STRING. -# Pre-req in dbaas: CNPG cluster has DB `trip_planner`, role `trip_planner`, -# and Vault role `static-creds/pg-trip-planner`. -resource "kubernetes_manifest" "db_external_secret" { - manifest = { - apiVersion = "external-secrets.io/v1beta1" - kind = "ExternalSecret" - metadata = { - name = "trip-planner-db-creds" - namespace = local.namespace - } - spec = { - refreshInterval = "15m" - secretStoreRef = { - name = "vault-database" - kind = "ClusterSecretStore" - } - target = { - name = "trip-planner-db-creds" - template = { - metadata = { - annotations = { - "reloader.stakater.com/match" = "true" - } - } - data = { - DB_CONNECTION_STRING = "postgresql+asyncpg://trip_planner:{{ .password }}@${var.postgresql_host}:5432/trip_planner" - DB_PASSWORD = "{{ .password }}" - } - } - } - data = [{ - secretKey = "password" - remoteRef = { - key = "static-creds/pg-trip-planner" - property = "password" - } - }] - } - } - depends_on = [kubernetes_namespace.trip_planner] -} - -resource "kubernetes_deployment" "trip_planner" { - metadata { - name = "trip-planner" - namespace = kubernetes_namespace.trip_planner.metadata[0].name - labels = merge(local.labels, { - tier = local.tiers.aux - }) - annotations = { - "reloader.stakater.com/search" = "true" - } - } - - spec { - # Single leader: Slack event deduplication and the webhook receiver want - # one writer. Recreate avoids two pods racing on the same DB session. - replicas = 1 - strategy { - type = "Recreate" - } - - selector { - match_labels = local.labels - } - - template { - metadata { - labels = local.labels - } - - spec { - image_pull_secrets { - name = "registry-credentials" - } - - init_container { - name = "migrate" - image = local.image - command = ["python", "-m", "trip_planner", "migrate"] - - env_from { - secret_ref { name = "trip-planner-secrets" } - } - env_from { - secret_ref { name = "trip-planner-db-creds" } - } - - resources { - requests = { cpu = "50m", memory = "128Mi" } - limits = { memory = "256Mi" } - } - } - - container { - name = "trip-planner" - image = local.image - - port { - container_port = 8080 - } - - env_from { - secret_ref { name = "trip-planner-secrets" } - } - env_from { - secret_ref { name = "trip-planner-db-creds" } - } - - readiness_probe { - http_get { - path = "/healthz" - port = 8080 - } - initial_delay_seconds = 5 - period_seconds = 10 - } - liveness_probe { - http_get { - path = "/healthz" - port = 8080 - } - initial_delay_seconds = 30 - period_seconds = 30 - } - - resources { - requests = { cpu = "100m", memory = "256Mi" } - limits = { memory = "256Mi" } - } - } - } - } - } - - lifecycle { - ignore_changes = [ - spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 - metadata[0].annotations["keel.sh/policy"], - metadata[0].annotations["keel.sh/trigger"], - metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2 - metadata[0].annotations["keel.sh/match-tag"], - spec[0].template[0].spec[0].container[0].image, # KEEL_IGNORE_IMAGE — Keel manages tag updates - spec[0].template[0].spec[0].init_container[0].image, - metadata[0].annotations["kubernetes.io/change-cause"], - metadata[0].annotations["deployment.kubernetes.io/revision"], - spec[0].template[0].metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1 - ] - } - - depends_on = [ - kubernetes_manifest.external_secret, - kubernetes_manifest.db_external_secret, - ] -} - -resource "kubernetes_service" "trip_planner" { - metadata { - name = "trip-planner" - namespace = kubernetes_namespace.trip_planner.metadata[0].name - labels = local.labels - } - - spec { - type = "ClusterIP" - selector = local.labels - - port { - name = "http" - port = 8080 - target_port = 8080 - } - } -} - -# Kyverno ClusterPolicy `sync-tls-secret` auto-clones the wildcard TLS -# secret into every namespace, so we don't need a setup_tls_secret module. - -# Public ingress for the Slack events webhook. All requests carry a -# Slack v0 HMAC signature (X-Slack-Signature header) that the app verifies -# before processing — Authentik forward-auth would intercept the POST and -# break Slack's delivery retries. -module "ingress" { - source = "../../modules/kubernetes/ingress_factory" - # auth = "none": Slack webhook receiver — gated by Slack v0 signature verification in-app, not Authentik - auth = "none" - anti_ai_scraping = false - dns_type = "proxied" - namespace = kubernetes_namespace.trip_planner.metadata[0].name - name = "trip-planner" - port = 8080 - tls_secret_name = var.tls_secret_name -} diff --git a/stacks/trip-planner/secrets b/stacks/trip-planner/secrets deleted file mode 120000 index ca54a7cf..00000000 --- a/stacks/trip-planner/secrets +++ /dev/null @@ -1 +0,0 @@ -../../secrets \ No newline at end of file diff --git a/stacks/trip-planner/terragrunt.hcl b/stacks/trip-planner/terragrunt.hcl deleted file mode 100644 index 9b6a537d..00000000 --- a/stacks/trip-planner/terragrunt.hcl +++ /dev/null @@ -1,23 +0,0 @@ -include "root" { - path = find_in_parent_folders() -} - -dependency "platform" { - config_path = "../platform" - skip_outputs = true -} - -dependency "vault" { - config_path = "../vault" - skip_outputs = true -} - -dependency "external-secrets" { - config_path = "../external-secrets" - skip_outputs = true -} - -inputs = { - # Override per-deploy in CI / commit. - image_tag = "latest" -} diff --git a/stacks/tripit/main.tf b/stacks/tripit/main.tf index 3029617f..197a80d0 100644 --- a/stacks/tripit/main.tf +++ b/stacks/tripit/main.tf @@ -143,6 +143,12 @@ resource "kubernetes_manifest" "external_secret" { { secretKey = "AERODATABOX_API_KEY", remoteRef = { key = "tripit", property = "AERODATABOX_API_KEY" } }, # UK rail status — Realtime Trains (data.rtt.io) long-life refresh token. { secretKey = "RTT_API_TOKEN", remoteRef = { key = "tripit", property = "RTT_API_TOKEN" } }, + # Planner subsystem (merged trip-planner): Slack v0-signature secret + TREK + # creds + claude-agent token. SLACK_BOT_TOKEN above is reused (nudges + planner). + { secretKey = "SLACK_SIGNING_SECRET", remoteRef = { key = "tripit", property = "SLACK_SIGNING_SECRET" } }, + { secretKey = "TREK_USER", remoteRef = { key = "tripit", property = "TREK_USER" } }, + { secretKey = "TREK_PASSWORD", remoteRef = { key = "tripit", property = "TREK_PASSWORD" } }, + { secretKey = "CLAUDE_AGENT_TOKEN", remoteRef = { key = "tripit", property = "CLAUDE_AGENT_TOKEN" } }, ] } } @@ -676,3 +682,22 @@ module "ingress_emails_confirm" { port = 8080 tls_secret_name = var.tls_secret_name } + +# Planner Slack webhook carve-out: POST /api/planner/slack/{events,interactions,commands} +# is gated by Slack v0 HMAC signature verification (SLACK_SIGNING_SECRET) in-app, not +# Authentik — Slack posts events server-to-server and can't do the forward-auth dance. +module "ingress_planner_slack" { + source = "../../modules/kubernetes/ingress_factory" + # auth = "none": Slack Events/Interactivity webhooks are gated by Slack v0 + # signature verification in-app (SLACK_SIGNING_SECRET), not Authentik. + auth = "none" + anti_ai_scraping = false + dns_type = "none" # main `module.ingress` owns the DNS record for this host + namespace = kubernetes_namespace.tripit.metadata[0].name + name = "tripit-planner-slack" + service_name = "tripit" + full_host = "tripit.viktorbarzin.me" + ingress_path = ["/api/planner/slack"] + port = 8080 + tls_secret_name = var.tls_secret_name +}