Notifications¶
tuppr can send a notification as each node's Talos upgrade begins. Delivery goes through apprise-go, so a single URL can target any of Apprise's 100+ services (Discord, Telegram, Gotify, ntfy, Pushover, email, generic webhooks, …).
Scope today
Notifications fire when a node's Talos upgrade starts. KubernetesUpgrade
does not emit notifications, and there are no success/failure notifications yet.
Enable¶
tuppr reads the notification URL from a Secret so the credential never lands in
a TalosUpgrade or in chart values. Create the Secret, then point the chart at
it:
apiVersion: v1
kind: Secret
metadata:
name: tuppr-notifications
namespace: system-upgrade
stringData:
# An Apprise URL - see https://appriseit.com/services/
url: discord://webhook_id/webhook_token
# Helm values
notification:
enabled: true
secretName: tuppr-notifications
secretKey: url # key within the Secret (default: url)
The notification URL¶
The URL uses Apprise syntax (not shoutrrr). A few common shapes:
| Service | Example URL |
|---|---|
| Discord | discord://webhook_id/webhook_token |
| Telegram | tgram://bottoken/ChatID |
| Gotify | gotifys://gotify.example.com/token |
| ntfy | ntfys://ntfy.sh/my-topic |
| Pushover | pover://user_key@token |
The authoritative per-service syntax (and every other supported service) is at https://appriseit.com/services/.
Customize the title and message¶
Both the title and message are rendered as Go templates, so you can tailor them. Leave a template empty to keep the built-in default.
notification:
enabled: true
secretName: tuppr-notifications
titleTemplate: "🚀 Talos upgrade — {{ .Node }}"
messageTemplate: "{{ .Node }} → Talos {{ .TargetVersion }} (plan {{ .Plan }})"
Context available to both templates:
| Field | Value |
|---|---|
.Node |
The node being upgraded. |
.CurrentVersion |
Its current Talos version (empty if undetermined). |
.TargetVersion |
The target Talos version. |
.Plan |
The TalosUpgrade resource name. |
Functions are the go-sprout/sprout
safe helper set plus a strict env ({{ env "NAME" }}, which errors on an
unset variable). Filesystem, network, and the sprig-backward aliases are
excluded, so a template can't read a file or make a network call.
sprout function names differ from sprig
sprout renames some helpers - it is toUpper, not upper, and the
sprig-backward aliases are intentionally absent. Check the
go-sprout/sprout registry list for the
exact names.
Route through chaski¶
chaski is a small webhook→ notification relay: it receives JSON, gates it with a CEL expression, renders templates, and forwards to one or more targets. Putting chaski between tuppr and your providers adds CEL gating, fan-out to several destinations, and provider conversion on top of tuppr's single-target send - and lets you centralize routing for more apps than just tuppr.
The trick: tuppr targets chaski using Apprise's generic JSON target, which POSTs to an HTTP endpoint.
flowchart LR
T["tuppr<br/>NOTIFICATION_URL = jsons://…/hooks/tuppr"]
-->|"POST {title, message, type, …}"| C["chaski<br/>/hooks/tuppr"]
C -->|whenExpr gate + templates| D[Discord]
C --> S[Slack]
C --> H["custom HTTP"]
tuppr side - put a json:// (HTTP) or jsons:// (HTTPS) URL pointing at
chaski's /hooks/{route} endpoint in the notification Secret:
stringData:
# route "tuppr"; in-cluster chaski Service on port 8080
url: json://chaski.system-upgrade.svc.cluster.local:8080/hooks/tuppr
Apprise's JSON target POSTs a body like this, which chaski exposes to CEL and
templates as payload:
{ "version": "1.0", "title": "Tuppr Upgrade Started", "message": "Node worker-01 is upgrading Talos from v1.13.6 -> v1.13.7", "type": "info", "attachments": [] }
chaski side - a route named tuppr that gates on the payload and fans out.
Here it forwards every upgrade to Discord and, for the west nodes only, also
to a PagerDuty-style HTTP endpoint:
targets:
discord:
apprise:
url: '{{ env "DISCORD_URL" }}'
oncall:
http:
url: '{{ env "ONCALL_WEBHOOK" }}'
method: POST
routes:
tuppr:
target: discord
# Relay every tuppr notification to Discord, retitled.
title: "Talos rollout"
message: "{{ .payload.message }}"
tuppr-oncall:
target: oncall
# Only page when a west-zone node starts upgrading.
whenExpr: payload.message.contains("worker-west")
message: "{{ .payload.message }}"
Test without sending
chaski accepts ?dryRun=1 to preview which targets match and what would render
(including "fired": false when a whenExpr gate doesn't match), so you can
confirm a route before wiring tuppr to it. Inbound token auth is off by default,
which is fine for cluster-internal senders like tuppr; set CHASKI_WEBHOOK_TOKEN
to require a bearer token.
Because tuppr sends plain text, keep the chaski templates referencing
{{ .payload.message }} / {{ .payload.title }}. For the full chaski
configuration surface (signature verification, per-target params, HTTP forward
options), see the chaski README.