From 24fdae4f159855207fd26907b99107268013bfbf Mon Sep 17 00:00:00 2001 From: migatu Date: Mon, 3 Aug 2026 18:07:53 +0200 Subject: [PATCH] conjurer: add production ('deploy') bot + data seeding/backup A second Conjurer bot alongside the test one, sharing librarian/musician/ radio and the API key, differing only in: * Discord token from the deploy-conjurer-netrc secret, * distinct names/labels (deploy-bot) and NodePort 32443, * /data on an NFS export (RWX) instead of a block PVC - so config/state can be uploaded while it runs (copy onto the share) and backed up concurrently. deploy-bot-backup: a daily CronJob that mirrors /data and keeps 30 days of dated snapshots of the critical small state (both memories, settings, accident log, transcripts) on NFS. Production only - the test bot's amnesia is fine. DEPLOY-BOT.md documents seeding, backup/restore, and the librarian/musician callback routing (they push to one bot; repoint CONJURER_MAIN_BOT to :32443 to feed this one). kubectl kustomize builds cleanly (10 objects). Co-Authored-By: Claude Opus 4.8 --- conjurer/DEPLOY-BOT.md | 75 +++++++++++++++++++++++++++++++++ conjurer/deploy-bot-backup.yaml | 65 ++++++++++++++++++++++++++++ conjurer/deploy-bot.yaml | 72 +++++++++++++++++++++++++++++++ conjurer/kustomization.yaml | 2 + 4 files changed, 214 insertions(+) create mode 100644 conjurer/DEPLOY-BOT.md create mode 100644 conjurer/deploy-bot-backup.yaml create mode 100644 conjurer/deploy-bot.yaml diff --git a/conjurer/DEPLOY-BOT.md b/conjurer/DEPLOY-BOT.md new file mode 100644 index 0000000..ee1269e --- /dev/null +++ b/conjurer/DEPLOY-BOT.md @@ -0,0 +1,75 @@ +# Production ("deploy") Conjurer bot + +A second Conjurer bot that runs **alongside** the test `bot` in the `conjurer` +namespace, sharing the same librarian / musician / radio and the shared +`CONJURER_API_KEY`. It differs from the test bot only in: + +- its Discord token: the **`deploy-conjurer-netrc`** secret (already created), +- distinct names/labels (`deploy-bot`) and NodePort **32443**, +- **`/data` on NFS (RWX)** instead of a block PVC — so you can seed/upload files + while it runs and back it up concurrently. + +Files: `deploy-bot.yaml` (Deployment + Service), `deploy-bot-backup.yaml` +(daily backup CronJob). Both are wired into `kustomization.yaml`. + +## One-time setup + +1. **Secret** — already exists as `deploy-conjurer-netrc` (a netrc carrying the + deploy Discord token, key `.netrc`). Nothing to do; the Deployment mounts it + at `/secrets/.netrc`. + +2. **NFS export** — create the data + backup dirs on the NFS server + (`192.168.1.34`, adjust to your real export): + ``` + /mnt/Tank1/conjurer_swap/deploy-bot/data + /mnt/Tank1/conjurer_swap/deploy-bot/backups + ``` + +## Seeding / uploading config & state into /data + +Because `/data` is a plain NFS export, you upload files by copying them onto the +share — **no `kubectl cp`, no scaling the bot down**. From the PVE box that holds +`/srv/data` (mount the same export there, or rsync over it): + +```bash +# on a host that can see the NFS export: +rsync -a /srv/data/ 192.168.1.34:/mnt/Tank1/conjurer_swap/deploy-bot/data/ +``` + +The bot roots everything under `CONJURER_DATA_DIR=/data`, so these land where it +expects them: + +``` +accident_log.json Conjurer_graphics/ music/ pamiec.json +pamiec_muzyki.json settings.json system_gpt_settings.json transcripts/ +``` + +Upload as much or as little as you like — the bot seeds any missing JSON on +first run. Live-editing `pamiec.json` while the bot writes to it can race; prefer +seeding before first start or during a brief `kubectl -n conjurer scale +deploy/deploy-bot --replicas=0` window. + +## Backups (production only) + +`deploy-bot-backup` runs daily (04:17) and writes to the `backups` export: + +- `current/` — a full rsync mirror of `/data` (incl. music/graphics), always + the latest state, +- `snapshots/deploy-state-.tgz` — dated archives of the critical small + state (both memories, settings, accident log, transcripts), kept + `RETENTION_DAYS` (30) days. + +The test bot is deliberately **not** backed up (amnesia there is fine). + +Restore example: +```bash +tar xzf snapshots/deploy-state-2026-08-03-0417.tgz -C /mnt/.../deploy-bot/data/ +``` + +## Routing librarian / musician callbacks + +The librarian (`CONJURER_MAIN_BOT`) and musician push search results and +"now playing" events to **one** bot address — currently the test bot's +`192.168.1.73:32442`. To make **this** bot receive them, repoint those services' +`CONJURER_MAIN_BOT` to `192.168.1.73:32443` (this bot's NodePort). Only one bot +can receive them at a time. diff --git a/conjurer/deploy-bot-backup.yaml b/conjurer/deploy-bot-backup.yaml new file mode 100644 index 0000000..3206964 --- /dev/null +++ b/conjurer/deploy-bot-backup.yaml @@ -0,0 +1,65 @@ +# Daily backup of the PRODUCTION ("deploy") bot's /data. +# +# Only production is backed up - the test bot's amnesia is fine, this bot's is +# not (pamiec.json / pamiec_muzyki.json are the conversation & music memory). +# Both volumes are NFS (RWX), so this runs while the bot is live - no RWO clash. +# +# Two-part backup: +# * a full, space-efficient MIRROR (rsync --delete) of everything, incl. music +# and graphics - always the current state, +# * dated SNAPSHOTS of just the small critical state (the memories, settings, +# accident log, transcripts) so you can roll back to a point in time. +# Snapshots older than RETENTION_DAYS are pruned. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: deploy-bot-backup + namespace: conjurer +spec: + schedule: "17 4 * * *" # every day at 04:17 + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + spec: + restartPolicy: Never + containers: + - name: backup + image: alpine:3.20 + command: ["/bin/sh", "-c"] + args: + - | + set -eu + apk add --no-cache rsync >/dev/null + STAMP="$(date +%F-%H%M)" + mkdir -p /backup/current /backup/snapshots + echo "[$STAMP] mirroring /data -> /backup/current" + rsync -a --delete /data/ /backup/current/ + echo "[$STAMP] snapshotting critical state" + tar czf "/backup/snapshots/deploy-state-$STAMP.tgz" -C /data \ + accident_log.json pamiec.json pamiec_muzyki.json \ + settings.json system_gpt_settings.json transcripts \ + 2>/dev/null || echo " (some files absent - skipped)" + echo "[$STAMP] pruning snapshots older than ${RETENTION_DAYS}d" + find /backup/snapshots -name 'deploy-state-*.tgz' -type f \ + -mtime +"${RETENTION_DAYS}" -delete + echo "[$STAMP] backup done" + env: + - { name: RETENTION_DAYS, value: "30" } + volumeMounts: + - { name: data, mountPath: /data, readOnly: true } + - { name: backup, mountPath: /backup } + volumes: + # Same export the bot mounts (read-only here). + - name: data + nfs: + server: 192.168.1.34 + path: /mnt/Tank1/conjurer_swap/deploy-bot/data + # Backup target - ADJUST to your export. + - name: backup + nfs: + server: 192.168.1.34 + path: /mnt/Tank1/conjurer_swap/deploy-bot/backups diff --git a/conjurer/deploy-bot.yaml b/conjurer/deploy-bot.yaml new file mode 100644 index 0000000..66a9259 --- /dev/null +++ b/conjurer/deploy-bot.yaml @@ -0,0 +1,72 @@ +# Production ("deploy") Conjurer bot. +# +# Same infrastructure as the test `bot` (shares librarian / musician / radio and +# the CONJURER_API_KEY), with three deliberate differences: +# 1. its Discord token comes from the `deploy-conjurer-netrc` secret, +# 2. distinct names/labels so it coexists with the test bot in this namespace, +# 3. /data is an NFS volume (RWX) instead of a block PVC - so config/state can +# be uploaded while the bot runs (just copy onto the share) and backed up +# concurrently (see deploy-bot-backup.yaml). The test bot keeps its RWO PVC. +# +# ┌─ IMPORTANT: librarian & musician call ONE bot back ─────────────────────────┐ +# │ The librarian (CONJURER_MAIN_BOT) and musician push search results and │ +# │ "now playing" events to a SINGLE bot address - currently the test bot's │ +# │ NodePort 192.168.1.73:32442. Only one bot can receive them. To make the │ +# │ PRODUCTION bot the one that gets librarian results / radio events, repoint │ +# │ those services' CONJURER_MAIN_BOT to this bot's NodePort (…:32443 below). │ +# └─────────────────────────────────────────────────────────────────────────────┘ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy-bot + namespace: conjurer +spec: + replicas: 1 # NIGDY więcej — jedna sesja gateway na token + strategy: { type: Recreate } # NIE RollingUpdate — dwa pody = wojna o sesję Discord + selector: { matchLabels: { app: deploy-bot } } + template: + metadata: { labels: { app: deploy-bot } } + spec: + imagePullSecrets: [{ name: gitea-registry }] + containers: + - name: bot + image: gitea.czernobog.pl/gitea/conjurer-bot:c8aae106 + ports: [{ containerPort: 5000 }] + env: + - { name: CONJURER_DATA_DIR, value: "/data" } + - { name: CONJURER_DISCORD_HOST, value: "0.0.0.0" } + - { name: CONJURER_DISCORD_PORT, value: "5000" } + - { name: CONJURER_API_KEY, value: "d97008b3-7a5a-11f1-acd1-000b0e0f00ed" } + - { name: CONJURER_NETRC_FILE, value: "/secrets/.netrc" } + - { name: CONJURER_LIBRARIAN_SERVICE, value: "http://librarian:5001" } + - { name: CONJURER_MUSICIAN_SERVICE, value: "http://192.168.1.89:5000" } + - { name: CONJURER_RADIO_SERVICE, value: "http://192.168.1.79:5005" } + - { name: CONJURER_FILE_SERVICE, value: "http://192.168.1.89:5000" } + - { name: CONJURER_RADIO_HARBOR, value: "http://192.168.1.79:54321" } + volumeMounts: + - { name: data, mountPath: /data } + - { name: netrc, mountPath: /secrets, readOnly: true } + resources: + requests: { cpu: "200m", memory: "256Mi" } + limits: { cpu: "2", memory: "1Gi" } + volumes: + # /data on NFS (RWX): lets you seed/upload files while the bot runs (copy + # straight onto the export from the PVE box that holds /srv/data) and lets + # the backup CronJob read it concurrently. ADJUST server/path to your + # actual export - mirrors the librarian's 192.168.1.34 Tank1 layout. + - name: data + nfs: + server: 192.168.1.34 + path: /mnt/Tank1/conjurer_swap/deploy-bot/data + - name: netrc + secret: { secretName: deploy-conjurer-netrc } +--- +apiVersion: v1 +kind: Service +metadata: { name: deploy-bot, namespace: conjurer } +spec: + type: NodePort # so librarian/musician COULD reach it if repointed + selector: { app: deploy-bot } + # Distinct pinned nodePort (test bot owns 32442). Point librarian/musician + # CONJURER_MAIN_BOT at 192.168.1.73:32443 to route their callbacks here. + ports: [{ port: 5000, targetPort: 5000, nodePort: 32443 }] diff --git a/conjurer/kustomization.yaml b/conjurer/kustomization.yaml index da80142..773206d 100644 --- a/conjurer/kustomization.yaml +++ b/conjurer/kustomization.yaml @@ -4,6 +4,8 @@ resources: - namespace.yaml - librarian.yaml - bot.yaml + - deploy-bot.yaml + - deploy-bot-backup.yaml images: - name: gitea.czernobog.pl/gitea/conjurer-librarian newTag: ac16b77f