From b713ec853c4083900a210066ad474e957bdd3df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Stensrud?= Date: Thu, 4 Jun 2026 18:47:08 +0000 Subject: [PATCH] feat(apps): forte-drop web + mcp argocd apps (prod) (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ArgoCD Applications + Keycloak clients + sealed secret for forte-drop **web + mcp** (PROD). ## What changed - **forte-drop** + **forte-drop-mcp** ArgoCD Applications (two-source: forte-helm chart + helm-prod-values). - **namespace.yaml** — explicit `forte-drop` Namespace at sync-wave -1, `Prune=false` (avoids first-sync race for namespaced resources; doesn't cascade-delete on base removal). - **keycloak-client-forte-drop** + **keycloak-client-forte-drop-mcp** — labeled config Secrets; the registrar creates the OIDC clients in the `forte` realm within ~2 min. - **forte-drop-secrets** SealedSecret — UpCloud S3 creds (existing drops bucket) + PG creds + PASSWORD_GATE_SECRET. Consumed by both deployments + the pg-backup CronJob. - **forte-drop-web PDB** — minAvailable 1 (selector verified against the live forteapp chart's pod labels). - Wired into `apps/overlays/upc-dev` (NOT base → stays out of upc-prod). ## Post-merge manual step (one-time) `auth-oidc` SealedSecret for the web sidecar is still commented out — it needs the `client-secret` the Keycloak registrar writes to `forte-drop-oidc-credentials` after first sync: ```bash CLIENT_SECRET=$(kubectl -n forte-drop get secret forte-drop-oidc-credentials -o jsonpath='{.data.client-secret}' | base64 -d) kubectl create secret generic auth-oidc -n forte-drop \ --from-literal=client-secret="$CLIENT_SECRET" \ --from-literal=cookie-secret="$(openssl rand -hex 32)" \ --dry-run=client -o yaml > private/auth-oidc.yaml kubeseal --format=yaml --controller-name=sealed-secrets-controller --controller-namespace=kube-system \ < private/auth-oidc.yaml > apps/base/forte-drop/auth-oidc-sealed.yaml # uncomment in kustomization, commit, push ``` ## Depends on - launchpad PR #17 (postgres + namespace via CreateNamespace). - helm-prod-values forte-drop PR (values). ## Review - [x] codex: namespace first-sync race → fixed (explicit namespace, sync-wave -1). - [x] Keycloak registrar unblocked (stale chibisafe/minio config secrets removed; registrar green). 🤖 Generated with Claude Code Co-authored-by: Sten Co-authored-by: Sten Co-authored-by: Danijel Simeunovic Reviewed-on: https://git.forteapps.net/Forte/launchpad/pulls/18 Reviewed-by: Danijel Simeunovic --- .../forte-drop-mcp/forte-drop-mcp.yaml | 37 +++++ .../upc-dev/forte-drop-mcp/kustomization.yaml | 8 + .../upc-dev/forte-drop-postgresql/RESTORE.md | 143 ++++++++++++++++++ .../forte-drop-postgresql.yaml | 40 +++++ .../forte-drop-postgresql/kustomization.yaml | 4 + .../resources/forte-drop-pg-creds-sealed.yaml | 14 ++ .../resources/kustomization.yaml | 6 + .../resources/pg-backup-cronjob.yaml | 93 ++++++++++++ .../resources/postgresql.yaml | 105 +++++++++++++ .../upc-dev/forte-drop/forte-drop-pdb.yaml | 24 +++ .../forte-drop/forte-drop-secrets-sealed.yaml | 24 +++ .../upc-dev/forte-drop/forte-drop.yaml | 37 +++++ .../keycloak-client-forte-drop.yaml | 38 +++++ .../upc-dev/forte-drop/kustomization.yaml | 8 + .../upc-dev/forte-drop/namespace.yaml | 17 +++ apps/overlays/upc-dev/kustomization.yaml | 10 +- infra/values/upc-dev/homepage-values.yaml | 2 +- 17 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 apps/overlays/upc-dev/forte-drop-mcp/forte-drop-mcp.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-mcp/kustomization.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/RESTORE.md create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/forte-drop-postgresql.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/kustomization.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/resources/forte-drop-pg-creds-sealed.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/resources/kustomization.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/resources/pg-backup-cronjob.yaml create mode 100644 apps/overlays/upc-dev/forte-drop-postgresql/resources/postgresql.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/forte-drop-pdb.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/forte-drop-secrets-sealed.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/forte-drop.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/keycloak-client-forte-drop.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/kustomization.yaml create mode 100644 apps/overlays/upc-dev/forte-drop/namespace.yaml diff --git a/apps/overlays/upc-dev/forte-drop-mcp/forte-drop-mcp.yaml b/apps/overlays/upc-dev/forte-drop-mcp/forte-drop-mcp.yaml new file mode 100644 index 0000000..0465bdb --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-mcp/forte-drop-mcp.yaml @@ -0,0 +1,37 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: forte-drop-mcp + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "" + notifications.argoproj.io/subscribe.on-sync-failed.slack: "" + notifications.argoproj.io/subscribe.on-degraded.slack: "" + labels: + app.kubernetes.io/name: forte-drop-mcp + app.kubernetes.io/part-of: apps + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + sources: + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git + path: forteapp + targetRevision: HEAD + helm: + valueFiles: + - $values/forte-drop-mcp/values.yaml + - repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git + targetRevision: HEAD + ref: values + destination: + server: https://kubernetes.default.svc + namespace: forte-drop + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/overlays/upc-dev/forte-drop-mcp/kustomization.yaml b/apps/overlays/upc-dev/forte-drop-mcp/kustomization.yaml new file mode 100644 index 0000000..869f482 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-mcp/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- forte-drop-mcp.yaml +# No keycloak-client config + no auth-oidc Secret for mcp mode. The chart's +# auth.type: mcp auto-registers the MCP client; the sidecar is an RFC 9728 +# resource server that validates tokens (no client-secret of its own). +# forte-drop-secrets (shared with web) covers PG + S3 creds. diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/RESTORE.md b/apps/overlays/upc-dev/forte-drop-postgresql/RESTORE.md new file mode 100644 index 0000000..028bd6b --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/RESTORE.md @@ -0,0 +1,143 @@ +# forte-drop Postgres — backup & restore runbook + +## What gets backed up + +A CronJob (`forte-drop-pg-backup`, namespace `forte-drop`) runs nightly at **02:00 UTC**: + +1. `pg_dump` of the `drops` database → gzip. +2. Upload to **UpCloud Managed Object Storage**: `s3://drops/_pgbackups/forte-drop-.sql.gz` + (the `_pgbackups/` prefix is collision-proof: app slugs match `/^[a-z0-9][a-z0-9-]{0,62}$/` + and can never start with `_`). +3. Retention: dumps older than **30 days** are pruned. + +S3 creds come from the `forte-drop-secrets` Secret (`S3_ENDPOINT` / `S3_KEY` / `S3_SECRET`). +Postgres creds from `forte-drop-pg-creds` (`pgusername` / `pgpassword`). + +> **Object storage is the durable tier.** App data + DB backups both live in UpCloud +> Managed Object Storage (replicated by UpCloud). The in-cluster Postgres PVC is the +> live working copy; the nightly dump is the recovery point. The PVC carries +> `Prune=false,Delete=false` so ArgoCD never deletes it. + +## Prerequisites + +```bash +export KUBECONFIG=~/Downloads/dev-fd-no-svg1_kubeconfig.yaml +# Confirm the namespace + DB pod are up: +kubectl -n forte-drop get pods -l app.kubernetes.io/name=postgresql +``` + +## List available backups + +```bash +# Run an ephemeral mc pod with the app's S3 creds: +kubectl -n forte-drop run mc-list --rm -it --restart=Never \ + --image=quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z \ + --overrides='{"spec":{"containers":[{"name":"mc","image":"quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z","command":["sh","-c","mc alias set obj \"$S3_ENDPOINT\" \"$S3_KEY\" \"$S3_SECRET\" >/dev/null && mc ls obj/drops/_pgbackups/"],"envFrom":[{"secretRef":{"name":"forte-drop-secrets"}}]}]}}' +``` + +## Manually trigger a backup (before risky changes) + +```bash +kubectl -n forte-drop create job --from=cronjob/forte-drop-pg-backup pg-backup-manual-$(date +%s) +# Watch: +kubectl -n forte-drop get jobs -l app.kubernetes.io/component=backup +kubectl -n forte-drop logs -l app.kubernetes.io/component=backup --tail=40 +``` + +## Restore a dump + +> **Destructive.** This overwrites the live `drops` database. Take a fresh manual +> backup first (above) and confirm with whoever owns the data before proceeding. + +### 1. Pick the dump to restore + +List backups (above), choose `forte-drop-.sql.gz`. + +### 2. Run a restore pod that pulls the dump and pipes it into Postgres + +```bash +DUMP="forte-drop-20260530T020000Z.sql.gz" # <-- set to the chosen file + +kubectl -n forte-drop run pg-restore --rm -it --restart=Never \ + --image=postgres:16-alpine \ + --overrides='{ + "spec": { + "containers": [{ + "name": "restore", + "image": "postgres:16-alpine", + "command": ["sh","-c","set -euo pipefail; \ + apk add --no-cache curl >/dev/null; \ + # download via mc is simpler — use a 2-step instead (see note). \ + echo placeholder"], + "envFrom": [ + {"secretRef":{"name":"forte-drop-pg-creds"}}, + {"secretRef":{"name":"forte-drop-secrets"}} + ] + }] + } + }' +``` + +**Simpler 2-pod approach (recommended — avoids cramming mc + psql in one image):** + +```bash +DUMP="forte-drop-20260530T020000Z.sql.gz" + +# (a) Download the dump from object storage to a local file: +kubectl -n forte-drop run mc-get --rm -it --restart=Never \ + --image=quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z \ + --overrides='{"spec":{"containers":[{"name":"mc","image":"quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z","command":["sh","-c","mc alias set obj \"$S3_ENDPOINT\" \"$S3_KEY\" \"$S3_SECRET\" >/dev/null && mc cat obj/drops/_pgbackups/'"$DUMP"'"],"envFrom":[{"secretRef":{"name":"forte-drop-secrets"}}]}]}}' \ + > /tmp/$DUMP + +# (b) Pipe it into the live Postgres via the service: +gunzip -c /tmp/$DUMP | kubectl -n forte-drop run pg-restore --rm -i --restart=Never \ + --image=postgres:16-alpine \ + --overrides='{"spec":{"containers":[{"name":"psql","image":"postgres:16-alpine","stdin":true,"command":["sh","-c","PGPASSWORD=\"$pgpassword\" psql -h forte-drop-postgresql.forte-drop.svc -U \"$pgusername\" -d drops"],"env":[{"name":"pgusername","valueFrom":{"secretKeyRef":{"name":"forte-drop-pg-creds","key":"pgusername"}}},{"name":"pgpassword","valueFrom":{"secretKeyRef":{"name":"forte-drop-pg-creds","key":"pgpassword"}}}]}]}}' +``` + +> The app's schema is created idempotently on boot (`CREATE TABLE IF NOT EXISTS` + +> `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in `src/repo/pg.ts`), and `pg_dump` +> output includes the data. For a clean restore into a fresh DB this just works. +> To restore over an existing DB with conflicting rows, drop/recreate the `drops` +> database first (coordinate downtime — scale the web Deployment to 0 during the +> restore so the app isn't writing). + +### 3. Verify + +```bash +kubectl -n forte-drop run pg-check --rm -it --restart=Never \ + --image=postgres:16-alpine \ + --env="PGPASSWORD=$(kubectl -n forte-drop get secret forte-drop-pg-creds -o jsonpath='{.data.pgpassword}' | base64 -d)" \ + --command -- psql -h forte-drop-postgresql.forte-drop.svc -U drops -d drops \ + -c "SELECT count(*) AS drops FROM drops;" -c "SELECT count(*) AS view_hits FROM view_hits;" +``` + +### 4. Bring the app back + +```bash +# If you scaled web to 0 for the restore: +kubectl -n forte-drop scale deploy/forte-drop --replicas=2 +``` + +## Object data (uploaded drop files) + +Drop files live in `s3://drops//...` in the same managed bucket. They are +**not** part of the pg backup (the dump only holds metadata). Object storage is +UpCloud-managed/replicated, so no separate file backup is configured. If a +file-level backup is later required, mirror the bucket to a second bucket/region: + +```bash +mc mirror --overwrite obj/drops/ backup-target/drops-mirror/ +``` + +(Exclude `_pgbackups/` from the app-data mirror if you split them.) + +## Disaster scenarios + +| Scenario | Recovery | +|---|---| +| Postgres pod crash / reschedule | StatefulSet reattaches the PVC; ~1–2 min downtime; no data loss. | +| PVC lost / corrupted | Recreate StatefulSet, restore latest nightly dump (above). Data since last dump is lost. | +| Accidental `drops` table data loss | Restore latest dump; or `pg_restore` a single table from a dump. | +| Namespace deleted | PVC has `Prune=false,Delete=false`; recreate Applications, PVC re-binds, app recovers. Backups in object storage are independent. | +| Object storage bucket lost | UpCloud-managed (replicated). If the IAM key is rotated, update `forte-drop-secrets` (re-seal). | diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/forte-drop-postgresql.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/forte-drop-postgresql.yaml new file mode 100644 index 0000000..309639b --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/forte-drop-postgresql.yaml @@ -0,0 +1,40 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: forte-drop-postgresql + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" + labels: + app.kubernetes.io/name: forte-drop-postgresql + app.kubernetes.io/part-of: apps + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + path: apps/overlays/upc-dev/forte-drop-postgresql/resources + + destination: + server: https://kubernetes.default.svc + namespace: forte-drop + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/kustomization.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/kustomization.yaml new file mode 100644 index 0000000..28749fb --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- forte-drop-postgresql.yaml diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/resources/forte-drop-pg-creds-sealed.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/resources/forte-drop-pg-creds-sealed.yaml new file mode 100644 index 0000000..ad22994 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/resources/forte-drop-pg-creds-sealed.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: forte-drop-pg-creds + namespace: forte-drop +spec: + encryptedData: + pgpassword: AgBYokuQRCTmPGC8soB7n8W39nmSAZDTV97i77NmdMh5ndaZNtCtXxhMcpM2z8kaGv2tCWIh47dr38a5tGPZ0TsmeEQOF9Rbbtq1fyR7pJwT8S+N2Z2zu354wNWQlltEAVvTvthe2wTer/BpyRofeSZprxihmfNpuHUP8rLsnIXln5tOWDzJ8hnRWoYZFITaihC2qrJj/kFE0Rfdcmzt1tSq3jCB/rWijVaJF9XSh4rzQoqZDiUNjDPUjyERILw59JWU4zf9OKcqNHDnmpXBR4LSjLhd9waN6ElEzO4gGcVaHISKrTwewX1ONwPHDnw6lqkQObyBPx8aUsGzxLkUhNDtvIYDkB4BKWP5Qu4bNcSztIbrxi6l7Lr/DWC9qTbKm1/p83rc6r8VqRMURUyQg8/vBlCHOIUbZ8DM1OfNlMd8gvcSkaxEVIdCDUjguCvE3cGyG4cqv2unllZQ+9417WwLNJecT6x1EL3nQyAlK5c9vUIbcVyaFlbSUcGB7xmPgZrZ6/3RDyOH6Tmew1ssV9gLvdaehscUE0fjnnFnJpczkwdyxIOSNLIkjlWetCKEbhowJbzk05h3M2p6XQQOuNTnsYjAADMGD72GUgAQlY8KXmDELtv09KELcXbeYS4gABPpMrVmvZymq8lqQ13Py8o+cIqbrU5V86WxASTfQ5gMo/ymYabuhTBIapcnaKR1dFCCfu8deh5f2HJ6/1NjdWR+XvEshg+EF5OkTUInukX4vA== + pgusername: AgCs6vyQ8CIv5OneP/jMltIPGdZQbpq/BFmQM1mkBD61Ve+anzve5K0Gkg+zsNfbZf0pOPAXtu4C4aL1Lwv7gqpoe4Hp/UEb/X9uLfJ1b8ZitmM1XsPmmSiCskHjrc2BLkAvfrVIXkHc3LOY2uZ/E5stc6Ss2WFE8/uzzVXW0B8fdEK0criludQ8iwR1gypulEcDNomXgkK/1gmmCWosUcVv4jDMDhqBD+b9WYnBB6J73gUclWVMvYDFdNas2PuoRzu5Twc9TAZrTxN5lvLOXAonOo0YiUbUhEC83sfMWYDT5/9OxqcJhAxtgFe9j83MpCwLSwfeLZm7UsUapWDb60MxPJLGvoGD/ZOhkeYt/YCZYROa57TMslVIL5YU1KCiNWvtRjIqnvdiBxI7MRvPUfAoawS4ktT5PDhTTfrixFbaF95jul2kKBXV+OYB1UNsFhcCgZx9rzYRt4lNmBv4m4HeXIp3EYY8VlGLQ45BVVqjJ4QkISvb7ifQWH1aPMQllj+J3GwW0KJN0dEgsh1LT+C7W5I5mq461NOTF1eih/XRBeuPoLlgApxiGXvFCTx8lji2/JIdOaqcg29hdabSprxa0YMStChi2pbtHhRzAuFCp8mInGt8Q406vu67Y4/51yuwI40YeDVu0lf010TB+/v2Zy3OrNyjlqrD5JNynsLuRl3UhuAKC14Xhg/MiDLvTzfsYE8aog== + template: + metadata: + name: forte-drop-pg-creds + namespace: forte-drop diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/resources/kustomization.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/resources/kustomization.yaml new file mode 100644 index 0000000..7b5a754 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/resources/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- postgresql.yaml +- forte-drop-pg-creds-sealed.yaml +- pg-backup-cronjob.yaml diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/resources/pg-backup-cronjob.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/resources/pg-backup-cronjob.yaml new file mode 100644 index 0000000..4304424 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/resources/pg-backup-cronjob.yaml @@ -0,0 +1,93 @@ +# Nightly logical backup of the forte-drop Postgres → UpCloud Managed Object Storage. +# Dumps to s3://drops/_pgbackups/ (the `_` prefix is collision-proof: app slugs match +# /^[a-z0-9][a-z0-9-]{0,62}$/ and can never start with `_`). Retains 30 days. +# +# Pod shape: initContainer pg_dump → shared emptyDir → mc upload + retention prune. +# Both images pinned. S3 creds reuse forte-drop-secrets (the app's UpCloud user has +# s3:* on the drops bucket). PG creds from forte-drop-pg-creds. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: forte-drop-pg-backup + namespace: forte-drop + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/component: backup +spec: + schedule: "0 2 * * *" # 02:00 UTC daily + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/component: backup + spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + fsGroup: 65532 + volumes: + - name: work + emptyDir: {} + initContainers: + - name: dump + image: postgres:16-alpine + command: + - sh + - -c + - | + set -euo pipefail + TS=$(date -u +%Y%m%dT%H%M%SZ) + echo "dumping to /work/forte-drop-${TS}.sql.gz" + PGPASSWORD="$PGPASSWORD" pg_dump \ + -h forte-drop-postgresql.forte-drop.svc \ + -p 5432 -U "$PGUSER" -d drops \ + --no-owner --no-privileges \ + | gzip -9 > "/work/forte-drop-${TS}.sql.gz" + echo "dump complete: $(ls -lh /work/)" + env: + - name: PGUSER + valueFrom: + secretKeyRef: { name: forte-drop-pg-creds, key: pgusername } + - name: PGPASSWORD + valueFrom: + secretKeyRef: { name: forte-drop-pg-creds, key: pgpassword } + volumeMounts: + - name: work + mountPath: /work + containers: + - name: upload + image: quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z + command: + - sh + - -c + - | + set -euo pipefail + mc alias set obj "$S3_ENDPOINT" "$S3_KEY" "$S3_SECRET" + mc cp /work/*.sql.gz "obj/${S3_BUCKET}/_pgbackups/" + echo "uploaded. pruning backups older than 30d:" + mc rm --recursive --force --older-than 30d "obj/${S3_BUCKET}/_pgbackups/" || true + echo "backup retention pass complete" + env: + - name: S3_ENDPOINT + valueFrom: + secretKeyRef: { name: forte-drop-secrets, key: S3_ENDPOINT } + - name: S3_BUCKET + value: "drops" + - name: S3_KEY + valueFrom: + secretKeyRef: { name: forte-drop-secrets, key: S3_KEY } + - name: S3_SECRET + valueFrom: + secretKeyRef: { name: forte-drop-secrets, key: S3_SECRET } + volumeMounts: + - name: work + mountPath: /work diff --git a/apps/overlays/upc-dev/forte-drop-postgresql/resources/postgresql.yaml b/apps/overlays/upc-dev/forte-drop-postgresql/resources/postgresql.yaml new file mode 100644 index 0000000..4fa4aa8 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop-postgresql/resources/postgresql.yaml @@ -0,0 +1,105 @@ +apiVersion: v1 +kind: Service +metadata: + name: forte-drop-postgresql + namespace: forte-drop + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/component: database +spec: + type: ClusterIP + ports: + - name: tcp-postgresql + port: 5432 + targetPort: tcp-postgresql + selector: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: forte-drop-postgresql + namespace: forte-drop + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/component: database +spec: + serviceName: forte-drop-postgresql + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + template: + metadata: + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/component: database + spec: + containers: + - name: postgresql + image: postgres:16-alpine + # NOTE: no securityContext. The official postgres image's entrypoint must + # start as root to chown a fresh /var/lib/postgresql/data, then drops to + # the postgres user (uid 70 in alpine) via gosu. Forcing runAsNonRoot here + # breaks initdb on a fresh PVC. Matches the vaultwarden-postgresql pattern. + ports: + - name: tcp-postgresql + containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: forte-drop-pg-creds + key: pgusername + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: forte-drop-pg-creds + key: pgpassword + - name: POSTGRES_DB + value: drops + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - sh + - -c + - pg_isready -U "$POSTGRES_USER" -d drops + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - sh + - -c + - pg_isready -U "$POSTGRES_USER" -d drops + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeClaimTemplates: + - metadata: + name: data + annotations: + argocd.argoproj.io/sync-options: Prune=false,Delete=false + spec: + accessModes: + - ReadWriteOnce + storageClassName: upcloud-block-storage-maxiops + resources: + requests: + storage: 5Gi diff --git a/apps/overlays/upc-dev/forte-drop/forte-drop-pdb.yaml b/apps/overlays/upc-dev/forte-drop/forte-drop-pdb.yaml new file mode 100644 index 0000000..85c07e3 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/forte-drop-pdb.yaml @@ -0,0 +1,24 @@ +# Keep at least 1 web pod up during voluntary disruptions (node drain, upgrade). +# Pairs with replicaCount: 2 so a drain can evict one pod while the other serves. +# +# Selector verified against live forteapp-chart deployments (mcp10x, argocd-mcp): +# the chart's pod selector is {app.kubernetes.io/instance, app.kubernetes.io/name, +# component: app} where instance/name == the ArgoCD Application (Helm release) name. +# Using all three labels also disambiguates the web pods from the forte-drop-mcp +# deployment that shares the forte-drop namespace (its instance/name == forte-drop-mcp). +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: forte-drop-web + namespace: forte-drop + labels: + app.kubernetes.io/name: forte-drop + app.kubernetes.io/part-of: apps + app.kubernetes.io/managed-by: argocd +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/instance: forte-drop + app.kubernetes.io/name: forte-drop + component: app diff --git a/apps/overlays/upc-dev/forte-drop/forte-drop-secrets-sealed.yaml b/apps/overlays/upc-dev/forte-drop/forte-drop-secrets-sealed.yaml new file mode 100644 index 0000000..0d5b90a --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/forte-drop-secrets-sealed.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: forte-drop-secrets + namespace: forte-drop +spec: + encryptedData: + BASE_DOMAIN: AgAFybdBryVb2AQuGQC8REXzW0YZlyycJp/KeXnROkW71UjDe4qMAWkWszrJWxZMvAPO/tXmibp7jEol6aB5GKG0k3tswWoprTFXLd9CMR2U9SWR3ZCol4npPXo7uOxhBcNSVt+cDXyejSiFTi6goY2oOtbKAJSF9Nv7Z5ePaqhhFni3ntcmM0S1Ad1l3QR7VvyazHFBXfO0b8Z9NgYsUNbGrXWDwoSAZIv3ly3wx90AXn+dXX5FNPtl9CtyAVhHsl3liwQdhEwS2krZZjj7NiQTCfNXp7BSB9ZETpo9KkoV4AZNy1zupd3HpeXHsyhHjq/JqXIAF3iFU0tZTWjhcwnehYdEU5oduwfLCWym5PYgpiQAGiazpkm1Ss3/PYpZYnR2nWv60b1Pa5i79ZiPNi4GL67AiWoJDw6QxV0Kbzi0AvUkZI1E2PeIJvv1w9NKdMRo49xK8LUx2qSTpWeqRP+1kzklHqclTuNVxiWtR2wUgdoLzvU7p5ETu7kPEmaoE8rYw4dKgQvHlMok2Ky2JsELGBkCiYjUN75T+yNlGs5dzbiwtWOja/r0dJ3ZGBQjcK4/BbTLiMYsrxmJTPPF/2zhCOlFY6cfcRMmc7Mwr68mK9m2rTOJQNjBMDoASiqVMmeSqfRSln7JNb1pAeq4xcz9YJMBJhPy2XNiBvRJK3pGIjVcNST0jSpic1X01NJTy7aFbcniZzYnsKJV61AQb+daGEsB1Ib3GnJ+Rv8+9NfvWg== + PASSWORD_GATE_SECRET: AgDIiAPb7bCpTeORoZXIn7Is/yKT7Qm+qSexgmXuXpe+WaLCJDWM5uOlvxp5tbtK2KD3SNtUDrOJQYfWYBOAPrBMBnSpDr8Ie7/wlJBxXCfROk1TldOU3s5O+5OnFfTDS9rAdcWdZdJ+Bt0aktnpuHJpjdurFca5o8ne5SRwYtGk6mNinCYRcnwMApZ9Y7IxvC9xzOi1QIhoOepMrotRkVJtbrXiclN5cI+P/nU8LaGwM4AEJtO3L8zY5QK30SZ/Tsz20Qo8HHGdIBLhHbhTALrFe8+n+Egda66Vr1DkKgDERW59PeWBd6xHCRGNuAk2ZzUmuiN5DJmWgacAGy9QblMMjhOIGKgiqsd4jx/8wC8FoyxBSstGFajahL2C6oFzCpbp5NiS185znN0ohJXqQzkp5RbcBliSYUZgHC8D1jDGxzcTksD2Hgh2NIAlJOStsfB3fUb6hWIFzxim0lyP9kFM5y92Sf5B1/h6PeHKOi1CMC5RFAEKhoyM4W1W+NxASJeT/NptTehcrjBPxLVKBPh/qgMcuNYNk34EB0asELalYBJ5cxzt31LLiLE/uDxCydXiSk7ACho7LxNffcUUakEwwsJDmjFiCscu83TfZq2vw5/2bgOzUng4XGDBgYwwr/KEHueGX4Keg255R3KqxyiMSKDi4CUEtYShjSsSYwU62rdogOQu20N3HGVn+/CksqKky7GoD958d4PwT5MGJwQhp8EjemBomDxTKNsO+C4moMNZpAEBtPlP65Lc1cfcaLlaFrwP4VtYHLXXBIvMzCC+ + PGDATABASE: AgCCkY7AUaoy2V+fRXrEeLODlHOTXWXmWCW6MBWEY1J32QrN0bWUmwDdTxv5KHchbhH8lra9bABNOGfRcz59GIg8fH3fwTWOE8luh9cD50QkphjZPe9eGXc6YzheG0CG5hDnUoiJjnP9/l5cZc6sRxAo7for/bbLa0zja3VMzI+NMhVVRZT01G5R/Aoyf+B/TGm4mYbMyWAIgEEfjl50yIkc4WscfQrRkbxAtBF2qFuS3OL95TYSRULBAt0f3Z2WpCdS2b/pUyHJuuoi8aTo1a9QLFMrUdxVi6ydIKaMcx8xR8DlXQLOOdtIQu4TrgzGX2MgHwrbWuf/SFO5IWB/6/JI3yubo+i88M9LwPKsGeslcquoBG3Ibqlnmw8pcPrXUwBq2BowjAqpGsxdiR5XIi2j8QnpA8dTmFARX9CWKHoXFH5+uxx7SSTPG+izlGtVspsu5xx3F8MZ6eStInCyBimTVrn74+IvMPEBrj7wThO6Bl9EA3POkTRf6AmjuPItTgZK8lfXY/t0qDN5zApeB051+jSGZ0/o4PaY414vCm/+OteuCAI4ooCpDOwG3QD52VtpmoAvGjtuWExrvCHMW+hTIxsNbYJ+2SE1oQy8n8J5kKK+AU/SnYf/VEpbawHrpU48g6sDUHQzzPhoLN41j67qopmfRZAo1r5Tb4n7MnEOF3Yi7aT3lE6JvB3gZUSRqnDYSWObTg== + PGHOST: AgCWDPCQ4P+vPKQXmzNspODUJYVidwQDzFRxY8JDj5JYW1u+2xK8LqE2uWWYCW3YjF+H6yJoH6gu35KqOtjm0E23zMlknwZwkK+L93J/nrnNZHCznoub9jEzaAsTRhytvuIUBPQnFulP/t9Q04RjmR9c14puS7VS/tM57/cLq5zq/BiSYL3Wx2lblE7Xn6k44+8robPP3YkmlOuqziM1li/qevB/jxRV9YFjIpCz6D7WG6Kkx9CA6fzgalMJ5ErUVTJOuLDDty3qKd+9mg4WCLahjyzhPq/rIPUFE77EBDCulnnlMyvOSPSP7pMzkaNR1Ce3cOxOYAZ5mO2u5Y5pXt34V8iPe87N85KHQNZTu1Mu5ELMLdhEYOlO5ZX2x/RsdEuriWz1NW8MC57pEUGE/XrFhDe6x80eHi++qDRat2d04rTBOtlceZMmPVxs8tTa3VaD4PepekSogddLVooscLL7lYIpyR0Q6qOA1NdF2uOpx7hf6QLY1HeD4V8RSVVxV9uK5lUTrnBjtU/aUwnGs0xKxVNdBEKwG5Kh7qdsgyBiEa9V4Y2ElteGHave4J7xzimdASrbgMnlgIfcMuo94eX/0M0BAmpD4bEByMCWbcFgAfNnIOpXLZvoEfZ3DxVysmCzD+0nq+CEatdOjKp11EKbtf3H4PgnKqWDe2p8FNPXhev60CMhFvXS/QYg9fe2YJt/wqcN5MstCej4IuiOJkuFJ0UhZ/F5760xhuPYCUWP1suIVZM= + PGPASSWORD: AgBH32G+EUtc3jzGCA9bf27TCbzgK9xz+r4dqd0QQJL9xHbqgOARGVVaQ88AOkWV5VgYjqc/GFp51jLzVOHxgLdkqO/oCBuX9ajQEoGfq24AxFnaB7fh+Vlc3/N9yhT8lWoxHmHjyMVeX75g/9KvhNaRKBgiWQNHlt1C2FNh1h3U/aMfWVJIENmKKH2A5sxWe5haB7nynZc9r1QXBQKa7XVpuxAFXDHz3j3cFyR5Qflp+ac2APEM1/xbiaZDgkBtBd6dsDoCP56Dr1m91kaRGgbeX6WmRJ/Y89WAp4yt3QVfa8uGL1+DrBBMcfB1nAQKA45eZjPE6zTOEHxgTETCcmXJQiOzttDmBHRkIClOLLipgGDJwMqtgQoEMoJKjXMC0rsRy0NVRmibZa310R3PQjuHrQXxRD9ZAXkYg3opwLKeKi07b/7mvLHr7hU81fkBGnqNm/6heOSAqDZfRdregbBbcI/go72aypn2vQ5R+ozCdfwcp1tGla8FGpkI+zAdBKihp5Yo21VZ83FlIMq2JHF2+tv58C+LFeyqL1nr6BUmGKUQ+lEOnRzGYo1sbO5wBChc6yP3ZbzZYfxfvXAdfDY7vZUsareOC4uyR1wDnIiJgQ4kqmAKf7HulJJatKNgsvbmukj7c6lHLsfFRg5pwLO6iese9TZgtima2wkdcRpHSdt4ycnyHbwrEZ4kepfFlN1pGUl573/3l2cOdzO+WLCqV96P5myL6OOmCTxOaLdSyA== + PGPORT: AgCXyGjhlTcq3ffD+ZZZxe5gvqRLQl2MYGl7SfJDjw4LMdr3iojAeQNdYZIeZN4Bec9thhz1DI/Bc5Iiw5TiB3MAAvl/XtQM/T23JrSlUsfqECuHKQGw5kkgeVCds4j4d/IHFt9yBv4W4+ogcL5TZIGGgMiKa/99T77xgE4OqjeqqC+bpFuLObiKt5sYcqcHl2DDshTy4xeukfdA2yI3N9Tq7zfbLpZUKHWWe1gE+FbA0s+QA1ZCYv9uEtu/E5BWOYxE0u4GKhMDfMpguks0CNnIoRpovY3vmOxzFtqa7RpPoQrQtEN2Xtizu/G8K8p+mZLQ0cf3KX0rwp8tAsVKWrp8FYOy6+2OLYlhmDl1hK+M15czaL60DVBqiRIlQFdj9K+s5KQ0qfsG9m4v2WPSeCLEKwcsyLyq9vzXbDKwcMN2DOMOan8VDo04UZzkfdZa3lztfw7MWIdpvB0+cpgK3mdbbPCqSIQ8UTjeSVBgEz4EPUds81W8gqAgmeVKpUQplgFuLNwCTnCkuMSNiA93D3dgGccXzXiDrIBIcLzxHAWr0XEjISyD/pmNEowbDvXB5yRlNi1ZB5AHsRPurUYs4XtZN5ar+sxyex78tG18OkcBbABatBp3ol28Rs1LEN+HJupzNl4XDUx35/j3t/FPcw9PkcF+hzaQL7aWj4EDEWfjlzz5FEBlU5N6sd078tB79TpasuIk + PGUSER: AgDH9oSzu61NVOUlK7Evr278VuOx7ZxZgdsk+kdJmwpBGX7r5gmPrdomh/mqLYTuLokkanFiTfchRWHc76FvjbA/KxqwCq1qsZbW+dXrRtx/z1wQApKxNUJ7JolwMwP7tHE6QlGzO2mWj6RUROnhKpNybJXVvC3E5sSyz2QWC9hjamQP997RGA9yiiT/OShC7I6drFYR5cRDtpjW7Sy46qhMwlCRppiKh3wOV7qIAa0aPQE3Rfcg2WpK2ugRL1N+SiVnM+wPQwYVLiDaVF40vP40Kari99hIgmhcbjPeGG3kGX5VLww9KGm7iryrW3Yx45L/CCh1arUUpjkK2FGLVKtb3+YmDadnOA/I8Rr5kebhoMc93E3U3+mDfQA3cO/23xgpOJEGRCQwBlN9mazqkdq4zQkb4+nuxsdyQcxYtncgxhfCcZ0mXnbX2aW2kYcxKqa/jNjBcEpGMvos7dq6QzNq2nHrITo15S74M0292CAje2NFvKURA/KZnT26dDw3e5xa74E1nI/tBJEHWrUwRXpPu7naCZ2sZMxQV6ixQMuDakx3YamXZmMwgFO2FZ6ZL9BDDsbV4+JAsNwEaHGIIaTbE28R/xPIcUqcxrQV4ZWmHnJXFGyJ0XXxJ57GGjs7QwvvzAm+9WGYtlSC6H/8rX8uZIQLr3llVbJMuLpIv45i3p0Nkx8jyxGSG3rNQ4l3K0rjly2qZg== + S3_BUCKET: AgBcx2UvWafkVQ4fc+Xuc+gCLm5O5DceYSYBslL1bd8p4hKI/2hJF9z9UoLyAedyyrx8pHizcugkcIccrV1iyfQod2lDrivVJCy3qHz0mrQ6ZCOAk9ApNJTYUn+x/AYZL9EKVj/vfEzSwOTqlri6H53aUrZr8lv01TqLcrZDeG+jv64psydYKmSESkPl0iQ9DOx2sGkowNYbiTjux+ED9yXDMDdzojhMepbuUHhjNr7V7Z2NpX8+pNVz2o8o/oIA0zAJs3+C02018N3cyJ9P9BP2/qR6N31aykyI4P6GU5WlL0CDGKaheYy9ukM5x8SoU87yZBHLUxN0MhrMVKoFswU6CwTG/A8Gjkp84ce2cvlnuXP6JEMlqgSb+O+SeCH8+kwL9A87xmb42OOXSGokS188/2/13b6S733WL81B0RX1X1pOfWybpJzDhgiOC3Pjn18ItVHjKw3FlY6kwk1+P1vbZoRl343avRdyQyJam28A9UvZeTG+ac7drRTDndKYKGP8IWg/A32MEFSiKHsvnRQoYE7KORyzJxp610L1+w46KOUF5VF2afch02Jt4uLcmCOyUb6LdcWln3Jiz+wydJI1HK1RPio5oUT5d5se3K3DCa+GffbPMorHe/SdzqaBH2+uBcQn+wZO7JC1ei7hz4U1xdV41gkG1uq7Xz49ymW6P7G6qUr65MtvD73iMRlnJt/mdkw5UQ== + S3_ENDPOINT: AgDA5n96LeW1spJfmm7f9lcus5Ndv/91HAlhOfv0YwP+9R8rQhKF1sEL9Oi7Fpd4VJCQi9vDbeWYjGOMtsMqsLJivcZK6367rdSqL/YpKax9SkDgU+UG5Bpr5SDv4p5C3/J4+GPkZV+BduJQlNVPycTrdM80VBK8aALCNs7gGxy68v6mMgI90MpTp84ZeWgtJ7ZpTySivpwgycwcezldJTYDDjOvVfbK79trsRzDtjbAZzbDn0M8At4LYAfY7JTqcdCRHSGnzyg+81CX2jjyMOH0K55SsIqPEBdXqvh+VmC+jPyFMzfAN5hA8+otWTXfI7VucxAMZtRUiLU3I4/kIO60OkVjEf/SWrJv3nKb85aVfvRZ9k/A7kJ/ysKTXz2/iSAHU1DHTJlMW7SfhXciotx1p3BWOREF0QAQvrNCURFP+eZogBCIuiP38fD0gHkhG2BZkAY/OVZTr17a3F02kwemAuJ85hgUf0iwFV+/LD6X2OiqM85lnac/3B5Qp7UQf+Enn5RTqd90Pk/7QtAf/zTDmfxiWTkwpKXeQ62xcnHL9dQnUmDDs16eJZd04AJGgT8yrC//2lVOht7u6KuyG/+JXRxLBXAkGkr68lHBBuGmeVZ/t7ChL2Asyfu/uUTl9E1BOUfNvZXxwyLgqueL0Uw/MGLgswBsP6pYNGraMF+K5v7dse8cUxpyqk3s1C1zqoAhkehsCBtTjI+hF90G9Nh6mVSocoH12znt7fgUnwNbpg== + S3_KEY: AgAiciTM2ZNVlU1M1CNNXLkhCEYYbO7q5+Mp/DoC4OHBgIDKVfHurH39Dniuwxe6DcvE3vG2glRyTxQEg/ASLcwa7HBwBAwXe3wl1tRGM5Bp40/Vq935BXpkhcdp2fSoup8lPEbKS8q+L5LOqUlx7jmnkHXbI1tasz63KE8O9RUFDdQ8Gxy3nn/u4xkvibYxwmo60ApLKYgOu/ODPEETrWBcITHAVFUxbA8Kr9X9mPm3VpfrnFcUlxsCFr/zZwE/Y01eWdi8GGafb+apDPKMd7mAsLHFcPIQlpkHVT1M21qwwntZg9yV0RBACNu5BVPUgbmtUOQeWYMXn3FE+NJ7ajfdKAUCcEUV/f4s00b0S7jJTJwOUixDquMKSfu00AwDRCs8UcouikZe110uWnfEF3tVE0xQGF/3ItLni9VugBz7wQv7ACvmwnHmX5ZcjE0hxYcIS7ABWgHOZxgWoRWPao8eNAATipafcVIG1szl5ZMNTmAqHFyp2dlNU3zaiW6fz4q4CU7SrlhsrtqYM788qHvpJvDpFdF/i6oitH9CgpwmdCpH6YbBXxatnkWq9bqjEFcSZGDfDyT+iZaoPwhiOfaEoCyKlZ9RLLaK3E8zFcCDRXHnvnkqPtP/+VG30xz9pIat2EVB1N4b/kVIrr+fIM28mwk0vkC/tU8T55GF5BZr7VaYedHM9DVcQ4OJl7Ctrc9Ki8PXrne8gywyomA0F+YY9lxdDw== + S3_REGION: AgAAHTbNQ3gGnvg67ck2N5zSKKhMwR1j6pi/tZzYMEPK8jSnDTBkLYAt6ZVRtdsO+dG9kjnMsc/xTUMxJspbvQkgLSd9mG5FzJ37rBn+azCCSTDYBKq2ddGK1Yf/9w7MxOgN8aCyD4QCFOzR0EI49GbVYQynxDD5BwYuf7y4t2xCYt5wRGsjyNAmH3202Z90XKUkts8Na1hyD+xrrtrAtNfugyZqKo2WUSsHu82TD+cu5xZI51oQ9w9Mh9LaH3nfn/X5S+t17TjYvI7/c6hOwCVv10OdoaZa+SzqOvy7XxnqAShAYkPqJKfWhjecE1b9c/Cun32X4MRI0GBWyA02z4nR+WBbaVmasicx6hchVn8/wZeKMIl68F5LE7184MKKPNNwqsYslwFhuWq8dEw3TaVvnWgx3+cSMiX15SBwcLtE2UzJp+jjN/dpQ2MM0+6uV1GK4mNso5JAwGpUUUi2i+V1Ng7uXipI+5J9w6K0IMg1puGqFyahby42saSuH6vuPnjSx+2dXQTlgbl2SvCBoCgyOOJs4q5IafEvupAmRCNzx2HHv8/z6CFbSQZITQ3plmyNGLFXjynVw/6Q1PD4z3Pows4uVcYOEPbC0UoaXVgwgdBWa2N44BUhpdbqJUCyKuapLigpjKujG43jmdLzuk6gaPeH7SJTZKr624vs9hrGhzQYKdl6FZOQhmCFRDKXVCUiH2Z2pfwd3oo= + S3_SECRET: AgCcVQ7YtBAGpKBm+rE/hQBHrFlX5O0JO94xkZeoAppA9Tf8YR/PguZRGBWgLdNEJRI8C08lRRCUX3PY68jTySyjamb32iQkslOXGjAfnULeNGoGg05nLY2ZDYCEom6ieL8cc2xfbrV3yHoPQ7yVz9vcLjh1vATxyfdkqMapl8FpvQf0k0Zecmw3rLWE9y6vAn6Gb+/CWTnuhcW/8uDykmjIBTDQQddWshaZi+HosHyDbNxlnGj4U8mie68wytpS+Unp1gIWWE0hvelqO/3OUEEBB1OYMLV2DW8v86HXAE1Ix9jiCpSbyB+UzjOlrE/p4fJpeG4FtUC+/5ibRSxxgQRQYklKFJmdRDYWUnOngjgcT/Ewe41mTrpCUvb+jtir68pYLmVrLoha7S60w1YQHNkDAN2GftOyBjkkt6MtUDNzvNkfnKqKGUWyDSC27yfJdE/9k/4lDxQs0Sp20kIuz66/culBpg/s/oPSNs4SolCqG3GVLlKL775uqwLLuDN3txlPLb+Ex5vZAUapke+rn2zXzJVc1qlPfI/96vSEy6cx58LXdBadmBXn6c4Uy2MDa66EwsxOMXxzGLTd7AGkd5oeQVYfVPdTfGV5zx1AdzQhP3u/DD5FhKeWGDOr21iYB2jNm/P/hw0nFP2pf83W4/jLzPvuth1LF/WLF8cjclnGbcep2Kxrh/Xq0LmufofuVJyEI9/fl6onl5KIa6ZnVBJ8TsQesXJtNEKt9cPHiCvBKfLj5C+a4FlY + template: + metadata: + name: forte-drop-secrets + namespace: forte-drop diff --git a/apps/overlays/upc-dev/forte-drop/forte-drop.yaml b/apps/overlays/upc-dev/forte-drop/forte-drop.yaml new file mode 100644 index 0000000..08610ba --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/forte-drop.yaml @@ -0,0 +1,37 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: forte-drop + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "" + notifications.argoproj.io/subscribe.on-sync-failed.slack: "" + notifications.argoproj.io/subscribe.on-degraded.slack: "" + labels: + app.kubernetes.io/name: forte-drop + app.kubernetes.io/part-of: apps + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + sources: + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git + path: forteapp + targetRevision: HEAD + helm: + valueFiles: + - $values/forte-drop/values.yaml + - repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git + targetRevision: HEAD + ref: values + destination: + server: https://kubernetes.default.svc + namespace: forte-drop + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/overlays/upc-dev/forte-drop/keycloak-client-forte-drop.yaml b/apps/overlays/upc-dev/forte-drop/keycloak-client-forte-drop.yaml new file mode 100644 index 0000000..64906dd --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/keycloak-client-forte-drop.yaml @@ -0,0 +1,38 @@ +# Labeled config Secret read by the Keycloak Client Registrar. Kyverno clones it +# to the keycloak namespace; a CronJob registers the OIDC client in the forte +# realm and writes the credentials back as forte-drop-oidc-credentials in THIS +# namespace (~2 min). The forte-helm auth sidecar (auth.type: oidc) consumes that +# registrar-created Secret automatically — no manual SealedSecret step needed. +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-client-forte-drop + namespace: forte-drop + labels: + keycloak.forteapps.net/client-config: "true" + annotations: + keycloak.forteapps.net/source-namespace: "forte-drop" +stringData: + client.json: | + { + "clientId": "forte-drop", + "name": "Forte Drop (web)", + "enabled": true, + "protocol": "openid-connect", + "clientAuthenticatorType": "client-secret", + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "redirectUris": ["https://drop.forteapps.net/auth/callback"], + "webOrigins": ["https://drop.forteapps.net"], + "defaultClientScopes": ["openid","email","profile"], + "secret": { + "namespace": "forte-drop", + "name": "forte-drop-oidc-credentials", + "keys": { + "clientId": "client-id", + "clientSecret": "client-secret" + } + } + } diff --git a/apps/overlays/upc-dev/forte-drop/kustomization.yaml b/apps/overlays/upc-dev/forte-drop/kustomization.yaml new file mode 100644 index 0000000..080f3f7 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- namespace.yaml +- forte-drop.yaml +- keycloak-client-forte-drop.yaml +- forte-drop-pdb.yaml +- forte-drop-secrets-sealed.yaml diff --git a/apps/overlays/upc-dev/forte-drop/namespace.yaml b/apps/overlays/upc-dev/forte-drop/namespace.yaml new file mode 100644 index 0000000..dd91428 --- /dev/null +++ b/apps/overlays/upc-dev/forte-drop/namespace.yaml @@ -0,0 +1,17 @@ +# Owns the forte-drop namespace shared by the web + mcp deployments and the +# postgres StatefulSet (infra overlay). sync-wave -1 ensures the namespace exists +# before the namespaced Secrets/PDB in this base apply (avoids a first-sync +# "namespaces forte-drop not found" race when the business-apps parent syncs). +# Prune=false so removing this base never cascade-deletes the namespace (and with +# it postgres data + the mcp deployment) — matches the earlier decision to keep +# namespace ownership decoupled from any single workload. +apiVersion: v1 +kind: Namespace +metadata: + name: forte-drop + annotations: + argocd.argoproj.io/sync-wave: "-1" + argocd.argoproj.io/sync-options: Prune=false + labels: + app.kubernetes.io/managed-by: argocd + app.kubernetes.io/part-of: apps diff --git a/apps/overlays/upc-dev/kustomization.yaml b/apps/overlays/upc-dev/kustomization.yaml index 1895aac..04cedb3 100644 --- a/apps/overlays/upc-dev/kustomization.yaml +++ b/apps/overlays/upc-dev/kustomization.yaml @@ -2,6 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base +- forte-drop-postgresql +- forte-drop +- forte-drop-mcp -# No patches needed — base already has "upc-dev" paths -# upc-dev is the default/base cluster +# No patches needed — base apps already default to "upc-dev" value paths +# (upc-dev is the default/base cluster). +# forte-drop (postgres + web + mcp) and dbunk-demo are upc-dev-only apps — their +# values hardcode upc-dev hosts (drop.forteapps.net etc.) and must not sync to +# upc-prod, so they live here in the overlay rather than in apps/base/. diff --git a/infra/values/upc-dev/homepage-values.yaml b/infra/values/upc-dev/homepage-values.yaml index ac71704..dbde2c1 100644 --- a/infra/values/upc-dev/homepage-values.yaml +++ b/infra/values/upc-dev/homepage-values.yaml @@ -60,7 +60,7 @@ config: description: Teknisk kompetanse fra offentlige anbud icon: forte - Forte Drop: - href: https://drop.hackathon.forteapps.net + href: https://drop.forteapps.net description: Self-hosted HTML-drops + MCP for Claude icon: forte - Forte Feedback: