adding test containers
Some checks failed
Check scripts syntax / check-scripts-syntax (push) Successful in 3s
Test automated-nfs-backup / test-automated-nfs-backup (push) Successful in 4s
Haven Notify Build and Deploy / Build Haven Notify Image (pull_request) Has been cancelled
Haven Notify Build and Deploy / Build Haven Notify Image (PR) (pull_request) Has been cancelled
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (pull_request) Has been cancelled
Haven Notify Build and Deploy / Test Haven Notify (pull_request) Has been cancelled

This commit is contained in:
2026-07-16 09:14:51 -03:00
parent 0043c4ae00
commit b1cdf15cc8
19 changed files with 2390 additions and 298 deletions

View File

@@ -1,214 +1,37 @@
package main
import (
"bytes"
"encoding/json"
"context"
"errors"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"os/signal"
"syscall"
)
// Notification payload
type Notification struct {
Title string `json:"title"`
Message string `json:"message"`
}
const listenAddress = ":8080"
func main() {
http.HandleFunc("/notify", notifyHandler)
http.HandleFunc("/ready", readinessHandler)
http.HandleFunc("/live", livenessHandler)
http.HandleFunc("/template/notify/", templateNotifyHandler)
log.Println("Starting server on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
logger := log.Default()
if err := run(ctx, os.Getenv("WEBHOOK_URL"), listenAddress, logger); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatalf("Server stopped unexpectedly: %v", err)
}
}
func notifyHandler(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
}
var notif Notification
if err := json.NewDecoder(r.Body).Decode(&notif); err != nil {
log.Printf("Invalid payload: %v", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid payload"))
return
}
log.Printf("Received notification payload: Title='%s', Message='%s'", notif.Title, notif.Message)
// Call Discord notification function
if err := sendDiscordNotification(notif.Title, notif.Message); err != nil {
log.Printf("Failed to send Discord notification: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to send Discord notification"))
return
}
log.Printf("Notification sent successfully for Title='%s'", notif.Title)
w.WriteHeader(http.StatusOK)
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 {
webhookURL := os.Getenv("WEBHOOK_URL")
if webhookURL == "" {
log.Printf("WEBHOOK_URL environment variable not set")
return fmt.Errorf("WEBHOOK_URL environment variable not set")
}
// Discord webhook payload
type discordPayload struct {
Content string `json:"content"`
}
content := "**" + title + "**\n" + message
payload := discordPayload{Content: content}
jsonData, err := json.Marshal(payload)
func run(ctx context.Context, webhookURL, address string, logger *log.Logger) error {
app, err := newApplication(applicationConfig{WebhookURL: webhookURL, Logger: logger})
if err != nil {
log.Printf("Failed to marshal Discord payload: %v", err)
return err
return fmt.Errorf("configuration failed: %w", err)
}
log.Printf("Sending Discord notification: Title='%s', Message='%s'", title, message)
resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData))
listener, err := netListen("tcp", address)
if err != nil {
log.Printf("Error posting to Discord webhook: %v", err)
return err
return fmt.Errorf("listen on %s: %w", address, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Discord webhook returned status: %s", resp.Status)
return fmt.Errorf("Discord webhook returned status: %s", resp.Status)
}
log.Printf("Discord notification sent successfully: Title='%s'", title)
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"))
logger.Printf("Starting server on %s", address)
return serve(ctx, newHTTPServer(app.handler()), listener)
}