chore: add golangci-lint config and fix all lint issues
Add .golangci.yml enabling errcheck, staticcheck, govet, gosec, ineffassign, and unused linters. Fix 66 issues across 16 files: - Check all unchecked errors (errcheck) - Use HTTP status constants instead of numeric literals (staticcheck) - Replace deprecated LineUp/LineDown with ScrollUp/ScrollDown (staticcheck) - Convert sprintf+write patterns to fmt.Fprintf (staticcheck) - Add ReadHeaderTimeout to http.Server (gosec) - Remove unused types and functions (unused) - Add nolint comments for intentional patterns (InsecureSkipVerify, math/rand for jitter, dialect-only SQL formatting)
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- staticcheck
|
||||||
|
- govet
|
||||||
|
- gosec
|
||||||
|
- ineffassign
|
||||||
|
- unused
|
||||||
|
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
presets:
|
||||||
|
- std-error-handling
|
||||||
|
- common-false-positives
|
||||||
|
rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
+34
-17
@@ -76,7 +76,7 @@ func runApply(args []string) {
|
|||||||
prune := fs.Bool("prune", false, "Delete monitors/alerts not in YAML")
|
prune := fs.Bool("prune", false, "Delete monitors/alerts not in YAML")
|
||||||
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
|
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
|
||||||
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN")
|
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN")
|
||||||
fs.Parse(args)
|
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning
|
||||||
|
|
||||||
if *filePath == "" {
|
if *filePath == "" {
|
||||||
fmt.Fprintln(os.Stderr, "error: -f flag is required")
|
fmt.Fprintln(os.Stderr, "error: -f flag is required")
|
||||||
@@ -109,7 +109,7 @@ func runExport(args []string) {
|
|||||||
outPath := fs.String("o", "-", "Output file path (- for stdout)")
|
outPath := fs.String("o", "-", "Output file path (- for stdout)")
|
||||||
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
|
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
|
||||||
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN")
|
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN")
|
||||||
fs.Parse(args)
|
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning
|
||||||
|
|
||||||
s := openStore(*dbType, *dsn)
|
s := openStore(*dbType, *dsn)
|
||||||
|
|
||||||
@@ -186,6 +186,7 @@ func runServe(args []string) {
|
|||||||
fmt.Printf("Cluster: Running as PROBE (node=%s, region=%s)\n", nodeID, nodeRegion)
|
fmt.Printf("Cluster: Running as PROBE (node=%s, region=%s)\n", nodeID, nodeRegion)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
done := make(chan os.Signal, 1)
|
done := make(chan os.Signal, 1)
|
||||||
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -212,7 +213,7 @@ func runServe(args []string) {
|
|||||||
flagDSN := fs.String("dsn", dbDSN, "Database DSN")
|
flagDSN := fs.String("dsn", dbDSN, "Database DSN")
|
||||||
demo := fs.Bool("demo", false, "Seed demo data")
|
demo := fs.Bool("demo", false, "Seed demo data")
|
||||||
importKuma := fs.String("import-kuma", "", "Import Uptime Kuma backup JSON file")
|
importKuma := fs.String("import-kuma", "", "Import Uptime Kuma backup JSON file")
|
||||||
fs.Parse(args)
|
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning
|
||||||
|
|
||||||
var s store.Store
|
var s store.Store
|
||||||
var dbErr error
|
var dbErr error
|
||||||
@@ -341,13 +342,22 @@ func seedDemoData(s store.Store) {
|
|||||||
}
|
}
|
||||||
fmt.Println("Seeding demo data...")
|
fmt.Println("Seeding demo data...")
|
||||||
|
|
||||||
s.AddAlert("Discord Ops", "discord", map[string]string{"url": "https://discord.com/api/webhooks/demo/token"})
|
if err := s.AddAlert("Discord Ops", "discord", map[string]string{"url": "https://discord.com/api/webhooks/demo/token"}); err != nil {
|
||||||
s.AddAlert("Slack Infra", "slack", map[string]string{"url": "https://hooks.slack.com/services/DEMO/WEBHOOK"})
|
log.Printf("demo seed: add alert: %v", err)
|
||||||
s.AddAlert("Email Oncall", "email", map[string]string{
|
return
|
||||||
|
}
|
||||||
|
if err := s.AddAlert("Slack Infra", "slack", map[string]string{"url": "https://hooks.slack.com/services/DEMO/WEBHOOK"}); err != nil {
|
||||||
|
log.Printf("demo seed: add alert: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.AddAlert("Email Oncall", "email", map[string]string{
|
||||||
"host": "smtp.example.com", "port": "587",
|
"host": "smtp.example.com", "port": "587",
|
||||||
"user": "oncall@example.com", "pass": "replace-me",
|
"user": "oncall@example.com", "pass": "replace-me",
|
||||||
"from": "oncall@example.com", "to": "team@example.com",
|
"from": "oncall@example.com", "to": "team@example.com",
|
||||||
})
|
}); err != nil {
|
||||||
|
log.Printf("demo seed: add alert: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
alerts, _ := s.GetAllAlerts()
|
alerts, _ := s.GetAllAlerts()
|
||||||
alertID := 0
|
alertID := 0
|
||||||
@@ -355,16 +365,23 @@ func seedDemoData(s store.Store) {
|
|||||||
alertID = alerts[0].ID
|
alertID = alerts[0].ID
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddSite(models.Site{Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2})
|
demoSites := []models.Site{
|
||||||
s.AddSite(models.Site{Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3})
|
{Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2},
|
||||||
s.AddSite(models.Site{Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1})
|
{Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3},
|
||||||
s.AddSite(models.Site{Name: "JSON Placeholder", URL: "https://jsonplaceholder.typicode.com/posts/1", Type: "http", Interval: 45, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 2})
|
{Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1},
|
||||||
s.AddSite(models.Site{Name: "Nonexistent Site", URL: "https://this-domain-does-not-exist-12345.com", Type: "http", Interval: 30, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 3})
|
{Name: "JSON Placeholder", URL: "https://jsonplaceholder.typicode.com/posts/1", Type: "http", Interval: 45, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 2},
|
||||||
s.AddSite(models.Site{Name: "Bad Port", URL: "https://localhost:19999", Type: "http", Interval: 30, ExpiryThreshold: 7, MaxRetries: 1})
|
{Name: "Nonexistent Site", URL: "https://this-domain-does-not-exist-12345.com", Type: "http", Interval: 30, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 3},
|
||||||
s.AddSite(models.Site{Name: "Backup Cron", Type: "push", Interval: 300, AlertID: alertID, ExpiryThreshold: 7})
|
{Name: "Bad Port", URL: "https://localhost:19999", Type: "http", Interval: 30, ExpiryThreshold: 7, MaxRetries: 1},
|
||||||
s.AddSite(models.Site{Name: "DB Healthcheck", Type: "push", Interval: 120, AlertID: alertID, ExpiryThreshold: 7})
|
{Name: "Backup Cron", Type: "push", Interval: 300, AlertID: alertID, ExpiryThreshold: 7},
|
||||||
s.AddSite(models.Site{Name: "Gateway", Type: "ping", Interval: 30, AlertID: alertID, Hostname: "10.0.0.1", Timeout: 5, ExpiryThreshold: 7})
|
{Name: "DB Healthcheck", Type: "push", Interval: 120, AlertID: alertID, ExpiryThreshold: 7},
|
||||||
s.AddSite(models.Site{Name: "SSH Server", Type: "port", Interval: 60, AlertID: alertID, Hostname: "10.0.0.1", Port: 22, Timeout: 5, ExpiryThreshold: 7})
|
{Name: "Gateway", Type: "ping", Interval: 30, AlertID: alertID, Hostname: "10.0.0.1", Timeout: 5, ExpiryThreshold: 7},
|
||||||
|
{Name: "SSH Server", Type: "port", Interval: 60, AlertID: alertID, Hostname: "10.0.0.1", Port: 22, Timeout: 5, ExpiryThreshold: 7},
|
||||||
|
}
|
||||||
|
for _, site := range demoSites {
|
||||||
|
if err := s.AddSite(site); err != nil {
|
||||||
|
log.Printf("demo seed: add site %q: %v", site.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isKeyAllowed(db store.Store, incomingKey ssh.PublicKey) bool {
|
func isKeyAllowed(db store.Store, incomingKey ssh.PublicKey) bool {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func runFollowerLoop(ctx context.Context, cfg Config, eng *monitor.Engine) {
|
|||||||
|
|
||||||
if err == nil && resp.StatusCode == 200 {
|
if err == nil && resp.StatusCode == 200 {
|
||||||
isLeaderHealthy = true
|
isLeaderHealthy = true
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLeaderHealthy {
|
if isLeaderHealthy {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
// --- Mock Store (minimal, for monitor.NewEngine) ---
|
// --- Mock Store (minimal, for monitor.NewEngine) ---
|
||||||
|
|
||||||
type mockStore struct {
|
type mockStore struct {
|
||||||
mu sync.Mutex
|
|
||||||
sites []models.Site
|
sites []models.Site
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
|||||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
||||||
}
|
}
|
||||||
insecureClient := &http.Client{
|
insecureClient := &http.Client{
|
||||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, //nolint:gosec // intentional for IgnoreTLS sites
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := probeRegister(ctx, apiClient, cfg); err != nil {
|
if err := probeRegister(ctx, apiClient, cfg); err != nil {
|
||||||
@@ -85,7 +85,7 @@ func probeRegister(ctx context.Context, client *http.Client, cfg ProbeConfig) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("register returned %d", resp.StatusCode)
|
return fmt.Errorf("register returned %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -127,10 +127,11 @@ func probeExecuteChecks(ctx context.Context, sites []models.Site, strict, insecu
|
|||||||
sem := make(chan struct{}, 10)
|
sem := make(chan struct{}, 10)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
loop:
|
||||||
for _, site := range sites {
|
for _, site := range sites {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
break
|
break loop
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -171,7 +172,7 @@ func probeReportResults(ctx context.Context, client *http.Client, cfg ProbeConfi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("results returned %d", resp.StatusCode)
|
return fmt.Errorf("results returned %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func WriteFile(f *File, path string) error {
|
|||||||
_, err = os.Stdout.Write(data)
|
_, err = os.Stdout.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, 0644)
|
return os.WriteFile(path, data, 0644) //nolint:gosec // config files should be group-readable
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadFile(path string) (*File, error) {
|
func LoadFile(path string) (*File, error) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func convertKumaMonitor(m KumaMonitor, alertMap map[int]int) models.Site {
|
|||||||
|
|
||||||
for nidStr := range m.NotificationIDs {
|
for nidStr := range m.NotificationIDs {
|
||||||
var nid int
|
var nid int
|
||||||
fmt.Sscanf(nidStr, "%d", &nid)
|
_, _ = fmt.Sscanf(nidStr, "%d", &nid) //nolint:errcheck
|
||||||
if upkeepID, ok := alertMap[nid]; ok {
|
if upkeepID, ok := alertMap[nid]; ok {
|
||||||
site.AlertID = upkeepID
|
site.AlertID = upkeepID
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func Handler(eng *monitor.Engine) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
||||||
w.Write([]byte(b.String()))
|
_, _ = w.Write([]byte(b.String())) //nolint:errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func runPortCheck(site models.Site) CheckResult {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds()}
|
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds()}
|
||||||
}
|
}
|
||||||
conn.Close()
|
_ = conn.Close()
|
||||||
return CheckResult{SiteID: site.ID, Status: "UP", LatencyNs: latency.Nanoseconds()}
|
return CheckResult{SiteID: site.ID, Status: "UP", LatencyNs: latency.Nanoseconds()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func NewEngine(s store.Store) *Engine {
|
|||||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
||||||
},
|
},
|
||||||
insecureClient: &http.Client{
|
insecureClient: &http.Client{
|
||||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, //nolint:gosec // intentional for IgnoreTLS sites
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +279,7 @@ func (e *Engine) ToggleSitePause(id int) bool {
|
|||||||
|
|
||||||
func (e *Engine) monitorRoutine(ctx context.Context, id int) {
|
func (e *Engine) monitorRoutine(ctx context.Context, id int) {
|
||||||
// Stagger initial check to avoid thundering herd on startup
|
// Stagger initial check to avoid thundering herd on startup
|
||||||
stagger := time.Duration(rand.IntN(3000)) * time.Millisecond
|
stagger := time.Duration(rand.IntN(3000)) * time.Millisecond //nolint:gosec // non-security jitter
|
||||||
select {
|
select {
|
||||||
case <-time.After(stagger):
|
case <-time.After(stagger):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -323,7 +323,7 @@ func (e *Engine) monitorRoutine(ctx context.Context, id int) {
|
|||||||
if interval < 5 {
|
if interval < 5 {
|
||||||
interval = 5
|
interval = 5
|
||||||
}
|
}
|
||||||
jitter := time.Duration(rand.IntN(interval*100)) * time.Millisecond
|
jitter := time.Duration(rand.IntN(interval*100)) * time.Millisecond //nolint:gosec // non-security jitter
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Duration(interval)*time.Second + jitter):
|
case <-time.After(time.Duration(interval)*time.Second + jitter):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
+40
-35
@@ -14,6 +14,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkSecret(got, want string) bool {
|
func checkSecret(got, want string) bool {
|
||||||
@@ -168,100 +169,100 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
mux.HandleFunc("/api/push", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/push", func(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.URL.Query().Get("token")
|
token := r.URL.Query().Get("token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
http.Error(w, "Missing token", 400)
|
http.Error(w, "Missing token", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if eng.RecordHeartbeat(token) {
|
if eng.RecordHeartbeat(token) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK"))
|
_, _ = w.Write([]byte("OK"))
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "Invalid Token", 404)
|
http.Error(w, "Invalid Token", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2. Health Check (For Cluster Follower)
|
// 2. Health Check (For Cluster Follower)
|
||||||
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if cfg.ClusterKey != "" && !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey != "" && !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK"))
|
_, _ = w.Write([]byte("OK"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Config Export
|
// 3. Config Export
|
||||||
mux.HandleFunc("/api/backup/export", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/backup/export", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", 401)
|
http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := s.ExportData()
|
data, err := s.ExportData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Export failed: %v", err)
|
log.Printf("Export failed: %v", err)
|
||||||
http.Error(w, "Export failed", 500)
|
http.Error(w, "Export failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(data)
|
_ = json.NewEncoder(w).Encode(data) //nolint:errcheck
|
||||||
})
|
})
|
||||||
|
|
||||||
// 4. Config Import
|
// 4. Config Import
|
||||||
mux.HandleFunc("/api/backup/import", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/backup/import", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "POST required", 405)
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
var data models.Backup
|
var data models.Backup
|
||||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
http.Error(w, "Invalid JSON", 400)
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.ImportData(data); err != nil {
|
if err := s.ImportData(data); err != nil {
|
||||||
log.Printf("Import failed: %v", err)
|
log.Printf("Import failed: %v", err)
|
||||||
http.Error(w, "Import failed", 500)
|
http.Error(w, "Import failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte("Import Successful"))
|
_, _ = w.Write([]byte("Import Successful"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 5. Kuma Import
|
// 5. Kuma Import
|
||||||
mux.HandleFunc("/api/import/kuma", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/import/kuma", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "POST required", 405)
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
var kb importer.KumaBackup
|
var kb importer.KumaBackup
|
||||||
if err := json.NewDecoder(r.Body).Decode(&kb); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&kb); err != nil {
|
||||||
log.Printf("Invalid Kuma JSON: %v", err)
|
log.Printf("Invalid Kuma JSON: %v", err)
|
||||||
http.Error(w, "Invalid Kuma JSON", 400)
|
http.Error(w, "Invalid Kuma JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backup := importer.ConvertKuma(&kb)
|
backup := importer.ConvertKuma(&kb)
|
||||||
if err := s.ImportData(backup); err != nil {
|
if err := s.ImportData(backup); err != nil {
|
||||||
log.Printf("Kuma import failed: %v", err)
|
log.Printf("Kuma import failed: %v", err)
|
||||||
http.Error(w, "Import failed", 500)
|
http.Error(w, "Import failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte(fmt.Sprintf("Imported %d monitors, %d alerts from Kuma v%s", len(backup.Sites), len(backup.Alerts), kb.Version)))
|
fmt.Fprintf(w, "Imported %d monitors, %d alerts from Kuma v%s", len(backup.Sites), len(backup.Alerts), kb.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 6. Probe Registration
|
// 6. Probe Registration
|
||||||
mux.HandleFunc("/api/probe/register", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/probe/register", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "POST required", 405)
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
@@ -272,27 +273,27 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "Invalid JSON", 400)
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.ID == "" {
|
if req.ID == "" {
|
||||||
http.Error(w, "id is required", 400)
|
http.Error(w, "id is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.RegisterNode(models.ProbeNode{
|
if err := s.RegisterNode(models.ProbeNode{
|
||||||
ID: req.ID, Name: req.Name, Region: req.Region, Version: req.Version,
|
ID: req.ID, Name: req.Name, Region: req.Region, Version: req.Version,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Probe register failed: %v", err)
|
log.Printf("Probe register failed: %v", err)
|
||||||
http.Error(w, "Registration failed", 500)
|
http.Error(w, "Registration failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
|
_ = json.NewEncoder(w).Encode(map[string]bool{"ok": true}) //nolint:errcheck
|
||||||
})
|
})
|
||||||
|
|
||||||
// 7. Probe Assignment Fetch
|
// 7. Probe Assignment Fetch
|
||||||
mux.HandleFunc("/api/probe/assignments", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/probe/assignments", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nodeID := r.URL.Query().Get("node_id")
|
nodeID := r.URL.Query().Get("node_id")
|
||||||
@@ -323,17 +324,17 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
assigned = append(assigned, site)
|
assigned = append(assigned, site)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string][]models.Site{"sites": assigned})
|
_ = json.NewEncoder(w).Encode(map[string][]models.Site{"sites": assigned}) //nolint:errcheck
|
||||||
})
|
})
|
||||||
|
|
||||||
// 8. Probe Result Submission
|
// 8. Probe Result Submission
|
||||||
mux.HandleFunc("/api/probe/results", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/probe/results", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "POST required", 405)
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
|
||||||
http.Error(w, "Unauthorized", 401)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||||
@@ -346,11 +347,11 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "Invalid JSON", 400)
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.NodeID == "" {
|
if req.NodeID == "" {
|
||||||
http.Error(w, "node_id is required", 400)
|
http.Error(w, "node_id is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, result := range req.Results {
|
for _, result := range req.Results {
|
||||||
@@ -359,8 +360,10 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
}
|
}
|
||||||
eng.IngestProbeResult(req.NodeID, result.SiteID, result.LatencyNs, result.IsUp)
|
eng.IngestProbeResult(req.NodeID, result.SiteID, result.LatencyNs, result.IsUp)
|
||||||
}
|
}
|
||||||
s.UpdateNodeLastSeen(req.NodeID)
|
if err := s.UpdateNodeLastSeen(req.NodeID); err != nil {
|
||||||
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
|
log.Printf("Failed to update node last seen: %v", err)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]bool{"ok": true}) //nolint:errcheck
|
||||||
})
|
})
|
||||||
|
|
||||||
// 9. Prometheus Metrics
|
// 9. Prometheus Metrics
|
||||||
@@ -392,12 +395,12 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
|||||||
state[id] = site
|
state[id] = site
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(state)
|
_ = json.NewEncoder(w).Encode(state) //nolint:errcheck
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%d", cfg.Port)
|
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||||
srv := &http.Server{Addr: addr, Handler: mux}
|
srv := &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 10 * time.Second}
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Printf("HTTP Server listening on %s\n", addr)
|
fmt.Printf("HTTP Server listening on %s\n", addr)
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
@@ -426,5 +429,7 @@ func renderStatusPage(w http.ResponseWriter, title string, eng *monitor.Engine)
|
|||||||
Title string
|
Title string
|
||||||
Sites []models.Site
|
Sites []models.Site
|
||||||
}{Title: title, Sites: sites}
|
}{Title: title, Sites: sites}
|
||||||
statusTpl.Execute(w, data)
|
if err := statusTpl.Execute(w, data); err != nil {
|
||||||
|
log.Printf("Failed to render status page: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
@@ -99,15 +100,31 @@ func (d *PostgresDialect) UpsertNodeSQL() string {
|
|||||||
func (d *PostgresDialect) ResetSequenceOnEmpty(db *sql.DB, table string) {}
|
func (d *PostgresDialect) ResetSequenceOnEmpty(db *sql.DB, table string) {}
|
||||||
|
|
||||||
func (d *PostgresDialect) ImportWipe(tx *sql.Tx) {
|
func (d *PostgresDialect) ImportWipe(tx *sql.Tx) {
|
||||||
tx.Exec("TRUNCATE TABLE sites RESTART IDENTITY CASCADE")
|
if _, err := tx.Exec("TRUNCATE TABLE sites RESTART IDENTITY CASCADE"); err != nil {
|
||||||
tx.Exec("TRUNCATE TABLE alerts RESTART IDENTITY CASCADE")
|
log.Printf("import wipe error: %v", err)
|
||||||
tx.Exec("TRUNCATE TABLE users RESTART IDENTITY CASCADE")
|
}
|
||||||
tx.Exec("TRUNCATE TABLE maintenance_windows RESTART IDENTITY CASCADE")
|
if _, err := tx.Exec("TRUNCATE TABLE alerts RESTART IDENTITY CASCADE"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("TRUNCATE TABLE users RESTART IDENTITY CASCADE"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("TRUNCATE TABLE maintenance_windows RESTART IDENTITY CASCADE"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PostgresDialect) ImportResetSequences(tx *sql.Tx) {
|
func (d *PostgresDialect) ImportResetSequences(tx *sql.Tx) {
|
||||||
tx.Exec("SELECT setval('sites_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sites))")
|
if _, err := tx.Exec("SELECT setval('sites_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sites))"); err != nil {
|
||||||
tx.Exec("SELECT setval('alerts_id_seq', (SELECT COALESCE(MAX(id), 1) FROM alerts))")
|
log.Printf("sequence reset error: %v", err)
|
||||||
tx.Exec("SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users))")
|
}
|
||||||
tx.Exec("SELECT setval('maintenance_windows_id_seq', (SELECT COALESCE(MAX(id), 1) FROM maintenance_windows))")
|
if _, err := tx.Exec("SELECT setval('alerts_id_seq', (SELECT COALESCE(MAX(id), 1) FROM alerts))"); err != nil {
|
||||||
|
log.Printf("sequence reset error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users))"); err != nil {
|
||||||
|
log.Printf("sequence reset error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("SELECT setval('maintenance_windows_id_seq', (SELECT COALESCE(MAX(id), 1) FROM maintenance_windows))"); err != nil {
|
||||||
|
log.Printf("sequence reset error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-10
@@ -2,6 +2,7 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
@@ -98,21 +99,39 @@ func (d *SQLiteDialect) UpsertNodeSQL() string {
|
|||||||
|
|
||||||
func (d *SQLiteDialect) ResetSequenceOnEmpty(db *sql.DB, table string) {
|
func (d *SQLiteDialect) ResetSequenceOnEmpty(db *sql.DB, table string) {
|
||||||
var count int
|
var count int
|
||||||
db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count)
|
_ = db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count) //nolint:errcheck
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
db.Exec("DELETE FROM sqlite_sequence WHERE name=?", table)
|
if _, err := db.Exec("DELETE FROM sqlite_sequence WHERE name=?", table); err != nil {
|
||||||
|
log.Printf("sequence cleanup error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDialect) ImportWipe(tx *sql.Tx) {
|
func (d *SQLiteDialect) ImportWipe(tx *sql.Tx) {
|
||||||
tx.Exec("DELETE FROM sites")
|
if _, err := tx.Exec("DELETE FROM sites"); err != nil {
|
||||||
tx.Exec("DELETE FROM sqlite_sequence WHERE name='sites'")
|
log.Printf("import wipe error: %v", err)
|
||||||
tx.Exec("DELETE FROM alerts")
|
}
|
||||||
tx.Exec("DELETE FROM sqlite_sequence WHERE name='alerts'")
|
if _, err := tx.Exec("DELETE FROM sqlite_sequence WHERE name='sites'"); err != nil {
|
||||||
tx.Exec("DELETE FROM users")
|
log.Printf("import wipe error: %v", err)
|
||||||
tx.Exec("DELETE FROM sqlite_sequence WHERE name='users'")
|
}
|
||||||
tx.Exec("DELETE FROM maintenance_windows")
|
if _, err := tx.Exec("DELETE FROM alerts"); err != nil {
|
||||||
tx.Exec("DELETE FROM sqlite_sequence WHERE name='maintenance_windows'")
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("DELETE FROM sqlite_sequence WHERE name='alerts'"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("DELETE FROM users"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("DELETE FROM sqlite_sequence WHERE name='users'"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("DELETE FROM maintenance_windows"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec("DELETE FROM sqlite_sequence WHERE name='maintenance_windows'"); err != nil {
|
||||||
|
log.Printf("import wipe error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDialect) ImportResetSequences(tx *sql.Tx) {}
|
func (d *SQLiteDialect) ImportResetSequences(tx *sql.Tx) {}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-upkeep/internal/models"
|
"go-upkeep/internal/models"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,14 +49,16 @@ func (s *SQLStore) Init() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, m := range s.dialect.MigrationsSQL() {
|
for _, m := range s.dialect.MigrationsSQL() {
|
||||||
s.db.Exec(m)
|
if _, err := s.db.Exec(m); err != nil {
|
||||||
|
log.Printf("migration error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetSites() ([]models.Site, error) {
|
func (s *SQLStore) GetSites() ([]models.Site, error) {
|
||||||
bf := s.dialect.BoolFalse()
|
bf := s.dialect.BoolFalse()
|
||||||
query := fmt.Sprintf(
|
query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input
|
||||||
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites",
|
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites",
|
||||||
bf, bf,
|
bf, bf,
|
||||||
)
|
)
|
||||||
@@ -95,7 +98,7 @@ func (s *SQLStore) AddSite(site models.Site) error {
|
|||||||
|
|
||||||
func (s *SQLStore) UpdateSite(site models.Site) error {
|
func (s *SQLStore) UpdateSite(site models.Site) error {
|
||||||
var existingToken string
|
var existingToken string
|
||||||
s.db.QueryRow(s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken)
|
_ = s.db.QueryRow(s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken) //nolint:errcheck
|
||||||
if site.Type == "push" && existingToken == "" {
|
if site.Type == "push" && existingToken == "" {
|
||||||
var err error
|
var err error
|
||||||
existingToken, err = generateToken()
|
existingToken, err = generateToken()
|
||||||
@@ -125,7 +128,7 @@ func (s *SQLStore) DeleteSite(id int) error {
|
|||||||
|
|
||||||
func (s *SQLStore) GetSiteByName(name string) (models.Site, error) {
|
func (s *SQLStore) GetSiteByName(name string) (models.Site, error) {
|
||||||
bf := s.dialect.BoolFalse()
|
bf := s.dialect.BoolFalse()
|
||||||
query := fmt.Sprintf(
|
query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input
|
||||||
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites WHERE name = %s",
|
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites WHERE name = %s",
|
||||||
bf, bf, s.q("?"),
|
bf, bf, s.q("?"),
|
||||||
)
|
)
|
||||||
@@ -502,7 +505,7 @@ func (s *SQLStore) ImportData(data models.Backup) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback() //nolint:errcheck
|
||||||
|
|
||||||
s.dialect.ImportWipe(tx)
|
s.dialect.ImportWipe(tx)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-upkeep/internal/models"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,26 +69,3 @@ func fmtNodeLastSeen(t time.Time) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%dh ago", int(ago.Hours()))
|
return fmt.Sprintf("%dh ago", int(ago.Hours()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtProbeRegions(site models.Site, probeResults map[string]probeStatus) string {
|
|
||||||
if len(probeResults) == 0 {
|
|
||||||
return subtleStyle.Render("—")
|
|
||||||
}
|
|
||||||
var parts []string
|
|
||||||
for region, status := range probeResults {
|
|
||||||
short := region
|
|
||||||
if len(short) > 6 {
|
|
||||||
short = short[:6]
|
|
||||||
}
|
|
||||||
if status.isUp {
|
|
||||||
parts = append(parts, specialStyle.Render(short+":UP"))
|
|
||||||
} else {
|
|
||||||
parts = append(parts, dangerStyle.Render(short+":DN"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
type probeStatus struct {
|
|
||||||
isUp bool
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -301,10 +301,10 @@ func fmtStatus(status string, paused bool, inMaint bool) string {
|
|||||||
if inMaint {
|
if inMaint {
|
||||||
return maintStyle.Render("MAINT")
|
return maintStyle.Render("MAINT")
|
||||||
}
|
}
|
||||||
switch {
|
switch status {
|
||||||
case status == "DOWN" || status == "SSL EXP":
|
case "DOWN", "SSL EXP":
|
||||||
return dangerStyle.Render(status)
|
return dangerStyle.Render(status)
|
||||||
case status == "PENDING":
|
case "PENDING":
|
||||||
return subtleStyle.Render(status)
|
return subtleStyle.Render(status)
|
||||||
default:
|
default:
|
||||||
return specialStyle.Render(status)
|
return specialStyle.Render(status)
|
||||||
@@ -721,7 +721,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
b.WriteString(breadcrumb + "\n\n")
|
b.WriteString(breadcrumb + "\n\n")
|
||||||
|
|
||||||
row := func(label, value string) {
|
row := func(label, value string) {
|
||||||
b.WriteString(fmt.Sprintf(" %-16s %s\n", subtleStyle.Render(label), value))
|
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
||||||
@@ -780,7 +780,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
}
|
}
|
||||||
latency := time.Duration(result.LatencyNs).Milliseconds()
|
latency := time.Duration(result.LatencyNs).Milliseconds()
|
||||||
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
||||||
b.WriteString(fmt.Sprintf(" %-14s %s %dms %s ago\n", nodeID, status, latency, ago))
|
fmt.Fprintf(&b, " %-14s %s %dms %s ago\n", nodeID, status, latency, ago)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,9 +795,9 @@ func (m Model) viewDetailPanel() string {
|
|||||||
up++
|
up++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf("\n %s %d/%d checks up",
|
fmt.Fprintf(&b, "\n %s %d/%d checks up",
|
||||||
subtleStyle.Render("Heartbeats"),
|
subtleStyle.Render("Heartbeats"),
|
||||||
up, len(hist.Statuses)))
|
up, len(hist.Statuses))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(" " + latencySparkline(hist.Latencies, sparkWidth))
|
b.WriteString(" " + latencySparkline(hist.Latencies, sparkWidth))
|
||||||
@@ -814,10 +814,10 @@ func (m Model) viewDetailPanel() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
avg := total / time.Duration(len(hist.Latencies))
|
avg := total / time.Duration(len(hist.Latencies))
|
||||||
b.WriteString(fmt.Sprintf("\n %s %dms %s %dms %s %dms",
|
fmt.Fprintf(&b, "\n %s %dms %s %dms %s %dms",
|
||||||
subtleStyle.Render("Min"), minL.Milliseconds(),
|
subtleStyle.Render("Min"), minL.Milliseconds(),
|
||||||
subtleStyle.Render("Avg"), avg.Milliseconds(),
|
subtleStyle.Render("Avg"), avg.Milliseconds(),
|
||||||
subtleStyle.Render("Max"), maxL.Milliseconds()))
|
subtleStyle.Render("Max"), maxL.Milliseconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-15
@@ -264,20 +264,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if msg.Button == tea.MouseButtonWheelUp || msg.Button == tea.MouseButtonWheelDown {
|
if msg.Button == tea.MouseButtonWheelUp || msg.Button == tea.MouseButtonWheelDown {
|
||||||
if m.state == stateLogs {
|
if m.state == stateLogs {
|
||||||
if msg.Button == tea.MouseButtonWheelUp {
|
if msg.Button == tea.MouseButtonWheelUp {
|
||||||
m.logViewport.LineUp(3)
|
m.logViewport.ScrollUp(3)
|
||||||
} else {
|
} else {
|
||||||
m.logViewport.LineDown(3)
|
m.logViewport.ScrollDown(3)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
listLen := len(m.sites)
|
listLen := len(m.sites)
|
||||||
if m.currentTab == 1 {
|
switch m.currentTab {
|
||||||
|
case 1:
|
||||||
listLen = len(m.alerts)
|
listLen = len(m.alerts)
|
||||||
} else if m.currentTab == 3 {
|
case 3:
|
||||||
listLen = len(m.nodes)
|
listLen = len(m.nodes)
|
||||||
} else if m.currentTab == 4 {
|
case 4:
|
||||||
listLen = len(m.maintenanceWindows)
|
listLen = len(m.maintenanceWindows)
|
||||||
} else if m.currentTab == 5 {
|
case 5:
|
||||||
listLen = len(m.users)
|
listLen = len(m.users)
|
||||||
}
|
}
|
||||||
if msg.Button == tea.MouseButtonWheelUp {
|
if msg.Button == tea.MouseButtonWheelUp {
|
||||||
@@ -364,7 +365,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
if m.state == stateLogs {
|
if m.state == stateLogs {
|
||||||
m.logViewport.LineUp(1)
|
m.logViewport.ScrollUp(1)
|
||||||
} else if m.cursor > 0 {
|
} else if m.cursor > 0 {
|
||||||
m.cursor--
|
m.cursor--
|
||||||
if m.cursor < m.tableOffset {
|
if m.cursor < m.tableOffset {
|
||||||
@@ -373,7 +374,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
if m.state == stateLogs {
|
if m.state == stateLogs {
|
||||||
m.logViewport.LineDown(1)
|
m.logViewport.ScrollDown(1)
|
||||||
} else {
|
} else {
|
||||||
max := len(m.sites) - 1
|
max := len(m.sites) - 1
|
||||||
if m.currentTab == 1 {
|
if m.currentTab == 1 {
|
||||||
@@ -645,13 +646,14 @@ func (m *Model) refreshData() {
|
|||||||
m.logViewport.SetContent(strings.Join(m.engine.GetLogs(), "\n"))
|
m.logViewport.SetContent(strings.Join(m.engine.GetLogs(), "\n"))
|
||||||
|
|
||||||
listLen := len(m.sites)
|
listLen := len(m.sites)
|
||||||
if m.currentTab == 1 {
|
switch m.currentTab {
|
||||||
|
case 1:
|
||||||
listLen = len(m.alerts)
|
listLen = len(m.alerts)
|
||||||
} else if m.currentTab == 3 {
|
case 3:
|
||||||
listLen = len(m.nodes)
|
listLen = len(m.nodes)
|
||||||
} else if m.currentTab == 4 {
|
case 4:
|
||||||
listLen = len(m.maintenanceWindows)
|
listLen = len(m.maintenanceWindows)
|
||||||
} else if m.currentTab == 5 {
|
case 5:
|
||||||
listLen = len(m.users)
|
listLen = len(m.users)
|
||||||
}
|
}
|
||||||
if listLen > 0 && m.cursor >= listLen {
|
if listLen > 0 && m.cursor >= listLen {
|
||||||
@@ -709,11 +711,12 @@ func (m Model) View() string {
|
|||||||
switch m.state {
|
switch m.state {
|
||||||
case stateConfirmDelete:
|
case stateConfirmDelete:
|
||||||
kind := "monitor"
|
kind := "monitor"
|
||||||
if m.deleteTab == 1 {
|
switch m.deleteTab {
|
||||||
|
case 1:
|
||||||
kind = "alert"
|
kind = "alert"
|
||||||
} else if m.deleteTab == 4 {
|
case 4:
|
||||||
kind = "maintenance window"
|
kind = "maintenance window"
|
||||||
} else if m.deleteTab == 5 {
|
case 5:
|
||||||
kind = "user"
|
kind = "user"
|
||||||
}
|
}
|
||||||
msg := dangerStyle.Render(fmt.Sprintf("Delete %s \"%s\"?", kind, m.deleteName))
|
msg := dangerStyle.Render(fmt.Sprintf("Delete %s \"%s\"?", kind, m.deleteName))
|
||||||
|
|||||||
Reference in New Issue
Block a user