Ga naar inhoud

Design — why this repo works the way it does

The problem this solves

The OpenWoo config is an OpenRegister configuration export — an OpenAPI-enveloped JSON document (openapi / info / components / x-openregister) whose components hold the config buckets: registers, schemas, mappings, sources, rules, synchronizations, endpoints, jobs, workflows, objects. It is ~7,500 lines pretty-printed, devs hand-edit and re-export it, and mistakes in it silently break app functionality.

When an export is taken from an instance that has imported into PostgreSQL, runtime state (sync cursors, content hashes, last-synced timestamps, created/updated metadata) leaks back into the export and pollutes the config. That noise causes broken diffs and unpredictable imports. This repo version-controls the config and gates it in CI before it can ever reach a tenant.

Zero third-party dependencies is deliberate: full auditability, no supply-chain surface, reproducible anywhere python3 exists.

Two tracks: source validation and target configuration

Track Question it answers Tooling Needs a tenant?
Source — config validation Is the config artefact correct and portable? scripts/oac.py (lint / sanitize) + tests/ no — runs on the file
Target — configuration, validation & repair Is a running tenant in the desired state, and bring it there scripts/provision.py yes — points at a tenant URL

The source track is the CI gate: pollution, dangling refs and bad authorization keys are caught on the JSON before it can reach a tenant. The target track drives a real tenant over the API and asserts each step — some steps validate (verify-import, sync-check), others configure or repair (settings, oc-settings, import, authorization, catalog, delete-menu, credentials).

Provisioning is operator-driven (public URL by default)

The target track is operator-driven: after Nextcloud-base deploys a tenant, an operator runs provision.py (or the GUI) to converge the WOO config and set the source connection. By default it drives the tenant's public URL (a trusted domain), so it needs no standing in-cluster wiring. We deliberately avoided standing Argo apps for this — the operator runs it on demand.

In-cluster mode (opt-in), for when the public record is unreliable. The public host is published by external-dns with a very short TTL and can briefly flap while a tenant's ingress is (re)created; because nothing caches a short-TTL record, a single run does one DNS lookup per step and any lookup landing in an "absent" moment aborts the whole run with [Errno -5] No address associated with hostname. In-cluster mode sidesteps this: it connects to the tenant's cluster-local Service (http://nextcloud.<org>-<env>.svc.cluster.local:8080, resolved over cluster.local which never flaps) while presenting the public host via --host-header — so Nextcloud still sees a trusted domain (the earlier "internal Host isn't trusted" objection is exactly what --host-header resolves). It also avoids a hairpin from the in-cluster GUI to its own cluster's public ingress IP. Enable it with provision.py --host-header or the GUI's "Via in-cluster service" checkbox (default on for *.commonground.nu); it falls back to the public base for any other host.

Hosted control-plane (webgui/)

The operator flow is also available as a small hosted web GUI — a Flask app (webgui/server.py) that runs provision.py all from a form and streams the log back. One hosted instance can converge any tenant — over the tenant's public URL, or (default for *.commonground.nu) via the cluster-local Service with --host-header, see the in-cluster note above.

  • Auth: no login of its own — it sits behind oauth2-proxy → Keycloak (realm commonground), which brokers Google. The app fails closed (REQUIRE_AUTH, default on): every route except /healthz returns 403 without the proxy's identity header. See webgui/auth/README.md.
  • Creds model A: the operator types the tenant password + source key per run; nothing is stored. Secrets go to the subprocess via env, never argv, never logs.
  • Deploy: webgui/deploy/ (kustomize) — app on 127.0.0.1 + oauth2-proxy sidecar as the sole listener, NetworkPolicy, nginx Ingress + TLS, at platform.commonground.nu. Build the image with make image. See webgui/deploy/README.md. Local dev: REQUIRE_AUTH=false python3 webgui/server.py.

The host is named generically (platform.) because the control-plane is intended to grow beyond provisioning (e.g. driving deployments) over time.

How Nextcloud-base consumes this

Nextcloud-base (the GitOps platform) does not own this config. It consumes a tagged, validated version of config/woo.configuration.json — the same way it pins app versions. Config errors are caught in this repo's CI, before they can reach a tenant.