diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..03a7019
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,255 @@
+# Sunbeam Infrastructure
+
+Kubernetes manifests for a self-hosted collaboration platform. Single-node k3s cluster deployed via Kustomize + Helm. All YAML, no Terraform, no Pulumi.
+
+## How to Deploy
+
+```bash
+# Local (Lima VM on macOS):
+sunbeam up # full stack bring-up
+sunbeam apply # re-apply all manifests
+sunbeam apply lasuite # apply single namespace
+
+# Or raw kustomize:
+kustomize build --enable-helm overlays/local | sed 's/DOMAIN_SUFFIX/192.168.5.2.sslip.io/g' | kubectl apply --server-side -f -
+```
+
+There is no `make`, no CI pipeline, no Terraform. Manifests are applied with `sunbeam apply` (which runs kustomize build + sed + kubectl apply).
+
+## Critical Rules
+
+- **Do NOT add Helm charts when plain YAML works.** Most resources here are plain YAML. Helm is only used for complex upstream charts (Kratos, Hydra, CloudNativePG, etc.) that have their own release cycles. A new Deployment, Service, or ConfigMap should be plain YAML.
+- **Do NOT create Ingress resources.** This project does not use Kubernetes Ingress. Routing is handled by Pingora (a custom reverse proxy) configured via a TOML ConfigMap in `base/ingress/`. To expose a new service, add a `[[routes]]` entry to the Pingora config.
+- **Do NOT introduce Terraform, Pulumi, or any IaC tool.** Everything is Kustomize.
+- **Do NOT modify overlays when the change belongs in base.** Base holds the canonical config; overlays only hold environment-specific patches. If something applies to both local and production, it goes in base.
+- **Do NOT add RBAC, NetworkPolicy, or PodSecurityPolicy resources.** Linkerd service mesh handles mTLS. RBAC is managed at the k3s level.
+- **Do NOT create new namespaces** without being asked. The namespace layout is intentional.
+- **Do NOT hardcode domains.** Use `DOMAIN_SUFFIX` as a placeholder — it gets substituted at deploy time.
+- **Never commit TLS keys or secrets.** Secrets are managed by OpenBao + Vault Secrets Operator, not stored in this repo.
+
+## Directory Structure
+
+```
+base/ # Canonical manifests (environment-agnostic)
+ {namespace}/ # One directory per Kubernetes namespace
+ kustomization.yaml # Resources, patches, helmCharts
+ namespace.yaml # Namespace definition with Linkerd injection
+ vault-secrets.yaml # VaultAuth + VaultStaticSecret + VaultDynamicSecret
+ {app}-deployment.yaml # Deployments
+ {app}-service.yaml # Services
+ {app}-config.yaml # ConfigMaps
+ {chart}-values.yaml # Helm chart values
+ patch-{what}.yaml # Strategic merge patches
+
+overlays/
+ local/ # Lima VM dev overlay (macOS)
+ kustomization.yaml # Selects base dirs, adds local patches + image overrides
+ patch-*.yaml / values-*.yaml
+ production/ # Scaleway server overlay
+ kustomization.yaml
+ patch-*.yaml / values-*.yaml
+
+scripts/ # Bash automation (local-up.sh, local-down.sh, etc.)
+secrets/ # TLS cert placeholders (gitignored)
+```
+
+## Manifest Conventions — Follow These Exactly
+
+### kustomization.yaml
+
+```yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: {namespace-name}
+
+resources:
+ - namespace.yaml
+ - {app}-deployment.yaml
+ - {app}-service.yaml
+ - vault-secrets.yaml
+
+# Helm charts only for complex upstream projects
+helmCharts:
+ - name: {chart}
+ repo: https://...
+ version: "X.Y.Z"
+ releaseName: {name}
+ namespace: {ns}
+ valuesFile: {chart}-values.yaml
+
+patches:
+ - path: patch-{what}.yaml
+ target:
+ kind: Deployment
+ name: {name}
+```
+
+### Namespace definition
+
+Every namespace gets Linkerd injection:
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: {name}
+ annotations:
+ linkerd.io/inject: enabled
+```
+
+### Deployments
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {app}
+ namespace: {ns}
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: {app}
+ template:
+ metadata:
+ labels:
+ app: {app}
+ spec:
+ containers:
+ - name: {app}
+ image: {app} # Short name — Kustomize images: section handles registry
+ envFrom:
+ - configMapRef:
+ name: {app}-config # Bulk env from ConfigMap
+ env:
+ - name: DB_PASSWORD # Individual secrets from VSO-synced K8s Secrets
+ valueFrom:
+ secretKeyRef:
+ name: {app}-db-credentials
+ key: password
+ livenessProbe:
+ httpGet:
+ path: /__lbheartbeat__
+ port: 8000
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /__heartbeat__
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ resources:
+ limits:
+ memory: 512Mi
+ cpu: 500m
+ requests:
+ memory: 128Mi
+ cpu: 100m
+```
+
+Key patterns:
+- `image: {app}` uses a short name — the actual registry is set via `images:` in the overlay kustomization.yaml
+- Django apps use `/__lbheartbeat__` (liveness) and `/__heartbeat__` (readiness)
+- Init containers run migrations: `python manage.py migrate --no-input`
+- `envFrom` for ConfigMaps, individual `env` entries for secrets
+
+### Secrets (Vault Secrets Operator)
+
+Secrets are NEVER stored in this repo. They flow: OpenBao → VaultStaticSecret/VaultDynamicSecret CRD → K8s Secret → Pod env.
+
+```yaml
+# VaultAuth — one per namespace, always named vso-auth
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultAuth
+metadata:
+ name: vso-auth
+ namespace: {ns}
+spec:
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: vso
+ serviceAccount: default
+
+---
+# Static secrets (OIDC keys, Django secrets, etc.)
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: {secret-name}
+ namespace: {ns}
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: {openbao-path}
+ refreshAfter: 30s
+ destination:
+ name: {k8s-secret-name}
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ KEY_NAME:
+ text: '{{ index .Secrets "openbao-key" }}'
+
+---
+# Dynamic secrets (DB credentials, rotate every 5m)
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultDynamicSecret
+metadata:
+ name: {app}-db-credentials
+ namespace: {ns}
+spec:
+ vaultAuthRef: vso-auth
+ mount: database
+ path: static-creds/{app}
+ allowStaticCreds: true
+ refreshAfter: 5m
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: {app}
+ destination:
+ name: {app}-db-credentials
+ create: true
+ transformation:
+ templates:
+ password:
+ text: 'postgresql://{{ index .Secrets "username" }}:{{ index .Secrets "password" }}@postgres-rw.data.svc.cluster.local:5432/{db}'
+```
+
+### Shared ConfigMaps
+
+Apps in `lasuite` namespace share these ConfigMaps via `envFrom`:
+- `lasuite-postgres` — DB_HOST, DB_PORT, DB_ENGINE
+- `lasuite-valkey` — REDIS_URL, CELERY_BROKER_URL
+- `lasuite-s3` — AWS_S3_ENDPOINT_URL, AWS_S3_REGION_NAME
+- `lasuite-oidc-provider` — OIDC endpoints (uses DOMAIN_SUFFIX)
+
+### Overlay patches
+
+Patches in overlays are strategic merge patches. Name them `patch-{what}.yaml` or `values-{what}.yaml`:
+```yaml
+# overlays/local/patch-oidc-verify-ssl.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: lasuite-oidc-provider
+ namespace: lasuite
+data:
+ OIDC_VERIFY_SSL: "false"
+```
+
+## What NOT to Do
+
+- Don't create abstract base layers, nested overlays, or "common" directories. The structure is flat: `base/{namespace}/` and `overlays/{env}/`.
+- Don't use `configMapGenerator` or `secretGenerator` — ConfigMaps are plain YAML resources, secrets come from VSO.
+- Don't add `commonLabels` or `commonAnnotations` in kustomization.yaml — labels are set per-resource.
+- Don't use JSON patches when a strategic merge patch works.
+- Don't wrap simple services in Helm charts.
+- Don't add comments explaining what standard Kubernetes fields do. Only comment non-obvious decisions.
+- Don't change Helm chart versions without being asked — version pinning is intentional.
+- Don't add monitoring/alerting rules unless asked — monitoring lives in `base/monitoring/`.
+- Don't split a single component across multiple kustomization.yaml directories.
diff --git a/base/build/buildkitd-deployment.yaml b/base/build/buildkitd-deployment.yaml
new file mode 100644
index 0000000..93f7862
--- /dev/null
+++ b/base/build/buildkitd-deployment.yaml
@@ -0,0 +1,43 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: buildkitd
+ namespace: build
+spec:
+ replicas: 1
+ strategy:
+ type: Recreate
+ selector:
+ matchLabels:
+ app: buildkitd
+ template:
+ metadata:
+ labels:
+ app: buildkitd
+ spec:
+ # Use host network so buildkitd can push to src.DOMAIN_SUFFIX (Gitea registry
+ # via Pingora) without DNS resolution issues. The registry runs on the same
+ # node, so host networking routes traffic back to localhost directly.
+ hostNetwork: true
+ dnsPolicy: None
+ dnsConfig:
+ nameservers:
+ - 8.8.8.8
+ - 1.1.1.1
+ containers:
+ - name: buildkitd
+ image: moby/buildkit:v0.28.0
+ args:
+ - --addr
+ - tcp://0.0.0.0:1234
+ ports:
+ - containerPort: 1234
+ securityContext:
+ privileged: true
+ resources:
+ requests:
+ cpu: "500m"
+ memory: "1Gi"
+ limits:
+ cpu: "4"
+ memory: "8Gi"
diff --git a/base/build/buildkitd-service.yaml b/base/build/buildkitd-service.yaml
new file mode 100644
index 0000000..3b5e3e9
--- /dev/null
+++ b/base/build/buildkitd-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: buildkitd
+ namespace: build
+spec:
+ selector:
+ app: buildkitd
+ ports:
+ - port: 1234
+ targetPort: 1234
diff --git a/base/build/kustomization.yaml b/base/build/kustomization.yaml
new file mode 100644
index 0000000..fd0d0af
--- /dev/null
+++ b/base/build/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - namespace.yaml
+ - buildkitd-deployment.yaml
+ - buildkitd-service.yaml
diff --git a/base/build/namespace.yaml b/base/build/namespace.yaml
new file mode 100644
index 0000000..9f9eb35
--- /dev/null
+++ b/base/build/namespace.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: build
diff --git a/base/devtools/vault-secrets.yaml b/base/devtools/vault-secrets.yaml
index 3bd065e..dc03aad 100644
--- a/base/devtools/vault-secrets.yaml
+++ b/base/devtools/vault-secrets.yaml
@@ -24,7 +24,7 @@ spec:
allowStaticCreds: true
refreshAfter: 5m
rolloutRestartTargets:
- - kind: StatefulSet
+ - kind: Deployment
name: gitea
destination:
name: gitea-db-credentials
@@ -47,6 +47,9 @@ spec:
type: kv-v2
path: seaweedfs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: gitea
destination:
name: gitea-s3-credentials
create: true
@@ -70,6 +73,9 @@ spec:
type: kv-v2
path: gitea
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: gitea
destination:
name: gitea-admin-credentials
create: true
diff --git a/base/ingress/pingora-servicemonitor.yaml b/base/ingress/pingora-servicemonitor.yaml
index cb61496..a31ab9c 100644
--- a/base/ingress/pingora-servicemonitor.yaml
+++ b/base/ingress/pingora-servicemonitor.yaml
@@ -5,6 +5,7 @@ metadata:
namespace: ingress
labels:
app: pingora
+ release: kube-prometheus-stack
spec:
selector:
matchLabels:
diff --git a/base/lasuite/collabora-deployment.yaml b/base/lasuite/collabora-deployment.yaml
new file mode 100644
index 0000000..f2e091a
--- /dev/null
+++ b/base/lasuite/collabora-deployment.yaml
@@ -0,0 +1,54 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: collabora
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: collabora
+ template:
+ metadata:
+ labels:
+ app: collabora
+ spec:
+ containers:
+ - name: collabora
+ image: collabora/code:latest
+ ports:
+ - containerPort: 9980
+ env:
+ # Regex of allowed WOPI host origins (Drive's public URL). Escape the dot.
+ - name: aliasgroup1
+ value: "https://drive\\.DOMAIN_SUFFIX:443"
+ # Public hostname — Collabora uses this in self-referencing URLs.
+ - name: server_name
+ value: "docs.DOMAIN_SUFFIX"
+ # TLS is terminated at Pingora; disable Collabora's built-in TLS.
+ - name: extra_params
+ value: "--o:ssl.enable=false --o:ssl.termination=true"
+ - name: dictionaries
+ value: "en_US fr_FR"
+ - name: username
+ valueFrom:
+ secretKeyRef:
+ name: collabora-credentials
+ key: username
+ - name: password
+ valueFrom:
+ secretKeyRef:
+ name: collabora-credentials
+ key: password
+ securityContext:
+ capabilities:
+ add:
+ - SYS_CHROOT
+ - SYS_ADMIN
+ resources:
+ limits:
+ memory: 1Gi
+ cpu: 1000m
+ requests:
+ memory: 512Mi
+ cpu: 100m
diff --git a/base/lasuite/collabora-service.yaml b/base/lasuite/collabora-service.yaml
new file mode 100644
index 0000000..cd7d212
--- /dev/null
+++ b/base/lasuite/collabora-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: collabora
+ namespace: lasuite
+spec:
+ selector:
+ app: collabora
+ ports:
+ - port: 9980
+ targetPort: 9980
diff --git a/base/lasuite/docs-frontend-nginx-configmap.yaml b/base/lasuite/docs-frontend-nginx-configmap.yaml
index f8a7631..4db6cbf 100644
--- a/base/lasuite/docs-frontend-nginx-configmap.yaml
+++ b/base/lasuite/docs-frontend-nginx-configmap.yaml
@@ -1,7 +1,4 @@
-# nginx config for docs-frontend that injects the brand theme CSS at serve time.
-# sub_filter injects the theme.css link before so Cunningham CSS variables
-# are overridden at runtime without rebuilding the app.
-# gzip must be off for sub_filter to work on HTML responses.
+# nginx config for docs-frontend.
apiVersion: v1
kind: ConfigMap
metadata:
@@ -17,9 +14,9 @@ data:
root /app;
gzip off;
- sub_filter '' '';
sub_filter_once off;
- sub_filter_types text/html;
+ sub_filter_types text/html application/javascript;
+ sub_filter '' '';
location / {
try_files $uri index.html $uri/index.html =404;
diff --git a/base/lasuite/docs-values.yaml b/base/lasuite/docs-values.yaml
index 5463f49..bc6e9ee 100644
--- a/base/lasuite/docs-values.yaml
+++ b/base/lasuite/docs-values.yaml
@@ -155,6 +155,7 @@ backend:
height: "auto"
alt: ""
withTitle: true
+ css_url: "https://integration.DOMAIN_SUFFIX/api/v2/theme.css"
waffle:
apiUrl: "https://integration.DOMAIN_SUFFIX/api/v2/services.json"
widgetPath: "https://integration.DOMAIN_SUFFIX/api/v2/lagaufre.js"
diff --git a/base/lasuite/drive-frontend-nginx-configmap.yaml b/base/lasuite/drive-frontend-nginx-configmap.yaml
new file mode 100644
index 0000000..4dc4f0e
--- /dev/null
+++ b/base/lasuite/drive-frontend-nginx-configmap.yaml
@@ -0,0 +1,81 @@
+# nginx config for drive-frontend.
+#
+# /media/ requests are validated via auth_request to the Drive backend before
+# being proxied to SeaweedFS. This avoids exposing S3 credentials to the browser
+# while still serving private files through the CDN path.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: drive-frontend-nginx-conf
+ namespace: lasuite
+data:
+ default.conf: |
+ server {
+ listen 8080;
+ server_name localhost;
+ server_tokens off;
+
+ root /usr/share/nginx/html;
+
+ # Rewrite hardcoded upstream La Gaufre widget and services API URLs to our
+ # integration service. sub_filter doesn't work on gzip-compressed responses.
+ gzip off;
+ sub_filter_once off;
+ sub_filter_types text/html application/javascript;
+ sub_filter 'https://static.suite.anct.gouv.fr/widgets/lagaufre.js'
+ 'https://integration.DOMAIN_SUFFIX/api/v2/lagaufre.js';
+ sub_filter 'https://lasuite.numerique.gouv.fr/api/services'
+ 'https://integration.DOMAIN_SUFFIX/api/v2/services.json';
+ sub_filter 'https://operateurs.suite.anct.gouv.fr/api/v1.0/lagaufre/services/?operator=9f5624fc-ef99-4d10-ae3f-403a81eb16ef&siret=21870030000013'
+ 'https://integration.DOMAIN_SUFFIX/api/v2/services.json';
+ sub_filter '' '';
+
+ # Public file viewer — Next.js static export generates a literal [id].html
+ # template for this dynamic route. Serve it for any file UUID so the
+ # client-side router hydrates the correct FilePage component without auth.
+ location ~ ^/explorer/items/files/[^/]+$ {
+ try_files $uri $uri.html /explorer/items/files/[id].html;
+ }
+
+ # Item detail routes (folders, workspaces, shared items).
+ location ~ ^/explorer/items/[^/]+$ {
+ try_files $uri $uri.html /explorer/items/[id].html;
+ }
+
+ location / {
+ # Try the exact path, then path + .html (Next.js static export generates
+ # e.g. explorer/items/my-files.html), then fall back to index.html for
+ # client-side routes that have no pre-rendered file.
+ try_files $uri $uri.html /index.html;
+ }
+
+ # Protected media: auth via Drive backend, then proxy to S3 with signed headers.
+ # media-auth returns S3 SigV4 Authorization/X-Amz-Date headers; nginx captures
+ # and forwards them so SeaweedFS can verify the request.
+ location /media/ {
+ auth_request /internal/media-auth;
+ auth_request_set $auth_header $upstream_http_authorization;
+ auth_request_set $amz_date $upstream_http_x_amz_date;
+ auth_request_set $amz_content $upstream_http_x_amz_content_sha256;
+ proxy_set_header Authorization $auth_header;
+ proxy_set_header X-Amz-Date $amz_date;
+ proxy_set_header X-Amz-Content-Sha256 $amz_content;
+ proxy_pass http://seaweedfs-filer.storage.svc.cluster.local:8333/sunbeam-drive/;
+ }
+
+ # Internal subrequest: Django checks session and item access, returns S3 auth headers.
+ location = /internal/media-auth {
+ internal;
+ proxy_pass http://drive-backend.lasuite.svc.cluster.local:80/api/v1.0/items/media-auth/;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ proxy_set_header Host drive.sunbeam.pt;
+ proxy_set_header X-Original-URL $scheme://$host$request_uri;
+ }
+
+ error_page 500 502 503 504 @blank_error;
+ location @blank_error {
+ return 200 '';
+ add_header Content-Type text/html;
+ }
+ }
diff --git a/base/lasuite/drive-values.yaml b/base/lasuite/drive-values.yaml
new file mode 100644
index 0000000..68a9dcb
--- /dev/null
+++ b/base/lasuite/drive-values.yaml
@@ -0,0 +1,200 @@
+# La Suite Numérique — Drive (drive chart).
+# Env vars use the chart's dict-based envVars schema:
+# string value → rendered as env.value
+# map value → rendered as env.valueFrom (configMapKeyRef / secretKeyRef)
+# DOMAIN_SUFFIX is substituted by sed at deploy time.
+#
+# Required secrets (created by seed script):
+# oidc-drive — CLIENT_ID, CLIENT_SECRET (created by Hydra Maester)
+# drive-db-credentials — password (VaultDynamicSecret, DB engine)
+# drive-django-secret — DJANGO_SECRET_KEY (VaultStaticSecret)
+# seaweedfs-s3-credentials — S3_ACCESS_KEY, S3_SECRET_KEY (shared)
+
+fullnameOverride: drive
+
+backend:
+ createsuperuser:
+ # No superuser — users authenticate via OIDC.
+ # The chart always renders this Job; override command so it exits 0.
+ command: ["true"]
+
+ envVars: &backendEnvVars
+ # ── Database ──────────────────────────────────────────────────────────────
+ DB_NAME: drive_db
+ DB_USER: drive
+ DB_HOST:
+ configMapKeyRef:
+ name: lasuite-postgres
+ key: DB_HOST
+ DB_PORT:
+ configMapKeyRef:
+ name: lasuite-postgres
+ key: DB_PORT
+ # Drive uses psycopg3 backend (no _psycopg2 suffix).
+ DB_ENGINE: django.db.backends.postgresql
+ DB_PASSWORD:
+ secretKeyRef:
+ name: drive-db-credentials
+ key: password
+
+ # ── Redis / Celery ────────────────────────────────────────────────────────
+ REDIS_URL:
+ configMapKeyRef:
+ name: lasuite-valkey
+ key: REDIS_URL
+ # Drive uses DJANGO_CELERY_BROKER_URL (not CELERY_BROKER_URL).
+ DJANGO_CELERY_BROKER_URL:
+ configMapKeyRef:
+ name: lasuite-valkey
+ key: CELERY_BROKER_URL
+
+ # ── S3 (file storage) ─────────────────────────────────────────────────────
+ AWS_STORAGE_BUCKET_NAME: sunbeam-drive
+ AWS_S3_ENDPOINT_URL:
+ configMapKeyRef:
+ name: lasuite-s3
+ key: AWS_S3_ENDPOINT_URL
+ AWS_S3_REGION_NAME:
+ configMapKeyRef:
+ name: lasuite-s3
+ key: AWS_S3_REGION_NAME
+ AWS_DEFAULT_ACL:
+ configMapKeyRef:
+ name: lasuite-s3
+ key: AWS_DEFAULT_ACL
+ # Drive uses AWS_S3_ACCESS_KEY_ID / AWS_S3_SECRET_ACCESS_KEY (with _S3_ prefix).
+ AWS_S3_ACCESS_KEY_ID:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_ACCESS_KEY
+ AWS_S3_SECRET_ACCESS_KEY:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_SECRET_KEY
+ # Base URL for media file references so nginx auth proxy receives full paths.
+ MEDIA_BASE_URL: https://drive.DOMAIN_SUFFIX
+
+ # ── OIDC (Hydra) ──────────────────────────────────────────────────────────
+ OIDC_RP_CLIENT_ID:
+ secretKeyRef:
+ name: oidc-drive
+ key: CLIENT_ID
+ OIDC_RP_CLIENT_SECRET:
+ secretKeyRef:
+ name: oidc-drive
+ key: CLIENT_SECRET
+ OIDC_RP_SIGN_ALGO:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_RP_SIGN_ALGO
+ OIDC_RP_SCOPES:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_RP_SCOPES
+ OIDC_OP_JWKS_ENDPOINT:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_OP_JWKS_ENDPOINT
+ OIDC_OP_AUTHORIZATION_ENDPOINT:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_OP_AUTHORIZATION_ENDPOINT
+ OIDC_OP_TOKEN_ENDPOINT:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_OP_TOKEN_ENDPOINT
+ OIDC_OP_USER_ENDPOINT:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_OP_USER_ENDPOINT
+ OIDC_OP_LOGOUT_ENDPOINT:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_OP_LOGOUT_ENDPOINT
+ OIDC_VERIFY_SSL:
+ configMapKeyRef:
+ name: lasuite-oidc-provider
+ key: OIDC_VERIFY_SSL
+
+ # ── Resource Server (Drive as OAuth2 RS for Messages integration) ─────────
+ OIDC_RESOURCE_SERVER_ENABLED: "True"
+ # Hydra issuer URL — must match the `iss` claim in introspection responses.
+ OIDC_OP_URL: https://auth.DOMAIN_SUFFIX/
+ # Hydra token introspection endpoint (admin port — no client auth required).
+ OIDC_OP_INTROSPECTION_ENDPOINT: http://hydra-admin.ory.svc.cluster.local:4445/admin/oauth2/introspect
+ # Drive authenticates to Hydra introspection using its own OIDC client creds.
+ OIDC_RS_CLIENT_ID:
+ secretKeyRef:
+ name: oidc-drive
+ key: CLIENT_ID
+ OIDC_RS_CLIENT_SECRET:
+ secretKeyRef:
+ name: oidc-drive
+ key: CLIENT_SECRET
+ # Only accept tokens issued to the messages OAuth2 client (ListValue, comma-separated).
+ OIDC_RS_ALLOWED_AUDIENCES:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_ID
+
+ # ── Django ────────────────────────────────────────────────────────────────
+ DJANGO_SECRET_KEY:
+ secretKeyRef:
+ name: drive-django-secret
+ key: DJANGO_SECRET_KEY
+ DJANGO_CONFIGURATION: Production
+ ALLOWED_HOSTS: drive.DOMAIN_SUFFIX
+ DJANGO_ALLOWED_HOSTS: drive.DOMAIN_SUFFIX
+ DJANGO_CSRF_TRUSTED_ORIGINS: https://drive.DOMAIN_SUFFIX
+ LOGIN_REDIRECT_URL: /
+ LOGOUT_REDIRECT_URL: /
+ SESSION_COOKIE_AGE: "3600"
+ # Session cache TTL must match SESSION_COOKIE_AGE; default is 30s which
+ # causes sessions to expire in Valkey while the cookie remains valid.
+ CACHES_SESSION_TIMEOUT: "3600"
+ # Silent login disabled: the callback redirects back to the returnTo URL
+ # (not LOGIN_REDIRECT_URL) on login_required, causing an infinite reload loop
+ # when the user has no Hydra session. UserProfile shows a Login button instead.
+ FRONTEND_SILENT_LOGIN_ENABLED: "false"
+ # Redirect unauthenticated visitors at / straight to OIDC login instead of
+ # showing the La Suite marketing landing page. returnTo brings them to
+ # their files after successful auth.
+ FRONTEND_EXTERNAL_HOME_URL: "https://drive.DOMAIN_SUFFIX/api/v1.0/authenticate/?returnTo=https%3A%2F%2Fdrive.DOMAIN_SUFFIX%2Fexplorer%2Fitems%2Fmy-files"
+
+ # Allow Messages to call Drive SDK relay cross-origin.
+ SDK_CORS_ALLOWED_ORIGINS: "https://mail.DOMAIN_SUFFIX"
+ CORS_ALLOWED_ORIGINS: "https://mail.DOMAIN_SUFFIX"
+
+ # Allow all file types — self-hosted instance, no need to restrict uploads.
+ RESTRICT_UPLOAD_FILE_TYPE: "False"
+
+ # ── WOPI / Collabora ──────────────────────────────────────────────────────
+ # Comma-separated list of enabled WOPI client names.
+ # Inject Sunbeam theme CSS from the integration service.
+ FRONTEND_CSS_URL: "https://integration.DOMAIN_SUFFIX/api/v2/theme.css"
+
+ WOPI_CLIENTS: collabora
+ # Discovery XML endpoint — Collabora registers supported MIME types here.
+ WOPI_COLLABORA_DISCOVERY_URL: http://collabora.lasuite.svc.cluster.local:9980/hosting/discovery
+ # Base URL Drive uses when building wopi_src callback URLs for Collabora.
+ WOPI_SRC_BASE_URL: https://drive.DOMAIN_SUFFIX
+
+ themeCustomization:
+ enabled: true
+ file_content:
+ css_url: "https://integration.DOMAIN_SUFFIX/api/v2/theme.css"
+ waffle:
+ apiUrl: "https://integration.DOMAIN_SUFFIX/api/v2/services.json"
+ widgetPath: "https://integration.DOMAIN_SUFFIX/api/v2/lagaufre.js"
+ label: "O Estúdio"
+ closeLabel: "Fechar"
+ newWindowLabelSuffix: " · nova janela"
+
+ingress:
+ enabled: false
+
+ingressAdmin:
+ enabled: false
+
+ingressMedia:
+ enabled: false
diff --git a/base/lasuite/hive-config.yaml b/base/lasuite/hive-config.yaml
index 94263bf..a260b38 100644
--- a/base/lasuite/hive-config.yaml
+++ b/base/lasuite/hive-config.yaml
@@ -6,7 +6,7 @@ metadata:
data:
config.toml: |
[drive]
- base_url = "http://drive.lasuite.svc.cluster.local:8000"
+ base_url = "http://drive-backend.lasuite.svc.cluster.local:80"
workspace = "Game Assets"
oidc_client_id = "hive"
oidc_token_url = "http://hydra.ory.svc.cluster.local:4444/oauth2/token"
diff --git a/base/lasuite/integration-deployment.yaml b/base/lasuite/integration-deployment.yaml
index 1e378d6..de15057 100644
--- a/base/lasuite/integration-deployment.yaml
+++ b/base/lasuite/integration-deployment.yaml
@@ -20,20 +20,15 @@ data:
services.json: |
{
"services": [
- {
- "name": "Docs",
- "url": "https://docs.DOMAIN_SUFFIX",
- "logo": "https://integration.DOMAIN_SUFFIX/logos/docs.svg?v=2"
- },
{
"name": "Reuniões",
"url": "https://meet.DOMAIN_SUFFIX",
"logo": "https://integration.DOMAIN_SUFFIX/logos/visio.svg?v=2"
},
{
- "name": "Humans",
- "url": "https://people.DOMAIN_SUFFIX",
- "logo": "https://integration.DOMAIN_SUFFIX/logos/people.svg?v=2"
+ "name": "Drive",
+ "url": "https://drive.DOMAIN_SUFFIX",
+ "logo": "https://integration.DOMAIN_SUFFIX/logos/drive.svg?v=1"
}
]
}
diff --git a/base/lasuite/kustomization.yaml b/base/lasuite/kustomization.yaml
index 07ef139..4bf8151 100644
--- a/base/lasuite/kustomization.yaml
+++ b/base/lasuite/kustomization.yaml
@@ -15,7 +15,8 @@ resources:
- vault-secrets.yaml
- integration-deployment.yaml
- people-frontend-nginx-configmap.yaml
- - docs-frontend-nginx-configmap.yaml
+ - collabora-deployment.yaml
+ - collabora-service.yaml
- meet-config.yaml
- meet-backend-deployment.yaml
- meet-backend-service.yaml
@@ -23,12 +24,29 @@ resources:
- meet-frontend-nginx-configmap.yaml
- meet-frontend-deployment.yaml
- meet-frontend-service.yaml
+ - drive-frontend-nginx-configmap.yaml
+ - messages-config.yaml
+ - messages-backend-deployment.yaml
+ - messages-backend-service.yaml
+ - messages-frontend-theme-configmap.yaml
+ - messages-frontend-deployment.yaml
+ - messages-frontend-service.yaml
+ - messages-worker-deployment.yaml
+ - messages-mta-in-deployment.yaml
+ - messages-mta-in-service.yaml
+ - messages-mta-out-deployment.yaml
+ - messages-mta-out-service.yaml
+ - messages-mpa-dkim-config.yaml
+ - messages-mpa-deployment.yaml
+ - messages-mpa-service.yaml
+ - messages-socks-proxy-deployment.yaml
+ - messages-socks-proxy-service.yaml
patches:
# Rewrite hardcoded production integration URL + inject theme CSS in people-frontend
- path: patch-people-frontend-nginx.yaml
- # Inject theme CSS in docs-frontend
- - path: patch-docs-frontend-nginx.yaml
+ # Mount media auth proxy nginx config in drive-frontend
+ - path: patch-drive-frontend-nginx.yaml
# La Suite Numérique Helm charts.
# Charts with a published Helm repo use helmCharts below.
@@ -42,10 +60,10 @@ helmCharts:
namespace: lasuite
valuesFile: people-values.yaml
- # helm repo add docs https://suitenumerique.github.io/docs/
- - name: docs
- repo: https://suitenumerique.github.io/docs/
- version: "4.5.0"
- releaseName: docs
+ # helm repo add drive https://suitenumerique.github.io/drive/
+ - name: drive
+ repo: https://suitenumerique.github.io/drive/
+ version: "0.14.0"
+ releaseName: drive
namespace: lasuite
- valuesFile: docs-values.yaml
+ valuesFile: drive-values.yaml
diff --git a/base/lasuite/messages-backend-deployment.yaml b/base/lasuite/messages-backend-deployment.yaml
new file mode 100644
index 0000000..6caeddf
--- /dev/null
+++ b/base/lasuite/messages-backend-deployment.yaml
@@ -0,0 +1,183 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-backend
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-backend
+ template:
+ metadata:
+ labels:
+ app: messages-backend
+ spec:
+ initContainers:
+ - name: migrate
+ image: messages-backend
+ command: ["python", "manage.py", "migrate", "--no-input"]
+ envFrom:
+ - configMapRef:
+ name: messages-config
+ - configMapRef:
+ name: lasuite-postgres
+ - configMapRef:
+ name: lasuite-valkey
+ - configMapRef:
+ name: lasuite-s3
+ - configMapRef:
+ name: lasuite-oidc-provider
+ env:
+ - name: DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-db-credentials
+ key: password
+ - name: DJANGO_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: DJANGO_SECRET_KEY
+ - name: SALT_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: SALT_KEY
+ - name: MDA_API_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: MDA_API_SECRET
+ - name: OIDC_RP_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_ID
+ - name: OIDC_RP_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_SECRET
+ - name: AWS_S3_ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_ACCESS_KEY
+ - name: AWS_S3_SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_SECRET_KEY
+ - name: RSPAMD_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-mpa-credentials
+ key: RSPAMD_password
+ - name: OIDC_STORE_REFRESH_TOKEN_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: OIDC_STORE_REFRESH_TOKEN_KEY
+ - name: OIDC_RP_SCOPES
+ value: "openid email profile offline_access"
+ resources:
+ limits:
+ memory: 1Gi
+ cpu: 500m
+ requests:
+ memory: 256Mi
+ cpu: 100m
+ containers:
+ - name: messages-backend
+ image: messages-backend
+ command:
+ - gunicorn
+ - -c
+ - /app/gunicorn.conf.py
+ - messages.wsgi:application
+ ports:
+ - containerPort: 8000
+ envFrom:
+ - configMapRef:
+ name: messages-config
+ - configMapRef:
+ name: lasuite-postgres
+ - configMapRef:
+ name: lasuite-valkey
+ - configMapRef:
+ name: lasuite-s3
+ - configMapRef:
+ name: lasuite-oidc-provider
+ env:
+ - name: DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-db-credentials
+ key: password
+ - name: DJANGO_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: DJANGO_SECRET_KEY
+ - name: SALT_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: SALT_KEY
+ - name: MDA_API_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: MDA_API_SECRET
+ - name: OIDC_RP_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_ID
+ - name: OIDC_RP_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_SECRET
+ - name: AWS_S3_ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_ACCESS_KEY
+ - name: AWS_S3_SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_SECRET_KEY
+ - name: RSPAMD_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-mpa-credentials
+ key: RSPAMD_password
+ - name: OIDC_STORE_REFRESH_TOKEN_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: OIDC_STORE_REFRESH_TOKEN_KEY
+ - name: OIDC_RP_SCOPES
+ value: "openid email profile offline_access"
+ livenessProbe:
+ httpGet:
+ path: /__heartbeat__/
+ port: 8000
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /__heartbeat__/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ resources:
+ limits:
+ memory: 1Gi
+ cpu: 500m
+ requests:
+ memory: 256Mi
+ cpu: 100m
diff --git a/base/lasuite/messages-backend-service.yaml b/base/lasuite/messages-backend-service.yaml
new file mode 100644
index 0000000..ba5ede4
--- /dev/null
+++ b/base/lasuite/messages-backend-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-backend
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-backend
+ ports:
+ - port: 80
+ targetPort: 8000
diff --git a/base/lasuite/messages-config.yaml b/base/lasuite/messages-config.yaml
new file mode 100644
index 0000000..43fc5ed
--- /dev/null
+++ b/base/lasuite/messages-config.yaml
@@ -0,0 +1,45 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: messages-config
+ namespace: lasuite
+data:
+ DJANGO_CONFIGURATION: Production
+ DJANGO_SETTINGS_MODULE: messages.settings
+ DJANGO_ALLOWED_HOSTS: mail.DOMAIN_SUFFIX,messages-backend.lasuite.svc.cluster.local
+ ALLOWED_HOSTS: mail.DOMAIN_SUFFIX,messages-backend.lasuite.svc.cluster.local
+ DJANGO_CSRF_TRUSTED_ORIGINS: https://mail.DOMAIN_SUFFIX
+ DB_NAME: messages_db
+ DB_USER: messages
+ OPENSEARCH_URL: http://opensearch.data.svc.cluster.local:9200
+ MDA_API_BASE_URL: http://messages-backend.lasuite.svc.cluster.local:80/api/v1.0/
+ MYHOSTNAME: mail.DOMAIN_SUFFIX
+ # rspamd URL (auth token injected separately from messages-mpa-credentials secret)
+ SPAM_RSPAMD_URL: http://messages-mpa.lasuite.svc.cluster.local:8010/_api
+ MESSAGES_FRONTEND_BACKEND_SERVER: messages-backend.lasuite.svc.cluster.local:80
+ STORAGE_MESSAGE_IMPORTS_BUCKET_NAME: sunbeam-messages-imports
+ STORAGE_MESSAGE_IMPORTS_ENDPOINT_URL: http://seaweedfs-filer.storage.svc.cluster.local:8333
+ AWS_STORAGE_BUCKET_NAME: sunbeam-messages
+ IDENTITY_PROVIDER: oidc
+ FRONTEND_THEME: default
+ DRIVE_BASE_URL: https://drive.DOMAIN_SUFFIX
+ LOGIN_REDIRECT_URL: https://mail.DOMAIN_SUFFIX
+ LOGOUT_REDIRECT_URL: https://mail.DOMAIN_SUFFIX
+ OIDC_REDIRECT_ALLOWED_HOSTS: '["https://auth.DOMAIN_SUFFIX"]'
+ MTA_OUT_MODE: direct
+ # Create user accounts on first OIDC login (required — no pre-provisioning)
+ OIDC_CREATE_USER: "True"
+ # Redirect to home on auth failure (avoids HttpResponseRedirect(None) → /callback/None 404)
+ LOGIN_REDIRECT_URL_FAILURE: https://mail.DOMAIN_SUFFIX
+ # Store OIDC tokens in session so the Drive integration can proxy requests on behalf of the user.
+ OIDC_STORE_ACCESS_TOKEN: "True"
+ OIDC_STORE_REFRESH_TOKEN: "True"
+ # Session lives 7 days — long enough to survive overnight/weekend without re-auth.
+ # Default is 43200 (12h) which forces a login after a browser restart.
+ SESSION_COOKIE_AGE: "604800"
+ # Renew the id token 60 s before it expires (access_token TTL = 1h).
+ # Without this the default falls back to SESSION_COOKIE_AGE (7 days), which means
+ # every request sees the 1h token as "expiring within 7 days" and triggers a
+ # prompt=none renewal on every page load — causing repeated auth loops.
+ OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS: "60"
+ # offline_access scope is set directly in the deployment env (overrides lasuite-oidc-provider envFrom).
diff --git a/base/lasuite/messages-frontend-deployment.yaml b/base/lasuite/messages-frontend-deployment.yaml
new file mode 100644
index 0000000..a2f09aa
--- /dev/null
+++ b/base/lasuite/messages-frontend-deployment.yaml
@@ -0,0 +1,53 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-frontend
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-frontend
+ template:
+ metadata:
+ labels:
+ app: messages-frontend
+ spec:
+ containers:
+ - name: messages-frontend
+ image: messages-frontend
+ ports:
+ - containerPort: 8080
+ env:
+ - name: MESSAGES_FRONTEND_BACKEND_SERVER
+ value: messages-backend.lasuite.svc.cluster.local:80
+ - name: PORT
+ value: "8080"
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 10
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ volumeMounts:
+ - name: theme
+ mountPath: /app/sunbeam-theme.css
+ subPath: sunbeam-theme.css
+ readOnly: true
+ resources:
+ limits:
+ memory: 256Mi
+ cpu: 250m
+ requests:
+ memory: 64Mi
+ cpu: 50m
+ volumes:
+ - name: theme
+ configMap:
+ name: messages-frontend-theme
diff --git a/base/lasuite/messages-frontend-service.yaml b/base/lasuite/messages-frontend-service.yaml
new file mode 100644
index 0000000..3ec68ca
--- /dev/null
+++ b/base/lasuite/messages-frontend-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-frontend
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-frontend
+ ports:
+ - port: 80
+ targetPort: 8080
diff --git a/base/lasuite/messages-frontend-theme-configmap.yaml b/base/lasuite/messages-frontend-theme-configmap.yaml
new file mode 100644
index 0000000..023aee7
--- /dev/null
+++ b/base/lasuite/messages-frontend-theme-configmap.yaml
@@ -0,0 +1,50 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: messages-frontend-theme
+ namespace: lasuite
+data:
+ sunbeam-theme.css: |
+ /*
+ * O Estúdio — runtime brand overrides for messages-frontend.
+ * Loaded via injected in _document.tsx.
+ * Override Cunningham v4 --c--globals--* variables (no rebuild for updates).
+ */
+ @import url('https://fonts.googleapis.com/css2?family=Ysabeau:ital,wght@0,100..900;1,100..900&display=swap');
+
+ :root {
+ --c--globals--font--families--base: 'Ysabeau Variable', Inter, sans-serif;
+ --c--globals--font--families--accent: 'Ysabeau Variable', Inter, sans-serif;
+
+ /* Brand — amber/gold palette */
+ --c--globals--colors--brand-050: #fffbeb;
+ --c--globals--colors--brand-100: #fef3c7;
+ --c--globals--colors--brand-150: #fde9a0;
+ --c--globals--colors--brand-200: #fde68a;
+ --c--globals--colors--brand-250: #fde047;
+ --c--globals--colors--brand-300: #fcd34d;
+ --c--globals--colors--brand-350: #fbcf3f;
+ --c--globals--colors--brand-400: #fbbf24;
+ --c--globals--colors--brand-450: #f8b31a;
+ --c--globals--colors--brand-500: #f59e0b;
+ --c--globals--colors--brand-550: #e8920a;
+ --c--globals--colors--brand-600: #d97706;
+ --c--globals--colors--brand-650: #c26d05;
+ --c--globals--colors--brand-700: #b45309;
+ --c--globals--colors--brand-750: #9a4508;
+ --c--globals--colors--brand-800: #92400e;
+ --c--globals--colors--brand-850: #7c370c;
+ --c--globals--colors--brand-900: #78350f;
+ --c--globals--colors--brand-950: #451a03;
+
+ /* Logo gradient */
+ --c--globals--colors--logo-1: #f59e0b;
+ --c--globals--colors--logo-2: #d97706;
+ --c--globals--colors--logo-1-light: #f59e0b;
+ --c--globals--colors--logo-2-light: #d97706;
+ --c--globals--colors--logo-1-dark: #fcd34d;
+ --c--globals--colors--logo-2-dark: #fbbf24;
+
+ /* PWA theme color */
+ --sunbeam-brand: #f59e0b;
+ }
diff --git a/base/lasuite/messages-mpa-deployment.yaml b/base/lasuite/messages-mpa-deployment.yaml
new file mode 100644
index 0000000..6341104
--- /dev/null
+++ b/base/lasuite/messages-mpa-deployment.yaml
@@ -0,0 +1,56 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-mpa
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-mpa
+ template:
+ metadata:
+ labels:
+ app: messages-mpa
+ spec:
+ containers:
+ - name: messages-mpa
+ image: messages-mpa
+ ports:
+ - containerPort: 8010
+ env:
+ - name: RSPAMD_password
+ valueFrom:
+ secretKeyRef:
+ name: messages-mpa-credentials
+ key: RSPAMD_password
+ - name: PORT
+ value: "8010"
+ - name: REDIS_HOST
+ value: valkey.data.svc.cluster.local
+ - name: REDIS_PORT
+ value: "6379"
+ volumeMounts:
+ - name: dkim-key
+ mountPath: /etc/rspamd/dkim
+ readOnly: true
+ - name: dkim-signing-conf
+ mountPath: /etc/rspamd/local.d
+ readOnly: true
+ resources:
+ limits:
+ memory: 768Mi
+ cpu: 250m
+ requests:
+ memory: 256Mi
+ cpu: 50m
+ volumes:
+ - name: dkim-key
+ secret:
+ secretName: messages-dkim-key
+ items:
+ - key: dkim-private-key
+ path: default.sunbeam.pt.key
+ - name: dkim-signing-conf
+ configMap:
+ name: messages-mpa-rspamd-config
diff --git a/base/lasuite/messages-mpa-dkim-config.yaml b/base/lasuite/messages-mpa-dkim-config.yaml
new file mode 100644
index 0000000..baff81e
--- /dev/null
+++ b/base/lasuite/messages-mpa-dkim-config.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: messages-mpa-rspamd-config
+ namespace: lasuite
+data:
+ dkim_signing.conf: |
+ enabled = true;
+ selector = "default";
+ path = "/etc/rspamd/dkim/$domain.$selector.key";
+ sign_authenticated = true;
+ sign_local = true;
+ use_domain = "header";
diff --git a/base/lasuite/messages-mpa-service.yaml b/base/lasuite/messages-mpa-service.yaml
new file mode 100644
index 0000000..73801ae
--- /dev/null
+++ b/base/lasuite/messages-mpa-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-mpa
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-mpa
+ ports:
+ - port: 8010
+ targetPort: 8010
diff --git a/base/lasuite/messages-mta-in-deployment.yaml b/base/lasuite/messages-mta-in-deployment.yaml
new file mode 100644
index 0000000..f302ba5
--- /dev/null
+++ b/base/lasuite/messages-mta-in-deployment.yaml
@@ -0,0 +1,43 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-mta-in
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-mta-in
+ template:
+ metadata:
+ labels:
+ app: messages-mta-in
+ spec:
+ containers:
+ - name: messages-mta-in
+ image: messages-mta-in
+ ports:
+ - containerPort: 25
+ env:
+ - name: MDA_API_BASE_URL
+ valueFrom:
+ configMapKeyRef:
+ name: messages-config
+ key: MDA_API_BASE_URL
+ - name: MDA_API_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: MDA_API_SECRET
+ - name: MAX_INCOMING_EMAIL_SIZE
+ value: "30000000"
+ securityContext:
+ capabilities:
+ add: ["NET_BIND_SERVICE"]
+ resources:
+ limits:
+ memory: 256Mi
+ cpu: 250m
+ requests:
+ memory: 64Mi
+ cpu: 50m
diff --git a/base/lasuite/messages-mta-in-service.yaml b/base/lasuite/messages-mta-in-service.yaml
new file mode 100644
index 0000000..0eda54f
--- /dev/null
+++ b/base/lasuite/messages-mta-in-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-mta-in
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-mta-in
+ ports:
+ - port: 25
+ targetPort: 25
diff --git a/base/lasuite/messages-mta-out-deployment.yaml b/base/lasuite/messages-mta-out-deployment.yaml
new file mode 100644
index 0000000..d315065
--- /dev/null
+++ b/base/lasuite/messages-mta-out-deployment.yaml
@@ -0,0 +1,43 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-mta-out
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-mta-out
+ template:
+ metadata:
+ labels:
+ app: messages-mta-out
+ spec:
+ containers:
+ - name: messages-mta-out
+ image: messages-mta-out
+ ports:
+ - containerPort: 587
+ env:
+ - name: MYHOSTNAME
+ valueFrom:
+ configMapKeyRef:
+ name: messages-config
+ key: MYHOSTNAME
+ - name: SMTP_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: messages-mta-out-credentials
+ key: SMTP_USERNAME
+ - name: SMTP_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-mta-out-credentials
+ key: SMTP_PASSWORD
+ resources:
+ limits:
+ memory: 256Mi
+ cpu: 250m
+ requests:
+ memory: 64Mi
+ cpu: 50m
diff --git a/base/lasuite/messages-mta-out-service.yaml b/base/lasuite/messages-mta-out-service.yaml
new file mode 100644
index 0000000..0f7c089
--- /dev/null
+++ b/base/lasuite/messages-mta-out-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-mta-out
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-mta-out
+ ports:
+ - port: 587
+ targetPort: 587
diff --git a/base/lasuite/messages-socks-proxy-deployment.yaml b/base/lasuite/messages-socks-proxy-deployment.yaml
new file mode 100644
index 0000000..00775fd
--- /dev/null
+++ b/base/lasuite/messages-socks-proxy-deployment.yaml
@@ -0,0 +1,35 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-socks-proxy
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-socks-proxy
+ template:
+ metadata:
+ labels:
+ app: messages-socks-proxy
+ spec:
+ containers:
+ - name: messages-socks-proxy
+ image: messages-socks-proxy
+ ports:
+ - containerPort: 1080
+ env:
+ - name: PROXY_USERS
+ valueFrom:
+ secretKeyRef:
+ name: messages-socks-credentials
+ key: PROXY_USERS
+ - name: PROXY_SOURCE_IP_WHITELIST
+ value: 10.0.0.0/8
+ resources:
+ limits:
+ memory: 128Mi
+ cpu: 100m
+ requests:
+ memory: 32Mi
+ cpu: 25m
diff --git a/base/lasuite/messages-socks-proxy-service.yaml b/base/lasuite/messages-socks-proxy-service.yaml
new file mode 100644
index 0000000..221b561
--- /dev/null
+++ b/base/lasuite/messages-socks-proxy-service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: messages-socks-proxy
+ namespace: lasuite
+spec:
+ selector:
+ app: messages-socks-proxy
+ ports:
+ - port: 1080
+ targetPort: 1080
diff --git a/base/lasuite/messages-worker-deployment.yaml b/base/lasuite/messages-worker-deployment.yaml
new file mode 100644
index 0000000..93b79c5
--- /dev/null
+++ b/base/lasuite/messages-worker-deployment.yaml
@@ -0,0 +1,90 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-worker
+ namespace: lasuite
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: messages-worker
+ template:
+ metadata:
+ labels:
+ app: messages-worker
+ spec:
+ containers:
+ - name: messages-worker
+ image: messages-backend
+ command: ["python", "worker.py", "--loglevel=INFO", "--concurrency=3"]
+ envFrom:
+ - configMapRef:
+ name: messages-config
+ - configMapRef:
+ name: lasuite-postgres
+ - configMapRef:
+ name: lasuite-valkey
+ - configMapRef:
+ name: lasuite-s3
+ - configMapRef:
+ name: lasuite-oidc-provider
+ env:
+ - name: DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-db-credentials
+ key: password
+ - name: DJANGO_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: DJANGO_SECRET_KEY
+ - name: SALT_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: SALT_KEY
+ - name: MDA_API_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: MDA_API_SECRET
+ - name: OIDC_RP_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_ID
+ - name: OIDC_RP_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: oidc-messages
+ key: CLIENT_SECRET
+ - name: AWS_S3_ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_ACCESS_KEY
+ - name: AWS_S3_SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_SECRET_KEY
+ - name: RSPAMD_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: messages-mpa-credentials
+ key: RSPAMD_password
+ - name: OIDC_STORE_REFRESH_TOKEN_KEY
+ valueFrom:
+ secretKeyRef:
+ name: messages-django-secret
+ key: OIDC_STORE_REFRESH_TOKEN_KEY
+ - name: OIDC_RP_SCOPES
+ value: "openid email profile offline_access"
+ resources:
+ limits:
+ memory: 1Gi
+ cpu: "1"
+ requests:
+ memory: 256Mi
+ cpu: 100m
diff --git a/base/lasuite/oidc-clients.yaml b/base/lasuite/oidc-clients.yaml
index 3938dc3..aa318c8 100644
--- a/base/lasuite/oidc-clients.yaml
+++ b/base/lasuite/oidc-clients.yaml
@@ -41,7 +41,9 @@ spec:
- code
scope: openid email profile
redirectUris:
- - https://drive.DOMAIN_SUFFIX/oidc/callback/
+ - https://drive.DOMAIN_SUFFIX/api/v1.0/callback/
+ postLogoutRedirectUris:
+ - https://drive.DOMAIN_SUFFIX/api/v1.0/logout-callback/
tokenEndpointAuthMethod: client_secret_post
secretName: oidc-drive
skipConsent: true
@@ -68,25 +70,8 @@ spec:
secretName: oidc-meet
skipConsent: true
---
-# ── Conversations (chat) ──────────────────────────────────────────────────────
-apiVersion: hydra.ory.sh/v1alpha1
-kind: OAuth2Client
-metadata:
- name: conversations
- namespace: lasuite
-spec:
- clientName: Chat
- grantTypes:
- - authorization_code
- - refresh_token
- responseTypes:
- - code
- scope: openid email profile
- redirectUris:
- - https://chat.DOMAIN_SUFFIX/oidc/callback/
- tokenEndpointAuthMethod: client_secret_post
- secretName: oidc-conversations
- skipConsent: true
+# ── Conversations (chat) — replaced by Tuwunel in matrix namespace ───────────
+# OAuth2Client for tuwunel is in base/matrix/hydra-oauth2client.yaml
---
# ── Messages (mail) ───────────────────────────────────────────────────────────
apiVersion: hydra.ory.sh/v1alpha1
@@ -101,9 +86,11 @@ spec:
- refresh_token
responseTypes:
- code
- scope: openid email profile
+ scope: openid email profile offline_access
redirectUris:
- - https://mail.DOMAIN_SUFFIX/oidc/callback/
+ - https://mail.DOMAIN_SUFFIX/api/v1.0/callback/
+ postLogoutRedirectUris:
+ - https://mail.DOMAIN_SUFFIX/api/v1.0/logout-callback/
tokenEndpointAuthMethod: client_secret_post
secretName: oidc-messages
skipConsent: true
diff --git a/base/lasuite/patch-drive-frontend-nginx.yaml b/base/lasuite/patch-drive-frontend-nginx.yaml
new file mode 100644
index 0000000..71cf591
--- /dev/null
+++ b/base/lasuite/patch-drive-frontend-nginx.yaml
@@ -0,0 +1,20 @@
+# Patch: mount the nginx ConfigMap into drive-frontend to enable the media
+# auth_request proxy (validates Drive session before serving S3 files).
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: drive-frontend
+ namespace: lasuite
+spec:
+ template:
+ spec:
+ containers:
+ - name: drive
+ volumeMounts:
+ - name: nginx-conf
+ mountPath: /etc/nginx/conf.d/default.conf
+ subPath: default.conf
+ volumes:
+ - name: nginx-conf
+ configMap:
+ name: drive-frontend-nginx-conf
diff --git a/base/lasuite/seaweedfs-buckets.yaml b/base/lasuite/seaweedfs-buckets.yaml
index 13c98f7..da66ffa 100644
--- a/base/lasuite/seaweedfs-buckets.yaml
+++ b/base/lasuite/seaweedfs-buckets.yaml
@@ -30,10 +30,16 @@ spec:
sunbeam-conversations \
sunbeam-people \
sunbeam-git-lfs \
- sunbeam-game-assets; do
+ sunbeam-game-assets \
+ sunbeam-ml-models; do
mc mb --ignore-existing "weed/$bucket"
echo "Ensured bucket: $bucket"
done
+
+ # Enable object versioning on buckets that require it.
+ # Drive's WOPI GetFile response includes X-WOPI-ItemVersion from S3 VersionId.
+ mc versioning enable weed/sunbeam-drive
+ echo "Versioning enabled: sunbeam-drive"
envFrom:
- secretRef:
name: seaweedfs-s3-credentials
diff --git a/base/lasuite/vault-secrets.yaml b/base/lasuite/vault-secrets.yaml
index 3768406..85adb5e 100644
--- a/base/lasuite/vault-secrets.yaml
+++ b/base/lasuite/vault-secrets.yaml
@@ -22,6 +22,33 @@ spec:
type: kv-v2
path: seaweedfs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: hive
+ - kind: Deployment
+ name: people-backend
+ - kind: Deployment
+ name: people-celery-worker
+ - kind: Deployment
+ name: people-celery-beat
+ - kind: Deployment
+ name: docs-backend
+ - kind: Deployment
+ name: docs-celery-worker
+ - kind: Deployment
+ name: docs-y-provider
+ - kind: Deployment
+ name: drive-backend
+ - kind: Deployment
+ name: drive-backend-celery-default
+ - kind: Deployment
+ name: meet-backend
+ - kind: Deployment
+ name: meet-celery-worker
+ - kind: Deployment
+ name: messages-backend
+ - kind: Deployment
+ name: messages-worker
destination:
name: seaweedfs-s3-credentials
create: true
@@ -70,6 +97,9 @@ spec:
type: kv-v2
path: hive
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: hive
destination:
name: hive-oidc
create: true
@@ -122,6 +152,13 @@ spec:
type: kv-v2
path: people
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: people-backend
+ - kind: Deployment
+ name: people-celery-worker
+ - kind: Deployment
+ name: people-celery-beat
destination:
name: people-django-secret
create: true
@@ -172,6 +209,13 @@ spec:
type: kv-v2
path: docs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: docs-backend
+ - kind: Deployment
+ name: docs-celery-worker
+ - kind: Deployment
+ name: docs-y-provider
destination:
name: docs-django-secret
create: true
@@ -193,6 +237,11 @@ spec:
type: kv-v2
path: docs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: docs-backend
+ - kind: Deployment
+ name: docs-y-provider
destination:
name: docs-collaboration-secret
create: true
@@ -241,6 +290,11 @@ spec:
type: kv-v2
path: meet
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: meet-backend
+ - kind: Deployment
+ name: meet-celery-worker
destination:
name: meet-django-secret
create: true
@@ -264,6 +318,11 @@ spec:
type: kv-v2
path: livekit
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: meet-backend
+ - kind: Deployment
+ name: meet-celery-worker
destination:
name: meet-livekit
create: true
@@ -275,3 +334,241 @@ spec:
text: "{{ index .Secrets \"api-key\" }}"
LIVEKIT_API_SECRET:
text: "{{ index .Secrets \"api-secret\" }}"
+---
+# Drive DB credentials from OpenBao database secrets engine (static role, 24h rotation).
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultDynamicSecret
+metadata:
+ name: drive-db-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: database
+ path: static-creds/drive
+ allowStaticCreds: true
+ refreshAfter: 5m
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: drive-backend
+ - kind: Deployment
+ name: drive-backend-celery-default
+ destination:
+ name: drive-db-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ password:
+ text: "{{ index .Secrets \"password\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: drive-django-secret
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: drive
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: drive-backend
+ - kind: Deployment
+ name: drive-backend-celery-default
+ destination:
+ name: drive-django-secret
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ DJANGO_SECRET_KEY:
+ text: "{{ index .Secrets \"django-secret-key\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: collabora-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: collabora
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: collabora
+ destination:
+ name: collabora-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ username:
+ text: "{{ index .Secrets \"username\" }}"
+ password:
+ text: "{{ index .Secrets \"password\" }}"
+---
+# Messages DB credentials from OpenBao database secrets engine (static role, 24h rotation).
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultDynamicSecret
+metadata:
+ name: messages-db-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: database
+ path: static-creds/messages
+ allowStaticCreds: true
+ refreshAfter: 5m
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-backend
+ - kind: Deployment
+ name: messages-worker
+ destination:
+ name: messages-db-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ password:
+ text: "{{ index .Secrets \"password\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: messages-django-secret
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: messages
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-backend
+ - kind: Deployment
+ name: messages-worker
+ - kind: Deployment
+ name: messages-mta-in
+ destination:
+ name: messages-django-secret
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ DJANGO_SECRET_KEY:
+ text: "{{ index .Secrets \"django-secret-key\" }}"
+ SALT_KEY:
+ text: "{{ index .Secrets \"salt-key\" }}"
+ MDA_API_SECRET:
+ text: "{{ index .Secrets \"mda-api-secret\" }}"
+ OIDC_STORE_REFRESH_TOKEN_KEY:
+ text: "{{ index .Secrets \"oidc-refresh-token-key\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: messages-dkim-key
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: messages
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-mpa
+ destination:
+ name: messages-dkim-key
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ dkim-private-key:
+ text: "{{ index .Secrets \"dkim-private-key\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: messages-mpa-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: messages
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-mpa
+ destination:
+ name: messages-mpa-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ RSPAMD_password:
+ text: "{{ index .Secrets \"rspamd-password\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: messages-socks-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: messages
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-socks-proxy
+ destination:
+ name: messages-socks-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ PROXY_USERS:
+ text: "{{ index .Secrets \"socks-proxy-users\" }}"
+---
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: messages-mta-out-credentials
+ namespace: lasuite
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: messages
+ refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: messages-mta-out
+ destination:
+ name: messages-mta-out-credentials
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ SMTP_USERNAME:
+ text: "{{ index .Secrets \"mta-out-smtp-username\" }}"
+ SMTP_PASSWORD:
+ text: "{{ index .Secrets \"mta-out-smtp-password\" }}"
diff --git a/base/media/vault-secrets.yaml b/base/media/vault-secrets.yaml
index fcee390..4ca44d8 100644
--- a/base/media/vault-secrets.yaml
+++ b/base/media/vault-secrets.yaml
@@ -23,6 +23,9 @@ spec:
type: kv-v2
path: livekit
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: livekit-server
destination:
name: livekit-api-credentials
create: true
@@ -31,4 +34,4 @@ spec:
excludeRaw: true
templates:
keys.yaml:
- text: "{{ index .Secrets \"keys.yaml\" }}"
+ text: '{{ index .Secrets "api-key" }}: {{ index .Secrets "api-secret" }}'
diff --git a/base/monitoring/grafana-oauth2client.yaml b/base/monitoring/grafana-oauth2client.yaml
index d053e19..3d23bc1 100644
--- a/base/monitoring/grafana-oauth2client.yaml
+++ b/base/monitoring/grafana-oauth2client.yaml
@@ -24,9 +24,9 @@ spec:
- code
scope: openid email profile
redirectUris:
- - https://grafana.DOMAIN_SUFFIX/login/generic_oauth
+ - https://metrics.DOMAIN_SUFFIX/login/generic_oauth
postLogoutRedirectUris:
- - https://grafana.DOMAIN_SUFFIX/
+ - https://metrics.DOMAIN_SUFFIX/
tokenEndpointAuthMethod: client_secret_post
secretName: grafana-oidc
skipConsent: true
diff --git a/base/monitoring/prometheus-values.yaml b/base/monitoring/prometheus-values.yaml
index 8510f79..cab5bc3 100644
--- a/base/monitoring/prometheus-values.yaml
+++ b/base/monitoring/prometheus-values.yaml
@@ -38,38 +38,30 @@ grafana:
skip_org_role_sync: true
sidecar:
datasources:
- # Disable the auto-provisioned ClusterIP datasource; we define it
- # explicitly below using the external URL so Grafana's backend reaches
- # Prometheus via Pingora (https://systemmetrics.DOMAIN_SUFFIX) rather
- # than the cluster-internal ClusterIP which is blocked by network policy.
defaultDatasourceEnabled: false
additionalDataSources:
- name: Prometheus
type: prometheus
- url: "https://systemmetrics.DOMAIN_SUFFIX"
+ url: "http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090"
access: proxy
isDefault: true
jsonData:
timeInterval: 30s
- name: Loki
type: loki
- url: "https://systemlogs.DOMAIN_SUFFIX"
+ url: "http://loki-gateway.monitoring.svc.cluster.local:80"
access: proxy
isDefault: false
- name: Tempo
type: tempo
- url: "https://systemtracing.DOMAIN_SUFFIX"
+ url: "http://tempo.monitoring.svc.cluster.local:3200"
access: proxy
isDefault: false
prometheus:
prometheusSpec:
retention: 90d
- # hostNetwork allows Prometheus to reach kubelet (10250) and node-exporter
- # (9100) on the node's public InternalIP. On a single-node bare-metal
- # server, pod-to-node-public-IP traffic doesn't route without this.
- hostNetwork: true
additionalArgs:
# Allow browser-direct queries from the Grafana UI origin.
- name: web.cors.origin
diff --git a/base/monitoring/vault-secrets.yaml b/base/monitoring/vault-secrets.yaml
index 592bada..2e8a361 100644
--- a/base/monitoring/vault-secrets.yaml
+++ b/base/monitoring/vault-secrets.yaml
@@ -23,6 +23,9 @@ spec:
type: kv-v2
path: grafana
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: kube-prometheus-stack-grafana
destination:
name: grafana-admin
create: true
diff --git a/base/ory/vault-secrets.yaml b/base/ory/vault-secrets.yaml
index e39008d..4b5773c 100644
--- a/base/ory/vault-secrets.yaml
+++ b/base/ory/vault-secrets.yaml
@@ -23,6 +23,9 @@ spec:
type: kv-v2
path: hydra
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: hydra
destination:
name: hydra
create: true
@@ -49,6 +52,11 @@ spec:
type: kv-v2
path: kratos
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: kratos
+ - kind: StatefulSet
+ name: kratos-courier
destination:
name: kratos-app-secrets
create: true
@@ -90,30 +98,6 @@ spec:
dsn:
text: "postgresql://{{ index .Secrets \"username\" }}:{{ index .Secrets \"password\" }}@postgres-rw.data.svc.cluster.local:5432/kratos_db?sslmode=disable"
---
-# Login UI session cookie + CSRF protection secrets.
-apiVersion: secrets.hashicorp.com/v1beta1
-kind: VaultStaticSecret
-metadata:
- name: login-ui-secrets
- namespace: ory
-spec:
- vaultAuthRef: vso-auth
- mount: secret
- type: kv-v2
- path: login-ui
- refreshAfter: 30s
- destination:
- name: login-ui-secrets
- create: true
- overwrite: true
- transformation:
- excludeRaw: true
- templates:
- cookie-secret:
- text: "{{ index .Secrets \"cookie-secret\" }}"
- csrf-cookie-secret:
- text: "{{ index .Secrets \"csrf-cookie-secret\" }}"
----
# Hydra DB credentials from OpenBao database secrets engine (static role, 24h rotation).
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
@@ -151,6 +135,9 @@ spec:
type: kv-v2
path: kratos-admin
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: kratos-admin-ui
destination:
name: kratos-admin-ui-secrets
create: true
@@ -164,3 +151,7 @@ spec:
text: "{{ index .Secrets \"csrf-cookie-secret\" }}"
admin-identity-ids:
text: "{{ index .Secrets \"admin-identity-ids\" }}"
+ s3-access-key:
+ text: "{{ index .Secrets \"s3-access-key\" }}"
+ s3-secret-key:
+ text: "{{ index .Secrets \"s3-secret-key\" }}"
diff --git a/base/storage/kustomization.yaml b/base/storage/kustomization.yaml
index 7442318..bd745bc 100644
--- a/base/storage/kustomization.yaml
+++ b/base/storage/kustomization.yaml
@@ -11,3 +11,4 @@ resources:
- seaweedfs-filer.yaml
- seaweedfs-filer-pvc.yaml
- vault-secrets.yaml
+ - seaweedfs-remote-sync.yaml
diff --git a/base/storage/seaweedfs-remote-sync.yaml b/base/storage/seaweedfs-remote-sync.yaml
new file mode 100644
index 0000000..981b9bf
--- /dev/null
+++ b/base/storage/seaweedfs-remote-sync.yaml
@@ -0,0 +1,62 @@
+# SeaweedFS S3 mirror — hourly mc mirror from SeaweedFS → Scaleway Object Storage.
+# Mirrors all buckets to s3://sunbeam-backups/seaweedfs//.
+# No --remove: deleted files are left in Scaleway (versioning provides recovery window).
+# concurrencyPolicy: Forbid prevents overlap if a run takes longer than an hour.
+---
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: seaweedfs-s3-mirror
+ namespace: storage
+spec:
+ schedule: "0 * * * *"
+ concurrencyPolicy: Forbid
+ successfulJobsHistoryLimit: 3
+ failedJobsHistoryLimit: 3
+ jobTemplate:
+ spec:
+ activeDeadlineSeconds: 3300
+ template:
+ spec:
+ restartPolicy: OnFailure
+ containers:
+ - name: mirror
+ image: minio/mc:latest
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -e
+ mc alias set seaweed \
+ http://seaweedfs-filer.storage.svc.cluster.local:8333 \
+ "${S3_ACCESS_KEY}" "${S3_SECRET_KEY}"
+ mc alias set scaleway \
+ https://s3.fr-par.scw.cloud \
+ "${ACCESS_KEY_ID}" "${SECRET_ACCESS_KEY}"
+ mc mirror --overwrite seaweed/ scaleway/sunbeam-backups/seaweedfs/
+ env:
+ - name: S3_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_ACCESS_KEY
+ - name: S3_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: seaweedfs-s3-credentials
+ key: S3_SECRET_KEY
+ - name: ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: scaleway-s3-creds
+ key: ACCESS_KEY_ID
+ - name: SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: scaleway-s3-creds
+ key: SECRET_ACCESS_KEY
+ resources:
+ requests:
+ memory: 128Mi
+ cpu: 10m
+ limits:
+ memory: 512Mi
diff --git a/base/storage/seaweedfs-volume.yaml b/base/storage/seaweedfs-volume.yaml
index 4012000..3b1ebe9 100644
--- a/base/storage/seaweedfs-volume.yaml
+++ b/base/storage/seaweedfs-volume.yaml
@@ -46,7 +46,7 @@ spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
- storage: 20Gi
+ storage: 400Gi
---
apiVersion: v1
kind: Service
diff --git a/base/storage/vault-secrets.yaml b/base/storage/vault-secrets.yaml
index dfb3858..5815d86 100644
--- a/base/storage/vault-secrets.yaml
+++ b/base/storage/vault-secrets.yaml
@@ -11,6 +11,31 @@ spec:
role: vso
serviceAccount: default
---
+# Scaleway S3 credentials for SeaweedFS remote sync.
+# Same KV path as barman; synced separately so storage namespace has its own Secret.
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: scaleway-s3-creds
+ namespace: storage
+spec:
+ vaultAuthRef: vso-auth
+ mount: secret
+ type: kv-v2
+ path: scaleway-s3
+ refreshAfter: 30s
+ destination:
+ name: scaleway-s3-creds
+ create: true
+ overwrite: true
+ transformation:
+ excludeRaw: true
+ templates:
+ ACCESS_KEY_ID:
+ text: "{{ index .Secrets \"access-key-id\" }}"
+ SECRET_ACCESS_KEY:
+ text: "{{ index .Secrets \"secret-access-key\" }}"
+---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
@@ -22,6 +47,9 @@ spec:
type: kv-v2
path: seaweedfs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: seaweedfs-filer
destination:
name: seaweedfs-s3-credentials
create: true
@@ -45,6 +73,9 @@ spec:
type: kv-v2
path: seaweedfs
refreshAfter: 30s
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: seaweedfs-filer
destination:
name: seaweedfs-s3-json
create: true
diff --git a/overlays/local/kustomization.yaml b/overlays/local/kustomization.yaml
index 2c0d821..a24ed58 100644
--- a/overlays/local/kustomization.yaml
+++ b/overlays/local/kustomization.yaml
@@ -12,6 +12,7 @@ kind: Kustomization
# replace DOMAIN_SUFFIX with .sslip.io before kubectl apply.
resources:
+ - ../../base/build
- ../../base/ingress
- ../../base/ory
- ../../base/data
@@ -34,6 +35,7 @@ images:
newName: src.DOMAIN_SUFFIX/studio/people-backend
- name: lasuite/people-frontend
newName: src.DOMAIN_SUFFIX/studio/people-frontend
+ newTag: latest
# amd64-only impress (Docs) images — same mirror pattern.
- name: lasuite/impress-backend
diff --git a/overlays/local/values-resources.yaml b/overlays/local/values-resources.yaml
index 9ac9b1b..c0585ea 100644
--- a/overlays/local/values-resources.yaml
+++ b/overlays/local/values-resources.yaml
@@ -180,78 +180,36 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: docs-celery-worker
+ name: collabora
namespace: lasuite
spec:
- replicas: 1
template:
spec:
containers:
- - name: docs
- env:
- # Celery workers: 2 concurrent workers fits within local memory budget.
- - name: CELERY_WORKER_CONCURRENCY
- value: "2"
- resources:
- limits:
- memory: 384Mi
- requests:
- memory: 128Mi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-backend
- namespace: lasuite
-spec:
- replicas: 1
- template:
- spec:
- containers:
- - name: docs
- env:
- # 2 uvicorn workers instead of the default 4 to stay within local memory budget.
- # Each worker loads the full Django+impress app (~150 MB), so 4 workers
- # pushed peak RSS above 384 Mi and triggered OOMKill at startup.
- - name: WEB_CONCURRENCY
- value: "2"
+ - name: collabora
resources:
limits:
memory: 512Mi
+ cpu: 500m
requests:
- memory: 192Mi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-frontend
- namespace: lasuite
-spec:
- replicas: 1
- template:
- spec:
- containers:
- - name: docs
- resources:
- limits:
- memory: 128Mi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-y-provider
- namespace: lasuite
-spec:
- replicas: 1
- template:
- spec:
- containers:
- - name: docs
- resources:
- limits:
memory: 256Mi
+ cpu: 50m
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: buildkitd
+ namespace: build
+spec:
+ template:
+ spec:
+ containers:
+ - name: buildkitd
+ resources:
requests:
- memory: 64Mi
+ cpu: "250m"
+ memory: "256Mi"
+ limits:
+ cpu: "2"
+ memory: "2Gi"
diff --git a/overlays/production/patch-mta-in-hostport.yaml b/overlays/production/patch-mta-in-hostport.yaml
new file mode 100644
index 0000000..69e3292
--- /dev/null
+++ b/overlays/production/patch-mta-in-hostport.yaml
@@ -0,0 +1,15 @@
+# Bind MTA-in port 25 to the host so inbound email reaches the pod directly.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: messages-mta-in
+ namespace: lasuite
+spec:
+ template:
+ spec:
+ containers:
+ - name: messages-mta-in
+ ports:
+ - containerPort: 25
+ hostPort: 25
+ protocol: TCP
diff --git a/overlays/production/postgres-scheduled-backup.yaml b/overlays/production/postgres-scheduled-backup.yaml
index ced67fe..1efd487 100644
--- a/overlays/production/postgres-scheduled-backup.yaml
+++ b/overlays/production/postgres-scheduled-backup.yaml
@@ -4,8 +4,8 @@ metadata:
name: postgres-daily
namespace: data
spec:
- # Daily at 02:00 UTC
- schedule: "0 2 * * *"
+ # Daily at 02:00 UTC (CNPG uses 6-field cron: second minute hour dom month dow)
+ schedule: "0 0 2 * * *"
backupOwnerReference: self
cluster:
name: postgres
diff --git a/overlays/production/values-resources.yaml b/overlays/production/values-resources.yaml
index 026fddf..1744858 100644
--- a/overlays/production/values-resources.yaml
+++ b/overlays/production/values-resources.yaml
@@ -149,24 +149,6 @@ spec:
limits:
memory: 128Mi
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: login-ui
- namespace: ory
-spec:
- template:
- spec:
- containers:
- - name: login-ui
- resources:
- requests:
- memory: 128Mi
- cpu: 50m
- limits:
- memory: 384Mi
-
---
apiVersion: apps/v1
kind: Deployment
@@ -215,79 +197,17 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: docs-celery-worker
+ name: collabora
namespace: lasuite
spec:
- replicas: 2
template:
spec:
containers:
- - name: docs
- env:
- - name: CELERY_WORKER_CONCURRENCY
- value: "4"
+ - name: collabora
resources:
requests:
memory: 512Mi
- cpu: 250m
- limits:
- memory: 1Gi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-backend
- namespace: lasuite
-spec:
- replicas: 2
- template:
- spec:
- containers:
- - name: docs
- env:
- - name: WEB_CONCURRENCY
- value: "4"
- resources:
- requests:
- memory: 512Mi
- cpu: 250m
- limits:
- memory: 1Gi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-frontend
- namespace: lasuite
-spec:
- replicas: 2
- template:
- spec:
- containers:
- - name: docs
- resources:
- requests:
- memory: 64Mi
- limits:
- memory: 256Mi
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: docs-y-provider
- namespace: lasuite
-spec:
- replicas: 1
- template:
- spec:
- containers:
- - name: docs
- resources:
- requests:
- memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
+ cpu: 1000m