Full-agent Telegram bot: restart workloads and trigger CI reruns from chat. Runs as a pod in K3s.
  • Go 97.7%
  • Shell 1.8%
  • Dockerfile 0.5%
Find a file
Yusuf Taylan Yuksel 6baa9a8f21
All checks were successful
Deploy telegram-agent / build-and-push (push) Successful in 1m4s
Deploy telegram-agent / deploy (push) Successful in 14s
telegram-agent: fix fressi capacity, poll backoff, token redaction
usage_data decodes to map[string]map[string]int ("1" aged 2+, "2"
under 2), not map[string][]int, so every call failed and occupancy
was never known. SpacesUsed also counted under-2s as two spaces;
each class's own #reservationsmax is 10 for every observed class and
one-space-per-child makes occupancy peak at exactly that, while two
pushed classes past the gym's own ceiling. Capacity is now read per
class; FRESSI_CAPACITY is fallback only.

getUpdates retried immediately on error, so one 502 became a 32
req/s storm. Now exponential backoff 1s-30s reset on success,
honouring retry_after. Bot token redacted from logged errors
(url.Error embedded it in the request URL). Timeout now longPoll+20s.
GetUpdates takes a context.

fressi monitoring now starts at boot instead of needing /fressi on
after every restart, and calendarWeeksLocked skips a failed
location/week instead of discarding every result.

Verified live: reservationsmax 10, peak occupancy 10, arrival 18:45
rejected while 19:15 offered.
2026-10-04 16:37:21 +03:00
.forgejo/workflows ci: kaniko clones context itself, drop actions/checkout 2026-09-20 16:11:44 +03:00
app telegram-agent: fix fressi capacity, poll backoff, token redaction 2026-10-04 16:37:21 +03:00
manifests telegram-agent: update for fressi booking 2026-09-20 16:28:52 +03:00
scripts Initial import of telegram-agent as standalone repo 2026-09-15 21:45:00 +03:00
.gitignore telegram-agent: fix fressi capacity, poll backoff, token redaction 2026-10-04 16:37:21 +03:00
Dockerfile Initial import of telegram-agent as standalone repo 2026-09-15 21:45:00 +03:00
README.md telegram-agent: remove patch file 2026-09-15 21:48:11 +03:00

telegram-agent

Full-agent Telegram bot: restart workloads and trigger reruns from a chat message. Runs as a pod in K3s, no public exposure.

How it reaches the outside world (and vice versa)

  • Telegram: long polling (getUpdates), not a webhook — the pod only makes outbound HTTPS to api.telegram.org. No Service, no MetalLB IP, no Caddy route. Nothing on the internet can reach this pod directly.
  • Kubernetes API: in-cluster, via the auto-mounted ServiceAccount token — no kubeconfig to manage.
  • Forgejo: outbound HTTPS to git.yukselcloud.com's API, using a fine-grained token (Actions:write, scoped to lab/homelab only).

Safety model

  • Chat-ID allowlist: every incoming update is checked against TELEGRAM_AUTHORIZED_CHAT_ID; anything else is silently ignored.
  • Confirm before acting: /restart and /rerun stage the action and wait up to 60s for a plain yes reply from the same chat before doing anything. /status is read-only and doesn't need confirmation.
  • Namespace allowlist: hardcoded in app/config.go, kept in sync with which namespaces have a telegram-agent-actions RoleBinding applied. Changing what the bot can touch means a code change through the normal GitOps review, not a live config edit.
  • Least-privilege RBAC: a purpose-built ClusterRole (clusterrole-actions.yaml) with only the verbs /restart and /rerun actually need — not the built-in edit role.

Commands

/status <namespace>
/restart <namespace> <deployment|statefulset|daemonset> <name>
/rerun k8s <namespace> <cronjob>
/rerun ci <workflow-file.yaml> [ref, default main]
/fressi <status|check>

Setup

  1. Get a bot token from @BotFather, and your numeric chat ID from @userinfobot.

  2. Mint a Forgejo fine-grained token scoped to lab/homelab, Actions: Read and Write only.

  3. Add workflow_dispatch: to the on: block of any .forgejo/workflows/* file you want /rerun ci able to trigger — the existing push-only workflows can't be dispatched via API as-is.

  4. Apply the manifests (or let the GitOps workflow do it after first push):

    kubectl apply -f manifests/namespace.yaml
    kubectl apply -f manifests/serviceaccount.yaml
    kubectl apply -f manifests/clusterrolebinding-view.yaml
    kubectl apply -f manifests/clusterrole-actions.yaml
    kubectl apply -f manifests/rolebinding-media-actions.yaml
    kubectl apply -f manifests/rolebinding-monitoring-actions.yaml
    
  5. Create the real Secret (never commit it — see the comment in manifests/secret-bot-config.yaml.example):

    kubectl -n telegram-agent create secret generic telegram-agent-config \
      --from-literal=TELEGRAM_BOT_TOKEN='...' \
      --from-literal=TELEGRAM_AUTHORIZED_CHAT_ID='...' \
      --from-literal=FORGEJO_TOKEN='...' \
      --from-literal=FORGEJO_BASE_URL='https://git.yukselcloud.com' \
      --from-literal=FORGEJO_OWNER='lab' \
      --from-literal=FORGEJO_REPO='homelab'
    
  6. kubectl apply -f manifests/deployment.yaml, or push and let .forgejo/workflows/deploy-telegram-agent.yaml do it — check the comment at the top of that workflow first: it assumes the runner can build/push a Docker image directly, which hasn't been verified against how this cluster's runner is actually set up.

Extending it with push notifications later

homelab-event-collector already plans to notify via ntfy once its ingress layer (webhook + K8s watcher) is built. Once that's routing events through Redis/Redpanda, adding Telegram as a second sink is small: a goroutine here subscribing to the same channel and calling SendMessage. Not built yet because the upstream pipeline isn't either — no point guessing its shape before it exists.

Adding a new managed namespace

  1. Copy manifests/rolebinding-media-actions.yaml, change the namespace, apply it.
  2. Add the namespace to AllowedNamespaces in app/config.go, redeploy.

Both steps are required — RBAC without the app-level allowlist just means the bot tries and gets a 403; the allowlist without RBAC means a confusing in-cluster permission error instead of a clean "not allowed" reply.