Skip to content
Merged
9 changes: 9 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -750,10 +750,19 @@ Subcommands:
| `remove-member` | Remove a pubkey from the relay membership list (`--pubkey`, optional `--role` guard); publishes kind:13534 roster |
| `list-members` | List all relay members |
| `generate-key` | Generate a new Nostr keypair (for bootstrapping) |
| `deletions` | Submit, inspect, approve, abort, unblock, run, or drain durable whole-community deletion requests |
| `storage-snapshot` | Run one isolated S3 accounting scan and publish its complete Postgres snapshot |
| `reconcile-channels` | Emit kind:39000/39002 discovery events for channels missing them (idempotent) |

The `buzz-admin` binary is shipped in the relay Docker image (`/usr/local/bin/buzz-admin`) and is the recommended way to manage relay membership in production. Use `./run.sh add-member`, `./run.sh remove-member`, and `./run.sh list-members` in Docker Compose deployments.

Kubernetes deployments may schedule the typed one-shot
`buzz-admin deletions drain` command directly. The pod owns its bounded
Postgres/Redis clients and S3 client; it does not call relay HTTP. Durable
requests, leases, retry timing, and checkpoints in Postgres are the handoff and
execution authority, so Kubernetes uses `Forbid` concurrency and zero Job
retries rather than introducing a second retry system.

---

### buzz-test-client — Integration Test Harness
Expand Down
4 changes: 2 additions & 2 deletions deploy/charts/buzz/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: |
PostgreSQL and Redis. Configurable for single-node evaluation
(subcharts on) and HA production (external services, existingSecret).
type: application
version: 0.1.9
version: 0.1.10
appVersion: "0.1.0"
home: https://github.com/block/buzz
sources:
Expand All @@ -24,7 +24,7 @@ maintainers:
annotations:
artifacthub.io/changes: |
- kind: added
description: Optional isolated storage-accounting CronJob with durable relay snapshots.
description: Optional typed deletion-drain operator CronJob using durable database leases.
artifacthub.io/license: Apache-2.0

# Optional eval-only subcharts. Production deploys disable both and point
Expand Down
24 changes: 23 additions & 1 deletion deploy/charts/buzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This chart has two operating profiles selected by values:
## Quickstart (eval only)

