From 1e9427899229635549e99f0319d3e52baee64cf5 Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Thu, 11 Jun 2026 15:55:45 -0400 Subject: [PATCH 1/8] feat(deploy): add production Helm chart for Buzz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `deploy/charts/buzz/` Helm chart targeting two profiles selected by values: - Production (default): external Postgres/Redis/Typesense/S3 via `secrets.existingSecret`, no chart-side autogeneration, GitOps-safe (ArgoCD / Flux), HA-capable (`replicaCount >= 2` with Redis + RWX git PVC). - Quickstart (`--set quickstart=true`): CloudPirates Postgres + Redis subcharts, chart-managed Secret via `lookup`, single replica, evaluation only. Hard `fail` guards in `_validate.tpl` reject misconfigurations at template time: - missing `relayUrl` - `replicaCount > 1` without Redis or RWX git PVC - missing/malformed `ownerPubkey` when `requireRelayMembership=true` - `ingress.enabled` and `httproute.enabled` both true - missing Postgres or Typesense source `values.schema.json` rejects malformed types / enums at `helm install` time, before templates render — layered defense with `_validate.tpl`. Env wiring matches the project's decided contract: - `RELAY_OWNER_PUBKEY` (no `BUZZ_` prefix; matches `config.rs`) - `BUZZ_AUTO_MIGRATE=true` default — relies on the relay's embedded sqlx migrations (block/sprout#988) - `BUZZ_RELAY_PRIVATE_KEY` is stable across redeploys via `secrets.existingSecret` (production) or the `lookup` pattern with `resource-policy: keep` (quickstart) Includes: - `examples/argocd-app.yaml`, `examples/flux-helmrelease.yaml`, `examples/secret-sample.yaml` — canonical GitOps configurations - `tests/*.yaml` — `helm-unittest` suites covering validation, secret wiring, and networking - `ci/quickstart-values.yaml` for `ct install` (kind, gated) - `tests/fixtures/*` for render-only matrix in CI - `.github/workflows/helm-chart.yml`: `ct lint` + `helm-unittest` + render matrix per-PR; full `ct install` is `workflow_dispatch` gated, runs once `ghcr.io/block/buzz` is publicly published Out of scope for this PR (intentional, per Eva's dispatch): - OCI chart publish + cosign signing → follow-up - In-chart Typesense subchart → bring-your-own for v1 (see README "Honest limitations") - Minimal-mode (`BUZZ_PUBSUB=local` / pg search / filesystem media) → upstream relay work Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .github/workflows/helm-chart.yml | 97 ++++++++ .gitignore | 3 + ct.yaml | 9 + deploy/charts/buzz/Chart.lock | 9 + deploy/charts/buzz/Chart.yaml | 42 ++++ deploy/charts/buzz/README.md | 97 ++++++++ deploy/charts/buzz/ci/quickstart-values.yaml | 19 ++ deploy/charts/buzz/examples/argocd-app.yaml | 76 ++++++ .../buzz/examples/flux-helmrelease.yaml | 64 +++++ .../charts/buzz/examples/secret-sample.yaml | 30 +++ deploy/charts/buzz/templates/NOTES.txt | 83 +++++++ deploy/charts/buzz/templates/_helpers.tpl | 86 +++++++ deploy/charts/buzz/templates/_validate.tpl | 58 +++++ deploy/charts/buzz/templates/deployment.yaml | 183 ++++++++++++++ deploy/charts/buzz/templates/httproute.yaml | 28 +++ deploy/charts/buzz/templates/ingress.yaml | 43 ++++ deploy/charts/buzz/templates/pdb.yaml | 18 ++ deploy/charts/buzz/templates/pvc-git.yaml | 22 ++ .../charts/buzz/templates/secret-chart.yaml | 91 +++++++ deploy/charts/buzz/templates/service.yaml | 19 ++ .../charts/buzz/templates/serviceaccount.yaml | 13 + .../charts/buzz/tests/fixtures/ha-values.yaml | 22 ++ .../production-existing-secret-values.yaml | 27 +++ deploy/charts/buzz/tests/networking_test.yaml | 75 ++++++ deploy/charts/buzz/tests/secrets_test.yaml | 92 +++++++ deploy/charts/buzz/tests/validation_test.yaml | 123 ++++++++++ deploy/charts/buzz/values.schema.json | 224 ++++++++++++++++++ deploy/charts/buzz/values.yaml | 222 +++++++++++++++++ 28 files changed, 1875 insertions(+) create mode 100644 .github/workflows/helm-chart.yml create mode 100644 ct.yaml create mode 100644 deploy/charts/buzz/Chart.lock create mode 100644 deploy/charts/buzz/Chart.yaml create mode 100644 deploy/charts/buzz/README.md create mode 100644 deploy/charts/buzz/ci/quickstart-values.yaml create mode 100644 deploy/charts/buzz/examples/argocd-app.yaml create mode 100644 deploy/charts/buzz/examples/flux-helmrelease.yaml create mode 100644 deploy/charts/buzz/examples/secret-sample.yaml create mode 100644 deploy/charts/buzz/templates/NOTES.txt create mode 100644 deploy/charts/buzz/templates/_helpers.tpl create mode 100644 deploy/charts/buzz/templates/_validate.tpl create mode 100644 deploy/charts/buzz/templates/deployment.yaml create mode 100644 deploy/charts/buzz/templates/httproute.yaml create mode 100644 deploy/charts/buzz/templates/ingress.yaml create mode 100644 deploy/charts/buzz/templates/pdb.yaml create mode 100644 deploy/charts/buzz/templates/pvc-git.yaml create mode 100644 deploy/charts/buzz/templates/secret-chart.yaml create mode 100644 deploy/charts/buzz/templates/service.yaml create mode 100644 deploy/charts/buzz/templates/serviceaccount.yaml create mode 100644 deploy/charts/buzz/tests/fixtures/ha-values.yaml create mode 100644 deploy/charts/buzz/tests/fixtures/production-existing-secret-values.yaml create mode 100644 deploy/charts/buzz/tests/networking_test.yaml create mode 100644 deploy/charts/buzz/tests/secrets_test.yaml create mode 100644 deploy/charts/buzz/tests/validation_test.yaml create mode 100644 deploy/charts/buzz/values.schema.json create mode 100644 deploy/charts/buzz/values.yaml diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml new file mode 100644 index 000000000..1ca421736 --- /dev/null +++ b/.github/workflows/helm-chart.yml @@ -0,0 +1,97 @@ +name: helm chart + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "deploy/charts/buzz/**" + - ".github/workflows/helm-chart.yml" + - "ct.yaml" + pull_request: + paths: + - "deploy/charts/buzz/**" + - ".github/workflows/helm-chart.yml" + - "ct.yaml" + +jobs: + lint-and-unittest: + name: lint + unittest + render matrix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.4 + + - name: Install helm-unittest plugin + run: helm plugin install --version 0.8.2 https://github.com/helm-unittest/helm-unittest + + - name: Set up Python (for chart-testing) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.7.0 + + - name: Build chart dependencies + run: helm dependency build deploy/charts/buzz + + - name: ct lint + run: ct lint --config ct.yaml --all + + - name: helm-unittest + run: helm unittest deploy/charts/buzz + + - name: helm template (render every fixture) + run: | + set -euo pipefail + for f in deploy/charts/buzz/ci/*-values.yaml deploy/charts/buzz/tests/fixtures/*-values.yaml; do + echo "::group::render $f" + helm template buzz deploy/charts/buzz -f "$f" + echo "::endgroup::" + done + + install-on-kind: + # Full end-to-end install requires the public ghcr.io/block/buzz image to + # exist and to embed Max's startup migrations. Runs only after Sami's + # image PR merges (`workflow_dispatch`) or on a schedule once main carries + # both prerequisites. Render/lint above is the per-PR signal. + name: install on kind (gated) + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + needs: lint-and-unittest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.4 + + - name: Set up Python (for chart-testing) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.7.0 + + - name: Create kind cluster + uses: helm/kind-action@v1.10.0 + with: + version: v0.24.0 + node_image: kindest/node:v1.31.0 + + - name: Build chart dependencies + run: helm dependency build deploy/charts/buzz + + - name: ct install (quickstart profile) + run: ct install --config ct.yaml --charts deploy/charts/buzz --helm-extra-args "--timeout 600s" diff --git a/.gitignore b/.gitignore index 9a7436439..3fdd5d7c3 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ identity.key # mesh-llm build cache .cache/ + +# Helm dependency tarballs — regenerable from Chart.lock via `helm dependency build` +deploy/charts/*/charts/*.tgz diff --git a/ct.yaml b/ct.yaml new file mode 100644 index 000000000..23d1467a1 --- /dev/null +++ b/ct.yaml @@ -0,0 +1,9 @@ +chart-dirs: + - deploy/charts +chart-repos: [] +helm-extra-args: --timeout 600s +target-branch: main +# Skip OCI subchart fetches that need docker.io anonymous access — ct +# resolves these locally via `helm dependency build` in the workflow. +remote: origin +validate-maintainers: false diff --git a/deploy/charts/buzz/Chart.lock b/deploy/charts/buzz/Chart.lock new file mode 100644 index 000000000..9987bcf85 --- /dev/null +++ b/deploy/charts/buzz/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: postgres + repository: oci://registry-1.docker.io/cloudpirates + version: 0.19.5 +- name: redis + repository: oci://registry-1.docker.io/cloudpirates + version: 0.30.3 +digest: sha256:9c0df32008f782064104ecec8552f348841f120852d49ace66966a56ba904348 +generated: "2026-06-11T15:45:41.764379-04:00" diff --git a/deploy/charts/buzz/Chart.yaml b/deploy/charts/buzz/Chart.yaml new file mode 100644 index 000000000..0a1a32d3c --- /dev/null +++ b/deploy/charts/buzz/Chart.yaml @@ -0,0 +1,42 @@ +apiVersion: v2 +name: buzz +description: | + Buzz — a Nostr-based messaging platform for human–agent collaboration. + + A single relay binary serving WebSocket + REST + web UI, backed by + PostgreSQL, Redis, and Typesense. Configurable for single-node evaluation + (subcharts on) and HA production (external services, existingSecret). +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/block/buzz +sources: + - https://github.com/block/buzz +keywords: + - nostr + - relay + - messaging + - websocket + - chat +maintainers: + - name: Block + url: https://github.com/block +annotations: + artifacthub.io/changes: | + - kind: added + description: Initial chart for Buzz relay. + artifacthub.io/license: Apache-2.0 + +# Optional eval-only subcharts. Production deploys disable both and point +# externalPostgresql / externalRedis (or secrets.existingSecret) at managed +# services. +dependencies: + - name: postgres + version: "0.19.x" + repository: oci://registry-1.docker.io/cloudpirates + condition: postgresql.enabled + alias: postgresql + - name: redis + version: "0.30.x" + repository: oci://registry-1.docker.io/cloudpirates + condition: redis.enabled diff --git a/deploy/charts/buzz/README.md b/deploy/charts/buzz/README.md new file mode 100644 index 000000000..ff6c1b032 --- /dev/null +++ b/deploy/charts/buzz/README.md @@ -0,0 +1,97 @@ +# Buzz Helm Chart + +[Buzz](https://github.com/block/buzz) is a Nostr-based messaging platform for human–agent collaboration: a single relay binary serving WebSocket + REST + web UI, backed by PostgreSQL, Redis, Typesense, and S3-compatible object storage. + +This chart has two operating profiles selected by values: + +| Profile | When | What you get | +|---|---|---| +| **Production** (default) | Self-hosted multi-tenant, regulated, or GitOps-managed | External managed Postgres/Redis/Typesense/S3, `secrets.existingSecret:`, no chart-side autogen, HA-capable (`replicaCount ≥ 2`) | +| **Quickstart** (`--set quickstart=true`) | Eval, single-node, one-off demo | In-cluster Postgres + Redis subcharts ([CloudPirates](https://github.com/cloudpirates)), chart auto-generates relay secrets, single replica | + +## Quickstart (eval only) + +```sh +helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.0 \ + --create-namespace --namespace buzz \ + --set quickstart=true \ + --set postgresql.enabled=true \ + --set redis.enabled=true \ + --set relayUrl=wss://buzz.example.com \ + --set ownerPubkey=<64-char-hex-pubkey> \ + --set typesense.url=http://typesense.buzz.svc.cluster.local:8108 \ + --set typesense.apiKey= +``` + +Quickstart still requires an externally managed Typesense in v1; bring up a minimal Typesense Pod/StatefulSet in your namespace, or set `typesense.url` and `typesense.apiKey` to a hosted instance. See the open question in `OPEN_QUESTIONS` at the bottom of this README. + +## Production (GitOps) + +The chart is designed for ArgoCD and Flux. Both render charts with `helm template`, in which mode Helm's `lookup` function returns empty — any chart-side `randAlphaNum` call would regenerate secrets on every sync. The chart-managed Secret path is **only** safe for `helm install` / `helm upgrade`. + +Production deploys MUST use `secrets.existingSecret:`. The Secret is consumed for any keys present and ignored for keys missing — extras are harmless. + +See: + +- [`examples/argocd-app.yaml`](examples/argocd-app.yaml) — ArgoCD Application +- [`examples/flux-helmrelease.yaml`](examples/flux-helmrelease.yaml) — Flux HelmRelease v2 +- [`examples/secret-sample.yaml`](examples/secret-sample.yaml) — Secret schema + +## Required inputs + +| Key | What | When required | +|---|---|---| +| `relayUrl` | Public `wss://` URL clients connect to | Always | +| `ownerPubkey` | 64-char lowercase hex Nostr pubkey of the relay operator | When `relay.requireRelayMembership=true` (default) | +| `secrets.existingSecret` | Name of pre-created Secret | Production / GitOps | +| `externalPostgresql.url` / `externalRedis.url` / `typesense.url` | External service URLs | When the matching subchart is disabled (default) | + +The chart fails at `helm install` / `helm template` time with a clear message if any of these are missing or malformed (see `templates/_validate.tpl`). + +## HA (production) + +`replicaCount > 1` hard-requires both: + +- Redis (`redis.enabled=true`, `externalRedis.url`, or `REDIS_URL` in `existingSecret`) — for `buzz-pubsub` fan-out +- ReadWriteMany git PVC — `persistence.git.accessMode: ReadWriteMany` with a RWX storage class (e.g. `efs-sc` on AWS, `azurefile-csi` on Azure) + +The chart **template-fails** if either invariant is broken. No silent degradation. + +## Upgrades + +Schema migrations are embedded in the relay binary via `sqlx::migrate!` and run at startup, gated by `BUZZ_AUTO_MIGRATE` (default `true`). Multiple replicas race-safely behind a Postgres advisory lock. `helm upgrade` is the entire upgrade procedure. + +If you prefer decoupling migrations from serving, set `migrate.autoMigrate=false` and run `buzz-admin migrate` (separate Pod / one-shot Job) before upgrading. A pre-upgrade Helm Job for this is on the chart roadmap; the values knob `migrate.preUpgradeJob.enabled` is reserved. + +## Backups + +Save these. Losing any of them is data loss. See NOTES.txt printed by `helm install` for the live list: + +1. `BUZZ_RELAY_PRIVATE_KEY` — relay identity. Rotating it = new identity (federation peers will not recognize the relay). +2. PostgreSQL database — the canonical event store. +3. S3 bucket — media blobs (chart default bucket: `buzz-media`). +4. Git PVC — repo on-disk state served by the relay's git endpoint. +5. Owner private key — held by the operator, not by this chart. Restore by re-installing with the same `ownerPubkey`. + +## Honest limitations (v1) + +- **Typesense has no in-chart subchart.** Bring your own Typesense; the chart wires it via `typesense.url` + `typesense.apiKey` (or `TYPESENSE_URL` / `TYPESENSE_API_KEY` in `existingSecret`). The roadmap depends on either an upstream community chart hitting our quality bar or a minimal in-chart StatefulSet behind a quickstart flag. +- **Minimal-mode is not yet supported.** The relay's `BUZZ_PUBSUB=local` / `BUZZ_SEARCH=pg` / filesystem media paths are upstream work in progress. Until then, "quickstart" still needs Typesense. +- **OCI publish to GHCR + cosign signing** is a follow-up PR. For now, install the chart from source: `helm install buzz ./deploy/charts/buzz` after cloning the repo. + +## Development + +```sh +# Render every fixture +for f in ci/*-values.yaml tests/fixtures/*-values.yaml; do + helm template buzz . -f "$f" >/dev/null && echo "ok: $f" +done + +# Unit tests +helm plugin install https://github.com/helm-unittest/helm-unittest +helm unittest . + +# Lint +helm dependency build . +ct lint --config ../../../ct.yaml --charts . +``` diff --git a/deploy/charts/buzz/ci/quickstart-values.yaml b/deploy/charts/buzz/ci/quickstart-values.yaml new file mode 100644 index 000000000..ea5815d29 --- /dev/null +++ b/deploy/charts/buzz/ci/quickstart-values.yaml @@ -0,0 +1,19 @@ +# Quickstart / eval: subcharts on, autogen secrets, single replica. +# This is the scenario `ct install` exercises against a kind cluster — it +# spins up postgres + redis in-cluster so the relay can actually start. +quickstart: true +postgresql: + enabled: true +redis: + enabled: true +relayUrl: wss://buzz.test.local +ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000001" +typesense: + url: "http://typesense.default.svc.cluster.local:8108" + apiKey: "ci-fake-key" +relay: + # Don't enforce membership in CI — we're testing the chart renders and the + # Pod starts, not relay business logic. + requireRelayMembership: false +podDisruptionBudget: + enabled: false diff --git a/deploy/charts/buzz/examples/argocd-app.yaml b/deploy/charts/buzz/examples/argocd-app.yaml new file mode 100644 index 000000000..612f6e4b2 --- /dev/null +++ b/deploy/charts/buzz/examples/argocd-app.yaml @@ -0,0 +1,76 @@ +# ArgoCD Application — GitOps-safe Buzz install. +# +# Prerequisite: a Secret named `buzz-secrets` in namespace `buzz` containing +# (any subset of) the keys consumed by `secrets.existingSecret`. See +# `secret-sample.yaml` for the schema. +# +# Why `existingSecret` and not chart autogen: ArgoCD renders manifests with +# `helm template`, in which mode Helm's `lookup` function returns empty and +# any chart-side `randAlphaNum` call regenerates on every sync. The +# chart-managed Secret path is for `helm install` / `helm upgrade` only. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: buzz + namespace: argocd +spec: + project: default + source: + repoURL: oci://ghcr.io/block/buzz/charts + chart: buzz + targetRevision: 0.1.0 + helm: + releaseName: buzz + values: | + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" # replace + replicaCount: 3 + + secrets: + existingSecret: buzz-secrets + + externalPostgresql: + # DATABASE_URL also lives in buzz-secrets; this is here only if you + # prefer the URL stored in values vs. the Secret. Pick one. + url: "" + + externalRedis: + url: "" + + typesense: + url: "http://typesense.buzz.svc.cluster.local:8108" + # apiKey lives in buzz-secrets + + s3: + endpoint: "https://s3.us-east-1.amazonaws.com" + bucket: "buzz-media" + # accessKey / secretKey live in buzz-secrets + + persistence: + git: + enabled: true + accessMode: ReadWriteMany # required: replicaCount > 1 + storageClass: efs-sc # provider-specific RWX class + size: 50Gi + + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + # WebSocket: long-lived; raise proxy timeouts. + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + tls: + - hosts: [buzz.example.com] + secretName: buzz-tls + destination: + server: https://kubernetes.default.svc + namespace: buzz + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/deploy/charts/buzz/examples/flux-helmrelease.yaml b/deploy/charts/buzz/examples/flux-helmrelease.yaml new file mode 100644 index 000000000..a6b6276c5 --- /dev/null +++ b/deploy/charts/buzz/examples/flux-helmrelease.yaml @@ -0,0 +1,64 @@ +# Flux HelmRelease — GitOps-safe Buzz install. +# +# Prerequisite: a Secret named `buzz-secrets` in namespace `buzz`. See +# `secret-sample.yaml`. Flux renders the chart server-side via the +# helm-controller, which (like ArgoCD) treats chart-side `randAlphaNum` +# autogen as non-idempotent. `existingSecret` is the only safe path. +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: buzz + namespace: buzz +spec: + type: oci + url: oci://ghcr.io/block/buzz/charts + interval: 10m +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: buzz + namespace: buzz +spec: + interval: 10m + chart: + spec: + chart: buzz + version: "0.1.0" + sourceRef: + kind: HelmRepository + name: buzz + values: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" # replace + replicaCount: 3 + + secrets: + existingSecret: buzz-secrets + + typesense: + url: "http://typesense.buzz.svc.cluster.local:8108" + # apiKey lives in buzz-secrets + + s3: + endpoint: "https://s3.us-east-1.amazonaws.com" + bucket: "buzz-media" + + persistence: + git: + enabled: true + accessMode: ReadWriteMany + storageClass: efs-sc + size: 50Gi + + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + tls: + - hosts: [buzz.example.com] + secretName: buzz-tls diff --git a/deploy/charts/buzz/examples/secret-sample.yaml b/deploy/charts/buzz/examples/secret-sample.yaml new file mode 100644 index 000000000..c842038b4 --- /dev/null +++ b/deploy/charts/buzz/examples/secret-sample.yaml @@ -0,0 +1,30 @@ +# Sample Secret matching `secrets.existingSecret: buzz-secrets`. +# +# Manage this via SealedSecrets / SOPS / External Secrets / Vault — anything +# that keeps the unencrypted form out of git. The chart reads keys it finds +# and leaves the rest as Pod env vars marked `optional: true`. +# +# Keys consumed by the relay (all optional unless required by relay config): +# BUZZ_RELAY_PRIVATE_KEY — 64-char hex; relay identity (do NOT rotate) +# BUZZ_GIT_HOOK_HMAC_SECRET — 32+ chars; required when replicaCount > 1 +# DATABASE_URL — postgres://... +# REDIS_URL — redis://... (required when replicaCount > 1) +# TYPESENSE_URL +# TYPESENSE_API_KEY +# BUZZ_S3_ACCESS_KEY +# BUZZ_S3_SECRET_KEY +apiVersion: v1 +kind: Secret +metadata: + name: buzz-secrets + namespace: buzz +type: Opaque +stringData: + BUZZ_RELAY_PRIVATE_KEY: "REPLACE_WITH_64_HEX" + BUZZ_GIT_HOOK_HMAC_SECRET: "REPLACE_WITH_RANDOM_64_CHARS" + DATABASE_URL: "postgres://buzz:REPLACE@postgres.buzz.svc.cluster.local:5432/buzz?sslmode=require" + REDIS_URL: "redis://:REPLACE@redis.buzz.svc.cluster.local:6379" + TYPESENSE_URL: "http://typesense.buzz.svc.cluster.local:8108" + TYPESENSE_API_KEY: "REPLACE" + BUZZ_S3_ACCESS_KEY: "REPLACE" + BUZZ_S3_SECRET_KEY: "REPLACE" diff --git a/deploy/charts/buzz/templates/NOTES.txt b/deploy/charts/buzz/templates/NOTES.txt new file mode 100644 index 000000000..8f963ae85 --- /dev/null +++ b/deploy/charts/buzz/templates/NOTES.txt @@ -0,0 +1,83 @@ +══════════════════════════════════════════════════════════════════════════════ + Buzz {{ .Chart.AppVersion }} — release "{{ .Release.Name }}" (namespace {{ .Release.Namespace }}) +══════════════════════════════════════════════════════════════════════════════ + +▶ Relay URL + {{ .Values.relayUrl }} + +▶ Owner pubkey + {{ .Values.ownerPubkey }} + {{- if not .Values.ownerPubkey }} + ⚠ ownerPubkey is empty — this is only valid when relay.requireRelayMembership=false. + {{- end }} + +▶ Health + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "buzz.fullname" . }} 8080:{{ .Values.service.healthPort }} + curl http://localhost:8080/_readiness + +{{ if not .Values.ingress.enabled }}{{ if not .Values.httproute.enabled }} +▶ Networking + Neither ingress nor Gateway API HTTPRoute is enabled. Expose the relay + through your own gateway, then ensure clients reach .Values.relayUrl + ({{ .Values.relayUrl }}) over wss://. Long-lived WebSocket connections + require generous proxy read/send timeouts (≥ 1h). +{{ end }}{{ end }} + +────────────────────────────────────────────────────────────────────────────── + Profile +────────────────────────────────────────────────────────────────────────────── +{{ if or .Values.postgresql.enabled .Values.redis.enabled }} +⚠ QUICKSTART / EVALUATION PROFILE + {{ if .Values.postgresql.enabled }}- In-cluster Postgres subchart (CloudPirates){{ end }} + {{ if .Values.redis.enabled }}- In-cluster Redis subchart (CloudPirates){{ end }} + - Chart auto-generates secrets via the `lookup` pattern. This is NOT + GitOps-safe — secrets will silently rotate under ArgoCD/Flux. For + production, see examples/argocd-app.yaml or examples/flux-helmrelease.yaml. + +{{ else }} +✓ PRODUCTION PROFILE + External Postgres, Redis (if enabled), Typesense, S3. + {{ if .Values.secrets.existingSecret }}- Secrets sourced from: {{ .Values.secrets.existingSecret }}{{ end }} +{{ end }} + +────────────────────────────────────────────────────────────────────────────── + Backups — save these +────────────────────────────────────────────────────────────────────────────── + 1. BUZZ_RELAY_PRIVATE_KEY — relay identity. Rotating it = identity change; + federation peers will treat the relay as a new identity. + 2. PostgreSQL database{{ if .Values.postgresql.enabled }} ({{ .Release.Name }}-postgresql PVC){{ end }} + 3. S3 bucket "{{ .Values.s3.bucket }}" — media blobs + 4. Git PVC ({{ include "buzz.fullname" . }}-git) — repo on-disk state + 5. Owner private key (held by the operator, NOT the chart) — restore by + re-installing with the same ownerPubkey. + +────────────────────────────────────────────────────────────────────────────── + Degradation warnings +────────────────────────────────────────────────────────────────────────────── +{{- if not .Values.relay.requireAuthToken }} + ⚠ relay.requireAuthToken=false — REST API bypasses token auth. Production + should set this to true. +{{- end }} +{{- if not .Values.relay.requireRelayMembership }} + ⚠ relay.requireRelayMembership=false — relay is OPEN. Anyone can publish. +{{- end }} +{{- if and (gt (.Values.replicaCount | int) 1) (eq .Values.persistence.git.accessMode "ReadWriteOnce") }} + ⚠ replicaCount > 1 with ReadWriteOnce git PVC will fail at template time + (this message should never appear — file a bug). +{{- end }} +{{- if not .Values.secrets.existingSecret }} +{{- if not (or .Values.postgresql.enabled .Values.redis.enabled) }} + ⚠ Chart-managed Secret is in use (no secrets.existingSecret). This is fine + for `helm install` / `helm upgrade` but NOT safe under GitOps tools that + `helm template` to render manifests — the `lookup` function returns empty + in that mode and secrets will silently rotate. Use existingSecret for + ArgoCD / Flux. +{{- end }} +{{- end }} + +────────────────────────────────────────────────────────────────────────────── + Useful commands +────────────────────────────────────────────────────────────────────────────── + kubectl -n {{ .Release.Namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }} + kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/instance={{ .Release.Name }} --tail=200 + kubectl -n {{ .Release.Namespace }} rollout status deployment/{{ include "buzz.fullname" . }} diff --git a/deploy/charts/buzz/templates/_helpers.tpl b/deploy/charts/buzz/templates/_helpers.tpl new file mode 100644 index 000000000..e6774252e --- /dev/null +++ b/deploy/charts/buzz/templates/_helpers.tpl @@ -0,0 +1,86 @@ +{{/* Standard naming/labels helpers. */}} + +{{- define "buzz.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "buzz.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 -}} + +{{- define "buzz.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "buzz.labels" -}} +helm.sh/chart: {{ include "buzz.chart" . }} +{{ include "buzz.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: buzz +{{- end -}} + +{{- define "buzz.selectorLabels" -}} +app.kubernetes.io/name: {{ include "buzz.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "buzz.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "buzz.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{- define "buzz.image" -}} +{{- $tag := default .Chart.AppVersion .Values.image.tag -}} +{{- printf "%s:%s" .Values.image.repository $tag -}} +{{- end -}} + +{{/* +Name of the chart-managed Secret holding relay-identity material and any +chart-composed connection strings. +*/}} +{{- define "buzz.chartSecretName" -}} +{{- printf "%s-relay" (include "buzz.fullname" .) -}} +{{- end -}} + +{{/* +The Secret name the relay should pull env from. If the operator supplied +secrets.existingSecret, use that. Otherwise use the chart-managed one. +*/}} +{{- define "buzz.envSecretName" -}} +{{- if .Values.secrets.existingSecret -}} +{{- .Values.secrets.existingSecret -}} +{{- else -}} +{{- include "buzz.chartSecretName" . -}} +{{- end -}} +{{- end -}} + +{{/* Host derived from relayUrl, used as ingress default + media domain. */}} +{{- define "buzz.relayHost" -}} +{{- $url := required "relayUrl is required: set --set relayUrl=wss://your.domain" .Values.relayUrl -}} +{{- $stripped := $url | replace "wss://" "" | replace "ws://" "" | replace "https://" "" | replace "http://" "" -}} +{{- first (splitList "/" $stripped) -}} +{{- end -}} + +{{/* Default media base URL: https:///media derived from relayUrl. */}} +{{- define "buzz.mediaBaseUrl" -}} +{{- if .Values.mediaBaseUrl -}} +{{- .Values.mediaBaseUrl -}} +{{- else -}} +{{- printf "https://%s/media" (include "buzz.relayHost" .) -}} +{{- end -}} +{{- end -}} diff --git a/deploy/charts/buzz/templates/_validate.tpl b/deploy/charts/buzz/templates/_validate.tpl new file mode 100644 index 000000000..e314cfb60 --- /dev/null +++ b/deploy/charts/buzz/templates/_validate.tpl @@ -0,0 +1,58 @@ +{{/* +Hard fail guards. Included from every rendered template so misconfigs +surface at template time regardless of which manifest helm renders first. +*/}} + +{{- define "buzz.validate" -}} + +{{/* relayUrl is required */}} +{{- if not .Values.relayUrl -}} + {{- fail "relayUrl is required: set --set relayUrl=wss://your.domain" -}} +{{- end -}} + +{{/* replicaCount > 1 requires Redis */}} +{{- if gt (.Values.replicaCount | int) 1 -}} + {{- if and (not .Values.redis.enabled) (not .Values.externalRedis.url) (not .Values.secrets.existingSecret) -}} + {{- fail (printf "replicaCount=%d requires Redis for buzz-pubsub. Enable redis.enabled=true, set externalRedis.url, or provide secrets.existingSecret with key REDIS_URL." (.Values.replicaCount | int)) -}} + {{- end -}} +{{- end -}} + +{{/* replicaCount > 1 requires ReadWriteMany git storage */}} +{{- if gt (.Values.replicaCount | int) 1 -}} + {{- if and .Values.persistence.git.enabled (not .Values.persistence.git.existingClaim) -}} + {{- if ne .Values.persistence.git.accessMode "ReadWriteMany" -}} + {{- fail (printf "replicaCount=%d requires persistence.git.accessMode=ReadWriteMany (got %q). The relay's git on-disk state must be shared across replicas." (.Values.replicaCount | int) .Values.persistence.git.accessMode) -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* Owner pubkey required when requireRelayMembership */}} +{{- if .Values.relay.requireRelayMembership -}} + {{- if not .Values.ownerPubkey -}} + {{- fail "ownerPubkey is required when relay.requireRelayMembership=true. Set ownerPubkey to the 64-char lowercase hex Nostr pubkey of the relay operator, or set relay.requireRelayMembership=false for an open relay." -}} + {{- end -}} +{{- end -}} + +{{/* ownerPubkey format check */}} +{{- if .Values.ownerPubkey -}} + {{- if not (regexMatch "^[0-9a-f]{64}$" .Values.ownerPubkey) -}} + {{- fail (printf "ownerPubkey must be 64 lowercase hex characters (got %d chars; must match ^[0-9a-f]{64}$)." (len .Values.ownerPubkey)) -}} + {{- end -}} +{{- end -}} + +{{/* ingress + httproute mutually exclusive */}} +{{- if and .Values.ingress.enabled .Values.httproute.enabled -}} + {{- fail "ingress.enabled and httproute.enabled cannot both be true — choose one." -}} +{{- end -}} + +{{/* Postgres source must exist somewhere */}} +{{- if not (or .Values.postgresql.enabled .Values.externalPostgresql.url .Values.secrets.existingSecret) -}} + {{- fail "Postgres source missing: enable postgresql.enabled=true, set externalPostgresql.url, or provide secrets.existingSecret with key DATABASE_URL." -}} +{{- end -}} + +{{/* Typesense source must exist somewhere */}} +{{- if not (or .Values.typesense.url .Values.secrets.existingSecret) -}} + {{- fail "Typesense source missing: set typesense.url + typesense.apiKey, or provide secrets.existingSecret with keys TYPESENSE_URL + TYPESENSE_API_KEY." -}} +{{- end -}} + +{{- end -}} diff --git a/deploy/charts/buzz/templates/deployment.yaml b/deploy/charts/buzz/templates/deployment.yaml new file mode 100644 index 000000000..a07dd18e3 --- /dev/null +++ b/deploy/charts/buzz/templates/deployment.yaml @@ -0,0 +1,183 @@ +{{- include "buzz.validate" . -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "buzz.fullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + {{- include "buzz.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "buzz.selectorLabels" . | nindent 8 }} + {{- with .Values.relay.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + # Roll pods when the chart-managed Secret changes. + checksum/secret: {{ include (print $.Template.BasePath "/secret-chart.yaml") . | sha256sum }} + {{- with .Values.relay.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "buzz.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.relay.securityContext | nindent 8 }} + terminationGracePeriodSeconds: {{ .Values.relay.terminationGracePeriodSeconds }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.relay.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.relay.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.relay.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.relay.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: relay + image: {{ include "buzz.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.relay.containerSecurityContext | nindent 12 }} + ports: + - { name: app, containerPort: 3000, protocol: TCP } + - { name: health, containerPort: {{ .Values.service.healthPort }}, protocol: TCP } + - { name: metrics, containerPort: {{ .Values.service.metricsPort }}, protocol: TCP } + env: + # ── Networking ─────────────────────────────────────────── + - { name: BUZZ_BIND_ADDR, value: {{ .Values.relay.bindAddr | quote }} } + - { name: BUZZ_HEALTH_PORT, value: {{ .Values.service.healthPort | quote }} } + - { name: BUZZ_METRICS_PORT, value: {{ .Values.service.metricsPort | quote }} } + - { name: RELAY_URL, value: {{ .Values.relayUrl | quote }} } + - { name: BUZZ_MEDIA_BASE_URL, value: {{ include "buzz.mediaBaseUrl" . | quote }} } + - { name: BUZZ_MEDIA_SERVER_DOMAIN, value: {{ include "buzz.relayHost" . | quote }} } + + # ── Behavior ───────────────────────────────────────────── + - { name: BUZZ_MAX_CONNECTIONS, value: {{ .Values.relay.maxConnections | quote }} } + - { name: BUZZ_MAX_CONCURRENT_HANDLERS, value: {{ .Values.relay.maxConcurrentHandlers | quote }} } + - { name: BUZZ_SEND_BUFFER, value: {{ .Values.relay.sendBuffer | quote }} } + - { name: BUZZ_REQUIRE_AUTH_TOKEN, value: {{ .Values.relay.requireAuthToken | quote }} } + - { name: BUZZ_REQUIRE_RELAY_MEMBERSHIP, value: {{ .Values.relay.requireRelayMembership | quote }} } + - { name: BUZZ_ALLOW_NIP_OA_AUTH, value: {{ .Values.relay.allowNipOaAuth | quote }} } + - { name: BUZZ_PUBKEY_ALLOWLIST, value: {{ .Values.relay.pubkeyAllowlist | quote }} } + {{- if .Values.relay.corsOrigins }} + - { name: BUZZ_CORS_ORIGINS, value: {{ join "," .Values.relay.corsOrigins | quote }} } + {{- end }} + {{- if gt (.Values.relay.ephemeralTtlOverride | int) 0 }} + - { name: BUZZ_EPHEMERAL_TTL_OVERRIDE, value: {{ .Values.relay.ephemeralTtlOverride | quote }} } + {{- end }} + + # ── Owner ──────────────────────────────────────────────── + - { name: RELAY_OWNER_PUBKEY, value: {{ .Values.ownerPubkey | quote }} } + + # ── Migrations ─────────────────────────────────────────── + - { name: BUZZ_AUTO_MIGRATE, value: {{ .Values.migrate.autoMigrate | quote }} } + + # ── Git ────────────────────────────────────────────────── + - { name: BUZZ_GIT_REPO_PATH, value: {{ .Values.persistence.git.mountPath | quote }} } + - { name: BUZZ_GIT_MAX_PACK_BYTES, value: {{ .Values.git.maxPackBytes | quote }} } + - { name: BUZZ_GIT_MAX_REPOS_PER_PUBKEY, value: {{ .Values.git.maxReposPerPubkey | quote }} } + - { name: BUZZ_GIT_MAX_CONCURRENT_OPS, value: {{ .Values.git.maxConcurrentOps | quote }} } + + # ── S3 (non-secret) ────────────────────────────────────── + {{- if .Values.s3.endpoint }} + - { name: BUZZ_S3_ENDPOINT, value: {{ .Values.s3.endpoint | quote }} } + {{- end }} + - { name: BUZZ_S3_BUCKET, value: {{ .Values.s3.bucket | quote }} } + + # ── Secrets (from chart-managed or existing) ───────────── + - name: BUZZ_RELAY_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: BUZZ_RELAY_PRIVATE_KEY + optional: true + - name: BUZZ_GIT_HOOK_HMAC_SECRET + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: BUZZ_GIT_HOOK_HMAC_SECRET + optional: true + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: DATABASE_URL + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: REDIS_URL + optional: {{ and (eq (.Values.replicaCount | int) 1) (not .Values.redis.enabled) (not .Values.externalRedis.url) }} + - name: TYPESENSE_URL + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: TYPESENSE_URL + - name: TYPESENSE_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: TYPESENSE_API_KEY + - name: BUZZ_S3_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: BUZZ_S3_ACCESS_KEY + optional: true + - name: BUZZ_S3_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.envSecretName" . }} + key: BUZZ_S3_SECRET_KEY + optional: true + + {{- with .Values.relay.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.relay.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + + livenessProbe: + {{- toYaml .Values.relay.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.relay.readinessProbe | nindent 12 }} + startupProbe: + {{- toYaml .Values.relay.startupProbe | nindent 12 }} + + resources: + {{- toYaml .Values.relay.resources | nindent 12 }} + + volumeMounts: + {{- if .Values.persistence.git.enabled }} + - { name: git-repos, mountPath: {{ .Values.persistence.git.mountPath | quote }} } + {{- end }} + + volumes: + {{- if .Values.persistence.git.enabled }} + - name: git-repos + persistentVolumeClaim: + claimName: {{ default (printf "%s-git" (include "buzz.fullname" .)) .Values.persistence.git.existingClaim }} + {{- end }} diff --git a/deploy/charts/buzz/templates/httproute.yaml b/deploy/charts/buzz/templates/httproute.yaml new file mode 100644 index 000000000..2c0b77d69 --- /dev/null +++ b/deploy/charts/buzz/templates/httproute.yaml @@ -0,0 +1,28 @@ +{{- include "buzz.validate" . -}} +{{- if .Values.httproute.enabled -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "buzz.fullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} +spec: + parentRefs: + {{- toYaml .Values.httproute.parentRefs | nindent 4 }} + {{- with .Values.httproute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- if .Values.httproute.rules }} + {{- toYaml .Values.httproute.rules | nindent 4 }} + {{- else }} + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "buzz.fullname" . }} + port: {{ .Values.service.port }} + {{- end }} +{{- end }} diff --git a/deploy/charts/buzz/templates/ingress.yaml b/deploy/charts/buzz/templates/ingress.yaml new file mode 100644 index 000000000..4f678eb41 --- /dev/null +++ b/deploy/charts/buzz/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- include "buzz.validate" . -}} +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "buzz.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- $defaultHost := include "buzz.relayHost" . -}} +{{- $hosts := .Values.ingress.hosts -}} +{{- if not $hosts -}} +{{- $hosts = list (dict "host" $defaultHost "paths" (list (dict "path" "/" "pathType" "Prefix"))) -}} +{{- end -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range $hosts }} + - host: {{ .host | default $defaultHost | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | default "/" }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/charts/buzz/templates/pdb.yaml b/deploy/charts/buzz/templates/pdb.yaml new file mode 100644 index 000000000..335e8e46c --- /dev/null +++ b/deploy/charts/buzz/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- include "buzz.validate" . -}} +{{- if and .Values.podDisruptionBudget.enabled (gt (.Values.replicaCount | int) 1) -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "buzz.fullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "buzz.selectorLabels" . | nindent 6 }} + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/deploy/charts/buzz/templates/pvc-git.yaml b/deploy/charts/buzz/templates/pvc-git.yaml new file mode 100644 index 000000000..81b7ba9f2 --- /dev/null +++ b/deploy/charts/buzz/templates/pvc-git.yaml @@ -0,0 +1,22 @@ +{{- include "buzz.validate" . -}} +{{- if and .Values.persistence.git.enabled (not .Values.persistence.git.existingClaim) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "buzz.fullname" . }}-git + labels: + {{- include "buzz.labels" . | nindent 4 }} + {{- with .Values.persistence.git.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.git.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.git.size }} + {{- if .Values.persistence.git.storageClass }} + storageClassName: {{ .Values.persistence.git.storageClass | quote }} + {{- end }} +{{- end -}} diff --git a/deploy/charts/buzz/templates/secret-chart.yaml b/deploy/charts/buzz/templates/secret-chart.yaml new file mode 100644 index 000000000..6824575a4 --- /dev/null +++ b/deploy/charts/buzz/templates/secret-chart.yaml @@ -0,0 +1,91 @@ +{{- include "buzz.validate" . -}} +{{- /* +Chart-managed Secret. + +Renders only when at least one chart-managed value is needed (no +secrets.existingSecret provided OR in-cluster Postgres composes DATABASE_URL +here). Persists across upgrades via the `lookup` pattern. Not GitOps-safe — +ArgoCD/Flux users should provide secrets.existingSecret instead. +*/ -}} + +{{- if not .Values.secrets.existingSecret -}} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace (include "buzz.chartSecretName" .)) | default dict -}} +{{- $existingData := (get $existing "data") | default dict -}} + +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "buzz.chartSecretName" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + + {{- /* Relay private key (relay identity; rotation = identity change) */}} + {{- if .Values.secrets.relayPrivateKey }} + BUZZ_RELAY_PRIVATE_KEY: {{ .Values.secrets.relayPrivateKey | b64enc | quote }} + {{- else if (index $existingData "BUZZ_RELAY_PRIVATE_KEY") }} + BUZZ_RELAY_PRIVATE_KEY: {{ index $existingData "BUZZ_RELAY_PRIVATE_KEY" | quote }} + {{- else }} + BUZZ_RELAY_PRIVATE_KEY: {{ randAlphaNum 64 | lower | b64enc | quote }} + {{- end }} + + {{- /* Git hook HMAC (required when replicaCount > 1) */}} + {{- if .Values.secrets.gitHookHmacSecret }} + BUZZ_GIT_HOOK_HMAC_SECRET: {{ .Values.secrets.gitHookHmacSecret | b64enc | quote }} + {{- else if (index $existingData "BUZZ_GIT_HOOK_HMAC_SECRET") }} + BUZZ_GIT_HOOK_HMAC_SECRET: {{ index $existingData "BUZZ_GIT_HOOK_HMAC_SECRET" | quote }} + {{- else }} + BUZZ_GIT_HOOK_HMAC_SECRET: {{ randAlphaNum 64 | b64enc | quote }} + {{- end }} + + {{- /* In-cluster Postgres: compose DATABASE_URL + postgres-password */}} + {{- if .Values.postgresql.enabled }} + {{- $pgHost := printf "%s-postgresql" .Release.Name }} + {{- $pgDb := .Values.postgresql.auth.database }} + {{- $pgUser := .Values.postgresql.auth.username }} + {{- $pgPass := "" }} + {{- if (index $existingData "postgres-password") }} + {{- $pgPass = index $existingData "postgres-password" | b64dec }} + {{- else }} + {{- $pgPass = randAlphaNum 24 }} + {{- end }} + postgres-password: {{ $pgPass | b64enc | quote }} + DATABASE_URL: {{ printf "postgres://%s:%s@%s:5432/%s" $pgUser $pgPass $pgHost $pgDb | b64enc | quote }} + {{- else if .Values.externalPostgresql.url }} + DATABASE_URL: {{ .Values.externalPostgresql.url | b64enc | quote }} + {{- end }} + + {{- /* In-cluster Redis: compose REDIS_URL */}} + {{- if .Values.redis.enabled }} + {{- $redisHost := printf "%s-redis-master" .Release.Name }} + {{- $redisPass := "" }} + {{- if (index $existingData "redis-password") }} + {{- $redisPass = index $existingData "redis-password" | b64dec }} + {{- else }} + {{- $redisPass = randAlphaNum 24 }} + {{- end }} + redis-password: {{ $redisPass | b64enc | quote }} + REDIS_URL: {{ printf "redis://:%s@%s:6379" $redisPass $redisHost | b64enc | quote }} + {{- else if .Values.externalRedis.url }} + REDIS_URL: {{ .Values.externalRedis.url | b64enc | quote }} + {{- end }} + + {{- /* Typesense — pass through values (no subchart) */}} + {{- if .Values.typesense.url }} + TYPESENSE_URL: {{ .Values.typesense.url | b64enc | quote }} + {{- end }} + {{- if .Values.typesense.apiKey }} + TYPESENSE_API_KEY: {{ .Values.typesense.apiKey | b64enc | quote }} + {{- end }} + + {{- /* S3 creds */}} + {{- if .Values.s3.accessKey }} + BUZZ_S3_ACCESS_KEY: {{ .Values.s3.accessKey | b64enc | quote }} + {{- end }} + {{- if .Values.s3.secretKey }} + BUZZ_S3_SECRET_KEY: {{ .Values.s3.secretKey | b64enc | quote }} + {{- end }} +{{- end -}} diff --git a/deploy/charts/buzz/templates/service.yaml b/deploy/charts/buzz/templates/service.yaml new file mode 100644 index 000000000..88834bb53 --- /dev/null +++ b/deploy/charts/buzz/templates/service.yaml @@ -0,0 +1,19 @@ +{{- include "buzz.validate" . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "buzz.fullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "buzz.selectorLabels" . | nindent 4 }} + ports: + - { name: app, port: {{ .Values.service.port }}, targetPort: app, protocol: TCP } + - { name: health, port: {{ .Values.service.healthPort }}, targetPort: health, protocol: TCP } + - { name: metrics, port: {{ .Values.service.metricsPort }}, targetPort: metrics, protocol: TCP } diff --git a/deploy/charts/buzz/templates/serviceaccount.yaml b/deploy/charts/buzz/templates/serviceaccount.yaml new file mode 100644 index 000000000..60be80038 --- /dev/null +++ b/deploy/charts/buzz/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- include "buzz.validate" . -}} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "buzz.serviceAccountName" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/charts/buzz/tests/fixtures/ha-values.yaml b/deploy/charts/buzz/tests/fixtures/ha-values.yaml new file mode 100644 index 000000000..c1e014ae1 --- /dev/null +++ b/deploy/charts/buzz/tests/fixtures/ha-values.yaml @@ -0,0 +1,22 @@ +# HA shape: replicas=3 + Redis + RWX. Render-only check. +relayUrl: wss://buzz.example.com +ownerPubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" +replicaCount: 3 +secrets: + existingSecret: buzz-secrets +externalPostgresql: + url: "postgres://buzz:pw@postgres.example.com:5432/buzz" +externalRedis: + url: "redis://:pw@redis.example.com:6379" +typesense: + url: "https://typesense.example.com" +s3: + bucket: "buzz-media" +persistence: + git: + enabled: true + accessMode: ReadWriteMany + size: 50Gi +podDisruptionBudget: + enabled: true + minAvailable: 2 diff --git a/deploy/charts/buzz/tests/fixtures/production-existing-secret-values.yaml b/deploy/charts/buzz/tests/fixtures/production-existing-secret-values.yaml new file mode 100644 index 000000000..c875c84e9 --- /dev/null +++ b/deploy/charts/buzz/tests/fixtures/production-existing-secret-values.yaml @@ -0,0 +1,27 @@ +# Production / GitOps shape: external services, existingSecret. Renders only; +# `ct install` is not asked to satisfy the external services. +relayUrl: wss://buzz.example.com +ownerPubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" +secrets: + existingSecret: buzz-secrets +externalPostgresql: + url: "postgres://buzz:pw@postgres.example.com:5432/buzz" +externalRedis: + url: "redis://:pw@redis.example.com:6379" +typesense: + url: "https://typesense.example.com" +s3: + endpoint: "https://s3.us-east-1.amazonaws.com" + bucket: "buzz-media" +persistence: + git: + enabled: true + accessMode: ReadWriteMany + size: 50Gi +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" +podDisruptionBudget: + enabled: false diff --git a/deploy/charts/buzz/tests/networking_test.yaml b/deploy/charts/buzz/tests/networking_test.yaml new file mode 100644 index 000000000..c5ea969e0 --- /dev/null +++ b/deploy/charts/buzz/tests/networking_test.yaml @@ -0,0 +1,75 @@ +suite: networking +templates: + - templates/ingress.yaml + - templates/httproute.yaml + - templates/service.yaml +tests: + - it: Service exposes app/health/metrics ports + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - equal: + path: spec.ports[0].name + value: app + template: templates/service.yaml + - equal: + path: spec.ports[0].port + value: 3000 + template: templates/service.yaml + + - it: Ingress disabled by default + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - hasDocuments: + count: 0 + template: templates/ingress.yaml + - hasDocuments: + count: 0 + template: templates/httproute.yaml + + - it: Ingress renders with derived host when relayUrl provided + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + ingress.enabled: true + ingress.className: nginx + asserts: + - hasDocuments: + count: 1 + template: templates/ingress.yaml + - equal: + path: spec.ingressClassName + value: nginx + template: templates/ingress.yaml + + - it: HTTPRoute renders when enabled + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + httproute.enabled: true + httproute.parentRefs: + - name: my-gateway + namespace: gateway-system + asserts: + - hasDocuments: + count: 1 + template: templates/httproute.yaml + - equal: + path: kind + value: HTTPRoute + template: templates/httproute.yaml diff --git a/deploy/charts/buzz/tests/secrets_test.yaml b/deploy/charts/buzz/tests/secrets_test.yaml new file mode 100644 index 000000000..124565fd5 --- /dev/null +++ b/deploy/charts/buzz/tests/secrets_test.yaml @@ -0,0 +1,92 @@ +suite: secrets wiring +templates: + - templates/secret-chart.yaml + - templates/deployment.yaml +tests: + - it: chart-managed Secret is rendered when existingSecret is empty + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - hasDocuments: + count: 1 + template: templates/secret-chart.yaml + - equal: + path: kind + value: Secret + template: templates/secret-chart.yaml + - equal: + path: metadata.annotations["helm.sh/resource-policy"] + value: keep + template: templates/secret-chart.yaml + + - it: chart-managed Secret is NOT rendered when existingSecret is set + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + secrets.existingSecret: "buzz-secrets" + asserts: + - hasDocuments: + count: 0 + template: templates/secret-chart.yaml + + - it: Deployment env points BUZZ_RELAY_PRIVATE_KEY at existingSecret when set + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + secrets.existingSecret: "buzz-secrets" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BUZZ_RELAY_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: buzz-secrets + key: BUZZ_RELAY_PRIVATE_KEY + optional: true + template: templates/deployment.yaml + + - it: RELAY_OWNER_PUBKEY env is set (not BUZZ_RELAY_OWNER_PUBKEY) + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: RELAY_OWNER_PUBKEY + value: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + template: templates/deployment.yaml + - notContains: + path: spec.template.spec.containers[0].env + content: + name: BUZZ_RELAY_OWNER_PUBKEY + template: templates/deployment.yaml + + - it: BUZZ_AUTO_MIGRATE defaults to "true" + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BUZZ_AUTO_MIGRATE + value: "true" + template: templates/deployment.yaml diff --git a/deploy/charts/buzz/tests/validation_test.yaml b/deploy/charts/buzz/tests/validation_test.yaml new file mode 100644 index 000000000..cae57da92 --- /dev/null +++ b/deploy/charts/buzz/tests/validation_test.yaml @@ -0,0 +1,123 @@ +suite: validation +templates: + - templates/deployment.yaml + - templates/serviceaccount.yaml +tests: + - it: fails when relayUrl is missing + set: + relayUrl: "" + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - failedTemplate: + errorMessage: "relayUrl is required: set --set relayUrl=wss://your.domain" + + - it: fails when ownerPubkey is missing and requireRelayMembership is true + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - failedTemplate: {} + + - it: fails when ownerPubkey is not 64 lowercase hex + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "NOTAHEX" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - failedTemplate: {} + + - it: fails when replicaCount>1 without Redis + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + replicaCount: 3 + asserts: + - failedTemplate: + errorPattern: "replicaCount=3 requires Redis" + + - it: fails when replicaCount>1 with RWO git PVC + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + typesense.url: http://ts:8108 + typesense.apiKey: k + replicaCount: 3 + persistence.git.accessMode: ReadWriteOnce + asserts: + - failedTemplate: + errorPattern: "requires persistence.git.accessMode=ReadWriteMany" + + - it: fails when ingress and httproute both enabled + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + ingress.enabled: true + httproute.enabled: true + asserts: + - failedTemplate: + errorPattern: "cannot both be true" + + - it: fails when Postgres source is missing + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - failedTemplate: + errorPattern: "Postgres source missing" + + - it: fails when Typesense source is missing + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + asserts: + - failedTemplate: + errorPattern: "Typesense source missing" + + - it: renders cleanly in production profile (external pg/redis/typesense) + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - hasDocuments: + count: 2 + + - it: renders HA cleanly with replicaCount=3 + RWX + Redis + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + typesense.url: http://ts:8108 + typesense.apiKey: k + replicaCount: 3 + persistence.git.accessMode: ReadWriteMany + asserts: + - hasDocuments: + count: 2 + - equal: + path: spec.replicas + value: 3 + template: templates/deployment.yaml diff --git a/deploy/charts/buzz/values.schema.json b/deploy/charts/buzz/values.schema.json new file mode 100644 index 000000000..eb4e8142a --- /dev/null +++ b/deploy/charts/buzz/values.schema.json @@ -0,0 +1,224 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Buzz Helm chart values", + "description": "Schema for values.yaml. Catches misconfiguration at `helm install` time before _validate.tpl runtime fails. Both layers are intentional: schema rejects malformed inputs; templates reject inconsistent combinations.", + "type": "object", + "additionalProperties": true, + "properties": { + "quickstart": { + "type": "boolean", + "description": "Master toggle for evaluation profile (enables postgresql + redis subcharts and chart-side autogen). Not GitOps-safe." + }, + "image": { + "type": "object", + "additionalProperties": false, + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }, + "pullSecrets": { + "type": "array", + "items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } } + } + }, + "required": ["repository", "pullPolicy"] + }, + "replicaCount": { + "type": "integer", + "minimum": 1, + "description": "Replica count for the relay Deployment. replicaCount > 1 requires Redis (for buzz-pubsub) and ReadWriteMany git storage — enforced by _validate.tpl." + }, + "relayUrl": { + "type": "string", + "pattern": "^(wss?://.+)?$", + "description": "Public wss:// URL clients connect to. Required (validated by _validate.tpl). Drives RELAY_URL, default mediaBaseUrl, default ingress host." + }, + "mediaBaseUrl": { + "type": "string", + "pattern": "^(https?://.+)?$" + }, + "ownerPubkey": { + "type": "string", + "pattern": "^([0-9a-f]{64})?$", + "description": "64-char lowercase hex Nostr pubkey of the relay operator. Required when relay.requireRelayMembership=true." + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "properties": { + "existingSecret": { "type": "string", "description": "Name of an externally managed Secret. Production / GitOps path." }, + "relayPrivateKey": { "type": "string" }, + "gitHookHmacSecret": { "type": "string" } + } + }, + "relay": { + "type": "object", + "additionalProperties": true, + "properties": { + "bindAddr": { "type": "string", "minLength": 1 }, + "maxConnections": { "type": "integer", "minimum": 1 }, + "maxConcurrentHandlers": { "type": "integer", "minimum": 1 }, + "sendBuffer": { "type": "integer", "minimum": 1 }, + "requireAuthToken": { "type": "boolean" }, + "requireRelayMembership": { "type": "boolean" }, + "allowNipOaAuth": { "type": "boolean" }, + "pubkeyAllowlist": { "type": "boolean" }, + "corsOrigins": { + "type": "array", + "items": { "type": "string" } + }, + "ephemeralTtlOverride": { "type": "integer", "minimum": 0 } + } + }, + "service": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "healthPort": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "metricsPort": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "serviceAccount": { + "type": "object", + "additionalProperties": false, + "properties": { + "create": { "type": "boolean" }, + "name": { "type": "string" }, + "annotations": { "type": "object" } + } + }, + "podDisruptionBudget": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "minAvailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] }, + "maxUnavailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] } + } + }, + "ingress": { + "type": "object", + "additionalProperties": true, + "properties": { + "enabled": { "type": "boolean" }, + "className": { "type": "string" }, + "annotations": { "type": "object" }, + "hosts": { "type": "array" }, + "tls": { "type": "array" } + } + }, + "httproute": { + "type": "object", + "additionalProperties": true, + "properties": { + "enabled": { "type": "boolean" }, + "parentRefs": { "type": "array" }, + "hostnames": { "type": "array", "items": { "type": "string" } }, + "rules": { "type": "array" } + } + }, + "persistence": { + "type": "object", + "additionalProperties": false, + "properties": { + "git": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "mountPath": { "type": "string", "minLength": 1 }, + "storageClass": { "type": "string" }, + "accessMode": { "type": "string", "enum": ["ReadWriteOnce", "ReadWriteMany", "ReadOnlyMany", "ReadWriteOncePod"] }, + "size": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?(E|P|T|G|M|K|Ei|Pi|Ti|Gi|Mi|Ki)?$" }, + "annotations": { "type": "object" }, + "existingClaim": { "type": "string" } + } + } + } + }, + "postgresql": { + "type": "object", + "additionalProperties": true, + "properties": { "enabled": { "type": "boolean" } } + }, + "externalPostgresql": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { "type": "string", "pattern": "^(postgres(ql)?://.+)?$" } + } + }, + "redis": { + "type": "object", + "additionalProperties": true, + "properties": { "enabled": { "type": "boolean" } } + }, + "externalRedis": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { "type": "string", "pattern": "^(rediss?://.+)?$" } + } + }, + "typesense": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { "type": "string", "pattern": "^(https?://.+)?$" }, + "apiKey": { "type": "string" } + } + }, + "s3": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { "type": "string", "pattern": "^(https?://.+)?$" }, + "bucket": { "type": "string", "minLength": 1 }, + "accessKey": { "type": "string" }, + "secretKey": { "type": "string" } + } + }, + "git": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxPackBytes": { "type": "integer", "minimum": 1 }, + "maxReposPerPubkey": { "type": "integer", "minimum": 1 }, + "maxConcurrentOps": { "type": "integer", "minimum": 1 } + } + }, + "migrate": { + "type": "object", + "additionalProperties": false, + "properties": { + "autoMigrate": { "type": "boolean" }, + "preUpgradeJob": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "resources": { "type": "object" }, + "backoffLimit": { "type": "integer", "minimum": 0 }, + "activeDeadlineSeconds": { "type": "integer", "minimum": 1 } + } + } + } + }, + "serviceMonitor": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "namespace": { "type": "string" }, + "interval": { "type": "string" }, + "scrapeTimeout": { "type": "string" }, + "labels": { "type": "object" } + } + }, + "extraManifests": { + "type": "array" + } + } +} diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml new file mode 100644 index 000000000..43193d611 --- /dev/null +++ b/deploy/charts/buzz/values.yaml @@ -0,0 +1,222 @@ +# Default values for buzz. +# +# Two supported tiers: +# +# PRODUCTION (default) — external Postgres/Redis/Typesense/S3, existingSecret +# refs everywhere, no chart-side autogeneration, GitOps-safe (ArgoCD/Flux). +# HA-ready: replicaCount >= 2 (requires Redis and RWX storage for git). +# +# QUICKSTART (`--set quickstart=true`) — enables in-cluster Postgres + Redis +# subcharts, chart auto-generates relay secrets via the `lookup` pattern +# (NOT GitOps-safe — see README), single replica, evaluation only. +# +# See examples/argocd-app.yaml and examples/flux-helmrelease.yaml for the +# canonical GitOps configurations. + +# Master toggle for the evaluation profile. Equivalent to setting +# postgresql.enabled=true and redis.enabled=true. +quickstart: false + +# ── Image ──────────────────────────────────────────────────────────────────── +image: + repository: ghcr.io/block/buzz + tag: "" # empty → .Chart.AppVersion + pullPolicy: IfNotPresent + pullSecrets: [] + +# ── Topology ──────────────────────────────────────────────────────────────── +# replicaCount > 1 hard-requires: +# - Redis for buzz-pubsub (in-cluster or external) +# - ReadWriteMany storage for the git PVC (or shared FS via existingClaim) +replicaCount: 1 + +# ── Public URL ─────────────────────────────────────────────────────────────── +# Required. The wss:// URL clients use to connect. Drives: +# - RELAY_URL env (relay-side) +# - Default mediaBaseUrl (https:///media) +# - Default ingress host +relayUrl: "" +mediaBaseUrl: "" + +# ── Owner ──────────────────────────────────────────────────────────────────── +# 64-char lowercase hex Nostr pubkey of the relay operator. Required when +# relay.requireRelayMembership=true (the production default). +ownerPubkey: "" + +# ── Chart-managed secrets ──────────────────────────────────────────────────── +# Production / GitOps path: create a Secret out-of-band with these keys and +# point `secrets.existingSecret` at it. Any key omitted from the existing +# Secret falls back to chart-side autogen (only effective at first install). +# +# Expected keys (all optional unless required by relay config): +# BUZZ_RELAY_PRIVATE_KEY — 64-char hex; relay identity (rotation = identity change) +# BUZZ_GIT_HOOK_HMAC_SECRET — 32+ chars; required when replicaCount > 1 +# DATABASE_URL — full Postgres URL (preferred over externalPostgresql.url) +# REDIS_URL — full Redis URL with auth +# TYPESENSE_URL — Typesense base URL +# TYPESENSE_API_KEY — Typesense API key +# BUZZ_S3_ACCESS_KEY — S3 access key +# BUZZ_S3_SECRET_KEY — S3 secret key +secrets: + existingSecret: "" + # Inline overrides (NOT recommended for production; they land in values). + relayPrivateKey: "" + gitHookHmacSecret: "" + +# ── Relay behavior ─────────────────────────────────────────────────────────── +relay: + bindAddr: "0.0.0.0:3000" + maxConnections: 10000 + maxConcurrentHandlers: 1024 + sendBuffer: 1000 + requireAuthToken: true + requireRelayMembership: true + allowNipOaAuth: true + pubkeyAllowlist: false + corsOrigins: [] + ephemeralTtlOverride: 0 + + livenessProbe: + httpGet: { path: /_liveness, port: health } + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: { path: /_readiness, port: health } + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + httpGet: { path: /_liveness, port: health } + failureThreshold: 60 + periodSeconds: 2 + + resources: + requests: { cpu: "500m", memory: "512Mi" } + limits: { cpu: "2", memory: "2Gi" } + + podAnnotations: {} + podLabels: {} + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: { type: RuntimeDefault } + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: { drop: [ALL] } + readOnlyRootFilesystem: false # git writes need a writable repo path + terminationGracePeriodSeconds: 60 + + extraEnv: [] + extraEnvFrom: [] + +# ── Service ────────────────────────────────────────────────────────────────── +service: + type: ClusterIP + port: 3000 + healthPort: 8080 + metricsPort: 9102 + annotations: {} + +serviceAccount: + create: true + name: "" + annotations: {} + +podDisruptionBudget: + enabled: true + minAvailable: 1 + maxUnavailable: "" + +# ── Ingress (classic) ──────────────────────────────────────────────────────── +# Mutually exclusive with httproute.enabled. +ingress: + enabled: false + className: "" + annotations: {} + hosts: [] # empty → derived from relayUrl + tls: [] # [{hosts: [...], secretName: "..."}] + +# ── Gateway API (HTTPRoute) ────────────────────────────────────────────────── +httproute: + enabled: false + parentRefs: [] + hostnames: [] + rules: [] # empty → default match-all → service + +# ── Git on-disk state ──────────────────────────────────────────────────────── +persistence: + git: + enabled: true + mountPath: /var/lib/buzz/git + storageClass: "" + accessMode: ReadWriteOnce # MUST be ReadWriteMany if replicaCount > 1 + size: 10Gi + annotations: {} + existingClaim: "" + +# ── Postgres ───────────────────────────────────────────────────────────────── +postgresql: + enabled: false + auth: + database: buzz + username: buzz + primary: + persistence: { enabled: true, size: 10Gi } +externalPostgresql: + url: "" # postgres://user:pass@host:5432/db + +# ── Redis ──────────────────────────────────────────────────────────────────── +redis: + enabled: false + master: + persistence: { enabled: true, size: 4Gi } +externalRedis: + url: "" # redis://:pass@host:6379 + +# ── Typesense ──────────────────────────────────────────────────────────────── +typesense: + url: "" + apiKey: "" + +# ── S3 / object storage (media) ────────────────────────────────────────────── +s3: + endpoint: "" + bucket: "buzz-media" + accessKey: "" + secretKey: "" + +# ── Git server config ──────────────────────────────────────────────────────── +git: + maxPackBytes: 524288000 # 500 MiB + maxReposPerPubkey: 100 + maxConcurrentOps: 20 + +# ── Migrations ─────────────────────────────────────────────────────────────── +# Relay runs sqlx migrations at startup via BUZZ_AUTO_MIGRATE=true. +migrate: + autoMigrate: true + preUpgradeJob: + enabled: false + resources: {} + backoffLimit: 3 + activeDeadlineSeconds: 600 + +# ── Monitoring ─────────────────────────────────────────────────────────────── +serviceMonitor: + enabled: false + namespace: "" + interval: 30s + scrapeTimeout: 10s + labels: {} + +# ── Free-form extra manifests ──────────────────────────────────────────────── +extraManifests: [] From 52f9a6e59fb4a0911b6a07dcadcfd28a381c092d Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Thu, 11 Jun 2026 16:01:55 -0400 Subject: [PATCH 2/8] docs(helm): warn operators when BUZZ_AUTO_MIGRATE is disabled Per @Max's review on PR #990: if an operator sets migrate.autoMigrate=false, the chart does not run migrations. Readiness only proves DB reachability, not schema freshness, so a pod can come up healthy against an unmigrated schema and fail under load. - NOTES.txt: add Degradation warning conditional on .Values.migrate.autoMigrate - README.md: sharpen the upgrade section to put operator responsibility front and center Verified: helm install --dry-run with migrate.autoMigrate=false renders the warning; default (true) stays silent. helm lint clean. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- deploy/charts/buzz/README.md | 2 +- deploy/charts/buzz/templates/NOTES.txt | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/deploy/charts/buzz/README.md b/deploy/charts/buzz/README.md index ff6c1b032..8f692e976 100644 --- a/deploy/charts/buzz/README.md +++ b/deploy/charts/buzz/README.md @@ -61,7 +61,7 @@ The chart **template-fails** if either invariant is broken. No silent degradatio Schema migrations are embedded in the relay binary via `sqlx::migrate!` and run at startup, gated by `BUZZ_AUTO_MIGRATE` (default `true`). Multiple replicas race-safely behind a Postgres advisory lock. `helm upgrade` is the entire upgrade procedure. -If you prefer decoupling migrations from serving, set `migrate.autoMigrate=false` and run `buzz-admin migrate` (separate Pod / one-shot Job) before upgrading. A pre-upgrade Helm Job for this is on the chart roadmap; the values knob `migrate.preUpgradeJob.enabled` is reserved. +If you prefer decoupling migrations from serving, set `migrate.autoMigrate=false`. **In that mode the chart does not run migrations for you** — you own running `buzz-admin migrate` (separate Pod / one-shot Job) against the database before every `helm install` / `helm upgrade`. Readiness probes only verify DB connectivity, not schema freshness, so a pod will appear healthy against an unmigrated schema and fail under load. A pre-upgrade Helm Job for this is on the chart roadmap; the values knob `migrate.preUpgradeJob.enabled` is reserved. ## Backups diff --git a/deploy/charts/buzz/templates/NOTES.txt b/deploy/charts/buzz/templates/NOTES.txt index 8f963ae85..0957dc2d8 100644 --- a/deploy/charts/buzz/templates/NOTES.txt +++ b/deploy/charts/buzz/templates/NOTES.txt @@ -61,6 +61,12 @@ {{- if not .Values.relay.requireRelayMembership }} ⚠ relay.requireRelayMembership=false — relay is OPEN. Anyone can publish. {{- end }} +{{- if not .Values.migrate.autoMigrate }} + ⚠ migrate.autoMigrate=false — relay startup will NOT run sqlx migrations. + You must run `buzz-admin migrate` against the database before every + `helm install` / `helm upgrade`, or pods will start against an unmigrated + schema. Readiness probes only verify DB connectivity, not schema freshness. +{{- end }} {{- if and (gt (.Values.replicaCount | int) 1) (eq .Values.persistence.git.accessMode "ReadWriteOnce") }} ⚠ replicaCount > 1 with ReadWriteOnce git PVC will fail at template time (this message should never appear — file a bug). From b9d4fc52d2454098db682242acb6a8237978f336 Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Thu, 11 Jun 2026 16:05:19 -0400 Subject: [PATCH 3/8] docs(helm): close Dawn's two non-blocking nits on PR #990 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add examples/ingress-cert-manager.yaml — a two-document file containing both a chart values fragment (ingress block with cert-manager annotations for the Let's Encrypt HTTP-01 flow) and a cluster-scoped ClusterIssuer manifest applied with kubectl. Helm reads only the first document; the second is for cluster operators. Closes the rubric-4 'TLS by default' gap without making cert-manager a chart dependency. 2. NOTES.txt: warn when secrets.relayPrivateKey or secrets.gitHookHmacSecret are set inline. Both are labeled 'NOT recommended' in values.yaml comments; a render-time warning makes the operator see it. Includes pointer to examples/secret-sample.yaml for the canonical fix. Verified: helm install --dry-run renders the cert-manager annotations correctly; inline-secret warning fires for one or both keys with proper comma joining; default install stays silent on both. helm lint clean. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .../buzz/examples/ingress-cert-manager.yaml | 69 +++++++++++++++++++ deploy/charts/buzz/templates/NOTES.txt | 7 ++ 2 files changed, 76 insertions(+) create mode 100644 deploy/charts/buzz/examples/ingress-cert-manager.yaml diff --git a/deploy/charts/buzz/examples/ingress-cert-manager.yaml b/deploy/charts/buzz/examples/ingress-cert-manager.yaml new file mode 100644 index 000000000..e2df24796 --- /dev/null +++ b/deploy/charts/buzz/examples/ingress-cert-manager.yaml @@ -0,0 +1,69 @@ +# Ingress + automatic TLS via cert-manager (Let's Encrypt HTTP-01). +# +# This file is BOTH: +# 1. A values fragment for the chart (the `ingress:` block) — pass with `-f`. +# 2. A ClusterIssuer manifest at the bottom — apply ONCE per cluster with +# `kubectl apply -f`. The chart does not manage it. +# +# Helm's value-file parser reads ONLY the first YAML document; the second +# document (the ClusterIssuer) is ignored by Helm. That is intentional — the +# ClusterIssuer is cluster-scoped and outlives any single release. +# +# Prerequisite: cert-manager installed in the cluster. The chart does not +# depend on it — that is a cluster operator decision. +# +# helm install cert-manager cert-manager \ +# --repo https://charts.jetstack.io \ +# --namespace cert-manager --create-namespace \ +# --set crds.enabled=true +# +# Apply this file in two passes: +# +# # 1. Install the cluster-scoped ClusterIssuer (idempotent): +# kubectl apply -f deploy/charts/buzz/examples/ingress-cert-manager.yaml +# +# # 2. Install/upgrade the chart with the values fragment: +# helm upgrade --install buzz ./deploy/charts/buzz \ +# -f values-production.yaml \ +# -f deploy/charts/buzz/examples/ingress-cert-manager.yaml \ +# --set relayUrl=wss://buzz.example.com +# +# Why HTTP-01: works for any public-DNS host without DNS-API credentials. +# Switch to DNS-01 if your relay is on a private/split-DNS host or you want +# wildcard certs. + +# ── Values fragment (Helm reads this document) ─────────────────────────────── +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + # Long-lived WebSocket connections — generous timeouts. + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + hosts: + - host: buzz.example.com # replace; must match relayUrl host + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - buzz.example.com # replace + secretName: buzz-tls # cert-manager creates this Secret + +--- +# ── ClusterIssuer (kubectl apply, NOT consumed by Helm) ────────────────────── +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: ops@example.com # replace + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - http01: + ingress: + class: nginx # match your ingress controller's ingressClass diff --git a/deploy/charts/buzz/templates/NOTES.txt b/deploy/charts/buzz/templates/NOTES.txt index 0957dc2d8..d9f54a938 100644 --- a/deploy/charts/buzz/templates/NOTES.txt +++ b/deploy/charts/buzz/templates/NOTES.txt @@ -67,6 +67,13 @@ `helm install` / `helm upgrade`, or pods will start against an unmigrated schema. Readiness probes only verify DB connectivity, not schema freshness. {{- end }} +{{- if or .Values.secrets.relayPrivateKey .Values.secrets.gitHookHmacSecret }} + ⚠ Inline secret values are set in values.yaml + ({{ if .Values.secrets.relayPrivateKey }}secrets.relayPrivateKey{{ end }}{{ if and .Values.secrets.relayPrivateKey .Values.secrets.gitHookHmacSecret }}, {{ end }}{{ if .Values.secrets.gitHookHmacSecret }}secrets.gitHookHmacSecret{{ end }}). + Inline overrides leak secrets into git history and CI logs. Move them to a + Kubernetes Secret and reference it via secrets.existingSecret — see + examples/secret-sample.yaml. +{{- end }} {{- if and (gt (.Values.replicaCount | int) 1) (eq .Values.persistence.git.accessMode "ReadWriteOnce") }} ⚠ replicaCount > 1 with ReadWriteOnce git PVC will fail at template time (this message should never appear — file a bug). From e352f218b807ac3b5ec824f941100bf28b36c011 Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Thu, 11 Jun 2026 16:31:50 -0400 Subject: [PATCH 4/8] fix(helm): satisfy ct lint + zizmor/Semgrep on PR #990 values.yaml: expand 9 flow-style mappings (livenessProbe/readinessProbe/ startupProbe httpGet, resources requests/limits, securityContext seccompProfile, containerSecurityContext capabilities, postgresql and redis primary.persistence) to block style. The chart-testing default yamllint config (lintconf.yaml) flags any spaces inside flow braces; empty {} and [] forms are kept where they're idiomatic (podAnnotations, nodeSelector, etc.) since those don't have inner-brace spacing. .github/workflows/helm-chart.yml: SHA-pin the five third-party action refs flagged by zizmor (unpinned-uses) and Semgrep: azure/setup-helm@v4 -> 1a275c3b... # v4.3.1 (x2) helm/chart-testing-action@v2.7.0 -> 0d28d314... # v2.7.0 (x2) helm/kind-action@v1.10.0 -> 0025e74a... # v1.10.0 Matches the pinning pattern Sami established in .github/workflows/ docker.yml. actions/checkout and actions/setup-python were not flagged (zizmor allowlists first-party actions/* refs) so left as-is. Verified locally: ct.yaml + helm dependency build + helm template against ci/quickstart, tests/fixtures/ha, and tests/fixtures/ production-existing-secret all render clean. helm lint clean. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .github/workflows/helm-chart.yml | 10 +++++----- deploy/charts/buzz/values.yaml | 34 +++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml index 1ca421736..4d6d47617 100644 --- a/.github/workflows/helm-chart.yml +++ b/.github/workflows/helm-chart.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: v3.16.4 @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - name: Set up chart-testing - uses: helm/chart-testing-action@v2.7.0 + uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 - name: Build chart dependencies run: helm dependency build deploy/charts/buzz @@ -72,7 +72,7 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: v3.16.4 @@ -82,10 +82,10 @@ jobs: python-version: "3.12" - name: Set up chart-testing - uses: helm/chart-testing-action@v2.7.0 + uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 - name: Create kind cluster - uses: helm/kind-action@v1.10.0 + uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 with: version: v0.24.0 node_image: kindest/node:v1.31.0 diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml index 43193d611..fb2fb95ec 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -77,25 +77,35 @@ relay: ephemeralTtlOverride: 0 livenessProbe: - httpGet: { path: /_liveness, port: health } + httpGet: + path: /_liveness + port: health initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: - httpGet: { path: /_readiness, port: health } + httpGet: + path: /_readiness + port: health initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 startupProbe: - httpGet: { path: /_liveness, port: health } + httpGet: + path: /_liveness + port: health failureThreshold: 60 periodSeconds: 2 resources: - requests: { cpu: "500m", memory: "512Mi" } - limits: { cpu: "2", memory: "2Gi" } + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "2" + memory: "2Gi" podAnnotations: {} podLabels: {} @@ -108,10 +118,12 @@ relay: runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 - seccompProfile: { type: RuntimeDefault } + seccompProfile: + type: RuntimeDefault containerSecurityContext: allowPrivilegeEscalation: false - capabilities: { drop: [ALL] } + capabilities: + drop: [ALL] readOnlyRootFilesystem: false # git writes need a writable repo path terminationGracePeriodSeconds: 60 @@ -170,7 +182,9 @@ postgresql: database: buzz username: buzz primary: - persistence: { enabled: true, size: 10Gi } + persistence: + enabled: true + size: 10Gi externalPostgresql: url: "" # postgres://user:pass@host:5432/db @@ -178,7 +192,9 @@ externalPostgresql: redis: enabled: false master: - persistence: { enabled: true, size: 4Gi } + persistence: + enabled: true + size: 4Gi externalRedis: url: "" # redis://:pass@host:6379 From d293ed4e8122a7043fc296f5cb3ba1f8f4a7bdfa Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Thu, 11 Jun 2026 16:43:15 -0400 Subject: [PATCH 5/8] test(helm): fix failedTemplate scope + split positive renders into own suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit helm-unittest 0.8.2 runs `failedTemplate` asserts per-template in the suite's `templates:` list. With multiple templates listed and `fail` firing from only one (e.g. serviceaccount.yaml's `include buzz.validate`), the assertion sees "No failed document" for the other-template scope and the test fails despite the overall render failing. Two fixes: 1. Scope `validation_test.yaml` to `templates/deployment.yaml` only. That's the entry point with `include "buzz.validate"`, sufficient to exercise every guard. Side benefit: positive renders that asserted `hasDocuments: count: 2` had the wrong number anyway (production profile renders 5 docs, not 2). 2. New `render_test.yaml` covers positive renders with the full template list — needed because deployment.yaml's checksum annotation does `include (print $.Template.BasePath "/secret-chart.yaml")`, which only resolves if secret-chart.yaml is loaded by the suite. Asserts target specific fields with per-assert `template:` instead of fragile document counts. Also adjusts the "ownerPubkey is not 64 lowercase hex" test to match the schema-validation error pattern, since values.schema.json's regex runs before template rendering and is the actual gate. Local: `helm unittest` → 19/19 passing across 4 suites. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- deploy/charts/buzz/tests/render_test.yaml | 51 +++++++++++++++++++ deploy/charts/buzz/tests/validation_test.yaml | 39 ++------------ 2 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 deploy/charts/buzz/tests/render_test.yaml diff --git a/deploy/charts/buzz/tests/render_test.yaml b/deploy/charts/buzz/tests/render_test.yaml new file mode 100644 index 000000000..42758b075 --- /dev/null +++ b/deploy/charts/buzz/tests/render_test.yaml @@ -0,0 +1,51 @@ +suite: production render +# Multi-template scope: needed so $.Template.BasePath lookups in deployment.yaml +# (e.g. checksum/secret include of secret-chart.yaml) resolve at render time. +templates: + - templates/deployment.yaml + - templates/secret-chart.yaml + - templates/serviceaccount.yaml + - templates/service.yaml + - templates/pvc-git.yaml +tests: + - it: renders cleanly in production profile (external pg/redis/typesense) + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - equal: + path: kind + value: Deployment + template: templates/deployment.yaml + - equal: + path: kind + value: ServiceAccount + template: templates/serviceaccount.yaml + - equal: + path: kind + value: Service + template: templates/service.yaml + + - it: renders HA cleanly with replicaCount=3 + RWX + Redis + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + typesense.url: http://ts:8108 + typesense.apiKey: k + replicaCount: 3 + persistence.git.accessMode: ReadWriteMany + asserts: + - equal: + path: spec.replicas + value: 3 + template: templates/deployment.yaml + - equal: + path: spec.accessModes[0] + value: ReadWriteMany + template: templates/pvc-git.yaml diff --git a/deploy/charts/buzz/tests/validation_test.yaml b/deploy/charts/buzz/tests/validation_test.yaml index cae57da92..89302be8b 100644 --- a/deploy/charts/buzz/tests/validation_test.yaml +++ b/deploy/charts/buzz/tests/validation_test.yaml @@ -1,7 +1,6 @@ suite: validation templates: - templates/deployment.yaml - - templates/serviceaccount.yaml tests: - it: fails when relayUrl is missing set: @@ -22,9 +21,10 @@ tests: typesense.url: http://ts:8108 typesense.apiKey: k asserts: - - failedTemplate: {} + - failedTemplate: + errorPattern: "ownerPubkey is required when relay.requireRelayMembership=true" - - it: fails when ownerPubkey is not 64 lowercase hex + - it: fails when ownerPubkey is not 64 lowercase hex (schema-level) set: relayUrl: wss://buzz.example.com ownerPubkey: "NOTAHEX" @@ -32,7 +32,8 @@ tests: typesense.url: http://ts:8108 typesense.apiKey: k asserts: - - failedTemplate: {} + - failedTemplate: + errorPattern: "ownerPubkey: Does not match pattern" - it: fails when replicaCount>1 without Redis set: @@ -91,33 +92,3 @@ tests: asserts: - failedTemplate: errorPattern: "Typesense source missing" - - - it: renders cleanly in production profile (external pg/redis/typesense) - set: - relayUrl: wss://buzz.example.com - ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" - externalPostgresql.url: postgres://u:p@h:5432/d - externalRedis.url: redis://h:6379 - typesense.url: http://ts:8108 - typesense.apiKey: k - asserts: - - hasDocuments: - count: 2 - - - it: renders HA cleanly with replicaCount=3 + RWX + Redis - set: - relayUrl: wss://buzz.example.com - ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" - externalPostgresql.url: postgres://u:p@h:5432/d - externalRedis.url: redis://h:6379 - typesense.url: http://ts:8108 - typesense.apiKey: k - replicaCount: 3 - persistence.git.accessMode: ReadWriteMany - asserts: - - hasDocuments: - count: 2 - - equal: - path: spec.replicas - value: 3 - template: templates/deployment.yaml From 4cf18334c2a15b70c58520ee86a15a1e12d48d59 Mon Sep 17 00:00:00 2001 From: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Mon, 15 Jun 2026 15:56:20 -0400 Subject: [PATCH 6/8] fix(helm): wire quickstart subchart credentials to the chart-managed Secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quickstart profile composed DATABASE_URL/REDIS_URL in the chart-managed Secret with chart-generated passwords, but the CloudPirates postgres/redis subcharts each generated their *own* independent passwords and bound their Services at names the URLs didn't match. Result: a default `helm install --set quickstart=true` brought pg/redis up but the relay could never authenticate (password mismatch) or, for redis, never resolve the host (`-redis-master` Service does not exist in standalone mode). - Point postgresql.auth.existingSecret / redis.auth.existingSecret at the chart-managed Secret (`-relay`) with matching key names, so the servers initialize with the exact password the relay's URL embeds — one source of truth instead of two diverging randoms. - Fix the composed REDIS_URL host: standalone CloudPirates redis renders `-redis`, not `-redis-master`. - Correct two no-op persistence paths (redis.master.* / postgresql.primary.*) to the keys CloudPirates actually reads (redis.persistence / postgresql.persistence); the prior nesting was silently ignored. - Add a regression test asserting DATABASE_URL/REDIS_URL resolve to the real subchart Service hosts and never `-redis-master`. Verified live on a kind/docker-desktop cluster against the published ghcr.io/block/buzz:0.1.0 image: pg+redis 1/1, relay logs "Postgres connected", psql/redis-cli with the chart-secret passwords succeed (select 1 / PONG). The relay still CrashLoops on absent schema (relation "events" does not exist) because :0.1.0 predates the auto-migration code (#988) — connectivity is fixed; schema bootstrap lands with #988 + a re-cut image. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .../charts/buzz/templates/secret-chart.yaml | 2 +- deploy/charts/buzz/tests/secrets_test.yaml | 32 +++++++++++++++++++ deploy/charts/buzz/values.yaml | 26 ++++++++++----- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/deploy/charts/buzz/templates/secret-chart.yaml b/deploy/charts/buzz/templates/secret-chart.yaml index 6824575a4..f2db3f04d 100644 --- a/deploy/charts/buzz/templates/secret-chart.yaml +++ b/deploy/charts/buzz/templates/secret-chart.yaml @@ -60,7 +60,7 @@ data: {{- /* In-cluster Redis: compose REDIS_URL */}} {{- if .Values.redis.enabled }} - {{- $redisHost := printf "%s-redis-master" .Release.Name }} + {{- $redisHost := printf "%s-redis" .Release.Name }} {{- $redisPass := "" }} {{- if (index $existingData "redis-password") }} {{- $redisPass = index $existingData "redis-password" | b64dec }} diff --git a/deploy/charts/buzz/tests/secrets_test.yaml b/deploy/charts/buzz/tests/secrets_test.yaml index 124565fd5..23ed19670 100644 --- a/deploy/charts/buzz/tests/secrets_test.yaml +++ b/deploy/charts/buzz/tests/secrets_test.yaml @@ -90,3 +90,35 @@ tests: name: BUZZ_AUTO_MIGRATE value: "true" template: templates/deployment.yaml + + - it: quickstart composes DATABASE_URL/REDIS_URL at the actual subchart Service hosts + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + # Postgres Service is "-postgresql"; Redis (standalone) is + # "-redis" — NOT "-redis-master". A host mismatch here is the + # connection-refused class of bug this guards against. + - matchRegex: + path: data.DATABASE_URL + decodeBase64: true + pattern: "@rel-postgresql:5432/buzz$" + template: templates/secret-chart.yaml + - matchRegex: + path: data.REDIS_URL + decodeBase64: true + pattern: "@rel-redis:6379$" + template: templates/secret-chart.yaml + documentIndex: 0 + - notMatchRegex: + path: data.REDIS_URL + decodeBase64: true + pattern: "redis-master" + template: templates/secret-chart.yaml + documentIndex: 0 diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml index fb2fb95ec..5414fb203 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -176,25 +176,35 @@ persistence: existingClaim: "" # ── Postgres ───────────────────────────────────────────────────────────────── +# Eval-only CloudPirates subchart. The relay's DATABASE_URL is composed in the +# chart-managed Secret with a chart-generated password; auth.existingSecret +# points this subchart at that same Secret/key so server and client agree. postgresql: enabled: false auth: database: buzz username: buzz - primary: - persistence: - enabled: true - size: 10Gi + existingSecret: '{{ if contains "buzz" .Release.Name }}{{ .Release.Name }}-relay{{ else }}{{ .Release.Name }}-buzz-relay{{ end }}' + secretKeys: + adminPasswordKey: postgres-password + persistence: + enabled: true + size: 10Gi externalPostgresql: url: "" # postgres://user:pass@host:5432/db # ── Redis ──────────────────────────────────────────────────────────────────── +# Eval-only CloudPirates subchart (standalone). REDIS_URL is composed in the +# chart-managed Secret; auth.existingSecret points the subchart at that Secret +# so the server password matches the URL the relay dials. redis: enabled: false - master: - persistence: - enabled: true - size: 4Gi + auth: + existingSecret: '{{ if contains "buzz" .Release.Name }}{{ .Release.Name }}-relay{{ else }}{{ .Release.Name }}-buzz-relay{{ end }}' + existingSecretPasswordKey: redis-password + persistence: + enabled: true + size: 4Gi externalRedis: url: "" # redis://:pass@host:6379 From 8ffd5adbfe53de0e8793843cd92d7262b560f80c Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Tue, 16 Jun 2026 10:00:09 -0400 Subject: [PATCH 7/8] fix(helm): generate valid hex relay key in chart-managed Secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit randAlphaNum 64 emits letters g-z, which are neither hex nor bech32, so nostr::Keys::parse rejects the autogenerated BUZZ_RELAY_PRIVATE_KEY and the relay crashes at startup with "invalid BUZZ_RELAY_PRIVATE_KEY". Pipe through sha256sum to produce exactly 64 lowercase hex chars — a valid secp256k1 secret key. Add a unittest asserting the autogen key matches ^[0-9a-f]{64}$ so the bug can't regress. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- deploy/charts/buzz/templates/secret-chart.yaml | 2 +- deploy/charts/buzz/tests/secrets_test.yaml | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/deploy/charts/buzz/templates/secret-chart.yaml b/deploy/charts/buzz/templates/secret-chart.yaml index f2db3f04d..ae51224ab 100644 --- a/deploy/charts/buzz/templates/secret-chart.yaml +++ b/deploy/charts/buzz/templates/secret-chart.yaml @@ -29,7 +29,7 @@ data: {{- else if (index $existingData "BUZZ_RELAY_PRIVATE_KEY") }} BUZZ_RELAY_PRIVATE_KEY: {{ index $existingData "BUZZ_RELAY_PRIVATE_KEY" | quote }} {{- else }} - BUZZ_RELAY_PRIVATE_KEY: {{ randAlphaNum 64 | lower | b64enc | quote }} + BUZZ_RELAY_PRIVATE_KEY: {{ randAlphaNum 64 | sha256sum | b64enc | quote }} {{- end }} {{- /* Git hook HMAC (required when replicaCount > 1) */}} diff --git a/deploy/charts/buzz/tests/secrets_test.yaml b/deploy/charts/buzz/tests/secrets_test.yaml index 23ed19670..e7c10cabe 100644 --- a/deploy/charts/buzz/tests/secrets_test.yaml +++ b/deploy/charts/buzz/tests/secrets_test.yaml @@ -122,3 +122,12 @@ tests: pattern: "redis-master" template: templates/secret-chart.yaml documentIndex: 0 + # Autogenerated relay key MUST be a valid Nostr secret key: 64 lowercase + # hex chars. randAlphaNum produces letters g-z that fail nostr::Keys::parse + # and crash relay startup with "invalid BUZZ_RELAY_PRIVATE_KEY". + - matchRegex: + path: data.BUZZ_RELAY_PRIVATE_KEY + decodeBase64: true + pattern: "^[0-9a-f]{64}$" + template: templates/secret-chart.yaml + documentIndex: 0 From 0b646addae34c12387605e60995b5e7dabdf728c Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Tue, 16 Jun 2026 10:41:52 -0400 Subject: [PATCH 8/8] feat(helm): bundle MinIO + Typesense in quickstart, external in prod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quickstart profile now stands up MinIO and Typesense in-cluster alongside the existing Postgres + Redis subcharts, so the relay starts with zero external services and passes its A3 S3 conformance probe. The production profile leaves minio.enabled/typesense.enabled off and points s3.endpoint + typesense.url (or BUZZ_S3_* / TYPESENSE_URL via existingSecret) at managed services. - values.yaml: minio + typesense.enabled/image/persistence blocks; pinned MinIO image tags (minio:RELEASE.2025-09-07T16-13-09Z, mc:RELEASE.2025-08-13T08-35-41Z); corrected the misleading quickstart flag comments (it is an intent marker, not a behavior switch). - templates: quickstart-minio{,.init}.yaml + quickstart-typesense.yaml Deployments with bucket-create Job, existingSecret-conflict guards. - _validate.tpl: typesense guard now keys on .enabled; added symmetric S3-source guard (relay hard-fails its S3 probe without storage). - _helpers.tpl: buzz.relaySelectorLabels (selectorLabels + component:relay) scopes the relay Deployment/Service/PDB so they no longer match the bundled MinIO/Typesense pods. - NOTES.txt + README: document the bundled quickstart and external prod. - tests: 28/28 across 6 suites — bundled render, S3/TYPESENSE_URL composition, S3-missing + minio-existingSecret guards, selector isolation. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- deploy/charts/buzz/README.md | 31 +++-- deploy/charts/buzz/ci/quickstart-values.yaml | 10 +- deploy/charts/buzz/templates/NOTES.txt | 4 +- deploy/charts/buzz/templates/_helpers.tpl | 40 +++++++ deploy/charts/buzz/templates/_validate.tpl | 10 +- deploy/charts/buzz/templates/deployment.yaml | 9 +- deploy/charts/buzz/templates/pdb.yaml | 2 +- .../buzz/templates/quickstart-minio-init.yaml | 51 ++++++++ .../buzz/templates/quickstart-minio.yaml | 99 ++++++++++++++++ .../buzz/templates/quickstart-typesense.yaml | 96 +++++++++++++++ .../charts/buzz/templates/secret-chart.yaml | 40 ++++++- deploy/charts/buzz/templates/service.yaml | 2 +- deploy/charts/buzz/tests/networking_test.yaml | 12 ++ .../buzz/tests/quickstart_bundled_test.yaml | 112 ++++++++++++++++++ .../buzz/tests/quickstart_guards_test.yaml | 35 ++++++ deploy/charts/buzz/tests/render_test.yaml | 6 + deploy/charts/buzz/tests/secrets_test.yaml | 18 +++ deploy/charts/buzz/tests/validation_test.yaml | 11 ++ deploy/charts/buzz/values.schema.json | 29 ++++- deploy/charts/buzz/values.yaml | 37 +++++- 20 files changed, 625 insertions(+), 29 deletions(-) create mode 100644 deploy/charts/buzz/templates/quickstart-minio-init.yaml create mode 100644 deploy/charts/buzz/templates/quickstart-minio.yaml create mode 100644 deploy/charts/buzz/templates/quickstart-typesense.yaml create mode 100644 deploy/charts/buzz/tests/quickstart_bundled_test.yaml create mode 100644 deploy/charts/buzz/tests/quickstart_guards_test.yaml diff --git a/deploy/charts/buzz/README.md b/deploy/charts/buzz/README.md index 8f692e976..ab08200b2 100644 --- a/deploy/charts/buzz/README.md +++ b/deploy/charts/buzz/README.md @@ -7,7 +7,7 @@ This chart has two operating profiles selected by values: | Profile | When | What you get | |---|---|---| | **Production** (default) | Self-hosted multi-tenant, regulated, or GitOps-managed | External managed Postgres/Redis/Typesense/S3, `secrets.existingSecret:`, no chart-side autogen, HA-capable (`replicaCount ≥ 2`) | -| **Quickstart** (`--set quickstart=true`) | Eval, single-node, one-off demo | In-cluster Postgres + Redis subcharts ([CloudPirates](https://github.com/cloudpirates)), chart auto-generates relay secrets, single replica | +| **Quickstart** (eval) | Eval, single-node, one-off demo | In-cluster Postgres + Redis + MinIO + Typesense subcharts/Deployments, chart auto-generates relay + service secrets, single replica | ## Quickstart (eval only) @@ -17,13 +17,19 @@ helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.0 \ --set quickstart=true \ --set postgresql.enabled=true \ --set redis.enabled=true \ + --set minio.enabled=true \ + --set typesense.enabled=true \ --set relayUrl=wss://buzz.example.com \ - --set ownerPubkey=<64-char-hex-pubkey> \ - --set typesense.url=http://typesense.buzz.svc.cluster.local:8108 \ - --set typesense.apiKey= + --set ownerPubkey=<64-char-hex-pubkey> ``` -Quickstart still requires an externally managed Typesense in v1; bring up a minimal Typesense Pod/StatefulSet in your namespace, or set `typesense.url` and `typesense.apiKey` to a hosted instance. See the open question in `OPEN_QUESTIONS` at the bottom of this README. +This brings up **everything in-cluster** — Postgres, Redis, MinIO (with its +bucket created by a post-install Job), and Typesense — and composes the relay's +`BUZZ_S3_ENDPOINT` / `TYPESENSE_URL` plus autogenerated credentials +automatically. No external services required. The `quickstart=true` flag is an +intent marker surfaced in NOTES.txt; the bundled services are opted in via the +four `*.enabled` flags above (see `ci/quickstart-values.yaml` for the exact set +CI installs). Eval-only: every bundled service is a single replica with no HA. ## Production (GitOps) @@ -44,7 +50,7 @@ See: | `relayUrl` | Public `wss://` URL clients connect to | Always | | `ownerPubkey` | 64-char lowercase hex Nostr pubkey of the relay operator | When `relay.requireRelayMembership=true` (default) | | `secrets.existingSecret` | Name of pre-created Secret | Production / GitOps | -| `externalPostgresql.url` / `externalRedis.url` / `typesense.url` | External service URLs | When the matching subchart is disabled (default) | +| `externalPostgresql.url` / `externalRedis.url` / `typesense.url` / `s3.endpoint` | External service URLs | Production — when the matching bundled service is disabled (the default) | The chart fails at `helm install` / `helm template` time with a clear message if any of these are missing or malformed (see `templates/_validate.tpl`). @@ -75,8 +81,17 @@ Save these. Losing any of them is data loss. See NOTES.txt printed by `helm inst ## Honest limitations (v1) -- **Typesense has no in-chart subchart.** Bring your own Typesense; the chart wires it via `typesense.url` + `typesense.apiKey` (or `TYPESENSE_URL` / `TYPESENSE_API_KEY` in `existingSecret`). The roadmap depends on either an upstream community chart hitting our quality bar or a minimal in-chart StatefulSet behind a quickstart flag. -- **Minimal-mode is not yet supported.** The relay's `BUZZ_PUBSUB=local` / `BUZZ_SEARCH=pg` / filesystem media paths are upstream work in progress. Until then, "quickstart" still needs Typesense. +- **Bundled MinIO + Typesense are eval-only.** The quickstart profile runs an + in-cluster MinIO and Typesense (single replica, no HA, `lookup`-autogenerated + credentials) so the relay starts with zero external services. Production + leaves `minio.enabled` / `typesense.enabled` off and points `s3.endpoint` + + `typesense.url` (or `BUZZ_S3_*` / `TYPESENSE_URL` in `existingSecret`) at + managed S3-compatible storage and Typesense. The bundled Deployments are not + GitOps-safe and are not intended for production traffic. +- **Minimal-mode is not yet supported.** The relay's `BUZZ_PUBSUB=local` / + `BUZZ_SEARCH=pg` / filesystem media paths are upstream work in progress — + even quickstart currently stands up real Redis, Typesense, and S3 rather than + the relay's single-node fallbacks. - **OCI publish to GHCR + cosign signing** is a follow-up PR. For now, install the chart from source: `helm install buzz ./deploy/charts/buzz` after cloning the repo. ## Development diff --git a/deploy/charts/buzz/ci/quickstart-values.yaml b/deploy/charts/buzz/ci/quickstart-values.yaml index ea5815d29..71d4e76a4 100644 --- a/deploy/charts/buzz/ci/quickstart-values.yaml +++ b/deploy/charts/buzz/ci/quickstart-values.yaml @@ -1,16 +1,18 @@ # Quickstart / eval: subcharts on, autogen secrets, single replica. # This is the scenario `ct install` exercises against a kind cluster — it -# spins up postgres + redis in-cluster so the relay can actually start. +# spins up postgres + redis + minio + typesense in-cluster so the relay can +# actually start and pass its S3 conformance probe. quickstart: true postgresql: enabled: true redis: enabled: true +minio: + enabled: true +typesense: + enabled: true relayUrl: wss://buzz.test.local ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000001" -typesense: - url: "http://typesense.default.svc.cluster.local:8108" - apiKey: "ci-fake-key" relay: # Don't enforce membership in CI — we're testing the chart renders and the # Pod starts, not relay business logic. diff --git a/deploy/charts/buzz/templates/NOTES.txt b/deploy/charts/buzz/templates/NOTES.txt index d9f54a938..35a1080ad 100644 --- a/deploy/charts/buzz/templates/NOTES.txt +++ b/deploy/charts/buzz/templates/NOTES.txt @@ -26,10 +26,12 @@ ────────────────────────────────────────────────────────────────────────────── Profile ────────────────────────────────────────────────────────────────────────────── -{{ if or .Values.postgresql.enabled .Values.redis.enabled }} +{{ if or .Values.postgresql.enabled .Values.redis.enabled .Values.minio.enabled .Values.typesense.enabled }} ⚠ QUICKSTART / EVALUATION PROFILE {{ if .Values.postgresql.enabled }}- In-cluster Postgres subchart (CloudPirates){{ end }} {{ if .Values.redis.enabled }}- In-cluster Redis subchart (CloudPirates){{ end }} + {{ if .Values.minio.enabled }}- In-cluster MinIO (eval-only, single replica; bucket "{{ .Values.s3.bucket }}" created by post-install Job){{ end }} + {{ if .Values.typesense.enabled }}- In-cluster Typesense (eval-only, single replica){{ end }} - Chart auto-generates secrets via the `lookup` pattern. This is NOT GitOps-safe — secrets will silently rotate under ArgoCD/Flux. For production, see examples/argocd-app.yaml or examples/flux-helmrelease.yaml. diff --git a/deploy/charts/buzz/templates/_helpers.tpl b/deploy/charts/buzz/templates/_helpers.tpl index e6774252e..e7ac0ed16 100644 --- a/deploy/charts/buzz/templates/_helpers.tpl +++ b/deploy/charts/buzz/templates/_helpers.tpl @@ -36,6 +36,14 @@ app.kubernetes.io/name: {{ include "buzz.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} +{{/* Relay-specific selector: scopes the relay Deployment + Service so they do + not also match the quickstart MinIO/Typesense pods, which share the base + selectorLabels but carry their own component label. */}} +{{- define "buzz.relaySelectorLabels" -}} +{{ include "buzz.selectorLabels" . }} +app.kubernetes.io/component: relay +{{- end -}} + {{- define "buzz.serviceAccountName" -}} {{- if .Values.serviceAccount.create -}} {{- default (include "buzz.fullname" .) .Values.serviceAccount.name -}} @@ -84,3 +92,35 @@ secrets.existingSecret, use that. Otherwise use the chart-managed one. {{- printf "https://%s/media" (include "buzz.relayHost" .) -}} {{- end -}} {{- end -}} + +{{/* Quickstart-only in-cluster service hostnames (eval profile). */}} +{{- define "buzz.minioFullname" -}} +{{- printf "%s-minio" (include "buzz.fullname" .) -}} +{{- end -}} + +{{- define "buzz.typesenseFullname" -}} +{{- printf "%s-typesense" (include "buzz.fullname" .) -}} +{{- end -}} + +{{/* In-cluster MinIO endpoint, used when minio.enabled and s3.endpoint unset. */}} +{{- define "buzz.minioEndpoint" -}} +{{- printf "http://%s.%s.svc.cluster.local:9000" (include "buzz.minioFullname" .) .Release.Namespace -}} +{{- end -}} + +{{/* Effective S3 endpoint: explicit s3.endpoint wins, else bundled MinIO. */}} +{{- define "buzz.s3Endpoint" -}} +{{- if .Values.s3.endpoint -}} +{{- .Values.s3.endpoint -}} +{{- else if .Values.minio.enabled -}} +{{- include "buzz.minioEndpoint" . -}} +{{- end -}} +{{- end -}} + +{{/* In-cluster Typesense URL, used when typesense.enabled and url unset. */}} +{{- define "buzz.typesenseUrl" -}} +{{- if .Values.typesense.url -}} +{{- .Values.typesense.url -}} +{{- else if .Values.typesense.enabled -}} +{{- printf "http://%s.%s.svc.cluster.local:8108" (include "buzz.typesenseFullname" .) .Release.Namespace -}} +{{- end -}} +{{- end -}} diff --git a/deploy/charts/buzz/templates/_validate.tpl b/deploy/charts/buzz/templates/_validate.tpl index e314cfb60..a6435f606 100644 --- a/deploy/charts/buzz/templates/_validate.tpl +++ b/deploy/charts/buzz/templates/_validate.tpl @@ -51,8 +51,14 @@ surface at template time regardless of which manifest helm renders first. {{- end -}} {{/* Typesense source must exist somewhere */}} -{{- if not (or .Values.typesense.url .Values.secrets.existingSecret) -}} - {{- fail "Typesense source missing: set typesense.url + typesense.apiKey, or provide secrets.existingSecret with keys TYPESENSE_URL + TYPESENSE_API_KEY." -}} +{{- if not (or .Values.typesense.enabled .Values.typesense.url .Values.secrets.existingSecret) -}} + {{- fail "Typesense source missing: enable typesense.enabled=true (quickstart in-cluster), set typesense.url + typesense.apiKey, or provide secrets.existingSecret with keys TYPESENSE_URL + TYPESENSE_API_KEY." -}} +{{- end -}} + +{{/* S3 / object-storage source must exist somewhere (relay hard-fails its + startup conformance probe without a reachable bucket). */}} +{{- if not (or .Values.minio.enabled .Values.s3.endpoint .Values.secrets.existingSecret) -}} + {{- fail "S3/object-storage source missing: enable minio.enabled=true (quickstart in-cluster), set s3.endpoint + s3.bucket + credentials, or provide secrets.existingSecret with keys BUZZ_S3_ACCESS_KEY + BUZZ_S3_SECRET_KEY. The relay runs a startup S3 conformance probe and exits if storage is unreachable." -}} {{- end -}} {{- end -}} diff --git a/deploy/charts/buzz/templates/deployment.yaml b/deploy/charts/buzz/templates/deployment.yaml index a07dd18e3..e234fb8e4 100644 --- a/deploy/charts/buzz/templates/deployment.yaml +++ b/deploy/charts/buzz/templates/deployment.yaml @@ -14,11 +14,11 @@ spec: maxUnavailable: 0 selector: matchLabels: - {{- include "buzz.selectorLabels" . | nindent 6 }} + {{- include "buzz.relaySelectorLabels" . | nindent 6 }} template: metadata: labels: - {{- include "buzz.selectorLabels" . | nindent 8 }} + {{- include "buzz.relaySelectorLabels" . | nindent 8 }} {{- with .Values.relay.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} @@ -100,8 +100,9 @@ spec: - { name: BUZZ_GIT_MAX_CONCURRENT_OPS, value: {{ .Values.git.maxConcurrentOps | quote }} } # ── S3 (non-secret) ────────────────────────────────────── - {{- if .Values.s3.endpoint }} - - { name: BUZZ_S3_ENDPOINT, value: {{ .Values.s3.endpoint | quote }} } + {{- $s3Endpoint := include "buzz.s3Endpoint" . }} + {{- if $s3Endpoint }} + - { name: BUZZ_S3_ENDPOINT, value: {{ $s3Endpoint | quote }} } {{- end }} - { name: BUZZ_S3_BUCKET, value: {{ .Values.s3.bucket | quote }} } diff --git a/deploy/charts/buzz/templates/pdb.yaml b/deploy/charts/buzz/templates/pdb.yaml index 335e8e46c..ce2bc4d8e 100644 --- a/deploy/charts/buzz/templates/pdb.yaml +++ b/deploy/charts/buzz/templates/pdb.yaml @@ -9,7 +9,7 @@ metadata: spec: selector: matchLabels: - {{- include "buzz.selectorLabels" . | nindent 6 }} + {{- include "buzz.relaySelectorLabels" . | nindent 6 }} {{- if .Values.podDisruptionBudget.minAvailable }} minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} {{- else if .Values.podDisruptionBudget.maxUnavailable }} diff --git a/deploy/charts/buzz/templates/quickstart-minio-init.yaml b/deploy/charts/buzz/templates/quickstart-minio-init.yaml new file mode 100644 index 000000000..044f4c584 --- /dev/null +++ b/deploy/charts/buzz/templates/quickstart-minio-init.yaml @@ -0,0 +1,51 @@ +{{- /* +Creates the media bucket in the bundled MinIO after install/upgrade. Mirrors +the docker-compose `minio-init` step. Hook-managed so it re-runs on upgrade +and is garbage-collected. Quickstart-only. +*/ -}} +{{- if .Values.minio.enabled -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "buzz.minioFullname" . }}-init + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: minio-init + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "0" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: 10 + template: + metadata: + labels: + {{- include "buzz.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: minio-init + spec: + restartPolicy: OnFailure + containers: + - name: mc + image: {{ .Values.minio.mcImage | quote }} + env: + - name: S3_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.chartSecretName" . }} + key: BUZZ_S3_ACCESS_KEY + - name: S3_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.chartSecretName" . }} + key: BUZZ_S3_SECRET_KEY + command: ["/bin/sh", "-c"] + args: + - | + set -e + until mc alias set local {{ include "buzz.minioEndpoint" . }} "$S3_ACCESS_KEY" "$S3_SECRET_KEY"; do + echo "waiting for MinIO..."; sleep 3 + done + mc mb --ignore-existing local/{{ .Values.s3.bucket }} + mc anonymous set none local/{{ .Values.s3.bucket }} + echo "bucket {{ .Values.s3.bucket }} ready" +{{- end -}} diff --git a/deploy/charts/buzz/templates/quickstart-minio.yaml b/deploy/charts/buzz/templates/quickstart-minio.yaml new file mode 100644 index 000000000..9685a7ff3 --- /dev/null +++ b/deploy/charts/buzz/templates/quickstart-minio.yaml @@ -0,0 +1,99 @@ +{{- /* +Eval-only in-cluster MinIO for the quickstart profile. NOT for production — +single replica, no TLS, credentials from the chart-managed Secret. Production +deploys leave minio.enabled=false and point s3.* at managed object storage. +*/ -}} +{{- if .Values.minio.enabled -}} +{{- if .Values.secrets.existingSecret -}} +{{- fail "minio.enabled=true (quickstart) is incompatible with secrets.existingSecret. Quickstart autogenerates MinIO credentials in the chart-managed Secret; for external S3 set minio.enabled=false and provide BUZZ_S3_ACCESS_KEY/BUZZ_S3_SECRET_KEY." -}} +{{- end -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "buzz.minioFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "buzz.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: minio + template: + metadata: + labels: + {{- include "buzz.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: minio + spec: + containers: + - name: minio + image: {{ .Values.minio.image | quote }} + args: ["server", "/data", "--console-address", ":9001"] + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: {{ include "buzz.chartSecretName" . }} + key: BUZZ_S3_ACCESS_KEY + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "buzz.chartSecretName" . }} + key: BUZZ_S3_SECRET_KEY + ports: + - { name: api, containerPort: 9000 } + - { name: console, containerPort: 9001 } + readinessProbe: + httpGet: { path: /minio/health/ready, port: api } + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: { path: /minio/health/live, port: api } + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - { name: data, mountPath: /data } + volumes: + - name: data + {{- if .Values.minio.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "buzz.minioFullname" . }} + {{- else }} + emptyDir: {} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "buzz.minioFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: minio +spec: + selector: + {{- include "buzz.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: minio + ports: + - { name: api, port: 9000, targetPort: api } + - { name: console, port: 9001, targetPort: console } +{{- if .Values.minio.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "buzz.minioFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: minio + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.minio.persistence.size | quote }} +{{- end }} +{{- end -}} diff --git a/deploy/charts/buzz/templates/quickstart-typesense.yaml b/deploy/charts/buzz/templates/quickstart-typesense.yaml new file mode 100644 index 000000000..7184d5c5f --- /dev/null +++ b/deploy/charts/buzz/templates/quickstart-typesense.yaml @@ -0,0 +1,96 @@ +{{- /* +Eval-only in-cluster Typesense for the quickstart profile. NOT for production — +single replica, no TLS, API key from the chart-managed Secret. Production +deploys leave typesense.enabled=false and point typesense.url/apiKey (or +secrets.existingSecret) at a managed Typesense service. +*/ -}} +{{- if .Values.typesense.enabled -}} +{{- if .Values.secrets.existingSecret -}} +{{- fail "typesense.enabled=true (quickstart) is incompatible with secrets.existingSecret. Quickstart autogenerates the Typesense key in the chart-managed Secret; for external Typesense set typesense.enabled=false and provide TYPESENSE_URL/TYPESENSE_API_KEY." -}} +{{- end -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "buzz.typesenseFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: typesense +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "buzz.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: typesense + template: + metadata: + labels: + {{- include "buzz.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: typesense + spec: + containers: + - name: typesense + image: {{ .Values.typesense.image | quote }} + args: + - "--data-dir=/data" + - "--api-key=$(TYPESENSE_API_KEY)" + - "--enable-cors" + env: + - name: TYPESENSE_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "buzz.chartSecretName" . }} + key: TYPESENSE_API_KEY + ports: + - { name: http, containerPort: 8108 } + readinessProbe: + httpGet: { path: /health, port: http } + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + httpGet: { path: /health, port: http } + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - { name: data, mountPath: /data } + volumes: + - name: data + {{- if .Values.typesense.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "buzz.typesenseFullname" . }} + {{- else }} + emptyDir: {} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "buzz.typesenseFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: typesense +spec: + selector: + {{- include "buzz.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: typesense + ports: + - { name: http, port: 8108, targetPort: http } +{{- if .Values.typesense.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "buzz.typesenseFullname" . }} + labels: + {{- include "buzz.labels" . | nindent 4 }} + app.kubernetes.io/component: typesense + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.typesense.persistence.size | quote }} +{{- end }} +{{- end -}} diff --git a/deploy/charts/buzz/templates/secret-chart.yaml b/deploy/charts/buzz/templates/secret-chart.yaml index ae51224ab..0a7677bad 100644 --- a/deploy/charts/buzz/templates/secret-chart.yaml +++ b/deploy/charts/buzz/templates/secret-chart.yaml @@ -73,19 +73,55 @@ data: REDIS_URL: {{ .Values.externalRedis.url | b64enc | quote }} {{- end }} - {{- /* Typesense — pass through values (no subchart) */}} + {{- /* Typesense — bundled (quickstart) composes URL + autogen key; else + pass through external values. */}} + {{- if .Values.typesense.enabled }} + {{- $tsKey := "" }} + {{- if (index $existingData "TYPESENSE_API_KEY") }} + {{- $tsKey = index $existingData "TYPESENSE_API_KEY" | b64dec }} + {{- else if .Values.typesense.apiKey }} + {{- $tsKey = .Values.typesense.apiKey }} + {{- else }} + {{- $tsKey = randAlphaNum 32 }} + {{- end }} + TYPESENSE_URL: {{ include "buzz.typesenseUrl" . | b64enc | quote }} + TYPESENSE_API_KEY: {{ $tsKey | b64enc | quote }} + {{- else }} {{- if .Values.typesense.url }} TYPESENSE_URL: {{ .Values.typesense.url | b64enc | quote }} {{- end }} {{- if .Values.typesense.apiKey }} TYPESENSE_API_KEY: {{ .Values.typesense.apiKey | b64enc | quote }} {{- end }} + {{- end }} - {{- /* S3 creds */}} + {{- /* S3 creds — bundled MinIO (quickstart) autogenerates; else pass + through external values. */}} + {{- if .Values.minio.enabled }} + {{- $s3Access := "" }} + {{- if (index $existingData "BUZZ_S3_ACCESS_KEY") }} + {{- $s3Access = index $existingData "BUZZ_S3_ACCESS_KEY" | b64dec }} + {{- else if .Values.s3.accessKey }} + {{- $s3Access = .Values.s3.accessKey }} + {{- else }} + {{- $s3Access = printf "buzz-%s" (randAlphaNum 12 | lower) }} + {{- end }} + {{- $s3Secret := "" }} + {{- if (index $existingData "BUZZ_S3_SECRET_KEY") }} + {{- $s3Secret = index $existingData "BUZZ_S3_SECRET_KEY" | b64dec }} + {{- else if .Values.s3.secretKey }} + {{- $s3Secret = .Values.s3.secretKey }} + {{- else }} + {{- $s3Secret = randAlphaNum 32 }} + {{- end }} + BUZZ_S3_ACCESS_KEY: {{ $s3Access | b64enc | quote }} + BUZZ_S3_SECRET_KEY: {{ $s3Secret | b64enc | quote }} + {{- else }} {{- if .Values.s3.accessKey }} BUZZ_S3_ACCESS_KEY: {{ .Values.s3.accessKey | b64enc | quote }} {{- end }} {{- if .Values.s3.secretKey }} BUZZ_S3_SECRET_KEY: {{ .Values.s3.secretKey | b64enc | quote }} {{- end }} + {{- end }} {{- end -}} diff --git a/deploy/charts/buzz/templates/service.yaml b/deploy/charts/buzz/templates/service.yaml index 88834bb53..5e7375432 100644 --- a/deploy/charts/buzz/templates/service.yaml +++ b/deploy/charts/buzz/templates/service.yaml @@ -12,7 +12,7 @@ metadata: spec: type: {{ .Values.service.type }} selector: - {{- include "buzz.selectorLabels" . | nindent 4 }} + {{- include "buzz.relaySelectorLabels" . | nindent 4 }} ports: - { name: app, port: {{ .Values.service.port }}, targetPort: app, protocol: TCP } - { name: health, port: {{ .Values.service.healthPort }}, targetPort: health, protocol: TCP } diff --git a/deploy/charts/buzz/tests/networking_test.yaml b/deploy/charts/buzz/tests/networking_test.yaml index c5ea969e0..e925bbb2d 100644 --- a/deploy/charts/buzz/tests/networking_test.yaml +++ b/deploy/charts/buzz/tests/networking_test.yaml @@ -11,6 +11,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - equal: path: spec.ports[0].name @@ -28,6 +31,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - hasDocuments: count: 0 @@ -43,6 +49,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s ingress.enabled: true ingress.className: nginx asserts: @@ -61,6 +70,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s httproute.enabled: true httproute.parentRefs: - name: my-gateway diff --git a/deploy/charts/buzz/tests/quickstart_bundled_test.yaml b/deploy/charts/buzz/tests/quickstart_bundled_test.yaml new file mode 100644 index 000000000..7acb8ab75 --- /dev/null +++ b/deploy/charts/buzz/tests/quickstart_bundled_test.yaml @@ -0,0 +1,112 @@ +suite: quickstart bundled services +# The dev quickstart must stand up MinIO + Typesense in-cluster so the relay's +# startup S3 conformance probe passes with zero external dependencies. +templates: + - templates/quickstart-minio.yaml + - templates/quickstart-minio-init.yaml + - templates/quickstart-typesense.yaml + - templates/deployment.yaml + - templates/secret-chart.yaml + - templates/service.yaml +tests: + - it: renders the in-cluster MinIO Deployment when minio.enabled + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.enabled: true + minio.enabled: true + asserts: + - containsDocument: + kind: Deployment + apiVersion: apps/v1 + name: rel-buzz-minio + template: templates/quickstart-minio.yaml + documentIndex: 0 + - containsDocument: + kind: Job + apiVersion: batch/v1 + name: rel-buzz-minio-init + template: templates/quickstart-minio-init.yaml + + - it: renders the in-cluster Typesense Deployment when typesense.enabled + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.enabled: true + minio.enabled: true + asserts: + - containsDocument: + kind: Deployment + apiVersion: apps/v1 + name: rel-buzz-typesense + template: templates/quickstart-typesense.yaml + documentIndex: 0 + + - it: relay S3 endpoint resolves to the bundled MinIO Service + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.enabled: true + minio.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BUZZ_S3_ENDPOINT + value: "http://rel-buzz-minio.NAMESPACE.svc.cluster.local:9000" + template: templates/deployment.yaml + + - it: chart Secret composes TYPESENSE_URL at the bundled Typesense Service + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.enabled: true + minio.enabled: true + asserts: + - matchRegex: + path: data.TYPESENSE_URL + decodeBase64: true + pattern: "^http://rel-buzz-typesense\\..+:8108$" + template: templates/secret-chart.yaml + documentIndex: 0 + # MinIO creds the relay reads must exist for the conformance probe. + - isNotNullOrEmpty: + path: data.BUZZ_S3_ACCESS_KEY + template: templates/secret-chart.yaml + documentIndex: 0 + - isNotNullOrEmpty: + path: data.BUZZ_S3_SECRET_KEY + template: templates/secret-chart.yaml + documentIndex: 0 + + - it: relay Service selector is scoped to component=relay (must NOT match bundled MinIO/Typesense pods) + release: + name: rel + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + redis.enabled: true + typesense.enabled: true + minio.enabled: true + asserts: + - equal: + path: spec.selector["app.kubernetes.io/component"] + value: relay + template: templates/service.yaml diff --git a/deploy/charts/buzz/tests/quickstart_guards_test.yaml b/deploy/charts/buzz/tests/quickstart_guards_test.yaml new file mode 100644 index 000000000..9054cf5be --- /dev/null +++ b/deploy/charts/buzz/tests/quickstart_guards_test.yaml @@ -0,0 +1,35 @@ +suite: quickstart guards +# Quickstart autogenerates MinIO/Typesense creds in the chart-managed Secret, +# so it is mutually exclusive with secrets.existingSecret. Isolated single- +# template suites so the fail-guard is the only document under assertion. +tests: + - it: minio.enabled is incompatible with existingSecret + templates: + - templates/quickstart-minio.yaml + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + typesense.url: http://ts:8108 + typesense.apiKey: k + minio.enabled: true + secrets.existingSecret: "buzz-secrets" + asserts: + - failedTemplate: + errorPattern: "incompatible with secrets.existingSecret" + + - it: typesense.enabled is incompatible with existingSecret + templates: + - templates/quickstart-typesense.yaml + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + postgresql.enabled: true + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s + typesense.enabled: true + secrets.existingSecret: "buzz-secrets" + asserts: + - failedTemplate: + errorPattern: "incompatible with secrets.existingSecret" diff --git a/deploy/charts/buzz/tests/render_test.yaml b/deploy/charts/buzz/tests/render_test.yaml index 42758b075..970aeeab8 100644 --- a/deploy/charts/buzz/tests/render_test.yaml +++ b/deploy/charts/buzz/tests/render_test.yaml @@ -16,6 +16,9 @@ tests: externalRedis.url: redis://h:6379 typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - equal: path: kind @@ -38,6 +41,9 @@ tests: externalRedis.url: redis://h:6379 typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s replicaCount: 3 persistence.git.accessMode: ReadWriteMany asserts: diff --git a/deploy/charts/buzz/tests/secrets_test.yaml b/deploy/charts/buzz/tests/secrets_test.yaml index e7c10cabe..1a3690c1f 100644 --- a/deploy/charts/buzz/tests/secrets_test.yaml +++ b/deploy/charts/buzz/tests/secrets_test.yaml @@ -10,6 +10,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - hasDocuments: count: 1 @@ -30,6 +33,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s secrets.existingSecret: "buzz-secrets" asserts: - hasDocuments: @@ -43,6 +49,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s secrets.existingSecret: "buzz-secrets" asserts: - contains: @@ -63,6 +72,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - contains: path: spec.template.spec.containers[0].env @@ -83,6 +95,9 @@ tests: externalPostgresql.url: postgres://u:p@h:5432/d typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: - contains: path: spec.template.spec.containers[0].env @@ -101,6 +116,9 @@ tests: redis.enabled: true typesense.url: http://ts:8108 typesense.apiKey: k + s3.endpoint: http://minio:9000 + s3.accessKey: a + s3.secretKey: s asserts: # Postgres Service is "-postgresql"; Redis (standalone) is # "-redis" — NOT "-redis-master". A host mismatch here is the diff --git a/deploy/charts/buzz/tests/validation_test.yaml b/deploy/charts/buzz/tests/validation_test.yaml index 89302be8b..56e656ae8 100644 --- a/deploy/charts/buzz/tests/validation_test.yaml +++ b/deploy/charts/buzz/tests/validation_test.yaml @@ -92,3 +92,14 @@ tests: asserts: - failedTemplate: errorPattern: "Typesense source missing" + + - it: fails when S3/object-storage source is missing + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + typesense.url: http://ts:8108 + typesense.apiKey: k + asserts: + - failedTemplate: + errorPattern: "S3/object-storage source missing" diff --git a/deploy/charts/buzz/values.schema.json b/deploy/charts/buzz/values.schema.json index eb4e8142a..38f7b204d 100644 --- a/deploy/charts/buzz/values.schema.json +++ b/deploy/charts/buzz/values.schema.json @@ -166,8 +166,18 @@ "type": "object", "additionalProperties": false, "properties": { + "enabled": { "type": "boolean" }, "url": { "type": "string", "pattern": "^(https?://.+)?$" }, - "apiKey": { "type": "string" } + "apiKey": { "type": "string" }, + "image": { "type": "string", "minLength": 1 }, + "persistence": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "size": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?(E|P|T|G|M|K|Ei|Pi|Ti|Gi|Mi|Ki)?$" } + } + } } }, "s3": { @@ -180,6 +190,23 @@ "secretKey": { "type": "string" } } }, + "minio": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "image": { "type": "string", "minLength": 1 }, + "mcImage": { "type": "string", "minLength": 1 }, + "persistence": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "size": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?(E|P|T|G|M|K|Ei|Pi|Ti|Gi|Mi|Ki)?$" } + } + } + } + }, "git": { "type": "object", "additionalProperties": false, diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml index 5414fb203..15a70ec28 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -6,15 +6,18 @@ # refs everywhere, no chart-side autogeneration, GitOps-safe (ArgoCD/Flux). # HA-ready: replicaCount >= 2 (requires Redis and RWX storage for git). # -# QUICKSTART (`--set quickstart=true`) — enables in-cluster Postgres + Redis -# subcharts, chart auto-generates relay secrets via the `lookup` pattern -# (NOT GitOps-safe — see README), single replica, evaluation only. +# QUICKSTART — bundles in-cluster Postgres + Redis + MinIO + Typesense and +# auto-generates relay secrets via the `lookup` pattern (NOT GitOps-safe — +# see README), single replica, evaluation only. Opt in by enabling each +# bundled service: postgresql.enabled, redis.enabled, minio.enabled, +# typesense.enabled. See ci/quickstart-values.yaml and the README. # # See examples/argocd-app.yaml and examples/flux-helmrelease.yaml for the # canonical GitOps configurations. -# Master toggle for the evaluation profile. Equivalent to setting -# postgresql.enabled=true and redis.enabled=true. +# Intent marker for the evaluation profile, surfaced in NOTES.txt. It does NOT +# by itself enable any bundled service — set the per-service .enabled flags +# (postgresql / redis / minio / typesense) to bring them up in-cluster. quickstart: false # ── Image ──────────────────────────────────────────────────────────────────── @@ -209,17 +212,41 @@ externalRedis: url: "" # redis://:pass@host:6379 # ── Typesense ──────────────────────────────────────────────────────────────── +# Production: point url/apiKey at an external Typesense service (or supply +# TYPESENSE_URL/TYPESENSE_API_KEY via secrets.existingSecret). +# Quickstart (`enabled: true`): the chart runs an in-cluster, eval-only +# Typesense Deployment and composes TYPESENSE_URL with an autogenerated key. typesense: + enabled: false # quickstart: set true for bundled in-cluster Typesense url: "" apiKey: "" + image: typesense/typesense:30.2 + persistence: + enabled: true + size: 4Gi # ── S3 / object storage (media) ────────────────────────────────────────────── +# Production: point endpoint/bucket at an external S3-compatible service and +# supply credentials (inline below or via secrets.existingSecret). +# Quickstart (`minio.enabled: true`): the chart runs an in-cluster, eval-only +# MinIO Deployment, creates the bucket via a post-install Job, and composes +# the endpoint + autogenerated credentials automatically. s3: endpoint: "" bucket: "buzz-media" accessKey: "" secretKey: "" +# In-cluster MinIO for the quickstart profile only. Production deploys leave +# this disabled and use s3.* (or secrets.existingSecret) against managed S3. +minio: + enabled: false # quickstart: set true for bundled in-cluster MinIO + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + mcImage: minio/mc:RELEASE.2025-08-13T08-35-41Z + persistence: + enabled: true + size: 10Gi + # ── Git server config ──────────────────────────────────────────────────────── git: maxPackBytes: 524288000 # 500 MiB