feat(manifests): add bot-seeder to register strategy bots via POST /api/register

The bot-seeder Deployment runs a shell script at startup that:
- Checks GET /api/bots and skips any bot already registered (idempotent)
- Waits for each bot's /health endpoint before registering
- POSTs to /api/register with name, owner=system, and cluster-internal endpoint_url
- Captures the returned shared_secret and upserts acb-bot-<name>-secret via the K8s REST API
- Sleeps forever after all 6 bots are registered

Also adds reloader.stakater.com/auto: "true" to all 6 bot Deployments so Reloader
triggers a rolling restart when the seeder writes/updates their secrets, ensuring
pods pick up the correct BOT_SECRET for HMAC validation.

RBAC: bot-seeder ServiceAccount + Role (get/create/patch secrets) + RoleBinding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-23 23:27:03 -04:00
parent 1c61d80bd4
commit bceb686322
8 changed files with 236 additions and 0 deletions

View file

@ -18,6 +18,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector:

View file

@ -18,6 +18,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector:

View file

@ -19,6 +19,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector:

View file

@ -18,6 +18,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector:

View file

@ -18,6 +18,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector:

View file

@ -0,0 +1,182 @@
# bot-seeder: Registers all 6 strategy bots via POST /api/register and stores
# the returned shared_secret in each bot's K8s Secret (acb-bot-<name>-secret).
#
# Registration flow (idempotent):
# 1. GET /api/bots — skip bot if name already registered
# 2. Wait for bot's /health to respond (bot pod must be Running first)
# 3. POST /api/register → capture shared_secret
# 4. Create/patch acb-bot-<name>-secret via K8s REST API
# 5. Reloader detects the secret change and rolling-restarts the bot Deployment
#
# After all 6 bots are registered the script sleeps forever (idempotent on restart).
#
# Staging file — sync to declarative-config/k8s/iad-acb/acb-bots/
---
apiVersion: v1
kind: ConfigMap
metadata:
name: bot-seeder-script
namespace: acb-bots
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
data:
seed.sh: |
#!/bin/sh
set -eu
# Alpine: install required tools (curl for HTTP, jq for JSON parsing)
apk add --no-cache curl jq > /dev/null 2>&1
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
KUBE_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
KUBE_API="https://kubernetes.default.svc"
NS="acb-bots"
ACB_API="http://acb-api.ai-code-battle.svc.cluster.local"
# Wait for the ACB API to become available
echo "[seeder] waiting for ACB API at ${ACB_API}/health ..."
until curl -sf "${ACB_API}/health" > /dev/null 2>&1; do
sleep 5
done
echo "[seeder] ACB API ready"
# upsert_bot_secret <secret-name> <shared-secret-value>
# Creates the K8s Secret if it does not exist, patches the shared-secret key if it does.
upsert_bot_secret() {
local secret_name="$1"
local secret_value="$2"
local b64
b64=$(printf '%s' "$secret_value" | base64 | tr -d '\n')
local http_code
http_code=$(curl -sk --cacert "$KUBE_CA" \
-H "Authorization: Bearer $KUBE_TOKEN" \
-o /dev/null -w '%{http_code}' \
"${KUBE_API}/api/v1/namespaces/${NS}/secrets/${secret_name}")
if [ "$http_code" = "200" ]; then
curl -sf --cacert "$KUBE_CA" \
-H "Authorization: Bearer $KUBE_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-X PATCH \
"${KUBE_API}/api/v1/namespaces/${NS}/secrets/${secret_name}" \
-d "{\"data\":{\"shared-secret\":\"${b64}\"}}" \
-o /dev/null
echo "[seeder] patched secret ${secret_name}"
else
curl -sf --cacert "$KUBE_CA" \
-H "Authorization: Bearer $KUBE_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
"${KUBE_API}/api/v1/namespaces/${NS}/secrets" \
-d "{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"name\":\"${secret_name}\",\"namespace\":\"${NS}\"},\"type\":\"Opaque\",\"data\":{\"shared-secret\":\"${b64}\"}}" \
-o /dev/null
echo "[seeder] created secret ${secret_name}"
fi
}
# register_bot <short-name> <DisplayName>
# Idempotent: skips if a bot named <DisplayName>Bot is already in /api/bots.
register_bot() {
local short_name="$1"
local display_name="$2"
local bot_name="${display_name}Bot"
local endpoint="http://bot-${short_name}.${NS}.svc.cluster.local"
# Idempotency check: skip if already registered
local existing
existing=$(curl -sf "${ACB_API}/api/bots" \
| jq -r ".bots[] | select(.name == \"${bot_name}\") | .bot_id // empty")
if [ -n "$existing" ]; then
echo "[seeder] ${bot_name} already registered (${existing}), skipping"
return 0
fi
# Wait for the bot's /health endpoint
echo "[seeder] waiting for ${endpoint}/health ..."
local attempts=0
until curl -sf "${endpoint}/health" > /dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 72 ]; then
echo "[seeder] ERROR: ${endpoint}/health unreachable after 6 minutes, aborting"
exit 1
fi
sleep 5
done
echo "[seeder] ${bot_name} health OK"
# Register via ACB API
local response
response=$(curl -sf -X POST "${ACB_API}/api/register" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${bot_name}\",\"owner\":\"system\",\"endpoint_url\":\"${endpoint}\"}")
local bot_id shared_secret
bot_id=$(printf '%s' "$response" | jq -r '.bot_id // empty')
shared_secret=$(printf '%s' "$response" | jq -r '.shared_secret // empty')
if [ -z "$bot_id" ]; then
echo "[seeder] ERROR: registration failed for ${bot_name}: ${response}"
exit 1
fi
echo "[seeder] registered ${bot_name}: bot_id=${bot_id}"
# Store the shared_secret so the bot pod can validate HMAC on /turn requests.
# Reloader detects the secret change and triggers a rolling restart of the bot Deployment.
upsert_bot_secret "acb-bot-${short_name}-secret" "$shared_secret"
}
register_bot "random" "Random"
register_bot "gatherer" "Gatherer"
register_bot "rusher" "Rusher"
register_bot "guardian" "Guardian"
register_bot "swarm" "Swarm"
register_bot "hunter" "Hunter"
echo "[seeder] all 6 bots registered successfully, sleeping forever"
exec sleep infinity
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bot-seeder
namespace: acb-bots
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: bot-seeder
template:
metadata:
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
spec:
serviceAccountName: bot-seeder
restartPolicy: Always
containers:
- name: seeder
image: alpine:3.21
command: ["/bin/sh", "/scripts/seed.sh"]
volumeMounts:
- name: scripts
mountPath: /scripts
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi
volumes:
- name: scripts
configMap:
name: bot-seeder-script
defaultMode: 0755

View file

@ -0,0 +1,48 @@
# bot-seeder-rbac.yml: RBAC for the bot registration seeder
# ServiceAccount + Role allowing get/create/patch Secrets in the acb-bots namespace.
# The seeder uses the K8s REST API (via curl + the mounted ServiceAccount token)
# to upsert acb-bot-<name>-secret with the shared_secret returned by POST /api/register.
#
# Staging file — sync to declarative-config/k8s/iad-acb/acb-bots/
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: bot-seeder
namespace: acb-bots
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: bot-seeder
namespace: acb-bots
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: bot-seeder
namespace: acb-bots
labels:
app.kubernetes.io/name: bot-seeder
app.kubernetes.io/part-of: ai-code-battle
app.kubernetes.io/component: seeder
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: bot-seeder
subjects:
- kind: ServiceAccount
name: bot-seeder
namespace: acb-bots

View file

@ -18,6 +18,7 @@ metadata:
argocd-image-updater.argoproj.io/app.update-strategy: name
argocd-image-updater.argoproj.io/app.allow-tags: 'regexp:^sha-[0-9a-f]+$'
argocd-image-updater.argoproj.io/write-back-method: argocd
reloader.stakater.com/auto: "true"
spec:
replicas: 1
selector: