feat(deploy): add production Helm chart for Buzz (#990)

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
tlongwell-block
2026-06-17 21:26:33 -04:00
committed by GitHub
co-authored by npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm Eva
parent aadbe67354
commit 629fb57bf0
35 changed files with 2642 additions and 0 deletions
+97
View File
@@ -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@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
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@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # 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@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
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@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0
- name: Create kind cluster
uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # 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"
+3
View File
@@ -64,3 +64,6 @@ identity.key
# mesh-llm build cache
.cache/
# Helm dependency tarballs — regenerable from Chart.lock via `helm dependency build`
deploy/charts/*/charts/*.tgz
+9
View File
@@ -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
+9
View File
@@ -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"
+42
View File
@@ -0,0 +1,42 @@
apiVersion: v2
name: buzz
description: |
Buzz — a Nostr-based messaging platform for humanagent 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
+112
View File
@@ -0,0 +1,112 @@
# Buzz Helm Chart
[Buzz](https://github.com/block/buzz) is a Nostr-based messaging platform for humanagent 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** (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)
```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 minio.enabled=true \
--set typesense.enabled=true \
--set relayUrl=wss://buzz.example.com \
--set ownerPubkey=<64-char-hex-pubkey>
```
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)
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` / `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`).
## 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`. **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
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)
- **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
```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 .
```
@@ -0,0 +1,21 @@
# Quickstart / eval: subcharts on, autogen secrets, single replica.
# This is the scenario `ct install` exercises against a kind cluster — it
# 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"
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
@@ -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
@@ -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
@@ -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
@@ -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"
+98
View File
@@ -0,0 +1,98 @@
══════════════════════════════════════════════════════════════════════════════
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 .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.
{{ 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 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 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).
{{- 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" . }}
+126
View File
@@ -0,0 +1,126 @@
{{/* 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 -}}
{{/* 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 -}}
{{- 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://<host>/media derived from relayUrl. */}}
{{- define "buzz.mediaBaseUrl" -}}
{{- if .Values.mediaBaseUrl -}}
{{- .Values.mediaBaseUrl -}}
{{- else -}}
{{- 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 -}}
@@ -0,0 +1,64 @@
{{/*
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.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 -}}
@@ -0,0 +1,184 @@
{{- 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.relaySelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "buzz.relaySelectorLabels" . | 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) ──────────────────────────────────────
{{- $s3Endpoint := include "buzz.s3Endpoint" . }}
{{- if $s3Endpoint }}
- { name: BUZZ_S3_ENDPOINT, value: {{ $s3Endpoint | 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 }}
@@ -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 }}
+43
View File
@@ -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 }}
+18
View File
@@ -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.relaySelectorLabels" . | nindent 6 }}
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- else if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
{{- end }}
+22
View File
@@ -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 -}}
@@ -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 -}}
@@ -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 -}}
@@ -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 -}}
@@ -0,0 +1,127 @@
{{- 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 | sha256sum | 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" .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 — 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 — 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 -}}
+19
View File
@@ -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.relaySelectorLabels" . | 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 }
@@ -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 }}
+22
View File
@@ -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
@@ -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
@@ -0,0 +1,87 @@
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
@@ -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
@@ -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"
+57
View File
@@ -0,0 +1,57 @@
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
+151
View File
@@ -0,0 +1,151 @@
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
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
s3.endpoint: http://minio:9000
s3.accessKey: a
s3.secretKey: s
asserts:
# Postgres Service is "<release>-postgresql"; Redis (standalone) is
# "<release>-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
# 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
@@ -0,0 +1,105 @@
suite: validation
templates:
- templates/deployment.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:
errorPattern: "ownerPubkey is required when relay.requireRelayMembership=true"
- it: fails when ownerPubkey is not 64 lowercase hex (schema-level)
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:
errorPattern: "ownerPubkey: Does not match pattern"
- 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: 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"
+251
View File
@@ -0,0 +1,251 @@
{
"$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": {
"enabled": { "type": "boolean" },
"url": { "type": "string", "pattern": "^(https?://.+)?$" },
"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": {
"type": "object",
"additionalProperties": false,
"properties": {
"endpoint": { "type": "string", "pattern": "^(https?://.+)?$" },
"bucket": { "type": "string", "minLength": 1 },
"accessKey": { "type": "string" },
"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,
"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"
}
}
}
+275
View File
@@ -0,0 +1,275 @@
# 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 — 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.
# 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 ────────────────────────────────────────────────────────────────────
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://<host>/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 ─────────────────────────────────────────────────────────────────
# 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
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
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
# ── 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
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: []