Files
uptop/internal/importer/kuma.go
T
lerko ae141c62ba fix(store): replace panic with error return, handle unmarshal errors
generateToken() now returns (string, error) instead of panicking on
crypto/rand failure. All json.Unmarshal calls for alert settings now
check and propagate errors instead of silently ignoring them.

Adds Close() to Store interface for graceful shutdown support.
Skips malformed notification entries during Kuma import.
2026-05-23 13:15:39 -04:00

189 lines
4.8 KiB
Go

package importer
import (
"encoding/json"
"fmt"
"go-upkeep/internal/models"
"os"
"strings"
)
type KumaBackup struct {
Version string `json:"version"`
MonitorList []KumaMonitor `json:"monitorList"`
NotificationList []KumaNotifEntry `json:"notificationList"`
}
type KumaMonitor struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
URL string `json:"url"`
Hostname string `json:"hostname"`
Port int `json:"port"`
Type string `json:"type"`
Interval int `json:"interval"`
Timeout int `json:"timeout"`
MaxRetries int `json:"maxretries"`
Method string `json:"method"`
AcceptedCodes []string `json:"accepted_statuscodes"`
IgnoreTLS bool `json:"ignoreTls"`
Parent int `json:"parent"`
Active bool `json:"active"`
DNSResolveType string `json:"dns_resolve_type"`
DNSResolveServer string `json:"dns_resolve_server"`
NotificationIDs map[string]bool `json:"notificationIDList"`
ExpiryNotif bool `json:"expiryNotification"`
Tags []json.RawMessage `json:"tags"`
}
type KumaNotifEntry struct {
ID int `json:"id"`
Name string `json:"name"`
Config string `json:"config"`
}
type KumaNotifConfig struct {
Type string `json:"type"`
URL string `json:"ntfyserverurl"`
Topic string `json:"ntfytopic"`
Priority int `json:"ntfyPriority"`
AuthMode string `json:"ntfyAuthenticationMethod"`
Username string `json:"ntfyusername"`
Password string `json:"ntfypassword"`
}
func LoadKumaFile(path string) (*KumaBackup, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
var backup KumaBackup
if err := json.Unmarshal(data, &backup); err != nil {
return nil, fmt.Errorf("parse JSON: %w", err)
}
return &backup, nil
}
func ConvertKuma(kb *KumaBackup) models.Backup {
alertMap := convertKumaNotifications(kb.NotificationList)
var alerts []models.AlertConfig
for _, a := range alertMap {
alerts = append(alerts, a)
}
kumaToUpkeepAlert := make(map[int]int)
for _, n := range kb.NotificationList {
if a, ok := alertMap[n.ID]; ok {
kumaToUpkeepAlert[n.ID] = a.ID
}
}
var sites []models.Site
for _, m := range kb.MonitorList {
site := convertKumaMonitor(m, kumaToUpkeepAlert)
sites = append(sites, site)
}
return models.Backup{
Sites: sites,
Alerts: alerts,
}
}
func convertKumaNotifications(entries []KumaNotifEntry) map[int]models.AlertConfig {
result := make(map[int]models.AlertConfig)
for _, entry := range entries {
var cfg KumaNotifConfig
if err := json.Unmarshal([]byte(entry.Config), &cfg); err != nil {
continue
}
alert := models.AlertConfig{
ID: entry.ID,
Name: entry.Name,
Settings: make(map[string]string),
}
switch cfg.Type {
case "ntfy":
alert.Type = "ntfy"
alert.Settings["url"] = strings.TrimRight(cfg.URL, "/")
alert.Settings["topic"] = cfg.Topic
alert.Settings["priority"] = fmt.Sprintf("%d", cfg.Priority)
if cfg.AuthMode == "usernamePassword" {
alert.Settings["username"] = cfg.Username
alert.Settings["password"] = cfg.Password
}
case "discord":
alert.Type = "discord"
alert.Settings["url"] = cfg.URL
case "slack":
alert.Type = "slack"
alert.Settings["url"] = cfg.URL
default:
alert.Type = "webhook"
alert.Settings["url"] = cfg.URL
}
result[entry.ID] = alert
}
return result
}
func convertKumaMonitor(m KumaMonitor, alertMap map[int]int) models.Site {
site := models.Site{
ID: m.ID,
Name: m.Name,
Description: m.Description,
Type: m.Type,
Interval: m.Interval,
Timeout: m.Timeout,
MaxRetries: m.MaxRetries,
Method: m.Method,
Hostname: m.Hostname,
Port: m.Port,
IgnoreTLS: m.IgnoreTLS,
ParentID: m.Parent,
}
if len(m.AcceptedCodes) > 0 {
site.AcceptedCodes = strings.Join(m.AcceptedCodes, ",")
}
site.DNSResolveType = m.DNSResolveType
site.DNSServer = m.DNSResolveServer
switch m.Type {
case "http":
site.URL = m.URL
site.CheckSSL = m.ExpiryNotif
case "ping":
if m.Hostname != "" {
site.Hostname = m.Hostname
}
case "port":
if m.Hostname != "" {
site.Hostname = m.Hostname
}
case "dns":
if m.Hostname != "" {
site.Hostname = m.Hostname
}
case "group":
// groups are organizational only
}
for nidStr := range m.NotificationIDs {
var nid int
fmt.Sscanf(nidStr, "%d", &nid)
if upkeepID, ok := alertMap[nid]; ok {
site.AlertID = upkeepID
break
}
}
return site
}