Files
pulse/internal/api/availability_handlers.go
T
2026-05-06 14:08:03 +01:00

309 lines
10 KiB
Go

package api
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
)
const availabilityTargetsPathPrefix = "/api/availability-targets/"
type AvailabilityHandlers struct {
getPersistence func(ctx context.Context) *config.ConfigPersistence
getMonitor func(ctx context.Context) *monitoring.Monitor
}
type availabilityTargetResponse struct {
config.AvailabilityTarget
Status *monitoring.AvailabilityProbeStatus `json:"status,omitempty"`
}
type availabilityTestResponse struct {
Success bool `json:"success"`
LatencyMillis int64 `json:"latencyMillis"`
Error string `json:"error,omitempty"`
}
func NewAvailabilityHandlers(
getPersistence func(ctx context.Context) *config.ConfigPersistence,
getMonitor func(ctx context.Context) *monitoring.Monitor,
) *AvailabilityHandlers {
return &AvailabilityHandlers{
getPersistence: getPersistence,
getMonitor: getMonitor,
}
}
func (h *AvailabilityHandlers) HandleList(w http.ResponseWriter, r *http.Request) {
if mock.IsMockEnabled() {
writeJSON(w, http.StatusOK, mockAvailabilityTargetResponses())
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
targets, err := persistence.LoadAvailabilityTargets()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_load_failed", "Failed to load availability targets", map[string]string{"error": err.Error()})
return
}
statuses := map[string]monitoring.AvailabilityProbeStatus{}
if monitor := h.monitorForRequest(r.Context()); monitor != nil {
statuses = monitor.AvailabilityStatusSnapshot()
}
responses := make([]availabilityTargetResponse, 0, len(targets))
for _, target := range targets {
response := availabilityTargetResponse{AvailabilityTarget: config.NormalizeAvailabilityTarget(target)}
if status, ok := statuses[target.ID]; ok {
statusCopy := status
response.Status = &statusCopy
}
responses = append(responses, response)
}
writeJSON(w, http.StatusOK, responses)
}
func (h *AvailabilityHandlers) HandleAdd(w http.ResponseWriter, r *http.Request) {
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
target, ok := decodeAvailabilityTargetRequest(w, r, config.NewAvailabilityTarget())
if !ok {
return
}
target = config.NormalizeAvailabilityTarget(target)
if err := target.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
targets, err := persistence.LoadAvailabilityTargets()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_load_failed", "Failed to load availability targets", map[string]string{"error": err.Error()})
return
}
for _, existing := range targets {
if strings.TrimSpace(existing.ID) == target.ID {
writeErrorResponse(w, http.StatusConflict, "availability_duplicate", "Availability target ID already exists", nil)
return
}
}
targets = append(targets, target)
if err := persistence.SaveAvailabilityTargets(targets); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_save_failed", "Failed to save availability targets", map[string]string{"error": err.Error()})
return
}
h.refreshMonitor(r.Context())
writeJSON(w, http.StatusCreated, target)
}
func (h *AvailabilityHandlers) HandleUpdate(w http.ResponseWriter, r *http.Request) {
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
targetID, ok := availabilityTargetIDFromPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_target_id", "Availability target ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
targets, err := persistence.LoadAvailabilityTargets()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_load_failed", "Failed to load availability targets", map[string]string{"error": err.Error()})
return
}
index := -1
for i := range targets {
if strings.TrimSpace(targets[i].ID) == targetID {
index = i
break
}
}
if index < 0 {
writeErrorResponse(w, http.StatusNotFound, "availability_not_found", "Availability target not found", nil)
return
}
target, ok := decodeAvailabilityTargetRequest(w, r, targets[index])
if !ok {
return
}
target.ID = targetID
target = config.NormalizeAvailabilityTarget(target)
if err := target.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
targets[index] = target
if err := persistence.SaveAvailabilityTargets(targets); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_save_failed", "Failed to save availability targets", map[string]string{"error": err.Error()})
return
}
h.refreshMonitor(r.Context())
writeJSON(w, http.StatusOK, target)
}
func (h *AvailabilityHandlers) HandleDelete(w http.ResponseWriter, r *http.Request) {
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
targetID, ok := availabilityTargetIDFromPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_target_id", "Availability target ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
targets, err := persistence.LoadAvailabilityTargets()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_load_failed", "Failed to load availability targets", map[string]string{"error": err.Error()})
return
}
index := -1
for i := range targets {
if strings.TrimSpace(targets[i].ID) == targetID {
index = i
break
}
}
if index < 0 {
writeErrorResponse(w, http.StatusNotFound, "availability_not_found", "Availability target not found", nil)
return
}
targets = append(targets[:index], targets[index+1:]...)
if err := persistence.SaveAvailabilityTargets(targets); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_save_failed", "Failed to save availability targets", map[string]string{"error": err.Error()})
return
}
h.refreshMonitor(r.Context())
writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": targetID})
}
func (h *AvailabilityHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Request) {
target, ok := decodeAvailabilityTargetRequest(w, r, config.NewAvailabilityTarget())
if !ok {
return
}
h.testTarget(w, r, target)
}
func (h *AvailabilityHandlers) HandleTestSavedConnection(w http.ResponseWriter, r *http.Request) {
targetID, ok := availabilityTargetIDFromPath(strings.TrimSuffix(r.URL.Path, "/test"))
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_target_id", "Availability target ID is required", nil)
return
}
if mock.IsMockEnabled() {
if response, ok := mockAvailabilityTestResponse(targetID); ok {
writeJSON(w, http.StatusOK, response)
return
}
writeErrorResponse(w, http.StatusNotFound, "availability_not_found", "Availability target not found", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
targets, err := persistence.LoadAvailabilityTargets()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_load_failed", "Failed to load availability targets", map[string]string{"error": err.Error()})
return
}
for _, target := range targets {
if strings.TrimSpace(target.ID) == targetID {
h.testTarget(w, r, target)
return
}
}
writeErrorResponse(w, http.StatusNotFound, "availability_not_found", "Availability target not found", nil)
}
func (h *AvailabilityHandlers) testTarget(w http.ResponseWriter, r *http.Request, target config.AvailabilityTarget) {
target = config.NormalizeAvailabilityTarget(target)
if err := target.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
start := time.Now()
err := monitoring.ProbeAvailabilityTarget(r.Context(), target)
response := availabilityTestResponse{
Success: err == nil,
LatencyMillis: time.Since(start).Milliseconds(),
}
if err != nil {
response.Error = err.Error()
}
writeJSON(w, http.StatusOK, response)
}
func decodeAvailabilityTargetRequest(w http.ResponseWriter, r *http.Request, base config.AvailabilityTarget) (config.AvailabilityTarget, bool) {
r.Body = http.MaxBytesReader(w, r.Body, 16*1024)
defer r.Body.Close()
target := base
if err := json.NewDecoder(r.Body).Decode(&target); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid JSON body", nil)
return config.AvailabilityTarget{}, false
}
return target, true
}
func availabilityTargetIDFromPath(path string) (string, bool) {
id := strings.TrimPrefix(path, availabilityTargetsPathPrefix)
id = strings.Trim(id, "/")
if id == "" || strings.Contains(id, "/") {
return "", false
}
return id, true
}
func (h *AvailabilityHandlers) persistenceForRequest(w http.ResponseWriter, ctx context.Context) *config.ConfigPersistence {
if h == nil || h.getPersistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_unavailable", "Availability target persistence is unavailable", nil)
return nil
}
persistence := h.getPersistence(ctx)
if persistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "availability_unavailable", "Availability target persistence is unavailable", nil)
return nil
}
return persistence
}
func (h *AvailabilityHandlers) monitorForRequest(ctx context.Context) *monitoring.Monitor {
if h == nil || h.getMonitor == nil {
return nil
}
return h.getMonitor(ctx)
}
func (h *AvailabilityHandlers) refreshMonitor(ctx context.Context) {
if monitor := h.monitorForRequest(ctx); monitor != nil {
monitor.RefreshAvailabilityTargets()
}
}