From 699c3ef74544f189785ff187d9091c8b6c6a31f8 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Sun, 5 Jul 2026 20:43:22 -0700 Subject: [PATCH 1/3] feat(foundry): add on-prem k3s cluster tree + Cloudflare Tunnel Parallel apps-foundry/ + manifests-foundry/ tree for cutover to the foundry k3s cluster. Existing EKS tree (apps/, manifests/) stays put until the on-prem stack is verified in production. - Terraform: cloudflare_zero_trust_tunnel_cloudflared (gr-foundry) + remote-managed ingress rules + 4 proxied CNAMEs for sentinel-v5, mapache, vault, argocd. random provider added. - cloudflared Deployment (2 replicas) as the tunnel connector; token bootstrapped manually from terraform output. - Sentinel/mapache/vault manifests copied verbatim from the EKS tree, diverging only in ingress.yaml (Traefik, no ALB annotations, no TLS block) and postgres.yaml (public gr-postgres.gauchoracing.com hostname instead of the internal EC2 hostname). - argocd-server-public Ingress adds argocd.gauchoracing.com to the foundry ArgoCD alongside foundry-infra's internal Ingress. - Cutover runbook embedded as comments in foundry.tf + apps-foundry/*. --- infra/environments/prod/backend.tf | 4 + infra/environments/prod/foundry.tf | 124 ++++++++++ infra/environments/prod/outputs.tf | 11 + .../apps-foundry/argocd-server-ingress.yaml | 25 ++ kubernetes/apps-foundry/cloudflared.yaml | 33 +++ kubernetes/apps-foundry/mapache.yaml | 45 ++++ kubernetes/apps-foundry/sentinel.yaml | 48 ++++ .../apps-foundry/vault-k8s-operator.yaml | 27 +++ kubernetes/apps-foundry/vault.yaml | 35 +++ kubernetes/bootstrap/root-foundry.yaml | 43 ++++ .../argocd-server-ingress/ingress.yaml | 35 +++ .../cloudflared/deployment.yaml | 81 +++++++ .../cloudflared/namespace.yaml | 4 + .../manifests-foundry/mapache/auth.yaml | 96 ++++++++ .../mapache/configmap-kerbecs.yaml | 225 ++++++++++++++++++ .../manifests-foundry/mapache/dashboard.yaml | 50 ++++ .../manifests-foundry/mapache/foreman.yaml | 76 ++++++ .../manifests-foundry/mapache/gr26.yaml | 123 ++++++++++ .../manifests-foundry/mapache/ingress.yaml | 25 ++ .../manifests-foundry/mapache/kerbecs.yaml | 80 +++++++ .../mapache/kustomization.yaml | 54 +++++ .../manifests-foundry/mapache/live.yaml | 80 +++++++ .../manifests-foundry/mapache/namespace.yaml | 4 + .../manifests-foundry/mapache/postgres.yaml | 15 ++ .../manifests-foundry/mapache/query.yaml | 99 ++++++++ .../manifests-foundry/mapache/vehicle.yaml | 76 ++++++ .../sentinel/configmap-kerbecs.yaml | 194 +++++++++++++++ .../manifests-foundry/sentinel/core.yaml | 86 +++++++ .../manifests-foundry/sentinel/discord.yaml | 90 +++++++ .../manifests-foundry/sentinel/google.yaml | 105 ++++++++ .../manifests-foundry/sentinel/ingress.yaml | 28 +++ .../manifests-foundry/sentinel/kerbecs.yaml | 79 ++++++ .../sentinel/kustomization.yaml | 47 ++++ .../manifests-foundry/sentinel/namespace.yaml | 4 + .../manifests-foundry/sentinel/oauth.yaml | 115 +++++++++ .../manifests-foundry/sentinel/postgres.yaml | 17 ++ .../manifests-foundry/sentinel/saml.yaml | 101 ++++++++ .../manifests-foundry/sentinel/web.yaml | 49 ++++ .../vault/configmap-kerbecs.yaml | 62 +++++ .../manifests-foundry/vault/ingress.yaml | 24 ++ .../manifests-foundry/vault/kerbecs.yaml | 74 ++++++ .../vault/kustomization.yaml | 19 ++ .../manifests-foundry/vault/namespace.yaml | 4 + .../manifests-foundry/vault/postgres.yaml | 11 + .../manifests-foundry/vault/server.yaml | 81 +++++++ kubernetes/manifests-foundry/vault/web.yaml | 47 ++++ 46 files changed, 2755 insertions(+) create mode 100644 infra/environments/prod/foundry.tf create mode 100644 kubernetes/apps-foundry/argocd-server-ingress.yaml create mode 100644 kubernetes/apps-foundry/cloudflared.yaml create mode 100644 kubernetes/apps-foundry/mapache.yaml create mode 100644 kubernetes/apps-foundry/sentinel.yaml create mode 100644 kubernetes/apps-foundry/vault-k8s-operator.yaml create mode 100644 kubernetes/apps-foundry/vault.yaml create mode 100644 kubernetes/bootstrap/root-foundry.yaml create mode 100644 kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml create mode 100644 kubernetes/manifests-foundry/cloudflared/deployment.yaml create mode 100644 kubernetes/manifests-foundry/cloudflared/namespace.yaml create mode 100644 kubernetes/manifests-foundry/mapache/auth.yaml create mode 100644 kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/mapache/dashboard.yaml create mode 100644 kubernetes/manifests-foundry/mapache/foreman.yaml create mode 100644 kubernetes/manifests-foundry/mapache/gr26.yaml create mode 100644 kubernetes/manifests-foundry/mapache/ingress.yaml create mode 100644 kubernetes/manifests-foundry/mapache/kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/mapache/kustomization.yaml create mode 100644 kubernetes/manifests-foundry/mapache/live.yaml create mode 100644 kubernetes/manifests-foundry/mapache/namespace.yaml create mode 100644 kubernetes/manifests-foundry/mapache/postgres.yaml create mode 100644 kubernetes/manifests-foundry/mapache/query.yaml create mode 100644 kubernetes/manifests-foundry/mapache/vehicle.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/configmap-kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/core.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/discord.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/google.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/ingress.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/kustomization.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/namespace.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/oauth.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/postgres.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/saml.yaml create mode 100644 kubernetes/manifests-foundry/sentinel/web.yaml create mode 100644 kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/vault/ingress.yaml create mode 100644 kubernetes/manifests-foundry/vault/kerbecs.yaml create mode 100644 kubernetes/manifests-foundry/vault/kustomization.yaml create mode 100644 kubernetes/manifests-foundry/vault/namespace.yaml create mode 100644 kubernetes/manifests-foundry/vault/postgres.yaml create mode 100644 kubernetes/manifests-foundry/vault/server.yaml create mode 100644 kubernetes/manifests-foundry/vault/web.yaml diff --git a/infra/environments/prod/backend.tf b/infra/environments/prod/backend.tf index ecf80e7..edc92c4 100644 --- a/infra/environments/prod/backend.tf +++ b/infra/environments/prod/backend.tf @@ -28,6 +28,10 @@ terraform { source = "hashicorp/tls" version = "~> 4.0" } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } } } diff --git a/infra/environments/prod/foundry.tf b/infra/environments/prod/foundry.tf new file mode 100644 index 0000000..60cc890 --- /dev/null +++ b/infra/environments/prod/foundry.tf @@ -0,0 +1,124 @@ +# Foundry (on-prem k3s) cutover resources — the tunnel that connects the +# on-prem cluster to Cloudflare, plus the public DNS records that route +# the four public hostnames to it. +# +# Compute (EKS + ALB + ACM origin cert) still lives in main.tf. Once the +# cutover has soaked and the on-prem cluster owns traffic, delete the +# EKS-side resources from main.tf (module.eks / module.argocd / +# module.origin_cert) and remove kubernetes/apps/ from the repo. +# +# Off-cluster data services (gr-postgres, gr-mqtt, gr-clickhouse) stay in +# AWS — the on-prem cluster reaches them over the public internet via +# their existing Cloudflare A records. +# +# Cutover sequence (short version): +# +# 1. terraform apply this file (fails on step 3 records if EKS +# external-dns still owns them; delete those records via the CF +# dashboard first). +# 2. Populate cloudflared-secrets on the on-prem cluster: +# kubectl -n cloudflared create secret generic cloudflared-secrets \ +# --from-literal=TUNNEL_TOKEN="$(terraform output -raw foundry_tunnel_token)" +# 3. Apply kubernetes/bootstrap/root-foundry.yaml on the on-prem +# ArgoCD, populate the per-stack Secrets, wait for pods to reach +# Healthy against gr-postgres / gr-mqtt / gr-clickhouse. +# 4. On the EKS cluster: kubectl -n external-dns scale deploy +# external-dns --replicas=0 so it stops recreating the ALB records. +# 5. terraform apply again if step 1 stopped short. DNS records +# now CNAME to the tunnel; traffic starts landing on-prem within +# the CF TTL. +# 6. Bake. When confident, delete module.eks + module.argocd + +# module.origin_cert from main.tf and re-apply. + +# Account ID for the tunnel resources. The CF API token used by the +# provider is scoped to a single account — return that account's ID. +data "cloudflare_accounts" "current" {} + +locals { + cloudflare_account_id = data.cloudflare_accounts.current.result[0].id +} + +# 32-byte tunnel secret, stored in TF state. +resource "random_bytes" "foundry_tunnel_secret" { + length = 32 +} + +# Named tunnel. config_src = "cloudflare" means CF hosts the ingress rule +# table (managed by cloudflare_zero_trust_tunnel_cloudflared_config +# below); the on-prem cloudflared pod boots with just a token and pulls +# the ruleset from CF at runtime. +resource "cloudflare_zero_trust_tunnel_cloudflared" "foundry" { + account_id = local.cloudflare_account_id + name = "gr-foundry" + tunnel_secret = random_bytes.foundry_tunnel_secret.base64 + config_src = "cloudflare" +} + +# Ingress rules: every public hostname lands on the on-prem cluster's +# Traefik service, and Traefik does host-based routing to the right +# Service in the right namespace. Adding a new public hostname is one +# more block here + one more cloudflare_dns_record below. +resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" { + account_id = local.cloudflare_account_id + tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.foundry.id + + config = { + ingress = [ + { + hostname = "sentinel-v5.gauchoracing.com" + service = "http://traefik.kube-system.svc.cluster.local:80" + }, + { + hostname = "mapache.gauchoracing.com" + service = "http://traefik.kube-system.svc.cluster.local:80" + }, + { + hostname = "vault.gauchoracing.com" + service = "http://traefik.kube-system.svc.cluster.local:80" + }, + { + hostname = "argocd.gauchoracing.com" + service = "http://traefik.kube-system.svc.cluster.local:80" + }, + # Catch-all — required last entry per CF tunnel ingress schema. + { + service = "http_status:404" + }, + ] + } +} + +# Data source for the tunnel token; consumed by the cloudflared pods on +# the on-prem cluster via TUNNEL_TOKEN. Read with: +# terraform output -raw foundry_tunnel_token +data "cloudflare_zero_trust_tunnel_cloudflared_token" "foundry" { + account_id = local.cloudflare_account_id + tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.foundry.id +} + +# Public DNS records. Each is a proxied CNAME to the tunnel's CF-managed +# hostname (.cfargotunnel.com); CF proxies + terminates TLS at the +# edge and forwards HTTP through the tunnel. +# +# These were previously created by EKS external-dns. Before first apply, +# either scale that Deployment to 0 or delete the records via the CF +# dashboard so terraform doesn't hit a 409. +locals { + foundry_hostnames = [ + "sentinel-v5", + "mapache", + "vault", + "argocd", + ] +} + +resource "cloudflare_dns_record" "foundry" { + for_each = toset(local.foundry_hostnames) + + zone_id = data.cloudflare_zone.gauchoracing.id + name = each.value + type = "CNAME" + content = "${cloudflare_zero_trust_tunnel_cloudflared.foundry.id}.cfargotunnel.com" + ttl = 1 # 1 = auto (required by CF when proxied=true) + proxied = true +} diff --git a/infra/environments/prod/outputs.tf b/infra/environments/prod/outputs.tf index 8e102ff..4d16cab 100644 --- a/infra/environments/prod/outputs.tf +++ b/infra/environments/prod/outputs.tf @@ -86,3 +86,14 @@ output "clickhouse_admin_password" { value = module.clickhouse.admin_password sensitive = true } + +output "foundry_tunnel_id" { + description = "UUID of the gr-foundry Cloudflare tunnel. DNS CNAMEs point at .cfargotunnel.com." + value = cloudflare_zero_trust_tunnel_cloudflared.foundry.id +} + +output "foundry_tunnel_token" { + description = "Tunnel connector token for the gr-foundry tunnel. Consumed by the cloudflared pods on the on-prem cluster via TUNNEL_TOKEN. Read with `terraform output -raw foundry_tunnel_token`." + value = data.cloudflare_zero_trust_tunnel_cloudflared_token.foundry.token + sensitive = true +} diff --git a/kubernetes/apps-foundry/argocd-server-ingress.yaml b/kubernetes/apps-foundry/argocd-server-ingress.yaml new file mode 100644 index 0000000..1b77c7a --- /dev/null +++ b/kubernetes/apps-foundry/argocd-server-ingress.yaml @@ -0,0 +1,25 @@ +# Adds the public argocd.gauchoracing.com Ingress to the argocd namespace +# on the foundry k3s cluster. Complements (doesn't replace) the internal +# argocd.internal.bk1031.dev Ingress that foundry-infra manages in the +# same namespace — different Ingress names, both point at argocd-server. + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: argocd-server-ingress + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/manifests-foundry/argocd-server-ingress + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/kubernetes/apps-foundry/cloudflared.yaml b/kubernetes/apps-foundry/cloudflared.yaml new file mode 100644 index 0000000..82c8fa6 --- /dev/null +++ b/kubernetes/apps-foundry/cloudflared.yaml @@ -0,0 +1,33 @@ +# cloudflared: outbound tunnel connector. Fronts every public +# gauchoracing.com hostname on this cluster — the tunnel and its route +# table live in Terraform (foundry.tf), the pod that terminates the +# tunnel here lives in this Application. +# +# Manual one-time Secret bootstrap after this first syncs: +# +# kubectl -n cloudflared create secret generic cloudflared-secrets \ +# --from-literal=TUNNEL_TOKEN="$(cd ../../infra/environments/prod && terraform output -raw foundry_tunnel_token)" + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cloudflared + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/manifests-foundry/cloudflared + destination: + server: https://kubernetes.default.svc + namespace: cloudflared + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/apps-foundry/mapache.yaml b/kubernetes/apps-foundry/mapache.yaml new file mode 100644 index 0000000..312e94a --- /dev/null +++ b/kubernetes/apps-foundry/mapache.yaml @@ -0,0 +1,45 @@ +# Mapache on the foundry k3s cluster. Same manifest set as the EKS tree +# (kubernetes/manifests/mapache/), diverging only in ingress.yaml +# (Traefik) and postgres.yaml (public Cloudflare hostname). +# +# All three off-cluster data services (gr-postgres, gr-mqtt, +# gr-clickhouse) stay in AWS and are reached over the public internet +# from on-prem via their gauchoracing.com hostnames. gr26 continues +# uploading Epic Shelter Parquet to s3://gaucho-racing/ using the same +# IAM keys — AWS_ACCESS_KEY_ID/SECRET stay in mapache-secrets. +# +# Manual Secret needed once after first sync: +# +# kubectl -n mapache create secret generic mapache-secrets \ +# --from-literal=DATABASE_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \ +# --from-literal=MQTT_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw mqtt_password)" \ +# --from-literal=CLICKHOUSE_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw clickhouse_admin_password)" \ +# --from-literal=KERBECS_PASSWORD=... \ +# --from-literal=SENTINEL_CLIENT_SECRET=... \ +# --from-literal=SENTINEL_SA_TOKEN=... \ +# --from-literal=AWS_ACCESS_KEY_ID=... \ +# --from-literal=AWS_SECRET_ACCESS_KEY=... + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mapache + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/manifests-foundry/mapache + destination: + server: https://kubernetes.default.svc + namespace: mapache + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/apps-foundry/sentinel.yaml b/kubernetes/apps-foundry/sentinel.yaml new file mode 100644 index 0000000..9bb91a8 --- /dev/null +++ b/kubernetes/apps-foundry/sentinel.yaml @@ -0,0 +1,48 @@ +# Sentinel on the foundry k3s cluster. Same manifest set as the EKS tree +# (kubernetes/manifests/sentinel/), diverging only in ingress.yaml +# (Traefik instead of ALB) and postgres.yaml (public Cloudflare hostname +# for the gr-postgres EC2 instead of the internal EC2 hostname). +# +# Manual Secret needed once after this Application first syncs. Copy the +# external-facing values from the EKS cluster verbatim so identity state +# (Discord, INTERNAL_BOOTSTRAP_SECRET) stays coherent across environments +# during cutover: +# +# kubectl --context eks -n sentinel get secret sentinel-secrets -o yaml \ +# | grep -v '^\s*resourceVersion:\|^\s*uid:\|^\s*creationTimestamp:\|^\s*namespace:' \ +# | kubectl --context foundry apply -f - +# +# Or create from scratch: +# +# kubectl -n sentinel create secret generic sentinel-secrets \ +# --from-literal=POSTGRES_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \ +# --from-literal=INTERNAL_BOOTSTRAP_SECRET=... \ +# --from-literal=KERBECS_PASSWORD=... \ +# --from-literal=DISCORD_TOKEN=... \ +# --from-literal=DISCORD_CLIENT_ID=... \ +# --from-literal=DISCORD_CLIENT_SECRET=... \ +# --from-literal=GOOGLE_SERVICE_ACCOUNT=... + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: sentinel + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/manifests-foundry/sentinel + destination: + server: https://kubernetes.default.svc + namespace: sentinel + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/apps-foundry/vault-k8s-operator.yaml b/kubernetes/apps-foundry/vault-k8s-operator.yaml new file mode 100644 index 0000000..2ba7dad --- /dev/null +++ b/kubernetes/apps-foundry/vault-k8s-operator.yaml @@ -0,0 +1,27 @@ +# vault-k8s-operator: mirrors the EKS install (kubernetes/apps/vault-k8s-operator.yaml). +# Same upstream chart, same version pin — the operator syncs secrets from +# vault-server into the cluster via VaultSecret CRDs. + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault-k8s-operator + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/vault-k8s-operator.git + targetRevision: v1.1.0 + path: config/default + destination: + server: https://kubernetes.default.svc + namespace: vault-k8s-operator-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/apps-foundry/vault.yaml b/kubernetes/apps-foundry/vault.yaml new file mode 100644 index 0000000..49618c4 --- /dev/null +++ b/kubernetes/apps-foundry/vault.yaml @@ -0,0 +1,35 @@ +# Vault on the foundry k3s cluster. Same manifest set as the EKS tree +# (kubernetes/manifests/vault/), diverging only in ingress.yaml (Traefik) +# and postgres.yaml (public Cloudflare hostname). +# +# Manual Secret needed once after first sync: +# +# kubectl -n vault create secret generic vault-secrets \ +# --from-literal=POSTGRES_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \ +# --from-literal=KERBECS_PASSWORD=... \ +# --from-literal=SENTINEL_CLIENT_SECRET=... \ +# --from-literal=SENTINEL_SA_TOKEN=... + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/manifests-foundry/vault + destination: + server: https://kubernetes.default.svc + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/bootstrap/root-foundry.yaml b/kubernetes/bootstrap/root-foundry.yaml new file mode 100644 index 0000000..4792bdb --- /dev/null +++ b/kubernetes/bootstrap/root-foundry.yaml @@ -0,0 +1,43 @@ +# Root ArgoCD Application for the foundry k3s cluster. +# +# The foundry cluster runs its own ArgoCD (installed via foundry-infra). +# This is a SECOND root app-of-apps that lives alongside foundry-infra's +# root — one ArgoCD instance, two independently-reconciled trees. Apply +# once manually on the foundry cluster after the initial cutover prep: +# +# kubectl --context foundry apply -f kubernetes/bootstrap/root-foundry.yaml +# +# From then on, ArgoCD reads kubernetes/apps-foundry/ from this repo and +# reconciles every Application resource it finds there. +# +# EKS is still managed by kubernetes/bootstrap/root.yaml → kubernetes/apps/. +# The two roots do NOT share a cluster; each root's ArgoCD is scoped to +# its own cluster via server: https://kubernetes.default.svc. +# +# See kubernetes/apps-foundry/*.yaml for the per-stack manual bootstrap +# steps (secret creation, DB creation, etc.). + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: root-foundry + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/Gaucho-Racing/infrastructure.git + targetRevision: main + path: kubernetes/apps-foundry + directory: + recurse: true + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml b/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml new file mode 100644 index 0000000..ac9cf1e --- /dev/null +++ b/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml @@ -0,0 +1,35 @@ +# Public Ingress for the ArgoCD UI on the foundry k3s cluster. +# The foundry-infra repo already manages an internal Ingress +# (argocd.internal.bk1031.dev) — this one adds the public gauchoracing.com +# hostname. Distinct Ingress name (argocd-server-public) to avoid colliding +# with the foundry-managed argocd-server Ingress in the same namespace. +# +# Requires the ArgoCD Helm values to have `server.insecure: true` so the +# argocd-server Service serves plain HTTP on port 80 (traffic path is +# Cloudflare edge → cloudflared → Traefik → argocd-server, all HTTP). +# If your foundry-infra argocd install doesn't have this, patch +# argocd-cmd-params-cm: +# kubectl -n argocd patch configmap argocd-cmd-params-cm \ +# --patch '{"data":{"server.insecure":"true"}}' +# kubectl -n argocd rollout restart deployment argocd-server +# +# DNS for argocd.gauchoracing.com is managed by Terraform (foundry.tf). + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: argocd-server-public + namespace: argocd +spec: + ingressClassName: traefik + rules: + - host: argocd.gauchoracing.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: argocd-server + port: + number: 80 diff --git a/kubernetes/manifests-foundry/cloudflared/deployment.yaml b/kubernetes/manifests-foundry/cloudflared/deployment.yaml new file mode 100644 index 0000000..edb2cd4 --- /dev/null +++ b/kubernetes/manifests-foundry/cloudflared/deployment.yaml @@ -0,0 +1,81 @@ +# cloudflared: outbound Cloudflare Tunnel connector. +# +# One tunnel (gr-foundry) fronts every gauchoracing.com hostname routed +# to this cluster. Tunnel + per-hostname routes + DNS records are all +# terraform-managed (see infra/environments/prod/foundry.tf) — this +# Deployment just runs the connector. +# +# Manual bootstrap after this Application first syncs — the Secret's +# TUNNEL_TOKEN comes from `terraform output -raw foundry_tunnel_token`: +# +# kubectl -n cloudflared create secret generic cloudflared-secrets \ +# --from-literal=TUNNEL_TOKEN="$(cd ../../infra/environments/prod && terraform output -raw foundry_tunnel_token)" +# +# Two replicas so a pod restart doesn't drop the tunnel; each opens +# its own set of connections to the Cloudflare edge and CF load-balances +# across them automatically. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cloudflared + namespace: cloudflared +spec: + replicas: 2 + selector: + matchLabels: + app: cloudflared + template: + metadata: + labels: + app: cloudflared + spec: + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: cloudflared + containers: + - name: cloudflared + # Pin to a specific tag; bump manually when reviewing releases. + # https://github.com/cloudflare/cloudflared/releases + image: cloudflare/cloudflared:2025.11.1 + args: + - tunnel + - --no-autoupdate + - --metrics + - 0.0.0.0:2000 + - run + env: + - name: TUNNEL_TOKEN + valueFrom: + secretKeyRef: + name: cloudflared-secrets + key: TUNNEL_TOKEN + ports: + - containerPort: 2000 + name: metrics + livenessProbe: + httpGet: + path: /ready + port: 2000 + failureThreshold: 1 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 2000 + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi diff --git a/kubernetes/manifests-foundry/cloudflared/namespace.yaml b/kubernetes/manifests-foundry/cloudflared/namespace.yaml new file mode 100644 index 0000000..620b8ae --- /dev/null +++ b/kubernetes/manifests-foundry/cloudflared/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cloudflared diff --git a/kubernetes/manifests-foundry/mapache/auth.yaml b/kubernetes/manifests-foundry/mapache/auth.yaml new file mode 100644 index 0000000..11932a9 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/auth.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: auth + template: + metadata: + labels: + app: auth + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: auth + containers: + - name: auth + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/mapache/auth:latest + ports: + - containerPort: 7002 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "7002" + - name: DATABASE_HOST + value: postgres.mapache.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: mapache + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: DATABASE_PASSWORD + - name: KERBECS_ENDPOINT + value: http://kerbecs.mapache.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: KERBECS_PASSWORD + # Sentinel v5: JWKS lives at /api/core/keys under SENTINEL_URL, + # so the separate JWKS env var is gone. SA token replaces the + # v4 static SENTINEL_TOKEN. + - name: SENTINEL_URL + value: https://sentinel-v5.gauchoracing.com + - name: SENTINEL_CLIENT_ID + value: TIvD6jCH3mGV + - name: SENTINEL_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: mapache-secrets + key: SENTINEL_CLIENT_SECRET + - name: SENTINEL_SA_TOKEN + valueFrom: + secretKeyRef: + name: mapache-secrets + key: SENTINEL_SA_TOKEN + - name: SENTINEL_REDIRECT_URI + value: https://mapache.gauchoracing.com/auth/login +--- +apiVersion: v1 +kind: Service +metadata: + name: auth + namespace: mapache +spec: + selector: + app: auth + ports: + - port: 7002 + targetPort: 7002 diff --git a/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml b/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml new file mode 100644 index 0000000..9f60375 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml @@ -0,0 +1,225 @@ +# Kerbecs gateway routing rules. Mounted into the kerbecs pod at +# /etc/kerbecs/kerbecs.yaml — KERBECS_CONFIG points the binary at it. +# +# Mirrors the dev Mapache/kerbecs.yaml route table, with upstream +# hostnames switched to in-cluster Services. The dashboard upstream +# points at port 80 (the nginx-served prod build), unlike the dev +# compose where Vite serves on 5173. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: kerbecs-config + namespace: mapache +data: + kerbecs.yaml: | + gateway: + name: mapache-gateway + version: 0.1.0 + env: ${ENV:PROD} + limits: + max_request_bytes: 100MB + max_response_bytes: 100MB + timeouts: + dial: 5s + headers: 30s + idle: 90s + overall: 0 + + listeners: + gateway: + port: "10310" + # CORS is set by each upstream service (gin-contrib/cors in Go + # services, fastapi.middleware.cors in query). Setting it here + # too caused duplicate Access-Control-Allow-Origin headers on + # passthrough routes (live SSE, gr26 WS, foreman events) which + # browsers reject. + cors: + enabled: false + admin: + port: "10300" + auth: + type: basic + username: ${KERBECS_USER:admin} + password: ${KERBECS_PASSWORD:admin} + + providers: + static: + # Hot-reload on ConfigMap updates (kerbecs >= 3.1.0). The watcher + # picks up the kubelet ..data symlink swap so route changes apply + # without restarting the pod. + watch: true + + upstreams: + auth: + name: mapache-auth + version: 0.1.0 + instances: + - http://auth.mapache.svc.cluster.local:7002 + + vehicle: + name: mapache-vehicle + version: 0.1.0 + instances: + - http://vehicle.mapache.svc.cluster.local:7003 + + gr26: + name: mapache-gr26 + version: 0.1.0 + instances: + - http://gr26.mapache.svc.cluster.local:7005 + + live: + name: mapache-live + version: 0.1.0 + instances: + - http://live.mapache.svc.cluster.local:7015 + + query: + name: mapache-query + version: 0.1.0 + instances: + - http://query.mapache.svc.cluster.local:7010 + + foreman: + name: mapache-foreman + version: 0.1.0 + instances: + - http://foreman.mapache.svc.cluster.local:7011 + + dashboard: + name: mapache-dashboard + version: 0.1.0 + instances: + - http://dashboard.mapache.svc.cluster.local:80 + + routes: + - name: auth + match: + path: /api/auth/* + upstream: auth + rewrite: + strip_prefix: /api + envelope: default + + - name: users + match: + path: /api/users/* + upstream: auth + rewrite: + strip_prefix: /api + envelope: default + + - name: vehicle-ping + match: + path: /api/vehicle/ping + upstream: vehicle + rewrite: + strip_prefix: /api + envelope: default + + - name: vehicles + match: + path: /api/vehicles + upstream: vehicle + rewrite: + strip_prefix: /api + envelope: default + + - name: vehicles-id + match: + path: /api/vehicles/* + upstream: vehicle + rewrite: + strip_prefix: /api + envelope: default + + - name: sessions + match: + path: /api/sessions + upstream: vehicle + rewrite: + strip_prefix: /api + envelope: default + + - name: sessions-id + match: + path: /api/sessions/* + upstream: vehicle + rewrite: + strip_prefix: /api + envelope: default + + - name: gr26-live + match: + path: /api/gr26/live + upstream: gr26 + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: gr26 + match: + path: /api/gr26/* + upstream: gr26 + rewrite: + strip_prefix: /api + envelope: default + + # Specific WS/SSE routes need passthrough so kerbecs doesn't wrap + # the response envelope or interfere with the connection upgrade. + # The catch-all `live` route below uses default envelope for the + # plain HTTP endpoints (/live/ping, /live/stats). + - name: live-ws + match: + path: /api/live/ws + upstream: live + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: live-sse + match: + path: /api/live/sse + upstream: live + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: live + match: + path: /api/live/* + upstream: live + rewrite: + strip_prefix: /api + envelope: default + + - name: query + match: + path: /api/query/* + upstream: query + rewrite: + strip_prefix: /api + envelope: default + + - name: foreman-events + match: + path: /api/foreman/events/* + upstream: foreman + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: foreman + match: + path: /api/foreman/* + upstream: foreman + rewrite: + strip_prefix: /api + envelope: default + + - name: dashboard + match: + path: /* + upstream: dashboard + envelope: passthrough diff --git a/kubernetes/manifests-foundry/mapache/dashboard.yaml b/kubernetes/manifests-foundry/mapache/dashboard.yaml new file mode 100644 index 0000000..2f66007 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/dashboard.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dashboard + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: dashboard + template: + metadata: + labels: + app: dashboard + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: dashboard + containers: + - name: dashboard + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/mapache/dashboard:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: dashboard + namespace: mapache +spec: + selector: + app: dashboard + ports: + - port: 80 + targetPort: 80 diff --git a/kubernetes/manifests-foundry/mapache/foreman.yaml b/kubernetes/manifests-foundry/mapache/foreman.yaml new file mode 100644 index 0000000..eca9ef6 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/foreman.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: foreman + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: foreman + template: + metadata: + labels: + app: foreman + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: foreman + containers: + - name: foreman + # Standalone Foreman lives in github.com/gaucho-racing/Foreman now. + # The kustomization.yaml `images:` block pins the tag — :latest + # here is just the placeholder kustomize matches against. + image: ghcr.io/gaucho-racing/foreman:latest + ports: + - containerPort: 7011 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "7011" + - name: DATABASE_HOST + value: postgres.mapache.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: mapache + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: DATABASE_PASSWORD + # Foreman shares the `mapache` database; default 'foreman_' + # prefix keeps foreman_jobs / foreman_job_runs / + # foreman_schedules namespaced away from host-app tables. + # Set explicitly so the choice is reviewable in the manifest. + - name: FOREMAN_TABLE_PREFIX + value: "foreman_" +--- +apiVersion: v1 +kind: Service +metadata: + name: foreman + namespace: mapache +spec: + selector: + app: foreman + ports: + - port: 7011 + targetPort: 7011 diff --git a/kubernetes/manifests-foundry/mapache/gr26.yaml b/kubernetes/manifests-foundry/mapache/gr26.yaml new file mode 100644 index 0000000..740b3cd --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/gr26.yaml @@ -0,0 +1,123 @@ +# gr26: telemetry ingest + the foreman worker pool. +# +# replicas=1 — the worker pool claims foreman jobs by lease, so scaling +# horizontally is supported once we know how much concurrency the Epic +# Shelter ingest needs. Start single-replica to keep the live MQTT +# subscriber simple. +# +# MQTT broker lives on the gr-mqtt EC2 outside the cluster. Pods reach +# it over the public hostname (gr-mqtt.gauchoracing.com); throughput is +# low enough that hairpinning out through the NAT gateway is fine. +# Auth uses the shared mapache-fleet credential (MQTT_USER=mapache). + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gr26 + namespace: mapache +spec: + replicas: 3 + selector: + matchLabels: + app: gr26 + template: + metadata: + labels: + app: gr26 + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: gr26 + containers: + - name: gr26 + image: ghcr.io/gaucho-racing/mapache/gr26:latest + ports: + - containerPort: 7005 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + env: + - name: ENV + value: PROD + - name: PORT + value: "7005" + # Telemetry storage (signal, gr26_can, ping) moved off Postgres + # to ClickHouse — gr26 holds no Postgres connection anymore. + # The gr26 process auto-creates the mapache database on first + # connect via the always-present `default` DB. + - name: CLICKHOUSE_HOST + value: gr-clickhouse.gauchoracing.com + - name: CLICKHOUSE_PORT + value: "9000" + - name: CLICKHOUSE_USER + value: admin + - name: CLICKHOUSE_DATABASE + value: mapache + - name: CLICKHOUSE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: CLICKHOUSE_PASSWORD + - name: KERBECS_ENDPOINT + value: http://kerbecs.mapache.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: KERBECS_PASSWORD + - name: MQTT_HOST + value: gr-mqtt.gauchoracing.com + - name: MQTT_PORT + value: "1883" + - name: MQTT_USER + value: mapache + - name: MQTT_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: MQTT_PASSWORD + - name: FOREMAN_ENDPOINT + value: http://foreman.mapache.svc.cluster.local:7011 + - name: NUM_WORKERS + value: "4" + - name: SHELTER_S3_BUCKET + value: gaucho-racing + - name: SHELTER_S3_REGION + value: us-west-2 + - name: SHELTER_S3_PREFIX + value: epic-shelter/tcm-26/ + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: mapache-secrets + key: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: mapache-secrets + key: AWS_SECRET_ACCESS_KEY +--- +apiVersion: v1 +kind: Service +metadata: + name: gr26 + namespace: mapache +spec: + selector: + app: gr26 + ports: + - port: 7005 + targetPort: 7005 diff --git a/kubernetes/manifests-foundry/mapache/ingress.yaml b/kubernetes/manifests-foundry/mapache/ingress.yaml new file mode 100644 index 0000000..672a8db --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/ingress.yaml @@ -0,0 +1,25 @@ +# Public entrypoint on the foundry k3s cluster. Traefik receives plain +# HTTP forwarded by the in-cluster cloudflared tunnel; TLS terminates at +# the Cloudflare edge. Host-based routing to the kerbecs gateway, which +# routes to the backend services + dashboard internally. +# +# DNS for mapache.gauchoracing.com is managed by Terraform (foundry.tf). + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mapache + namespace: mapache +spec: + ingressClassName: traefik + rules: + - host: mapache.gauchoracing.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kerbecs + port: + number: 10310 diff --git a/kubernetes/manifests-foundry/mapache/kerbecs.yaml b/kubernetes/manifests-foundry/mapache/kerbecs.yaml new file mode 100644 index 0000000..3d1de4d --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/kerbecs.yaml @@ -0,0 +1,80 @@ +# Gateway. Public traffic enters through the ALB (see ingress.yaml), +# kerbecs routes to the right upstream Service based on its config in +# the kerbecs-config ConfigMap. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kerbecs + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: kerbecs + template: + metadata: + labels: + app: kerbecs + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: kerbecs + containers: + - name: kerbecs + image: bk1031/kerbecs:latest + ports: + - containerPort: 10310 + name: gateway + - containerPort: 10300 + name: admin + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: KERBECS_CONFIG + value: /etc/kerbecs/kerbecs.yaml + - name: ENV + value: PROD + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: KERBECS_PASSWORD + volumeMounts: + - name: kerbecs-config + mountPath: /etc/kerbecs + readOnly: true + volumes: + - name: kerbecs-config + configMap: + name: kerbecs-config +--- +apiVersion: v1 +kind: Service +metadata: + name: kerbecs + namespace: mapache +spec: + selector: + app: kerbecs + ports: + - name: gateway + port: 10310 + targetPort: 10310 + - name: admin + port: 10300 + targetPort: 10300 diff --git a/kubernetes/manifests-foundry/mapache/kustomization.yaml b/kubernetes/manifests-foundry/mapache/kustomization.yaml new file mode 100644 index 0000000..0ce186d --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/kustomization.yaml @@ -0,0 +1,54 @@ +# Kustomize entrypoint — ArgoCD auto-detects this file and runs +# `kustomize build` instead of treating the dir as a flat manifest set. +# +# The `images` block below is the single source of truth for mapache +# service versions. Bumping a release looks like: +# +# kustomize edit set image \ +# ghcr.io/gaucho-racing/mapache/auth=ghcr.io/gaucho-racing/mapache/auth:3.4.0 +# +# (Run from this directory; the per-service publish workflows will +# eventually wrap this across all services on tag push.) That edit only +# touches this file — the Deployment YAMLs stay unchanged, so the diff +# in a release PR is one block. +# +# kerbecs is on its own version stream from the mapache services — +# bump it separately when there's a reason to. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - configmap-kerbecs.yaml + - postgres.yaml + - kerbecs.yaml + - auth.yaml + - vehicle.yaml + - gr26.yaml + - live.yaml + - query.yaml + - foreman.yaml + - dashboard.yaml + - ingress.yaml + +images: + - name: ghcr.io/gaucho-racing/mapache/auth + newTag: 3.9.6 + - name: ghcr.io/gaucho-racing/mapache/vehicle + newTag: 3.9.6 + - name: ghcr.io/gaucho-racing/mapache/gr26 + newTag: 3.9.6 + - name: ghcr.io/gaucho-racing/mapache/live + newTag: 3.9.6 + - name: ghcr.io/gaucho-racing/mapache/query + newTag: 3.9.6 + # Foreman is its own project now (github.com/gaucho-racing/Foreman) + # with its own release cadence. Track via its semver tags, not + # mapache's. Mapache services keep tracking together via newTag above. + - name: ghcr.io/gaucho-racing/foreman + newTag: 2.0.2 + - name: ghcr.io/gaucho-racing/mapache/dashboard + newTag: 3.9.6 + - name: bk1031/kerbecs + newTag: 3.2.0 diff --git a/kubernetes/manifests-foundry/mapache/live.yaml b/kubernetes/manifests-foundry/mapache/live.yaml new file mode 100644 index 0000000..25344b1 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/live.yaml @@ -0,0 +1,80 @@ +# live: WebSocket / SSE signal-streaming service. +# +# Subscribes to the `query/live/#` republish topic (non-shared) so every +# replica sees every signal — this is what unblocks the dashboard from +# the gr26 shared-sub partial-visibility issue. The in-process Hub + +# WebSocket endpoint that used to live on gr26 was extracted into this +# service in v3.5.0 (Mapache commit 22102fa). +# +# Cache window (CACHE_WINDOW_SEC=60) is the ring buffer length each +# replica keeps for newly-connecting clients. State is per-pod; a pod +# restart drops its cache, and clients that reconnect onto another pod +# see only that pod's cache for the first 60s. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: live + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: live + template: + metadata: + labels: + app: live + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: live + containers: + - name: live + image: ghcr.io/gaucho-racing/mapache/live:latest + ports: + - containerPort: 7015 + resources: + requests: + cpu: 50m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "7015" + - name: MQTT_HOST + value: gr-mqtt.gauchoracing.com + - name: MQTT_PORT + value: "1883" + - name: MQTT_USER + value: mapache + - name: MQTT_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: MQTT_PASSWORD + - name: CACHE_WINDOW_SEC + value: "60" +--- +apiVersion: v1 +kind: Service +metadata: + name: live + namespace: mapache +spec: + selector: + app: live + ports: + - port: 7015 + targetPort: 7015 diff --git a/kubernetes/manifests-foundry/mapache/namespace.yaml b/kubernetes/manifests-foundry/mapache/namespace.yaml new file mode 100644 index 0000000..f4e80c7 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mapache diff --git a/kubernetes/manifests-foundry/mapache/postgres.yaml b/kubernetes/manifests-foundry/mapache/postgres.yaml new file mode 100644 index 0000000..43620cc --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/postgres.yaml @@ -0,0 +1,15 @@ +# In-cluster handle for the gr-postgres EC2. Same instance as sentinel's +# postgres — DBs are isolated at the database level, not the instance. +# +# From on-prem, resolves via the Cloudflare A record +# gr-postgres.gauchoracing.com (unproxied) to the EIP. Traffic goes over +# the public internet on 5432; scram-sha-256 is the only gate. + +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: mapache +spec: + type: ExternalName + externalName: gr-postgres.gauchoracing.com diff --git a/kubernetes/manifests-foundry/mapache/query.yaml b/kubernetes/manifests-foundry/mapache/query.yaml new file mode 100644 index 0000000..62d4171 --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/query.yaml @@ -0,0 +1,99 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: query + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: query + template: + metadata: + labels: + app: query + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: query + containers: + - name: query + image: ghcr.io/gaucho-racing/mapache/query:latest + ports: + - containerPort: 7010 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "7010" + - name: DATABASE_HOST + value: postgres.mapache.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: mapache + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: DATABASE_PASSWORD + # ClickHouse holds the telemetry tables (signal, gr26_can, ping). + # The query service uses clickhouse-connect over HTTP, so it needs + # the HTTP port 8123 — distinct from gr26's native 9000. Password + # is shared with gr26 via mapache-secrets/CLICKHOUSE_PASSWORD. + - name: CLICKHOUSE_HOST + value: gr-clickhouse.gauchoracing.com + - name: CLICKHOUSE_PORT + value: "8123" + - name: CLICKHOUSE_USER + value: admin + - name: CLICKHOUSE_DATABASE + value: mapache + - name: CLICKHOUSE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: CLICKHOUSE_PASSWORD + - name: KERBECS_ENDPOINT + value: http://kerbecs.mapache.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: KERBECS_PASSWORD + # Sentinel v5: JWKS lives at /api/core/keys under SENTINEL_URL, + # so the separate JWKS env var is gone (main.py derives it). + - name: SENTINEL_URL + value: https://sentinel-v5.gauchoracing.com + - name: SENTINEL_CLIENT_ID + value: TIvD6jCH3mGV +--- +apiVersion: v1 +kind: Service +metadata: + name: query + namespace: mapache +spec: + selector: + app: query + ports: + - port: 7010 + targetPort: 7010 diff --git a/kubernetes/manifests-foundry/mapache/vehicle.yaml b/kubernetes/manifests-foundry/mapache/vehicle.yaml new file mode 100644 index 0000000..314b23f --- /dev/null +++ b/kubernetes/manifests-foundry/mapache/vehicle.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vehicle + namespace: mapache +spec: + replicas: 1 + selector: + matchLabels: + app: vehicle + template: + metadata: + labels: + app: vehicle + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: vehicle + containers: + - name: vehicle + image: ghcr.io/gaucho-racing/mapache/vehicle:latest + ports: + - containerPort: 7003 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "7003" + - name: DATABASE_HOST + value: postgres.mapache.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: mapache + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: DATABASE_PASSWORD + - name: KERBECS_ENDPOINT + value: http://kerbecs.mapache.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: mapache-secrets + key: KERBECS_PASSWORD +--- +apiVersion: v1 +kind: Service +metadata: + name: vehicle + namespace: mapache +spec: + selector: + app: vehicle + ports: + - port: 7003 + targetPort: 7003 diff --git a/kubernetes/manifests-foundry/sentinel/configmap-kerbecs.yaml b/kubernetes/manifests-foundry/sentinel/configmap-kerbecs.yaml new file mode 100644 index 0000000..3525624 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/configmap-kerbecs.yaml @@ -0,0 +1,194 @@ +# Kerbecs gateway routing rules. Mounted into the kerbecs pod at +# /etc/kerbecs/kerbecs.yaml — KERBECS_CONFIG points the binary at it. +# +# Upstreams reference the in-cluster Services by name (cross-namespace +# resolution not needed since everything lives in `sentinel`). The web +# upstream points at port 80 (the nginx-served prod build), unlike the +# docker-compose dev setup where vite serves on 5173. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: kerbecs-config + namespace: sentinel +data: + kerbecs.yaml: | + gateway: + name: sentinel-gateway + version: 0.1.0 + env: ${ENV:PROD} + timeouts: + dial: 5s + headers: 30s + idle: 50s + overall: 0 + + listeners: + gateway: + port: "10310" + cors: + enabled: true + allowed_origins: ["*"] + allow_credentials: false + admin: + port: "10300" + auth: + type: basic + username: ${KERBECS_USER:admin} + password: ${KERBECS_PASSWORD:admin} + + providers: + static: + # Hot-reload the config on ConfigMap updates (kerbecs >= 3.1.0). The + # watcher reacts to the kubelet ..data symlink swap, so route changes + # apply without restarting the pod. + watch: true + + upstreams: + core: + name: sentinel-core + version: 0.1.0 + instances: + - http://core.sentinel.svc.cluster.local:9999 + + oauth: + name: sentinel-oauth + version: 0.1.0 + instances: + - http://oauth.sentinel.svc.cluster.local:9997 + + discord: + name: sentinel-discord + version: 0.1.0 + instances: + - http://discord.sentinel.svc.cluster.local:9998 + + saml: + name: sentinel-saml + version: 0.1.0 + instances: + - http://saml.sentinel.svc.cluster.local:9996 + + google: + name: sentinel-google + version: 0.1.0 + instances: + - http://google.sentinel.svc.cluster.local:9995 + + web: + name: sentinel-web + version: 0.1.0 + instances: + - http://web.sentinel.svc.cluster.local:80 + + routes: + - name: core-internal + match: + path: /api/core/* + upstream: core + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: core-users + match: + path: /api/users/* + upstream: core + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: core-applications + match: + path: /api/applications/* + upstream: core + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: core-groups + match: + path: /api/groups/* + upstream: core + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: core-entities + match: + path: /api/entities/* + upstream: core + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: oauth + match: + path: /api/oauth/* + upstream: oauth + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: auth + match: + path: /api/auth/* + upstream: oauth + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: discord + match: + path: /api/discord/* + upstream: discord + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: google + match: + path: /api/google/* + upstream: google + rewrite: + strip_prefix: /api + envelope: passthrough + + # SAML consent endpoints — the SPA calls these through the /api prefix. + - name: saml + match: + path: /api/saml/* + upstream: saml + rewrite: + strip_prefix: /api + envelope: passthrough + + # OIDC discovery must live at the issuer root (no /api prefix), so route + # it straight to oauth without stripping anything. + - name: well-known + match: + path: /.well-known/* + upstream: oauth + envelope: passthrough + + # SAML IdP metadata and SSO endpoint live at the issuer root (no /api + # prefix) so the URLs published in metadata are clean and stable. The + # SPA-served /saml/authorize consent page is NOT listed here, so it falls + # through to the web frontend below. + - name: saml-metadata + match: + path: /saml/metadata + upstream: saml + envelope: passthrough + + - name: saml-sso + match: + path: /saml/sso + upstream: saml + envelope: passthrough + + - name: web-frontend + match: + path: /* + upstream: web + envelope: passthrough diff --git a/kubernetes/manifests-foundry/sentinel/core.yaml b/kubernetes/manifests-foundry/sentinel/core.yaml new file mode 100644 index 0000000..805afa1 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/core.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core + namespace: sentinel +spec: + replicas: 1 + selector: + matchLabels: + app: core + template: + metadata: + labels: + app: core + spec: + # Spread across at least 2 nodes (minDomains) but tolerate skew + # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). minDomains + # makes the scheduler treat the missing-domain count as 0 when + # only one node currently has matching pods, so the second + # replica blocks on schedule and Karpenter provisions a node + # (requires k8s ≥ 1.30 — we're on 1.35). + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: core + containers: + - name: core + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-core:latest + ports: + - containerPort: 9999 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9999" + - name: DATABASE_HOST + value: postgres.sentinel.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: sentinel + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: POSTGRES_PASSWORD + # OIDC issuer — the public base URL. MUST match oauth's ISSUER + # exactly; it's the `iss` claim stamped into signed tokens and the + # `issuer` advertised by the OAuth discovery document. + - name: ISSUER + value: https://sentinel-v5.gauchoracing.com + # Shared secret each non-core service exchanges at startup + # for its pre-seeded bearer JWT. Same value as on every + # other sentinel-* deployment. Rotating it requires a + # rolling restart of every service so each can re-fetch. + - name: INTERNAL_BOOTSTRAP_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: INTERNAL_BOOTSTRAP_SECRET +--- +apiVersion: v1 +kind: Service +metadata: + name: core + namespace: sentinel +spec: + selector: + app: core + ports: + - port: 9999 + targetPort: 9999 diff --git a/kubernetes/manifests-foundry/sentinel/discord.yaml b/kubernetes/manifests-foundry/sentinel/discord.yaml new file mode 100644 index 0000000..2078a57 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/discord.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: discord + namespace: sentinel +spec: + # Stays at 1 — multiple replicas would each open their own gateway + # connection and double-process every Discord event. + replicas: 1 + selector: + matchLabels: + app: discord + template: + metadata: + labels: + app: discord + spec: + containers: + - name: discord + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-discord:latest + ports: + - containerPort: 9998 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9998" + - name: DATABASE_HOST + value: postgres.sentinel.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: sentinel + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: POSTGRES_PASSWORD + # Resolve inter-service calls through the kerbecs gateway's admin + # API (replaces the rincon registry). Same admin creds the kerbecs + # deployment uses. + - name: KERBECS_ENDPOINT + value: http://kerbecs.sentinel.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: KERBECS_PASSWORD + - name: DISCORD_GUILD + value: "756738476887638107" + - name: DISCORD_PREFIX + value: "!" + - name: WEB_BASE_URL + value: https://sentinel-v5.gauchoracing.com + - name: DISCORD_TOKEN + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: DISCORD_TOKEN + # Same value as on core; used at startup to exchange for + # this service's pre-seeded bearer JWT. + - name: INTERNAL_BOOTSTRAP_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: INTERNAL_BOOTSTRAP_SECRET +--- +apiVersion: v1 +kind: Service +metadata: + name: discord + namespace: sentinel +spec: + selector: + app: discord + ports: + - port: 9998 + targetPort: 9998 diff --git a/kubernetes/manifests-foundry/sentinel/google.yaml b/kubernetes/manifests-foundry/sentinel/google.yaml new file mode 100644 index 0000000..845f125 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/google.yaml @@ -0,0 +1,105 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: google + namespace: sentinel +spec: + # Single replica: the service runs a reconcile cron that mirrors group + # membership out to Google Groups. With >1 replica each pod runs its own + # cron, so two sweeps could write the same Google Group concurrently. The + # writes are idempotent, but the cancel-and-restart coalescing and the + # GOOGLE_SYNC_MAX_REMOVALS guard are per-process — keep one cron owner. This + # is low-volume periodic work, so one replica is plenty. + replicas: 1 + selector: + matchLabels: + app: google + template: + metadata: + labels: + app: google + spec: + # Topology spread — see comment in core.yaml. Inert at replicas: 1. + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: google + containers: + - name: google + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-google:latest + ports: + - containerPort: 9995 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9995" + - name: DATABASE_HOST + value: postgres.sentinel.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: sentinel + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: POSTGRES_PASSWORD + # Resolve inter-service calls through the kerbecs gateway's admin + # API. Same admin creds the kerbecs deployment uses. + - name: KERBECS_ENDPOINT + value: http://kerbecs.sentinel.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: KERBECS_PASSWORD + # Shared secret exchanged at startup for this service's pre-seeded + # bearer JWT. Same value as every other sentinel-* deployment. + - name: INTERNAL_BOOTSTRAP_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: INTERNAL_BOOTSTRAP_SECRET + # Service-account JSON key for domain-wide delegation (scope + # admin.directory.group.member). The only real secret here — marked + # optional so the pod still boots before it's populated; sync just + # no-ops until it's set (then restart the pod to pick it up). + - name: GOOGLE_SERVICE_ACCOUNT + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: GOOGLE_SERVICE_ACCOUNT + optional: true + # The super-admin the service account impersonates. Just an email, + # not a credential, so it's a plain value rather than a secret. + - name: GOOGLE_ADMIN_SUBJECT + value: admin@gauchoracing.com +--- +apiVersion: v1 +kind: Service +metadata: + name: google + namespace: sentinel +spec: + selector: + app: google + ports: + - port: 9995 + targetPort: 9995 diff --git a/kubernetes/manifests-foundry/sentinel/ingress.yaml b/kubernetes/manifests-foundry/sentinel/ingress.yaml new file mode 100644 index 0000000..59229c1 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/ingress.yaml @@ -0,0 +1,28 @@ +# Public entrypoint on the foundry k3s cluster. Traefik receives plain +# HTTP forwarded by the in-cluster cloudflared tunnel; TLS terminates at +# the Cloudflare edge. Host-based routing to the kerbecs gateway, which +# then routes to core/oauth/discord/saml/google/web internally. +# +# The public DNS record for sentinel-v5.gauchoracing.com is managed by +# Terraform (foundry.tf) as a CNAME to .cfargotunnel.com — +# external-dns is intentionally NOT annotating this Ingress, so the +# record stays terraform-owned during and after cutover. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sentinel + namespace: sentinel +spec: + ingressClassName: traefik + rules: + - host: sentinel-v5.gauchoracing.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kerbecs + port: + number: 10310 diff --git a/kubernetes/manifests-foundry/sentinel/kerbecs.yaml b/kubernetes/manifests-foundry/sentinel/kerbecs.yaml new file mode 100644 index 0000000..a8ec737 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/kerbecs.yaml @@ -0,0 +1,79 @@ +# Gateway. Public traffic enters through the ALB (see ingress.yaml), +# kerbecs routes to the right upstream Service based on its config in +# the kerbecs-config ConfigMap. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kerbecs + namespace: sentinel +spec: + replicas: 1 + selector: + matchLabels: + app: kerbecs + template: + metadata: + labels: + app: kerbecs + spec: + # Topology spread — see comment in core.yaml. + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: kerbecs + containers: + - name: kerbecs + image: bk1031/kerbecs:latest + ports: + - containerPort: 10310 + name: gateway + - containerPort: 10300 + name: admin + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: KERBECS_CONFIG + value: /etc/kerbecs/kerbecs.yaml + - name: ENV + value: PROD + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: KERBECS_PASSWORD + volumeMounts: + - name: kerbecs-config + mountPath: /etc/kerbecs + readOnly: true + volumes: + - name: kerbecs-config + configMap: + name: kerbecs-config +--- +apiVersion: v1 +kind: Service +metadata: + name: kerbecs + namespace: sentinel +spec: + selector: + app: kerbecs + ports: + - name: gateway + port: 10310 + targetPort: 10310 + - name: admin + port: 10300 + targetPort: 10300 diff --git a/kubernetes/manifests-foundry/sentinel/kustomization.yaml b/kubernetes/manifests-foundry/sentinel/kustomization.yaml new file mode 100644 index 0000000..224aa4f --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/kustomization.yaml @@ -0,0 +1,47 @@ +# Kustomize entrypoint — ArgoCD auto-detects this file and runs +# `kustomize build` instead of treating the dir as a flat manifest set. +# +# The `images` block below is the single source of truth for sentinel +# service versions. Bumping a release looks like: +# +# kustomize edit set image \ +# ghcr.io/gaucho-racing/sentinel-core=ghcr.io/gaucho-racing/sentinel-core:5.2.0 +# +# (Run from this directory; CI can wrap it across all four images on +# tag push.) That edit only touches this file — the Deployment YAMLs +# stay unchanged, so the diff in a release PR is one block. +# +# rincon and kerbecs are on independent version streams from the +# sentinel services — bump them separately when there's a reason to. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - configmap-kerbecs.yaml + - postgres.yaml + - kerbecs.yaml + - core.yaml + - oauth.yaml + - discord.yaml + - saml.yaml + - google.yaml + - web.yaml + - ingress.yaml + +images: + - name: ghcr.io/gaucho-racing/sentinel-core + newTag: 5.8.1 + - name: ghcr.io/gaucho-racing/sentinel-oauth + newTag: 5.8.1 + - name: ghcr.io/gaucho-racing/sentinel-discord + newTag: 5.8.1 + - name: ghcr.io/gaucho-racing/sentinel-saml + newTag: 5.8.1 + - name: ghcr.io/gaucho-racing/sentinel-google + newTag: 5.8.1 + - name: ghcr.io/gaucho-racing/sentinel-web + newTag: 5.8.1 + - name: bk1031/kerbecs + newTag: 3.2.0 diff --git a/kubernetes/manifests-foundry/sentinel/namespace.yaml b/kubernetes/manifests-foundry/sentinel/namespace.yaml new file mode 100644 index 0000000..a9464d0 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: sentinel diff --git a/kubernetes/manifests-foundry/sentinel/oauth.yaml b/kubernetes/manifests-foundry/sentinel/oauth.yaml new file mode 100644 index 0000000..e2b1969 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/oauth.yaml @@ -0,0 +1,115 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oauth + namespace: sentinel +spec: + replicas: 1 + selector: + matchLabels: + app: oauth + template: + metadata: + labels: + app: oauth + spec: + # Topology spread — see comment in core.yaml. + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: oauth + containers: + - name: oauth + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-oauth:latest + ports: + - containerPort: 9997 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9997" + - name: DATABASE_HOST + value: postgres.sentinel.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: sentinel + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: POSTGRES_PASSWORD + # Resolve inter-service calls through the kerbecs gateway's admin + # API (replaces the rincon registry). Same admin creds the kerbecs + # deployment uses. + - name: KERBECS_ENDPOINT + value: http://kerbecs.sentinel.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: KERBECS_PASSWORD + # OIDC issuer — the public base URL. MUST match core's ISSUER + # exactly; the discovery document's `issuer` and the `iss` claim in + # ID tokens have to be byte-identical for relying parties. + - name: ISSUER + value: https://sentinel-v5.gauchoracing.com + # Discord OAuth client config for the "Continue with Discord" + # login button. Client id/secret are the Discord application's + # credentials; the redirect URI must byte-match what the web + # client sends in its authorize call (and what's registered + # on the Discord application's OAuth2 settings). + - name: DISCORD_CLIENT_ID + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: DISCORD_CLIENT_ID + - name: DISCORD_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: DISCORD_CLIENT_SECRET + - name: DISCORD_REDIRECT_URI + value: https://sentinel-v5.gauchoracing.com/auth/login/discord + # Shared secret exchanged at startup for this service's + # pre-seeded bearer JWT. Same value as every other + # sentinel-* deployment. + - name: INTERNAL_BOOTSTRAP_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: INTERNAL_BOOTSTRAP_SECRET + # client_id of the shared Google Workspace account application + # (team@gauchoracing.com). The oauth service overrides identity + # claims for this client so authorized users sign in as the shared + # account. Not a secret — the client_secret is what's sensitive. + - name: TEAM_GOOGLE_CLIENT_ID + value: uozOEnbeoJ2t +--- +apiVersion: v1 +kind: Service +metadata: + name: oauth + namespace: sentinel +spec: + selector: + app: oauth + ports: + - port: 9997 + targetPort: 9997 diff --git a/kubernetes/manifests-foundry/sentinel/postgres.yaml b/kubernetes/manifests-foundry/sentinel/postgres.yaml new file mode 100644 index 0000000..a207d06 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/postgres.yaml @@ -0,0 +1,17 @@ +# In-cluster handle for the gr-postgres EC2 (still in AWS after the +# compute migration — same instance backing mapache + vault). +# +# From on-prem, this resolves via the Cloudflare A record +# gr-postgres.gauchoracing.com (gray-cloud, unproxied) to the EIP +# 52.32.150.44. Traffic goes over the public internet on 5432; the +# 32-char scram-sha-256 password is the only gate. The SG on the EC2 +# allows 0.0.0.0/0 on 5432 (admin_cidr_blocks in modules/postgres-ec2). + +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: sentinel +spec: + type: ExternalName + externalName: gr-postgres.gauchoracing.com diff --git a/kubernetes/manifests-foundry/sentinel/saml.yaml b/kubernetes/manifests-foundry/sentinel/saml.yaml new file mode 100644 index 0000000..7709345 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/saml.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: saml + namespace: sentinel +spec: + # Single replica for now: on the very first boot the service generates and + # persists its SAML signing key + self-signed cert (saml_signing_key table). + # With >1 replica racing an empty table, two pods could each insert an active + # key and serve different certs, breaking SP signature verification. Once the + # row exists every replica loads the same key, so this can safely go to 2 + # after the first successful deploy. The IdP is interactive and low-volume, so + # one replica is fine in the meantime. + replicas: 1 + selector: + matchLabels: + app: saml + template: + metadata: + labels: + app: saml + spec: + # Topology spread — see comment in core.yaml. Inert at replicas: 1, + # kept so scaling up later spreads pods without an edit here. + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: saml + containers: + - name: saml + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-saml:latest + ports: + - containerPort: 9996 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9996" + - name: DATABASE_HOST + value: postgres.sentinel.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: sentinel + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: POSTGRES_PASSWORD + # Resolve inter-service calls through the kerbecs gateway's admin + # API (replaces the rincon registry). Same admin creds the kerbecs + # deployment uses. + - name: KERBECS_ENDPOINT + value: http://kerbecs.sentinel.svc.cluster.local:10300 + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: KERBECS_PASSWORD + # Public base URL. Doubles as the SAML IdP entityID (the metadata + # URL is derived from it), so it MUST match core's ISSUER exactly + # and stay byte-stable — changing it invalidates trust with every + # registered service provider. + - name: ISSUER + value: https://sentinel-v5.gauchoracing.com + # Shared secret exchanged at startup for this service's + # pre-seeded bearer JWT. Same value as every other + # sentinel-* deployment. + - name: INTERNAL_BOOTSTRAP_SECRET + valueFrom: + secretKeyRef: + name: sentinel-secrets + key: INTERNAL_BOOTSTRAP_SECRET +--- +apiVersion: v1 +kind: Service +metadata: + name: saml + namespace: sentinel +spec: + selector: + app: saml + ports: + - port: 9996 + targetPort: 9996 diff --git a/kubernetes/manifests-foundry/sentinel/web.yaml b/kubernetes/manifests-foundry/sentinel/web.yaml new file mode 100644 index 0000000..f6a30a0 --- /dev/null +++ b/kubernetes/manifests-foundry/sentinel/web.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web + namespace: sentinel +spec: + replicas: 1 + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + # Topology spread — see comment in core.yaml. + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: web + containers: + - name: web + # Tag is set by kustomization.yaml's images block. + image: ghcr.io/gaucho-racing/sentinel-web:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: web + namespace: sentinel +spec: + selector: + app: web + ports: + - port: 80 + targetPort: 80 diff --git a/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml b/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml new file mode 100644 index 0000000..a6f2d11 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kerbecs-config + namespace: vault +data: + kerbecs.yaml: | + gateway: + name: vault-gateway + version: 1.0.0 + env: ${ENV:PROD} + timeouts: + dial: 5s + headers: 30s + idle: 50s + overall: 0 + + listeners: + gateway: + port: "10310" + cors: + enabled: true + allowed_origins: ["*"] + allow_credentials: false + admin: + port: "10300" + auth: + type: basic + username: ${KERBECS_USER:admin} + password: ${KERBECS_PASSWORD:admin} + + providers: + static: + watch: true + + upstreams: + vault: + name: vault + version: 1.0.0 + instances: + - http://vault.vault.svc.cluster.local:9999 + + web: + name: vault-web + version: 1.0.0 + instances: + - http://web.vault.svc.cluster.local:80 + + routes: + - name: vault-api + match: + path: /api/* + upstream: vault + rewrite: + strip_prefix: /api + envelope: passthrough + + - name: vault-web + match: + path: /* + upstream: web + envelope: passthrough diff --git a/kubernetes/manifests-foundry/vault/ingress.yaml b/kubernetes/manifests-foundry/vault/ingress.yaml new file mode 100644 index 0000000..4de63f8 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/ingress.yaml @@ -0,0 +1,24 @@ +# Public entrypoint on the foundry k3s cluster. Traefik receives plain +# HTTP forwarded by the in-cluster cloudflared tunnel; TLS terminates at +# the Cloudflare edge. Host-based routing to the kerbecs gateway. +# +# DNS for vault.gauchoracing.com is managed by Terraform (foundry.tf). + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vault + namespace: vault +spec: + ingressClassName: traefik + rules: + - host: vault.gauchoracing.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kerbecs + port: + number: 10310 diff --git a/kubernetes/manifests-foundry/vault/kerbecs.yaml b/kubernetes/manifests-foundry/vault/kerbecs.yaml new file mode 100644 index 0000000..e968692 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/kerbecs.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kerbecs + namespace: vault +spec: + replicas: 1 + selector: + matchLabels: + app: kerbecs + template: + metadata: + labels: + app: kerbecs + spec: + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: kerbecs + containers: + - name: kerbecs + image: bk1031/kerbecs:latest + ports: + - containerPort: 10310 + name: gateway + - containerPort: 10300 + name: admin + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: KERBECS_CONFIG + value: /etc/kerbecs/kerbecs.yaml + - name: ENV + value: PROD + - name: KERBECS_USER + value: admin + - name: KERBECS_PASSWORD + valueFrom: + secretKeyRef: + name: vault-secrets + key: KERBECS_PASSWORD + volumeMounts: + - name: kerbecs-config + mountPath: /etc/kerbecs + readOnly: true + volumes: + - name: kerbecs-config + configMap: + name: kerbecs-config +--- +apiVersion: v1 +kind: Service +metadata: + name: kerbecs + namespace: vault +spec: + selector: + app: kerbecs + ports: + - name: gateway + port: 10310 + targetPort: 10310 + - name: admin + port: 10300 + targetPort: 10300 diff --git a/kubernetes/manifests-foundry/vault/kustomization.yaml b/kubernetes/manifests-foundry/vault/kustomization.yaml new file mode 100644 index 0000000..4168826 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - configmap-kerbecs.yaml + - postgres.yaml + - kerbecs.yaml + - server.yaml + - web.yaml + - ingress.yaml + +images: + - name: ghcr.io/gaucho-racing/vault-server + newTag: 1.5.0 + - name: ghcr.io/gaucho-racing/vault-web + newTag: 1.5.0 + - name: bk1031/kerbecs + newTag: 3.2.0 diff --git a/kubernetes/manifests-foundry/vault/namespace.yaml b/kubernetes/manifests-foundry/vault/namespace.yaml new file mode 100644 index 0000000..0158c8f --- /dev/null +++ b/kubernetes/manifests-foundry/vault/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vault diff --git a/kubernetes/manifests-foundry/vault/postgres.yaml b/kubernetes/manifests-foundry/vault/postgres.yaml new file mode 100644 index 0000000..6854be0 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/postgres.yaml @@ -0,0 +1,11 @@ +# In-cluster handle for the gr-postgres EC2. Same instance as sentinel + +# mapache; isolated by DB name (vault). + +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: vault +spec: + type: ExternalName + externalName: gr-postgres.gauchoracing.com diff --git a/kubernetes/manifests-foundry/vault/server.yaml b/kubernetes/manifests-foundry/vault/server.yaml new file mode 100644 index 0000000..3f1ef10 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/server.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vault + namespace: vault +spec: + replicas: 1 + selector: + matchLabels: + app: vault + template: + metadata: + labels: + app: vault + spec: + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: vault + containers: + - name: vault + image: ghcr.io/gaucho-racing/vault-server:latest + ports: + - containerPort: 9999 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + env: + - name: ENV + value: PROD + - name: PORT + value: "9999" + - name: DATABASE_HOST + value: postgres.vault.svc.cluster.local + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_USER + value: postgres + - name: DATABASE_NAME + value: vault + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: vault-secrets + key: POSTGRES_PASSWORD + - name: SENTINEL_URL + value: https://sentinel-v5.gauchoracing.com + - name: SENTINEL_CLIENT_ID + value: Ehf9Nl8SVs9w + - name: SENTINEL_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: vault-secrets + key: SENTINEL_CLIENT_SECRET + - name: SENTINEL_SA_TOKEN + valueFrom: + secretKeyRef: + name: vault-secrets + key: SENTINEL_SA_TOKEN + - name: SENTINEL_REDIRECT_URI + value: https://vault.gauchoracing.com/auth/login +--- +apiVersion: v1 +kind: Service +metadata: + name: vault + namespace: vault +spec: + selector: + app: vault + ports: + - port: 9999 + targetPort: 9999 diff --git a/kubernetes/manifests-foundry/vault/web.yaml b/kubernetes/manifests-foundry/vault/web.yaml new file mode 100644 index 0000000..4f2f1f0 --- /dev/null +++ b/kubernetes/manifests-foundry/vault/web.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web + namespace: vault +spec: + replicas: 1 + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + topologySpreadConstraints: + - maxSkew: 1 + minDomains: 2 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: web + containers: + - name: web + image: ghcr.io/gaucho-racing/vault-web:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: web + namespace: vault +spec: + selector: + app: web + ports: + - port: 80 + targetPort: 80 From 11672c2bf01dcaa4d670f5698f46cbb5120a5b5c Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Sun, 5 Jul 2026 20:48:33 -0700 Subject: [PATCH 2/3] chore(foundry): scope initial slice to sentinel only Drop mapache, vault, vault-k8s-operator, and argocd-server-ingress from apps-foundry + manifests-foundry. Remove them from the tunnel ingress rules and DNS records in foundry.tf. Only sentinel-v5.gauchoracing.com routes through the gr-foundry tunnel in this cutover; the other three hostnames stay on EKS until sentinel bakes. Adding a hostname later is one entry in the tunnel ingress list + one entry in local.foundry_hostnames + copying its manifest set from manifests/ to manifests-foundry/ with the same ingress + postgres adjustments used here. The initial commit's history is noisier this way (46 files added, 26 files removed) but avoids force-pushing during review. Squash-merge on merge if a clean history matters. --- infra/environments/prod/foundry.tf | 60 ++--- .../apps-foundry/argocd-server-ingress.yaml | 25 -- kubernetes/apps-foundry/mapache.yaml | 45 ---- .../apps-foundry/vault-k8s-operator.yaml | 27 --- kubernetes/apps-foundry/vault.yaml | 35 --- .../argocd-server-ingress/ingress.yaml | 35 --- .../manifests-foundry/mapache/auth.yaml | 96 -------- .../mapache/configmap-kerbecs.yaml | 225 ------------------ .../manifests-foundry/mapache/dashboard.yaml | 50 ---- .../manifests-foundry/mapache/foreman.yaml | 76 ------ .../manifests-foundry/mapache/gr26.yaml | 123 ---------- .../manifests-foundry/mapache/ingress.yaml | 25 -- .../manifests-foundry/mapache/kerbecs.yaml | 80 ------- .../mapache/kustomization.yaml | 54 ----- .../manifests-foundry/mapache/live.yaml | 80 ------- .../manifests-foundry/mapache/namespace.yaml | 4 - .../manifests-foundry/mapache/postgres.yaml | 15 -- .../manifests-foundry/mapache/query.yaml | 99 -------- .../manifests-foundry/mapache/vehicle.yaml | 76 ------ .../vault/configmap-kerbecs.yaml | 62 ----- .../manifests-foundry/vault/ingress.yaml | 24 -- .../manifests-foundry/vault/kerbecs.yaml | 74 ------ .../vault/kustomization.yaml | 19 -- .../manifests-foundry/vault/namespace.yaml | 4 - .../manifests-foundry/vault/postgres.yaml | 11 - .../manifests-foundry/vault/server.yaml | 81 ------- kubernetes/manifests-foundry/vault/web.yaml | 47 ---- 27 files changed, 23 insertions(+), 1529 deletions(-) delete mode 100644 kubernetes/apps-foundry/argocd-server-ingress.yaml delete mode 100644 kubernetes/apps-foundry/mapache.yaml delete mode 100644 kubernetes/apps-foundry/vault-k8s-operator.yaml delete mode 100644 kubernetes/apps-foundry/vault.yaml delete mode 100644 kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/auth.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/dashboard.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/foreman.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/gr26.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/ingress.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/kerbecs.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/kustomization.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/live.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/namespace.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/postgres.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/query.yaml delete mode 100644 kubernetes/manifests-foundry/mapache/vehicle.yaml delete mode 100644 kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml delete mode 100644 kubernetes/manifests-foundry/vault/ingress.yaml delete mode 100644 kubernetes/manifests-foundry/vault/kerbecs.yaml delete mode 100644 kubernetes/manifests-foundry/vault/kustomization.yaml delete mode 100644 kubernetes/manifests-foundry/vault/namespace.yaml delete mode 100644 kubernetes/manifests-foundry/vault/postgres.yaml delete mode 100644 kubernetes/manifests-foundry/vault/server.yaml delete mode 100644 kubernetes/manifests-foundry/vault/web.yaml diff --git a/infra/environments/prod/foundry.tf b/infra/environments/prod/foundry.tf index 60cc890..4607e78 100644 --- a/infra/environments/prod/foundry.tf +++ b/infra/environments/prod/foundry.tf @@ -1,11 +1,15 @@ # Foundry (on-prem k3s) cutover resources — the tunnel that connects the # on-prem cluster to Cloudflare, plus the public DNS records that route -# the four public hostnames to it. +# migrated hostnames to it. # -# Compute (EKS + ALB + ACM origin cert) still lives in main.tf. Once the -# cutover has soaked and the on-prem cluster owns traffic, delete the -# EKS-side resources from main.tf (module.eks / module.argocd / -# module.origin_cert) and remove kubernetes/apps/ from the repo. +# Scope of the first slice: sentinel-v5.gauchoracing.com only. mapache, +# vault, argocd all stay on EKS until this one bakes. Adding another +# hostname later is one entry in the ingress list + one entry in +# local.foundry_hostnames. +# +# Compute (EKS + ALB + ACM origin cert) still lives in main.tf. Once every +# hostname is migrated and soaked, delete module.eks / module.argocd / +# module.origin_cert from main.tf and remove kubernetes/apps/ from the repo. # # Off-cluster data services (gr-postgres, gr-mqtt, gr-clickhouse) stay in # AWS — the on-prem cluster reaches them over the public internet via @@ -13,22 +17,19 @@ # # Cutover sequence (short version): # -# 1. terraform apply this file (fails on step 3 records if EKS -# external-dns still owns them; delete those records via the CF -# dashboard first). +# 1. terraform apply this file. The sentinel-v5 record was previously +# owned by EKS external-dns — delete it via the CF dashboard first, +# or scale external-dns to 0 (kubectl -n external-dns scale deploy +# external-dns --replicas=0), so terraform doesn't hit a 409. # 2. Populate cloudflared-secrets on the on-prem cluster: # kubectl -n cloudflared create secret generic cloudflared-secrets \ # --from-literal=TUNNEL_TOKEN="$(terraform output -raw foundry_tunnel_token)" # 3. Apply kubernetes/bootstrap/root-foundry.yaml on the on-prem -# ArgoCD, populate the per-stack Secrets, wait for pods to reach -# Healthy against gr-postgres / gr-mqtt / gr-clickhouse. -# 4. On the EKS cluster: kubectl -n external-dns scale deploy -# external-dns --replicas=0 so it stops recreating the ALB records. -# 5. terraform apply again if step 1 stopped short. DNS records -# now CNAME to the tunnel; traffic starts landing on-prem within -# the CF TTL. -# 6. Bake. When confident, delete module.eks + module.argocd + -# module.origin_cert from main.tf and re-apply. +# ArgoCD, populate sentinel-secrets, wait for pods to reach Healthy +# against gr-postgres. +# 4. terraform apply again if step 1 stopped short. DNS flips +# sentinel-v5 to the tunnel within the CF TTL. +# 5. Bake. Add mapache/vault/argocd in follow-up PRs. # Account ID for the tunnel resources. The CF API token used by the # provider is scoped to a single account — return that account's ID. @@ -54,10 +55,10 @@ resource "cloudflare_zero_trust_tunnel_cloudflared" "foundry" { config_src = "cloudflare" } -# Ingress rules: every public hostname lands on the on-prem cluster's +# Ingress rules: every migrated hostname lands on the on-prem cluster's # Traefik service, and Traefik does host-based routing to the right # Service in the right namespace. Adding a new public hostname is one -# more block here + one more cloudflare_dns_record below. +# more block here + one more entry in local.foundry_hostnames. resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" { account_id = local.cloudflare_account_id tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.foundry.id @@ -68,18 +69,6 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" { hostname = "sentinel-v5.gauchoracing.com" service = "http://traefik.kube-system.svc.cluster.local:80" }, - { - hostname = "mapache.gauchoracing.com" - service = "http://traefik.kube-system.svc.cluster.local:80" - }, - { - hostname = "vault.gauchoracing.com" - service = "http://traefik.kube-system.svc.cluster.local:80" - }, - { - hostname = "argocd.gauchoracing.com" - service = "http://traefik.kube-system.svc.cluster.local:80" - }, # Catch-all — required last entry per CF tunnel ingress schema. { service = "http_status:404" @@ -100,15 +89,12 @@ data "cloudflare_zero_trust_tunnel_cloudflared_token" "foundry" { # hostname (.cfargotunnel.com); CF proxies + terminates TLS at the # edge and forwards HTTP through the tunnel. # -# These were previously created by EKS external-dns. Before first apply, -# either scale that Deployment to 0 or delete the records via the CF -# dashboard so terraform doesn't hit a 409. +# sentinel-v5 was previously created by EKS external-dns. Before first +# apply, either scale that Deployment to 0 or delete the record via the +# CF dashboard so terraform doesn't hit a 409. locals { foundry_hostnames = [ "sentinel-v5", - "mapache", - "vault", - "argocd", ] } diff --git a/kubernetes/apps-foundry/argocd-server-ingress.yaml b/kubernetes/apps-foundry/argocd-server-ingress.yaml deleted file mode 100644 index 1b77c7a..0000000 --- a/kubernetes/apps-foundry/argocd-server-ingress.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Adds the public argocd.gauchoracing.com Ingress to the argocd namespace -# on the foundry k3s cluster. Complements (doesn't replace) the internal -# argocd.internal.bk1031.dev Ingress that foundry-infra manages in the -# same namespace — different Ingress names, both point at argocd-server. - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: argocd-server-ingress - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - source: - repoURL: https://github.com/Gaucho-Racing/infrastructure.git - targetRevision: main - path: kubernetes/manifests-foundry/argocd-server-ingress - destination: - server: https://kubernetes.default.svc - namespace: argocd - syncPolicy: - automated: - prune: true - selfHeal: true diff --git a/kubernetes/apps-foundry/mapache.yaml b/kubernetes/apps-foundry/mapache.yaml deleted file mode 100644 index 312e94a..0000000 --- a/kubernetes/apps-foundry/mapache.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Mapache on the foundry k3s cluster. Same manifest set as the EKS tree -# (kubernetes/manifests/mapache/), diverging only in ingress.yaml -# (Traefik) and postgres.yaml (public Cloudflare hostname). -# -# All three off-cluster data services (gr-postgres, gr-mqtt, -# gr-clickhouse) stay in AWS and are reached over the public internet -# from on-prem via their gauchoracing.com hostnames. gr26 continues -# uploading Epic Shelter Parquet to s3://gaucho-racing/ using the same -# IAM keys — AWS_ACCESS_KEY_ID/SECRET stay in mapache-secrets. -# -# Manual Secret needed once after first sync: -# -# kubectl -n mapache create secret generic mapache-secrets \ -# --from-literal=DATABASE_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \ -# --from-literal=MQTT_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw mqtt_password)" \ -# --from-literal=CLICKHOUSE_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw clickhouse_admin_password)" \ -# --from-literal=KERBECS_PASSWORD=... \ -# --from-literal=SENTINEL_CLIENT_SECRET=... \ -# --from-literal=SENTINEL_SA_TOKEN=... \ -# --from-literal=AWS_ACCESS_KEY_ID=... \ -# --from-literal=AWS_SECRET_ACCESS_KEY=... - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: mapache - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - source: - repoURL: https://github.com/Gaucho-Racing/infrastructure.git - targetRevision: main - path: kubernetes/manifests-foundry/mapache - destination: - server: https://kubernetes.default.svc - namespace: mapache - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/kubernetes/apps-foundry/vault-k8s-operator.yaml b/kubernetes/apps-foundry/vault-k8s-operator.yaml deleted file mode 100644 index 2ba7dad..0000000 --- a/kubernetes/apps-foundry/vault-k8s-operator.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# vault-k8s-operator: mirrors the EKS install (kubernetes/apps/vault-k8s-operator.yaml). -# Same upstream chart, same version pin — the operator syncs secrets from -# vault-server into the cluster via VaultSecret CRDs. - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: vault-k8s-operator - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - source: - repoURL: https://github.com/Gaucho-Racing/vault-k8s-operator.git - targetRevision: v1.1.0 - path: config/default - destination: - server: https://kubernetes.default.svc - namespace: vault-k8s-operator-system - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/kubernetes/apps-foundry/vault.yaml b/kubernetes/apps-foundry/vault.yaml deleted file mode 100644 index 49618c4..0000000 --- a/kubernetes/apps-foundry/vault.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Vault on the foundry k3s cluster. Same manifest set as the EKS tree -# (kubernetes/manifests/vault/), diverging only in ingress.yaml (Traefik) -# and postgres.yaml (public Cloudflare hostname). -# -# Manual Secret needed once after first sync: -# -# kubectl -n vault create secret generic vault-secrets \ -# --from-literal=POSTGRES_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \ -# --from-literal=KERBECS_PASSWORD=... \ -# --from-literal=SENTINEL_CLIENT_SECRET=... \ -# --from-literal=SENTINEL_SA_TOKEN=... - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: vault - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - source: - repoURL: https://github.com/Gaucho-Racing/infrastructure.git - targetRevision: main - path: kubernetes/manifests-foundry/vault - destination: - server: https://kubernetes.default.svc - namespace: vault - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml b/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml deleted file mode 100644 index ac9cf1e..0000000 --- a/kubernetes/manifests-foundry/argocd-server-ingress/ingress.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Public Ingress for the ArgoCD UI on the foundry k3s cluster. -# The foundry-infra repo already manages an internal Ingress -# (argocd.internal.bk1031.dev) — this one adds the public gauchoracing.com -# hostname. Distinct Ingress name (argocd-server-public) to avoid colliding -# with the foundry-managed argocd-server Ingress in the same namespace. -# -# Requires the ArgoCD Helm values to have `server.insecure: true` so the -# argocd-server Service serves plain HTTP on port 80 (traffic path is -# Cloudflare edge → cloudflared → Traefik → argocd-server, all HTTP). -# If your foundry-infra argocd install doesn't have this, patch -# argocd-cmd-params-cm: -# kubectl -n argocd patch configmap argocd-cmd-params-cm \ -# --patch '{"data":{"server.insecure":"true"}}' -# kubectl -n argocd rollout restart deployment argocd-server -# -# DNS for argocd.gauchoracing.com is managed by Terraform (foundry.tf). - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: argocd-server-public - namespace: argocd -spec: - ingressClassName: traefik - rules: - - host: argocd.gauchoracing.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: argocd-server - port: - number: 80 diff --git a/kubernetes/manifests-foundry/mapache/auth.yaml b/kubernetes/manifests-foundry/mapache/auth.yaml deleted file mode 100644 index 11932a9..0000000 --- a/kubernetes/manifests-foundry/mapache/auth.yaml +++ /dev/null @@ -1,96 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: auth - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: auth - template: - metadata: - labels: - app: auth - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: auth - containers: - - name: auth - # Tag is set by kustomization.yaml's images block. - image: ghcr.io/gaucho-racing/mapache/auth:latest - ports: - - containerPort: 7002 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "7002" - - name: DATABASE_HOST - value: postgres.mapache.svc.cluster.local - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_USER - value: postgres - - name: DATABASE_NAME - value: mapache - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: DATABASE_PASSWORD - - name: KERBECS_ENDPOINT - value: http://kerbecs.mapache.svc.cluster.local:10300 - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: KERBECS_PASSWORD - # Sentinel v5: JWKS lives at /api/core/keys under SENTINEL_URL, - # so the separate JWKS env var is gone. SA token replaces the - # v4 static SENTINEL_TOKEN. - - name: SENTINEL_URL - value: https://sentinel-v5.gauchoracing.com - - name: SENTINEL_CLIENT_ID - value: TIvD6jCH3mGV - - name: SENTINEL_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: mapache-secrets - key: SENTINEL_CLIENT_SECRET - - name: SENTINEL_SA_TOKEN - valueFrom: - secretKeyRef: - name: mapache-secrets - key: SENTINEL_SA_TOKEN - - name: SENTINEL_REDIRECT_URI - value: https://mapache.gauchoracing.com/auth/login ---- -apiVersion: v1 -kind: Service -metadata: - name: auth - namespace: mapache -spec: - selector: - app: auth - ports: - - port: 7002 - targetPort: 7002 diff --git a/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml b/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml deleted file mode 100644 index 9f60375..0000000 --- a/kubernetes/manifests-foundry/mapache/configmap-kerbecs.yaml +++ /dev/null @@ -1,225 +0,0 @@ -# Kerbecs gateway routing rules. Mounted into the kerbecs pod at -# /etc/kerbecs/kerbecs.yaml — KERBECS_CONFIG points the binary at it. -# -# Mirrors the dev Mapache/kerbecs.yaml route table, with upstream -# hostnames switched to in-cluster Services. The dashboard upstream -# points at port 80 (the nginx-served prod build), unlike the dev -# compose where Vite serves on 5173. - -apiVersion: v1 -kind: ConfigMap -metadata: - name: kerbecs-config - namespace: mapache -data: - kerbecs.yaml: | - gateway: - name: mapache-gateway - version: 0.1.0 - env: ${ENV:PROD} - limits: - max_request_bytes: 100MB - max_response_bytes: 100MB - timeouts: - dial: 5s - headers: 30s - idle: 90s - overall: 0 - - listeners: - gateway: - port: "10310" - # CORS is set by each upstream service (gin-contrib/cors in Go - # services, fastapi.middleware.cors in query). Setting it here - # too caused duplicate Access-Control-Allow-Origin headers on - # passthrough routes (live SSE, gr26 WS, foreman events) which - # browsers reject. - cors: - enabled: false - admin: - port: "10300" - auth: - type: basic - username: ${KERBECS_USER:admin} - password: ${KERBECS_PASSWORD:admin} - - providers: - static: - # Hot-reload on ConfigMap updates (kerbecs >= 3.1.0). The watcher - # picks up the kubelet ..data symlink swap so route changes apply - # without restarting the pod. - watch: true - - upstreams: - auth: - name: mapache-auth - version: 0.1.0 - instances: - - http://auth.mapache.svc.cluster.local:7002 - - vehicle: - name: mapache-vehicle - version: 0.1.0 - instances: - - http://vehicle.mapache.svc.cluster.local:7003 - - gr26: - name: mapache-gr26 - version: 0.1.0 - instances: - - http://gr26.mapache.svc.cluster.local:7005 - - live: - name: mapache-live - version: 0.1.0 - instances: - - http://live.mapache.svc.cluster.local:7015 - - query: - name: mapache-query - version: 0.1.0 - instances: - - http://query.mapache.svc.cluster.local:7010 - - foreman: - name: mapache-foreman - version: 0.1.0 - instances: - - http://foreman.mapache.svc.cluster.local:7011 - - dashboard: - name: mapache-dashboard - version: 0.1.0 - instances: - - http://dashboard.mapache.svc.cluster.local:80 - - routes: - - name: auth - match: - path: /api/auth/* - upstream: auth - rewrite: - strip_prefix: /api - envelope: default - - - name: users - match: - path: /api/users/* - upstream: auth - rewrite: - strip_prefix: /api - envelope: default - - - name: vehicle-ping - match: - path: /api/vehicle/ping - upstream: vehicle - rewrite: - strip_prefix: /api - envelope: default - - - name: vehicles - match: - path: /api/vehicles - upstream: vehicle - rewrite: - strip_prefix: /api - envelope: default - - - name: vehicles-id - match: - path: /api/vehicles/* - upstream: vehicle - rewrite: - strip_prefix: /api - envelope: default - - - name: sessions - match: - path: /api/sessions - upstream: vehicle - rewrite: - strip_prefix: /api - envelope: default - - - name: sessions-id - match: - path: /api/sessions/* - upstream: vehicle - rewrite: - strip_prefix: /api - envelope: default - - - name: gr26-live - match: - path: /api/gr26/live - upstream: gr26 - rewrite: - strip_prefix: /api - envelope: passthrough - - - name: gr26 - match: - path: /api/gr26/* - upstream: gr26 - rewrite: - strip_prefix: /api - envelope: default - - # Specific WS/SSE routes need passthrough so kerbecs doesn't wrap - # the response envelope or interfere with the connection upgrade. - # The catch-all `live` route below uses default envelope for the - # plain HTTP endpoints (/live/ping, /live/stats). - - name: live-ws - match: - path: /api/live/ws - upstream: live - rewrite: - strip_prefix: /api - envelope: passthrough - - - name: live-sse - match: - path: /api/live/sse - upstream: live - rewrite: - strip_prefix: /api - envelope: passthrough - - - name: live - match: - path: /api/live/* - upstream: live - rewrite: - strip_prefix: /api - envelope: default - - - name: query - match: - path: /api/query/* - upstream: query - rewrite: - strip_prefix: /api - envelope: default - - - name: foreman-events - match: - path: /api/foreman/events/* - upstream: foreman - rewrite: - strip_prefix: /api - envelope: passthrough - - - name: foreman - match: - path: /api/foreman/* - upstream: foreman - rewrite: - strip_prefix: /api - envelope: default - - - name: dashboard - match: - path: /* - upstream: dashboard - envelope: passthrough diff --git a/kubernetes/manifests-foundry/mapache/dashboard.yaml b/kubernetes/manifests-foundry/mapache/dashboard.yaml deleted file mode 100644 index 2f66007..0000000 --- a/kubernetes/manifests-foundry/mapache/dashboard.yaml +++ /dev/null @@ -1,50 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: dashboard - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: dashboard - template: - metadata: - labels: - app: dashboard - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: dashboard - containers: - - name: dashboard - # Tag is set by kustomization.yaml's images block. - image: ghcr.io/gaucho-racing/mapache/dashboard:latest - ports: - - containerPort: 80 - resources: - requests: - cpu: 10m - memory: 32Mi - limits: - cpu: 100m - memory: 128Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: dashboard - namespace: mapache -spec: - selector: - app: dashboard - ports: - - port: 80 - targetPort: 80 diff --git a/kubernetes/manifests-foundry/mapache/foreman.yaml b/kubernetes/manifests-foundry/mapache/foreman.yaml deleted file mode 100644 index eca9ef6..0000000 --- a/kubernetes/manifests-foundry/mapache/foreman.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foreman - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: foreman - template: - metadata: - labels: - app: foreman - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: foreman - containers: - - name: foreman - # Standalone Foreman lives in github.com/gaucho-racing/Foreman now. - # The kustomization.yaml `images:` block pins the tag — :latest - # here is just the placeholder kustomize matches against. - image: ghcr.io/gaucho-racing/foreman:latest - ports: - - containerPort: 7011 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "7011" - - name: DATABASE_HOST - value: postgres.mapache.svc.cluster.local - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_USER - value: postgres - - name: DATABASE_NAME - value: mapache - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: DATABASE_PASSWORD - # Foreman shares the `mapache` database; default 'foreman_' - # prefix keeps foreman_jobs / foreman_job_runs / - # foreman_schedules namespaced away from host-app tables. - # Set explicitly so the choice is reviewable in the manifest. - - name: FOREMAN_TABLE_PREFIX - value: "foreman_" ---- -apiVersion: v1 -kind: Service -metadata: - name: foreman - namespace: mapache -spec: - selector: - app: foreman - ports: - - port: 7011 - targetPort: 7011 diff --git a/kubernetes/manifests-foundry/mapache/gr26.yaml b/kubernetes/manifests-foundry/mapache/gr26.yaml deleted file mode 100644 index 740b3cd..0000000 --- a/kubernetes/manifests-foundry/mapache/gr26.yaml +++ /dev/null @@ -1,123 +0,0 @@ -# gr26: telemetry ingest + the foreman worker pool. -# -# replicas=1 — the worker pool claims foreman jobs by lease, so scaling -# horizontally is supported once we know how much concurrency the Epic -# Shelter ingest needs. Start single-replica to keep the live MQTT -# subscriber simple. -# -# MQTT broker lives on the gr-mqtt EC2 outside the cluster. Pods reach -# it over the public hostname (gr-mqtt.gauchoracing.com); throughput is -# low enough that hairpinning out through the NAT gateway is fine. -# Auth uses the shared mapache-fleet credential (MQTT_USER=mapache). - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: gr26 - namespace: mapache -spec: - replicas: 3 - selector: - matchLabels: - app: gr26 - template: - metadata: - labels: - app: gr26 - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: gr26 - containers: - - name: gr26 - image: ghcr.io/gaucho-racing/mapache/gr26:latest - ports: - - containerPort: 7005 - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 1000m - memory: 1Gi - env: - - name: ENV - value: PROD - - name: PORT - value: "7005" - # Telemetry storage (signal, gr26_can, ping) moved off Postgres - # to ClickHouse — gr26 holds no Postgres connection anymore. - # The gr26 process auto-creates the mapache database on first - # connect via the always-present `default` DB. - - name: CLICKHOUSE_HOST - value: gr-clickhouse.gauchoracing.com - - name: CLICKHOUSE_PORT - value: "9000" - - name: CLICKHOUSE_USER - value: admin - - name: CLICKHOUSE_DATABASE - value: mapache - - name: CLICKHOUSE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: CLICKHOUSE_PASSWORD - - name: KERBECS_ENDPOINT - value: http://kerbecs.mapache.svc.cluster.local:10300 - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: KERBECS_PASSWORD - - name: MQTT_HOST - value: gr-mqtt.gauchoracing.com - - name: MQTT_PORT - value: "1883" - - name: MQTT_USER - value: mapache - - name: MQTT_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: MQTT_PASSWORD - - name: FOREMAN_ENDPOINT - value: http://foreman.mapache.svc.cluster.local:7011 - - name: NUM_WORKERS - value: "4" - - name: SHELTER_S3_BUCKET - value: gaucho-racing - - name: SHELTER_S3_REGION - value: us-west-2 - - name: SHELTER_S3_PREFIX - value: epic-shelter/tcm-26/ - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: mapache-secrets - key: AWS_ACCESS_KEY_ID - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: mapache-secrets - key: AWS_SECRET_ACCESS_KEY ---- -apiVersion: v1 -kind: Service -metadata: - name: gr26 - namespace: mapache -spec: - selector: - app: gr26 - ports: - - port: 7005 - targetPort: 7005 diff --git a/kubernetes/manifests-foundry/mapache/ingress.yaml b/kubernetes/manifests-foundry/mapache/ingress.yaml deleted file mode 100644 index 672a8db..0000000 --- a/kubernetes/manifests-foundry/mapache/ingress.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Public entrypoint on the foundry k3s cluster. Traefik receives plain -# HTTP forwarded by the in-cluster cloudflared tunnel; TLS terminates at -# the Cloudflare edge. Host-based routing to the kerbecs gateway, which -# routes to the backend services + dashboard internally. -# -# DNS for mapache.gauchoracing.com is managed by Terraform (foundry.tf). - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: mapache - namespace: mapache -spec: - ingressClassName: traefik - rules: - - host: mapache.gauchoracing.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: kerbecs - port: - number: 10310 diff --git a/kubernetes/manifests-foundry/mapache/kerbecs.yaml b/kubernetes/manifests-foundry/mapache/kerbecs.yaml deleted file mode 100644 index 3d1de4d..0000000 --- a/kubernetes/manifests-foundry/mapache/kerbecs.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# Gateway. Public traffic enters through the ALB (see ingress.yaml), -# kerbecs routes to the right upstream Service based on its config in -# the kerbecs-config ConfigMap. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kerbecs - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: kerbecs - template: - metadata: - labels: - app: kerbecs - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: kerbecs - containers: - - name: kerbecs - image: bk1031/kerbecs:latest - ports: - - containerPort: 10310 - name: gateway - - containerPort: 10300 - name: admin - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: KERBECS_CONFIG - value: /etc/kerbecs/kerbecs.yaml - - name: ENV - value: PROD - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: KERBECS_PASSWORD - volumeMounts: - - name: kerbecs-config - mountPath: /etc/kerbecs - readOnly: true - volumes: - - name: kerbecs-config - configMap: - name: kerbecs-config ---- -apiVersion: v1 -kind: Service -metadata: - name: kerbecs - namespace: mapache -spec: - selector: - app: kerbecs - ports: - - name: gateway - port: 10310 - targetPort: 10310 - - name: admin - port: 10300 - targetPort: 10300 diff --git a/kubernetes/manifests-foundry/mapache/kustomization.yaml b/kubernetes/manifests-foundry/mapache/kustomization.yaml deleted file mode 100644 index 0ce186d..0000000 --- a/kubernetes/manifests-foundry/mapache/kustomization.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# Kustomize entrypoint — ArgoCD auto-detects this file and runs -# `kustomize build` instead of treating the dir as a flat manifest set. -# -# The `images` block below is the single source of truth for mapache -# service versions. Bumping a release looks like: -# -# kustomize edit set image \ -# ghcr.io/gaucho-racing/mapache/auth=ghcr.io/gaucho-racing/mapache/auth:3.4.0 -# -# (Run from this directory; the per-service publish workflows will -# eventually wrap this across all services on tag push.) That edit only -# touches this file — the Deployment YAMLs stay unchanged, so the diff -# in a release PR is one block. -# -# kerbecs is on its own version stream from the mapache services — -# bump it separately when there's a reason to. - -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - namespace.yaml - - configmap-kerbecs.yaml - - postgres.yaml - - kerbecs.yaml - - auth.yaml - - vehicle.yaml - - gr26.yaml - - live.yaml - - query.yaml - - foreman.yaml - - dashboard.yaml - - ingress.yaml - -images: - - name: ghcr.io/gaucho-racing/mapache/auth - newTag: 3.9.6 - - name: ghcr.io/gaucho-racing/mapache/vehicle - newTag: 3.9.6 - - name: ghcr.io/gaucho-racing/mapache/gr26 - newTag: 3.9.6 - - name: ghcr.io/gaucho-racing/mapache/live - newTag: 3.9.6 - - name: ghcr.io/gaucho-racing/mapache/query - newTag: 3.9.6 - # Foreman is its own project now (github.com/gaucho-racing/Foreman) - # with its own release cadence. Track via its semver tags, not - # mapache's. Mapache services keep tracking together via newTag above. - - name: ghcr.io/gaucho-racing/foreman - newTag: 2.0.2 - - name: ghcr.io/gaucho-racing/mapache/dashboard - newTag: 3.9.6 - - name: bk1031/kerbecs - newTag: 3.2.0 diff --git a/kubernetes/manifests-foundry/mapache/live.yaml b/kubernetes/manifests-foundry/mapache/live.yaml deleted file mode 100644 index 25344b1..0000000 --- a/kubernetes/manifests-foundry/mapache/live.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# live: WebSocket / SSE signal-streaming service. -# -# Subscribes to the `query/live/#` republish topic (non-shared) so every -# replica sees every signal — this is what unblocks the dashboard from -# the gr26 shared-sub partial-visibility issue. The in-process Hub + -# WebSocket endpoint that used to live on gr26 was extracted into this -# service in v3.5.0 (Mapache commit 22102fa). -# -# Cache window (CACHE_WINDOW_SEC=60) is the ring buffer length each -# replica keeps for newly-connecting clients. State is per-pod; a pod -# restart drops its cache, and clients that reconnect onto another pod -# see only that pod's cache for the first 60s. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: live - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: live - template: - metadata: - labels: - app: live - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: live - containers: - - name: live - image: ghcr.io/gaucho-racing/mapache/live:latest - ports: - - containerPort: 7015 - resources: - requests: - cpu: 50m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "7015" - - name: MQTT_HOST - value: gr-mqtt.gauchoracing.com - - name: MQTT_PORT - value: "1883" - - name: MQTT_USER - value: mapache - - name: MQTT_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: MQTT_PASSWORD - - name: CACHE_WINDOW_SEC - value: "60" ---- -apiVersion: v1 -kind: Service -metadata: - name: live - namespace: mapache -spec: - selector: - app: live - ports: - - port: 7015 - targetPort: 7015 diff --git a/kubernetes/manifests-foundry/mapache/namespace.yaml b/kubernetes/manifests-foundry/mapache/namespace.yaml deleted file mode 100644 index f4e80c7..0000000 --- a/kubernetes/manifests-foundry/mapache/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: mapache diff --git a/kubernetes/manifests-foundry/mapache/postgres.yaml b/kubernetes/manifests-foundry/mapache/postgres.yaml deleted file mode 100644 index 43620cc..0000000 --- a/kubernetes/manifests-foundry/mapache/postgres.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# In-cluster handle for the gr-postgres EC2. Same instance as sentinel's -# postgres — DBs are isolated at the database level, not the instance. -# -# From on-prem, resolves via the Cloudflare A record -# gr-postgres.gauchoracing.com (unproxied) to the EIP. Traffic goes over -# the public internet on 5432; scram-sha-256 is the only gate. - -apiVersion: v1 -kind: Service -metadata: - name: postgres - namespace: mapache -spec: - type: ExternalName - externalName: gr-postgres.gauchoracing.com diff --git a/kubernetes/manifests-foundry/mapache/query.yaml b/kubernetes/manifests-foundry/mapache/query.yaml deleted file mode 100644 index 62d4171..0000000 --- a/kubernetes/manifests-foundry/mapache/query.yaml +++ /dev/null @@ -1,99 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: query - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: query - template: - metadata: - labels: - app: query - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: query - containers: - - name: query - image: ghcr.io/gaucho-racing/mapache/query:latest - ports: - - containerPort: 7010 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "7010" - - name: DATABASE_HOST - value: postgres.mapache.svc.cluster.local - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_USER - value: postgres - - name: DATABASE_NAME - value: mapache - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: DATABASE_PASSWORD - # ClickHouse holds the telemetry tables (signal, gr26_can, ping). - # The query service uses clickhouse-connect over HTTP, so it needs - # the HTTP port 8123 — distinct from gr26's native 9000. Password - # is shared with gr26 via mapache-secrets/CLICKHOUSE_PASSWORD. - - name: CLICKHOUSE_HOST - value: gr-clickhouse.gauchoracing.com - - name: CLICKHOUSE_PORT - value: "8123" - - name: CLICKHOUSE_USER - value: admin - - name: CLICKHOUSE_DATABASE - value: mapache - - name: CLICKHOUSE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: CLICKHOUSE_PASSWORD - - name: KERBECS_ENDPOINT - value: http://kerbecs.mapache.svc.cluster.local:10300 - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: KERBECS_PASSWORD - # Sentinel v5: JWKS lives at /api/core/keys under SENTINEL_URL, - # so the separate JWKS env var is gone (main.py derives it). - - name: SENTINEL_URL - value: https://sentinel-v5.gauchoracing.com - - name: SENTINEL_CLIENT_ID - value: TIvD6jCH3mGV ---- -apiVersion: v1 -kind: Service -metadata: - name: query - namespace: mapache -spec: - selector: - app: query - ports: - - port: 7010 - targetPort: 7010 diff --git a/kubernetes/manifests-foundry/mapache/vehicle.yaml b/kubernetes/manifests-foundry/mapache/vehicle.yaml deleted file mode 100644 index 314b23f..0000000 --- a/kubernetes/manifests-foundry/mapache/vehicle.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: vehicle - namespace: mapache -spec: - replicas: 1 - selector: - matchLabels: - app: vehicle - template: - metadata: - labels: - app: vehicle - spec: - # Spread across at least 2 nodes (minDomains) but tolerate skew - # up to 1 (so 3 replicas → [2,1], not forced [1,1,1]). - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: vehicle - containers: - - name: vehicle - image: ghcr.io/gaucho-racing/mapache/vehicle:latest - ports: - - containerPort: 7003 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "7003" - - name: DATABASE_HOST - value: postgres.mapache.svc.cluster.local - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_USER - value: postgres - - name: DATABASE_NAME - value: mapache - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: DATABASE_PASSWORD - - name: KERBECS_ENDPOINT - value: http://kerbecs.mapache.svc.cluster.local:10300 - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: mapache-secrets - key: KERBECS_PASSWORD ---- -apiVersion: v1 -kind: Service -metadata: - name: vehicle - namespace: mapache -spec: - selector: - app: vehicle - ports: - - port: 7003 - targetPort: 7003 diff --git a/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml b/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml deleted file mode 100644 index a6f2d11..0000000 --- a/kubernetes/manifests-foundry/vault/configmap-kerbecs.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: kerbecs-config - namespace: vault -data: - kerbecs.yaml: | - gateway: - name: vault-gateway - version: 1.0.0 - env: ${ENV:PROD} - timeouts: - dial: 5s - headers: 30s - idle: 50s - overall: 0 - - listeners: - gateway: - port: "10310" - cors: - enabled: true - allowed_origins: ["*"] - allow_credentials: false - admin: - port: "10300" - auth: - type: basic - username: ${KERBECS_USER:admin} - password: ${KERBECS_PASSWORD:admin} - - providers: - static: - watch: true - - upstreams: - vault: - name: vault - version: 1.0.0 - instances: - - http://vault.vault.svc.cluster.local:9999 - - web: - name: vault-web - version: 1.0.0 - instances: - - http://web.vault.svc.cluster.local:80 - - routes: - - name: vault-api - match: - path: /api/* - upstream: vault - rewrite: - strip_prefix: /api - envelope: passthrough - - - name: vault-web - match: - path: /* - upstream: web - envelope: passthrough diff --git a/kubernetes/manifests-foundry/vault/ingress.yaml b/kubernetes/manifests-foundry/vault/ingress.yaml deleted file mode 100644 index 4de63f8..0000000 --- a/kubernetes/manifests-foundry/vault/ingress.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Public entrypoint on the foundry k3s cluster. Traefik receives plain -# HTTP forwarded by the in-cluster cloudflared tunnel; TLS terminates at -# the Cloudflare edge. Host-based routing to the kerbecs gateway. -# -# DNS for vault.gauchoracing.com is managed by Terraform (foundry.tf). - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: vault - namespace: vault -spec: - ingressClassName: traefik - rules: - - host: vault.gauchoracing.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: kerbecs - port: - number: 10310 diff --git a/kubernetes/manifests-foundry/vault/kerbecs.yaml b/kubernetes/manifests-foundry/vault/kerbecs.yaml deleted file mode 100644 index e968692..0000000 --- a/kubernetes/manifests-foundry/vault/kerbecs.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kerbecs - namespace: vault -spec: - replicas: 1 - selector: - matchLabels: - app: kerbecs - template: - metadata: - labels: - app: kerbecs - spec: - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: kerbecs - containers: - - name: kerbecs - image: bk1031/kerbecs:latest - ports: - - containerPort: 10310 - name: gateway - - containerPort: 10300 - name: admin - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: KERBECS_CONFIG - value: /etc/kerbecs/kerbecs.yaml - - name: ENV - value: PROD - - name: KERBECS_USER - value: admin - - name: KERBECS_PASSWORD - valueFrom: - secretKeyRef: - name: vault-secrets - key: KERBECS_PASSWORD - volumeMounts: - - name: kerbecs-config - mountPath: /etc/kerbecs - readOnly: true - volumes: - - name: kerbecs-config - configMap: - name: kerbecs-config ---- -apiVersion: v1 -kind: Service -metadata: - name: kerbecs - namespace: vault -spec: - selector: - app: kerbecs - ports: - - name: gateway - port: 10310 - targetPort: 10310 - - name: admin - port: 10300 - targetPort: 10300 diff --git a/kubernetes/manifests-foundry/vault/kustomization.yaml b/kubernetes/manifests-foundry/vault/kustomization.yaml deleted file mode 100644 index 4168826..0000000 --- a/kubernetes/manifests-foundry/vault/kustomization.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - namespace.yaml - - configmap-kerbecs.yaml - - postgres.yaml - - kerbecs.yaml - - server.yaml - - web.yaml - - ingress.yaml - -images: - - name: ghcr.io/gaucho-racing/vault-server - newTag: 1.5.0 - - name: ghcr.io/gaucho-racing/vault-web - newTag: 1.5.0 - - name: bk1031/kerbecs - newTag: 3.2.0 diff --git a/kubernetes/manifests-foundry/vault/namespace.yaml b/kubernetes/manifests-foundry/vault/namespace.yaml deleted file mode 100644 index 0158c8f..0000000 --- a/kubernetes/manifests-foundry/vault/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: vault diff --git a/kubernetes/manifests-foundry/vault/postgres.yaml b/kubernetes/manifests-foundry/vault/postgres.yaml deleted file mode 100644 index 6854be0..0000000 --- a/kubernetes/manifests-foundry/vault/postgres.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# In-cluster handle for the gr-postgres EC2. Same instance as sentinel + -# mapache; isolated by DB name (vault). - -apiVersion: v1 -kind: Service -metadata: - name: postgres - namespace: vault -spec: - type: ExternalName - externalName: gr-postgres.gauchoracing.com diff --git a/kubernetes/manifests-foundry/vault/server.yaml b/kubernetes/manifests-foundry/vault/server.yaml deleted file mode 100644 index 3f1ef10..0000000 --- a/kubernetes/manifests-foundry/vault/server.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: vault - namespace: vault -spec: - replicas: 1 - selector: - matchLabels: - app: vault - template: - metadata: - labels: - app: vault - spec: - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: vault - containers: - - name: vault - image: ghcr.io/gaucho-racing/vault-server:latest - ports: - - containerPort: 9999 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - env: - - name: ENV - value: PROD - - name: PORT - value: "9999" - - name: DATABASE_HOST - value: postgres.vault.svc.cluster.local - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_USER - value: postgres - - name: DATABASE_NAME - value: vault - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: vault-secrets - key: POSTGRES_PASSWORD - - name: SENTINEL_URL - value: https://sentinel-v5.gauchoracing.com - - name: SENTINEL_CLIENT_ID - value: Ehf9Nl8SVs9w - - name: SENTINEL_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: vault-secrets - key: SENTINEL_CLIENT_SECRET - - name: SENTINEL_SA_TOKEN - valueFrom: - secretKeyRef: - name: vault-secrets - key: SENTINEL_SA_TOKEN - - name: SENTINEL_REDIRECT_URI - value: https://vault.gauchoracing.com/auth/login ---- -apiVersion: v1 -kind: Service -metadata: - name: vault - namespace: vault -spec: - selector: - app: vault - ports: - - port: 9999 - targetPort: 9999 diff --git a/kubernetes/manifests-foundry/vault/web.yaml b/kubernetes/manifests-foundry/vault/web.yaml deleted file mode 100644 index 4f2f1f0..0000000 --- a/kubernetes/manifests-foundry/vault/web.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: web - namespace: vault -spec: - replicas: 1 - selector: - matchLabels: - app: web - template: - metadata: - labels: - app: web - spec: - topologySpreadConstraints: - - maxSkew: 1 - minDomains: 2 - topologyKey: kubernetes.io/hostname - whenUnsatisfiable: DoNotSchedule - labelSelector: - matchLabels: - app: web - containers: - - name: web - image: ghcr.io/gaucho-racing/vault-web:latest - ports: - - containerPort: 80 - resources: - requests: - cpu: 10m - memory: 32Mi - limits: - cpu: 100m - memory: 128Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: web - namespace: vault -spec: - selector: - app: web - ports: - - port: 80 - targetPort: 80 From 10be2b4d69f2cf2b63ecdbc663656d905e907525 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Sun, 5 Jul 2026 21:27:43 -0700 Subject: [PATCH 3/3] refactor(foundry): catch-all tunnel + external-dns for multi-cluster scaling Replaces the explicit-hostname tunnel config + terraform-managed DNS records with a single catch-all tunnel rule that forwards everything to Traefik, and delegates DNS reconciliation to external-dns on the foundry cluster. Adding a new hostname is now one edit: the Ingress in the service's manifest set. No terraform change, no CF dashboard step. Same pattern scales to N clusters: each cluster runs its own tunnel + its own external-dns with a distinct txtOwnerId, managing its own disjoint hostname set in the shared gauchoracing.com zone. - foundry.tf: tunnel config -> single catch-all rule; drop cloudflare_dns_record.foundry + local.foundry_hostnames; update runbook comment for the new bootstrap sequence. - apps-foundry/external-dns.yaml: Helm-chart Application with txtOwnerId=gr-foundry, txtPrefix=fdry-. Reads the tunnel UUID from a manually-created ConfigMap (populated from `terraform output`) so the tunnel ID never has to live in git. --default-targets forces every record to CNAME the tunnel regardless of the Ingress's LB status field. - manifests-foundry/sentinel/ingress.yaml: add external-dns.alpha.kubernetes.io/cloudflare-proxied annotation so external-dns writes the record orange-cloud. --- infra/environments/prod/foundry.tf | 106 ++++++++---------- kubernetes/apps-foundry/external-dns.yaml | 99 ++++++++++++++++ .../manifests-foundry/sentinel/ingress.yaml | 11 +- 3 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 kubernetes/apps-foundry/external-dns.yaml diff --git a/infra/environments/prod/foundry.tf b/infra/environments/prod/foundry.tf index 4607e78..d8f6115 100644 --- a/infra/environments/prod/foundry.tf +++ b/infra/environments/prod/foundry.tf @@ -1,35 +1,50 @@ -# Foundry (on-prem k3s) cutover resources — the tunnel that connects the -# on-prem cluster to Cloudflare, plus the public DNS records that route -# migrated hostnames to it. +# Foundry (on-prem k3s) cutover resources — the shared Cloudflare Tunnel +# that connects the on-prem cluster to Cloudflare. Public DNS records for +# individual hostnames are managed by external-dns on the foundry cluster +# (see kubernetes/apps-foundry/external-dns.yaml), not here. # -# Scope of the first slice: sentinel-v5.gauchoracing.com only. mapache, -# vault, argocd all stay on EKS until this one bakes. Adding another -# hostname later is one entry in the ingress list + one entry in -# local.foundry_hostnames. +# Design: the tunnel has a single catch-all ingress rule that forwards +# ALL traffic to the Traefik service. Traefik does Host-based routing +# in-cluster to the right namespace. Adding a new public hostname is +# just an Ingress resource in that service's manifest set — external-dns +# writes the CNAME automatically, no terraform edit needed. Same shape +# works for a second cluster (gr-other-cluster with its own tunnel + own +# external-dns using txtOwnerId=gr-other-cluster). # -# Compute (EKS + ALB + ACM origin cert) still lives in main.tf. Once every -# hostname is migrated and soaked, delete module.eks / module.argocd / -# module.origin_cert from main.tf and remove kubernetes/apps/ from the repo. +# Compute (EKS + ALB + ACM origin cert) still lives in main.tf. Once +# every hostname is migrated off EKS and the on-prem stack has soaked, +# delete module.eks / module.argocd / module.origin_cert from main.tf +# and remove kubernetes/apps/ from the repo. # -# Off-cluster data services (gr-postgres, gr-mqtt, gr-clickhouse) stay in -# AWS — the on-prem cluster reaches them over the public internet via +# Off-cluster data services (gr-postgres, gr-mqtt, gr-clickhouse) stay +# in AWS — the on-prem cluster reaches them over the public internet via # their existing Cloudflare A records. # -# Cutover sequence (short version): +# Cutover sequence for the first slice (sentinel): # -# 1. terraform apply this file. The sentinel-v5 record was previously -# owned by EKS external-dns — delete it via the CF dashboard first, -# or scale external-dns to 0 (kubectl -n external-dns scale deploy -# external-dns --replicas=0), so terraform doesn't hit a 409. -# 2. Populate cloudflared-secrets on the on-prem cluster: +# 1. terraform apply this file. Creates the tunnel; no DNS records +# yet, so no conflict with EKS external-dns. +# 2. Populate cloudflared-secrets + external-dns-config on foundry: # kubectl -n cloudflared create secret generic cloudflared-secrets \ # --from-literal=TUNNEL_TOKEN="$(terraform output -raw foundry_tunnel_token)" -# 3. Apply kubernetes/bootstrap/root-foundry.yaml on the on-prem -# ArgoCD, populate sentinel-secrets, wait for pods to reach Healthy -# against gr-postgres. -# 4. terraform apply again if step 1 stopped short. DNS flips -# sentinel-v5 to the tunnel within the CF TTL. -# 5. Bake. Add mapache/vault/argocd in follow-up PRs. +# kubectl -n external-dns create configmap foundry-tunnel-target \ +# --from-literal=target="$(terraform output -raw foundry_tunnel_id).cfargotunnel.com" +# kubectl -n external-dns create secret generic cloudflare-api-token \ +# --from-literal=api-token="$CLOUDFLARE_API_TOKEN" +# 3. Apply kubernetes/bootstrap/root-foundry.yaml on the foundry +# ArgoCD, populate sentinel-secrets, wait for pods Healthy. +# 4. Cutover for one hostname: +# a. Delete the existing sentinel-v5.gauchoracing.com record via CF +# dashboard (or scale EKS external-dns to 0 first — it'll take +# its four records with it). +# b. Foundry external-dns notices the sentinel Ingress with no +# record, writes CNAME → .cfargotunnel.com within +# its --interval (1m default). +# c. Traffic starts landing on foundry within CF TTL. +# 5. Bake. Add mapache/vault/argocd in follow-up PRs — each is a copy +# of manifests// into manifests-foundry// with two file +# changes (ingress.yaml → Traefik + external-dns annotation, +# postgres.yaml → public hostname). No terraform edit. # Account ID for the tunnel resources. The CF API token used by the # provider is scoped to a single account — return that account's ID. @@ -55,10 +70,14 @@ resource "cloudflare_zero_trust_tunnel_cloudflared" "foundry" { config_src = "cloudflare" } -# Ingress rules: every migrated hostname lands on the on-prem cluster's -# Traefik service, and Traefik does host-based routing to the right -# Service in the right namespace. Adding a new public hostname is one -# more block here + one more entry in local.foundry_hostnames. +# Catch-all tunnel config. Every request that arrives via a DNS record +# CNAMEd to .cfargotunnel.com goes to the foundry Traefik +# service regardless of Host header; Traefik does the Host-based routing +# to the right in-cluster Service. +# +# Adding a hostname to the tunnel is NOT done here — it's implicit: any +# DNS record (written by external-dns for a matching Ingress) that +# points at this tunnel gets served. resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" { account_id = local.cloudflare_account_id tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.foundry.id @@ -66,12 +85,7 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" { config = { ingress = [ { - hostname = "sentinel-v5.gauchoracing.com" - service = "http://traefik.kube-system.svc.cluster.local:80" - }, - # Catch-all — required last entry per CF tunnel ingress schema. - { - service = "http_status:404" + service = "http://traefik.kube-system.svc.cluster.local:80" }, ] } @@ -84,27 +98,3 @@ data "cloudflare_zero_trust_tunnel_cloudflared_token" "foundry" { account_id = local.cloudflare_account_id tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.foundry.id } - -# Public DNS records. Each is a proxied CNAME to the tunnel's CF-managed -# hostname (.cfargotunnel.com); CF proxies + terminates TLS at the -# edge and forwards HTTP through the tunnel. -# -# sentinel-v5 was previously created by EKS external-dns. Before first -# apply, either scale that Deployment to 0 or delete the record via the -# CF dashboard so terraform doesn't hit a 409. -locals { - foundry_hostnames = [ - "sentinel-v5", - ] -} - -resource "cloudflare_dns_record" "foundry" { - for_each = toset(local.foundry_hostnames) - - zone_id = data.cloudflare_zone.gauchoracing.id - name = each.value - type = "CNAME" - content = "${cloudflare_zero_trust_tunnel_cloudflared.foundry.id}.cfargotunnel.com" - ttl = 1 # 1 = auto (required by CF when proxied=true) - proxied = true -} diff --git a/kubernetes/apps-foundry/external-dns.yaml b/kubernetes/apps-foundry/external-dns.yaml new file mode 100644 index 0000000..03d7ed2 --- /dev/null +++ b/kubernetes/apps-foundry/external-dns.yaml @@ -0,0 +1,99 @@ +# external-dns for the foundry k3s cluster. +# +# Watches Ingress resources in this cluster and reconciles matching +# Cloudflare DNS records. Because the foundry Traefik service is +# behind a Cloudflare Tunnel (see infra/environments/prod/foundry.tf), +# every record it writes needs to CNAME to .cfargotunnel.com +# rather than the LB status field's node IPs — that's what --default-targets +# does below. +# +# Multi-cluster: this instance uses txtOwnerId=gr-foundry so it only +# manages records it wrote itself. A second on-prem cluster later gets +# its own external-dns Application with a different txtOwnerId and its +# own tunnel target; the two never touch each other's records. +# +# Manual one-time bootstrap after this Application first syncs: +# +# kubectl -n external-dns create secret generic cloudflare-api-token \ +# --from-literal=api-token="$CLOUDFLARE_API_TOKEN" +# +# kubectl -n external-dns create configmap foundry-tunnel-target \ +# --from-literal=target="$(cd ../../infra/environments/prod && terraform output -raw foundry_tunnel_id).cfargotunnel.com" + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-dns + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://kubernetes-sigs.github.io/external-dns + chart: external-dns + targetRevision: 1.18.0 + helm: + releaseName: external-dns + values: | + provider: + name: cloudflare + + domainFilters: + - gauchoracing.com + + # sync = create + update + delete based on cluster state. Same + # policy as the EKS instance so behavior stays consistent while + # both clusters exist. + policy: sync + + # TXT registry with a foundry-specific owner ID keeps the foundry + # external-dns from stepping on records the EKS instance owns + # (gr-prod) and vice-versa. Prefix distinguishes the ownership + # TXT record name too, so a `dig` on any record shows which + # cluster owns it. + registry: txt + txtOwnerId: gr-foundry + txtPrefix: fdry- + + # Watch Ingress only — no Service source, so a LoadBalancer + # Service can't accidentally publish itself. + sources: + - ingress + + env: + - name: CF_API_TOKEN + valueFrom: + secretKeyRef: + name: cloudflare-api-token + key: api-token + # FOUNDRY_TUNNEL_TARGET is populated from a ConfigMap created + # by the manual bootstrap step above. Kubernetes substitutes + # $(VAR) in container args at pod start, so the tunnel UUID + # flows into --default-targets without needing to be in git. + - name: FOUNDRY_TUNNEL_TARGET + valueFrom: + configMapKeyRef: + name: foundry-tunnel-target + key: target + + extraArgs: + # Force every record this instance writes to CNAME the tunnel, + # regardless of what the Ingress's status.loadBalancer field + # says. Traefik on k3s advertises node IPs there via klipper-lb; + # those aren't publicly routable and are irrelevant since + # traffic arrives through the tunnel anyway. + - "--default-targets=$(FOUNDRY_TUNNEL_TARGET)" + + logLevel: info + logFormat: json + destination: + server: https://kubernetes.default.svc + namespace: external-dns + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/kubernetes/manifests-foundry/sentinel/ingress.yaml b/kubernetes/manifests-foundry/sentinel/ingress.yaml index 59229c1..4ac88bc 100644 --- a/kubernetes/manifests-foundry/sentinel/ingress.yaml +++ b/kubernetes/manifests-foundry/sentinel/ingress.yaml @@ -3,16 +3,19 @@ # the Cloudflare edge. Host-based routing to the kerbecs gateway, which # then routes to core/oauth/discord/saml/google/web internally. # -# The public DNS record for sentinel-v5.gauchoracing.com is managed by -# Terraform (foundry.tf) as a CNAME to .cfargotunnel.com — -# external-dns is intentionally NOT annotating this Ingress, so the -# record stays terraform-owned during and after cutover. +# DNS is managed by the foundry external-dns instance: +# - hostname is derived from spec.rules[].host +# - cloudflare-proxied annotation flips the CF record to orange-cloud +# - the record's CNAME target comes from external-dns's +# --default-targets flag (points at the gr-foundry tunnel) apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: sentinel namespace: sentinel + annotations: + external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" spec: ingressClassName: traefik rules: