nextcloud: real mesh module, MariaDB→PostgreSQL, S3 via _FILE secrets #59

Merged
jschoubben merged 2 commits from feat/nextcloud-module-postgres-migration into main 2026-09-26 13:06:32 +00:00
Owner

Converts Nextcloud from a HAL-managed, MariaDB-backed app into a proper nox mesh module: postgres-database + s3-bucket + route requirements, own-secrets admin account, a sidecar (mesh-nextcloud) exposing occ/OCS-backed tools.

Built on the existing scaffold (found already checked in, requires/binds/secrets shape pre-existing). What this branch adds on top:

  • Fixed the pinned image digest — the scaffold's digest resolved to PHP 8.5, incompatible with Nextcloud 30 (needs ≤8.4); pinned the digest matching HAL's real running version (PHP 8.3.28).
  • Admin password delivery: the scaffold's own comment flagged OBJECTSTORE_S3_SECRET/admin password as "awaiting a bed". First attempt (a runtime-admin-env env-file resource) was correctly rejected by push's secrets-in-environment check (hq issue 041) — fixed properly with a real _FILE-suffixed env var + volume-mounted secret file, plus matching client.ts support, mirroring minio's client pattern.
  • MESH_NEXTCLOUD_URL was hardcoded to :80 instead of the mesh-assigned ${port:80} — same bug class as an earlier keycloak fix.
  • The sidecar's occ() shells out to docker exec, but the runtime image never carried the docker CLI binary, only the mounted socket — copied it in from Docker's own official docker:cli image via a named build stage (declared as a pinned build.on base per ADR 0097; the host's legacy, non-BuildKit docker build doesn't expand ARGs inside COPY --from, so it needed its own stage rather than COPY --from=${ARG}).
  • The migrated data has no literal admin account — HAL's real admin login is a personal account (jochens), not a generic one. Reusing it would mean resetting a real user's own password without asking. Gave the module its own dedicated mesh-admin admin-group account instead (created once by hand on novox to match this manifest for the already-migrated data; a genuinely fresh install seeds it automatically via NEXTCLOUD_ADMIN_USER/NEXTCLOUD_ADMIN_PASSWORD).

Live on novox: database converted MariaDB→PostgreSQL via occ db:convert-type (real backup taken first, /var/backups/nextcloud-pre-pg-migration/), data directory copied (not moved) into the module's own path, verified via occ status / occ user:list matching HAL's real 6 users, and the sidecar's occ and OCS Sharing API both authenticate successfully against the live instance.

Not yet done: the public route (drive.novox.be) has not been cut over from HAL to this module.

Test plan

  • occ status on the live container reports installed: true, pgsql, no pending DB upgrade
  • occ user:list matches HAL's original 6 users exactly
  • sidecar (mesh-nextcloud) logs clean, no ENOENT/fetch failed/401 after startup
  • OCS Sharing API returns 200 with the new mesh-admin credentials
  • /status.php on the module's own container returns 200, maintenance: false
  • public route cutover (follow-up, not in this PR)
Converts Nextcloud from a HAL-managed, MariaDB-backed app into a proper nox mesh module: `postgres-database` + `s3-bucket` + `route` requirements, own-secrets admin account, a sidecar (`mesh-nextcloud`) exposing `occ`/OCS-backed tools. Built on the existing scaffold (found already checked in, `requires`/`binds`/`secrets` shape pre-existing). What this branch adds on top: - Fixed the pinned image digest — the scaffold's digest resolved to PHP 8.5, incompatible with Nextcloud 30 (needs ≤8.4); pinned the digest matching HAL's real running version (PHP 8.3.28). - Admin password delivery: the scaffold's own comment flagged `OBJECTSTORE_S3_SECRET`/admin password as "awaiting a bed". First attempt (a `runtime-admin-env` env-file resource) was correctly rejected by `push`'s secrets-in-environment check (hq issue 041) — fixed properly with a real `_FILE`-suffixed env var + volume-mounted secret file, plus matching `client.ts` support, mirroring minio's client pattern. - `MESH_NEXTCLOUD_URL` was hardcoded to `:80` instead of the mesh-assigned `${port:80}` — same bug class as an earlier keycloak fix. - The sidecar's `occ()` shells out to `docker exec`, but the runtime image never carried the `docker` CLI binary, only the mounted socket — copied it in from Docker's own official `docker:cli` image via a named build stage (declared as a pinned `build.on` base per ADR 0097; the host's legacy, non-BuildKit `docker build` doesn't expand ARGs inside `COPY --from`, so it needed its own stage rather than `COPY --from=${ARG}`). - The migrated data has no literal `admin` account — HAL's real admin login is a personal account (`jochens`), not a generic one. Reusing it would mean resetting a real user's own password without asking. Gave the module its own dedicated `mesh-admin` admin-group account instead (created once by hand on novox to match this manifest for the already-migrated data; a genuinely fresh install seeds it automatically via `NEXTCLOUD_ADMIN_USER`/`NEXTCLOUD_ADMIN_PASSWORD`). Live on novox: database converted MariaDB→PostgreSQL via `occ db:convert-type` (real backup taken first, `/var/backups/nextcloud-pre-pg-migration/`), data directory copied (not moved) into the module's own path, verified via `occ status` / `occ user:list` matching HAL's real 6 users, and the sidecar's `occ` and OCS Sharing API both authenticate successfully against the live instance. Not yet done: the public route (`drive.novox.be`) has not been cut over from HAL to this module. ## Test plan - [x] `occ status` on the live container reports `installed: true`, `pgsql`, no pending DB upgrade - [x] `occ user:list` matches HAL's original 6 users exactly - [x] sidecar (`mesh-nextcloud`) logs clean, no `ENOENT`/`fetch failed`/401 after startup - [x] OCS Sharing API returns 200 with the new `mesh-admin` credentials - [x] `/status.php` on the module's own container returns 200, `maintenance: false` - [ ] public route cutover (follow-up, not in this PR)
jschoubben added 7 commits 2026-09-25 11:53:19 +00:00
The pinned digest resolved to a PHP 8.5.10 image; Nextcloud 30 refuses to
run above PHP 8.4. Repinned to the current digest for the nextcloud:30 tag
(matches HAL's own NEXTCLOUD_VERSION), which carries PHP 8.3.28 -- the
same version the data being migrated was actually running under.
The sidecar's own client needs MESH_NEXTCLOUD_ADMIN_PASSWORD to list
shares over the OCS API, but the runtime container's env/volumes never
carried it -- only the server container did. Delivered the same way every
other sealed value in this manifest already is: a generated env-file with
the ${secret:admin} substitution, not a raw value in the container's env.
The mesh's own check caught it: an env-file-loaded secret still reaches
the process environment, readable via docker inspect and /proc (hq
04-ISSUES/041) -- the same class of exposure the file-based delivery
exists to avoid. Added MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE support to the
client, matching the pattern the minio client already uses, and mounted
the sealed admin secret directly rather than writing it into an env-file.
- MESH_NEXTCLOUD_URL hardcoded :80 instead of the mesh-assigned ${port:80}
- sidecar's occ() shells to docker exec but the docker CLI binary was never
  present in the runtime image, only the mounted socket
build refused to reach docker:cli implicitly (novox/hq ADR 0097); pin it by
digest and thread it through as DOCKER_CLI, redeclared in the final stage
since args declared before the first FROM don't carry past it
the legacy (non-BuildKit) docker build this host runs doesn't expand ARGs
inside COPY --from — only FROM. Give it its own named stage instead
the migrated data has no literal 'admin' account — HAL's real admin login
is a personal account (jochens), not a generic one. Resetting that would
touch a real user's own credential, so the module gets its own dedicated
admin-group service account instead, same pattern as the minio per-module
service accounts. mesh-admin was created once by hand on novox to match
this manifest for the already-migrated data; a genuinely fresh install
seeds it automatically via NEXTCLOUD_ADMIN_USER/NEXTCLOUD_ADMIN_PASSWORD.
jschoubben added 1 commit 2026-09-25 12:12:22 +00:00
minio runs with MINIO_REGION=eu-west; nextcloud's S3 config never set a
region, so every object write (avatars, file writes) failed signature
validation with AuthorizationHeaderMalformed, surfacing as Internal Server
Error on real page loads
jschoubben added 1 commit 2026-09-25 12:32:46 +00:00
the s3-bucket binding already carries serves.region (same mechanism as
at/port); ${bound:s3-bucket:region} tracks whatever minio is actually
configured with instead of a copy that can drift
jschoubben added 1 commit 2026-09-25 12:43:00 +00:00
OBJECTSTORE_S3_BUCKET=nextcloud was a leftover from before the module
existed — that bucket was created by hand during tonight's earlier HAL
credential stopgap. The mesh's own minio provisioner derives its own
bucket name from the consumer's access-key identity (bucketFor(as) in
minio/client.ts) rather than honouring contributes.s3-bucket.bucket — by
design, so teardown can recompute the name with nothing persisted — and
minted mesh-novox-ncloud, a different bucket. mesh_novox_ncloud's scoped
policy only covers that bucket, so every S3 write 403'd with AccessDenied
trying to touch the old one. Pointed both the request hint and the real
env var at the bucket that's actually there.
Author
Owner

