Improvements for haven-notify
This commit is contained in:
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
---
|
---
|
||||||
|
@@ -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"))
|
||||||
|
}
|
||||||
|
40
haven-notify/template/backup_template.tmpl
Normal file
40
haven-notify/template/backup_template.tmpl
Normal 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}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user