ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
263 lines
11 KiB
YAML
263 lines
11 KiB
YAML
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: terminus-config
|
|
namespace: trmnl
|
|
data:
|
|
TZ: "Europe/Berlin"
|
|
HANAMI_PORT: "2345"
|
|
# What the device is told to fetch from. Terminus bakes this into the
|
|
# absolute `image_url` it hands back on /api/display, so it has to be a
|
|
# name the DEVICE can resolve and fetch — and it has to carry the same
|
|
# scheme the device polls with, or every screen fetch is a second,
|
|
# cleartext connection. No trailing slash: the same string is typed into
|
|
# the captive portal at provisioning time.
|
|
#
|
|
# https since device-ingress.yaml took over trmnl-device.renner.uno. The
|
|
# earlier value was http on the theory that the firmware would reject an
|
|
# internal CA; there is no internal CA here — letsencrypt-cluster issues
|
|
# publicly-trusted certs for LAN-only names via DNS-01.
|
|
API_URI: "https://trmnl-device.renner.uno"
|
|
KEYVALUE_URL: "redis://terminus-valkey:6379"
|
|
# Rack::Attack rate-limit bypass. Beyond upstream's RFC1918 defaults this
|
|
# adds fc00::/7, which covers the pod CIDR (fd10:244::/48), the Cilium LB
|
|
# pool (fd99::/120) and the LAN ULA — without it every IPv6 client,
|
|
# including the device and Traefik, is rate-limited as if it were remote.
|
|
RACK_ATTACK_ALLOWED_SUBNETS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1,::1,fc00::/7"
|
|
# 10 years, against a default of 1800s (30 minutes).
|
|
#
|
|
# Documented upstream in doc/configuration.adoc:
|
|
# API_ACCESS_TOKEN_PERIOD — "The amount of time, in seconds, the JWT
|
|
# access token remains valid before needing to refresh your access
|
|
# token." Default: 1,800 seconds (30 minutes).
|
|
# Corroborated on this instance: a token minted here was rejected about
|
|
# thirty minutes later, i.e. the documented default behaving as written.
|
|
#
|
|
# This exists for the renderer, which pushes screens to /api/screens with
|
|
# a JWT held in OpenBao. At the default period that arrangement cannot
|
|
# work unattended, and it fails in a way that has no recovery: minting a
|
|
# replacement token via POST /api/jwt requires a still-VALID access token
|
|
# in the Authorization header, so once one expires it cannot be refreshed
|
|
# from itself. Observed exactly that on first deploy —
|
|
# [webhook] Webhook failed: 400 {"error":"expired JWT access token"}
|
|
# [webhook] BYOS auth: refresh failed (400) {"error":"expired JWT ..."}
|
|
# — the refresh attempt being rejected by the very expiry it was trying
|
|
# to repair.
|
|
#
|
|
# The renderer's own code assumes "stock Terminus disables session
|
|
# expiration and issues effectively permanent tokens". That is simply not
|
|
# true of this deployment, so the assumption is made true here rather
|
|
# than worked around in the client.
|
|
#
|
|
# The trade is deliberate: a long-lived JWT with full Terminus API rights
|
|
# now sits in OpenBao. Accepted because the alternative was putting the
|
|
# Terminus account PASSWORD into the cluster so an init container could
|
|
# mint a short-lived token on every pod start, which is a strictly worse
|
|
# secret to hold. The token is revocable by rotating the account's
|
|
# credentials; it grants access only to this LAN-only instance.
|
|
#
|
|
# NOTE: the period is fixed at mint time. Changing this does not extend
|
|
# tokens that already exist — they must be re-minted afterwards.
|
|
API_ACCESS_TOKEN_PERIOD: "315360000"
|
|
# The OTHER half of the same problem, and the reason the first fix
|
|
# did not hold. Terminus has TWO independent expiries: the JWT's own
|
|
# lifetime (above) and the SESSION behind it. Raising only the token
|
|
# period left the session at its defaults, so pushes kept working for
|
|
# a while and then failed with
|
|
# HTTP 401 {"error":"This session has expired, please login again"}
|
|
# which is a different error, from a different mechanism, than the
|
|
# "expired JWT access token" the first fix addressed.
|
|
#
|
|
# Documented in doc/configuration.adoc:
|
|
# SESSION_EXPIRATION_ENABLED — "Determines if your browser session
|
|
# or JWT API token is expired or not." Default: true.
|
|
# SESSION_INACTIVITY_LIMIT 1800s, SESSION_LIFETIME_LIMIT 86400s.
|
|
#
|
|
# Disabling it is exactly the state the renderer's own code assumes
|
|
# ("stock Terminus disables session expiration and issues ~100-year
|
|
# tokens") — an assumption that is false by default and has to be
|
|
# made true here, same as the token period.
|
|
#
|
|
# Same trade as above, and it is the deciding one for an unattended
|
|
# appliance: a session that expires needs a human with a password to
|
|
# restore it, and this panel has neither.
|
|
SESSION_EXPIRATION_ENABLED: "false"
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: terminus
|
|
namespace: trmnl
|
|
labels:
|
|
app.kubernetes.io/name: terminus
|
|
annotations:
|
|
# terminus-config is consumed via envFrom, and env vars are read ONCE at
|
|
# container start — Kubernetes does not hot-reload them. The ConfigMap is
|
|
# a plain resource, not a configMapGenerator, so editing it does not
|
|
# change the pod template hash and Flux will not roll the pods either.
|
|
# Without this annotation a ConfigMap-only change (such as raising
|
|
# API_ACCESS_TOKEN_PERIOD) applies to git and to the ConfigMap object
|
|
# while the running app keeps the old value indefinitely — a silent
|
|
# no-op, with the manifest and reality disagreeing.
|
|
#
|
|
# Named per-ConfigMap rather than the blanket `reloader.stakater.com/
|
|
# auto: "true"`, matching mosquitto-deployment.yaml: enrolling anything
|
|
# further stays a visible, reviewable edit to this line.
|
|
configmap.reloader.stakater.com/reload: terminus-config
|
|
spec:
|
|
replicas: 1
|
|
# RWO PVC shared by both containers in this pod, and APP_SETUP runs
|
|
# migrations on boot — two concurrent web pods would race on both.
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: terminus
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: terminus
|
|
spec:
|
|
enableServiceLinks: false
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: OnRootMismatch
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
# Tag pinned deliberately and NOT enrolled in Flux image automation.
|
|
# Terminus is pre-1.0 with no GitHub Releases and no changelog; an
|
|
# unattended bump that changes the firmware or /api/display contract
|
|
# is a device-bricking class of risk. Bumps are a reviewed, manual
|
|
# act — see kubernetes/infrastructure/image-automation/README.md.
|
|
- name: terminus
|
|
image: ghcr.io/usetrmnl/terminus:0.78.0 # {"$imagepolicy": "flux-system:terminus"}
|
|
ports:
|
|
- name: http
|
|
containerPort: 2345
|
|
envFrom:
|
|
- configMapRef:
|
|
name: terminus-config
|
|
env:
|
|
# Runs `hanami assets compile` + `hanami db migrate` in the
|
|
# entrypoint before exec'ing puma. Set on the web container
|
|
# only — the sidekiq sidecar must not race the migration.
|
|
- name: APP_SETUP
|
|
value: "true"
|
|
# CNPG generates this Secret; `uri` already carries the rotated
|
|
# password and points at the tr-pg-rw service.
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: tr-pg-app
|
|
key: uri
|
|
- name: APP_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: terminus-secrets
|
|
key: APP_SECRET
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /app/public/uploads
|
|
subPath: uploads
|
|
- name: data
|
|
mountPath: /app/public/fonts
|
|
subPath: fonts
|
|
# TCP probes, not HTTP: Terminus exposes no documented health
|
|
# endpoint, and every real route is behind either auth or
|
|
# Rack::Attack. Tighten to an HTTP GET once the route set is
|
|
# confirmed against the running app.
|
|
startupProbe:
|
|
tcpSocket:
|
|
port: http
|
|
periodSeconds: 10
|
|
# ~10 min: first boot compiles assets and migrates a fresh DB.
|
|
failureThreshold: 60
|
|
livenessProbe:
|
|
tcpSocket:
|
|
port: http
|
|
periodSeconds: 30
|
|
timeoutSeconds: 5
|
|
readinessProbe:
|
|
tcpSocket:
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
resources:
|
|
requests:
|
|
cpu: 200m
|
|
memory: 512Mi
|
|
limits:
|
|
memory: 1Gi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
- name: sidekiq
|
|
image: ghcr.io/usetrmnl/terminus:0.78.0 # {"$imagepolicy": "flux-system:terminus"}
|
|
command: ["bundle", "exec", "sidekiq", "-r", "./config/sidekiq.rb"]
|
|
envFrom:
|
|
- configMapRef:
|
|
name: terminus-config
|
|
# EXPECTED on a fresh deploy: this container loses the race against
|
|
# the web container's migration and exits 1 with
|
|
# `PG::UndefinedTable: relation "account" does not exist`, then
|
|
# comes up healthy on the restart once the schema exists.
|
|
# Observed on first deploy 2026-08-10; converged in ~1s. There is
|
|
# no upstream ordering hook, and gating it behind an initContainer
|
|
# that probes for a specific table would couple us to Terminus's
|
|
# schema. So this is left to Kubernetes' restart, deliberately —
|
|
# a single sidekiq restart shortly after a rollout is benign. More
|
|
# than a couple, or restarts long after a rollout, are NOT this.
|
|
env:
|
|
# No APP_SETUP here — see the web container.
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: tr-pg-app
|
|
key: uri
|
|
- name: APP_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: terminus-secrets
|
|
key: APP_SECRET
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /app/public/uploads
|
|
subPath: uploads
|
|
- name: data
|
|
mountPath: /app/public/fonts
|
|
subPath: fonts
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
limits:
|
|
memory: 512Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: terminus-data
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: terminus
|
|
namespace: trmnl
|
|
labels:
|
|
app.kubernetes.io/name: terminus
|
|
spec:
|
|
selector:
|
|
app.kubernetes.io/name: terminus
|
|
ports:
|
|
- name: http
|
|
port: 80
|
|
targetPort: http
|