Improvements for haven-notify
Some checks failed
Check scripts syntax / check-scripts-syntax (push) Successful in 1m19s
Haven Notify Build and Deploy / Build Haven Notify Image (push) Failing after 1m51s
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (push) Successful in 33s

This commit is contained in:
2025-08-21 21:11:22 -03:00
parent 7520d70ce9
commit 100262513b
5 changed files with 201 additions and 41 deletions

View File

@@ -16,40 +16,8 @@ env:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
jobs: jobs:
# build_haven_notify: build_haven_notify:
# name: Build Haven Notify Image (arm64) name: Build Haven Notify Image
# runs-on: ubuntu-arm64
# steps:
# - name: Check out repository
# uses: actions/checkout@v2
# - name: Install Docker
# run: |
# apt-get update
# apt-get install -y docker.io
# - name: Log in to Container Registry
# run: |
# echo "${{ secrets.REGISTRY_PASSWORD }}" \
# | docker login "${{ env.REGISTRY_HOST }}" \
# -u "${{ env.REGISTRY_USERNAME }}" \
# --password-stdin
# - name: Build and Push Image
# run: |
# TAG=latest
# cd haven-notify
# docker build \
# -t "${{ env.IMAGE_NOTIFY }}:${TAG}" \
# -f Dockerfile .
# docker push "${{ env.IMAGE_NOTIFY }}:${TAG}"
build_haven_notify_amd64:
name: Build Haven Notify Image (amd64)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -74,14 +42,15 @@ jobs:
cd haven-notify cd haven-notify
docker build \ docker buildx build \
-t "${{ env.IMAGE_NOTIFY }}:${TAG}" \ -t "${{ env.IMAGE_NOTIFY }}:${TAG}" \
--platform linux/amd64,linux/arm64 \
-f Dockerfile . -f Dockerfile .
docker push "${{ env.IMAGE_NOTIFY }}:${TAG}" docker push "${{ env.IMAGE_NOTIFY }}:${TAG}"
deploy_haven_notify: deploy_haven_notify:
name: Deploy Haven Notify name: Deploy Haven Notify (internal)
runs-on: ubuntu-amd64 runs-on: ubuntu-amd64
needs: build_haven_notify_amd64 needs: build_haven_notify_amd64
steps: steps:

View File

@@ -3,12 +3,12 @@
## Overview ## Overview
Haven Notify is an internal service designed to send notifications to a specified Discord channel. Haven Notify is an internal service designed to send notifications to a specified Discord channel.
It is built in Go and can be deployed as a container or managed service. It's built in Go and can be deployed as a container or managed service.
## Prerequisites ## Prerequisites
- Go 1.18 or newer (for local development) - Go 1.18 or newer
- Docker (for containerized deployment) - Docker
- Discord Webhook URL - A Discord Webhook URL
## API Specification ## API Specification
@@ -23,6 +23,23 @@ It is built in Go and can be deployed as a container or managed service.
} }
``` ```
- **Endpoint**: `/template/notify/backup_template`
- **Method**: `POST`
- **Request Body**:
```json
{
"title": "Notification Title",
"message": "Notification Message",
"backupSizeInMB": 500,
"extra": [
{
"name": "Additional Info",
"value": "Some extra information"
}
]
}
```
## Setup & Usage ## Setup & Usage
### Docker ### Docker

View File

@@ -26,6 +26,18 @@ spec:
secretKeyRef: secretKeyRef:
name: discord-webhook name: discord-webhook
key: HAVEN_WEBHOOK_URL key: HAVEN_WEBHOOK_URL
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
nodeSelector: nodeSelector:
kubernetes.io/arch: amd64 kubernetes.io/arch: amd64
--- ---

View File