```sh
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.8 \
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.10 \
--create-namespace --namespace buzz \
--set quickstart=true \
--set postgresql.enabled=true \
Expand Down Expand Up @@ -116,6 +116,28 @@ is still parsed strictly, but reachability and addressing errors surface on the
first storage operation. `/_readiness` tests no external dependency in either
case — see the readiness contract below.

## Community deletion operator job

`operatorJobs.deletionDrain` is a disabled-by-default, typed CronJob for
`/usr/local/bin/buzz-admin deletions drain`. It runs inside the relay image with
bounded Job lifetime/history, `concurrencyPolicy: Forbid`, `backoffLimit: 0`,
and no relay HTTP call. Postgres deletion requests, leases, retries, and
checkpoints remain the execution authority.

The pod receives only `DATABASE_URL`, `REDIS_URL`, and required S3
configuration/credential variables. It does not receive the relay private key,
git-hook secret, relay URL, service links, or a generic environment registry.
The chart disables the ordinary Kubernetes API service-account token mount;
platform workload-identity admission may still inject its own projected token
and provider environment variables. Schedule,
deadline, history, termination grace, resources, service account, pod labels,
and pod annotations are independently configurable under
`operatorJobs.deletionDrain`.

See [`docs/operator-community-deletion.md`](../../../docs/operator-community-deletion.md)
for enablement, permissions, the staffed first-run procedure, failure recovery,
and the current explicit approval/alerting boundaries.

### Early-startup telemetry contract

`buzz_process_lifecycle` JSON records are the authoritative history for the
Expand Down
7 changes: 7 additions & 0 deletions deploy/charts/buzz/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
{{- end -}}
{{- end -}}

{{/* Kubernetes CronJob names are limited to 52 characters. */}}
{{- define "buzz.cronJobName" -}}
{{- $maxBaseLength := sub 51 (len .suffix) | int -}}
{{- $base := include "buzz.fullname" .root | trunc $maxBaseLength | trimSuffix "-" -}}
{{- printf "%s-%s" $base .suffix -}}
{{- end -}}

{{- define "buzz.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
Expand Down
91 changes: 91 additions & 0 deletions deploy/charts/buzz/templates/_operator-jobs.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{{/* Closed rendering foundation for typed Buzz operator jobs. */}}

{{- define "buzz.operatorCronJob" -}}
{{- $root := .root -}}
{{- if eq .type "deletionDrain" -}}
{{- $job := $root.Values.operatorJobs.deletionDrain -}}
{{- range $label := list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component" -}}
{{- if hasKey $job.podLabels $label -}}
{{- fail (printf "operatorJobs.deletionDrain.podLabels may not set chart-owned label %q" $label) -}}
{{- end -}}
{{- end -}}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "buzz.cronJobName" (dict "root" $root "suffix" "deletion-drain") }}
labels:
{{- include "buzz.labels" $root | nindent 4 }}
app.kubernetes.io/component: deletion-drain
spec:
schedule: {{ $job.schedule | quote }}
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: {{ $job.successfulJobsHistoryLimit }}
failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }}
jobTemplate:
spec:
activeDeadlineSeconds: {{ $job.activeDeadlineSeconds }}
backoffLimit: 0
template:
metadata:
labels:
{{- include "buzz.selectorLabels" $root | nindent 12 }}
app.kubernetes.io/component: deletion-drain
{{- with $job.podLabels }}
{{- toYaml . | nindent 12 }}
{{- end }}
annotations:
{{- toYaml $job.podAnnotations | nindent 12 }}
spec:
restartPolicy: Never
terminationGracePeriodSeconds: {{ $job.terminationGracePeriodSeconds }}
serviceAccountName: {{ default (include "buzz.serviceAccountName" $root) $job.serviceAccountName }}
automountServiceAccountToken: false
enableServiceLinks: false
securityContext:
{{- toYaml $root.Values.relay.securityContext | nindent 12 }}
{{- with $root.Values.image.pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
containers:
- name: deletion-drain
image: {{ include "buzz.image" $root }}
imagePullPolicy: {{ $root.Values.image.pullPolicy }}
securityContext:
{{- omit $root.Values.relay.containerSecurityContext "readOnlyRootFilesystem" | toYaml | nindent 16 }}
readOnlyRootFilesystem: true
command: ["/usr/local/bin/buzz-admin"]
args: ["deletions", "drain"]
env:
- { name: BUZZ_S3_ENDPOINT, value: {{ required "s3.endpoint is required when operatorJobs.deletionDrain.enabled=true" (include "buzz.s3Endpoint" $root) | quote }} }
- { name: BUZZ_S3_BUCKET, value: {{ required "s3.bucket is required when operatorJobs.deletionDrain.enabled=true" $root.Values.s3.bucket | quote }} }
- { name: BUZZ_S3_REGION, value: {{ $root.Values.s3.region | quote }} }
- { name: BUZZ_S3_ADDRESSING_STYLE, value: {{ $root.Values.s3.addressingStyle | quote }} }
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "buzz.envSecretName" $root }}
key: DATABASE_URL
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: {{ include "buzz.envSecretName" $root }}
key: REDIS_URL
- name: BUZZ_S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "buzz.envSecretName" $root }}
key: BUZZ_S3_ACCESS_KEY
optional: true
- name: BUZZ_S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "buzz.envSecretName" $root }}
key: BUZZ_S3_SECRET_KEY
optional: true
resources:
{{- toYaml $job.resources | nindent 16 }}
{{- else -}}
{{- fail (printf "unsupported typed operator job %q" .type) -}}
{{- end -}}
{{- end -}}
7 changes: 7 additions & 0 deletions deploy/charts/buzz/templates/_validate.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ surface at template time regardless of which manifest helm renders first.
{{- end -}}
{{- end -}}

{{/* The deletion executor always uses Redis for tenant-scoped invalidation. */}}
{{- if .Values.operatorJobs.deletionDrain.enabled -}}
{{- if and (not .Values.redis.enabled) (not .Values.externalRedis.url) (not .Values.secrets.existingSecret) -}}
{{- fail "operatorJobs.deletionDrain requires Redis. Enable redis.enabled=true, set externalRedis.url, or provide secrets.existingSecret with key REDIS_URL." -}}
{{- end -}}
{{- end -}}

{{/* Multiple replicas do NOT require ReadWriteMany git storage.

Git ref/object state is object-store-backed: every read and write hydrates
Expand Down
4 changes: 4 additions & 0 deletions deploy/charts/buzz/templates/deletion-drain-cronjob.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{- include "buzz.validate" . -}}
{{- if .Values.operatorJobs.deletionDrain.enabled }}
{{- include "buzz.operatorCronJob" (dict "root" . "type" "deletionDrain") }}
{{- end }}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "buzz.fullname" . }}-storage-accounting
name: {{ include "buzz.cronJobName" (dict "root" . "suffix" "storage-accounting") }}
labels:
{{- include "buzz.labels" . | nindent 4 }}
app.kubernetes.io/component: storage-accounting
Expand Down
Loading
Loading