- Scheme 85.4%
- Dockerfile 14.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| bl0t | ||
| docker | ||
| templates | ||
| .gitignore | ||
| .sops.yaml | ||
| Chart.yaml | ||
| main.scm | ||
| README.md | ||
| values.yaml | ||
bl0t
Personal IRC bot, written in Guile Scheme. Connects to IRC, and (as it grows) interacts with other services on the cluster -- starting with zoitestream (streaming scheduling/access), eventually the Molly Brown Gemini capsule once that's migrated off the legacy NixOS box.
Layout
bl0t/
├── main.scm entry point / application logic
├── bl0t/
│ ├── irc.scm (bl0t irc) - low-level IRC client bits.
│ │ Candidate for extraction into its own
│ │ standalone repo/library once stable.
│ ├── zoitestream.scm (bl0t zoitestream) - HTTP client for the
│ │ zoitestream streaming API
│ └── config.scm (bl0t config) - env var loading helpers
├── docker/
│ └── Dockerfile Guile runtime + system deps ONLY -- no
│ application source is baked into the image.
├── Chart.yaml This repo IS the Helm chart -- see "Deployment
├── values.yaml model" below.
├── templates/
└── README.md
As more small reusable pieces get pulled out (the IRC library being the
obvious first one), they get their own (bl0t whatever) module for now,
and may eventually graduate to their own repo/library once the
interface is stable enough to reuse elsewhere.
Local development
GUILE_LOAD_PATH=. guile main.scm
main.scm does not add its own directory to %load-path (that turned
out to be unreliable -- see "A gotcha we hit" below). Set
GUILE_LOAD_PATH to the repo root (or -L .) so (use-modules (bl0t irc)) etc. resolve, both locally and in the deployed container (where
the Dockerfile sets GUILE_LOAD_PATH=/app/src for the same reason).
Logging
All IRC traffic (send-irc/handle-line in bl0t/irc.scm) logs to
log-port, which defaults to stdout. This is deliberate: in the
deployed pod, stdout is exactly what fluent-bit already ships to
S3/Athena for every other service in this cluster -- bl0t needs zero
extra logging infrastructure to show up there, it just needed to
actually write to stdout instead of a private file.
Locally, running guile main.scm directly means that noisy IRC traffic
shares your terminal with anything you type. For actual REPL-based
iteration, run it as a REPL server instead so your interactive session
and the log noise are two separate streams:
GUILE_LOAD_PATH=. guile --listen main.scm
Then connect a client separately -- nc localhost 37146, or
geiser-connect-local in Emacs -- and evaluate things there. The
--listen terminal keeps showing the raw IRC firehose; your client
connection only shows the results of what you evaluate. Redirect/tee
the --listen terminal's output to a file if you want it to persist
past that terminal closing -- there's no code-level file logging to
configure, it's just stdout either way.
Deployment model
This repo is the Helm chart that ArgoCD deploys -- Chart.yaml,
values.yaml, and templates/ live right here, alongside the actual
.scm source they package. There used to be a second copy of this
chart in dumpnet-argo/charts/bl0t/, with the .scm files manually
copied into it -- that duplication was a real design mistake (the two
copies drifted out of sync more than once) and has been resolved by
moving the chart here instead of leaving a copy behind.
This is deliberately not the usual "build a Docker image per code change" pattern used by the other services in this cluster (repertory-api, zoitestream). Guile is interpreted and this bot's source is small, so instead:
- The Docker image (built from
docker/Dockerfile) contains only the Guile runtime and system packages -- it rarely needs to change. - The actual source (
main.scm,bl0t/*.scm) is packaged straight into a Kubernetes ConfigMap bytemplates/configmap.yaml(via Helm's.Files.Get), mounted into the container at/app/src. - A checksum of that ConfigMap's contents is baked into the
Deployment's pod-template annotations, so any code change
automatically triggers a pod restart on the next ArgoCD sync -- no
manual
docker build/push/kubectl delete podcycle required. - The
dumpnet-argoArgoCDApplication(manifests/services/bl0t.yamlthere) points its primary Helm source directly at this repo, and only pulls the globalvalues.yaml(registry host/user, domain, etc.) fromdumpnet-argoas a second values-only source -- same multi-source pattern every other chart in that repo already uses.
Day-to-day update loop
- Edit
.scmfiles here. git push.- ArgoCD picks up the change (its normal reconciliation loop, or
near-instantly if a Forgejo repo webhook is pointed at ArgoCD's
webhook endpoint), the ConfigMap content and checksum annotation
change together, and the pod restarts automatically running the new
code. Nothing else to do, and nothing to touch in
dumpnet-argo. - Only rebuild/push the Docker image (
git.keane.sh/ian/bl0t:latest) if you change system-level dependencies (a new apt package, a Guile version bump, etc.) -- ordinary application logic changes never need this.make build-bl0tindumpnet-argohandles that build/push.
Adding a new source file
ConfigMap keys can't contain slashes, so any file under bl0t/ needs
three touch points, not just dropping the file in:
- Add the file under
bl0t/as usual. - Add a flattened
data:entry intemplates/configmap.yaml(e.g.bl0t/whatever.scm-> keybl0t_whatever.scm). - Add a matching
items:entry intemplates/bl0t.yaml's volume definition, mapping that flattened key back to the realbl0t/whatever.scmpath inside the mounted volume.
Forgetting step 2 or 3 means the file exists in the repo but either
isn't in the ConfigMap, or is in the ConfigMap but not mounted where
main.scm's (use-modules ...) expects to find it.
A gotcha we hit
An earlier version of the deployed main.scm had
(add-to-load-path (dirname (current-filename))) at the top, to make
(use-modules (bl0t irc)) resolve without relying on GUILE_LOAD_PATH.
Don't do this: current-filename only resolves to a real path when
Guile successfully compiles the file to bytecode first. If that
compile step fails for any reason and Guile falls back to plain
interpretation, current-filename evaluates to #f instead, and
(dirname #f) crashes with a fairly opaque scm_to_utf8_stringn error.
This is exactly what took the bot down in production once. Rely on
GUILE_LOAD_PATH (already set by the Dockerfile, and by convention set
by hand for local dev per above) instead.
Secrets
bl0t-secrets (IRC connection details, the zoitestream CREDS_TOKEN)
comes from the usual single dumpnet Secrets Manager blob via ESO,
same convention as every other service in dumpnet-argo -- see
templates/external-secret.yaml. Nothing bot-specific about how
secrets are handled; only the application source delivery mechanism
is unusual in this chart.
Non-goals for this chart
- No Service/Ingress: this is an outbound-only IRC client, nothing needs to route traffic to it.
- No CI/build pipeline: deliberately avoided, see above.
- Gemini/Molly Brown hosting: a separate future chart, not this one,
even though
bl0twill eventually write files that a Gemini server serves.
Why not a normal Docker-image-per-change pipeline?
See the gemlog writeup in ~/code/gemlog for the full reasoning --
short version: for a small, frequently-iterated, interpreted-language
service like this, a ConfigMap-mounted-source + checksum-annotation
pattern gets you real GitOps (push to git, cluster reconciles) without
a build step, while staying comfortably within ConfigMap's 1MiB limit
for the foreseeable future.