Reviewed against main (73 commits landed since; merges clean).

Almost all of this PR is already on main. The title describes the real mesh module, the MariaDB→PostgreSQL move and S3 via _FILE secrets, but the remaining diff against main is two lines: the S3 bucket renamed from nextcloud to a name carrying this mesh's own name, in the s3-bucket request and in the server env that must agree with it. Worth retitling, or closing with a note if the rename is the only thing left wanted.

On the rename itself: every other module asks for a plain bucket — nextcloud, photos — and a name carrying the mesh's name is particular to one installation, which is what ADR 0112 (proposed) says a definition may not hold. Recorded as hq issue 122 along with the two public-URL literals in #55 and #58; the bucket is the same shape with a different subject, an adopted resource's real name.

It is not urgent, which is the useful part. The module is not deployed — the file-sync service running on the control-node is still the predecessor's container on its own database, so nothing is currently reading either bucket name. That makes this the one instance of the three that can wait for 122's answer without leaving a live service misbehaving.

If the existing bucket must be reused at cutover, ADR 0112's shape for it is a setting on the assignment rather than a value in the definition — which is exactly the third open question on 122.

Reviewed against `main` (73 commits landed since; merges clean). **Almost all of this PR is already on `main`.** The title describes the real mesh module, the MariaDB→PostgreSQL move and S3 via `_FILE` secrets, but the remaining diff against `main` is two lines: the S3 bucket renamed from `nextcloud` to a name carrying this mesh's own name, in the `s3-bucket` request and in the server env that must agree with it. Worth retitling, or closing with a note if the rename is the only thing left wanted. **On the rename itself:** every other module asks for a plain bucket — `nextcloud`, `photos` — and a name carrying the mesh's name is particular to one installation, which is what ADR 0112 (proposed) says a definition may not hold. Recorded as **hq issue 122** along with the two public-URL literals in #55 and #58; the bucket is the same shape with a different subject, an adopted resource's real name. **It is not urgent, which is the useful part.** The module is not deployed — the file-sync service running on the control-node is still the predecessor's container on its own database, so nothing is currently reading either bucket name. That makes this the one instance of the three that can wait for 122's answer without leaving a live service misbehaving. If the existing bucket must be reused at cutover, ADR 0112's shape for it is a setting on the assignment rather than a value in the definition — which is exactly the third open question on 122.
jschoubben added 1 commit 2026-09-26 13:06:15 +00:00
Author
Owner

Merging, by the same reading as #55 and #58: a name particular to one installation in a definition is not a new precedent here — main already carries literal public names in five modules — and hq issue 122 now carries the mechanism that would remove all of them together, including this one.

Two things that made it safe to take rather than hold:

  • Nothing reads it yet. The module is not deployed; the file-sync service on the control-node is still the predecessor's container on its own database. The bucket name matters at cutover, not now.
  • It matches where the data is going. A data migration is in flight on that node, so the manifest agreeing with the real bucket is the useful state, and disagreeing with it at cutover is the expensive one.

Verified: the manifest parses through the controller's ParseManifest, and internal/catalogue passes against the catalogue as changed. main merged in first (73 commits).

Still worth retitling if this PR is referenced later: everything its title describes — the real mesh module, MariaDB→PostgreSQL, S3 via _FILE secrets — was already on main. What lands here is the bucket rename, two lines.

Merging, by the same reading as #55 and #58: a name particular to one installation in a definition is not a new precedent here — `main` already carries literal public names in five modules — and **hq issue 122** now carries the mechanism that would remove all of them together, including this one. Two things that made it safe to take rather than hold: - **Nothing reads it yet.** The module is not deployed; the file-sync service on the control-node is still the predecessor's container on its own database. The bucket name matters at cutover, not now. - **It matches where the data is going.** A data migration is in flight on that node, so the manifest agreeing with the real bucket is the useful state, and disagreeing with it at cutover is the expensive one. Verified: the manifest parses through the controller's `ParseManifest`, and `internal/catalogue` passes against the catalogue as changed. `main` merged in first (73 commits). Still worth retitling if this PR is referenced later: everything its title describes — the real mesh module, MariaDB→PostgreSQL, S3 via `_FILE` secrets — was already on `main`. What lands here is the bucket rename, two lines.
jschoubben merged commit 3d73c9f54e into main 2026-09-26 13:06:32 +00:00
jschoubben deleted branch feat/nextcloud-module-postgres-migration 2026-09-26 13:06:33 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#59