Automation components for scheduled and event-driven workflows:
Scheduler:
- scheduler.sh for cron-based workflow execution
- Logs workflow runs to ~/.claude/logs/workflows/
- Notifies dashboard on completion
Alertmanager Integration:
- webhook-receiver.sh for processing alerts
- Dashboard endpoint /api/webhooks/alertmanager
- Example alertmanager-config.yaml with routing rules
- Maps alerts to workflows (crashloop, node issues, resources)
New Incident Workflows:
- node-issue-response.yaml: Handle NotReady/unreachable nodes
- resource-pressure-response.yaml: Respond to memory/CPU overcommit
- argocd-sync-failure.yaml: Investigate and fix sync failures
Dashboard Updates:
- POST /api/webhooks/alertmanager endpoint
- POST /api/workflows/{name}/complete endpoint
- Alerts create pending actions for visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"embed"
|
|
"flag"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
"github.com/will/k8s-agent-dashboard/internal/api"
|
|
"github.com/will/k8s-agent-dashboard/internal/store"
|
|
)
|
|
|
|
//go:embed all:web
|
|
var webFS embed.FS
|
|
|
|
func main() {
|
|
port := flag.String("port", "8080", "Server port")
|
|
dataDir := flag.String("data", "/data", "Data directory for state")
|
|
flag.Parse()
|
|
|
|
// Initialize store
|
|
s, err := store.New(*dataDir)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize store: %v", err)
|
|
}
|
|
|
|
// Create router
|
|
r := chi.NewRouter()
|
|
|
|
// Middleware
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(middleware.Compress(5))
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Content-Type"},
|
|
ExposedHeaders: []string{"Link"},
|
|
AllowCredentials: false,
|
|
MaxAge: 300,
|
|
}))
|
|
|
|
// API routes
|
|
r.Route("/api", func(r chi.Router) {
|
|
r.Get("/health", api.HealthCheck)
|
|
r.Get("/status", api.GetClusterStatus(s))
|
|
r.Get("/pending", api.GetPendingActions(s))
|
|
r.Post("/pending/{id}/approve", api.ApproveAction(s))
|
|
r.Post("/pending/{id}/reject", api.RejectAction(s))
|
|
r.Get("/history", api.GetActionHistory(s))
|
|
r.Get("/workflows", api.GetWorkflows(s))
|
|
r.Post("/workflows/{name}/run", api.RunWorkflow(s))
|
|
r.Post("/workflows/{name}/complete", api.CompleteWorkflow(s))
|
|
|
|
// Webhook endpoints
|
|
r.Post("/webhooks/alertmanager", api.AlertmanagerWebhook(s))
|
|
})
|
|
|
|
// Static files
|
|
webContent, err := fs.Sub(webFS, "web")
|
|
if err != nil {
|
|
log.Fatalf("Failed to get web content: %v", err)
|
|
}
|
|
|
|
fileServer := http.FileServer(http.FS(webContent))
|
|
r.Handle("/*", fileServer)
|
|
|
|
// Start server
|
|
addr := ":" + *port
|
|
if envPort := os.Getenv("PORT"); envPort != "" {
|
|
addr = ":" + envPort
|
|
}
|
|
|
|
log.Printf("Starting server on %s", addr)
|
|
log.Printf("Data directory: %s", *dataDir)
|
|
|
|
if err := http.ListenAndServe(addr, r); err != nil {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}
|