From 1af31e1596a7201a20dca528f4928fdb2b67af15 Mon Sep 17 00:00:00 2001 From: "mwesterweel@hotmail.com" Date: Fri, 1 May 2026 12:17:46 +0200 Subject: [PATCH 1/5] openspec: add bootstrap-react-platform change proposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Planning docs for mirroring the Nextcloud-base GitOps shape onto the WOO PWA. ApplicationSet + layered Helm values replace the mcc create-react per-tenant manifest generator. Tenant files become 2 lines for the common case; namespace pairs with the Nextcloud co-tenant; image tag pinned to semver (no more latest); branding migration sources truth from live cluster state. No scaffold yet — proposal/design/tasks only. CHANGELOG and .gitignore initialised. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 24 ++++ CHANGELOG.md | 11 ++ .../bootstrap-react-platform/.openspec.yaml | 4 + .../bootstrap-react-platform/design.md | 124 ++++++++++++++++++ .../bootstrap-react-platform/proposal.md | 58 ++++++++ .../changes/bootstrap-react-platform/tasks.md | 99 ++++++++++++++ 6 files changed, 320 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 openspec/changes/bootstrap-react-platform/.openspec.yaml create mode 100644 openspec/changes/bootstrap-react-platform/design.md create mode 100644 openspec/changes/bootstrap-react-platform/proposal.md create mode 100644 openspec/changes/bootstrap-react-platform/tasks.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4df7672 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Editor / OS +.DS_Store +.idea/ +.vscode/ +*.swp +*~ + +# Helm artifacts +charts/*.tgz +charts/**/charts/ + +# Secrets — never commit, per global policy +*.secret.yaml +*.secret.yml +secrets/ +env.local +.env +.env.* + +# Discovery output (live cluster snapshots — see openspec task 1.2) +openspec/**/cluster-state.yaml + +# Local tool caches +.kubeconform-cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6a6a1dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this repository are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Added + +- **2026-05-01** — Initial openspec change `bootstrap-react-platform`. Proposal/design/tasks for mirroring the Nextcloud-base GitOps shape onto the WOO PWA: ApplicationSet + layered Helm values (`common.yaml` → `env/*.yaml` → `tenants/tenant-*.yaml`), vendored `woo-website` chart, AppProject sync-window governance. Replaces the `mcc create-react` per-tenant manifest generator. Tenant files become 2 lines (`name`, `environment`) for the common case; namespace pairs with the Nextcloud co-tenant (`-`); image tag pinned to semver in `common.yaml` (no more `latest`); branding migration sources truth from live cluster state. No code or values scaffolded yet — planning docs only. Files: `openspec/changes/bootstrap-react-platform/{.openspec.yaml,proposal.md,design.md,tasks.md}`. diff --git a/openspec/changes/bootstrap-react-platform/.openspec.yaml b/openspec/changes/bootstrap-react-platform/.openspec.yaml new file mode 100644 index 0000000..839fef6 --- /dev/null +++ b/openspec/changes/bootstrap-react-platform/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-05-01 +updated: 2026-05-01 +pivot: Mirror Nextcloud-base GitOps shape for the WOO PWA — replace per-tenant generator script with ApplicationSet + layered values diff --git a/openspec/changes/bootstrap-react-platform/design.md b/openspec/changes/bootstrap-react-platform/design.md new file mode 100644 index 0000000..1b1b1ed --- /dev/null +++ b/openspec/changes/bootstrap-react-platform/design.md @@ -0,0 +1,124 @@ +## Context + +WOO PWA tenants today are deployed via `mcc create-react `, which generates a stand-alone Argo CD Application manifest from `toolchain/scripts/deployment/create_react.sh`. The script encodes platform conventions (domain suffixes, upstream host pattern, nodepool placement, ingress, TLS) directly in bash. Each tenant manifest is ~50 lines, and platform-wide convention changes require regenerating every manifest. + +`Nextcloud-base` already runs the same shape correctly for Nextcloud tenants: ApplicationSet, layered Helm values (`common.yaml` → `env/*.yaml` → `tenants/tenant-*.yaml`), wave-based rollouts, AppProject sync-window governance. The WOO PWA needs the same — minus everything a static page does not have: + +- No database — no `db/` values layer +- No shared cache, no shared connection pooler — no Redis, no PgBouncer +- No tenant secrets — no External Secrets Operator, no fallback secret generator +- No persistent storage — image is self-contained; runtime config is a small ConfigMap written by the existing chart + +The Helm chart `helm/woo-website` in `woo-website-template-apiv2` is already platform-grade (non-root, capability drop, explicit probes, dedicated SA, resource limits). Its `values.yaml` even names react-base in a comment as the intended platform layer. The chart does not need to change for this proposal. + +## Goals / Non-Goals + +**Goals:** +- A two-line tenant file (`name`, `environment`) is sufficient for the common case. Branding/hostname/API overrides are optional. +- Platform conventions (nodepool, ingress, TLS naming, upstream API base path) live exactly once, in `common.yaml` or `env/*.yaml`. Changing them is a single PR. +- Adding a tenant is pure GitOps: copy template, edit ~2–10 lines, commit, push. No CLI, no cluster access required. +- The Application resource produced by the ApplicationSet is byte-equivalent (name, namespace, parameters, values) to what the bash script produces today, so cut-over per tenant is a no-op data-wise. +- Sync-window and AppProject governance mirror Nextcloud-base — independent project, independent windows, but identical policy shape. + +**Non-Goals:** +- Migrating tenant data — a static frontend has no per-tenant state. +- Replacing the upstream chart — `helm/woo-website` stays as-is; we vendor a copy. +- Replacing `mcc` CLI — the create-react command is deprecated by this proposal but its replacement (a tenant-file generator) is a follow-up change. +- Changing the nodepool placement — staying on `role: prod-nextcloud` is status quo. A separate openspec change can move frontends off the Nextcloud nodepool later. +- Adding auth — the PWA serves public content. Any future auth is a per-tenant opt-in flag and is not in scope here. + +## Decisions + +### Decision 1: ApplicationSet over per-Application generator + +**Choice**: a single `ApplicationSet` named `react-tenants` watches `values/tenants/tenant-*.yaml` and generates one Application per file. Application name, namespace, parameters, and inline `values:` are templated from the tenant file plus `common.yaml` and `env/*.yaml`. + +**Alternatives**: +- Keep the bash generator, just version-control the output: still suffers from convention drift and is not GitOps for "add a tenant." +- Per-tenant Application files in Git, no ApplicationSet: works but every platform change still touches every tenant file. + +**Why ApplicationSet wins**: the value layering is the single source of truth. Convention changes are one-PR. The upstream chart is unchanged. This is exactly the Nextcloud-base shape, already proven on this cluster. + +### Decision 2: Vendor the chart at `react-base/charts/woo-website/` + +**Choice**: copy `woo-website-template-apiv2/helm/woo-website/` into `react-base/charts/woo-website/`. The ApplicationSet sources the chart from `react-base` itself, not from the upstream repo. + +**Alternatives**: +- Helm chart dependency pointing at upstream repo URL: requires the upstream repo to be a Helm chart registry or pinned by commit. Today's manifests pin `targetRevision: main`, which is drift-prone. +- OCI registry: cleaner long-term, but requires CI/CD on `woo-website-template-apiv2` to publish — out of scope. + +**Why vendor wins**: audit trail is one repo. Chart and platform values move together in a single PR. We can flip to OCI once upstream publishes; the change is local to `common.yaml` and the ApplicationSet `source:`. + +### Decision 3: Pair the frontend namespace with the Nextcloud namespace + +**Choice**: ApplicationSet templates produce `name: --reactfront` (matches today) and `destination.namespace: -` — i.e. the WOO PWA pod lives in the **same** namespace as the corresponding Nextcloud tenant (`almere-accept`, `almere-prod`, etc.). Today's manifests put the frontend in a bare `` namespace; that pattern is dropped. + +**Why**: co-locating the frontend with its upstream Nextcloud aligns the operational mental model — one namespace per (org, env) tuple, regardless of stack layer. Diagnostics, RBAC, quotas, and netpols become tractable per (org, env) rather than per technology. Application *name* is unchanged so monitoring and Argo dashboards keep their identifiers. + +**Consequence — this is a namespace migration per tenant, not a re-adoption.** The Deployment/Service/Ingress today live in ``. After cut-over they live in `-`. Argo CD cannot move resources across namespaces; cut-over is "delete in old namespace, create in new namespace," with brief downtime per tenant. See Migration Plan. + +### Decision 4: Co-tenancy with Nextcloud — pod-level labels, not namespace-level + +**Choice**: the Nextcloud platform labels namespaces `app.kubernetes.io/part-of: nextcloud-platform` and that label drives its NetworkPolicies. The react platform does **not** relabel the namespace. Instead, react NetworkPolicies select pods by **pod label** `app.kubernetes.io/part-of: react-platform`. Both platforms can coexist in the same namespace without policy collisions. + +**Why**: rewriting the namespace label would break Nextcloud's NetworkPolicies. Selecting by pod label scopes each platform's policies to its own pods only; cross-platform traffic (frontend → its own Nextcloud upstream) goes via the public ingress, not pod-to-pod, so no special intra-namespace allow is needed. + +### Decision 5: Image tag pinned to semver in `common.yaml` — never `latest` + +**Choice**: `common.yaml` carries `pwa.image.tag: `. Bumping the tag is a platform-wide change. Today's `latest` is dropped on bootstrap; an explicit current semver is pinned. + +**Why**: `latest` is a drift source — pods restarted at different times can run different code. Pinning in `common.yaml` means every tenant runs the same version, and version rollouts are explicit, reviewable PRs. Per the AppProject sync-window policy, image bumps fall under "platform changes" and only sync Mon–Thu 17:00–07:00 Europe/Amsterdam — they affect every tenant. + +### Decision 6: Keep current node placement in `common.yaml` + +**Choice**: `nodeSelector: {role: prod-nextcloud}` and the `nextcloud-only` toleration go into `common.yaml` verbatim. Frontends continue to schedule on the Nextcloud nodepool. + +**Why**: status quo, predictable, no infra change. Reinforced by Decision 3 — frontend pods now live in the same namespace as their Nextcloud upstream, and scheduling them on the same nodepool is consistent with that shape. Filing a separate openspec change later (`react-frontends-off-nextcloud-nodepool`) is the right place if we ever want to split them. + +### Decision 7: Sync-window governance mirrors Nextcloud-base, separate AppProject + +**Choice**: a new AppProject `react-platform` enforces the same Mon–Thu 17:00–07:00 Europe/Amsterdam window for platform-level changes. Tenant-config additions (`values/tenants/` only) are unrestricted. Wave-0 (canary) syncs first. Image-tag bumps in `common.yaml` count as platform-level changes and fall inside the window. + +**Why**: the policy is sound for any GitOps platform; copying it gives us familiar guardrails. Independent project means an outage on the WOO platform cannot pause Nextcloud syncs and vice versa. AppProject `destinations` must include all `-` namespaces — exactly the same list Nextcloud-base's project allows, since the namespaces are shared. + +## Risks / Trade-offs + +| Risk | Severity | Mitigation | +|---|---|---| +| Per-tenant downtime during namespace migration (delete in old ns → create in new ns) | Medium | Schedule cut-over inside the 17:00–07:00 window; cert-manager re-issues TLS automatically; canary tenant first proves the procedure. Static page → no session/data loss, just a brief 5xx during reconcile. | +| TLS cert re-issue rate-limit (Let's Encrypt 5/week per host) if a tenant cut-over fails repeatedly | Low | Use staging issuer for canary's first attempt; only switch to prod issuer once pattern is proven. Stagger waves so worst case is one cert per host per week. | +| New Ingress in new namespace conflicts with old Ingress on same hostname mid-migration | Medium | Cut-over deletes old Application (and its Ingress) **before** the ApplicationSet syncs; serialise per tenant. Brief window where neither Ingress claims the host is intentional. | +| Co-tenancy with Nextcloud breaks Nextcloud's NetworkPolicies | Medium | Decision 4 — keep namespace label `nextcloud-platform`; react policies select pods by `app.kubernetes.io/part-of: react-platform` pod label only. Verify on canary cut-over that Nextcloud pod traffic is unaffected. | +| Vendored chart drifts from upstream | Low | `charts/woo-website/` carries the upstream Chart.yaml `version`; bumping is a deliberate PR. Drift is the *point* — we want platform-controlled rollouts. | +| Convention change in `common.yaml` ripples through all tenants at once (image bump, nodepool change) | Medium | Sync-window restricts platform changes to off-hours; canary syncs first; wave-based rollout limits blast radius. | +| Existing branding hand-edited into live Applications is lost on cut-over | High → Low after task 1.3 | Source of truth for branding is the live cluster (`kubectl get application -o yaml`). Task 1.3 dumps every tenant's `helm.values` before scaffolding any tenant file. | + +## Migration Plan + +State to migrate per tenant: one standalone Argo CD Application named `--reactfront`, plus its Deployment/Service/Ingress in namespace ``. Move target: same Application name, but managed by the new ApplicationSet, with workload running in namespace `-` (the existing Nextcloud namespace). + +There is no per-tenant data to migrate — the page is static, served from the image, with runtime config from a ConfigMap. + +Per-tenant cut-over (start with canary, inside the 17:00–07:00 sync window): +1. Verify the Nextcloud namespace `-` already exists and is labeled `app.kubernetes.io/part-of: nextcloud-platform`. (It will, for any tenant that has a Nextcloud counterpart in that env.) +2. Create `values/tenants/tenant-.yaml`. Branding comes from the live cluster Application (task 1.3), not from the generator script — anything hand-edited post-`create_react.sh` lives only in the cluster. +3. Run `./scripts/smoke-checks.sh` and visually diff the rendered manifest against the live Application's `spec.source.helm.values`. Resolve every diff before proceeding. +4. Delete the standalone Application **with cascade**: `kubectl delete application -n argocd --reactfront` (default cascade — removes Deployment, Service, Ingress in old namespace ``). The hostname is briefly unclaimed. +5. Sync the ApplicationSet — produces an Application with the same name targeting namespace `-`. Resources are created fresh. +6. cert-manager re-issues TLS for the same hostname into the new namespace. Wait for `kubectl get cert -n -` to show Ready=True. +7. Verify ingress is reachable, Application is Healthy/Synced, branding is intact (curl the page, check theme classname / org name). +8. Verify Nextcloud co-tenant pod is unaffected: `kubectl get pods -n -` and check Nextcloud pod restart count is unchanged. +9. Move to next tenant. + +**Total downtime per tenant**: typically 1–3 minutes (delete → recreate → TLS issuance). Acceptable for the static frontend during a sync window. + +**Rollback per tenant**: re-apply the original Application manifest from `toolchain/woo-application--.yaml` (which still targets namespace ``) and remove `tenant-.yaml` from `values/tenants/`. The ApplicationSet stops managing it and the standalone Application is back. Old TLS in the original namespace will need to be re-issued; cert-manager handles this. + +## Resolved Questions + +All four open questions from the initial draft are answered: + +1. **Namespace convention**: pair with the Nextcloud namespace — `-` (e.g. `almere-accept`, `almere-prod`). The bare-name namespace pattern in today's manifests is dropped on cut-over. (See Decision 3.) +2. **Cluster topology**: one cluster hosts both accept and prod, separated by namespace. ApplicationSet `destination.server` is a single value in the template. +3. **Image tag**: never `latest`. Pin to current semver in `common.yaml`. Image-tag bumps are platform-level changes and respect the 17:00–07:00 sync window. (See Decision 5.) +4. **Branding source-of-truth**: the live cluster's `kubectl get application -o yaml` is authoritative. Task 1.3 dumps every tenant's `spec.source.helm.values` and writes the result into the per-tenant YAML before any cut-over. (See Risks table.) diff --git a/openspec/changes/bootstrap-react-platform/proposal.md b/openspec/changes/bootstrap-react-platform/proposal.md new file mode 100644 index 0000000..f97f76f --- /dev/null +++ b/openspec/changes/bootstrap-react-platform/proposal.md @@ -0,0 +1,58 @@ +## Why + +WOO PWA tenants are deployed today via `mcc create-react `, which shells out to `toolchain/scripts/deployment/create_react.sh`. The script generates a per-tenant Argo CD Application manifest with all conventions hardcoded in bash: + +- Domain pattern (`.openwoo.app` / `.accept.openwoo.app`) +- Upstream API host (`.commonground.nu` / `.accept.commonground.nu`) and base path (`/apps/opencatalogi/api`) +- Node placement (`role: prod-nextcloud` + `nextcloud-only` toleration) +- Ingress class, TLS secret name, deploy strategy, image tag + +Each generated manifest is ~50 lines that mostly repeat platform convention. Consequences: + +- **Convention changes are O(N)**: moving frontends to a different nodepool, switching ingress class, or changing the domain suffix requires regenerating every tenant manifest. +- **No drift detection**: there is no shared `common.yaml` defining "what every WOO frontend should look like." A platform-wide fix can pass review for one tenant and silently miss the others. +- **Adding a tenant is not pure GitOps**: the CLI must reach the cluster (`kubectl apply` / `argocd app create`) — there is no PR-only path. +- **The static frontend has no real reason for any of this complexity** — it has no DB, no shared cache, no secrets, and no per-tenant state. + +`Nextcloud-base` already solves the exact same shape for Nextcloud tenants (multi-tenant GitOps, layered Helm values, ApplicationSet, wave-based rollouts). Mirror it for the WOO PWA, minus everything a static page does not need (Redis, PgBouncer, ESO, DB layer, S3 storage class, RWX storage). + +## What Changes + +- New repo skeleton at `react-base/`: + - `react-platform/argo/` — `ApplicationSet` (one Application per `values/tenants/tenant-*.yaml`) + `AppProject` with sync-window governance mirrored from Nextcloud-base + - `react-platform/platform/policies/` — NetworkPolicies (default-deny + allow-ingress-controller + allow-egress to public API hosts) + - `react-platform/values/common.yaml` — chart pin, image, nodeSelector/tolerations, ingress className, deploy strategy, security context, upstream base path, default theme classname + - `react-platform/values/env/{accept,prod}.yaml` — `domainSuffix`, `upstreamHostTemplate`, replicas, HPA on/off, resources + - `react-platform/values/tenants/tenant-*.yaml` — minimal per-tenant: `name`, `environment`, optional `branding`/`hostname`/`apiBaseUrl` overrides + - `react-platform/values/templates/tenant-template.yaml` — copy-paste starting point + - `react-platform/scripts/` — `validate-values.sh`, `smoke-checks.sh` (helm template + kubeconform). No secret scripts (frontends have none). + - `react-platform/docs/` — `ADDING-TENANT.md`, `ROLLOUTS.md` +- `charts/woo-website/` — vendored copy of the existing chart from `woo-website-template-apiv2/helm/woo-website` (locked alongside the platform; can flip to OCI registry later) +- `openspec/changes/` — this proposal +- `CLAUDE.md`, `README.md`, `CHANGELOG.md` +- ApplicationSet template auto-derives, per tenant: Application name (`--reactfront` — matches today, no rename), namespace (`-` — **paired with the Nextcloud namespace; this is a namespace migration from today's bare-name pattern**), hostname, upstream host, upstream base, TLS secret name, wave (default `"1"`) +- Image tag pinned to a semver in `common.yaml` — never `latest`. Image bumps are platform-wide changes and respect the AppProject sync window. + +## Capabilities + +### New Capabilities + +- `tenant-as-data`: a tenant is a 2-line YAML file (name + environment). Branding/hostname/API overrides only set if non-default. +- `wave-rollouts`: canary (wave 0) syncs first, validate, then wave 1+. Mirrors Nextcloud-base. +- `policy-baseline`: NetworkPolicies enforce default-deny + ingress-controller-only ingress + egress to upstream API hosts only. + +### Removed from Scope + +- `secrets`: the WOO PWA serves only public content. No tenant secrets, no ESO, no `create-tenant-secret.sh`. +- `shared-platform-services`: no Redis, no PgBouncer, no DB layer files. Static nginx pod has no shared state. +- `storage`: no PVC, no StorageClass change. Image is self-contained; runtime config is a small ConfigMap written by the chart. + +## Impact + +- **New repo**: `react-base/` (currently empty except `README.md`). User-owned — `CHANGELOG.md` required per global rule. +- **Argo CD**: new `AppProject` `react-platform`, new `ApplicationSet` `react-tenants`. Independent from `nextcloud-platform` project — separate RBAC, separate sync windows. +- **Existing per-tenant manifests** in `toolchain/woo-application-*.yaml` and cluster-side Applications named `--reactfront`: superseded by ApplicationSet-managed Applications with the same names but a **different target namespace** (`-` instead of ``). Cut-over per tenant has 1–3 minutes of downtime: delete old Application + workload in `` namespace, ApplicationSet creates fresh in `-`, cert-manager re-issues TLS. Run inside the sync window. Start with canary. +- **Co-tenancy with Nextcloud**: WOO frontend pods will share namespaces with their Nextcloud upstreams. Namespace label stays `nextcloud-platform`; react NetworkPolicies select pods by `app.kubernetes.io/part-of: react-platform` pod label. No collision with Nextcloud's namespace-label-based policies. +- **`mcc create-react` CLI**: deprecated. Optional follow-up: replace with a `mcc add-react-tenant` that writes `tenant-.yaml` and opens a PR, but **not in scope for this change** — the new flow is "copy template, commit, push." +- **Cluster infra**: no changes. No new StorageClasses, no new platform services, no namespace pre-bootstrap. +- **`charts/woo-website`**: vendored copy. Upstream `woo-website-template-apiv2` continues to publish images; the chart there is the source we vendor. diff --git a/openspec/changes/bootstrap-react-platform/tasks.md b/openspec/changes/bootstrap-react-platform/tasks.md new file mode 100644 index 0000000..102e430 --- /dev/null +++ b/openspec/changes/bootstrap-react-platform/tasks.md @@ -0,0 +1,99 @@ +## 1. Discovery — capture current cluster state before any scaffold + +Conventions are decided (see design.md "Resolved Questions"); discovery is now strictly about capturing the live state we will migrate from. + +- [ ] 1.1 Verify Nextcloud namespaces exist for every WOO tenant we plan to migrate: for each `--reactfront` Application currently in argocd, confirm namespace `-` exists and carries `app.kubernetes.io/part-of: nextcloud-platform`. Any missing pair = blocker (means there is no Nextcloud co-tenant; revisit whether namespace-pairing applies for that tenant). +- [ ] 1.2 Snapshot every existing reactfront Application's full source spec to one file: + ```bash + kubectl get applications -n argocd -o yaml \ + | yq '.items[] | select(.metadata.name | test("-reactfront$")) + | {name: .metadata.name, + ns_today: .spec.destination.namespace, + parameters: .spec.source.helm.parameters, + values: .spec.source.helm.values}' \ + > openspec/changes/bootstrap-react-platform/cluster-state.yaml + ``` + This file is the ground truth for branding (`pwa.env.GATSBY_*`, theme classname, favicon, etc.). Tenant files are derived from it, not from the bash generator. +- [ ] 1.3 Pick the current semver for `pwa.image.tag` from GHCR (`ghcr.io/conductionnl/woo-website-v2`). Record the chosen tag in this tasks file before scaffolding `common.yaml`. +- [ ] 1.4 Pick the canary tenant. Proposal: `test-mcc-accept` (no branding overrides → cleanest convention test). Confirm with operator before proceeding. + +## 2. Repo skeleton — no cluster impact + +- [ ] 2.1 Create directory tree under `react-base/`: + - `react-platform/{argo,platform,values,scripts,docs}/` + - `react-platform/values/{env,tenants,templates}/` + - `react-platform/platform/policies/` + - `charts/` +- [ ] 2.2 Vendor the chart: copy `woo-website-template-apiv2/helm/woo-website/` (refactor branch, current commit) to `react-base/charts/woo-website/`. Verify `helm template ./charts/woo-website` renders cleanly. Record the upstream commit hash in `charts/woo-website/UPSTREAM` so future bumps are traceable. +- [ ] 2.3 Write `react-base/CLAUDE.md`. Mirror Nextcloud-base structure (project overview, common commands, architecture, sync windows, adding-a-tenant). Strip every reference to DB / Redis / PgBouncer / ESO / S3 / RWX storage. Add a "Co-tenancy with Nextcloud" section explaining the shared-namespace model. +- [ ] 2.4 Write `react-base/README.md` (Dutch, same tone as Nextcloud-base; emphasise "2-line tenant" simplicity). +- [ ] 2.5 Initialise `react-base/CHANGELOG.md` with an entry for this scaffold (per global rule: user-owned project requires a changelog). +- [ ] 2.6 Write `react-base/.gitignore`: at minimum `charts/*.tgz`, `**/.DS_Store`, `*.secret.yaml`, `secrets/`, `env.local`, `cluster-state.yaml` if it ends up containing anything sensitive. + +## 3. Values layering — encodes platform conventions + +- [ ] 3.1 `values/common.yaml`: chart version pin, image registry, `pwa.image.tag: ` (never `latest`), `nodeSelector: {role: prod-nextcloud}`, `nextcloud-only` toleration, ingress `className: nginx`, `strategy: {type: Recreate}`, security context defaults from the chart, upstream base path `/apps/opencatalogi/api`, default theme classname, default `replicaCount`. **No domain or upstream host pattern here — those are env-specific.** +- [ ] 3.2 `values/env/accept.yaml`: `domainSuffix: accept.openwoo.app`, `upstreamHostTemplate: .accept.commonground.nu`, replicas, HPA off, accept-tier resources. +- [ ] 3.3 `values/env/prod.yaml`: `domainSuffix: openwoo.app`, `upstreamHostTemplate: .commonground.nu`, replicas, HPA on, prod-tier resources. +- [ ] 3.4 `values/templates/tenant-template.yaml`: 2-line minimum (`tenant.name`, `tenant.environment`) with commented optional overrides for `branding.*`, `hostname`, `apiBaseUrl`, `wave`. +- [ ] 3.5 Generate `values/tenants/tenant-.yaml` per existing tenant by translating `cluster-state.yaml` (task 1.2): copy any `pwa.env.GATSBY_*` and `NL_DESIGN_THEME_CLASSNAME` into the tenant's `branding` map. Tenants whose live state matches the platform default produce a 2-line file. + +## 4. Argo CD — AppProject and ApplicationSet + +- [ ] 4.1 Write `argo/projects/react-platform.yaml` (AppProject). Mirror Nextcloud-base AppProject: + - `sourceRepos`: this repo + - `destinations`: `namespace: '*-accept'` and `namespace: '*-prod'` on the single cluster server (matches Nextcloud-base destinations exactly, since namespaces are shared) + - sync windows: Mon–Thu 17:00–07:00 Europe/Amsterdam — *deny* outside the window for any change touching `values/common.yaml`, `values/env/*.yaml`, `argo/`, `platform/`, `charts/`. *Allow* anytime for changes scoped to `values/tenants/`. +- [ ] 4.2 Write `argo/applicationsets/react-tenants.yaml`. Generator: git directory match on `values/tenants/tenant-*.yaml`. Template: + - `metadata.name: {{ .tenant.name }}-{{ .tenant.environment }}-reactfront` + - `spec.destination.namespace: {{ .tenant.name }}-{{ .tenant.environment }}` (paired with Nextcloud namespace — see design.md Decision 3) + - `spec.destination.server`: the single in-cluster server + - `spec.source.path: charts/woo-website` + - `spec.source.helm.parameters`: derived `global.domain`, `pwa.upstream.base`, `pwa.upstream.host` from env templates + - `spec.source.helm.valueFiles`: `[../../values/common.yaml, ../../values/env/{{ .tenant.environment }}.yaml]` plus inline `values:` for tenant branding +- [ ] 4.3 Verify with `helm template` + `kubeconform`: each scaffolded tenant renders cleanly. Diff the rendered manifest against `cluster-state.yaml` for that tenant; the only expected delta is `metadata.namespace` (old `` → new `-`). + +## 5. Platform policies — pod-label-scoped, not namespace-label-scoped + +- [ ] 5.1 `platform/policies/networkpolicy-react-default-deny.yaml`: deny all ingress and egress for pods labeled `app.kubernetes.io/part-of: react-platform`. **Selects pods, not namespaces** — leaves Nextcloud co-tenant traffic untouched. +- [ ] 5.2 `platform/policies/networkpolicy-react-allow-ingress-controller.yaml`: allow ingress from the ingress-nginx namespace to react pods. +- [ ] 5.3 `platform/policies/networkpolicy-react-allow-egress-public-api.yaml`: allow react pods egress to DNS + the public-internet routable IP for the upstream API hosts. The upstream is *not* the in-cluster Nextcloud pod — the PWA hits the public hostname via ingress, so egress is "anywhere on TCP/443". If we ever switch to in-cluster API access, this rule needs revisiting. +- [ ] 5.4 Verify on canary cut-over (task 8) that the Nextcloud pod in the same namespace shows zero connectivity regression. + +## 6. Scripts + +- [ ] 6.1 `scripts/validate-values.sh`: required fields per tenant file (`tenant.name`, `tenant.environment` ∈ {accept, prod}). Mirror Nextcloud-base shape minus DB/secret checks. +- [ ] 6.2 `scripts/smoke-checks.sh`: `helm template` every tenant against the chart, pipe through `kubeconform` for schema check. No cluster connection required. +- [ ] 6.3 GitHub Actions workflow `.github/workflows/validate.yaml`: yamllint + smoke-checks + values-validate + gitleaks. Mirror Nextcloud-base validate.yaml minus DB-specific jobs. + +## 7. Docs + +- [ ] 7.1 `docs/ADDING-TENANT.md`: copy template, edit name + env, commit, push. ApplicationSet auto-detects. Include the namespace-pairing rule (frontend lands in same namespace as Nextcloud co-tenant; that namespace must already exist). +- [ ] 7.2 `docs/ROLLOUTS.md`: sync windows, wave 0 → 1 → 2 → 3 procedure, image-tag bump procedure (platform change → 17:00 window), rollback (revert PR). +- [ ] 7.3 `docs/MIGRATION.md`: per-tenant cut-over runbook from "namespace ``" to "namespace `-`". Lifted from design.md Migration Plan plus concrete `kubectl` commands. + +## 8. Canary cut-over — single tenant, in the sync window + +- [ ] 8.1 Confirm canary tenant from task 1.4. Verify the target namespace `-` exists and Nextcloud pod is running there. +- [ ] 8.2 Diff `helm template` output of the ApplicationSet-generated Application for this tenant against the live `cluster-state.yaml` row. Resolve every meaningful diff (namespace is expected to differ). +- [ ] 8.3 Wait for the next 17:00–07:00 Europe/Amsterdam window. Announce in the relevant ops channel. +- [ ] 8.4 Capture pre-state: `kubectl get all,ingress,cert -n ` and `kubectl get all,ingress,cert -n ` saved to a timestamped log. +- [ ] 8.5 Delete the standalone Application **with cascade** (default behaviour, not `--cascade=orphan` — we want the workload in the old namespace gone): `kubectl delete application -n argocd --reactfront`. Confirm Deployment/Service/Ingress in `` are removed. +- [ ] 8.6 Sync the ApplicationSet — produces an Application with the same name, targeting ``. Workload is created fresh. +- [ ] 8.7 Wait for cert-manager: `kubectl wait -n --for=condition=Ready cert/ --timeout=5m`. If it times out, check that the Ingress in old namespace is fully gone (host conflict prevents issuance). +- [ ] 8.8 Verify ingress reachable: `curl -I https://` returns 200 with the new TLS cert (check `openssl s_client -connect :443 -servername < /dev/null | openssl x509 -noout -dates`). +- [ ] 8.9 Verify branding survived: curl the page, grep for the expected theme classname / org name from the tenant file. +- [ ] 8.10 Verify Nextcloud co-tenant unaffected: `kubectl get pods -n ` shows Nextcloud pod restart count unchanged from the pre-state log. +- [ ] 8.11 Soak ~24h before any wave-1 cut-over. + +## 9. Wave rollout — only after canary soaks + +- [ ] 9.1 Wave 1: tenants without custom branding (those whose tenant file is just 2 lines). Repeat steps 8.4–8.10 per tenant inside a single sync window. +- [ ] 9.2 Wave 2: tenants with branding (Almere, etc.). Same procedure; pay extra attention to step 8.9 — verify favicon URL, jumbotron URL, org name match the tenant YAML. +- [ ] 9.3 Wave 3: stragglers and any tenant where step 8.2 produced unresolved diffs. + +## 10. Decommission the old path + +- [ ] 10.1 Mark `mcc create-react` as deprecated in toolchain CLI help text. Do not remove yet. +- [ ] 10.2 Move `toolchain/woo-application-*.yaml` files to `toolchain/archive/` (keep for one quarter as rollback reference). +- [ ] 10.3 File a follow-up openspec change `mcc-add-react-tenant`: replacement CLI that writes a tenant YAML and opens a PR. **Out of scope for this change.** From c4222ee5bd31c83abf582d9ad728b6419bbc520a Mon Sep 17 00:00:00 2001 From: "mwesterweel@hotmail.com" Date: Fri, 1 May 2026 12:58:37 +0200 Subject: [PATCH 2/5] openspec: document external dependencies (DNS, TLS) in bootstrap-react-platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DNS is handled by cluster-infra/external-dns with the Cloudflare provider and policy: sync — tenant Ingress creation/deletion auto-manages records in commonground.nu / openwoo.app / opencatalogi.nl zones. TLS is cert-manager via HTTP-01 (Cloudflare proxy intentionally off). Recorded in design.md (new External dependencies subsection), proposal.md (Removed from Scope), and tasks.md (7.1 will surface this in docs/ADDING-TENANT.md so operators know not to touch Cloudflare). Also broadens .gitignore to cover *.pem, *.key, *_rsa, *.crt, *.p12 per global secret-handling policy (pre-commit hook requirement). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 5 +++++ CHANGELOG.md | 1 + openspec/changes/bootstrap-react-platform/design.md | 8 ++++++++ openspec/changes/bootstrap-react-platform/proposal.md | 2 ++ openspec/changes/bootstrap-react-platform/tasks.md | 2 +- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4df7672..0a47f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ charts/**/charts/ # Secrets — never commit, per global policy *.secret.yaml *.secret.yml +*.pem +*.key +*_rsa +*.crt +*.p12 secrets/ env.local .env diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6a1dc..9a4a8ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - **2026-05-01** — Initial openspec change `bootstrap-react-platform`. Proposal/design/tasks for mirroring the Nextcloud-base GitOps shape onto the WOO PWA: ApplicationSet + layered Helm values (`common.yaml` → `env/*.yaml` → `tenants/tenant-*.yaml`), vendored `woo-website` chart, AppProject sync-window governance. Replaces the `mcc create-react` per-tenant manifest generator. Tenant files become 2 lines (`name`, `environment`) for the common case; namespace pairs with the Nextcloud co-tenant (`-`); image tag pinned to semver in `common.yaml` (no more `latest`); branding migration sources truth from live cluster state. No code or values scaffolded yet — planning docs only. Files: `openspec/changes/bootstrap-react-platform/{.openspec.yaml,proposal.md,design.md,tasks.md}`. +- **2026-05-01** — Document external dependencies in `bootstrap-react-platform`: DNS handled by `cluster-infra/external-dns` (Cloudflare, `policy: sync`, zones `commonground.nu`/`openwoo.app`/`opencatalogi.nl`) — tenant adds/removes auto-create/auto-reap DNS records. TLS via cert-manager + HTTP-01. Recorded in `design.md` (new "External dependencies" subsection), `proposal.md` (Removed from Scope), and `tasks.md` (task 7.1 will surface this in `docs/ADDING-TENANT.md`). diff --git a/openspec/changes/bootstrap-react-platform/design.md b/openspec/changes/bootstrap-react-platform/design.md index 1b1b1ed..47a89fb 100644 --- a/openspec/changes/bootstrap-react-platform/design.md +++ b/openspec/changes/bootstrap-react-platform/design.md @@ -11,6 +11,14 @@ WOO PWA tenants today are deployed via `mcc create-react `, which gen The Helm chart `helm/woo-website` in `woo-website-template-apiv2` is already platform-grade (non-root, capability drop, explicit probes, dedicated SA, resource limits). Its `values.yaml` even names react-base in a comment as the intended platform layer. The chart does not need to change for this proposal. +### External dependencies (already in place — not in scope) + +- **DNS**: `cluster-infra/external-dns/` runs cluster-wide with the Cloudflare provider and `policy: sync`. It watches `Ingress` resources and manages records in the `commonground.nu`, `openwoo.app`, and `opencatalogi.nl` zones. Tenant Ingress creation auto-creates the DNS record; tenant deletion auto-reaps it. **No Cloudflare action is needed when adding or removing a tenant.** +- **TLS**: cert-manager issues certificates via HTTP-01. Cloudflare proxy is intentionally off in `cluster-infra` so the challenge can complete; we must not flip it on. +- **Ingress controller**: ingress-nginx is the cluster's ingress class and is what `external-dns` resolves to. + +These three dependencies are owned by `cluster-infra` and are stable. If any of them moves (e.g. cert-manager → cert-issuer change, ingress class swap, additional Cloudflare zone), this design's `common.yaml` is the right place to react. + ## Goals / Non-Goals **Goals:** diff --git a/openspec/changes/bootstrap-react-platform/proposal.md b/openspec/changes/bootstrap-react-platform/proposal.md index f97f76f..fc65bbe 100644 --- a/openspec/changes/bootstrap-react-platform/proposal.md +++ b/openspec/changes/bootstrap-react-platform/proposal.md @@ -46,6 +46,8 @@ Each generated manifest is ~50 lines that mostly repeat platform convention. Con - `secrets`: the WOO PWA serves only public content. No tenant secrets, no ESO, no `create-tenant-secret.sh`. - `shared-platform-services`: no Redis, no PgBouncer, no DB layer files. Static nginx pod has no shared state. - `storage`: no PVC, no StorageClass change. Image is self-contained; runtime config is a small ConfigMap written by the chart. +- `dns`: handled by `cluster-infra/external-dns` (Cloudflare provider, `policy: sync`, zones `commonground.nu`/`openwoo.app`/`opencatalogi.nl`). Tenant Ingress creation/deletion auto-creates and auto-reaps the DNS record — operators never touch Cloudflare to add a tenant. +- `tls`: cert-manager + HTTP-01, already wired in `cluster-infra`. No per-tenant cert configuration in this repo. ## Impact diff --git a/openspec/changes/bootstrap-react-platform/tasks.md b/openspec/changes/bootstrap-react-platform/tasks.md index 102e430..04d99a4 100644 --- a/openspec/changes/bootstrap-react-platform/tasks.md +++ b/openspec/changes/bootstrap-react-platform/tasks.md @@ -68,7 +68,7 @@ Conventions are decided (see design.md "Resolved Questions"); discovery is now s ## 7. Docs -- [ ] 7.1 `docs/ADDING-TENANT.md`: copy template, edit name + env, commit, push. ApplicationSet auto-detects. Include the namespace-pairing rule (frontend lands in same namespace as Nextcloud co-tenant; that namespace must already exist). +- [ ] 7.1 `docs/ADDING-TENANT.md`: copy template, edit name + env, commit, push. ApplicationSet auto-detects. Include the namespace-pairing rule (frontend lands in same namespace as Nextcloud co-tenant; that namespace must already exist). State explicitly that **DNS is automatic** — `cluster-infra/external-dns` creates the Cloudflare record from the tenant's Ingress hostname; the operator does not touch Cloudflare. Same for TLS (cert-manager via HTTP-01). - [ ] 7.2 `docs/ROLLOUTS.md`: sync windows, wave 0 → 1 → 2 → 3 procedure, image-tag bump procedure (platform change → 17:00 window), rollback (revert PR). - [ ] 7.3 `docs/MIGRATION.md`: per-tenant cut-over runbook from "namespace ``" to "namespace `-`". Lifted from design.md Migration Plan plus concrete `kubectl` commands. From fa9e01f99fc9aba0a49ee415d3bb51f6b4a88c3f Mon Sep 17 00:00:00 2001 From: "mwesterweel@hotmail.com" Date: Fri, 1 May 2026 14:46:07 +0200 Subject: [PATCH 3/5] feat: scaffold react-platform (openspec phases 2-7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Nextcloud-base GitOps shape onto the WOO PWA. Two-line tenant files, layered Helm values, single ApplicationSet, AppProject sync window, co-tenancy with Nextcloud namespaces. Highlights - Vendored chart at charts/woo-website (upstream b1ac4e89, refactor branch) with added templates/networkpolicy.yaml (pod-label-scoped default-deny + allow-ingress + allow-egress). Divergences recorded in UPSTREAM. - values/common.yaml pins image tag to 1.0.0 (never latest); nodepool / toleration / ingress / security defaults from create_react.sh moved here. - env/{accept,prod}.yaml carry replicas + autoscaling + resource ceilings. Domain logic lives in the ApplicationSet template, not in env files. - AppProject react-platform mirrors Nextcloud-base sync windows (deny weekday 07-17 Europe/Amsterdam for platform changes). - ApplicationSet react-tenants generates one Application per values/tenants/tenant-*.yaml. Application name keeps today's --reactfront convention; namespace is now - (paired with Nextcloud co-tenant). - scripts/smoke-checks.sh reproduces the ApplicationSet inline-values block in bash so offline render testing matches what Argo would produce. Synthetic tenant-test-mcc renders 9 resources clean. - CLAUDE.md, README.md (Dutch), and docs/{ADDING-TENANT,ROLLOUTS, MIGRATION}.md cover operations + cut-over runbook. Discovery note: the existing manifests' strategy: {type: Recreate} is dead config — the chart Deployment template has no strategy block, so it was silently ignored. Default RollingUpdate is preserved (better for static frontend; no behavior change). Phase 1 (cluster discovery), 8 (canary cut-over), 9 (waves), and 10 (decommission old path) are deferred to live operations and require explicit operator go in a sync window. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + CLAUDE.md | 134 ++++++++ README.md | 187 +++++++++++ charts/woo-website/.helmignore | 25 ++ charts/woo-website/Chart.yaml | 23 ++ charts/woo-website/README.md | 118 +++++++ charts/woo-website/UPSTREAM | 16 + charts/woo-website/artifacthub-repo.yaml | 20 ++ charts/woo-website/index.yaml | 293 ++++++++++++++++++ charts/woo-website/templates/NOTES.txt | 22 ++ charts/woo-website/templates/_helpers.tpl | 88 ++++++ .../woo-website/templates/config-runtime.yaml | 14 + charts/woo-website/templates/deployment.yaml | 82 +++++ charts/woo-website/templates/hpa.yaml | 22 ++ charts/woo-website/templates/ingress.yaml | 89 ++++++ .../woo-website/templates/networkpolicy.yaml | 92 ++++++ charts/woo-website/templates/pwa-service.yaml | 14 + charts/woo-website/templates/secrets.yaml | 1 + .../woo-website/templates/serviceaccount.yaml | 12 + .../templates/tests/test-connection.yaml | 15 + charts/woo-website/values.yaml | 132 ++++++++ .../changes/bootstrap-react-platform/tasks.md | 60 ++-- .../argo/applicationsets/react-tenants.yaml | 157 ++++++++++ .../argo/projects/react-platform.yaml | 99 ++++++ react-platform/docs/ADDING-TENANT.md | 106 +++++++ react-platform/docs/MIGRATION.md | 127 ++++++++ react-platform/docs/ROLLOUTS.md | 81 +++++ react-platform/platform/policies/README.md | 35 +++ react-platform/scripts/smoke-checks.sh | 157 ++++++++++ react-platform/scripts/validate-values.sh | 76 +++++ react-platform/values/common.yaml | 127 ++++++++ react-platform/values/env/accept.yaml | 12 + react-platform/values/env/prod.yaml | 22 ++ .../values/templates/tenant-template.yaml | 53 ++++ .../values/tenants/tenant-test-mcc.yaml | 6 + 35 files changed, 2484 insertions(+), 34 deletions(-) create mode 100644 CLAUDE.md create mode 100644 charts/woo-website/.helmignore create mode 100644 charts/woo-website/Chart.yaml create mode 100644 charts/woo-website/README.md create mode 100644 charts/woo-website/UPSTREAM create mode 100644 charts/woo-website/artifacthub-repo.yaml create mode 100644 charts/woo-website/index.yaml create mode 100644 charts/woo-website/templates/NOTES.txt create mode 100644 charts/woo-website/templates/_helpers.tpl create mode 100644 charts/woo-website/templates/config-runtime.yaml create mode 100644 charts/woo-website/templates/deployment.yaml create mode 100644 charts/woo-website/templates/hpa.yaml create mode 100644 charts/woo-website/templates/ingress.yaml create mode 100644 charts/woo-website/templates/networkpolicy.yaml create mode 100644 charts/woo-website/templates/pwa-service.yaml create mode 100644 charts/woo-website/templates/secrets.yaml create mode 100644 charts/woo-website/templates/serviceaccount.yaml create mode 100644 charts/woo-website/templates/tests/test-connection.yaml create mode 100644 charts/woo-website/values.yaml create mode 100644 react-platform/argo/applicationsets/react-tenants.yaml create mode 100644 react-platform/argo/projects/react-platform.yaml create mode 100644 react-platform/docs/ADDING-TENANT.md create mode 100644 react-platform/docs/MIGRATION.md create mode 100644 react-platform/docs/ROLLOUTS.md create mode 100644 react-platform/platform/policies/README.md create mode 100755 react-platform/scripts/smoke-checks.sh create mode 100755 react-platform/scripts/validate-values.sh create mode 100644 react-platform/values/common.yaml create mode 100644 react-platform/values/env/accept.yaml create mode 100644 react-platform/values/env/prod.yaml create mode 100644 react-platform/values/templates/tenant-template.yaml create mode 100644 react-platform/values/tenants/tenant-test-mcc.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a4a8ec..cea8f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **2026-05-01** — Initial openspec change `bootstrap-react-platform`. Proposal/design/tasks for mirroring the Nextcloud-base GitOps shape onto the WOO PWA: ApplicationSet + layered Helm values (`common.yaml` → `env/*.yaml` → `tenants/tenant-*.yaml`), vendored `woo-website` chart, AppProject sync-window governance. Replaces the `mcc create-react` per-tenant manifest generator. Tenant files become 2 lines (`name`, `environment`) for the common case; namespace pairs with the Nextcloud co-tenant (`-`); image tag pinned to semver in `common.yaml` (no more `latest`); branding migration sources truth from live cluster state. No code or values scaffolded yet — planning docs only. Files: `openspec/changes/bootstrap-react-platform/{.openspec.yaml,proposal.md,design.md,tasks.md}`. - **2026-05-01** — Document external dependencies in `bootstrap-react-platform`: DNS handled by `cluster-infra/external-dns` (Cloudflare, `policy: sync`, zones `commonground.nu`/`openwoo.app`/`opencatalogi.nl`) — tenant adds/removes auto-create/auto-reap DNS records. TLS via cert-manager + HTTP-01. Recorded in `design.md` (new "External dependencies" subsection), `proposal.md` (Removed from Scope), and `tasks.md` (task 7.1 will surface this in `docs/ADDING-TENANT.md`). +- **2026-05-01** — Scaffold `react-platform/` (openspec phases 2–7). Vendored `charts/woo-website/` from `woo-website-template-apiv2@b1ac4e89` (refactor branch) plus added `templates/networkpolicy.yaml` for pod-label-scoped default-deny + allow-ingress + allow-egress policies. Wrote `react-platform/values/{common,env/accept,env/prod}.yaml` and `templates/tenant-template.yaml` (image pinned to `1.0.0`, `nodeSelector: role: prod-nextcloud`, ingress className `nginx`, security context platform-grade). Wrote AppProject `react-platform` and ApplicationSet `react-tenants` (mirrors Nextcloud-base shape, namespace `-`, Application name `--reactfront`). Wrote `scripts/{validate-values,smoke-checks}.sh` (smoke-check reproduces ApplicationSet inline values in bash for offline render testing). Wrote `docs/{ADDING-TENANT,ROLLOUTS,MIGRATION}.md`, `CLAUDE.md`, and updated `README.md`. Synthetic tenant `tenant-test-mcc.yaml` added as canary placeholder; smoke-checks pass (9 resources clean). Phase 1 (cluster discovery) and 8–10 (cut-overs) are deferred to live operations. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b53e83b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a **GitOps platform** for deploying multiple isolated WOO PWA frontends (React/Gatsby) on Kubernetes using Argo CD. It is the **frontend equivalent of `Nextcloud-base`**: same multi-tenant ApplicationSet shape, same wave-based rollouts, same AppProject sync-window governance — minus everything a static frontend doesn't need (no DB, no Redis, no PgBouncer, no External Secrets, no persistent storage). + +Each WOO PWA tenant lives in the **same namespace as its Nextcloud co-tenant** (e.g. `almere-accept`, `almere-prod`). The frontend pod and the Nextcloud backend share a namespace per (org, env) tuple. + +## Common Commands + +### Local validation (before commit) +```bash +./react-platform/scripts/validate-values.sh # required-field check on tenant files +./react-platform/scripts/smoke-checks.sh # helm template + kubeconform on every tenant +``` + +### Required local tools +`helm`, `kubeconform`, `yq` (mikefarah) + +## Architecture + +### Layered Helm values (3-file merge per tenant) +ApplicationSet `react-tenants` composes values in this order: +1. `react-platform/values/common.yaml` — base config for every tenant on every cluster (image, security, nodepool, ingress class) +2. `react-platform/values/env/{accept,prod}.yaml` — environment-specific overrides (replicas, HPA, resources) +3. `react-platform/values/tenants/tenant-.yaml` — per-tenant: name, environment, optional branding + +Plus an inline `values:` block computed by the ApplicationSet template that derives: +- `global.domain` — `.openwoo.app` (prod) or `.accept.openwoo.app` (accept) +- `pwa.upstream.host` and `pwa.upstream.base` — `.commonground.nu` etc. +- TLS secret name: `--tls` +- `commonLabels` including `app.kubernetes.io/part-of: react-platform` for NetworkPolicy targeting +- All branding env vars from `tenant.branding` + +### Tenant definition pattern +Two lines is the common case: +```yaml +tenant: + name: test-mcc + environment: accept +``` + +With branding (Almere-style): +```yaml +tenant: + name: almere + environment: accept + branding: + organisationName: "Gemeente Almere" + themeClassname: almere-theme + footerHideLogo: true + jumbotronImageUrl: "https://..." + faviconUrl: "..." +``` + +### Key directories +- `charts/woo-website/` — vendored Helm chart from `woo-website-template-apiv2`. See `charts/woo-website/UPSTREAM` for the sync point and react-base divergences. +- `react-platform/argo/` — AppProject (`react-platform`) and ApplicationSet (`react-tenants`) +- `react-platform/values/` — common, env, tenants, templates +- `react-platform/scripts/` — `validate-values.sh`, `smoke-checks.sh` +- `react-platform/docs/` — `ADDING-TENANT.md`, `ROLLOUTS.md`, `MIGRATION.md` +- `openspec/changes/` — design proposals (see `bootstrap-react-platform/` for the bootstrap) + +### Namespace convention +Namespace = `-`. Paired with the Nextcloud co-tenant managed by `Nextcloud-base`. The namespace is **not** auto-created by this repo's ApplicationSet — it must already exist via Nextcloud-base before the first sync of a frontend tenant. + +### Co-tenancy with Nextcloud +The frontend pod and Nextcloud pod share a namespace per (org, env). Two consequences: +- **NetworkPolicies are pod-label scoped** (`app.kubernetes.io/part-of: react-platform`) — they don't relabel the namespace and don't interfere with Nextcloud-base's namespace-label-scoped policies. +- **Namespace label stays `nextcloud-platform`**. Don't change it; this repo's NetworkPolicies don't depend on it. + +### Sync windows (governance) +The AppProject blocks platform-level syncs during weekday office hours (07:00–17:00 Europe/Amsterdam). Operational rule is stricter: + +- **Platform changes** (`values/common.yaml`, `values/env/*.yaml`, `argo/`, `charts/`): Mon–Thu 17:00–07:00 only. Never on Fri evenings, Saturdays, or Sundays — unless mwest2020 explicitly approves. Image-tag bumps count as platform changes. +- **Tenant config additions** (`values/tenants/` only): allowed at any time. +- **Canary (wave 0)**: syncs first in every rollout; validate before allowing other waves to proceed. + +### External dependencies (not owned by this repo) +- **DNS**: `cluster-infra/external-dns` runs cluster-wide with the Cloudflare provider, `policy: sync`, watches `Ingress` resources in `commonground.nu`/`openwoo.app`/`opencatalogi.nl` zones. Tenant Ingress creation/deletion auto-creates and auto-reaps the DNS record. **Operators never touch Cloudflare to add or remove a tenant.** +- **TLS**: cert-manager via HTTP-01. Cloudflare proxy is intentionally off in `cluster-infra` so HTTP-01 challenges work. Don't enable proxy. +- **Ingress controller**: ingress-nginx in namespace `ingress-nginx` (referenced by the chart's `networkPolicy.ingressNamespace` value). + +## CI/CD pipeline + +GitHub Actions on every push/PR (`.github/workflows/validate.yaml` — TODO, task 6.3 in openspec): + +| Job | Blocking? | What it checks | +|---|---|---| +| YAML lint | Yes | yamllint, sane line lengths | +| Values validation | Yes | `validate-values.sh` | +| Helm + kubeconform | Yes | `smoke-checks.sh` | +| Secret scanning | Yes | gitleaks | + +## Vendored chart + +`charts/woo-website/` is a pinned copy of `woo-website-template-apiv2/helm/woo-website` at a specific upstream commit (see `charts/woo-website/UPSTREAM`). Divergences from upstream: + +- `templates/networkpolicy.yaml` — added by react-base (pod-label-scoped NetworkPolicies) +- `values.yaml` — added `networkPolicy.{enabled,ingressNamespace}` block + +Bumping the vendored chart is a platform-level change (sync window applies). + +## Adding a new tenant + +See `react-platform/docs/ADDING-TENANT.md`. Short version: copy template, edit name + environment, commit, push. ApplicationSet picks it up on next sync. DNS, TLS, NetworkPolicies, and namespace co-tenancy are all automatic. + +## Migration from old standalone Applications + +See `react-platform/docs/MIGRATION.md`. Per-tenant cut-over with 1–3 minutes of downtime, inside the sync window. Canary (`test-mcc-accept`) first, soak 24h, then waves. + +## Resource policy + +Static nginx — tiny baseline. `common.yaml` ships `cpu: 50m/500m`, `memory: 64Mi/256Mi`. Prod env bumps the limit ceiling slightly. No need for per-tenant resource overrides unless a specific tenant has unusual traffic. + +## What this repo deliberately does NOT have + +- No database (frontend serves only public content) +- No Redis or PgBouncer (no shared cache, no DB connections) +- No External Secrets Operator (no tenant secrets) +- No PVCs or StorageClasses (image is self-contained) +- No `create-tenant-secret.sh` script (nothing to create) +- No S3 storage layer (no user data) + +If any of those become necessary later, prefer adding them via a new openspec change rather than expanding this scaffold organically. + +## Operational gotchas + +- **`pwa.image.tag: latest` is never used.** Tag is pinned in `common.yaml`. Bumping = platform change = sync window applies. +- **Namespace must exist before first sync.** Frontend ApplicationSet does not create namespaces — Nextcloud-base does. If a frontend tenant exists without a Nextcloud co-tenant, create the namespace manually with `app.kubernetes.io/part-of: nextcloud-platform` label. +- **Don't enable Cloudflare proxy** for any zone external-dns manages — it breaks cert-manager HTTP-01. +- **NetworkPolicies select pods, not namespaces.** Don't relabel the namespace expecting policy changes to follow; they won't. diff --git a/README.md b/README.md index 654fcae..b1a311a 100644 --- a/README.md +++ b/README.md @@ -1 +1,188 @@ # React-base + +
+ +![React](https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black) +![Gatsby](https://img.shields.io/badge/Gatsby-663399?style=for-the-badge&logo=gatsby&logoColor=white) +![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=for-the-badge&logo=kubernetes&logoColor=white) +![ArgoCD](https://img.shields.io/badge/Argo%20CD-EF7B4D?style=for-the-badge&logo=argo&logoColor=white) +![Helm](https://img.shields.io/badge/Helm-0F1689?style=for-the-badge&logo=helm&logoColor=white) + +**Een GitOps-platform voor het uitrollen van meerdere WOO PWA-frontends op Kubernetes** + +[Quick Start](#-quick-start) • +[Architectuur](#-architectuur) • +[Tenant toevoegen](#-tenant-toevoegen) • +[Documentatie](#-documentatie) + +
+ +--- + +## ✨ Kenmerken + +- 🪶 **Tenant in 2 regels YAML** — alles afgeleid van naam + omgeving +- 🔄 **GitOps-first** — alle config in Git, automatische sync via Argo CD +- 🤝 **Co-tenancy met Nextcloud** — frontend deelt namespace met zijn Nextcloud-backend +- 🌐 **Automatische DNS + TLS** — external-dns + cert-manager doen alles, geen Cloudflare-actie per tenant +- 🎯 **Wave-rollouts** — canary (wave 0) eerst, dan progressive +- 🔒 **NetworkPolicies meegeleverd** — default-deny per pod, geen impact op co-tenants + +## 🏗️ Architectuur + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Kubernetes Cluster │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────┐ ┌──────────────────────────┐ │ +│ │ ns: almere-accept │ │ ns: zuiddrecht-prod │ ... │ +│ │ ┌────────────────────┐ │ │ ┌────────────────────┐ │ │ +│ │ │ Nextcloud (BE) │ │ │ │ Nextcloud (BE) │ │ │ +│ │ │ Nextcloud-base ↑ │ │ │ │ Nextcloud-base ↑ │ │ │ +│ │ └────────────────────┘ │ │ └────────────────────┘ │ │ +│ │ ┌────────────────────┐ │ │ ┌────────────────────┐ │ │ +│ │ │ WOO PWA (FE) │ │ │ │ WOO PWA (FE) │ │ │ +│ │ │ React-base ↑ │ │ │ │ React-base ↑ │ │ │ +│ │ └────────────────────┘ │ │ └────────────────────┘ │ │ +│ └──────────────────────────┘ └──────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐│ +│ │ Cluster-infra: external-dns (Cloudflare), cert-manager, ingress-nginx ││ +│ └─────────────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────┐ + │ Cloudflare │ ← auto-sync via external-dns + │ (DNS only) │ + └─────────────┘ +``` + +### Waarom dit platform? + +Tot nu toe werd elke WOO PWA uitgerold via `mcc create-react `, +een bash-script dat per tenant een ~50-regel Argo Application-manifest +genereerde met alle conventies hardcoded (domein, upstream, nodepool, TLS). +Resultaat: convention-changes raakten *elke* tenant, drift was onzichtbaar, +en "tenant toevoegen" was geen pure GitOps-flow. + +Dit platform spiegelt `Nextcloud-base`: één ApplicationSet, gelaagde +Helm-values, één wijziging in `common.yaml` raakt iedereen tegelijk via een +gepland sync window. + +## 📁 Repository structuur + +``` +react-base/ +├── charts/ +│ └── woo-website/ # Vendored Helm chart (zie UPSTREAM) +├── react-platform/ +│ ├── argo/ +│ │ ├── projects/ # AppProject met sync windows +│ │ └── applicationsets/ # Tenant-generator +│ ├── platform/ +│ │ └── policies/ # README — policies leven in chart-templates +│ ├── values/ +│ │ ├── common.yaml # Platform-defaults voor alle tenants +│ │ ├── env/ # accept.yaml, prod.yaml +│ │ ├── tenants/ # tenant-.yaml (per tenant) +│ │ └── templates/ # tenant-template.yaml +│ ├── scripts/ # validate-values.sh, smoke-checks.sh +│ └── docs/ # ADDING-TENANT, ROLLOUTS, MIGRATION +├── openspec/ +│ └── changes/ +│ └── bootstrap-react-platform/ # Design proposal voor dit platform +├── CLAUDE.md +├── CHANGELOG.md +└── README.md (dit bestand) +``` + +## 🚀 Quick Start + +### Vereisten +- Kubernetes 1.28+ +- Argo CD geïnstalleerd +- `cluster-infra` repo deployed (external-dns + cert-manager + ingress-nginx) +- DNS-zones `commonground.nu` / `openwoo.app` / `opencatalogi.nl` op Cloudflare +- Tenant-namespace bestaat al via `Nextcloud-base` + +### 1. Clone +```bash +git clone https://github.com/ConductionNL/React-base.git +cd React-base +``` + +### 2. Apply AppProject + ApplicationSet +```bash +kubectl apply -f react-platform/argo/projects/react-platform.yaml +kubectl apply -f react-platform/argo/applicationsets/react-tenants.yaml +``` + +### 3. Eerste tenant +Zie [Tenant toevoegen](#-tenant-toevoegen) hieronder. + +## 🪶 Tenant toevoegen + +```yaml +# react-platform/values/tenants/tenant-mijn-org.yaml +tenant: + name: mijn-org + environment: accept +``` + +Dat is genoeg. Commit + push + merge → ApplicationSet pakt het op, +external-dns maakt het Cloudflare-record, cert-manager regelt TLS. + +Met branding: + +```yaml +tenant: + name: almere + environment: accept + branding: + organisationName: "Gemeente Almere" + themeClassname: almere-theme + footerHideLogo: true + jumbotronImageUrl: "https://..." + faviconUrl: "data:image/png;base64,..." +``` + +Voor de volledige walkthrough en validatie-stappen: zie +[`react-platform/docs/ADDING-TENANT.md`](react-platform/docs/ADDING-TENANT.md). + +## 🌐 Hostname-conventies + +| Omgeving | Frontend hostname | Upstream API host | +|---|---|---| +| `accept` | `.accept.openwoo.app` | `.accept.commonground.nu` | +| `prod` | `.openwoo.app` | `.commonground.nu` | + +Override per tenant via `tenant.hostname` of `tenant.apiBaseUrl` (zelden nodig). + +## 📚 Documentatie + +| Document | Onderwerp | +|---|---| +| [`CLAUDE.md`](CLAUDE.md) | Architectuur en regels — primaire referentie | +| [`react-platform/docs/ADDING-TENANT.md`](react-platform/docs/ADDING-TENANT.md) | Tenant toevoegen, validatie, removal | +| [`react-platform/docs/ROLLOUTS.md`](react-platform/docs/ROLLOUTS.md) | Sync windows, image-bumps, wave-volgorde, rollback | +| [`react-platform/docs/MIGRATION.md`](react-platform/docs/MIGRATION.md) | Cut-over van losse Applications naar deze ApplicationSet | +| [`openspec/changes/bootstrap-react-platform/`](openspec/changes/bootstrap-react-platform/) | Design proposal en taken | + +## 🔧 Validatie + +```bash +./react-platform/scripts/validate-values.sh # vereiste velden + filename +./react-platform/scripts/smoke-checks.sh # helm template + kubeconform +``` + +## 📝 License + +Onder de MIT License — zie LICENSE. + +--- + +
+ Built voor de CommonGround community +
diff --git a/charts/woo-website/.helmignore b/charts/woo-website/.helmignore new file mode 100644 index 0000000..534230f --- /dev/null +++ b/charts/woo-website/.helmignore @@ -0,0 +1,25 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +/*.tgz diff --git a/charts/woo-website/Chart.yaml b/charts/woo-website/Chart.yaml new file mode 100644 index 0000000..2fe1ee2 --- /dev/null +++ b/charts/woo-website/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: woo-website +description: A Helm chart for the Skeleton Gatsby Application + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# Chart-version: bump bij elke chart-wijziging (semver). +# 1.1.0 — platform-grade hardening: securityContext, resources.limits, +# dedicated SA, expliciete probes, pinned image bases (zie pwa/Dockerfile). +version: 1.1.0 + +# AppVersion: image-versie van de PWA. Sync met de image-tag die in CI gebouwd +# en gepusht wordt. Default in values.yaml is nog `latest` (drift-bron) — wordt +# pas weg als CI semver-tags publiceert. +appVersion: "1.0.0" diff --git a/charts/woo-website/README.md b/charts/woo-website/README.md new file mode 100644 index 0000000..ee27e0b --- /dev/null +++ b/charts/woo-website/README.md @@ -0,0 +1,118 @@ +## Woo PWA Helm chart + +This chart deploys the static Woo PWA behind an Ingress and configures the NGINX proxy to route `/api` to your backend. + +### Prerequisites +- Kubernetes cluster and `kubectl` +- Helm 3 +- Ability to pull from `ghcr.io` for the image tag you choose + +### Key values +- `pwa.image.image`: Container image repository (default `ghcr.io/conductionnl/woo-website-v2`) +- `pwa.image.tag`: Image tag (e.g. `helm`, `dev`, or your version) +- `pwa.upstream.host`: Host header for upstream API +- `pwa.upstream.base`: Base URL for upstream API (the proxy will forward `/api/*` here) +- `replicaCount`: Number of replicas +- `global.domain`: Public FQDN for the PWA +- `global.tls`: Whether to add TLS section on the Ingress +- `ingress.*`: Ingress class/annotations/extra hosts and TLS entries + +### Quickstart +1) Create an overrides file, for example `values.override.yaml`: + +```yaml +pwa: + image: + image: ghcr.io/conductionnl/woo-website-v2 + tag: helm + upstream: + host: softwarecatalogus.test.commonground.nu + base: https://softwarecatalogus.test.commonground.nu/apps/opencatalogi/api + +global: + domain: woo.example.org + tls: true + +ingress: + enabled: true + className: nginx + path: / + tls: + - secretName: woo-pwa-tls + hosts: + - woo.example.org +``` + +2) Install/upgrade: + +```bash +helm upgrade --install woo-pwa ./helm/woo-website -n woo --create-namespace -f values.override.yaml +``` + +3) Uninstall: + +```bash +helm uninstall woo-pwa -n woo +``` + +### Notes +- The image is a static build of Gatsby. Any `GATSBY_*` variables must be set at image build-time. For most cases, you only need to adjust the runtime proxy via `pwa.upstream.*`. +- If you need a custom theme or other `GATSBY_*` overrides, build your own image from `pwa/Dockerfile` with the appropriate build args, push it, and set `pwa.image.*` to your image and tag. + +### Deploy with Argo CD (UI) +- Repository URL: `https://github.com/ConductionNL/woo-website-template-apiv2.git` +- Revision: `container-setup-v1` (or `main` after merge) +- Path: `helm/woo-website` + +Values (paste in the UI as Helm parameters or a values block): +```yaml +pwa: + image: + image: ghcr.io/conductionnl/woo-website-v2 + tag: helm + upstream: + host: softwarecatalogus.test.commonground.nu + base: https://softwarecatalogus.test.commonground.nu/apps/opencatalogi/api + +global: + domain: woo.example.org + tls: true + +ingress: + enabled: true + className: nginx + path: / + tls: + - secretName: woo-pwa-tls + hosts: + - woo.example.org +``` +# Deploying to a Kubernetes Cluster + +In order to install the application in your own cloud environment we support installation in [Kubernetes](https://kubernetes.io) using the supplied [helm](https://helm.sh) chart. Kubernetes is a Container Orchestration Engine that has been standardised for Dutch municipalities under the [Haven](https://haven.commonground.nl) standard, and for which Helm is the default installation method of components. + +This helm chart can be installed with the help of Kubernetes Management Tools like [Rancher](https://rancher.com). + +This helm chart can be installed by running Helm from your local machine (see instructions on how to install Helm on [helm.sh](https://helm.sh/docs/intro/install/#through-package-managers), which requires to have [kubectl](https://kubernetes.io/docs/tasks/tools/) installed). + +If you have Helm and Kubectl installed and you have configured access to your cluster (usually via a kubeconfig file) you can run the following commands to install the application. + +```cli +$ helm repo add woo-website +$ helm install woowebsite woo-website/woo-website +``` + +The application can be adapted using the following helm values + +| Value | Description | Default | +|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|-------------------| +| `global.domain` | Sets the domain for the application and its dependencies. | `opencatalogi.nl` | +| `global.tls` | If the application should request TLS certificates for itself on the domains in `global.domain`, uses a cluster issuer named `letsencrypt-prod`. | `true` | + + +For more information about the helm file for the common gateway see the [installation manual of the common gateway](https://github.com/CommonGateway/CoreBundle/blob/master/docs/features/Installation.md#haven--kubernetes). +Keep in mind that if the common gateway is installed via the subchart of this chart, all values in the manual mentioned should be prefixed by `gateway.`. + +## Changelog + +- 0.0.1: First version based upon opencatalogi helm file \ No newline at end of file diff --git a/charts/woo-website/UPSTREAM b/charts/woo-website/UPSTREAM new file mode 100644 index 0000000..7e20f10 --- /dev/null +++ b/charts/woo-website/UPSTREAM @@ -0,0 +1,16 @@ +Vendored from: https://github.com/ConductionNL/woo-website-template-apiv2 +Path: helm/woo-website +Branch: refactor/platform-grade-cleanup +Commit: b1ac4e89e95e0170a24016b323a6eb888c60d8e6 +Vendored at: 2026-05-01 + +To re-sync: copy the chart back from the upstream repo at the desired commit +and update this file. Bumping the vendored chart is a platform-level change +and falls under the AppProject sync window (see argo/projects/react-platform.yaml). + +Divergences from upstream (re-apply on every sync): + - templates/networkpolicy.yaml — added by react-base. Pod-label-scoped + NetworkPolicies (default-deny, allow-ingress + from ingress-nginx, allow-egress 443). + - values.yaml — added `networkPolicy.{enabled,ingressNamespace}` + block at the bottom for the policy toggle. diff --git a/charts/woo-website/artifacthub-repo.yaml b/charts/woo-website/artifacthub-repo.yaml new file mode 100644 index 0000000..8d22c77 --- /dev/null +++ b/charts/woo-website/artifacthub-repo.yaml @@ -0,0 +1,20 @@ +# Artifact Hub repository metadata file +# +# Some settings like the verified publisher flag or the ignored packages won't +# be applied until the next time the repository is processed. Please keep in +# mind that the repository won't be processed if it has not changed since the +# last time it was processed. Depending on the repository kind, this is checked +# in a different way. For Helm http based repositories, we consider it has +# changed if the `index.yaml` file changes. For git based repositories, it does +# when the hash of the last commit in the branch you set up changes. This does +# NOT apply to ownership claim operations, which are processed immediately. +# +repositoryID: +owners: # (optional, used to claim repository ownership) + - name: Ruben van der linde + email: ruben@conduction.nl + - name: Conduction + email: info@conduction.nl +ignore: # (optional, packages that should not be indexed by Artifact Hub) + - name: postgresql + - name: redis diff --git a/charts/woo-website/index.yaml b/charts/woo-website/index.yaml new file mode 100644 index 0000000..482c555 --- /dev/null +++ b/charts/woo-website/index.yaml @@ -0,0 +1,293 @@ +apiVersion: v1 +entries: + commonground-gateway: + - apiVersion: v2 + appVersion: "2.2" + created: "2023-08-18T11:15:18.184369292+02:00" + dependencies: + - condition: postgresql.enabled + name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 12.1.2 + - condition: redis.enabled + name: redis + repository: https://charts.bitnami.com/bitnami + version: 17.3.11 + - name: rabbitmq + repository: https://charts.bitnami.com/bitnami + version: 11.9.1 + - condition: mongodb.enabled + name: mongodb + repository: https://charts.bitnami.com/bitnami + version: 13.4.4 + - condition: gateway-ui.enabled + name: gateway-ui + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/development/helm/ + version: 0.1.7 + description: A Helm chart for Kubernetes + digest: f3367f85691a72ee2022df449e7d817e965a9597180964df8e748f507d7c4e56 + name: commonground-gateway + type: application + urls: + - charts/commonground-gateway-1.5.3.tgz + version: 1.5.3 + opencatalogi: + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:18.147552834+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/development/api/helm/ + version: 1.5.3 + description: A Helm chart for the Skeleton Gatsby Application + digest: 412d23b997426b4854b1dbea5cba3cb1efec21ef973b806df3b004f9a7ec7412 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.6.tgz + version: 1.0.6 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:18.115539464+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/development/api/helm/ + version: 1.5.2 + description: A Helm chart for the Skeleton Gatsby Application + digest: b8851cd11621d8965ef0bc660df4ec102cf69014ae280c03fdcf5a2dbece2b15 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.5.tgz + version: 1.0.5 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:18.075652994+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/development/api/helm/ + version: 1.5.1 + description: A Helm chart for the Skeleton Gatsby Application + digest: 6b748ba1a02296a6affce75c87d9072bd9e287c37b72a251c6f90fcc307b8115 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.4.tgz + version: 1.0.4 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:18.032906954+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/development/api/helm/ + version: 1.5.0 + description: A Helm chart for the Skeleton Gatsby Application + digest: 3ef6405441cebeae2c92ca898a1cb57c01cbecda42072f4ea0fbd200f0fcffa8 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.3.tgz + version: 1.0.3 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:17.992008117+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.4.4 + description: A Helm chart for the Skeleton Gatsby Application + digest: ad7ced8f7a1cdef9b9071ffb29770c991d99d25b8ba35f0818a911e93b3ed9c3 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.2.tgz + version: 1.0.2 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:17.953153691+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.4.4 + description: A Helm chart for the Skeleton Gatsby Application + digest: ccefec45f231821d4fc1ee971d64ee5dc5bc14fc38403d6ff2ea98bbc42bb9a2 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.1.tgz + version: 1.0.1 + - apiVersion: v2 + appVersion: 1.0.0 + created: "2023-08-18T11:15:17.912086361+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.4.4 + description: A Helm chart for the Skeleton Gatsby Application + digest: 1a599f26bfbd73c38498195a68313d368eca8d37cac7e73ad763a7ce521d3758 + name: opencatalogi + type: application + urls: + - opencatalogi-1.0.0.tgz + version: 1.0.0 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.864980341+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.2.8 + - alias: gatewayui + condition: gatewayui.enabled + name: skeleton-app + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/refinement-demo-branch/helm + version: 0.1.4 + description: A Helm chart for the Skeleton Gatsby Application + digest: a19e0fb9156f3c6fc1584153a200389cdaeb11a4a37930b7b51aad6cdede89b0 + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.7.tgz + version: 0.9.7 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.789998741+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.2.8 + - alias: gatewayui + condition: gatewayui.enabled + name: skeleton-app + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/refinement-demo-branch/helm + version: 0.1.3 + description: A Helm chart for the Skeleton Gatsby Application + digest: 3d905051e4c726576b2c1eb2edd829ae2c3d328a06ae4c833110aa19c7dbfe3b + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.6.tgz + version: 0.9.6 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.707953896+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.1.4 + - alias: gatewayui + name: skeleton-app + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/refinement-demo-branch/helm + version: 0.1.2 + description: A Helm chart for the Skeleton Gatsby Application + digest: 12b8dc9b2f1c90f6e094d31953a1587444d09bb065b5d43de5f843dfe1b0341b + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.5.tgz + version: 0.9.5 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.635576269+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.1.4 + - alias: gatewayui + name: skeleton-app + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/demo/helm/ + version: 0.1.1 + description: A Helm chart for the Skeleton Gatsby Application + digest: 6e45554479770f2cb738a6811a71cc24a90634b68f1301e59c3cbf4b45c1a9a2 + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.4.tgz + version: 0.9.4 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.562620021+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.1.4 + description: A Helm chart for the Skeleton Gatsby Application + digest: f2a280df5961148f0b09ab9f92eb6a7bb892715fc790f854136daad1ff44a32b + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.3.tgz + version: 0.9.3 + - apiVersion: v2 + appVersion: 0.9.0 + created: "2023-08-18T11:15:17.51792693+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 1.1.1 + description: A Helm chart for the Skeleton Gatsby Application + digest: 0f81fbba6907bef450752601a5c6d7a3e1b324b51d1d49fcc132700d33af42ef + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.2.tgz + version: 0.9.2 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2023-08-18T11:15:17.465947553+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 0.1.12 + description: A Helm chart for the Skeleton Gatsby Application + digest: 481d521f5a2a9ec4611d2ab6eac82e336341138a4607d08118d905b297319f7a + name: opencatalogi + type: application + urls: + - opencatalogi-0.9.0.tgz + version: 0.9.0 + - apiVersion: v2 + appVersion: 0.1.0 + created: "2023-08-18T11:15:17.429513799+02:00" + dependencies: + - alias: gateway + condition: gateway.enabled + name: commonground-gateway + repository: https://raw.githubusercontent.com/ConductionNL/commonground-gateway/feature-cleanup/api/helm/ + version: 0.1.12 + description: A Helm chart for the Skeleton Gatsby Application + digest: 4c7e3d9f32803f610203e6227a40ae3b938dd17adfec738bd74654aaab0b828a + name: opencatalogi + type: application + urls: + - opencatalogi-0.1.0.tgz + version: 0.1.0 +generated: "2023-08-18T11:15:17.387154802+02:00" diff --git a/charts/woo-website/templates/NOTES.txt b/charts/woo-website/templates/NOTES.txt new file mode 100644 index 0000000..4b308b7 --- /dev/null +++ b/charts/woo-website/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "api-platform.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "api-platform.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "api-platform.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api-platform.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/woo-website/templates/_helpers.tpl b/charts/woo-website/templates/_helpers.tpl new file mode 100644 index 0000000..cbc9093 --- /dev/null +++ b/charts/woo-website/templates/_helpers.tpl @@ -0,0 +1,88 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "api-platform.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "api-platform.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "api-platform.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "api-platform.labels" -}} +helm.sh/chart: {{ include "api-platform.chart" . }} +{{ include "api-platform.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Common labels PWA +*/}} +{{- define "api-platform.labelsPWA" -}} +helm.sh/chart: {{ include "api-platform.chart" . }} +{{ include "api-platform.selectorLabelsPWA" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "api-platform.selectorLabels" -}} +app.kubernetes.io/name: {{ include "api-platform.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: {{ include "api-platform.name" . }} +{{- end }} + +{{/* +Selector labels PWA +*/}} +{{- define "api-platform.selectorLabelsPWA" -}} +app.kubernetes.io/name: {{ include "api-platform.name" . }}-pwa +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: {{ include "api-platform.name" . }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "api-platform.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "api-platform.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "commonground-gateway.fullname" -}} +{{- printf "%s-%s" .Release.Name "commonground-gateway" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/charts/woo-website/templates/config-runtime.yaml b/charts/woo-website/templates/config-runtime.yaml new file mode 100644 index 0000000..441a250 --- /dev/null +++ b/charts/woo-website/templates/config-runtime.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "api-platform.fullname" . }}-runtime + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +data: + runtime.json: | + {{- if .Values.pwa.runtime }} + {{ toJson .Values.pwa.runtime | nindent 4 }} + {{- else }} + {} + {{- end }} + diff --git a/charts/woo-website/templates/deployment.yaml b/charts/woo-website/templates/deployment.yaml new file mode 100644 index 0000000..7d80210 --- /dev/null +++ b/charts/woo-website/templates/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "api-platform.fullname" . }} + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "api-platform.selectorLabelsPWA" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "api-platform.selectorLabelsPWA" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.serviceAccount.create }} + serviceAccountName: {{ include "api-platform.serviceAccountName" . }} + {{- end }} + automountServiceAccountToken: false + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + volumes: + - name: tmpfs + emptyDir: {} + - name: tmpfs2 + emptyDir: {} + + containers: + - name: {{ .Chart.Name }}-pwa + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.pwa.image.image }}:{{ .Values.pwa.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.pwa.image.pullPolicy }} + volumeMounts: + - mountPath: /var/cache/nginx/client_temp + name: tmpfs + - mountPath: /var/run + name: tmpfs2 + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: UPSTREAM_HOST + value: {{ .Values.pwa.upstream.host | quote }} + - name: UPSTREAM_BASE + value: {{ .Values.pwa.upstream.base | quote }} + {{- if .Values.pwa.env }} + {{- range $k, $v := .Values.pwa.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/woo-website/templates/hpa.yaml b/charts/woo-website/templates/hpa.yaml new file mode 100644 index 0000000..e2230fc --- /dev/null +++ b/charts/woo-website/templates/hpa.yaml @@ -0,0 +1,22 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "api-platform.fullname" . }} + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "api-platform.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/charts/woo-website/templates/ingress.yaml b/charts/woo-website/templates/ingress.yaml new file mode 100644 index 0000000..8202724 --- /dev/null +++ b/charts/woo-website/templates/ingress.yaml @@ -0,0 +1,89 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "api-platform.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "api-platform.labels" . | nindent 4 }} + {{- if or .Values.ingress.annotations .Values.global.tls }} + annotations: + {{- with .Values.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.global.tls }} + cert-manager.io/cluster-issuer: letsencrypt-prod + {{- end }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- /* Eén gemerged TLS-blok: user-provided ingress.tls én — indien global.tls — cert-manager-issued voor global.domain */}} + {{- if or .Values.ingress.tls .Values.global.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- if .Values.global.tls }} + - hosts: + - {{ .Values.global.domain | quote }} + secretName: {{ $fullName }}-frontend-tls + {{- end }} + {{- end }} + rules: + - host: {{ .Values.global.domain | quote }} + http: + paths: + - path: {{ .Values.ingress.path }}/ + {{- if and .Values.ingress.pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .Values.ingress.pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/woo-website/templates/networkpolicy.yaml b/charts/woo-website/templates/networkpolicy.yaml new file mode 100644 index 0000000..b43c341 --- /dev/null +++ b/charts/woo-website/templates/networkpolicy.yaml @@ -0,0 +1,92 @@ +{{- /* +NetworkPolicies for the WOO PWA pod. + +Pod-label scoped (selector: app.kubernetes.io/part-of=react-platform) so +they coexist with Nextcloud co-tenant policies in the same namespace +without interfering — see Decision 4 in +openspec/changes/bootstrap-react-platform/design.md. + +Three policies layered: + 1. default-deny — deny all ingress + egress for react pods + 2. allow-ingress — permit traffic from ingress-nginx + 3. allow-egress — permit DNS + HTTPS to anywhere (upstream API is + reached via public ingress, not pod-to-pod) + +Added to the vendored chart by react-base — not in upstream +woo-website-template-apiv2. See charts/woo-website/UPSTREAM. +*/}} +{{- if .Values.networkPolicy.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "api-platform.fullname" . }}-default-deny + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/part-of: react-platform + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "api-platform.fullname" . }}-allow-ingress + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/part-of: react-platform + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.networkPolicy.ingressNamespace | default "ingress-nginx" }} + ports: + - protocol: TCP + port: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "api-platform.fullname" . }}-allow-egress + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/part-of: react-platform + policyTypes: + - Egress + egress: + # DNS — cluster resolver + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # HTTPS to anywhere — upstream API hit via public ingress. + # Locking this down to commonground.nu IPs is fragile (Cloudflare-backed, + # IPs rotate). 443-only is the practical baseline. + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + # RFC1918 — block in-cluster lateral movement. + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + ports: + - protocol: TCP + port: 443 +{{- end }} diff --git a/charts/woo-website/templates/pwa-service.yaml b/charts/woo-website/templates/pwa-service.yaml new file mode 100644 index 0000000..d1f6539 --- /dev/null +++ b/charts/woo-website/templates/pwa-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "api-platform.fullname" . }} + labels: + {{- include "api-platform.labelsPWA" . | nindent 4 }} +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + {{- include "api-platform.selectorLabelsPWA" . | nindent 4 }} diff --git a/charts/woo-website/templates/secrets.yaml b/charts/woo-website/templates/secrets.yaml new file mode 100644 index 0000000..377f388 --- /dev/null +++ b/charts/woo-website/templates/secrets.yaml @@ -0,0 +1 @@ +{{- /* Placeholder removed: no secrets needed for static PWA */ -}} diff --git a/charts/woo-website/templates/serviceaccount.yaml b/charts/woo-website/templates/serviceaccount.yaml new file mode 100644 index 0000000..a0d5bff --- /dev/null +++ b/charts/woo-website/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "api-platform.serviceAccountName" . }} + labels: + {{- include "api-platform.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/woo-website/templates/tests/test-connection.yaml b/charts/woo-website/templates/tests/test-connection.yaml new file mode 100644 index 0000000..f015683 --- /dev/null +++ b/charts/woo-website/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "api-platform.fullname" . }}-test-connection" + labels: + {{- include "api-platform.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "api-platform.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/woo-website/values.yaml b/charts/woo-website/values.yaml new file mode 100644 index 0000000..3b893bd --- /dev/null +++ b/charts/woo-website/values.yaml @@ -0,0 +1,132 @@ +# Default values for woo-website chart. +# Platform-grade hardening — zie CHANGELOG voor breaking-change-overwegingen. + +pwa: + image: + # NB: oorspronkelijke key is `image.image` (niet de standaard `image.repository`). + # Behouden voor backward compat met bestaande deployments. + image: "ghcr.io/conductionnl/woo-website-v2" + pullPolicy: IfNotPresent + # `latest` is drift-bron — pin op platform-niveau (bv. in react-base + # values/common.yaml) of override per tenant. Default blijft `latest` + # zodat bestaande Application-YAMLs blijven werken tot ze migreren. + tag: "latest" + upstream: + host: "" + base: "" + # Reserved voor toekomstige runtime-config (theming via ConfigMap i.p.v. + # build-time GATSBY_* args). Op dit moment ongebruikt; zie config-runtime.yaml. + runtime: {} + # Free-form env-vars naar de pod. Gebruikt door 30-generate-runtime.sh + # (NL_DESIGN_THEME_CLASSNAME, GATSBY_FAVICON_URL, etc.). + env: {} + +replicaCount: 2 + +global: + domain: woo.commonground.nu + tls: true + +gateway: + enabled: true + # Trusted proxies: vul met internal LB-IP als achter HTTPS-only LB + # config: + # trustedProxies: [] + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Dedicated SA per release — least-privilege, geen default-SA hergebruik. + create: true + annotations: {} + name: "" + +podAnnotations: {} + +# Pod-niveau security: non-root, seccomp default profile. +# fsGroup matched runAsGroup zodat mount-permissies kloppen. +podSecurityContext: + runAsNonRoot: true + runAsUser: 101 # nginx uid in nginx:alpine + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + +# Container-niveau security: drop alle caps, geen privilege escalatie. +# readOnlyRootFilesystem staat UIT omdat 30-generate-runtime.sh runtime.json +# schrijft naar /usr/share/nginx/html (= image-fs). Future fix: entrypoint +# laten schrijven naar /var/cache/runtime + nginx-config aanpassen. +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + capabilities: + drop: + - ALL + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + path: "" + pathType: ImplementationSpecific + className: nginx + annotations: {} + hosts: + tls: [] + +# Static-site nginx → laag baseline-verbruik, room voor traffic-spikes. +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Probes — explicit i.p.v. K8s defaults. +livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 2 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + +nodeSelector: {} +tolerations: [] +affinity: {} + +# NetworkPolicies — added by react-base (not in upstream chart). See +# templates/networkpolicy.yaml. Toggle off for testing or environments +# without NetworkPolicy support. +networkPolicy: + enabled: true + # Namespace where the ingress controller lives — used by the + # allow-ingress policy. Override if your cluster runs ingress-nginx + # in a non-standard namespace. + ingressNamespace: ingress-nginx diff --git a/openspec/changes/bootstrap-react-platform/tasks.md b/openspec/changes/bootstrap-react-platform/tasks.md index 04d99a4..7c8a64c 100644 --- a/openspec/changes/bootstrap-react-platform/tasks.md +++ b/openspec/changes/bootstrap-react-platform/tasks.md @@ -19,58 +19,50 @@ Conventions are decided (see design.md "Resolved Questions"); discovery is now s ## 2. Repo skeleton — no cluster impact -- [ ] 2.1 Create directory tree under `react-base/`: - - `react-platform/{argo,platform,values,scripts,docs}/` - - `react-platform/values/{env,tenants,templates}/` - - `react-platform/platform/policies/` - - `charts/` -- [ ] 2.2 Vendor the chart: copy `woo-website-template-apiv2/helm/woo-website/` (refactor branch, current commit) to `react-base/charts/woo-website/`. Verify `helm template ./charts/woo-website` renders cleanly. Record the upstream commit hash in `charts/woo-website/UPSTREAM` so future bumps are traceable. -- [ ] 2.3 Write `react-base/CLAUDE.md`. Mirror Nextcloud-base structure (project overview, common commands, architecture, sync windows, adding-a-tenant). Strip every reference to DB / Redis / PgBouncer / ESO / S3 / RWX storage. Add a "Co-tenancy with Nextcloud" section explaining the shared-namespace model. -- [ ] 2.4 Write `react-base/README.md` (Dutch, same tone as Nextcloud-base; emphasise "2-line tenant" simplicity). -- [ ] 2.5 Initialise `react-base/CHANGELOG.md` with an entry for this scaffold (per global rule: user-owned project requires a changelog). -- [ ] 2.6 Write `react-base/.gitignore`: at minimum `charts/*.tgz`, `**/.DS_Store`, `*.secret.yaml`, `secrets/`, `env.local`, `cluster-state.yaml` if it ends up containing anything sensitive. +- [x] 2.1 Create directory tree under `react-base/`. +- [x] 2.2 Vendor the chart at upstream commit `b1ac4e89` (refactor/platform-grade-cleanup branch). UPSTREAM file records sync point + react-base divergences (added `templates/networkpolicy.yaml`, added `networkPolicy` block in `values.yaml`). +- [x] 2.3 `react-base/CLAUDE.md` written. Mirrors Nextcloud-base structure, strips DB/Redis/PgBouncer/ESO/storage, adds Co-tenancy with Nextcloud section. +- [x] 2.4 `react-base/README.md` written (Dutch, mirrors Nextcloud-base tone, emphasises 2-line tenant). +- [x] 2.5 `react-base/CHANGELOG.md` initialised with bootstrap entry. +- [x] 2.6 `react-base/.gitignore` covers all required patterns plus `cluster-state.yaml`. ## 3. Values layering — encodes platform conventions -- [ ] 3.1 `values/common.yaml`: chart version pin, image registry, `pwa.image.tag: ` (never `latest`), `nodeSelector: {role: prod-nextcloud}`, `nextcloud-only` toleration, ingress `className: nginx`, `strategy: {type: Recreate}`, security context defaults from the chart, upstream base path `/apps/opencatalogi/api`, default theme classname, default `replicaCount`. **No domain or upstream host pattern here — those are env-specific.** -- [ ] 3.2 `values/env/accept.yaml`: `domainSuffix: accept.openwoo.app`, `upstreamHostTemplate: .accept.commonground.nu`, replicas, HPA off, accept-tier resources. -- [ ] 3.3 `values/env/prod.yaml`: `domainSuffix: openwoo.app`, `upstreamHostTemplate: .commonground.nu`, replicas, HPA on, prod-tier resources. -- [ ] 3.4 `values/templates/tenant-template.yaml`: 2-line minimum (`tenant.name`, `tenant.environment`) with commented optional overrides for `branding.*`, `hostname`, `apiBaseUrl`, `wave`. -- [ ] 3.5 Generate `values/tenants/tenant-.yaml` per existing tenant by translating `cluster-state.yaml` (task 1.2): copy any `pwa.env.GATSBY_*` and `NL_DESIGN_THEME_CLASSNAME` into the tenant's `branding` map. Tenants whose live state matches the platform default produce a 2-line file. +- [x] 3.1 `values/common.yaml` written. Image pinned to `1.0.0` (TODO: verify against GHCR — task 1.3). Discovery: today's `strategy: {type: Recreate}` is dead config (chart Deployment has no `strategy` block, silently ignored). Default RollingUpdate is preserved (better for static frontend). Includes `networkPolicy.{enabled,ingressNamespace}` toggle for chart-shipped policies. Domain logic is **not** here — moved to ApplicationSet template. +- [x] 3.2 `values/env/accept.yaml` written (replicaCount: 1, autoscaling off). +- [x] 3.3 `values/env/prod.yaml` written (replicaCount: 2, HPA enabled, modest limits bump). +- [x] 3.4 `values/templates/tenant-template.yaml` written. 2-line minimum + commented `branding`, `wave`, `hostname`, `apiBaseUrl`, `env` overrides. +- [ ] 3.5 Generate per-tenant files from `cluster-state.yaml` (depends on task 1.2 — deferred to live operations). Synthetic `tenant-test-mcc.yaml` added now as canary placeholder. ## 4. Argo CD — AppProject and ApplicationSet -- [ ] 4.1 Write `argo/projects/react-platform.yaml` (AppProject). Mirror Nextcloud-base AppProject: +- [x] 4.1 Write `argo/projects/react-platform.yaml` (AppProject). Mirror Nextcloud-base AppProject: - `sourceRepos`: this repo - `destinations`: `namespace: '*-accept'` and `namespace: '*-prod'` on the single cluster server (matches Nextcloud-base destinations exactly, since namespaces are shared) - sync windows: Mon–Thu 17:00–07:00 Europe/Amsterdam — *deny* outside the window for any change touching `values/common.yaml`, `values/env/*.yaml`, `argo/`, `platform/`, `charts/`. *Allow* anytime for changes scoped to `values/tenants/`. -- [ ] 4.2 Write `argo/applicationsets/react-tenants.yaml`. Generator: git directory match on `values/tenants/tenant-*.yaml`. Template: - - `metadata.name: {{ .tenant.name }}-{{ .tenant.environment }}-reactfront` - - `spec.destination.namespace: {{ .tenant.name }}-{{ .tenant.environment }}` (paired with Nextcloud namespace — see design.md Decision 3) - - `spec.destination.server`: the single in-cluster server - - `spec.source.path: charts/woo-website` - - `spec.source.helm.parameters`: derived `global.domain`, `pwa.upstream.base`, `pwa.upstream.host` from env templates - - `spec.source.helm.valueFiles`: `[../../values/common.yaml, ../../values/env/{{ .tenant.environment }}.yaml]` plus inline `values:` for tenant branding -- [ ] 4.3 Verify with `helm template` + `kubeconform`: each scaffolded tenant renders cleanly. Diff the rendered manifest against `cluster-state.yaml` for that tenant; the only expected delta is `metadata.namespace` (old `` → new `-`). +- [x] 4.2 Write `argo/applicationsets/react-tenants.yaml`. Used multi-source pattern (chart source + values ref source) mirroring Nextcloud-base. Inline `values:` block computes hostname / upstream / TLS secret / commonLabels / branding env vars from `.tenant.environment` switch and `.tenant.branding` map. `goTemplateOptions: ["missingkey=default"]` keeps minimal tenants working. +- [x] 4.3 Verified with `helm template` + `kubeconform`: synthetic tenant `test-mcc-accept` renders 9 resources, all schema-valid. Inline-values logic verified against the ApplicationSet template (smoke-checks.sh reproduces it in bash). ## 5. Platform policies — pod-label-scoped, not namespace-label-scoped -- [ ] 5.1 `platform/policies/networkpolicy-react-default-deny.yaml`: deny all ingress and egress for pods labeled `app.kubernetes.io/part-of: react-platform`. **Selects pods, not namespaces** — leaves Nextcloud co-tenant traffic untouched. -- [ ] 5.2 `platform/policies/networkpolicy-react-allow-ingress-controller.yaml`: allow ingress from the ingress-nginx namespace to react pods. -- [ ] 5.3 `platform/policies/networkpolicy-react-allow-egress-public-api.yaml`: allow react pods egress to DNS + the public-internet routable IP for the upstream API hosts. The upstream is *not* the in-cluster Nextcloud pod — the PWA hits the public hostname via ingress, so egress is "anywhere on TCP/443". If we ever switch to in-cluster API access, this rule needs revisiting. -- [ ] 5.4 Verify on canary cut-over (task 8) that the Nextcloud pod in the same namespace shows zero connectivity regression. +**Approach changed**: rather than standalone YAMLs in `platform/policies/`, NetworkPolicies are templated **in the vendored chart** at `charts/woo-website/templates/networkpolicy.yaml`. They ship per-release and get cleaned up on uninstall. The `platform/policies/` directory carries a README explaining where the policies actually live. Recorded as a divergence from upstream in `charts/woo-website/UPSTREAM`. + +- [x] 5.1 default-deny policy templated in chart. Selects pods by `app.kubernetes.io/part-of: react-platform`. +- [x] 5.2 allow-ingress policy: from `networkPolicy.ingressNamespace` (default `ingress-nginx`) to pod port 8080. +- [x] 5.3 allow-egress policy: DNS to `kube-system` + HTTPS (443) to public IPs (RFC1918 ranges blocked to prevent in-cluster lateral movement). +- [ ] 5.4 Verify Nextcloud co-tenant unaffected — deferred to canary cut-over (task 8). ## 6. Scripts -- [ ] 6.1 `scripts/validate-values.sh`: required fields per tenant file (`tenant.name`, `tenant.environment` ∈ {accept, prod}). Mirror Nextcloud-base shape minus DB/secret checks. -- [ ] 6.2 `scripts/smoke-checks.sh`: `helm template` every tenant against the chart, pipe through `kubeconform` for schema check. No cluster connection required. -- [ ] 6.3 GitHub Actions workflow `.github/workflows/validate.yaml`: yamllint + smoke-checks + values-validate + gitleaks. Mirror Nextcloud-base validate.yaml minus DB-specific jobs. +- [x] 6.1 `scripts/validate-values.sh` written. Required fields, env enum, filename convention. +- [x] 6.2 `scripts/smoke-checks.sh` written. Reproduces ApplicationSet inline-values in bash for offline render testing. Verified: 9 resources render clean for synthetic test tenant. +- [ ] 6.3 GitHub Actions workflow — deferred (one cycle of local validation first). ## 7. Docs -- [ ] 7.1 `docs/ADDING-TENANT.md`: copy template, edit name + env, commit, push. ApplicationSet auto-detects. Include the namespace-pairing rule (frontend lands in same namespace as Nextcloud co-tenant; that namespace must already exist). State explicitly that **DNS is automatic** — `cluster-infra/external-dns` creates the Cloudflare record from the tenant's Ingress hostname; the operator does not touch Cloudflare. Same for TLS (cert-manager via HTTP-01). -- [ ] 7.2 `docs/ROLLOUTS.md`: sync windows, wave 0 → 1 → 2 → 3 procedure, image-tag bump procedure (platform change → 17:00 window), rollback (revert PR). -- [ ] 7.3 `docs/MIGRATION.md`: per-tenant cut-over runbook from "namespace ``" to "namespace `-`". Lifted from design.md Migration Plan plus concrete `kubectl` commands. +- [x] 7.1 `react-platform/docs/ADDING-TENANT.md` written (Dutch). Includes namespace-pairing rule and explicit "DNS/TLS are automatic" callout. +- [x] 7.2 `react-platform/docs/ROLLOUTS.md` written (Dutch). Sync windows, image-bump procedure, wave order, rollback. +- [x] 7.3 `react-platform/docs/MIGRATION.md` written (Dutch). Per-tenant cut-over with concrete `kubectl` commands. ## 8. Canary cut-over — single tenant, in the sync window diff --git a/react-platform/argo/applicationsets/react-tenants.yaml b/react-platform/argo/applicationsets/react-tenants.yaml new file mode 100644 index 0000000..a9551df --- /dev/null +++ b/react-platform/argo/applicationsets/react-tenants.yaml @@ -0,0 +1,157 @@ +--- +# ApplicationSet that generates one Argo CD Application per tenant file in +# react-platform/values/tenants/. +# +# Per-tenant inputs come from tenant-.yaml (just `name` + `environment` +# is enough). Hostname, upstream API host/base, TLS secret, and namespace +# are all derived here from convention + the tenant.environment switch. +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: react-tenants + namespace: argocd + annotations: + argocd.argoproj.io/compare-options: IgnoreExtraneous +spec: + goTemplate: true + goTemplateOptions: ["missingkey=default"] + + # Don't auto-delete tenant Applications when their tenant-*.yaml is removed + # (manual cleanup forces operator review). + syncPolicy: + preserveResourcesOnDeletion: true + + generators: + - git: + repoURL: "https://github.com/ConductionNL/React-base.git" + revision: HEAD + files: + - path: "react-platform/values/tenants/tenant-*.yaml" + + template: + metadata: + # Application name matches today's create_react.sh output exactly so + # cut-over per tenant is "delete old, ApplicationSet creates same name." + name: "{{ .tenant.name }}-{{ .tenant.environment }}-reactfront" + namespace: argocd + labels: + app.kubernetes.io/part-of: react-platform + react.platform/tenant: "{{ .tenant.name }}" + react.platform/environment: "{{ .tenant.environment }}" + annotations: + argocd.argoproj.io/sync-wave: "{{ default \"1\" .tenant.wave }}" + finalizers: + - resources-finalizer.argocd.argoproj.io + spec: + project: react-platform + + sources: + # Vendored woo-website chart in this repo. + - repoURL: "https://github.com/ConductionNL/React-base.git" + targetRevision: HEAD + path: charts/woo-website + helm: + releaseName: "woo-website" + valueFiles: + - "$values/react-platform/values/common.yaml" + - "$values/react-platform/values/env/{{ .tenant.environment }}.yaml" + - "$values/react-platform/values/tenants/tenant-{{ .tenant.name }}.yaml" + # Inline values: derived hostname, upstream URL, TLS secret name, + # branding env, common labels for NetworkPolicy targeting. + values: | + {{- $name := .tenant.name -}} + {{- $env := .tenant.environment -}} + {{- $domainSuffix := "" -}} + {{- $upstreamSuffix := "" -}} + {{- if eq $env "prod" -}} + {{- $domainSuffix = "openwoo.app" -}} + {{- $upstreamSuffix = "commonground.nu" -}} + {{- else -}} + {{- $domainSuffix = printf "%s.openwoo.app" $env -}} + {{- $upstreamSuffix = printf "%s.commonground.nu" $env -}} + {{- end -}} + {{- $computedHost := printf "%s.%s" $name $domainSuffix -}} + {{- $host := default $computedHost .tenant.hostname -}} + {{- $upstreamHost := printf "%s.%s" $name $upstreamSuffix -}} + {{- $upstreamBase := printf "https://%s/apps/opencatalogi/api" $upstreamHost -}} + {{- $apiBase := default $upstreamBase .tenant.apiBaseUrl -}} + {{- $branding := default (dict) .tenant.branding -}} + {{- $extraEnv := default (dict) .tenant.env -}} + commonLabels: + app.kubernetes.io/part-of: react-platform + react.platform/tenant: "{{ $name }}" + react.platform/environment: "{{ $env }}" + global: + domain: "{{ $host }}" + tls: true + pwa: + upstream: + host: "{{ $upstreamHost }}" + base: "{{ $apiBase }}" + env: + {{- with index $branding "organisationName" }} + GATSBY_ORGANISATION_NAME: "{{ . }}" + {{- end }} + {{- with index $branding "themeClassname" }} + NL_DESIGN_THEME_CLASSNAME: "{{ . }}" + {{- end }} + {{- with index $branding "jumbotronImageUrl" }} + GATSBY_JUMBOTRON_IMAGE_URL: "{{ . }}" + {{- end }} + {{- with index $branding "faviconUrl" }} + GATSBY_FAVICON_URL: "{{ . }}" + {{- end }} + {{- if index $branding "footerHideLogo" }} + GATSBY_FOOTER_HIDE_LOGO: "{{ index $branding "footerHideLogo" }}" + {{- end }} + {{- range $k, $v := $extraEnv }} + {{ $k }}: "{{ $v }}" + {{- end }} + ingress: + hosts: + - host: "{{ $host }}" + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: "{{ $name }}-{{ $env }}-tls" + hosts: + - "{{ $host }}" + + # Values repo (this repo) — anchors $values references above. + - repoURL: "https://github.com/ConductionNL/React-base.git" + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + # Paired with the Nextcloud co-tenant namespace (e.g. almere-accept). + # Namespace must already exist (created by Nextcloud-base ApplicationSet). + namespace: "{{ .tenant.name }}-{{ .tenant.environment }}" + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - PrunePropagationPolicy=foreground + - PruneLast=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + + revisionHistoryLimit: 10 + + ignoreDifferences: + - group: apps + kind: Deployment + jsonPointers: + - /spec/replicas + - group: autoscaling + kind: HorizontalPodAutoscaler + jsonPointers: + - /status diff --git a/react-platform/argo/projects/react-platform.yaml b/react-platform/argo/projects/react-platform.yaml new file mode 100644 index 0000000..40a6da1 --- /dev/null +++ b/react-platform/argo/projects/react-platform.yaml @@ -0,0 +1,99 @@ +--- +# Argo CD AppProject for the React (WOO PWA) Platform. +# RBAC and resource boundaries for every tenant managed by ApplicationSet +# react-tenants. Independent from nextcloud-platform so an outage on one +# platform cannot pause syncs on the other. +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: react-platform + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: "Multi-tenant WOO PWA (React/Gatsby) platform managed by GitOps" + + sourceRepos: + - "git@github.com:ConductionNL/React-base.git" + - "https://github.com/ConductionNL/React-base.git" + - "https://github.com/conductionnl/React-base.git" + + # Destinations match Nextcloud-base exactly (namespaces are shared with + # Nextcloud co-tenants — see openspec/changes/bootstrap-react-platform/ + # design.md Decision 3). + destinations: + - namespace: "*-accept" + server: https://kubernetes.default.svc + - namespace: "*-test" + server: https://kubernetes.default.svc + - namespace: "*-prod" + server: https://kubernetes.default.svc + + clusterResourceWhitelist: + - group: "" + kind: Namespace + + namespaceResourceBlacklist: + - group: "" + kind: ResourceQuota + - group: "" + kind: LimitRange + + namespaceResourceWhitelist: + - group: "" + kind: "*" + - group: apps + kind: "*" + - group: networking.k8s.io + kind: "*" + - group: policy + kind: "*" + - group: autoscaling + kind: "*" + - group: cert-manager.io + kind: "*" + - group: monitoring.coreos.com + kind: "*" + + # Sync windows — platform-level changes blocked during office hours. + # Same shape as nextcloud-platform: weekday daytime denied, evenings and + # weekends allowed. Tenant additions (new tenant-*.yaml) are unaffected + # because they're scoped to a single Application, not the platform meta. + # + # Operational policy (CLAUDE.md): platform changes Mon–Thu 17:00–07:00 + # only — Fri evenings and weekends require explicit operator approval. + # That's enforced socially, not technically. + syncWindows: + - kind: deny + schedule: "0 7 * * 1-5" + duration: 10h + timeZone: Europe/Amsterdam + applications: + - "react-platform" + manualSync: false + + roles: + - name: platform-admin + description: "Full access to React/WOO platform" + policies: + - p, proj:react-platform:platform-admin, applications, *, react-platform/*, allow + - p, proj:react-platform:platform-admin, repositories, *, react-platform/*, allow + groups: + - platform-admins + + - name: tenant-viewer + description: "Read-only access to tenant applications" + policies: + - p, proj:react-platform:tenant-viewer, applications, get, react-platform/*, allow + - p, proj:react-platform:tenant-viewer, applications, sync, react-platform/*, deny + groups: + - tenant-viewers + + - name: tenant-operator + description: "Sync access to tenant applications" + policies: + - p, proj:react-platform:tenant-operator, applications, get, react-platform/*, allow + - p, proj:react-platform:tenant-operator, applications, sync, react-platform/*, allow + - p, proj:react-platform:tenant-operator, applications, action/*, react-platform/*, allow + groups: + - tenant-operators diff --git a/react-platform/docs/ADDING-TENANT.md b/react-platform/docs/ADDING-TENANT.md new file mode 100644 index 0000000..031c35e --- /dev/null +++ b/react-platform/docs/ADDING-TENANT.md @@ -0,0 +1,106 @@ +# Tenant toevoegen + +Een nieuwe WOO PWA-tenant aanmelden duurt twee minuten en is pure GitOps — +geen `kubectl`, geen Cloudflare, geen TLS-handelingen. + +## Stappen + +1. Kopieer `react-platform/values/templates/tenant-template.yaml` naar + `react-platform/values/tenants/tenant-.yaml` (bestandsnaam moet + matchen met `.tenant.name`). +2. Vul minimaal in: + ```yaml + tenant: + name: + environment: accept # of: prod + ``` +3. (Optioneel) Voeg branding-overrides toe — zie de template voor de + beschikbare velden. +4. Commit + push. Open een PR. +5. Na merge pakt de ApplicationSet `react-tenants` het bestand automatisch + op en maakt een Argo CD Application aan (`--reactfront`). + +## Wat gebeurt er automatisch? + +| Stap | Door wie | +|---|---| +| Argo Application aanmaken | ApplicationSet `react-tenants` | +| Namespace gebruiken | Bestaande co-tenant namespace `-` (Nextcloud-base) | +| Hostname genereren | ApplicationSet template — `.openwoo.app` (prod) of `.accept.openwoo.app` (accept) | +| DNS-record op Cloudflare | `cluster-infra/external-dns` (auto-create) | +| TLS-certificaat | cert-manager via HTTP-01 | +| NetworkPolicies | Vendored chart `templates/networkpolicy.yaml` | +| Pod-placement | `nodeSelector: role: prod-nextcloud` (zie `values/common.yaml`) | + +**Geen actie nodig op Cloudflare**, geen TLS-secret aanmaken, geen DNS-edits. +External-DNS reageert op de Ingress die de chart deployt. + +## Voorwaarde: namespace bestaat al + +De frontend-pod landt in dezelfde namespace als de Nextcloud-tenant +(bijvoorbeeld `almere-accept` of `almere-prod`). Die namespace wordt +beheerd door `Nextcloud-base` en moet er zijn vóór de eerste sync van +deze frontend. + +Verifieer met: +```bash +kubectl get ns - +``` + +Als de namespace ontbreekt: er is geen Nextcloud co-tenant in deze +omgeving. Maak die eerst aan via `Nextcloud-base` (`values/tenants/` +add tenant), of maak de namespace handmatig met label +`app.kubernetes.io/part-of: nextcloud-platform`. + +## Voorbeelden + +### Minimale tenant (geen branding) +```yaml +tenant: + name: test-mcc + environment: accept +``` + +### Tenant met branding (Almere-stijl) +```yaml +tenant: + name: almere + environment: accept + branding: + organisationName: "Gemeente Almere" + themeClassname: almere-theme + footerHideLogo: true + jumbotronImageUrl: "https://..." + faviconUrl: "data:image/png;base64,..." +``` + +### Tenant met afwijkende hostname +```yaml +tenant: + name: speciaal + environment: prod + hostname: woo.speciaal.gemeente.nl # override van convention +``` + +## Validatie vóór commit + +Voer beide scripts uit voor je een PR opent: + +```bash +./react-platform/scripts/validate-values.sh +./react-platform/scripts/smoke-checks.sh +``` + +`validate-values.sh` controleert vereiste velden en filename-conventie. +`smoke-checks.sh` rendert de chart met je tenant-config en valideert +het resultaat tegen Kubernetes-schema's. + +## Verwijderen + +1. Verwijder `react-platform/values/tenants/tenant-.yaml`. +2. Commit + push + merge. +3. Verwijder de bijbehorende Application **handmatig** in Argo CD — + `preserveResourcesOnDeletion: true` in de ApplicationSet voorkomt + per ongeluk verwijderen. Met cascade verdwijnt de Deployment, Service, + Ingress, NetworkPolicies, en cert-manager Cert. external-DNS reapt + het Cloudflare-record. diff --git a/react-platform/docs/MIGRATION.md b/react-platform/docs/MIGRATION.md new file mode 100644 index 0000000..2fddf35 --- /dev/null +++ b/react-platform/docs/MIGRATION.md @@ -0,0 +1,127 @@ +# Migratie: van losse Applications naar ApplicationSet + +Eenmalige cut-over per tenant. Verplaatst de WOO PWA-pod van namespace +`` naar namespace `-` (paired met de Nextcloud +co-tenant). Alleen voor tenants die vandaag draaien onder de oude +`mcc create-react`-flow; nieuwe tenants slaan dit hele proces over. + +## Wat verandert er? + +| Aspect | Vandaag | Na migratie | +|---|---|---| +| Argo Application-bron | Losstaand manifest in `toolchain/` | ApplicationSet `react-tenants` | +| Application-naam | `--reactfront` | `--reactfront` (gelijk) | +| Namespace | `` | `-` | +| Hostname | Identiek | Identiek | +| Image-tag | `latest` | Pinned semver in `common.yaml` | +| TLS-secret | `woo-pwa-tls` (in oude ns) | `--tls` (in nieuwe ns) | +| DNS | external-dns auto | external-dns auto | +| Branding | Inline in losse manifest | `tenant-.yaml` | + +## Voorbereiding (eenmalig) + +1. **Snapshot live cluster state** — vereist voor branding-migratie. + ```bash + kubectl get applications -n argocd -o yaml \ + | yq '.items[] | select(.metadata.name | test("-reactfront$")) + | {name: .metadata.name, + ns_today: .spec.destination.namespace, + parameters: .spec.source.helm.parameters, + values: .spec.source.helm.values}' \ + > openspec/changes/bootstrap-react-platform/cluster-state.yaml + ``` + Dit bestand staat in `.gitignore` — niet commiten. + +2. **Per tenant**: maak `react-platform/values/tenants/tenant-.yaml`. + Zet `tenant.name` + `tenant.environment`. Kopieer alle `pwa.env.GATSBY_*` + en `NL_DESIGN_THEME_CLASSNAME` uit `cluster-state.yaml` naar de + `branding`-map. Tenants zonder branding zijn 2 regels. + +3. **Verifieer renders**: + ```bash + ./react-platform/scripts/validate-values.sh + ./react-platform/scripts/smoke-checks.sh + ``` + +4. **Nextcloud-namespace check**: voor elke tenant, controleer dat + `-` bestaat als Nextcloud namespace. Als niet: + blocker — er is geen Nextcloud co-tenant en de pairing-aanname + geldt niet voor deze tenant. + +## Cut-over per tenant + +Uitvoeren binnen 17:00–07:00 sync window. Begin met canary +(`test-mcc-accept`). + +1. **Pre-state vastleggen**: + ```bash + kubectl get all,ingress,cert -n > /tmp/pre-old.txt + kubectl get all,ingress,cert -n - > /tmp/pre-new.txt + ``` + +2. **Diff render vs live**: vergelijk de `helm template`-output (run + smoke-checks met dit tenant) met de live Application's resolved + manifest. Resolve alle inhoudelijke verschillen — alleen + `metadata.namespace` mag verschillen. + +3. **Stop oude Application** (default cascade — verwijdert workload): + ```bash + kubectl delete application -n argocd --reactfront + ``` + Wacht tot Deployment, Service, Ingress weg zijn uit ``. Hostname + is nu kortdurend onbereikbaar. + +4. **Sync ApplicationSet** — Argo maakt de nieuwe Application met dezelfde + naam, target namespace `-`. Workload wordt vers gemaakt. + +5. **Wacht op TLS**: + ```bash + kubectl wait -n - --for=condition=Ready cert/--tls --timeout=5m + ``` + Als dit timeoutet: oude Ingress blokkeert mogelijk nog de hostname. + Verifieer met `kubectl get ingress -A | grep `. + +6. **Verifieer bereikbaarheid**: + ```bash + curl -I https://.[accept.]openwoo.app + openssl s_client -connect :443 -servername < /dev/null \ + | openssl x509 -noout -dates + ``` + Expect: 200 status, geldige TLS-cert. + +7. **Verifieer branding** (alleen voor tenants met overrides): + ```bash + curl -s https:// | grep -E '|' + ``` + +8. **Verifieer Nextcloud co-tenant onaangetast**: + ```bash + kubectl get pods -n - + ``` + Restart counts van Nextcloud-pods moeten gelijk zijn aan pre-state. + +9. **Soak**: ~24 uur draaien voor de volgende cut-over. + +**Totale downtime per tenant**: 1–3 minuten (delete → recreate → TLS-issuance). + +## Rollback per tenant + +1. Re-apply het oude manifest: + ```bash + kubectl apply -f toolchain/woo-application--.yaml + ``` +2. Verwijder `react-platform/values/tenants/tenant-.yaml` uit Git + (PR-revert). De ApplicationSet stopt met deze tenant te beheren. +3. cert-manager re-issued in de oude namespace zodra de oude Ingress + weer staat. + +## Volgorde + +1. Canary: `test-mcc-accept`. Soak 24h. +2. Wave 1: tenants zonder branding (2-regel tenant-bestanden). +3. Wave 2: tenants met branding (Almere, etc.). Extra check op + theme/org-name na cut-over. +4. Wave 3: edge cases. + +Image-tag wordt **niet** gebumpt tijdens de migratie — dat is een +aparte platform-change die na alle cut-overs gepland kan worden. diff --git a/react-platform/docs/ROLLOUTS.md b/react-platform/docs/ROLLOUTS.md new file mode 100644 index 0000000..773960b --- /dev/null +++ b/react-platform/docs/ROLLOUTS.md @@ -0,0 +1,81 @@ +# Rollouts + +## Sync windows + +Argo CD AppProject `react-platform` blokkeert platform-syncs tijdens +kantooruren. Schema (Europe/Amsterdam, automatisch CET/CEST): + +| Periode | Mag synced worden | +|---|---| +| Ma–Vr 07:00–17:00 | **Nee** (alleen platform `react-platform` Application) | +| Ma–Vr 17:00–07:00 | Ja | +| Weekend | Technisch ja, **operationeel: alleen na expliciete OK** | + +Operationele regel (uit `CLAUDE.md`): platform-wijzigingen alleen +**Ma–Do 17:00–07:00**. Vrijdagavond en weekenden alleen met goedkeuring +van mwest2020. Sociaal afgedwongen, niet technisch — de AppProject +blokkeert weekends niet zelf. + +Tenant-specifieke wijzigingen (alleen in `values/tenants/`) zijn altijd +toegestaan: ze raken één Application, niet de hele platform-meta. + +## Wat telt als platform-wijziging? + +- `values/common.yaml` — image-tag, security defaults, nodepool, ingress class +- `values/env/*.yaml` — replicas, HPA, resources per omgeving +- `argo/` — AppProject, ApplicationSet +- `charts/` — vendored chart bumps +- `react-platform/platform/` — netwerk policies (in chart) of platform-componenten + +## Wat is een tenant-wijziging? + +- Nieuwe `values/tenants/tenant-*.yaml` toevoegen +- Branding aanpassen in een bestaand tenant-bestand +- Tenant verwijderen + +## Image-tag bumpen + +Image-tags zijn pinned in `values/common.yaml`. Een bump werkt door op +**alle** tenants in **alle** omgevingen tegelijkertijd via Argo's auto-sync. + +Procedure: +1. Update `pwa.image.tag` in `values/common.yaml` (PR). +2. Wacht op een 17:00 sync window. +3. Argo synct alle Applications. Wave 0 (canary) eerst, dan wave 1+. +4. Verifieer canary werkt voordat wave 1 doorrolt. + +Rolling update: chart-default is RollingUpdate (geen `strategy: Recreate` +zoals in de oude losse manifests — die directive werd door de chart +genegeerd, geen functional change). Kortdurend overlap van oude + nieuwe +pod tijdens elke tenant-update. + +## Wave-volgorde + +Tenants kunnen `tenant.wave` zetten (default `"1"`). De ApplicationSet +zet `argocd.argoproj.io/sync-wave: ""` als annotation, Argo +respecteert dat tijdens sync. + +| Wave | Bedoeld voor | +|---|---| +| 0 | Canary tenant — kleine groep, valideren vóór de rest | +| 1 | Standaard tenants | +| 2+ | Late waves (specifiek branding, edge cases) | + +## Rollback + +Plat: revert de PR + sync. + +Image-tag rollback: +1. Revert de `values/common.yaml` commit. +2. Argo detecteert drift en sync't terug naar de vorige tag (mits binnen + sync window — anders manueel sync via Argo UI met operator-rol). + +Tenant-config rollback: +1. Revert tenant-bestand. +2. Direct sync (tenant changes hebben geen window-restrictie). + +## Soak-eis + +Na een platform-bump (image of chart): minimaal **24 uur** soaken op +canary (wave 0) voordat wave 1+ wordt vrijgegeven. Geen automatische +gate; operator-discipline. diff --git a/react-platform/platform/policies/README.md b/react-platform/platform/policies/README.md new file mode 100644 index 0000000..dc9018c --- /dev/null +++ b/react-platform/platform/policies/README.md @@ -0,0 +1,35 @@ +# NetworkPolicies + +NetworkPolicies for the WOO PWA pods are templated **inside the vendored +chart** at `charts/woo-website/templates/networkpolicy.yaml`. They ship +per-release and are cleaned up automatically when a tenant Application is +deleted. + +This directory is intentionally near-empty: separate platform-policy +manifests would only fit if the policies needed to apply to namespaces +without a tenant Application — which is not the case here. WOO frontends +always come with a release; the policies travel with them. + +## Why pod-label scoped? + +Tenant namespaces are shared with Nextcloud co-tenants (see +`openspec/changes/bootstrap-react-platform/design.md` Decision 4). The +react NetworkPolicies select pods by `app.kubernetes.io/part-of: react-platform` +so they don't interfere with Nextcloud's own namespace-label-scoped policies. + +## What they do + +| Policy | Direction | Allowed | +|---|---|---| +| `default-deny` | both | nothing — baseline lockdown | +| `allow-ingress` | ingress | from `ingress-nginx` ns to pod port 8080 | +| `allow-egress` | egress | DNS to `kube-system`; HTTPS (443) to public IPs only (RFC1918 ranges blocked to prevent in-cluster lateral movement) | + +## Toggling off + +For testing or clusters without NetworkPolicy enforcement, set in tenant or env values: + +```yaml +networkPolicy: + enabled: false +``` diff --git a/react-platform/scripts/smoke-checks.sh b/react-platform/scripts/smoke-checks.sh new file mode 100755 index 0000000..198732d --- /dev/null +++ b/react-platform/scripts/smoke-checks.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# smoke-checks.sh — render every tenant against the vendored chart and +# pipe through kubeconform for K8s schema validation. No cluster required. +# +# Run from repo root: +# ./react-platform/scripts/smoke-checks.sh +# +# This script reproduces the ApplicationSet's inline `values:` block in +# bash so the rendered manifest matches what Argo CD would actually +# produce (hostname, upstream URL, TLS secret name, branding env vars). +# Keep in sync with react-platform/argo/applicationsets/react-tenants.yaml. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHART="${REPO_ROOT}/charts/woo-website" +COMMON="${REPO_ROOT}/react-platform/values/common.yaml" +TENANTS_DIR="${REPO_ROOT}/react-platform/values/tenants" +ENV_DIR="${REPO_ROOT}/react-platform/values/env" + +for tool in helm kubeconform yq; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "[ERROR] required tool not installed: $tool" + exit 2 + fi +done + +# Render the ApplicationSet inline-values for one tenant. Mirrors the +# template in react-tenants.yaml — keep both sides in sync. +render_inline_values() { + local f="$1" + local name env host upstream_host upstream_base + name="$(yq -r '.tenant.name' "$f")" + env="$(yq -r '.tenant.environment' "$f")" + + if [[ "$env" == "prod" ]]; then + host="${name}.openwoo.app" + upstream_host="${name}.commonground.nu" + else + host="${name}.${env}.openwoo.app" + upstream_host="${name}.${env}.commonground.nu" + fi + upstream_base="https://${upstream_host}/apps/opencatalogi/api" + + # Optional overrides + local override_host override_api + override_host="$(yq -r '.tenant.hostname // ""' "$f")" + override_api="$(yq -r '.tenant.apiBaseUrl // ""' "$f")" + [[ -n "$override_host" ]] && host="$override_host" + [[ -n "$override_api" ]] && upstream_base="$override_api" + + cat </dev/null + echo "[OK] chart renders cleanly" + exit 0 +fi + +fail=0 +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +for f in "${files[@]}"; do + base="$(basename "$f")" + name="$(yq -r '.tenant.name // ""' "$f")" + env="$(yq -r '.tenant.environment // ""' "$f")" + env_file="${ENV_DIR}/${env}.yaml" + + if [[ -z "$name" || -z "$env" ]]; then + echo "[SKIP] $base: missing tenant.name or tenant.environment (run validate-values.sh)" + fail=1 + continue + fi + + if [[ ! -f "$env_file" ]]; then + echo "[FAIL] $base: env file not found: $env_file" + fail=1 + continue + fi + + inline="${tmpdir}/inline-${name}-${env}.yaml" + render_inline_values "$f" > "$inline" + + echo "→ ${name} (${env})" + if ! helm template "${name}-${env}" "$CHART" \ + -f "$COMMON" \ + -f "$env_file" \ + -f "$f" \ + -f "$inline" 2>/tmp/helm.err \ + | kubeconform -strict -summary -ignore-missing-schemas - 2>/tmp/kc.err; then + echo "[FAIL] $base" + [[ -s /tmp/helm.err ]] && cat /tmp/helm.err + [[ -s /tmp/kc.err ]] && cat /tmp/kc.err + fail=1 + fi +done + +if [[ $fail -ne 0 ]]; then + echo + echo "[FAIL] smoke checks failed" + exit 1 +fi + +echo +echo "[OK] all ${#files[@]} tenant render(s) clean" diff --git a/react-platform/scripts/validate-values.sh b/react-platform/scripts/validate-values.sh new file mode 100755 index 0000000..1a6808b --- /dev/null +++ b/react-platform/scripts/validate-values.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# validate-values.sh — required-field check for tenant YAML files. +# +# Run from repo root: +# ./react-platform/scripts/validate-values.sh +# +# Exit 0 = all tenants valid. Non-zero = something failed; per-file +# failures are printed before exit. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TENANTS_DIR="${REPO_ROOT}/react-platform/values/tenants" + +if ! command -v yq >/dev/null 2>&1; then + echo "[ERROR] yq is required but not installed (https://github.com/mikefarah/yq)" + exit 2 +fi + +if [[ ! -d "${TENANTS_DIR}" ]]; then + echo "[ERROR] tenants dir not found: ${TENANTS_DIR}" + exit 2 +fi + +shopt -s nullglob +files=("${TENANTS_DIR}"/tenant-*.yaml) +shopt -u nullglob + +if [[ ${#files[@]} -eq 0 ]]; then + echo "[OK] no tenant files yet — nothing to validate" + exit 0 +fi + +fail=0 +for f in "${files[@]}"; do + base="$(basename "$f")" + + name="$(yq -r '.tenant.name // ""' "$f")" + env="$(yq -r '.tenant.environment // ""' "$f")" + + if [[ -z "$name" ]]; then + echo "[FAIL] $base: missing required field .tenant.name" + fail=1 + elif [[ ! "$name" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then + echo "[FAIL] $base: .tenant.name '$name' must match [a-z0-9]([a-z0-9-]*[a-z0-9])?" + fail=1 + fi + + if [[ -z "$env" ]]; then + echo "[FAIL] $base: missing required field .tenant.environment" + fail=1 + elif [[ "$env" != "accept" && "$env" != "prod" ]]; then + echo "[FAIL] $base: .tenant.environment must be 'accept' or 'prod' (got '$env')" + fail=1 + fi + + # Convention check: filename should match tenant.name. + expected="tenant-${name}.yaml" + if [[ -n "$name" && "$base" != "$expected" ]]; then + echo "[FAIL] $base: filename should be '$expected' to match .tenant.name" + fail=1 + fi + + if [[ $fail -eq 0 ]]; then + echo "[OK] $base ($name / $env)" + fi +done + +if [[ $fail -ne 0 ]]; then + echo + echo "[FAIL] validation failed" + exit 1 +fi + +echo +echo "[OK] all ${#files[@]} tenant file(s) valid" diff --git a/react-platform/values/common.yaml b/react-platform/values/common.yaml new file mode 100644 index 0000000..982e7bf --- /dev/null +++ b/react-platform/values/common.yaml @@ -0,0 +1,127 @@ +--- +# common.yaml — base config for every WOO PWA tenant in every environment. +# Tenant files extend this; environment files (env/*.yaml) override env-specific +# bits. Anything in here applies to all tenants on every cluster. +# +# Convention: never put hostname or upstream-host patterns in here — those are +# environment-specific (env/*.yaml) or computed by the ApplicationSet template. + +# ---------- Image ---------- +# Image tag is pinned at the platform level (NEVER `latest`). Bumping the tag +# is a platform-wide change and falls under the AppProject sync window +# (see argo/projects/react-platform.yaml). +# +# Source of truth: Chart.yaml `appVersion` should match this tag. If they +# diverge, the explicit value here wins (chart's `default .Chart.AppVersion` +# fallback only applies when this is empty). +# +# TODO before first sync: verify "1.0.0" exists in +# ghcr.io/conductionnl/woo-website-v2 (task 1.3 in openspec/changes/ +# bootstrap-react-platform/tasks.md). +pwa: + image: + image: ghcr.io/conductionnl/woo-website-v2 + pullPolicy: IfNotPresent + tag: "1.0.0" + +# ---------- Pod placement ---------- +# Frontends co-locate on the Nextcloud nodepool (status quo from +# create_react.sh). A future openspec change can move them off; until then +# this is platform default. +nodeSelector: + role: prod-nextcloud + +tolerations: + - key: nextcloud-only + operator: Equal + value: "true" + effect: NoSchedule + +# ---------- Service ---------- +# Chart Service maps to container port 8080 (nginx in pwa/Dockerfile). +service: + type: ClusterIP + port: 80 + +# ---------- Ingress ---------- +# Ingress class is cluster-wide (ingress-nginx). Hostname and TLS secret are +# computed per-tenant by the ApplicationSet template. +# DNS records are auto-managed by cluster-infra/external-dns based on the +# Ingress hostname — no Cloudflare action when adding/removing tenants. +ingress: + enabled: true + className: nginx + pathType: ImplementationSpecific + +# ---------- Gateway ---------- +# Chart-internal flag. Keep on (matches today's manifests). +gateway: + enabled: true + +# ---------- Service account ---------- +# Dedicated SA per release (least-privilege, no default-SA hijack). +serviceAccount: + create: true + +# ---------- Security ---------- +# Pod-level: non-root, seccomp default, fsGroup matches runAsGroup so mount +# permissions line up. Values match the chart's platform-grade defaults but +# are pinned here so a chart-default change doesn't silently relax security. +podSecurityContext: + runAsNonRoot: true + runAsUser: 101 # nginx UID in nginx:alpine + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + +# Container-level: drop all caps, no privilege escalation. readOnlyRootFilesystem +# stays off until 30-generate-runtime.sh writes to a tmpfs instead of image-fs +# (see chart pwa/Dockerfile). +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + capabilities: + drop: + - ALL + +# ---------- Probes ---------- +# Pinned (chart defaults are sane for static nginx). +livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 2 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + +# ---------- Resources (platform default) ---------- +# Static nginx → tiny baseline. env/*.yaml can override for prod-tier. +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + +# ---------- NetworkPolicies ---------- +# Policies live in the vendored chart at templates/networkpolicy.yaml. +# Pod-label scoped (`app.kubernetes.io/part-of: react-platform`) to coexist +# with Nextcloud co-tenant policies in the same namespace. +networkPolicy: + enabled: true + ingressNamespace: ingress-nginx diff --git a/react-platform/values/env/accept.yaml b/react-platform/values/env/accept.yaml new file mode 100644 index 0000000..2eb7d5f --- /dev/null +++ b/react-platform/values/env/accept.yaml @@ -0,0 +1,12 @@ +--- +# env/accept.yaml — overrides for the "accept" environment. +# Hostname/upstream pattern is computed by the ApplicationSet template +# from tenant.environment, not from this file. + +replicaCount: 1 + +autoscaling: + enabled: false + +# Accept-tier resources: same as common.yaml defaults. +# Override here only if accept needs to differ from prod-tier baseline. diff --git a/react-platform/values/env/prod.yaml b/react-platform/values/env/prod.yaml new file mode 100644 index 0000000..79ab326 --- /dev/null +++ b/react-platform/values/env/prod.yaml @@ -0,0 +1,22 @@ +--- +# env/prod.yaml — overrides for the "prod" environment. +# Hostname/upstream pattern is computed by the ApplicationSet template +# from tenant.environment, not from this file. + +replicaCount: 2 + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + +# Prod-tier resources: small bump on limits to absorb traffic spikes. +# Static nginx → still tiny. +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 1000m + memory: 512Mi diff --git a/react-platform/values/templates/tenant-template.yaml b/react-platform/values/templates/tenant-template.yaml new file mode 100644 index 0000000..6e62e10 --- /dev/null +++ b/react-platform/values/templates/tenant-template.yaml @@ -0,0 +1,53 @@ +--- +# Per-tenant config — copy this file to ../tenants/tenant-.yaml, +# edit the two required fields, commit, push. ApplicationSet picks it up +# automatically. +# +# Required: +# tenant.name — short slug (e.g. "almere", "test-mcc"). Lowercase, +# digits, hyphens. No environment suffix. +# tenant.environment — "accept" or "prod". +# +# Everything else is derived: +# • Argo Application name = "--reactfront" +# • Namespace = "-" (paired with Nextcloud +# co-tenant — namespace must already exist) +# • Hostname = .openwoo.app (prod) +# .accept.openwoo.app (accept) +# • Upstream API host = .commonground.nu (prod) +# .accept.commonground.nu (accept) +# • Upstream base URL = https:///apps/opencatalogi/api +# • TLS cert = issued by cert-manager (HTTP-01) +# • DNS record = managed by cluster-infra/external-dns +# +# Set optional overrides only if your tenant differs from the defaults. + +tenant: + name: example + environment: accept + + # ---------- Optional overrides ---------- + + # Sync wave — controls rollout order (0 = canary, 1+ = waves). + # Default: "1" + # wave: "0" + + # Override the computed hostname (rare — only if tenant uses a non-standard domain). + # hostname: example.alt-domain.org + + # Override the computed upstream API URL (rare — only if backend lives elsewhere). + # apiBaseUrl: https://api.example.org/apps/opencatalogi/api + + # Per-tenant branding. All keys are optional. Anything you set lands in + # the pod's environment as the named GATSBY_*/NL_DESIGN_* variable. + branding: + organisationName: "" # GATSBY_ORGANISATION_NAME — visible in header + themeClassname: "" # NL_DESIGN_THEME_CLASSNAME — e.g. "almere-theme" + jumbotronImageUrl: "" # GATSBY_JUMBOTRON_IMAGE_URL + faviconUrl: "" # GATSBY_FAVICON_URL — http(s) URL or data:image/... + footerHideLogo: false # GATSBY_FOOTER_HIDE_LOGO + + # Extra free-form environment variables passed straight to the pod. + # Use this for anything not covered by `branding` above. + # env: + # GATSBY_SOMETHING_ELSE: "value" diff --git a/react-platform/values/tenants/tenant-test-mcc.yaml b/react-platform/values/tenants/tenant-test-mcc.yaml new file mode 100644 index 0000000..9d40d24 --- /dev/null +++ b/react-platform/values/tenants/tenant-test-mcc.yaml @@ -0,0 +1,6 @@ +--- +# Synthetic test tenant — minimal 2-line case (no branding overrides). +# Will be replaced by real per-tenant values during canary cut-over. +tenant: + name: test-mcc + environment: accept From 5697e524d60c513ac14bf0b2a8208fb6866c4879 Mon Sep 17 00:00:00 2001 From: "mwesterweel@hotmail.com" Date: Fri, 1 May 2026 15:33:58 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20canary=20tenant=20=E2=80=94=20first?= =?UTF-8?q?=20live=20test=20(canary.openwoo.app)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tenant file for the canary-prod react-base test. Wave 0. Triggers a deploy in the canary-prod namespace alongside the existing Nextcloud co-tenant. Replaces the synthetic test-mcc placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) --- react-platform/values/tenants/tenant-canary.yaml | 7 +++++++ react-platform/values/tenants/tenant-test-mcc.yaml | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 react-platform/values/tenants/tenant-canary.yaml delete mode 100644 react-platform/values/tenants/tenant-test-mcc.yaml diff --git a/react-platform/values/tenants/tenant-canary.yaml b/react-platform/values/tenants/tenant-canary.yaml new file mode 100644 index 0000000..6775faa --- /dev/null +++ b/react-platform/values/tenants/tenant-canary.yaml @@ -0,0 +1,7 @@ +--- +# Canary tenant — first live test of the react-base platform. +# Wave 0 so it syncs before any other tenant in a platform-wide rollout. +tenant: + name: canary + environment: prod + wave: "0" diff --git a/react-platform/values/tenants/tenant-test-mcc.yaml b/react-platform/values/tenants/tenant-test-mcc.yaml deleted file mode 100644 index 9d40d24..0000000 --- a/react-platform/values/tenants/tenant-test-mcc.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Synthetic test tenant — minimal 2-line case (no branding overrides). -# Will be replaced by real per-tenant values during canary cut-over. -tenant: - name: test-mcc - environment: accept From ffccb3a0aa26890019391367947dbdc7f46edccc Mon Sep 17 00:00:00 2001 From: "mwesterweel@hotmail.com" Date: Fri, 1 May 2026 16:23:55 +0200 Subject: [PATCH 5/5] feat: app-of-apps bootstrap + canary image override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Argo CD ziet React-base nog niet — repo is niet geregistreerd. Deze commit introduceert de bootstrap-shape zoals Nextcloud-base ook werkt: één eenmalig handmatig kubectl apply, daarna alles GitOps. Wijzigingen: - react-platform/argo/applications/root.yaml: root App-of-Apps Application die react-platform/argo/ recursief watcht. Sync't AppProject + ApplicationSet (en zichzelf, self-management). - react-platform/argo/projects/react-platform.yaml: sync-wave -1 zodat de AppProject vóór de ApplicationSet syncthet. - react-platform/argo/applicationsets/react-tenants.yaml: tijdelijk targetRevision development (TODO post-canary terug naar HEAD na merge naar main). Drie locaties. - react-platform/values/tenants/tenant-canary.yaml: pin pwa.image.tag expliciet op latest — common.yaml pin "1.0.0" bestaat (waarschijnlijk) niet in GHCR; alle bestaande tenants draaien :latest. - react-platform/docs/BOOTSTRAP.md: eenmalige bootstrap-stappen (argocd repo add + kubectl apply -f root.yaml) plus rollback en troubleshooting. Bootstrap-volgorde: 1. argocd repo add https://github.com/ConductionNL/React-base.git 2. kubectl apply -f react-platform/argo/applications/root.yaml 3. Argo sync't de rest, ApplicationSet pakt tenant-canary.yaml op, deploy't naar canary-prod ns naast Nextcloud co-tenant. Co-Authored-By: Claude Opus 4.7 (1M context) --- react-platform/argo/applications/root.yaml | 51 +++++++++ .../argo/applicationsets/react-tenants.yaml | 11 +- .../argo/projects/react-platform.yaml | 4 + react-platform/docs/BOOTSTRAP.md | 108 ++++++++++++++++++ .../values/tenants/tenant-canary.yaml | 8 ++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 react-platform/argo/applications/root.yaml create mode 100644 react-platform/docs/BOOTSTRAP.md diff --git a/react-platform/argo/applications/root.yaml b/react-platform/argo/applications/root.yaml new file mode 100644 index 0000000..119da19 --- /dev/null +++ b/react-platform/argo/applications/root.yaml @@ -0,0 +1,51 @@ +--- +# Root App-of-Apps Application for the react-platform. +# +# Eenmalig handmatig deployen (zie ../../docs/BOOTSTRAP.md). Daarna beheert +# Argo zichzelf: AppProject, ApplicationSet, en — recursief — deze root +# Application zelf, allemaal vanuit react-platform/argo/. +# +# Tenant-Applications worden niet door deze root gemaakt — daarvoor staat +# de ApplicationSet `react-tenants` in de loop, die op tenant-*.yaml watcht. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: react-platform + namespace: argocd + labels: + app.kubernetes.io/part-of: react-platform + react.platform/component: root + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + # Project default: bootstrap kan niet afhangen van de AppProject die + # hijzelf gaat aanmaken. Eens deployed kan dit niet schoner zonder + # twee-Project-bootstrap; default is acceptabel voor één meta-Application. + project: default + + source: + repoURL: https://github.com/ConductionNL/React-base.git + targetRevision: HEAD + # Recursief alles in argo/ syncen: projects/, applicationsets/, en + # applications/root.yaml zelf (self-management). Argo handelt circulaire + # self-references netjes af. + path: react-platform/argo + directory: + recurse: true + + destination: + server: https://kubernetes.default.svc + namespace: argocd + + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true + retry: + limit: 3 + backoff: + duration: 5s + factor: 2 + maxDuration: 1m diff --git a/react-platform/argo/applicationsets/react-tenants.yaml b/react-platform/argo/applicationsets/react-tenants.yaml index a9551df..409613d 100644 --- a/react-platform/argo/applicationsets/react-tenants.yaml +++ b/react-platform/argo/applicationsets/react-tenants.yaml @@ -24,7 +24,10 @@ spec: generators: - git: repoURL: "https://github.com/ConductionNL/React-base.git" - revision: HEAD + # TODO(post-canary): zet terug naar `HEAD` zodra development naar + # main is gemerged. Voor de eerste live test (canary.openwoo.app) + # wijst de generator én de tenant-source naar development. + revision: development files: - path: "react-platform/values/tenants/tenant-*.yaml" @@ -48,7 +51,8 @@ spec: sources: # Vendored woo-website chart in this repo. - repoURL: "https://github.com/ConductionNL/React-base.git" - targetRevision: HEAD + # TODO(post-canary): zet terug naar `HEAD` na merge development → main. + targetRevision: development path: charts/woo-website helm: releaseName: "woo-website" @@ -120,7 +124,8 @@ spec: # Values repo (this repo) — anchors $values references above. - repoURL: "https://github.com/ConductionNL/React-base.git" - targetRevision: HEAD + # TODO(post-canary): zet terug naar `HEAD` na merge development → main. + targetRevision: development ref: values destination: diff --git a/react-platform/argo/projects/react-platform.yaml b/react-platform/argo/projects/react-platform.yaml index 40a6da1..0e4392f 100644 --- a/react-platform/argo/projects/react-platform.yaml +++ b/react-platform/argo/projects/react-platform.yaml @@ -8,6 +8,10 @@ kind: AppProject metadata: name: react-platform namespace: argocd + annotations: + # Wave -1: zorgt dat de AppProject bestaat voordat de ApplicationSet + # erop wijst (en voor de Applications die die set genereert). + argocd.argoproj.io/sync-wave: "-1" finalizers: - resources-finalizer.argocd.argoproj.io spec: diff --git a/react-platform/docs/BOOTSTRAP.md b/react-platform/docs/BOOTSTRAP.md new file mode 100644 index 0000000..0c2f015 --- /dev/null +++ b/react-platform/docs/BOOTSTRAP.md @@ -0,0 +1,108 @@ +# Bootstrap + +Eenmalige setup om Argo CD `react-base` te laten zien. Daarna is alles GitOps. + +## Voorbereiding + +Vereist: +- `kubectl`-context op de target-cluster (Argo CD draait in de `argocd` ns) +- `argocd` CLI gelogd in op die Argo (`argocd login`) +- Repo `https://github.com/ConductionNL/React-base.git` is publiek bereikbaar + (verifieer met `curl -I https://github.com/ConductionNL/React-base.git`) + +## Stappen + +### 1. Repo registreren bij Argo CD + +```bash +argocd repo add https://github.com/ConductionNL/React-base.git +``` + +(Geen credentials nodig — public repo, zelfde patroon als +`woo-website-template-apiv2` dat de huidige `*-reactfront` Applications al +gebruiken.) + +Verifieer: +```bash +argocd repo list | grep React-base +``` + +### 2. Root Application deployen + +```bash +kubectl apply -f react-platform/argo/applications/root.yaml +``` + +Dit creëert één Argo Application genaamd `react-platform` (project: default, +namespace: argocd) die `react-platform/argo/` recursief watcht. Argo sync't +vervolgens: +- AppProject `react-platform` (sync-wave -1, dus eerst) +- ApplicationSet `react-tenants` (na de AppProject) +- Zichzelf — `applications/root.yaml` (self-management) + +Verifieer: +```bash +kubectl get app -n argocd react-platform +kubectl get appproject -n argocd react-platform +kubectl get appset -n argocd react-tenants +``` + +### 3. ApplicationSet pakt tenant-bestanden op + +Zodra `react-tenants` syncthet, scant hij `react-platform/values/tenants/` +en maakt per `tenant-*.yaml` bestand een Application. De eerste keer: +- `canary-prod-reactfront` (uit `tenant-canary.yaml`) + +Verifieer: +```bash +kubectl get app -n argocd canary-prod-reactfront +kubectl get pod,ingress,cert,networkpolicy -n canary-prod -l react.platform/tenant=canary +``` + +### 4. DNS + TLS auto-provisioning + +- `external-dns` (in `cluster-infra`) detecteert de nieuwe Ingress en maakt + het Cloudflare-record aan voor de hostname. +- `cert-manager` issued een TLS-cert via HTTP-01 challenge zodra DNS staat. + +Verifieer: +```bash +dig +short canary.openwoo.app # moet een IP teruggeven +curl -I https://canary.openwoo.app # 200 met geldige cert +``` + +## Branch-context (huidig: testfase op `development`) + +Tijdens de canary-validatie wijzen `applications/root.yaml`, +`applicationsets/react-tenants.yaml` en de tenant-source naar +`development`. Zodra de canary stabiel draait, één PR die alle drie naar +`HEAD` (= main) zet, gemerged in een sync window. + +Zoek in de repo naar `TODO(post-canary)` — dat zijn de drie locaties. + +## Rollback van de bootstrap + +Als de bootstrap misgaat (bv. ApplicationSet sync't niet, AppProject +RBAC fout, etc.): + +```bash +kubectl delete app -n argocd react-platform # cascade verwijdert de hele tree +argocd repo rm https://github.com/ConductionNL/React-base.git +``` + +Workload-pods in `-` namespaces blijven achter als +`syncOptions.preserveResourcesOnDeletion` gehonoreerd wordt door de +ApplicationSet (zo geconfigureerd). Verwijder die handmatig als nodig: +```bash +kubectl delete -n canary-prod -l react.platform/tenant=canary all,ingress,networkpolicy,cert +``` + +## Troubleshooting + +| Symptoom | Check | +|---|---| +| `react-platform` Application stuck in OutOfSync / ComparisonError | `argocd app get react-platform` — vaak een YAML-fout in `argo/` | +| ApplicationSet maakt geen Applications | Check generator pad + revision: `kubectl get appset react-tenants -o yaml` | +| Application Healthy maar pod ImagePullBackOff | Image-tag bestaat niet in GHCR — override `pwa.image.tag` in tenant-bestand | +| TLS-cert blijft pending | `kubectl describe cert -n `; vaak: hostname nog niet via DNS bereikbaar (external-dns ~1 min vertraging) of oude Ingress claimt de host nog | +| DNS-record verschijnt niet op Cloudflare | `kubectl logs -n external-dns deploy/external-dns` — vaak: zone niet in `domainFilters` of token-permissies missen | diff --git a/react-platform/values/tenants/tenant-canary.yaml b/react-platform/values/tenants/tenant-canary.yaml index 6775faa..27cb75f 100644 --- a/react-platform/values/tenants/tenant-canary.yaml +++ b/react-platform/values/tenants/tenant-canary.yaml @@ -5,3 +5,11 @@ tenant: name: canary environment: prod wave: "0" + +# Image-tag override voor de eerste test. common.yaml pin op "1.0.0" bestaat +# (waarschijnlijk) niet in GHCR — alle bestaande pods draaien :latest. Pin +# hier expliciet :latest tot we een echte semver/digest hebben (task 1.3). +# TODO: vervangen door semver/digest na verificatie tegen GHCR. +pwa: + image: + tag: latest