All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 3s
Haven Notify Build and Deploy / Test Haven Notify (push) Successful in 46s
Haven Notify Build and Deploy / Build Haven Notify Image (PR) (push) Has been skipped
Haven Notify Build and Deploy / Build Haven Notify Image (push) Successful in 20s
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (push) Successful in 3s
263 lines
7.0 KiB
Markdown
263 lines
7.0 KiB
Markdown
<div align="center">
|
|
<img src="./assets/widelogo.png" alt="Haven Notify Logo">
|
|
</div>
|
|
|
|
## Overview
|
|
|
|
Haven Notify is an internal Go service that validates notification requests, renders Discord-compatible payloads, and delivers them to a configured Discord webhook.
|
|
|
|
It runs as a standalone binary or container. Templates are embedded in the binary and validated at startup.
|
|
|
|
## Prerequisites
|
|
|
|
- Go 1.25.10
|
|
- A Docker API compatible with Testcontainers for the end-to-end suite
|
|
- A Discord webhook URL for normal service operation
|
|
|
|
`WEBHOOK_URL` is required and must be an absolute HTTP or HTTPS URL. The process exits before becoming ready when the configuration or embedded templates are invalid.
|
|
|
|
## API
|
|
|
|
All notification endpoints require `POST` and `Content-Type: application/json`. Request bodies are limited to 64 KiB, documented fields are required, and unknown fields are accepted for forward compatibility.
|
|
|
|
### V1 API
|
|
|
|
The original routes, request bodies, and Discord message appearance remain unchanged.
|
|
|
|
#### Send Notification
|
|
|
|
- **Endpoint**: `/notify`
|
|
- **Request Body**:
|
|
|
|
```json
|
|
{
|
|
"title": "Notification Title",
|
|
"message": "Notification Message"
|
|
}
|
|
```
|
|
|
|
#### Send Backup Notification
|
|
|
|
- **Endpoint**: `/template/notify/backup`
|
|
- **Request Body**:
|
|
|
|
```json
|
|
{
|
|
"title": "Nightly backup",
|
|
"asset": "Photos",
|
|
"backupSizeInMB": 1536,
|
|
"extra": [
|
|
{
|
|
"name": "Host",
|
|
"value": "nebula"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Send Update Notification
|
|
|
|
- **Endpoint**: `/template/notify/update`
|
|
- **Request Body**:
|
|
|
|
```json
|
|
{
|
|
"host": "nexus",
|
|
"asset": "k3s",
|
|
"time": 42
|
|
}
|
|
```
|
|
|
|
`time` is expressed in seconds.
|
|
|
|
#### Send Error Notification
|
|
|
|
- **Endpoint**: `/template/notify/error`
|
|
- **Request Body**:
|
|
|
|
```json
|
|
{
|
|
"caller": "backup-job",
|
|
"message": "Error while moving file",
|
|
"critical": true,
|
|
"extra": [
|
|
{
|
|
"name": "Path",
|
|
"value": "/mnt/backups"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### V2 API
|
|
|
|
V2 notifications use compact, consistently styled Discord embeds. Colors, icons, timestamps, field ordering, and layout are controlled by Haven Notify rather than the caller.
|
|
|
|
Every v2 request may include:
|
|
|
|
- `source`: a nonblank string identifying the caller.
|
|
- `occurredAt`: an RFC3339 timestamp. When omitted, Haven Notify uses the current UTC time.
|
|
- `extra`: an array of nonblank `name` and `value` strings. Short single-line values render inline; multiline or long values render full-width.
|
|
|
|
V2 preserves caller field order. Fixed notification fields are placed before `extra` fields so that trailing extras are discarded first if Discord's 25-field or 6,000-character embed limits are reached. Oversized text is safely truncated and extra fields may be omitted from the delivered message.
|
|
|
|
#### Send a Message
|
|
|
|
- **Endpoint**: `/api/v2/messages`
|
|
- **Required**: `message`
|
|
- **Optional**: `title`, `source`, `occurredAt`, `extra`
|
|
|
|
```json
|
|
{
|
|
"title": "Maintenance complete",
|
|
"message": "The storage job completed successfully.",
|
|
"source": "scheduler",
|
|
"extra": [
|
|
{
|
|
"name": "Region",
|
|
"value": "home"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Send a Backup Notification
|
|
|
|
- **Endpoint**: `/api/v2/notifications/backup`
|
|
- **Required**: `asset`, nonnegative integer `sizeBytes`
|
|
- **Optional**: `title`, `source`, nonnegative `durationSeconds`, `occurredAt`, `extra`
|
|
|
|
```json
|
|
{
|
|
"title": "Nightly backup",
|
|
"asset": "Photos",
|
|
"sizeBytes": 1610612736,
|
|
"durationSeconds": 125,
|
|
"source": "restic",
|
|
"extra": [
|
|
{
|
|
"name": "Host",
|
|
"value": "nebula"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`sizeBytes` is displayed using IEC units such as KiB, MiB, and GiB. Durations use a compact form such as `42.5s`, `2m 5s`, or `1h 2m 3s`.
|
|
|
|
#### Send an Update Notification
|
|
|
|
- **Endpoint**: `/api/v2/notifications/update`
|
|
- **Required**: `host`, `asset`, nonnegative `durationSeconds`
|
|
- **Optional**: `source`, `fromVersion`, `toVersion`, `occurredAt`, `extra`
|
|
|
|
```json
|
|
{
|
|
"host": "nexus",
|
|
"asset": "k3s",
|
|
"durationSeconds": 42.5,
|
|
"fromVersion": "1.32.5",
|
|
"toVersion": "1.33.1",
|
|
"source": "system-updater"
|
|
}
|
|
```
|
|
|
|
When both versions are supplied, the embed displays a transition such as `1.32.5 → 1.33.1`. A single supplied version is labeled as the previous or installed version.
|
|
|
|
#### Send an Error Notification
|
|
|
|
- **Endpoint**: `/api/v2/notifications/error`
|
|
- **Required**: `source`, `message`, `severity`
|
|
- **Optional**: `errorCode`, `occurredAt`, `extra`
|
|
|
|
```json
|
|
{
|
|
"source": "backup-job",
|
|
"message": "The repository is unavailable.",
|
|
"severity": "critical",
|
|
"errorCode": "E_REPOSITORY",
|
|
"extra": [
|
|
{
|
|
"name": "Path",
|
|
"value": "/mnt/backups"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`severity` must be `warning`, `error`, or `critical`; it selects the embed title, icon, and color.
|
|
|
|
### Health Probes
|
|
|
|
- `GET` or `HEAD /ready` returns `200 Ready` after configuration and templates initialize.
|
|
- `GET` or `HEAD /live` returns `200 Alive` while the process can serve HTTP.
|
|
|
|
### Response Statuses
|
|
|
|
| Status | Meaning |
|
|
|---|---|
|
|
| `200` | Notification delivered |
|
|
| `400` | Invalid or missing request data |
|
|
| `404` | Unknown route or template |
|
|
| `405` | Unsupported method |
|
|
| `415` | Content type is not JSON |
|
|
| `502` | Discord rejected the request or delivery retries failed |
|
|
| `504` | Discord delivery exceeded its timeout |
|
|
|
|
Haven Notify suppresses Discord mention parsing and safely truncates content to Discord's message and embed limits. Transport errors, rate limits, and server errors receive at most two retries within a ten-second total delivery budget.
|
|
|
|
## Run
|
|
|
|
### Go
|
|
|
|
```sh
|
|
WEBHOOK_URL=https://discord.com/api/webhooks/... go run .
|
|
```
|
|
|
|
### Docker
|
|
|
|
```sh
|
|
docker build -t haven-notify .
|
|
docker run --rm -p 8080:8080 \
|
|
-e WEBHOOK_URL=https://discord.com/api/webhooks/... \
|
|
haven-notify
|
|
```
|
|
|
|
The runtime container uses a non-root `haven-notify` user and contains only the binary, Alpine runtime files, and CA certificates.
|
|
|
|
## Tests
|
|
|
|
Run unit and HTTP contract tests:
|
|
|
|
```sh
|
|
go test ./...
|
|
```
|
|
|
|
Run static analysis, the race detector, and coverage (the race detector requires CGO support):
|
|
|
|
```sh
|
|
go vet ./...
|
|
go test -race -shuffle=on -count=1 -covermode=atomic -coverprofile=coverage.out ./...
|
|
go tool cover -func=coverage.out
|
|
```
|
|
|
|
Run the seeded fuzz target:
|
|
|
|
```sh
|
|
go test -run=^$ -fuzz=FuzzErrorNotificationHandler -fuzztime=10s .
|
|
```
|
|
|
|
Run the black-box container suite:
|
|
|
|
```sh
|
|
go test -tags=e2e -run '^TestContainerE2E$' -count=1 -timeout=5m -v .
|
|
```
|
|
|
|
The end-to-end suite builds the production Dockerfile, starts Haven Notify and a disposable MockServer on an isolated network, and verifies the complete HTTP-to-Discord payload flow. It never calls a real Discord webhook. Configure `DOCKER_HOST` or the Testcontainers host settings when the Docker daemon is not available through the platform's default socket.
|
|
|
|
## Deployment
|
|
|
|
The Kubernetes Deployment, Service, and Ingress manifest lives in the `haven` homelab repository at `infra/haven-notify.yaml`. This repository holds the service source, Dockerfile, tests, and Gitea Actions pipeline.
|
|
|
|
The production manifest maps the `HAVEN_WEBHOOK_URL` key from the `discord-webhook` secret into the container's `WEBHOOK_URL` environment variable.
|