feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
apiVersion: v2
|
||||
# Published to oci://ghcr.io/block/buzz/charts via push-chart-release/<version>
|
||||
# branches (see docs/push-gateway-deployment.md, "Gateway chart release").
|
||||
name: buzz-push-gateway
|
||||
description: Public capability-gated APNs last-hop gateway for Buzz
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
type: application
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- define "push.name" -}}{{ .Release.Name }}-buzz-push-gateway{{- end }}
|
||||
{{- define "push.labels" -}}
|
||||
app.kubernetes.io/name: buzz-push-gateway
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
{{- define "push.runtimeLabels" -}}
|
||||
{{ include "push.labels" . }}
|
||||
app.kubernetes.io/component: runtime
|
||||
{{- end }}
|
||||
{{- define "push.migrationLabels" -}}
|
||||
{{ include "push.labels" . }}
|
||||
app.kubernetes.io/component: migration
|
||||
{{- end }}
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
labels: {{- include "push.runtimeLabels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy: { type: RollingUpdate, rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } }
|
||||
selector:
|
||||
matchLabels: {{- include "push.runtimeLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels: {{- include "push.runtimeLabels" . | nindent 8 }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
terminationGracePeriodSeconds: 60
|
||||
securityContext: { runAsNonRoot: true, runAsUser: 65532, runAsGroup: 65532, fsGroup: 65532, seccompProfile: { type: RuntimeDefault } }
|
||||
{{- with .Values.image.pullSecrets }}
|
||||
imagePullSecrets: {{ toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: gateway
|
||||
image: "{{ .Values.image.repository }}{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ required "image.tag or image.digest is required" .Values.image.tag }}{{ end }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: [ALL] } }
|
||||
ports:
|
||||
- { name: public, containerPort: 8080 }
|
||||
- { name: health, containerPort: 8081 }
|
||||
env:
|
||||
- { name: BUZZ_PUSH_BIND_ADDR, value: "0.0.0.0:8080" }
|
||||
- { name: BUZZ_PUSH_HEALTH_ADDR, value: "0.0.0.0:8081" }
|
||||
- { name: BUZZ_PUSH_PUBLIC_DELIVERY_URL, value: {{ .Values.publicDeliveryUrl | quote }} }
|
||||
- { name: BUZZ_PUSH_MAX_GRANT_LIFETIME_SECONDS, value: {{ .Values.maxGrantLifetimeSeconds | quote }} }
|
||||
- { name: BUZZ_PUSH_ENABLED_PROFILES, value: {{ .Values.enabledProfiles | quote }} }
|
||||
- { name: BUZZ_PUSH_APP_ATTEST_APP_ID, value: {{ .Values.appAttestAppId | quote }} }
|
||||
- { name: BUZZ_PUSH_APP_ATTEST_ROOT_CERT_PATH, value: /run/buzz/app-attest/root.pem }
|
||||
- { name: BUZZ_PUSH_APNS_KEY_PATH, value: /run/buzz/apns/provider.p8 }
|
||||
{{- range $name := list "DATABASE_URL" "BUZZ_PUSH_APNS_KEY_ID" "BUZZ_PUSH_APNS_TEAM_ID" "BUZZ_PUSH_APNS_TOPIC" "BUZZ_PUSH_GRANT_KEYS" "BUZZ_PUSH_TOKEN_KEYS" }}
|
||||
- name: {{ $name }}
|
||||
valueFrom: { secretKeyRef: { name: {{ $.Values.existingSecret }}, key: {{ $name }} } }
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- { name: app-attest-root, mountPath: /run/buzz/app-attest, readOnly: true }
|
||||
- { name: apns-key, mountPath: /run/buzz/apns, readOnly: true }
|
||||
livenessProbe: { httpGet: { path: /_liveness, port: health }, periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 3 }
|
||||
readinessProbe: { httpGet: { path: /_readiness, port: health }, periodSeconds: 5, timeoutSeconds: 3, failureThreshold: 3 }
|
||||
startupProbe: { httpGet: { path: /_liveness, port: health }, periodSeconds: 2, failureThreshold: 60 }
|
||||
resources: {{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: app-attest-root
|
||||
secret: { secretName: {{ .Values.appAttestRoot.secretName }}, items: [{ key: {{ .Values.appAttestRoot.secretKey }}, path: root.pem }] }
|
||||
- name: apns-key
|
||||
secret: { secretName: {{ .Values.apnsKey.secretName }}, items: [{ key: {{ .Values.apnsKey.secretKey }}, path: provider.p8 }] }
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.topologySpreadConstraints }}
|
||||
topologySpreadConstraints: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{- if .Values.httpRoute.enabled }}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
spec:
|
||||
parentRefs: {{- toYaml .Values.httpRoute.parentRefs | nindent 4 }}
|
||||
hostnames: {{- toYaml .Values.httpRoute.hostnames | nindent 4 }}
|
||||
rules:
|
||||
- matches:
|
||||
- path: { type: PathPrefix, value: / }
|
||||
backendRefs:
|
||||
- { name: {{ include "push.name" . }}, port: {{ .Values.service.port }} }
|
||||
{{- end }}
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}-migrate
|
||||
labels: {{- include "push.migrationLabels" . | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/hook: pre-install,pre-upgrade
|
||||
helm.sh/hook-weight: "-5"
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels: {{- include "push.migrationLabels" . | nindent 8 }}
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
automountServiceAccountToken: false
|
||||
securityContext: { runAsNonRoot: true, runAsUser: 65532, runAsGroup: 65532, fsGroup: 65532, seccompProfile: { type: RuntimeDefault } }
|
||||
{{- with .Values.image.pullSecrets }}
|
||||
imagePullSecrets: {{ toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: migrate
|
||||
image: "{{ .Values.image.repository }}{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ required "image.tag or image.digest is required" .Values.image.tag }}{{ end }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args: ["--migrate-only"]
|
||||
securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: [ALL] } }
|
||||
env:
|
||||
- name: BUZZ_PUSH_RUNTIME_DATABASE_ROLE
|
||||
value: {{ .Values.migration.runtimeDatabaseRole | quote }}
|
||||
- name: DATABASE_URL
|
||||
valueFrom: { secretKeyRef: { name: {{ .Values.migration.existingSecret }}, key: {{ .Values.migration.databaseUrlKey }} } }
|
||||
resources: {{- toYaml .Values.migration.resources | nindent 12 }}
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}-migration
|
||||
labels: {{- include "push.migrationLabels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Hooks precede ordinary manifests. Keep this policy alive until the next
|
||||
# release's before-hook-creation cleanup so it covers the later Job hook.
|
||||
helm.sh/hook: pre-install,pre-upgrade
|
||||
helm.sh/hook-weight: "-10"
|
||||
helm.sh/hook-delete-policy: before-hook-creation
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {{- include "push.migrationLabels" . | nindent 6 }}
|
||||
policyTypes: [Ingress, Egress]
|
||||
ingress: []
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.dns.namespaceSelector | nindent 14 }}
|
||||
podSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.dns.podSelector | nindent 14 }}
|
||||
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]
|
||||
- to:
|
||||
{{- range .Values.networkPolicy.postgresEgressCidrs }}
|
||||
- ipBlock: { cidr: {{ . }} }
|
||||
{{- end }}
|
||||
ports: [{ port: 5432, protocol: TCP }]
|
||||
@@ -0,0 +1,42 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {{- include "push.runtimeLabels" . | nindent 6 }}
|
||||
policyTypes: [Ingress, Egress]
|
||||
ingress:
|
||||
- ports: [{ port: 8080, protocol: TCP }]
|
||||
{{- if .Values.networkPolicy.monitoring.enabled }}
|
||||
{{- if or (not .Values.networkPolicy.monitoring.namespaceSelector) (not .Values.networkPolicy.monitoring.podSelector) }}
|
||||
{{- fail "networkPolicy.monitoring.enabled requires non-empty namespaceSelector and podSelector so 8081 scrape ingress is scoped, never blanket" }}
|
||||
{{- end }}
|
||||
# Scoped scrape access to the private health/metrics port. Off by default so
|
||||
# 8081 has no pod ingress; when enabled the operator names their scraper.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.monitoring.namespaceSelector | nindent 14 }}
|
||||
podSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.monitoring.podSelector | nindent 14 }}
|
||||
ports: [{ port: 8081, protocol: TCP }]
|
||||
{{- end }}
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.dns.namespaceSelector | nindent 14 }}
|
||||
podSelector:
|
||||
matchLabels: {{- toYaml .Values.networkPolicy.dns.podSelector | nindent 14 }}
|
||||
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]
|
||||
- to:
|
||||
{{- range .Values.networkPolicy.apnsEgressCidrs }}
|
||||
- ipBlock: { cidr: {{ . }} }
|
||||
{{- end }}
|
||||
ports: [{ port: 443, protocol: TCP }]
|
||||
- to:
|
||||
{{- range .Values.networkPolicy.postgresEgressCidrs }}
|
||||
- ipBlock: { cidr: {{ . }} }
|
||||
{{- end }}
|
||||
ports: [{ port: 5432, protocol: TCP }]
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels: {{- include "push.runtimeLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.podMonitor.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PodMonitor
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
labels: {{- include "push.runtimeLabels" . | nindent 4 }}
|
||||
{{- with .Values.podMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels: {{- include "push.runtimeLabels" . | nindent 6 }}
|
||||
podMetricsEndpoints:
|
||||
# Scrape the private health port only; /metrics is never on the public Service.
|
||||
- port: health
|
||||
path: /metrics
|
||||
interval: {{ .Values.podMonitor.interval }}
|
||||
scrapeTimeout: {{ .Values.podMonitor.scrapeTimeout }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,89 @@
|
||||
{{- if .Values.prometheusRule.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
labels: {{- include "push.labels" . | nindent 4 }}
|
||||
{{- with .Values.prometheusRule.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
groups:
|
||||
- name: buzz-push-gateway
|
||||
rules:
|
||||
# Sustained configuration faults mean the provider credential/topic is
|
||||
# unhealthy; no endpoint is being invalidated but nothing is delivering.
|
||||
- alert: PushGatewayConfigurationFault
|
||||
expr: |
|
||||
sum(rate(push_gateway_apns_deliveries_total{outcome="configuration_fault"}[5m])) > 0
|
||||
for: 10m
|
||||
labels: { severity: critical }
|
||||
annotations:
|
||||
summary: Push gateway APNs configuration faults
|
||||
description: >-
|
||||
APNs is returning configuration faults (bad/expired provider token
|
||||
or topic). Deliveries are failing without invalidating endpoints.
|
||||
See runbook: check the APNs .p8 key, key id, team id, and topic.
|
||||
# Authority store unavailable at admission = durable dependency is down.
|
||||
- alert: PushGatewayAdmissionUnavailable
|
||||
expr: |
|
||||
sum(rate(push_gateway_admissions_total{result="unavailable"}[5m])) > 0
|
||||
for: 5m
|
||||
labels: { severity: critical }
|
||||
annotations:
|
||||
summary: Push gateway authority store unavailable
|
||||
description: >-
|
||||
authorize_delivery is returning Unavailable — the PostgreSQL
|
||||
authority store is unreachable or failing. Check DB connectivity
|
||||
and the pod's postgres egress NetworkPolicy.
|
||||
# Readiness failing on the authority cause = the pod will be pulled from
|
||||
# rotation; alert before all replicas drop out.
|
||||
- alert: PushGatewayReadinessAuthorityFailing
|
||||
expr: |
|
||||
sum(rate(push_gateway_readiness_failures_total{cause="authority"}[5m])) > 0
|
||||
for: 5m
|
||||
labels: { severity: warning }
|
||||
annotations:
|
||||
summary: Push gateway readiness failing on authority
|
||||
description: >-
|
||||
Readiness probes are failing because the authority store check
|
||||
fails. Replicas will be removed from the Service. Investigate DB
|
||||
health before capacity drops below the PodDisruptionBudget.
|
||||
# The retention reaper sweeps expired rows every 5m; a single transient
|
||||
# failure self-heals on the next tick. Alert on repeated failure —
|
||||
# at least two sweeps failing within ~30m (six ticks) — which grows the
|
||||
# bounded crash-before-release window and leaks storage.
|
||||
- alert: PushGatewayReaperFailing
|
||||
expr: |
|
||||
sum(increase(push_gateway_reaper_failures_total[30m])) >= 2
|
||||
for: 5m
|
||||
labels: { severity: warning }
|
||||
annotations:
|
||||
summary: Push gateway retention reaper failing
|
||||
description: >-
|
||||
The retention reaper has failed at least twice within 30m (it runs
|
||||
every 5m). Expired delivery reservations are not being swept,
|
||||
growing the bounded-until-expiry window. Check DB write availability.
|
||||
# High sustained fraction of retryable APNs outcomes indicates APNs
|
||||
# throttling or degradation. The ratio is a true fraction over the
|
||||
# window (increase = counts, not per-second rate), gated by a minimum
|
||||
# sample count so a couple of retries at trivial volume cannot trip it.
|
||||
- alert: PushGatewayHighApnsRetryRate
|
||||
expr: |
|
||||
(
|
||||
sum(increase(push_gateway_apns_deliveries_total{outcome="retry"}[10m]))
|
||||
/ sum(increase(push_gateway_apns_deliveries_total[10m]))
|
||||
> {{ .Values.prometheusRule.apnsRetryRatioThreshold }}
|
||||
)
|
||||
and
|
||||
sum(increase(push_gateway_apns_deliveries_total[10m])) >= {{ .Values.prometheusRule.apnsRetryMinSamples }}
|
||||
for: 15m
|
||||
labels: { severity: warning }
|
||||
annotations:
|
||||
summary: Push gateway high APNs retry ratio
|
||||
description: >-
|
||||
The retryable fraction of APNs attempts over a 10m window
|
||||
(429/500/503), above a minimum sample count, has exceeded the
|
||||
configured threshold continuously for 15m. APNs may be throttling
|
||||
or degraded; deliveries are delayed but not lost.
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "push.name" . }}
|
||||
labels: {{- include "push.runtimeLabels" . | nindent 4 }}
|
||||
spec:
|
||||
selector: {{- include "push.runtimeLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- { name: https, port: {{ .Values.service.port }}, targetPort: public }
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
auto_path = Path('.github/workflows/auto-tag-on-release-pr-merge.yml')
|
||||
publish_path = Path('.github/workflows/push-gateway-helm-chart.yml')
|
||||
auto_text = auto_path.read_text()
|
||||
publish_text = publish_path.read_text()
|
||||
# Parse first, then pin the cross-workflow strings whose agreement makes this a
|
||||
# reachable lane rather than an orphan publisher.
|
||||
yaml.safe_load(auto_text)
|
||||
yaml.safe_load(publish_text)
|
||||
for needle in (
|
||||
'push-chart-release/*)',
|
||||
'VERSION="${BRANCH#push-chart-release/}"',
|
||||
'TAG_PREFIX="push-chart-v"',
|
||||
'DISPATCH="push-gateway-helm-chart"',
|
||||
'push-gateway-helm-chart) WORKFLOW="push-gateway-helm-chart.yml"',
|
||||
):
|
||||
assert needle in auto_text, f'missing auto-tag gateway chart contract: {needle}'
|
||||
for needle in (
|
||||
'tags: ["push-chart-v[0-9]*"]',
|
||||
'version="${INPUT_VERSION:-${REF_NAME#push-chart-v}}"',
|
||||
'refs/tags/push-chart-v${version}^{commit}',
|
||||
'deploy/charts/buzz-push-gateway',
|
||||
):
|
||||
assert needle in publish_text, f'missing gateway chart publisher contract: {needle}'
|
||||
PY
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
out=$(mktemp); production_out=$(mktemp)
|
||||
trap 'rm -f "$out" "$production_out"' EXIT
|
||||
|
||||
# Defaults must lint and render without parameter injection.
|
||||
helm lint deploy/charts/buzz-push-gateway >/dev/null
|
||||
helm template push deploy/charts/buzz-push-gateway >"$out"
|
||||
# Production values must attach push.buzz.xyz to an explicit Gateway.
|
||||
production_args=(
|
||||
-f deploy/charts/buzz-push-gateway/values-production.yaml
|
||||
--set 'image.digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
--set 'appAttestAppId=REALTEAM.xyz.buzz'
|
||||
--set 'httpRoute.parentRefs[0].name=production-gateway'
|
||||
--set 'httpRoute.parentRefs[0].namespace=gateway-system'
|
||||
--set 'networkPolicy.postgresEgressCidrs[0]=10.42.0.0/16'
|
||||
)
|
||||
helm lint deploy/charts/buzz-push-gateway "${production_args[@]}" >/dev/null
|
||||
helm template push deploy/charts/buzz-push-gateway "${production_args[@]}" >"$production_out"
|
||||
|
||||
python3 - "$out" "$production_out" <<'PY'
|
||||
import sys,yaml
|
||||
xs=list(yaml.safe_load_all(open(sys.argv[1])))
|
||||
svc=next(x for x in xs if x and x.get('kind')=='Service')
|
||||
assert [p['targetPort'] for p in svc['spec']['ports']]==['public']
|
||||
d=next(x for x in xs if x and x.get('kind')=='Deployment')
|
||||
j=next(x for x in xs if x and x.get('kind')=='Job')
|
||||
runtime={'app.kubernetes.io/name':'buzz-push-gateway','app.kubernetes.io/instance':'push','app.kubernetes.io/component':'runtime'}
|
||||
migration={**runtime,'app.kubernetes.io/component':'migration'}
|
||||
assert svc['spec']['selector']==runtime
|
||||
assert d['spec']['selector']['matchLabels']==runtime
|
||||
assert d['spec']['template']['metadata']['labels']==runtime
|
||||
assert j['spec']['template']['metadata']['labels']==migration
|
||||
assert svc['spec']['selector'] != j['spec']['template']['metadata']['labels']
|
||||
jenv={e['name']:e for e in j['spec']['template']['spec']['containers'][0]['env']}
|
||||
assert jenv['BUZZ_PUSH_RUNTIME_DATABASE_ROLE']['value']=='buzz_push_gateway_runtime'
|
||||
assert 'valueFrom' in jenv['DATABASE_URL']
|
||||
assert j['spec']['template']['spec']['containers'][0]['args']==['--migrate-only']
|
||||
assert j['metadata']['annotations']=={
|
||||
'helm.sh/hook':'pre-install,pre-upgrade',
|
||||
'helm.sh/hook-weight':'-5',
|
||||
'helm.sh/hook-delete-policy':'before-hook-creation,hook-succeeded',
|
||||
}
|
||||
env={e['name'] for e in d['spec']['template']['spec']['containers'][0]['env']}
|
||||
required={'DATABASE_URL','BUZZ_PUSH_APNS_KEY_ID','BUZZ_PUSH_APNS_TEAM_ID','BUZZ_PUSH_APNS_TOPIC','BUZZ_PUSH_GRANT_KEYS','BUZZ_PUSH_TOKEN_KEYS','BUZZ_PUSH_MAX_GRANT_LIFETIME_SECONDS'}
|
||||
assert required <= env
|
||||
assert d['spec']['replicas'] >= 2
|
||||
assert not any(x and x.get('kind')=='HTTPRoute' for x in xs)
|
||||
# Observability is opt-in: default render exposes no scrape CRDs and 8081 stays
|
||||
# free of pod ingress (only 8080 is reachable).
|
||||
assert not any(x and x.get('kind') in ('PodMonitor','PrometheusRule') for x in xs)
|
||||
nps=[x for x in xs if x and x.get('kind')=='NetworkPolicy']
|
||||
np=next(x for x in nps if x['metadata']['name']=='push-buzz-push-gateway')
|
||||
migration_np=next(x for x in nps if x['metadata']['name']=='push-buzz-push-gateway-migration')
|
||||
assert np['spec']['podSelector']['matchLabels']==runtime
|
||||
assert migration_np['spec']['podSelector']['matchLabels']==migration
|
||||
assert migration_np['metadata']['annotations']=={
|
||||
'helm.sh/hook':'pre-install,pre-upgrade',
|
||||
'helm.sh/hook-weight':'-10',
|
||||
'helm.sh/hook-delete-policy':'before-hook-creation',
|
||||
}
|
||||
assert int(migration_np['metadata']['annotations']['helm.sh/hook-weight']) < int(j['metadata']['annotations']['helm.sh/hook-weight'])
|
||||
assert migration_np['spec']['ingress']==[]
|
||||
assert migration_np['spec']['policyTypes']==['Ingress','Egress']
|
||||
migration_ports={p['port'] for rule in migration_np['spec']['egress'] for p in rule.get('ports',[])}
|
||||
assert migration_ports=={53,5432}, migration_ports
|
||||
assert all(p['port'] != 443 for rule in migration_np['spec']['egress'] for p in rule.get('ports',[]))
|
||||
ingress_ports={p['port'] for rule in np['spec']['ingress'] for p in rule.get('ports',[])}
|
||||
assert ingress_ports=={8080}, ingress_ports
|
||||
production=list(yaml.safe_load_all(open(sys.argv[2])))
|
||||
route=next(x for x in production if x and x.get('kind')=='HTTPRoute')
|
||||
assert route['spec']['parentRefs']
|
||||
assert 'push.buzz.xyz' in route['spec']['hostnames']
|
||||
PY
|
||||
|
||||
# Enabling a route without a Gateway attachment must fail schema validation.
|
||||
if helm template push deploy/charts/buzz-push-gateway --set httpRoute.enabled=true >/dev/null 2>&1; then
|
||||
echo 'expected httpRoute.enabled=true without parentRefs to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The checked-in production contract is intentionally undeployable until CI or
|
||||
# the release system supplies an immutable digest and environment-owned values.
|
||||
if helm template push deploy/charts/buzz-push-gateway -f deploy/charts/buzz-push-gateway/values-production.yaml >/dev/null 2>&1; then
|
||||
echo 'expected uninjected production values to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enabling observability renders the scrape CRDs and adds a scoped 8081 ingress
|
||||
# keyed to the named monitoring source — never a blanket 8081 rule.
|
||||
monitoring_out=$(mktemp); trap 'rm -f "$out" "$production_out" "$monitoring_out"' EXIT
|
||||
helm template push deploy/charts/buzz-push-gateway \
|
||||
--set podMonitor.enabled=true \
|
||||
--set prometheusRule.enabled=true \
|
||||
--set networkPolicy.monitoring.enabled=true \
|
||||
--set 'networkPolicy.monitoring.namespaceSelector.kubernetes\.io/metadata\.name=monitoring' \
|
||||
--set 'networkPolicy.monitoring.podSelector.app\.kubernetes\.io/name=prometheus' \
|
||||
>"$monitoring_out"
|
||||
|
||||
python3 - "$monitoring_out" <<'PY'
|
||||
import sys,yaml
|
||||
xs=list(yaml.safe_load_all(open(sys.argv[1])))
|
||||
pm=next(x for x in xs if x and x.get('kind')=='PodMonitor')
|
||||
ep=pm['spec']['podMetricsEndpoints'][0]
|
||||
assert ep['port']=='health' and ep['path']=='/metrics', ep
|
||||
assert next(x for x in xs if x and x.get('kind')=='PrometheusRule')['spec']['groups']
|
||||
np=next(x for x in xs if x and x.get('kind')=='NetworkPolicy' and x['metadata']['name']=='push-buzz-push-gateway')
|
||||
mon=[r for r in np['spec']['ingress'] if {p['port'] for p in r.get('ports',[])}=={8081}]
|
||||
assert len(mon)==1, 'exactly one scoped 8081 ingress rule'
|
||||
frm=mon[0]['from'][0]
|
||||
# 8081 ingress must be scoped by both selectors, never empty/blanket.
|
||||
assert frm['namespaceSelector']['matchLabels'] and frm['podSelector']['matchLabels'], frm
|
||||
PY
|
||||
|
||||
# Negative: monitoring enabled with default empty selectors must fail (would
|
||||
# otherwise render a blanket 8081 rule matching all namespaces/pods).
|
||||
if helm template push deploy/charts/buzz-push-gateway \
|
||||
--set podMonitor.enabled=true \
|
||||
--set networkPolicy.monitoring.enabled=true >/dev/null 2>&1; then
|
||||
echo 'expected monitoring.enabled with empty selectors to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Negative: scrape flags must be coupled. PodMonitor without ingress = an
|
||||
# unreachable scraper; ingress without a PodMonitor = an open hole with no
|
||||
# scraper. Both mismatches must fail schema validation.
|
||||
if helm template push deploy/charts/buzz-push-gateway \
|
||||
--set podMonitor.enabled=true \
|
||||
--set 'networkPolicy.monitoring.namespaceSelector.kubernetes\.io/metadata\.name=monitoring' \
|
||||
--set 'networkPolicy.monitoring.podSelector.app\.kubernetes\.io/name=prometheus' \
|
||||
>/dev/null 2>&1; then
|
||||
echo 'expected podMonitor.enabled without monitoring ingress to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
if helm template push deploy/charts/buzz-push-gateway \
|
||||
--set networkPolicy.monitoring.enabled=true \
|
||||
--set 'networkPolicy.monitoring.namespaceSelector.kubernetes\.io/metadata\.name=monitoring' \
|
||||
--set 'networkPolicy.monitoring.podSelector.app\.kubernetes\.io/name=prometheus' \
|
||||
>/dev/null 2>&1; then
|
||||
echo 'expected monitoring ingress without podMonitor.enabled to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Negative: retry-ratio threshold is a fraction; a value > 1 must fail schema.
|
||||
if helm template push deploy/charts/buzz-push-gateway \
|
||||
--set prometheusRule.enabled=true \
|
||||
--set prometheusRule.apnsRetryRatioThreshold=2 >/dev/null 2>&1; then
|
||||
echo 'expected apnsRetryRatioThreshold=2 to fail' >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,15 @@
|
||||
# Required environment-owned values are deliberately invalid/empty here. A
|
||||
# production renderer must inject all of them; CI proves omission fails.
|
||||
image:
|
||||
tag: ""
|
||||
digest: ""
|
||||
appAttestAppId: ""
|
||||
httpRoute:
|
||||
enabled: true
|
||||
parentRefs: []
|
||||
hostnames:
|
||||
- push.buzz.xyz
|
||||
networkPolicy:
|
||||
apnsEgressCidrs:
|
||||
- 0.0.0.0/0
|
||||
postgresEgressCidrs: []
|
||||
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"replicaCount": {
|
||||
"type": "integer",
|
||||
"minimum": 2
|
||||
},
|
||||
"existingSecret": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"publicDeliveryUrl": {
|
||||
"const": "https://push.buzz.xyz/v1/deliveries/apns"
|
||||
},
|
||||
"maxGrantLifetimeSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 31536000
|
||||
},
|
||||
"appAttestAppId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"httpRoute": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"parentRefs",
|
||||
"hostnames"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parentRefs": {
|
||||
"type": "array"
|
||||
},
|
||||
"hostnames": {
|
||||
"type": "array",
|
||||
"contains": {
|
||||
"const": "push.buzz.xyz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"parentRefs": {
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"apnsEgressCidrs",
|
||||
"postgresEgressCidrs",
|
||||
"dns"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
},
|
||||
"apnsEgressCidrs": {
|
||||
"type": "array",
|
||||
"minItems": 1
|
||||
},
|
||||
"postgresEgressCidrs": {
|
||||
"type": "array",
|
||||
"minItems": 1
|
||||
},
|
||||
"dns": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"namespaceSelector",
|
||||
"podSelector"
|
||||
]
|
||||
},
|
||||
"monitoring": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"namespaceSelector",
|
||||
"podSelector"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"namespaceSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"podSelector": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"namespaceSelector": {
|
||||
"minProperties": 1
|
||||
},
|
||||
"podSelector": {
|
||||
"minProperties": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"podMonitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prometheusRule": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"apnsRetryRatioThreshold": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"apnsRetryMinSamples": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"repository"
|
||||
],
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"pattern": "^$|^sha256:[0-9a-f]{64}$"
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"tag": {
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"digest": {
|
||||
"pattern": "^sha256:[0-9a-f]{64}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"migration": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"existingSecret",
|
||||
"databaseUrlKey",
|
||||
"runtimeDatabaseRole",
|
||||
"resources"
|
||||
],
|
||||
"properties": {
|
||||
"existingSecret": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"databaseUrlKey": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"runtimeDatabaseRole": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]{0,62}$"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"replicaCount",
|
||||
"existingSecret",
|
||||
"publicDeliveryUrl",
|
||||
"maxGrantLifetimeSeconds",
|
||||
"appAttestAppId",
|
||||
"httpRoute",
|
||||
"image",
|
||||
"migration"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$comment": "Scraping opt-in is coupled: a PodMonitor and its scoped 8081 ingress must be enabled together, so we never render a scraper that cannot reach the port nor an ingress hole with no scraper.",
|
||||
"if": {
|
||||
"properties": {
|
||||
"podMonitor": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"podMonitor"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"networkPolicy": {
|
||||
"properties": {
|
||||
"monitoring": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"monitoring"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"networkPolicy"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"networkPolicy": {
|
||||
"properties": {
|
||||
"monitoring": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"monitoring"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"networkPolicy"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"podMonitor": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"podMonitor"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: ghcr.io/block/buzz-push-gateway
|
||||
# `main` is published by the push-gateway lane on every main push.
|
||||
tag: main
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
pullSecrets: []
|
||||
existingSecret: buzz-push-gateway
|
||||
# DDL-capable credentials are used only by the pre-install/pre-upgrade migration
|
||||
# Job. Runtime DATABASE_URL in existingSecret should have DML-only privileges.
|
||||
migration:
|
||||
existingSecret: buzz-push-gateway-migrations
|
||||
databaseUrlKey: DATABASE_URL
|
||||
# Existing LOGIN role used by runtime DATABASE_URL. Migrations grant it only
|
||||
# CONNECT plus DML on the six gateway tables in this dedicated database.
|
||||
runtimeDatabaseRole: buzz_push_gateway_runtime
|
||||
resources:
|
||||
requests: {cpu: 50m, memory: 64Mi}
|
||||
limits: {cpu: 250m, memory: 128Mi}
|
||||
publicDeliveryUrl: https://push.buzz.xyz/v1/deliveries/apns
|
||||
maxGrantLifetimeSeconds: 2592000
|
||||
enabledProfiles: buzz-ios-production
|
||||
# Example App Attest identifier. Production MUST override this with the exact
|
||||
# Apple TEAMID.bundle-id value (see values-production.yaml).
|
||||
appAttestAppId: TEAMID.xyz.buzz
|
||||
appAttestRoot:
|
||||
secretName: buzz-push-gateway
|
||||
secretKey: app-attest-root.pem
|
||||
apnsKey:
|
||||
secretName: buzz-push-gateway
|
||||
secretKey: apns-provider.p8
|
||||
service:
|
||||
port: 8080
|
||||
httpRoute:
|
||||
# Disabled by default so a generic install cannot claim an unattached route.
|
||||
# Production enables this with an explicit Gateway parentRef.
|
||||
enabled: false
|
||||
parentRefs: []
|
||||
hostnames: [push.buzz.xyz]
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 128Mi}
|
||||
limits: {cpu: "1", memory: 512Mi}
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
# Kubernetes NetworkPolicy cannot allow DNS names. Production operators must
|
||||
# narrow these CIDRs to their PostgreSQL/NAT destinations where supported.
|
||||
apnsEgressCidrs: [0.0.0.0/0]
|
||||
# Override with the actual database network. This example private range is
|
||||
# intentionally separate from broad APNs HTTPS egress.
|
||||
postgresEgressCidrs: [10.0.0.0/8]
|
||||
dns:
|
||||
namespaceSelector:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
k8s-app: kube-dns
|
||||
# Scoped ingress to the private metrics port (8081). Off by default so 8081
|
||||
# has no pod ingress at all; enable only alongside podMonitor and name the
|
||||
# scraper's namespace/pod so reachability stays narrow.
|
||||
monitoring:
|
||||
enabled: false
|
||||
namespaceSelector: {}
|
||||
podSelector: {}
|
||||
# Prometheus-operator PodMonitor scraping the private /metrics on port 8081.
|
||||
# Off by default; requires networkPolicy.monitoring to also be enabled.
|
||||
podMonitor:
|
||||
enabled: false
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
labels: {}
|
||||
# Prometheus-operator alerting rules. Off by default.
|
||||
prometheusRule:
|
||||
enabled: false
|
||||
labels: {}
|
||||
# Retryable-outcome fraction (0..1] that fires PushGatewayHighApnsRetryRate.
|
||||
apnsRetryRatioThreshold: 0.25
|
||||
# Minimum APNs attempts in the 10m window before the retry-ratio alert can
|
||||
# fire, so a couple of retries at trivial volume cannot trip it.
|
||||
apnsRetryMinSamples: 20
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: kubernetes.io/hostname
|
||||
whenUnsatisfiable: ScheduleAnyway
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: buzz-push-gateway
|
||||
@@ -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"
|
||||
@@ -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 and Redis. Configurable for single-node evaluation
|
||||
(subcharts on) and HA production (external services, existingSecret).
|
||||
type: application
|
||||
version: 0.1.7
|
||||
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: Generic init-container, volume, volume-mount, command, and args extension points for the relay Pod.
|
||||
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
|
||||
@@ -0,0 +1,261 @@
|
||||
# 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, 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/S3, `secrets.existingSecret:`, no chart-side autogen, HA-capable (`replicaCount ≥ 2`) |
|
||||
| **Quickstart** (eval) | Eval, single-node, one-off demo | In-cluster Postgres + Redis + MinIO 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.7 \
|
||||
--create-namespace --namespace buzz \
|
||||
--set quickstart=true \
|
||||
--set postgresql.enabled=true \
|
||||
--set redis.enabled=true \
|
||||
--set minio.enabled=true \
|
||||
--set relayUrl=wss://buzz.example.com \
|
||||
--set ownerPubkey=<64-char-hex-pubkey>
|
||||
```
|
||||
|
||||
This brings up **everything in-cluster** — Postgres, Redis, and MinIO (with
|
||||
its bucket created by a post-install Job) — and composes the relay's
|
||||
`BUZZ_S3_ENDPOINT` 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` / `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`).
|
||||
|
||||
## S3 URL addressing
|
||||
|
||||
Buzz uses one URL style for both media and Git/CAS object-store requests:
|
||||
|
||||
| `s3.addressingStyle` | Request shape | Use for |
|
||||
|---|---|---|
|
||||
| `path` (default) | `https://endpoint/bucket/key` | Bundled MinIO and endpoints whose DNS does not resolve bucket subdomains |
|
||||
| `virtual` | `https://bucket.endpoint/key` | AWS-style providers and new Railway Storage Buckets |
|
||||
|
||||
The chart always renders `s3.addressingStyle` as
|
||||
`BUZZ_S3_ADDRESSING_STYLE`. It renders `s3.region` as `BUZZ_S3_REGION` only
|
||||
when explicitly set, preserving the relay's existing `AWS_REGION` fallback for
|
||||
upgrades. Only `path` and `virtual` addressing styles are accepted; invalid
|
||||
values fail chart rendering and relay startup. The bundled MinIO quickstart
|
||||
deliberately keeps `path` because its Service DNS resolves one endpoint
|
||||
hostname, not arbitrary `<bucket>.<service>` names.
|
||||
|
||||
For a Railway Storage Bucket, map its variables to chart values in the service
|
||||
or generated Helm configuration:
|
||||
|
||||
```yaml
|
||||
s3:
|
||||
endpoint: "${{Object Storage.ENDPOINT}}"
|
||||
bucket: "${{Object Storage.BUCKET}}"
|
||||
region: "${{Object Storage.REGION}}"
|
||||
addressingStyle: virtual
|
||||
```
|
||||
|
||||
Store `BUZZ_S3_ACCESS_KEY=${{Object Storage.ACCESS_KEY_ID}}` and
|
||||
`BUZZ_S3_SECRET_KEY=${{Object Storage.SECRET_ACCESS_KEY}}` in the Secret named by
|
||||
`secrets.existingSecret`. Railway's Credentials tab is authoritative for older
|
||||
buckets, which may still require `path`. The setting changes request routing and
|
||||
SigV4 signing, so do not put the bucket into `s3.endpoint`; pass Railway's base
|
||||
`ENDPOINT` and `BUCKET` separately.
|
||||
|
||||
Object storage is contacted during relay startup only when
|
||||
`BUZZ_GIT_CONFORMANCE_PROBE` is enabled (the relay default). A probe failure is
|
||||
startup-fatal, so Kubernetes readiness never opens. If an operator explicitly
|
||||
disables that probe through `relay.extraEnv`, `/_readiness` does not test object
|
||||
storage; configuration is still parsed strictly, but reachability and addressing
|
||||
errors surface on the first storage operation.
|
||||
|
||||
## Relay Pod extensions
|
||||
|
||||
The chart exposes narrow extension points for init containers, volumes, relay
|
||||
volume mounts, and image command/argument overrides. `extraManifests` creates
|
||||
independent Kubernetes resources but cannot modify the chart-managed relay
|
||||
Deployment. These extension values insert fields into that Deployment, avoiding
|
||||
duplication of its environment, probes, security context, secrets, and
|
||||
chart-owned volumes.
|
||||
|
||||
For example, an init container can copy a wrapper binary into a shared volume
|
||||
and make that wrapper the relay entrypoint:
|
||||
|
||||
```yaml
|
||||
extraInitContainers:
|
||||
- name: install-wrapper
|
||||
image: example.com/wrapper-init:v1
|
||||
args: [/opt/wrapper/wrapper]
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
volumeMounts:
|
||||
- name: wrapper
|
||||
mountPath: /opt/wrapper
|
||||
|
||||
extraVolumes:
|
||||
- name: wrapper
|
||||
emptyDir: {}
|
||||
|
||||
relay:
|
||||
command: [/opt/wrapper/wrapper]
|
||||
args: [/usr/local/bin/buzz-relay]
|
||||
extraVolumeMounts:
|
||||
- name: wrapper
|
||||
mountPath: /opt/wrapper
|
||||
```
|
||||
|
||||
These values are raw Kubernetes fragments rendered with `toYaml`, not `tpl`.
|
||||
The chart does not validate cross-field relationships: extension names must not
|
||||
collide with chart-owned containers or volumes, mounts must reference existing
|
||||
volumes, and each init container must define an appropriate security context
|
||||
and resources. Empty `relay.command` and `relay.args` arrays preserve the image
|
||||
defaults; non-empty values override its entrypoint and arguments respectively.
|
||||
|
||||
## Device pairing relay
|
||||
|
||||
The chart can run Buzz's stateless pairing WebSocket relay as an independent
|
||||
Deployment and Service using the same image as the main relay:
|
||||
|
||||
```yaml
|
||||
pairingRelay:
|
||||
enabled: true
|
||||
url: wss://pairing.example.com
|
||||
```
|
||||
|
||||
`pairingRelay.url` is advertised in the main relay's NIP-11 document so Buzz
|
||||
clients connect directly to the dedicated endpoint. The chart does not create
|
||||
an Ingress or HTTPRoute for the pairing Service; route the public hostname to
|
||||
`<release>-buzz-pairing:5000` with your platform's ingress configuration.
|
||||
|
||||
## HA (production)
|
||||
|
||||
`replicaCount > 1` hard-requires Redis:
|
||||
|
||||
- Redis (`redis.enabled=true`, `externalRedis.url`, or `REDIS_URL` in `existingSecret`) — for `buzz-pubsub` fan-out
|
||||
|
||||
It does **not** require ReadWriteMany git storage. Git ref/object state is object-store-backed (each request hydrates an ephemeral repo from S3-compatible storage; writer serialization is the object-store pointer CAS — see `docs/git-on-object-storage.md`), and repo-name uniqueness lives in Postgres. Each replica can use its own `ReadWriteOnce` volume; no shared filesystem is needed.
|
||||
|
||||
The chart **template-fails** if the Redis invariant is broken at `replicaCount > 1`. No silent degradation.
|
||||
|
||||
### Relay autoscaling
|
||||
|
||||
The optional HPA scales the relay on the larger recommendation from CPU or
|
||||
average active WebSockets per pod:
|
||||
|
||||
```yaml
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 5
|
||||
maxReplicas: 15
|
||||
targetCPUUtilizationPercentage: 65
|
||||
websocketMetricEnabled: true
|
||||
websocketMetricName: buzz_ws_connections_active
|
||||
targetWebsocketConnections: 5000
|
||||
```
|
||||
|
||||
CPU scaling requires Kubernetes Metrics Server. Set `websocketMetricEnabled: false`
|
||||
for a CPU-only HPA. WebSocket scaling additionally requires a custom-metrics
|
||||
adapter (for example Prometheus Adapter) configured to expose the relay's
|
||||
`buzz_ws_connections_active` gauge as a pod metric with the name in
|
||||
`websocketMetricName`. The chart creates the HPA but deliberately does not
|
||||
install or configure a cluster-wide metrics adapter. Scale-down is gradual by
|
||||
default so long-lived WebSocket connections have time to drain.
|
||||
|
||||
## 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 is eval-only.** The quickstart profile runs an in-cluster
|
||||
MinIO (single replica, no HA, `lookup`-autogenerated credentials) so the
|
||||
relay starts with zero external object storage. Production leaves
|
||||
`minio.enabled` off and points `s3.endpoint` (or `BUZZ_S3_*` in
|
||||
`existingSecret`) at managed S3-compatible storage. The bundled Deployment is
|
||||
not GitOps-safe and is not intended for production traffic.
|
||||
- **Minimal-mode is not yet supported.** The relay's `BUZZ_PUBSUB=local` /
|
||||
filesystem media paths are upstream work in progress — even quickstart
|
||||
currently stands up real Redis and S3 rather than the relay's single-node
|
||||
fallbacks. (Full-text search already runs in Postgres, so no separate search
|
||||
service is provisioned.)
|
||||
- **Cosign signing of the published chart** is a follow-up (the relay image is
|
||||
attested via `actions/attest-build-provenance`; the chart is not yet). The
|
||||
chart itself is published to GHCR — see [Releasing](#releasing).
|
||||
|
||||
## Releasing
|
||||
|
||||
The chart is published to GHCR as an OCI artifact at
|
||||
`oci://ghcr.io/block/buzz/charts/buzz` by the `helm chart` workflow
|
||||
(`.github/workflows/helm-chart.yml`), versioned independently of the desktop app
|
||||
and the relay image via its own `chart-v*` tags. Every PR/`main` push still
|
||||
lints, unit-tests, and render-checks the chart; only a `chart-v*` tag publishes,
|
||||
so an in-progress `main` can never overwrite a released version.
|
||||
|
||||
To cut a release, push a `chart-release/<version>` branch whose `<version>`
|
||||
matches `Chart.yaml`'s `version`; merging it auto-tags `chart-v<version>` and
|
||||
dispatches the publish job (same lane machinery as the desktop and relay
|
||||
releases — see `.github/workflows/auto-tag-on-release-pr-merge.yml`). The publish
|
||||
job fails loudly if the tag version and `Chart.yaml` version disagree.
|
||||
|
||||
## 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,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 + minio 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
|
||||
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,80 @@
|
||||
# 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:
|
||||
# Argo CD >= 3.1 native OCI sources: repoURL must be the FULL chart
|
||||
# artifact path — with the `repoURL: …/charts` + `chart: buzz` split
|
||||
# form, the `chart` field is ignored for oci:// URLs and the fetch
|
||||
# fails with a 403 (`repository:block/buzz/charts:pull` denied). The
|
||||
# spec validator still requires `path`; use "." for OCI sources.
|
||||
repoURL: oci://ghcr.io/block/buzz/charts/buzz
|
||||
path: .
|
||||
targetRevision: 0.1.7
|
||||
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: ""
|
||||
|
||||
|
||||
s3:
|
||||
endpoint: "https://s3.us-east-1.amazonaws.com"
|
||||
bucket: "buzz-media"
|
||||
region: "us-east-1"
|
||||
addressingStyle: virtual
|
||||
# accessKey / secretKey live in buzz-secrets
|
||||
|
||||
persistence:
|
||||
git:
|
||||
enabled: true
|
||||
accessMode: ReadWriteOnce # RWO is fine at any replicaCount (object-store-backed git)
|
||||
storageClass: "" # any RWO class; no shared/RWX filesystem needed
|
||||
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,63 @@
|
||||
# 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
|
||||
|
||||
|
||||
s3:
|
||||
endpoint: "https://s3.us-east-1.amazonaws.com"
|
||||
bucket: "buzz-media"
|
||||
region: "us-east-1"
|
||||
addressingStyle: virtual
|
||||
|
||||
persistence:
|
||||
git:
|
||||
enabled: true
|
||||
accessMode: ReadWriteOnce # RWO is fine at any replicaCount (object-store-backed git)
|
||||
storageClass: "" # any RWO class; no shared/RWX filesystem needed
|
||||
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,27 @@
|
||||
# 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://...
|
||||
# READ_DATABASE_URL — postgres://... (optional read-replica; omit to disable read routing)
|
||||
# REDIS_URL — redis://... (required when replicaCount > 1)
|
||||
# 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"
|
||||
BUZZ_S3_ACCESS_KEY: "REPLACE"
|
||||
BUZZ_S3_SECRET_KEY: "REPLACE"
|
||||
@@ -0,0 +1,93 @@
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
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 }}
|
||||
⚠ 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 a bundled init Job){{ 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), 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 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" . }}
|
||||
@@ -0,0 +1,137 @@
|
||||
{{/* 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 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 -}}
|
||||
|
||||
{{/* 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 -}}
|
||||
|
||||
{{/* Minimum number of relay replicas the release can run. */}}
|
||||
{{- define "buzz.minimumReplicas" -}}
|
||||
{{- if .Values.autoscaling.enabled -}}
|
||||
{{- .Values.autoscaling.minReplicas -}}
|
||||
{{- else -}}
|
||||
{{- .Values.replicaCount -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Effective huddle-audio availability. Nil means safe chart default: on for
|
||||
one replica, off for multi-pod until an SFU/shared-room story exists. */}}
|
||||
{{- define "buzz.huddleAudioAvailable" -}}
|
||||
{{- if kindIs "invalid" .Values.relay.huddleAudioAvailable -}}
|
||||
{{- if gt (include "buzz.minimumReplicas" . | int) 1 -}}false{{- else -}}true{{- end -}}
|
||||
{{- else -}}
|
||||
{{- .Values.relay.huddleAudioAvailable -}}
|
||||
{{- end -}}
|
||||
{{- 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 -}}
|
||||
|
||||
{{- define "buzz.pairingRelaySelectorLabels" -}}
|
||||
{{ include "buzz.selectorLabels" . }}
|
||||
app.kubernetes.io/component: pairing-relay
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,86 @@
|
||||
{{/*
|
||||
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 -}}
|
||||
|
||||
{{/* Multiple replicas require Redis, whether fixed or autoscaled. */}}
|
||||
{{- $minimumReplicas := include "buzz.minimumReplicas" . | int -}}
|
||||
{{- if gt $minimumReplicas 1 -}}
|
||||
{{- if and (not .Values.redis.enabled) (not .Values.externalRedis.url) (not .Values.secrets.existingSecret) -}}
|
||||
{{- fail (printf "minimum replica count %d requires Redis for buzz-pubsub. Enable redis.enabled=true, set externalRedis.url, or provide secrets.existingSecret with key REDIS_URL." $minimumReplicas) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Multiple replicas do NOT require ReadWriteMany git storage.
|
||||
|
||||
Git ref/object state is object-store-backed: every read and write hydrates
|
||||
an ephemeral bare repo from S3-compatible storage per request, and writer
|
||||
serialization is the object-store pointer CAS
|
||||
(docs/git-on-object-storage.md, Inv_NoFork). No persistent git state lives
|
||||
on the PVC, so replicas do not need a shared ReadWriteMany volume to agree
|
||||
on refs. Repo-name uniqueness — the last shared-state need — now lives in
|
||||
Postgres (git_repo_names), not on local disk.
|
||||
|
||||
The prior hard-fail requiring persistence.git.accessMode=ReadWriteMany was
|
||||
removed here: its stated reason ("git on-disk state must be shared across
|
||||
replicas") is no longer true. Redis (validated above) remains the real
|
||||
multi-pod requirement for buzz-pubsub. */}}
|
||||
|
||||
{{/* Autoscaling bounds must be coherent. */}}
|
||||
{{- if .Values.autoscaling.enabled -}}
|
||||
{{- if lt (.Values.autoscaling.minReplicas | int) 1 -}}
|
||||
{{- fail "autoscaling.minReplicas must be at least 1" -}}
|
||||
{{- end -}}
|
||||
{{- if lt (.Values.autoscaling.maxReplicas | int) (.Values.autoscaling.minReplicas | int) -}}
|
||||
{{- fail "autoscaling.maxReplicas must be greater than or equal to autoscaling.minReplicas" -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.autoscaling.websocketMetricEnabled (not .Values.autoscaling.websocketMetricName) -}}
|
||||
{{- fail "autoscaling.websocketMetricName is required when WebSocket scaling is enabled" -}}
|
||||
{{- 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 -}}
|
||||
|
||||
{{/* Pairing relay deployment must have an advertised public URL. */}}
|
||||
{{- if and .Values.pairingRelay.enabled (not .Values.pairingRelay.url) -}}
|
||||
{{- fail "pairingRelay.url is required when pairingRelay.enabled=true" -}}
|
||||
{{- 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 -}}
|
||||
|
||||
{{/* S3 / object-storage source must exist somewhere. With the default
|
||||
BUZZ_GIT_CONFORMANCE_PROBE behavior, an unreachable bucket is detected
|
||||
before the relay opens its listener; operators can explicitly disable that
|
||||
startup gate. */}}
|
||||
{{- 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. By default the relay runs a startup S3 conformance probe and exits if storage is unreachable; disabling BUZZ_GIT_CONFORMANCE_PROBE also removes that startup storage check." -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,263 @@
|
||||
{{- include "buzz.validate" . -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "buzz.fullname" . }}
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
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 }}
|
||||
{{- if or .Values.minio.enabled .Values.extraInitContainers }}
|
||||
initContainers:
|
||||
{{- if .Values.minio.enabled }}
|
||||
# Quickstart only: the bundled MinIO bucket is created by a concurrent
|
||||
# init Job (templates/quickstart-minio-init.yaml). The relay's A3 S3
|
||||
# conformance probe is startup-fatal, so without this gate the relay Pods
|
||||
# CrashLoopBackOff (with growing backoff) until the bucket appears. Block
|
||||
# relay start until the bucket exists — deterministic, no crash-loops.
|
||||
- name: wait-for-bucket
|
||||
image: {{ .Values.minio.mcImage | quote }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.relay.containerSecurityContext | nindent 12 }}
|
||||
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
|
||||
export MC_CONFIG_DIR=/tmp/.mc
|
||||
until mc alias set local {{ include "buzz.minioEndpoint" . }} "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null 2>&1; do
|
||||
echo "waiting for MinIO..."; sleep 2
|
||||
done
|
||||
until mc stat local/{{ .Values.s3.bucket }} >/dev/null 2>&1; do
|
||||
echo "waiting for bucket {{ .Values.s3.bucket }}..."; sleep 2
|
||||
done
|
||||
echo "bucket {{ .Values.s3.bucket }} present"
|
||||
{{- end }}
|
||||
{{- with .Values.extraInitContainers }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: relay
|
||||
image: {{ include "buzz.image" . }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.relay.containerSecurityContext | nindent 12 }}
|
||||
{{- with .Values.relay.command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.relay.args }}
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
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 }} }
|
||||
{{- if .Values.pairingRelay.url }}
|
||||
- { name: BUZZ_PAIRING_RELAY_URL, value: {{ .Values.pairingRelay.url | quote }} }
|
||||
{{- end }}
|
||||
- { name: BUZZ_MEDIA_BASE_URL, value: {{ include "buzz.mediaBaseUrl" . | 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_DRAIN_JITTER_MS, value: {{ .Values.relay.drainJitterMs | 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 }}
|
||||
{{- if .Values.relay.uploadRecords }}
|
||||
- { name: BUZZ_MEDIA_UPLOAD_RECORDS, value: "true" }
|
||||
{{- end }}
|
||||
{{- if .Values.relay.uploadIpHeader }}
|
||||
- { name: BUZZ_MEDIA_UPLOAD_IP_HEADER, value: {{ .Values.relay.uploadIpHeader | quote }} }
|
||||
{{- end }}
|
||||
{{- if .Values.relay.uploadPortHeader }}
|
||||
- { name: BUZZ_MEDIA_UPLOAD_PORT_HEADER, value: {{ .Values.relay.uploadPortHeader | 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_PACK_CACHE_PATH, value: {{ .Values.git.packCachePath | quote }} }
|
||||
- { name: BUZZ_GIT_PACK_CACHE_MAX_BYTES, value: {{ .Values.git.packCacheMaxBytes | quote }} }
|
||||
- { name: BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS, value: {{ .Values.git.packCacheMaxConcurrentPopulations | 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 }} }
|
||||
{{- if .Values.s3.region }}
|
||||
- { name: BUZZ_S3_REGION, value: {{ .Values.s3.region | quote }} }
|
||||
{{- end }}
|
||||
- { name: BUZZ_S3_ADDRESSING_STYLE, value: {{ .Values.s3.addressingStyle | 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: READ_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "buzz.envSecretName" . }}
|
||||
key: READ_DATABASE_URL
|
||||
optional: true
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "buzz.envSecretName" . }}
|
||||
key: REDIS_URL
|
||||
optional: {{ and (eq (include "buzz.minimumReplicas" . | int) 1) (not .Values.redis.enabled) (not .Values.externalRedis.url) }}
|
||||
- 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
|
||||
|
||||
- name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: {{ include "buzz.huddleAudioAvailable" . | quote }}
|
||||
|
||||
{{- 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:
|
||||
- { name: git-repos, mountPath: {{ .Values.persistence.git.mountPath | quote }} }
|
||||
- { name: git-pack-cache, mountPath: {{ .Values.git.packCachePath | quote }} }
|
||||
{{- with .Values.relay.extraVolumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
volumes:
|
||||
- name: git-repos
|
||||
{{- if .Values.persistence.git.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ default (printf "%s-git" (include "buzz.fullname" .)) .Values.persistence.git.existingClaim }}
|
||||
{{- else }}
|
||||
emptyDir:
|
||||
sizeLimit: {{ .Values.persistence.git.size | quote }}
|
||||
{{- end }}
|
||||
- name: git-pack-cache
|
||||
emptyDir:
|
||||
sizeLimit: {{ .Values.git.packCacheVolumeSize | quote }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- /*
|
||||
Free-form extra manifests. Each entry in .Values.extraManifests is rendered
|
||||
through `tpl` (so entries may reference release values / helpers) and emitted
|
||||
as its own document. Lets operators ship companion objects (e.g. Istio mesh
|
||||
glue, NetworkPolicies) alongside the relay without forking the chart.
|
||||
*/ -}}
|
||||
{{- range .Values.extraManifests }}
|
||||
---
|
||||
{{ tpl (toYaml .) $ }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{- include "buzz.validate" . -}}
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "buzz.fullname" . }}
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "buzz.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
behavior:
|
||||
{{- toYaml .Values.autoscaling.behavior | nindent 4 }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- if .Values.autoscaling.websocketMetricEnabled }}
|
||||
- type: Pods
|
||||
pods:
|
||||
metric:
|
||||
name: {{ .Values.autoscaling.websocketMetricName }}
|
||||
target:
|
||||
type: AverageValue
|
||||
averageValue: {{ .Values.autoscaling.targetWebsocketConnections | quote }}
|
||||
{{- end }}
|
||||
{{- 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 }}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,72 @@
|
||||
{{- include "buzz.validate" . -}}
|
||||
{{- if .Values.pairingRelay.enabled -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "buzz.fullname" . }}-pairing
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: pairing-relay
|
||||
spec:
|
||||
replicas: {{ .Values.pairingRelay.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "buzz.pairingRelaySelectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "buzz.pairingRelaySelectorLabels" . | nindent 8 }}
|
||||
{{- with .Values.pairingRelay.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.pairingRelay.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
securityContext:
|
||||
{{- toYaml .Values.relay.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: pairing-relay
|
||||
image: {{ include "buzz.image" . }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command: ["/usr/local/bin/buzz-pair-relay"]
|
||||
securityContext:
|
||||
{{- toYaml .Values.relay.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: BUZZ_PAIR_RELAY_BIND_ADDR
|
||||
value: "0.0.0.0:{{ .Values.pairingRelay.service.port }}"
|
||||
ports:
|
||||
- name: websocket
|
||||
containerPort: {{ .Values.pairingRelay.service.port }}
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: websocket
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: websocket
|
||||
resources:
|
||||
{{- toYaml .Values.pairingRelay.resources | nindent 12 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "buzz.fullname" . }}-pairing
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: pairing-relay
|
||||
{{- with .Values.pairingRelay.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.pairingRelay.service.type }}
|
||||
selector:
|
||||
{{- include "buzz.pairingRelaySelectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: websocket
|
||||
port: {{ .Values.pairingRelay.service.port }}
|
||||
targetPort: websocket
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- include "buzz.validate" . -}}
|
||||
{{- if and .Values.podDisruptionBudget.enabled (gt (include "buzz.minimumReplicas" . | 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 }}
|
||||
@@ -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,64 @@
|
||||
{{- /*
|
||||
Creates the media bucket in the bundled MinIO. Mirrors the docker-compose
|
||||
`minio-init` step (which depends_on minio and runs concurrently, not as a
|
||||
lifecycle hook).
|
||||
|
||||
This is a NORMAL resource, deliberately NOT a helm hook. The relay's A3
|
||||
object-store conformance probe is startup-fatal and does not create the bucket
|
||||
itself, so the bucket must exist for a relay Pod to pass readiness. A
|
||||
`post-install` hook cannot satisfy that: `helm install --wait` blocks on the
|
||||
main resources (the relay Deployment) becoming Ready *before* post-install
|
||||
hooks run, but the relay can never become Ready without the bucket — a
|
||||
deadlock (relays CrashLoopBackOff forever). A `pre-install` hook deadlocks the
|
||||
other way: it would run to completion before MinIO (a normal resource) is
|
||||
created. Running the Job as a normal resource lets helm create MinIO, this Job,
|
||||
and the relay Deployment together; the Job's `until` loop waits for MinIO, and
|
||||
`--wait` waits for the Job to complete alongside the Deployment.
|
||||
|
||||
The name carries the release revision so `helm upgrade` creates a fresh Job
|
||||
(a completed Job's spec is immutable); `ttlSecondsAfterFinished` GCs it.
|
||||
Quickstart-only.
|
||||
*/ -}}
|
||||
{{- if .Values.minio.enabled -}}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "buzz.minioFullname" . }}-init-{{ .Release.Revision }}
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: minio-init
|
||||
spec:
|
||||
backoffLimit: 10
|
||||
ttlSecondsAfterFinished: 120
|
||||
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,105 @@
|
||||
{{- 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 }}
|
||||
|
||||
{{- /* 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 -}}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{- /*
|
||||
Prometheus Operator ServiceMonitor for the relay's /metrics endpoint.
|
||||
Opt-in (serviceMonitor.enabled). Selects the relay Service by the chart's
|
||||
selector labels (which the Service carries in its own metadata.labels) and
|
||||
scrapes the named `metrics` port.
|
||||
*/ -}}
|
||||
{{- if .Values.serviceMonitor.enabled -}}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "buzz.fullname" . }}
|
||||
{{- with .Values.serviceMonitor.namespace }}
|
||||
namespace: {{ . | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "buzz.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "buzz.selectorLabels" . | nindent 6 }}
|
||||
endpoints:
|
||||
- port: metrics
|
||||
interval: {{ .Values.serviceMonitor.interval | quote }}
|
||||
scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout | quote }}
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ .Release.Namespace }}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,70 @@
|
||||
suite: extraManifests + serviceMonitor renderers
|
||||
templates:
|
||||
- templates/extramanifests.yaml
|
||||
- templates/servicemonitor.yaml
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
tests:
|
||||
- it: emits nothing by default (extraManifests empty)
|
||||
template: templates/extramanifests.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders each extraManifests entry, tpl-resolving release values
|
||||
template: templates/extramanifests.yaml
|
||||
set:
|
||||
extraManifests:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: '{{ include "buzz.fullname" . }}-extra'
|
||||
data:
|
||||
host: '{{ .Values.relayUrl }}'
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- equal:
|
||||
path: kind
|
||||
value: ConfigMap
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: RELEASE-NAME-buzz-extra
|
||||
- equal:
|
||||
path: data.host
|
||||
value: wss://buzz.example.com
|
||||
|
||||
- it: omits the ServiceMonitor unless enabled
|
||||
template: templates/servicemonitor.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders a ServiceMonitor scraping the metrics port when enabled
|
||||
template: templates/servicemonitor.yaml
|
||||
set:
|
||||
serviceMonitor.enabled: true
|
||||
serviceMonitor.labels.release: kube-prometheus-stack
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- equal:
|
||||
path: kind
|
||||
value: ServiceMonitor
|
||||
- equal:
|
||||
path: spec.endpoints[0].port
|
||||
value: metrics
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/name"]
|
||||
value: buzz
|
||||
- notExists:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
- equal:
|
||||
path: metadata.labels.release
|
||||
value: kube-prometheus-stack
|
||||
@@ -0,0 +1,20 @@
|
||||
# HA shape: replicas=3 + Redis + RWO git (object-store-backed; no RWM). 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"
|
||||
s3:
|
||||
bucket: "buzz-media"
|
||||
persistence:
|
||||
git:
|
||||
enabled: true
|
||||
accessMode: ReadWriteOnce
|
||||
size: 50Gi
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 2
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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"
|
||||
s3:
|
||||
endpoint: "https://s3.us-east-1.amazonaws.com"
|
||||
bucket: "buzz-media"
|
||||
persistence:
|
||||
git:
|
||||
enabled: true
|
||||
accessMode: ReadWriteOnce
|
||||
size: 50Gi
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
@@ -0,0 +1,79 @@
|
||||
suite: relay horizontal pod autoscaler
|
||||
templates:
|
||||
- templates/hpa.yaml
|
||||
- templates/deployment.yaml
|
||||
- templates/secret-chart.yaml
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
tests:
|
||||
- it: omits the HPA by default
|
||||
template: templates/hpa.yaml
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: scales the relay on either CPU or average pod WebSockets
|
||||
template: templates/hpa.yaml
|
||||
set:
|
||||
autoscaling.enabled: true
|
||||
asserts:
|
||||
- isAPIVersion:
|
||||
of: autoscaling/v2
|
||||
- isKind:
|
||||
of: HorizontalPodAutoscaler
|
||||
- equal:
|
||||
path: spec.minReplicas
|
||||
value: 5
|
||||
- equal:
|
||||
path: spec.maxReplicas
|
||||
value: 15
|
||||
- equal:
|
||||
path: spec.scaleTargetRef.name
|
||||
value: RELEASE-NAME-buzz
|
||||
- equal:
|
||||
path: spec.metrics[0].resource.target.averageUtilization
|
||||
value: 65
|
||||
- equal:
|
||||
path: spec.metrics[1].pods.metric.name
|
||||
value: buzz_ws_connections_active
|
||||
- equal:
|
||||
path: spec.metrics[1].pods.target.averageValue
|
||||
value: "5000"
|
||||
- equal:
|
||||
path: spec.behavior.scaleDown.stabilizationWindowSeconds
|
||||
value: 600
|
||||
|
||||
- it: supports CPU-only autoscaling without a custom metrics adapter
|
||||
template: templates/hpa.yaml
|
||||
set:
|
||||
autoscaling.enabled: true
|
||||
autoscaling.websocketMetricEnabled: false
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.metrics
|
||||
count: 1
|
||||
- equal:
|
||||
path: spec.metrics[0].type
|
||||
value: Resource
|
||||
- equal:
|
||||
path: spec.metrics[0].resource.name
|
||||
value: cpu
|
||||
|
||||
- it: leaves Deployment replicas to the HPA and disables in-process huddle audio
|
||||
template: templates/deployment.yaml
|
||||
set:
|
||||
autoscaling.enabled: true
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.replicas
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: "false"
|
||||
@@ -0,0 +1,79 @@
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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,54 @@
|
||||
suite: optional pairing relay
|
||||
templates:
|
||||
- templates/deployment.yaml
|
||||
- templates/secret-chart.yaml
|
||||
- templates/pairing-relay.yaml
|
||||
- templates/serviceaccount.yaml
|
||||
tests:
|
||||
- it: does not render the pairing relay by default
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
template: templates/pairing-relay.yaml
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_PAIRING_RELAY_URL
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: renders and advertises the configured pairing relay
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
pairingRelay.enabled: true
|
||||
pairingRelay.url: wss://pairing.buzz.xyz
|
||||
asserts:
|
||||
- equal:
|
||||
path: kind
|
||||
value: Deployment
|
||||
documentIndex: 0
|
||||
template: templates/pairing-relay.yaml
|
||||
- equal:
|
||||
path: kind
|
||||
value: Service
|
||||
documentIndex: 1
|
||||
template: templates/pairing-relay.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_PAIRING_RELAY_URL
|
||||
value: wss://pairing.buzz.xyz
|
||||
template: templates/deployment.yaml
|
||||
@@ -0,0 +1,90 @@
|
||||
suite: quickstart bundled services
|
||||
# The dev quickstart must stand up MinIO 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/deployment.yaml
|
||||
- templates/secret-chart.yaml
|
||||
- templates/service.yaml
|
||||
tests:
|
||||
- it: renders the in-cluster MinIO Deployment when minio.enabled
|
||||
release:
|
||||
name: rel
|
||||
revision: 1
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
postgresql.enabled: true
|
||||
redis.enabled: true
|
||||
minio.enabled: true
|
||||
asserts:
|
||||
- containsDocument:
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
name: rel-buzz-minio
|
||||
template: templates/quickstart-minio.yaml
|
||||
documentIndex: 0
|
||||
# The bucket-init Job is a NORMAL resource (revision-suffixed name), not a
|
||||
# helm hook: a post-install hook deadlocks against `--wait` because the
|
||||
# relay's startup-fatal S3 probe can't pass until the bucket exists.
|
||||
- containsDocument:
|
||||
kind: Job
|
||||
apiVersion: batch/v1
|
||||
name: rel-buzz-minio-init-1
|
||||
template: templates/quickstart-minio-init.yaml
|
||||
- notExists:
|
||||
path: metadata.annotations["helm.sh/hook"]
|
||||
template: templates/quickstart-minio-init.yaml
|
||||
|
||||
- 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
|
||||
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 the bundled MinIO credentials the relay reads
|
||||
release:
|
||||
name: rel
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
postgresql.enabled: true
|
||||
redis.enabled: true
|
||||
minio.enabled: true
|
||||
asserts:
|
||||
# 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 pods)
|
||||
release:
|
||||
name: rel
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
postgresql.enabled: true
|
||||
redis.enabled: true
|
||||
minio.enabled: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/component"]
|
||||
value: relay
|
||||
template: templates/service.yaml
|
||||
@@ -0,0 +1,17 @@
|
||||
suite: quickstart guards
|
||||
# Quickstart autogenerates MinIO 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
|
||||
minio.enabled: true
|
||||
secrets.existingSecret: "buzz-secrets"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "incompatible with secrets.existingSecret"
|
||||
@@ -0,0 +1,291 @@
|
||||
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)
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
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
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_S3_REGION
|
||||
any: true
|
||||
template: templates/deployment.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_S3_ADDRESSING_STYLE
|
||||
value: "path"
|
||||
template: templates/deployment.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: "true"
|
||||
template: templates/deployment.yaml
|
||||
- it: renders virtual-hosted S3 addressing for providers that require it
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: https://storage.railway.app
|
||||
s3.bucket: buzz-media-example
|
||||
s3.region: auto
|
||||
s3.addressingStyle: virtual
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_S3_REGION
|
||||
value: "auto"
|
||||
template: templates/deployment.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_S3_ADDRESSING_STYLE
|
||||
value: "virtual"
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: lets an explicit value disable huddle audio in a single-replica render
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
relay.huddleAudioAvailable: false
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: "false"
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: lets an explicit value enable huddle audio in HA when the operator owns external audio behavior
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
replicaCount: 3
|
||||
persistence.git.accessMode: ReadWriteOnce
|
||||
relay.huddleAudioAvailable: true
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: "true"
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: renders HA cleanly with replicaCount=3 + RWO git + Redis (no RWM needed)
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
replicaCount: 3
|
||||
persistence.git.accessMode: ReadWriteOnce
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 3
|
||||
template: templates/deployment.yaml
|
||||
- equal:
|
||||
path: spec.accessModes[0]
|
||||
value: ReadWriteOnce
|
||||
template: templates/pvc-git.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: BUZZ_HUDDLE_AUDIO_AVAILABLE
|
||||
value: "false"
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: mounts a per-pod emptyDir git scratch volume when persistence is disabled
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
replicaCount: 5
|
||||
persistence.git.enabled: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 5
|
||||
template: templates/deployment.yaml
|
||||
# git-repos is still mounted (BUZZ_GIT_REPO_PATH must be a real dir)…
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
name: git-repos
|
||||
mountPath: /var/lib/buzz/git
|
||||
template: templates/deployment.yaml
|
||||
# …but as a per-pod emptyDir, not a shared PVC (no multi-attach).
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: git-repos
|
||||
emptyDir:
|
||||
sizeLimit: 10Gi
|
||||
template: templates/deployment.yaml
|
||||
# No PVC is rendered at all when persistence is disabled.
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
template: templates/pvc-git.yaml
|
||||
|
||||
- it: preserves image defaults when Pod extensions are empty
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.template.spec.initContainers
|
||||
template: templates/deployment.yaml
|
||||
- notExists:
|
||||
path: spec.template.spec.containers[0].command
|
||||
template: templates/deployment.yaml
|
||||
- notExists:
|
||||
path: spec.template.spec.containers[0].args
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: appends generic Pod extensions and overrides the relay command
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
externalRedis.url: redis://h:6379
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
relay.command:
|
||||
- /opt/wrapper/wrapper
|
||||
relay.args:
|
||||
- /usr/local/bin/buzz-relay
|
||||
relay.extraVolumeMounts:
|
||||
- name: wrapper
|
||||
mountPath: /opt/wrapper
|
||||
extraInitContainers:
|
||||
- name: install-wrapper
|
||||
image: example.com/wrapper-init:v1
|
||||
args:
|
||||
- /opt/wrapper/wrapper
|
||||
env:
|
||||
- name: LITERAL_TEMPLATE
|
||||
value: '{{ .Release.Name }}'
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
volumeMounts:
|
||||
- name: wrapper
|
||||
mountPath: /opt/wrapper
|
||||
extraVolumes:
|
||||
- name: wrapper
|
||||
emptyDir: {}
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].name
|
||||
value: install-wrapper
|
||||
template: templates/deployment.yaml
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].securityContext.runAsNonRoot
|
||||
value: true
|
||||
template: templates/deployment.yaml
|
||||
# Extension fragments are deliberately rendered with toYaml, not tpl.
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].env[0].value
|
||||
value: '{{ .Release.Name }}'
|
||||
template: templates/deployment.yaml
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].command
|
||||
value:
|
||||
- /opt/wrapper/wrapper
|
||||
template: templates/deployment.yaml
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].args
|
||||
value:
|
||||
- /usr/local/bin/buzz-relay
|
||||
template: templates/deployment.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
name: wrapper
|
||||
mountPath: /opt/wrapper
|
||||
template: templates/deployment.yaml
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: wrapper
|
||||
emptyDir: {}
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: appends generic init containers after the bundled MinIO readiness gate
|
||||
release:
|
||||
name: rel
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
postgresql.enabled: true
|
||||
redis.enabled: true
|
||||
minio.enabled: true
|
||||
extraInitContainers:
|
||||
- name: install-wrapper
|
||||
image: example.com/wrapper-init:v1
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[0].name
|
||||
value: wait-for-bucket
|
||||
template: templates/deployment.yaml
|
||||
- equal:
|
||||
path: spec.template.spec.initContainers[1].name
|
||||
value: install-wrapper
|
||||
template: templates/deployment.yaml
|
||||
@@ -0,0 +1,180 @@
|
||||
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
|
||||
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
|
||||
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
|
||||
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: Deployment env points READ_DATABASE_URL at existingSecret as optional
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
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: READ_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: buzz-secrets
|
||||
key: READ_DATABASE_URL
|
||||
optional: true
|
||||
template: templates/deployment.yaml
|
||||
|
||||
- it: READ_DATABASE_URL stays optional against the chart-managed Secret
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.accessKey: a
|
||||
s3.secretKey: s
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: READ_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: RELEASE-NAME-buzz-relay
|
||||
key: READ_DATABASE_URL
|
||||
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
|
||||
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
|
||||
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
|
||||
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,79 @@
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
replicaCount: 3
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "minimum replica count 3 requires Redis"
|
||||
|
||||
- it: fails when ingress and httproute both enabled
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
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"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "Postgres source missing"
|
||||
|
||||
- it: rejects an invalid S3 addressing style
|
||||
set:
|
||||
relayUrl: wss://buzz.example.com
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
externalPostgresql.url: postgres://u:p@h:5432/d
|
||||
s3.endpoint: http://minio:9000
|
||||
s3.addressingStyle: auto
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "s3.addressingStyle: s3.addressingStyle must be one of the following:.*path.*virtual"
|
||||
|
||||
- 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
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "S3/object-storage source missing"
|
||||
@@ -0,0 +1,301 @@
|
||||
{
|
||||
"$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) — enforced by _validate.tpl. Git storage does NOT need ReadWriteMany: git state is object-store-backed and repo names live in Postgres, so ReadWriteOnce is fine per replica."
|
||||
},
|
||||
"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 },
|
||||
"drainJitterMs": { "type": "integer", "minimum": 0 },
|
||||
"requireAuthToken": { "type": "boolean" },
|
||||
"requireRelayMembership": { "type": "boolean" },
|
||||
"allowNipOaAuth": { "type": "boolean" },
|
||||
"huddleAudioAvailable": {
|
||||
"type": ["boolean", "null"],
|
||||
"description": "Whether relay huddle audio is available. null means chart default: true for one replica, false for replicaCount > 1. Explicit true with replicaCount > 1 means the operator accepts/owns external multi-pod audio/SFU behavior."
|
||||
},
|
||||
"pubkeyAllowlist": { "type": "boolean" },
|
||||
"corsOrigins": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"ephemeralTtlOverride": { "type": "integer", "minimum": 0 },
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Optional relay container entrypoint override. Empty preserves the image default."
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Optional relay container arguments override. Empty preserves the image default."
|
||||
},
|
||||
"extraVolumeMounts": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Raw Kubernetes volumeMount fragments appended to the relay container."
|
||||
}
|
||||
}
|
||||
},
|
||||
"extraInitContainers": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Raw Kubernetes init-container fragments appended to the relay Pod."
|
||||
},
|
||||
"extraVolumes": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Raw Kubernetes volume fragments appended to the relay Pod."
|
||||
},
|
||||
"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?://.+)?$" }
|
||||
}
|
||||
},
|
||||
"s3": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"endpoint": { "type": "string", "pattern": "^(https?://.+)?$" },
|
||||
"bucket": { "type": "string", "minLength": 1 },
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "Optional S3 region used for SigV4 signing. When empty, BUZZ_S3_REGION is omitted so the relay can use AWS_REGION or its own default."
|
||||
},
|
||||
"addressingStyle": {
|
||||
"type": "string",
|
||||
"enum": ["path", "virtual"],
|
||||
"description": "S3 URL style shared by media and Git/CAS clients. Defaults to path for bundled MinIO compatibility."
|
||||
},
|
||||
"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 },
|
||||
"packCachePath": { "type": "string", "minLength": 1 },
|
||||
"packCacheMaxBytes": { "type": "integer", "minimum": 0 },
|
||||
"packCacheMaxConcurrentPopulations": { "type": "integer", "minimum": 1 },
|
||||
"packCacheVolumeSize": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?(E|P|T|G|M|K|Ei|Pi|Ti|Gi|Mi|Ki)?$" },
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"pairingRelay": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"url": {
|
||||
"type": "string",
|
||||
"pattern": "^(wss?://.+)?$",
|
||||
"description": "Public WebSocket URL advertised to clients for NIP-AB device pairing."
|
||||
},
|
||||
"replicaCount": { "type": "integer", "minimum": 1 },
|
||||
"service": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
|
||||
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"podAnnotations": { "type": "object" },
|
||||
"podLabels": { "type": "object" },
|
||||
"resources": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"extraManifests": {
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
# Default values for buzz.
|
||||
#
|
||||
# Two supported tiers:
|
||||
#
|
||||
# PRODUCTION (default) — external Postgres/Redis/S3, existingSecret
|
||||
# refs everywhere, no chart-side autogeneration, GitOps-safe (ArgoCD/Flux).
|
||||
# HA-ready: replicaCount >= 2 (requires Redis; git state is object-store-
|
||||
# backed, so no ReadWriteMany volume is needed — RWO per replica is fine).
|
||||
#
|
||||
# QUICKSTART — bundles in-cluster Postgres + Redis + MinIO 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.
|
||||
# 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) 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).
|
||||
# It does NOT require ReadWriteMany git storage: git ref/object state is
|
||||
# object-store-backed (each request hydrates an ephemeral repo from S3; writer
|
||||
# serialization is the object-store pointer CAS), and repo-name uniqueness lives
|
||||
# in Postgres. Each replica can use its own ReadWriteOnce volume (or none).
|
||||
replicaCount: 1
|
||||
|
||||
# ── Autoscaling ──────────────────────────────────────────────────────────────
|
||||
# Requires Metrics Server for CPU. Optional WebSocket scaling additionally
|
||||
# requires a custom-metrics adapter exposing its pod-level Prometheus gauge.
|
||||
# Kubernetes HPA uses the larger replica recommendation from enabled metrics.
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 5
|
||||
maxReplicas: 15
|
||||
targetCPUUtilizationPercentage: 65
|
||||
websocketMetricEnabled: true
|
||||
websocketMetricName: buzz_ws_connections_active
|
||||
targetWebsocketConnections: 5000
|
||||
behavior:
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 60
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 60
|
||||
selectPolicy: Max
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 600
|
||||
policies:
|
||||
- type: Pods
|
||||
value: 1
|
||||
periodSeconds: 120
|
||||
selectPolicy: Min
|
||||
|
||||
# ── 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)
|
||||
# READ_DATABASE_URL — optional Postgres read-replica URL; omit to keep all reads on the writer
|
||||
# REDIS_URL — full Redis URL with auth
|
||||
# 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
|
||||
# Graceful-shutdown reconnect jitter. On SIGTERM the relay closes every live
|
||||
# WebSocket with a 1012 Service Restart frame; with a rolling deploy this can
|
||||
# release a whole pod's sockets at once and stampede reconnects into the DB
|
||||
# pool. A positive value (milliseconds) spreads each close over a per-socket
|
||||
# random delay in [1, drainJitterMs], smoothing the reconnect herd. 0 (the
|
||||
# default) closes all sockets at once, preserving the previous behavior.
|
||||
# Values above 20000 are capped to 20000, leaving close-frame delivery
|
||||
# headroom under the relay's 30s hard-drain timeout (itself inside the 60s
|
||||
# terminationGracePeriodSeconds below).
|
||||
drainJitterMs: 0
|
||||
requireAuthToken: true
|
||||
requireRelayMembership: true
|
||||
allowNipOaAuth: true
|
||||
pubkeyAllowlist: false
|
||||
corsOrigins: []
|
||||
# Huddle audio is safe only for single-pod relay deployments until an SFU
|
||||
# exists. null lets the chart render false automatically when
|
||||
# replicaCount > 1. Explicit true with replicaCount > 1 means the operator
|
||||
# accepts/owns the external multi-pod audio/SFU behavior.
|
||||
huddleAudioAvailable: null
|
||||
ephemeralTtlOverride: 0
|
||||
# Per-upload-event records (`_uploads/` moderation side channel). Off by
|
||||
# default. Operators hosting communities for other people may have legal
|
||||
# obligations (e.g. NCMEC reporting for US-serving providers) that require
|
||||
# recording the network address of an upload; Buzz never collects IPs unless
|
||||
# uploadIpHeader is set. When set (e.g. "cf-connecting-ip"), the connecting
|
||||
# address reported by YOUR trusted edge is stored in the per-event record
|
||||
# only — never served to clients, never in event data. uploadRecords must be
|
||||
# true for uploadIpHeader to be valid (startup-checked).
|
||||
uploadRecords: false
|
||||
uploadIpHeader: ""
|
||||
uploadPortHeader: ""
|
||||
|
||||
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
|
||||
|
||||
# Optional image entrypoint/arguments overrides. Empty arrays preserve the
|
||||
# relay image's defaults. Consumers own compatibility with the selected image.
|
||||
command: []
|
||||
args: []
|
||||
# Appended to the chart-owned relay mounts. Names must match extraVolumes (or
|
||||
# another volume supplied by the platform) and must not collide with built-ins.
|
||||
extraVolumeMounts: []
|
||||
|
||||
extraEnv: []
|
||||
extraEnvFrom: []
|
||||
|
||||
# ── Pod extensions ──────────────────────────────────────────────────────────
|
||||
# Raw Kubernetes fragments appended to the relay Pod. They are rendered with
|
||||
# toYaml, not tpl. Init containers must define their own securityContext and
|
||||
# resources; names must not collide with chart-owned containers or volumes.
|
||||
extraInitContainers: []
|
||||
extraVolumes: []
|
||||
|
||||
# ── Device pairing relay ─────────────────────────────────────────────────────
|
||||
# Optional, stateless NIP-AB relay. When enabled, the main relay advertises
|
||||
# pairingRelay.url in NIP-11 and Buzz clients use it instead of the legacy
|
||||
# same-host /pair convention.
|
||||
pairingRelay:
|
||||
enabled: false
|
||||
url: ""
|
||||
replicaCount: 1
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 5000
|
||||
annotations: {}
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "32Mi"
|
||||
limits:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
|
||||
# ── 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 scratch volume ───────────────────────────────────────────────────────
|
||||
# Ephemeral working space only. No persistent git state lives here — reads/writes
|
||||
# hydrate ephemeral repos from object storage per request, and repo-name
|
||||
# uniqueness lives in Postgres.
|
||||
#
|
||||
# enabled: true → mount a PVC at mountPath (durable across pod restarts, but a
|
||||
# single ReadWriteOnce PVC binds to one node, so it does NOT support multi-pod
|
||||
# scheduling across nodes on a Deployment).
|
||||
# enabled: false → mount a per-pod emptyDir at mountPath (pure scratch), bounded
|
||||
# by size. This is the correct choice for multi-replica HA: each pod gets its
|
||||
# own local working space, nothing is shared, and there is no volume to
|
||||
# multi-attach. Safe because the object store + Postgres are the sources of
|
||||
# truth, not this disk.
|
||||
persistence:
|
||||
git:
|
||||
enabled: true
|
||||
mountPath: /var/lib/buzz/git
|
||||
storageClass: ""
|
||||
accessMode: ReadWriteOnce
|
||||
size: 10Gi # PVC capacity or emptyDir sizeLimit
|
||||
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 — placeholder example, sadscan:disable np.postgres.1
|
||||
|
||||
# ── 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
|
||||
|
||||
# ── 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.
|
||||
#
|
||||
# Storage metrics (hourly bucket sweep, BUZZ_STORAGE_METRICS — see env docs):
|
||||
# the credentials above must additionally grant `s3:ListBucket` on the bucket
|
||||
# ARN itself (bucket-level; distinct from the object-level GetObject/
|
||||
# PutObject/DeleteObject perms already required for media). Without it the
|
||||
# first sweep fails AccessDenied and buzz_storage_sweep_ok stays 0 — no other
|
||||
# media functionality is affected. Set BUZZ_STORAGE_METRICS=off to disable
|
||||
# the sweep entirely on a deployment that can't grant it.
|
||||
# Note: buzz_storage_sweep_failures is a process-local gauge — on leader
|
||||
# failover it resets to the new leader's local count, not a global total.
|
||||
# Note: on a failed sweep attempt, the next retry fires on the next usage tick
|
||||
# (default 300 s BUZZ_USAGE_METRICS_INTERVAL_SECS), not at sweep-interval
|
||||
# cadence — so a permanently missing s3:ListBucket yields one cheap LIST call
|
||||
# per tick until the permission is added.
|
||||
s3:
|
||||
endpoint: ""
|
||||
bucket: "buzz-media"
|
||||
# Optional SigV4 signing region. Leave empty to preserve the relay's
|
||||
# AWS_REGION fallback; set the provider's credential value when needed.
|
||||
region: ""
|
||||
# path: https://endpoint/bucket/key (bundled MinIO-compatible default)
|
||||
# virtual: https://bucket.endpoint/key (standard S3; required by new Railway buckets)
|
||||
addressingStyle: path
|
||||
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
|
||||
packCachePath: /var/cache/buzz/git-packs
|
||||
packCacheMaxBytes: 5368709120 # 5 GiB
|
||||
packCacheMaxConcurrentPopulations: 2
|
||||
packCacheVolumeSize: 7Gi # per-pod emptyDir; includes cold-population staging
|
||||
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: []
|
||||
@@ -0,0 +1,57 @@
|
||||
# Buzz production Docker Compose environment.
|
||||
# Copy to .env and replace every CHANGE_ME value before running.
|
||||
# The bootstrap script should generate this file for normal users.
|
||||
|
||||
# Image published by the public image pipeline. Use `:main` for pre-release testing. Pin `:sha-<7>` or a semver release tag for production.
|
||||
BUZZ_IMAGE=ghcr.io/block/buzz:main
|
||||
|
||||
# Public host name. Used by compose.caddy.yml and URL-derived settings below.
|
||||
BUZZ_DOMAIN=buzz.example.com
|
||||
RELAY_URL=wss://buzz.example.com
|
||||
BUZZ_MEDIA_BASE_URL=https://buzz.example.com/media
|
||||
BUZZ_MEDIA_SERVER_DOMAIN=buzz.example.com
|
||||
BUZZ_CORS_ORIGINS=https://buzz.example.com
|
||||
|
||||
# Production defaults. Closed relay mode requires RELAY_OWNER_PUBKEY and a stable relay key.
|
||||
BUZZ_REQUIRE_AUTH_TOKEN=true
|
||||
BUZZ_REQUIRE_RELAY_MEMBERSHIP=true
|
||||
BUZZ_ALLOW_NIP_OA_AUTH=true
|
||||
BUZZ_AUTO_MIGRATE=true
|
||||
BUZZ_GIT_CONFORMANCE_PROBE=true
|
||||
RUST_LOG=buzz_relay=info,buzz_db=info,buzz_auth=info,buzz_pubsub=info,tower_http=info
|
||||
|
||||
# Owner identity. Set to a 64-character hex Nostr pubkey.
|
||||
RELAY_OWNER_PUBKEY=CHANGE_ME_OWNER_PUBKEY_HEX
|
||||
|
||||
# Stable secrets. Generate once, keep in .env, and back up securely.
|
||||
BUZZ_RELAY_PRIVATE_KEY=CHANGE_ME_64_HEX_PRIVATE_KEY
|
||||
BUZZ_GIT_HOOK_HMAC_SECRET=CHANGE_ME_RANDOM_64_HEX
|
||||
POSTGRES_DB=buzz
|
||||
POSTGRES_USER=buzz
|
||||
POSTGRES_PASSWORD=CHANGE_ME_RANDOM_PASSWORD
|
||||
REDIS_PASSWORD=CHANGE_ME_RANDOM_PASSWORD
|
||||
BUZZ_S3_ACCESS_KEY=CHANGE_ME_RANDOM_ACCESS_KEY
|
||||
BUZZ_S3_SECRET_KEY=CHANGE_ME_RANDOM_SECRET_KEY
|
||||
BUZZ_S3_BUCKET=buzz-media
|
||||
# Bundled MinIO uses path-style URLs; deploy/compose/compose.yml pins this.
|
||||
BUZZ_S3_ADDRESSING_STYLE=path
|
||||
|
||||
# Optional host ports. Base compose publishes the relay directly on BUZZ_HTTP_PORT.
|
||||
BUZZ_HTTP_PORT=3000
|
||||
|
||||
# Optional custom Web UI. Setting this to an absolute directory containing
|
||||
# index.html makes run.sh automatically include compose.web.yml. Leave unset to
|
||||
# use the Web UI bundled in BUZZ_IMAGE.
|
||||
# BUZZ_WEB_HOST_DIR=/opt/buzz/web/dist
|
||||
|
||||
# Caddy host ports. Only used with compose.caddy.yml.
|
||||
CADDY_HTTP_PORT=80
|
||||
CADDY_HTTPS_PORT=443
|
||||
|
||||
# Dev override ports. Only used with compose.dev.yml.
|
||||
POSTGRES_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
MINIO_API_PORT=9000
|
||||
MINIO_CONSOLE_PORT=9001
|
||||
ADMINER_PORT=8082
|
||||
PROMETHEUS_PORT=9090
|
||||
@@ -0,0 +1,5 @@
|
||||
{$BUZZ_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
reverse_proxy relay:3000
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Buzz Docker Compose deployment
|
||||
|
||||
This is the single-node/VPS deployment bundle. It is intentionally separate from
|
||||
the root `docker-compose.yml`, which remains local development infrastructure.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd deploy/compose
|
||||
cp .env.example .env
|
||||
$EDITOR .env # replace every CHANGE_ME value
|
||||
./run.sh start
|
||||
```
|
||||
|
||||
For a public VPS with automatic Let's Encrypt certificates:
|
||||
|
||||
```bash
|
||||
cd deploy/compose
|
||||
BUZZ_COMPOSE_TLS=true ./run.sh start
|
||||
```
|
||||
|
||||
The bootstrap script should eventually replace manual `.env` editing for normal
|
||||
users. It is responsible for generating stable secrets and, optionally, an owner
|
||||
keypair.
|
||||
|
||||
## Production notes
|
||||
|
||||
- Requires Docker Compose v2.24.4 or newer; the TLS override uses Compose's
|
||||
`!reset` tag to remove the direct relay port when Caddy terminates HTTPS.
|
||||
- Default `BUZZ_IMAGE` tracks `ghcr.io/block/buzz:main` for early testing. Pin it to `ghcr.io/block/buzz:sha-<7>` or a semver release tag for production once available.
|
||||
- Keep `BUZZ_RELAY_PRIVATE_KEY`, `BUZZ_GIT_HOOK_HMAC_SECRET`, database/Redis,
|
||||
and S3 secrets stable across restarts.
|
||||
- `RELAY_OWNER_PUBKEY` is intentionally not prefixed with `BUZZ_`; it must be a
|
||||
64-character hex Nostr pubkey when closed relay mode is enabled.
|
||||
- `BUZZ_AUTO_MIGRATE` is opt-in. Set `BUZZ_AUTO_MIGRATE=true` or run
|
||||
`buzz-admin migrate` before starting the relay when bootstrapping a fresh
|
||||
database. Auto-migration requires an image that includes embedded SQLx
|
||||
migrations.
|
||||
- The stack uses Postgres, Redis, MinIO, and a git data volume because
|
||||
those are real Buzz dependencies today. Minimal mode can simplify this later.
|
||||
- The bundled Compose stack fixes the relay endpoint to `http://minio:9000` and
|
||||
`BUZZ_S3_ADDRESSING_STYLE=path`: Docker DNS resolves `minio`, not
|
||||
`<bucket>.minio`. It is not configurable for an external S3 provider through
|
||||
`.env`; use the Helm chart or a custom Compose configuration for providers
|
||||
such as new Railway Storage Buckets that require `virtual` addressing.
|
||||
|
||||
Run `./run.sh backup-hint` for the backup checklist.
|
||||
|
||||
## Validation
|
||||
|
||||
Before sharing an install link publicly, verify a fresh install with:
|
||||
|
||||
```bash
|
||||
cd deploy/compose
|
||||
cp .env.example .env
|
||||
$EDITOR .env
|
||||
./run.sh config
|
||||
./run.sh start
|
||||
curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness"
|
||||
./run.sh status
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
relay:
|
||||
ports: !reset []
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
depends_on:
|
||||
relay:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
BUZZ_DOMAIN: ${BUZZ_DOMAIN:?set BUZZ_DOMAIN}
|
||||
ports:
|
||||
- "${CADDY_HTTP_PORT:-80}:80"
|
||||
- "${CADDY_HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- buzz-caddy-data:/data
|
||||
- buzz-caddy-config:/config
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
volumes:
|
||||
buzz-caddy-data:
|
||||
labels:
|
||||
com.buzz.volume: caddy-data
|
||||
buzz-caddy-config:
|
||||
labels:
|
||||
com.buzz.volume: caddy-config
|
||||
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
postgres:
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
|
||||
redis:
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
|
||||
minio:
|
||||
ports:
|
||||
- "${MINIO_API_PORT:-9000}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: buzz-adminer
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: postgres
|
||||
ports:
|
||||
- "${ADMINER_PORT:-8082}:8080"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: buzz-prometheus
|
||||
volumes:
|
||||
- ../../prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- buzz-prometheus-data:/prometheus
|
||||
ports:
|
||||
- "${PROMETHEUS_PORT:-9090}:9090"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
volumes:
|
||||
buzz-prometheus-data:
|
||||
labels:
|
||||
com.buzz.volume: prometheus
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
relay:
|
||||
environment:
|
||||
BUZZ_SERVE_GIT_WEB_GUI: "true"
|
||||
BUZZ_WEB_DIR: /srv/buzz/web
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${BUZZ_WEB_HOST_DIR:?set BUZZ_WEB_HOST_DIR to an absolute web dist directory}
|
||||
target: /srv/buzz/web
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: false
|
||||
@@ -0,0 +1,142 @@
|
||||
name: buzz-prod
|
||||
|
||||
services:
|
||||
relay:
|
||||
image: ${BUZZ_IMAGE:-ghcr.io/block/buzz:main}
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
BUZZ_BIND_ADDR: 0.0.0.0:3000
|
||||
BUZZ_HEALTH_PORT: "8080"
|
||||
BUZZ_METRICS_PORT: "9102"
|
||||
DATABASE_URL: postgres://${POSTGRES_USER:-buzz}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-buzz}
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:?set REDIS_PASSWORD}@redis:6379
|
||||
BUZZ_S3_ENDPOINT: http://minio:9000
|
||||
# Docker DNS resolves `minio`, not arbitrary `<bucket>.minio` hosts.
|
||||
BUZZ_S3_ADDRESSING_STYLE: path
|
||||
BUZZ_S3_ACCESS_KEY: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY}
|
||||
BUZZ_S3_SECRET_KEY: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY}
|
||||
BUZZ_S3_BUCKET: ${BUZZ_S3_BUCKET:-buzz-media}
|
||||
BUZZ_GIT_REPO_PATH: /data/git
|
||||
BUZZ_AUTO_MIGRATE: ${BUZZ_AUTO_MIGRATE:-false}
|
||||
BUZZ_GIT_CONFORMANCE_PROBE: ${BUZZ_GIT_CONFORMANCE_PROBE:-true}
|
||||
ports:
|
||||
- "${BUZZ_HTTP_PORT:-3000}:3000"
|
||||
volumes:
|
||||
- buzz-git-data:/data/git
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
minio-init:
|
||||
condition: service_completed_successfully
|
||||
# Probe /_readiness over /dev/tcp because the runtime image has bash but no curl/wget/socat.
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"bash -ec 'exec 3<>/dev/tcp/127.0.0.1/8080; printf \"GET /_readiness HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n\" >&3; grep -q \"200 OK\" <&3'",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
start_period: 30s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-buzz}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-buzz}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- buzz-postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?set REDIS_PASSWORD}"]
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?set REDIS_PASSWORD}
|
||||
volumes:
|
||||
- buzz-redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
start_period: 5s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY}
|
||||
MINIO_ROOT_PASSWORD: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY}
|
||||
volumes:
|
||||
- buzz-minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
minio-init:
|
||||
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
BUZZ_S3_ACCESS_KEY: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY}
|
||||
BUZZ_S3_SECRET_KEY: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY}
|
||||
BUZZ_S3_BUCKET: ${BUZZ_S3_BUCKET:-buzz-media}
|
||||
entrypoint: >
|
||||
/bin/sh -euc '
|
||||
mc alias set local http://minio:9000 "$${BUZZ_S3_ACCESS_KEY}" "$${BUZZ_S3_SECRET_KEY}"
|
||||
mc mb --ignore-existing "local/$${BUZZ_S3_BUCKET}"
|
||||
mc anonymous set none "local/$${BUZZ_S3_BUCKET}"
|
||||
'
|
||||
restart: "no"
|
||||
networks:
|
||||
- buzz-net
|
||||
|
||||
volumes:
|
||||
buzz-postgres-data:
|
||||
labels:
|
||||
com.buzz.volume: postgres
|
||||
buzz-redis-data:
|
||||
labels:
|
||||
com.buzz.volume: redis
|
||||
buzz-minio-data:
|
||||
labels:
|
||||
com.buzz.volume: minio
|
||||
buzz-git-data:
|
||||
labels:
|
||||
com.buzz.volume: git
|
||||
|
||||
networks:
|
||||
buzz-net:
|
||||
driver: bridge
|
||||
labels:
|
||||
com.buzz.network: production
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
|
||||
COMPOSE_FILES=(-f compose.yml)
|
||||
if [[ "${BUZZ_COMPOSE_TLS:-false}" == "true" ]]; then
|
||||
COMPOSE_FILES+=(-f compose.caddy.yml)
|
||||
fi
|
||||
if [[ "${BUZZ_COMPOSE_DEV:-false}" == "true" ]]; then
|
||||
COMPOSE_FILES+=(-f compose.dev.yml)
|
||||
fi
|
||||
|
||||
WEB_OVERLAY_ENABLED=false
|
||||
if [[ "${BUZZ_WEB_HOST_DIR+x}" == "x" ]] || {
|
||||
[[ -f .env ]] && grep -Eq '^[[:space:]]*(export[[:space:]]+)?BUZZ_WEB_HOST_DIR[[:space:]]*=' .env
|
||||
}; then
|
||||
COMPOSE_FILES+=(-f compose.web.yml)
|
||||
WEB_OVERLAY_ENABLED=true
|
||||
fi
|
||||
|
||||
compose() {
|
||||
docker compose --env-file .env "${COMPOSE_FILES[@]}" "$@"
|
||||
}
|
||||
|
||||
require_env() {
|
||||
if [[ ! -f .env ]]; then
|
||||
cat >&2 <<'MSG'
|
||||
Missing deploy/compose/.env.
|
||||
|
||||
Copy .env.example to .env and replace every CHANGE_ME value, or run the bootstrap
|
||||
script once it lands. Do not start production with generated secrets missing.
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eq '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=.*CHANGE_ME' .env; then
|
||||
cat >&2 <<'MSG'
|
||||
deploy/compose/.env still contains CHANGE_ME placeholders.
|
||||
Generate stable secrets first; these values must not rotate on restart.
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
read_web_host_dir() {
|
||||
if [[ "${BUZZ_WEB_HOST_DIR+x}" == "x" ]]; then
|
||||
printf '%s' "${BUZZ_WEB_HOST_DIR}"
|
||||
return
|
||||
fi
|
||||
|
||||
local value
|
||||
value="$({
|
||||
sed -nE 's/^[[:space:]]*(export[[:space:]]+)?BUZZ_WEB_HOST_DIR[[:space:]]*=[[:space:]]*(.*)$/\2/p' .env || true
|
||||
} | tail -n 1)"
|
||||
value="${value%$'\r'}"
|
||||
value="${value#\"}"
|
||||
value="${value%\"}"
|
||||
value="${value#\'}"
|
||||
value="${value%\'}"
|
||||
printf '%s' "${value}"
|
||||
}
|
||||
|
||||
validate_web_overlay() {
|
||||
[[ "${WEB_OVERLAY_ENABLED}" == "true" ]] || return 0
|
||||
|
||||
local host_dir
|
||||
host_dir="$(read_web_host_dir)"
|
||||
if [[ -z "${host_dir}" ]]; then
|
||||
echo "BUZZ_WEB_HOST_DIR is set but empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${host_dir}" != /* ]]; then
|
||||
echo "BUZZ_WEB_HOST_DIR must be an absolute path." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${host_dir}" ]]; then
|
||||
echo "BUZZ_WEB_HOST_DIR does not exist or is not a directory: ${host_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -r "${host_dir}/index.html" ]]; then
|
||||
echo "BUZZ_WEB_HOST_DIR must contain a readable index.html: ${host_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_config() {
|
||||
require_env
|
||||
validate_web_overlay
|
||||
# Quiet mode validates the merged model without printing resolved secrets.
|
||||
compose config --quiet
|
||||
}
|
||||
|
||||
backup_hint() {
|
||||
cat <<'MSG'
|
||||
Back up these before upgrades and on a regular schedule:
|
||||
|
||||
- deploy/compose/.env, especially BUZZ_RELAY_PRIVATE_KEY, DB/Redis/S3 secrets, and BUZZ_GIT_HOOK_HMAC_SECRET
|
||||
- The owner private key if bootstrap generated one for RELAY_OWNER_PUBKEY
|
||||
- Postgres data (prefer pg_dump or a quiesced volume snapshot)
|
||||
- MinIO/S3 bucket contents for media and git objects
|
||||
- buzz-git-data volume (BUZZ_GIT_REPO_PATH=/data/git)
|
||||
- Caddy data/config volumes if using compose.caddy.yml
|
||||
|
||||
Keep Postgres + object/git state snapshots from the same maintenance window.
|
||||
MSG
|
||||
}
|
||||
|
||||
case "${1:-help}" in
|
||||
start|up)
|
||||
validate_config
|
||||
compose up -d --wait
|
||||
;;
|
||||
stop|down)
|
||||
compose down
|
||||
;;
|
||||
restart)
|
||||
validate_config
|
||||
compose up -d --wait --force-recreate relay
|
||||
;;
|
||||
pull)
|
||||
validate_config
|
||||
compose pull
|
||||
;;
|
||||
upgrade)
|
||||
validate_config
|
||||
compose pull
|
||||
compose up -d --wait
|
||||
backup_hint
|
||||
;;
|
||||
logs)
|
||||
shift || true
|
||||
compose logs -f "${@:-relay}"
|
||||
;;
|
||||
status|ps)
|
||||
compose ps
|
||||
;;
|
||||
config|validate)
|
||||
validate_config
|
||||
echo "Compose configuration is valid."
|
||||
;;
|
||||
backup-hint)
|
||||
backup_hint
|
||||
;;
|
||||
add-member)
|
||||
docker compose exec relay /usr/local/bin/buzz-admin add-member --pubkey "${2:?Usage: ./run.sh add-member <npub-or-hex> [--role member|admin]}" "${@:3}"
|
||||
;;
|
||||
remove-member)
|
||||
docker compose exec relay /usr/local/bin/buzz-admin remove-member --pubkey "${2:?Usage: ./run.sh remove-member <npub-or-hex> [--role member|admin]}" "${@:3}"
|
||||
;;
|
||||
list-members)
|
||||
docker compose exec relay /usr/local/bin/buzz-admin list-members
|
||||
;;
|
||||
help|-h|--help)
|
||||
cat <<'MSG'
|
||||
Usage: ./run.sh <command>
|
||||
|
||||
Commands:
|
||||
start Start Buzz with docker compose up -d --wait
|
||||
stop Stop containers without deleting volumes
|
||||
restart Recreate the relay after env/image changes
|
||||
pull Pull configured images
|
||||
upgrade Pull and restart, then print backup reminders
|
||||
logs [svc] Follow logs (default: relay)
|
||||
status Show compose service status
|
||||
config Validate merged Compose config without printing secrets
|
||||
validate Alias for config
|
||||
backup-hint Print the production backup checklist
|
||||
|
||||
add-member <npub-or-hex> [--role member|admin]
|
||||
Add a relay member (default role: member)
|
||||
remove-member <npub-or-hex> [--role member|admin]
|
||||
Remove a relay member
|
||||
list-members List all relay members
|
||||
|
||||
Note: when adding multiple members in a loop, add `sleep 1` between
|
||||
invocations to avoid same-second timestamp collisions in the kind:13534
|
||||
roster event. Do not use parallel adds (e.g. xargs -P).
|
||||
|
||||
Environment switches:
|
||||
BUZZ_COMPOSE_TLS=true Include compose.caddy.yml for automatic HTTPS
|
||||
BUZZ_COMPOSE_DEV=true Include compose.dev.yml for local admin ports/tools
|
||||
BUZZ_WEB_HOST_DIR=/abs Mount a custom web dist via compose.web.yml (read-only)
|
||||
MSG
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $1" >&2
|
||||
echo "Run ./run.sh help" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,3 @@
|
||||
# Corporate-proxy CA is machine/environment-specific and often internal.
|
||||
# Regenerate locally (see build-and-deploy.sh); never commit.
|
||||
proxy-ca.pem
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local docker-desktop k8s testbed for the Buzz relay mesh.
|
||||
#
|
||||
# Repeatable path: build image -> helm dep build -> helm install (quickstart HA,
|
||||
# 3 replicas) -> wait 3/3 Ready -> probe /_readiness on every pod. This is the
|
||||
# baseline every mesh build redeploys onto (mesh lane).
|
||||
#
|
||||
# Prereqs: docker-desktop k8s context Ready, helm >= 3.14, kubectl, docker.
|
||||
# docker-desktop shares the docker image store with k8s, so a locally-built
|
||||
# tag + pullPolicy: IfNotPresent needs no registry push or `kind load`.
|
||||
#
|
||||
# Usage:
|
||||
# deploy/local/build-and-deploy.sh # full: build + deploy
|
||||
# SKIP_BUILD=1 deploy/local/build-and-deploy.sh # redeploy existing image
|
||||
# IMAGE_TAG=mesh-abc1234 deploy/local/build-and-deploy.sh
|
||||
set -euo pipefail
|
||||
|
||||
# ── config ──────────────────────────────────────────────────────────────────
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
NS="${NS:-buzz-mesh}"
|
||||
RELEASE="${RELEASE:-buzz}"
|
||||
IMAGE_REPO="${IMAGE_REPO:-buzz-relay}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-mesh-local}"
|
||||
CHART="${REPO_ROOT}/deploy/charts/buzz"
|
||||
VALUES="${REPO_ROOT}/deploy/local/quickstart-ha-values.yaml"
|
||||
CA_PEM="${REPO_ROOT}/deploy/local/proxy-ca.pem"
|
||||
EXPECT_CTX="docker-desktop"
|
||||
REPLICAS=3
|
||||
EVID="${EVID:-/tmp/mesh-build/deploy-evidence-$(date +%Y%m%d-%H%M%S)}"
|
||||
mkdir -p "$EVID"
|
||||
|
||||
log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||
die() { printf '\033[1;31mFAIL: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── 0. guardrails ─────────────────────────────────────────────────────────────
|
||||
CTX="$(kubectl config current-context)"
|
||||
[ "$CTX" = "$EXPECT_CTX" ] || die "kube context is '$CTX', expected '$EXPECT_CTX' (refusing to touch a non-local cluster)"
|
||||
log "context: $CTX"; kubectl get nodes | tee "$EVID/nodes.txt"
|
||||
|
||||
# ── 1. corporate-proxy CA + npm mirror (TLS-intercepting gateway) ────────────
|
||||
# Two stacked blocks on Block's network: (a) the gateway re-signs TLS with
|
||||
# internal CAs the build container doesn't trust; (b) public registry.npmjs.org
|
||||
# is policy-blocked (Dependency Confusion mitigation), so npm/corepack must use
|
||||
# the Artifactory mirror. Both no-op on a normal network (build-args stay unset).
|
||||
CA_ARG=()
|
||||
REG_ARG=()
|
||||
# (a) Build a complete internal-CA bundle from the macOS System keychain.
|
||||
if [ ! -f "$CA_PEM" ] && command -v security >/dev/null 2>&1; then
|
||||
log "exporting Block internal CA bundle from System keychain"
|
||||
: > "$CA_PEM"
|
||||
for name in "Cloudflare Gateway CA" \
|
||||
"Service To Service AWS Native CA production G0" \
|
||||
"Corp Systems AWS Native CA production G0" \
|
||||
"Block, Inc CA G1" \
|
||||
"Square Primary Certificate Authority - G2"; do
|
||||
security find-certificate -a -c "$name" -p /Library/Keychains/System.keychain >> "$CA_PEM" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
if [ -f "$CA_PEM" ] && grep -q 'BEGIN CERTIFICATE' "$CA_PEM"; then
|
||||
CA_ARG=(--build-arg "EXTRA_CA_CERTS=deploy/local/proxy-ca.pem")
|
||||
log "using proxy CA bundle ($(grep -c 'BEGIN CERTIFICATE' "$CA_PEM") certs)"
|
||||
fi
|
||||
# (b) Use the host's configured npm registry (Artifactory) if it isn't public.
|
||||
HOST_NPM_REG="$(pnpm config get registry 2>/dev/null || echo '')"
|
||||
if [ -n "$HOST_NPM_REG" ] && ! echo "$HOST_NPM_REG" | grep -q 'registry.npmjs.org'; then
|
||||
REG_ARG=(--build-arg "NPM_REGISTRY=${HOST_NPM_REG}")
|
||||
log "using npm registry mirror: $HOST_NPM_REG"
|
||||
fi
|
||||
|
||||
# ── 2. build image ────────────────────────────────────────────────────────────
|
||||
if [ "${SKIP_BUILD:-0}" != "1" ]; then
|
||||
log "building ${IMAGE_REPO}:${IMAGE_TAG} (HEAD $(git -C "$REPO_ROOT" rev-parse --short HEAD))"
|
||||
git -C "$REPO_ROOT" rev-parse HEAD > "$EVID/build-sha.txt"
|
||||
docker build "${CA_ARG[@]}" "${REG_ARG[@]}" \
|
||||
-t "${IMAGE_REPO}:${IMAGE_TAG}" \
|
||||
-f "${REPO_ROOT}/Dockerfile" "${REPO_ROOT}" 2>&1 | tee "$EVID/build.log"
|
||||
else
|
||||
log "SKIP_BUILD=1 — reusing ${IMAGE_REPO}:${IMAGE_TAG}"
|
||||
fi
|
||||
docker image inspect "${IMAGE_REPO}:${IMAGE_TAG}" --format '{{.Id}} {{.Size}}' | tee "$EVID/image-id.txt"
|
||||
|
||||
# ── 3. chart deps + install ───────────────────────────────────────────────────
|
||||
log "helm dependency build"
|
||||
helm dependency build "$CHART" 2>&1 | tee "$EVID/helm-dep.txt"
|
||||
|
||||
log "helm upgrade --install $RELEASE (ns=$NS, replicas=$REPLICAS)"
|
||||
# No --wait here: the relay's A3 S3 probe is startup-fatal, so relays
|
||||
# CrashLoopBackOff a few times until the concurrent init Job creates the bucket.
|
||||
# helm --wait races that transient and can bail early; instead we own readiness
|
||||
# gating below (rollout status + per-pod probe), which tolerates the restarts.
|
||||
helm upgrade --install "$RELEASE" "$CHART" \
|
||||
--namespace "$NS" --create-namespace \
|
||||
--values "$VALUES" \
|
||||
--set image.repository="$IMAGE_REPO" \
|
||||
--set image.tag="$IMAGE_TAG" \
|
||||
--timeout 5m 2>&1 | tee "$EVID/helm-install.txt"
|
||||
helm_rc=${PIPESTATUS[0]}
|
||||
if [ "$helm_rc" != 0 ]; then
|
||||
kubectl -n "$NS" get pods -o wide | tee "$EVID/pods-onfail.txt"
|
||||
kubectl -n "$NS" describe pods -l app.kubernetes.io/name=buzz | tee "$EVID/describe-onfail.txt"
|
||||
kubectl -n "$NS" logs -l app.kubernetes.io/name=buzz --tail=100 --all-containers | tee "$EVID/logs-onfail.txt"
|
||||
die "helm install returned rc=$helm_rc"
|
||||
fi
|
||||
|
||||
# ── 4. verify 3/3 Ready ───────────────────────────────────────────────────────
|
||||
# Find the relay Deployment: everything under this release named "buzz" except
|
||||
# the bundled "*-minio" Deployment. (The chart fullname collapses
|
||||
# "<release>-<chart>" to "<release>" when the release name already contains the
|
||||
# chart name, so the name isn't always "<release>-buzz".)
|
||||
DEPLOY=""
|
||||
for d in $(kubectl -n "$NS" get deploy -l "app.kubernetes.io/instance=$RELEASE" \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
|
||||
case "$d" in *-minio) continue;; esac
|
||||
DEPLOY="$d"; break
|
||||
done
|
||||
[ -n "$DEPLOY" ] || die "could not locate the relay Deployment"
|
||||
log "waiting for $REPLICAS relay pods Ready (deployment: $DEPLOY)"
|
||||
kubectl -n "$NS" rollout status deployment/"$DEPLOY" --timeout=4m | tee "$EVID/rollout.txt"
|
||||
kubectl -n "$NS" get pods -o wide | tee "$EVID/pods.txt"
|
||||
|
||||
READY=$(kubectl -n "$NS" get deploy "$DEPLOY" -o jsonpath='{.status.readyReplicas}')
|
||||
[ "${READY:-0}" = "$REPLICAS" ] || die "readyReplicas=$READY, expected $REPLICAS"
|
||||
log "deployment reports $READY/$REPLICAS Ready"
|
||||
|
||||
# ── 5. probe /_readiness on EVERY relay pod (not just the deployment aggregate)
|
||||
# The bundled MinIO + init pods share app.kubernetes.io/name=buzz, so select by
|
||||
# the relay Deployment's own pod-template hash to hit only relay pods.
|
||||
log "probing /_readiness on each relay pod individually"
|
||||
: > "$EVID/readiness.txt"
|
||||
FAIL=0
|
||||
RELAY_PODS=$(kubectl -n "$NS" get pods \
|
||||
-l "app.kubernetes.io/name=buzz,app.kubernetes.io/instance=$RELEASE" \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.metadata.labels.app\.kubernetes\.io/component}{"\n"}{end}' \
|
||||
| awk '$2 != "minio" && $2 != "minio-init" {print $1}')
|
||||
for pod in $RELAY_PODS; do
|
||||
body=$(kubectl -n "$NS" exec "$pod" -- \
|
||||
sh -c 'curl -sS --max-time 5 http://127.0.0.1:8080/_readiness' 2>/dev/null || echo '<curl-failed>')
|
||||
echo "$pod -> $body" | tee -a "$EVID/readiness.txt"
|
||||
echo "$body" | grep -q '"status":"ready"' || FAIL=1
|
||||
done
|
||||
[ "$FAIL" = 0 ] || die "at least one pod is not reporting ready (see $EVID/readiness.txt)"
|
||||
|
||||
log "ALL $REPLICAS PODS READY — baseline up. Evidence: $EVID"
|
||||
echo "namespace=$NS release=$RELEASE image=${IMAGE_REPO}:${IMAGE_TAG}" | tee "$EVID/SUMMARY.txt"
|
||||
@@ -0,0 +1,44 @@
|
||||
# Local docker-desktop HA testbed: quickstart bundled services + 3 replicas.
|
||||
#
|
||||
# This is the baseline every mesh build redeploys onto (mesh lane, thread
|
||||
# fa90bba8 / kickoff 5a760d8d). It layers the HA shape on top of the CI
|
||||
# quickstart set (ci/quickstart-values.yaml): bundled Postgres/Redis/MinIO,
|
||||
# chart-autogenerated secrets, membership enforcement off.
|
||||
#
|
||||
# Two lines carry HA:
|
||||
# replicaCount: 3 — chart hard-requires Redis for buzz-pubsub
|
||||
# (quickstart provides it); the git-hook HMAC
|
||||
# secret autogenerates chart-side (secret-chart.yaml).
|
||||
# persistence.git.enabled — false → per-pod emptyDir. The default (true) is a
|
||||
# single ReadWriteOnce PVC that cannot multi-attach
|
||||
# across 3 pods on one node; it would wedge 2/3.
|
||||
# Object store + Postgres are the source of truth
|
||||
# for git state, not local disk (values.yaml docs
|
||||
# emptyDir as the correct multi-replica choice).
|
||||
quickstart: true
|
||||
postgresql:
|
||||
enabled: true
|
||||
redis:
|
||||
enabled: true
|
||||
minio:
|
||||
enabled: true
|
||||
relayUrl: wss://buzz.test.local
|
||||
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
relay:
|
||||
# Testbed, not business-logic validation — same rationale as CI quickstart.
|
||||
requireRelayMembership: false
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
|
||||
# ── HA shape ──────────────────────────────────────────────────────────────
|
||||
replicaCount: 3
|
||||
persistence:
|
||||
git:
|
||||
enabled: false
|
||||
|
||||
# Locally-built image; docker-desktop k8s shares the docker image store, so
|
||||
# IfNotPresent resolves the tag with no push / `kind load` step.
|
||||
image:
|
||||
repository: buzz-relay
|
||||
tag: mesh-local
|
||||
pullPolicy: IfNotPresent
|
||||
Reference in New Issue
Block a user