Architecture Map
One-screen orientation for a dev or agent: which repos exist, how they connect, and how GitOps, secrets and auth flow through the cluster. The deep runbooks live in the other files in this folder — this is the map, not the territory. Last verified 2026-06-23 against cluster
garden-wh2mnkj--con-prod-external(con-prod).
1. The repos — who owns what
This platform spans four repos. None is self-contained; a change often touches two.
| Repo | Role | Argo reads from |
|---|---|---|
| Nextcloud-base (this repo) | The multi-tenant Nextcloud platform: tenant values, charts, platform components, and the ESO consumers | Codeberg conduction/Nextcloud-base (main) |
| cluster-infra | Cluster-wide capabilities — external-dns, External Secrets Operator, storage | Codeberg Conduction/cluster-infra (main) |
| KeyCloak | Identity — realm commonground at iam.commonground.nu |
Codeberg Conduction/KeyCloak (main) |
| openwoo-app-config | The provisioning control-plane (platform.commonground.nu) that opens tenant PRs |
Codeberg Conduction/openwoo-app-config |
Golden rule: Argo reads Codeberg, never GitHub. GitHub remotes exist as mirrors (the
org was shadowbanned) but Argo ignores them. git push origin … on most of these repos
pushes to the GitHub mirror and does not deploy — push to the codeberg remote.
2. GitOps — how code reaches the cluster
dev / agent Codeberg (per repo) Argo CD (ns argocd) cluster
─────────── ─────────────────── ─────────────────── ───────
git push ──────────► main ──────────────────────► Application / AppSet ────────► workloads
GitHub mirror (ignored) ✗ │
├─ AppProject gates which
│ sourceRepos / destinations /
│ resource-kinds are allowed
└─ apps registered MANUALLY
(kubectl apply) for cluster-infra;
tenants via the nextcloud-tenants
ApplicationSet (goTemplate)
- AppProjects are the guardrail. If an app's source repo, destination namespace, or a
resource kind is not whitelisted in its
AppProject, the sync fails withInvalidSpecError/SyncFailed: resource ... not permitted. Widening the project is usually the missing step when wiring a new capability. - Tenant apps are generated by the
nextcloud-tenantsApplicationSet from a glob overvalues/tenants/tenant-*.yaml. The Argo Application is namednc-<tenant>; the namespace is the bare tenant name (e.g. appnc-straatje-accept→ nsstraatje-accept). - accept-environment tenants run on the same con-prod cluster as prod tenants.
3. Secrets — External Secrets Operator (ESO)
cluster-infra Nextcloud-base (consumers) tenant namespace
───────────── ────────────────────────── ────────────────
ESO operator + CRDs ──provides─► ClusterSecretStore ┐
(ns external-secrets, "nextcloud-shared-store" │
Helm chart 2.6.0) (kubernetes provider) ─reads──┐ │
▼ ├─► ExternalSecret ─► nextcloud-secrets
seed Secret (out-of-band): │ charts/tenant-secret (assembled
nextcloud-platform/ │ (managed tenants only) Secret)
nextcloud-s3-seed │
ClusterGenerator "nextcloud- │
password" (random pw) ────────────┘
- ESO is a cluster-wide capability, not Nextcloud-specific — that is why the operator
lives in
cluster-infra. Any app/namespace may consume it; only the Nextcloud consumers are wired today. Another team must add its ownExternalSecret+ store/generator and widen its ownAppProjectto permit theexternal-secrets.iokinds. - The shared Fuga S3 seed (
nextcloud-platform/nextcloud-s3-seed) is created out-of-band by copyings3-access-key/s3-secret-keyfrom any existing tenant'snextcloud-secrets(they are platform-identical) — never in Git. - A tenant's
nextcloud-secretsis ESO-rendered only when the tenant file setstenant.secrets.managed: true(new/web-created tenants). Existing tenants keep their script-applied secrets (scripts/create-tenant-secret.sh) untouched. SeeSECRETS.md. - ESO uses
external-secrets.io/v1(the 2.x major dropped the servedv1beta1);ClusterGeneratorisgenerators.external-secrets.io/v1alpha1. - Naming note: "External" reflects ESO's origin (syncing from external vaults like Vault/AWS). Here we use its internal features — the kubernetes provider (source is an in-cluster Secret) and generators (values made in-cluster). The same consumer shape would work later if a real external vault is added, which is why we standardised on it.
4. Auth / SSO (control-plane)
browser ─► oauth2-proxy ─► Keycloak (realm commonground, ─► Google (brokered IdP)
(openwoo- iam.commonground.nu)
platform) client: openwoo-provisioner
/logout ─► /oauth2/sign_out?rd=<Keycloak end-session> ─► back to platform.commonground.nu/
This is the openwoo-app-config provisioning control-plane, not a tenant concern, but it
shares the same Keycloak. Logout needs the Keycloak client to list
https://platform.commonground.nu/ as a Valid post logout redirect URI.
5. Why this storage architecture (node-upgrade resilience)
During Kubernetes node upgrades the provider blocks the OpenStack API, which breaks CSI
attach/mount and any in-cluster NFS provisioner (CreateContainerConfigError, mount
failures, outages). This platform removes the critical dependency on in-cluster NFS and
fragile CSI flows:
| Concern | Fragile (traditional) | This platform (resilient) |
|---|---|---|
| User files | NFS / block storage | S3 primary (Ceph RGW) |
| Config | RWX NFS volume | ConfigMaps + Secrets (stateless) |
| custom_apps | RWX NFS volume | baked into the image (immutable) |
| appdata | RWX NFS volume | minimal CephFS (provider-managed) |
| Sessions / locking | local / file-based | Redis (shared, distributed) |
Result: during a node upgrade pods reschedule without waiting on CSI; user data in S3 stays available.
6. Known issues / watch-outs (2026-06-23)
- Canary override in the
nextcloud-tenantsApplicationSet is selected via a templated filename plushelm.ignoreMissingValueFiles: true: canary tenants loadcanary-overrides.yaml, non-canary tenants resolve the name to a missing file that is skipped. This replaced an earlier{{- if }}list-control line that was invalid YAML and madekubectl applyfail (~line 62); the AppSet is appliable again the documented way. - Tenant deletion does NOT auto-remove the namespace. The ApplicationSet sets
preserveResourcesOnDeletion: true; manual cleanup is required. SeeREMOVING-TENANT.md. (If any doc says Argo auto-deletes the namespace, it is wrong.) - Chart version lives in the ApplicationSet (
targetRevision, default8.9.0) and per-tenant viatenant.chartVersion— not invalues/common.yaml.
7. For agents: start here
- Read this map, then the topic doc you need (see
README.mdin this folder for the index). - Argo reads Codeberg — a GitHub push will not deploy.
- AppProjects gate everything — a sync failure is often a missing whitelist entry.
- Namespace = bare tenant name (
straatje-accept), the Argo app isnc-<tenant>. - prod paths (
*-prod,clusters/prod/…) and pushes tomaingo through the operator, not blindly — confirm first. - Verify live state read-only (
kubectl get app -n argocd …) before asserting; this platform spans four repos plus a cluster and assumptions bite.