diff --git a/milky-way/environments/stage00/orion-system/main.jsonnet b/milky-way/environments/stage00/orion-system/main.jsonnet index a3f5199..8d11f3d 100644 --- a/milky-way/environments/stage00/orion-system/main.jsonnet +++ b/milky-way/environments/stage00/orion-system/main.jsonnet @@ -36,6 +36,7 @@ local gluetunLeakTest = import 'milky-way/lib/gluetun-leak-test.libsonnet'; local kubo = import 'milky-way/lib/kubo.libsonnet'; local kuboTest = import 'milky-way/lib/kubo-test.libsonnet'; local andrefIpfsDepot = import 'milky-way/lib/andref-ipfs-depot.libsonnet'; +local yutobotDiscord = import 'milky-way/lib/yutobot-discord.libsonnet'; local testExampleWhaleImageDigest = import 'milky-way/lib/test-example-whale-image-digest.libsonnet'; local letsEncryptCloudflare = import 'milky-way/lib/letsencrypt-cloudflare.libsonnet'; local testTraefikAcme = import 'milky-way/lib/test-traefik-acme-ingress.libsonnet'; @@ -851,6 +852,14 @@ local pubkeys = import 'magic/common/public_keys.json'; issuerName = activeLetsEncryptIssuerName, ), + // yutobot-discord: Yuto's personal Discord bot (lib/yutobot-discord.libsonnet), migrated off its + // old CapRover droplet. Outbound-only (gateway websocket, no server), so just a Secret + a + // single-replica Deployment. The whole .env comes from the sops binary secret; the container + // mounts it at /app/.env where the app's dotenv.config() reads it. + yutobotDiscord: yutobotDiscord.new( + envFileContent = secretsRegistry['discord/yutobot.env'], + ), + cilium: charts.cilium, traefikConfig: traefik.reconfigForCilium(), diff --git a/milky-way/lib/yutobot-discord.libsonnet b/milky-way/lib/yutobot-discord.libsonnet new file mode 100644 index 0000000..ac995c7 --- /dev/null +++ b/milky-way/lib/yutobot-discord.libsonnet @@ -0,0 +1,88 @@ +local images = import 'milky-way/lib/images.libsonnet'; + +// yutobot-discord: Yuto's personal Discord bot, migrated off its old CapRover droplet. +// +// One whale-built Node process (discord.js v12) that logs into Discord and reacts to guild events: +// canvas-rendered welcome/wii-menu cards, an owoifier, and simple call/response commands (see +// yutobot-discord/). It is OUTBOUND-ONLY -- it opens a gateway websocket to Discord and runs no +// inbound server -- so there is NO Service/Ingress/Certificate here, only a Secret + Deployment. +// +// Config: the app calls dotenv.config(), which reads ./.env from the process CWD. The whale image +// sets WorkingDir=/app, and this lib mounts the whole sops-managed .env (passed in as +// envFileContent -- one opaque Secret value) at /app/.env. Every DISCORD_* var lives in that file; +// nothing bot-specific is set as discrete env here (the image sets only TZ/SSL_CERT_FILE). +// +// Singleton: replicas 1 + Recreate. Two pods would each hold a live bot session and double-handle +// every Discord event (double welcomes/owoifies); Recreate guarantees the old pod is gone before +// the new one logs in. The bot keeps no persistent state, so there is no PVC. +{ + new( + envFileContent, // required -> the whole decrypted .env (from sops), mounted at /app/.env + name='yutobot-discord', + namespace='default', + image=images['yutobot-discord'].fullyQualifiedImageReferencePinned, + ):: { + local this = self, + // The Secret key that is both stored and mounted; keep the two in lockstep off one local. + local envFileName = '.env', + + // The entire .env as one opaque Secret value. stringData lets Kubernetes base64-encode it; the + // container mounts just this key as the file /app/.env (subPath), which dotenv then reads. + secret: { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: name + '-env', namespace: namespace }, + type: 'Opaque', + stringData: { [envFileName]: envFileContent }, + }, + + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: name, namespace: namespace }, + spec: { + replicas: 1, + strategy: { type: 'Recreate' }, // singleton bot session -- never run two pods at once + selector: { matchLabels: { app: name } }, + template: { + metadata: { + labels: {} + this.deployment.spec.selector.matchLabels, + // Roll the pod when the .env changes: a subPath Secret mount does not live-update, and + // editing a Secret doesn't roll a Deployment on its own. Hashing the file into the + // template makes `tk apply` restart the bot on a token/channel-id change. + annotations: { 'checksum/env': std.md5(envFileContent) }, + }, + spec: { + tolerations: [ + { key: 'ephemeral', operator: 'Exists', effect: 'NoSchedule' }, + ], + containers: [ + { + name: name, + image: image, + // dotenv reads ./.env from CWD; the image's WorkingDir is /app, so mount the single + // Secret key there. subPath keeps it a lone file instead of shadowing all of /app. + volumeMounts: [{ + name: 'env', + mountPath: '/app/' + envFileName, + subPath: envFileName, + readOnly: true, + }], + resources: { + // Idle footprint on the old droplet was ~80Mi; the only spike is canvas rendering + // a 1920x1080 card on demand, so 256Mi is ample headroom. + requests: { memory: '64Mi', cpu: '10m' }, + limits: { memory: '256Mi', cpu: '500m' }, + }, + }, + ], + volumes: [{ + name: 'env', + secret: { secretName: this.secret.metadata.name }, + }], + }, + }, + }, + }, + }, +}