From b0c17527f245c31be13798856e0988c397731947 Mon Sep 17 00:00:00 2001 From: rock Date: Thu, 10 Sep 2026 08:25:36 +0900 Subject: [PATCH] feat(gotify): add push notification server + SMTP email relay - Gotify server (ghcr.io/gotify/server:2.6.1) in notifications namespace - SMTP emailer sidecar polls messages, forwards as email via msmtp - Ingress at gotify.riotpiao.com with WebSocket support - 1Gi Longhorn PVC for message persistence - ArgoCD Application (wave 8, auto-sync) - Secrets template for admin creds, SMTP config, tokens - README with setup guide: tokens, Forgejo webhooks, SMTP providers Enables PR created/merged email notifications from Forgejo. --- k8s/apps/gotify/README.md | 107 ++++++++++++++++ k8s/apps/gotify/deployment.yaml | 194 +++++++++++++++++++++++++++++ k8s/apps/gotify/ingress.yaml | 26 ++++ k8s/apps/gotify/kustomization.yaml | 9 ++ k8s/apps/gotify/namespace.yaml | 6 + k8s/apps/gotify/pvc.yaml | 12 ++ k8s/apps/gotify/secrets.yaml | 48 +++++++ k8s/apps/gotify/service.yaml | 14 +++ k8s/argocd/apps/59-gotify.yaml | 32 +++++ 9 files changed, 448 insertions(+) create mode 100644 k8s/apps/gotify/README.md create mode 100644 k8s/apps/gotify/deployment.yaml create mode 100644 k8s/apps/gotify/ingress.yaml create mode 100644 k8s/apps/gotify/kustomization.yaml create mode 100644 k8s/apps/gotify/namespace.yaml create mode 100644 k8s/apps/gotify/pvc.yaml create mode 100644 k8s/apps/gotify/secrets.yaml create mode 100644 k8s/apps/gotify/service.yaml create mode 100644 k8s/argocd/apps/59-gotify.yaml diff --git a/k8s/apps/gotify/README.md b/k8s/apps/gotify/README.md new file mode 100644 index 0000000..91d4408 --- /dev/null +++ b/k8s/apps/gotify/README.md @@ -0,0 +1,107 @@ +# Gotify — Push Notifications + Email Relay + +Self-hosted notification server with SMTP email forwarding sidecar. + +## Architecture + +``` +Forgejo webhook ──POST──→ Gotify API (:80/message) + │ + ┌─────────┼─────────┐ + ▼ ▼ + Push notification SMTP emailer sidecar + (mobile/desktop) (polls → sends email) +``` + +## Setup (one-time, after first deploy) + +### 1. Encrypt secrets before committing + +```bash +# Edit secrets.yaml with real values first, then: +sops -e -i k8s/apps/gotify/secrets.yaml +``` + +### 2. Create Gotify app + client tokens + +1. Login to `https://gotify.riotpiao.com` with admin creds +2. **Applications** → Create `forgejo` → copy **app token** +3. **Clients** → Create `smtp-emailer` → copy **client token** +4. Update `gotify-tokens` secret: + ```bash + kubectl -n notifications create secret generic gotify-tokens \ + --from-literal=app-token= \ + --from-literal=client-token= \ + --dry-run=client -o yaml | kubectl apply -f - + ``` + +### 3. Configure Forgejo webhook + +In each Forgejo repo → **Settings** → **Webhooks** → **Add Webhook** → **Gotify**: + +| Field | Value | +|-------|-------| +| Target URL | `http://gotify.notifications.svc.cluster.local/message` | +| Token | The **app token** from step 2 | +| Events | Pull Request (Created, Merged, Closed) | + +Or via API: +```bash +FORGEJO_TOKEN="" +APP_TOKEN="" + +curl -s -X POST "https://forgejo.riotpiao.com/api/v1/repos/rock/homelab/hooks" \ + -H "Authorization: token $FORGEJO_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "gotify", + "active": true, + "config": { + "content_type": "json", + "url": "http://gotify.notifications.svc.cluster.local/message?token='"$APP_TOKEN"'" + }, + "events": ["pull_request", "pull_request_assign", "pull_request_review"], + "authorization_header": "" + }' +``` + +### 4. Add CoreDNS rewrite (if accessing via public hostname) + +Only needed if Cloudflare Tunnel is used for gotify.riotpiao.com: + +``` +# terraform/files/coredns/Corefile — add rewrite: +rewrite name gotify.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local +``` + +Then: `cd terraform && terraform apply && cd .. && make apply-cp` + +### 5. SMTP providers + +| Provider | Host | Port | Notes | +|----------|------|------|-------| +| Gmail | smtp.gmail.com | 587 | Use App Password (2FA required) | +| Resend | smtp.resend.com | 587 | Free 100 emails/day | +| Sendgrid | smtp.sendgrid.net | 587 | Free 100 emails/day | +| Mailgun | smtp.mailgun.org | 587 | Free 5000/month | + +## Notification priority levels + +| Priority | Meaning | Email forwarded? | +|----------|---------|-----------------| +| 0-4 | Low (info) | No (below MIN_PRIORITY=5) | +| 5-7 | Normal (PR created) | Yes | +| 8-10 | High (PR merged, failures) | Yes | + +## Verify + +```bash +# Test push notification +APP_TOKEN="" +curl -X POST "https://gotify.riotpiao.com/message?token=$APP_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title":"Test","message":"Hello from homelab","priority":5}' + +# Check email sidecar logs +kubectl -n notifications logs deployment/gotify -c smtp-emailer --tail=20 +``` diff --git a/k8s/apps/gotify/deployment.yaml b/k8s/apps/gotify/deployment.yaml new file mode 100644 index 0000000..e9c6fbd --- /dev/null +++ b/k8s/apps/gotify/deployment.yaml @@ -0,0 +1,194 @@ +# Gotify — self-hosted push notification server + SMTP email relay. +# Forgejo webhooks → Gotify → push notifications + email forwarding. +# Runs on control plane (no GPU needed), lightweight. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gotify + namespace: notifications + labels: + app: gotify +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: gotify + template: + metadata: + labels: + app: gotify + spec: + containers: + # --- Gotify server --- + - name: gotify + image: ghcr.io/gotify/server:2.6.1 + ports: + - containerPort: 80 + protocol: TCP + env: + - name: GOTIFY_DEFAULTUSER_NAME + valueFrom: + secretKeyRef: + name: gotify-admin + key: username + - name: GOTIFY_DEFAULTUSER_PASS + valueFrom: + secretKeyRef: + name: gotify-admin + key: password + - name: GOTIFY_SERVER_PORT + value: "80" + - name: GOTIFY_SERVER_KEEPALIVEPERIODSECONDS + value: "0" + - name: TZ + value: Asia/Tokyo + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + volumeMounts: + - mountPath: /app/data + name: data + livenessProbe: + httpGet: + path: /health + port: 80 + periodSeconds: 30 + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 80 + periodSeconds: 10 + initialDelaySeconds: 5 + + # --- SMTP emailer sidecar --- + # Watches Gotify WebSocket stream, forwards messages as email. + # https://github.com/eternal-flame-AD/gotify-broadcast + - name: smtp-emailer + image: ghcr.io/gotify/server:2.6.1 + command: + - /bin/sh + - -c + - | + # Wait for Gotify to be ready + until wget -qO- http://localhost:80/health >/dev/null 2>&1; do + echo "Waiting for Gotify..." + sleep 2 + done + echo "Gotify is ready, starting email relay..." + + # Poll Gotify messages and forward via SMTP using msmtp + # Install msmtp for lightweight SMTP sending + apk add --no-cache msmtp curl jq + + # Configure msmtp + cat > /tmp/msmtprc </dev/null) + + if [ -n "$MESSAGES" ]; then + echo "$MESSAGES" | jq -r '.messages[]? | @base64' | while read -r MSG; do + DECODED=$(echo "$MSG" | base64 -d) + ID=$(echo "$DECODED" | jq -r '.id') + TITLE=$(echo "$DECODED" | jq -r '.title // "Notification"') + BODY=$(echo "$DECODED" | jq -r '.message // ""') + PRIORITY=$(echo "$DECODED" | jq -r '.priority // 5') + APP=$(echo "$DECODED" | jq -r '.appid // 0') + DATE=$(echo "$DECODED" | jq -r '.date // ""') + + # Only forward messages with priority >= configured threshold + if [ "$PRIORITY" -ge "${MIN_PRIORITY:-0}" ]; then + printf "Subject: [Gotify] %s\nFrom: %s\nTo: %s\nContent-Type: text/plain; charset=UTF-8\n\n%s\n\n---\nPriority: %s\nDate: %s" \ + "$TITLE" "$SMTP_FROM" "$NOTIFY_EMAIL" "$BODY" "$PRIORITY" "$DATE" | \ + msmtp -C /tmp/msmtprc "$NOTIFY_EMAIL" && \ + echo "Email sent for message $ID: $TITLE" || \ + echo "Failed to send email for message $ID" + fi + + # Update last seen ID + if [ "$ID" -gt "$LAST_ID" ]; then + LAST_ID=$ID + fi + done + fi + + sleep ${POLL_INTERVAL:-30} + done + env: + - name: GOTIFY_CLIENT_TOKEN + valueFrom: + secretKeyRef: + name: gotify-tokens + key: client-token + - name: SMTP_HOST + valueFrom: + secretKeyRef: + name: gotify-smtp + key: host + - name: SMTP_PORT + valueFrom: + secretKeyRef: + name: gotify-smtp + key: port + - name: SMTP_FROM + valueFrom: + secretKeyRef: + name: gotify-smtp + key: from + - name: SMTP_USER + valueFrom: + secretKeyRef: + name: gotify-smtp + key: user + - name: SMTP_PASS + valueFrom: + secretKeyRef: + name: gotify-smtp + key: password + - name: NOTIFY_EMAIL + valueFrom: + secretKeyRef: + name: gotify-smtp + key: notify-email + - name: MIN_PRIORITY + value: "5" + - name: POLL_INTERVAL + value: "15" + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: gotify-data diff --git a/k8s/apps/gotify/ingress.yaml b/k8s/apps/gotify/ingress.yaml new file mode 100644 index 0000000..fde01cf --- /dev/null +++ b/k8s/apps/gotify/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gotify + namespace: notifications + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + # WebSocket support for Gotify client connections + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +spec: + ingressClassName: nginx + rules: + - host: gotify.riotpiao.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gotify + port: + number: 80 diff --git a/k8s/apps/gotify/kustomization.yaml b/k8s/apps/gotify/kustomization.yaml new file mode 100644 index 0000000..543a73a --- /dev/null +++ b/k8s/apps/gotify/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - pvc.yaml + - secrets.yaml + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/k8s/apps/gotify/namespace.yaml b/k8s/apps/gotify/namespace.yaml new file mode 100644 index 0000000..e6acc35 --- /dev/null +++ b/k8s/apps/gotify/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: notifications + labels: + kubernetes.io/metadata.name: notifications diff --git a/k8s/apps/gotify/pvc.yaml b/k8s/apps/gotify/pvc.yaml new file mode 100644 index 0000000..d040506 --- /dev/null +++ b/k8s/apps/gotify/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gotify-data + namespace: notifications +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 1Gi diff --git a/k8s/apps/gotify/secrets.yaml b/k8s/apps/gotify/secrets.yaml new file mode 100644 index 0000000..4ee3928 --- /dev/null +++ b/k8s/apps/gotify/secrets.yaml @@ -0,0 +1,48 @@ +# Gotify secrets — SOPS-encrypt before committing! +# sops -e -i k8s/apps/gotify/secrets.yaml +# +# After Gotify starts: +# 1. Login to gotify.riotpiao.com with admin creds below +# 2. Create an Application (e.g., "forgejo") → copy the app token +# 3. Create a Client → copy the client token +# 4. Update gotify-tokens secret with the client token +# 5. Configure Forgejo webhook (see README below) +--- +apiVersion: v1 +kind: Secret +metadata: + name: gotify-admin + namespace: notifications +type: Opaque +stringData: + username: admin + password: CHANGE_ME_BEFORE_DEPLOY +--- +apiVersion: v1 +kind: Secret +metadata: + name: gotify-tokens + namespace: notifications +type: Opaque +stringData: + # Create after first login: + # - App token: Gotify UI → Applications → Create → copy token + # - Client token: Gotify UI → Clients → Create → copy token + app-token: CHANGE_AFTER_FIRST_LOGIN + client-token: CHANGE_AFTER_FIRST_LOGIN +--- +apiVersion: v1 +kind: Secret +metadata: + name: gotify-smtp + namespace: notifications +type: Opaque +stringData: + # SMTP config for email forwarding + # Gmail example (use App Password, not account password): + host: smtp.gmail.com + port: "587" + from: gotify@riotpiao.com + user: CHANGE_ME + password: CHANGE_ME + notify-email: locartrock@gmail.com diff --git a/k8s/apps/gotify/service.yaml b/k8s/apps/gotify/service.yaml new file mode 100644 index 0000000..1d998b0 --- /dev/null +++ b/k8s/apps/gotify/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: gotify + namespace: notifications + labels: + app: gotify +spec: + selector: + app: gotify + ports: + - port: 80 + targetPort: 80 + protocol: TCP diff --git a/k8s/argocd/apps/59-gotify.yaml b/k8s/argocd/apps/59-gotify.yaml new file mode 100644 index 0000000..abc1f25 --- /dev/null +++ b/k8s/argocd/apps/59-gotify.yaml @@ -0,0 +1,32 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gotify + namespace: argocd + labels: + app.kubernetes.io/name: gotify + app.kubernetes.io/component: notifications + annotations: + argocd.argoproj.io/sync-wave: "8" +spec: + project: homelab + revisionHistoryLimit: 3 + source: + repoURL: https://forgejo.riotpiao.com/rock/homelab.git + targetRevision: main + path: k8s/apps/gotify + destination: + server: https://kubernetes.default.svc + namespace: notifications + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m