@@ -1,13 +1,16 @@
package main package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"strings"
"time"
) )
// Notification payload // Notification payload
@@ -18,6 +21,9 @@ type Notification struct {
func main() { func main() {
http.HandleFunc("/notify", notifyHandler) http.HandleFunc("/notify", notifyHandler)
http.HandleFunc("/ready", readinessHandler)
http.HandleFunc("/live", livenessHandler)
http.HandleFunc("/template/notify/", templateNotifyHandler)
log.Println("Starting server on :8080...") log.Println("Starting server on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
@@ -53,6 +59,18 @@ func notifyHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Notification sent")) w.Write([]byte("Notification sent"))
} }
// Readiness handler
func readinessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Ready"))
}
// Liveness handler
func livenessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Alive"))
}
func sendDiscordNotification(title, message string) error { func sendDiscordNotification(title, message string) error {
webhookURL := os.Getenv("WEBHOOK_URL") webhookURL := os.Getenv("WEBHOOK_URL")
if webhookURL == "" { if webhookURL == "" {
@@ -90,3 +108,107 @@ func sendDiscordNotification(title, message string) error {
log.Printf("Discord notification sent successfully: Title='%s'", title) log.Printf("Discord notification sent successfully: Title='%s'", title)
return nil return nil
} }
func templateNotifyHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Incoming %s request from %s to %s", r.Method, r.RemoteAddr, r.URL.Path)
if r.Method != http.MethodPost {
log.Printf("Method not allowed: %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("Method not allowed"))
return
}
templateName := r.URL.Path[len("/template/notify/"):] // Extract template name
if templateName == "" {
log.Printf("Template name not provided")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Template name not provided"))
return
}
templatePath := "template/" + templateName + ".tmpl"
templateData, err := ioutil.ReadFile(templatePath)
if err != nil {
log.Printf("Failed to read template: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to read template"))
return
}
tmpl, err := template.New(templateName).Funcs(template.FuncMap{
"formatSize": func(size float64) string {
if size > 1024 {
return fmt.Sprintf("%.2f GiB", size/1024)
}
return fmt.Sprintf("%.2f MiB", size)
},
"upper": strings.ToUpper,
"lower": strings.ToLower,
"title": strings.Title,
"now": func() string {
return fmt.Sprintf("%d", time.Now().Unix())
},
"formatTime": func(timestamp string) string {
if timestamp == "" {
return time.Now().Format("2006-01-02T15:04:05Z")
}
return timestamp
},
}).Parse(string(templateData))
if err != nil {
log.Printf("Failed to parse template: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to parse template"))
return
}
var rawPayload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&rawPayload); err != nil {
log.Printf("Invalid payload: %v", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid payload"))
return
}
// Normalize keys to lowercase for case-insensitive parsing
payload := make(map[string]interface{})
for key, value := range rawPayload {
payload[strings.ToLower(key)] = value
}
var filledTemplate bytes.Buffer
if err := tmpl.Execute(&filledTemplate, payload); err != nil {
log.Printf("Failed to execute template: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to execute template"))
return
}
webhookURL := os.Getenv("WEBHOOK_URL")
if webhookURL == "" {
log.Printf("WEBHOOK_URL environment variable not set")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("WEBHOOK_URL environment variable not set"))
return
}
resp, err := http.Post(webhookURL, "application/json", &filledTemplate)
if err != nil {
log.Printf("Error posting to Discord webhook: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to send notification"))
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Discord webhook returned status: %s", resp.Status)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to send notification"))
return
}
log.Printf("Notification sent successfully using template '%s'", templateName)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Notification sent"))
}

View File

@@ -0,0 +1,40 @@
{{/*
Docker Backup Notification Template
Variables expected:
- .title: The backup title/name
- .asset: The asset being backed up
- .backupsizeinmb: The backup size in MB (will be formatted automatically)
- .extra: Optional array of additional fields with .name and .value
Template Functions Available:
- formatSize: Formats size in MB/GiB automatically
*/}}
{
"embeds": [
{
"title": "📦 Docker Backup - {{.title}}",
"description": "**{{.asset}}** has been backup-ed successfully! ✅🫡\n",
"color": 3066993,
"fields": [
{
"name": "💾 Backup Size",
"value": "{{if .backupsizeinmb}}{{formatSize .backupsizeinmb}}{{else}}Unknown{{end}}",
"inline": true
}
{{- if .extra}}
{{- range $index, $field := .extra}},
{
"name": "{{$field.name}}",
"value": "{{$field.value}}",
"inline": true
}
{{- end}}
{{- end}}
],
"footer": {
"text": "✨ Haven Notify ✨"
}{{if .timestamp}},
"timestamp": "{{formatTime .timestamp}}"{{end}}
}
]
}