Add per-node health checks with status indicators and scheduled probes.
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -231,6 +232,164 @@ func (app *App) nodesTestSSHHandler(writer http.ResponseWriter, request *http.Re
|
||||
})
|
||||
}
|
||||
|
||||
type patchNodeRequest struct {
|
||||
HealthCheckIntervalSeconds *int `json:"health_check_interval_seconds"`
|
||||
}
|
||||
|
||||
func (app *App) nodesPatchHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := app.authorizeNodesUpdate(request); err != nil {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := UserFromContext(request.Context())
|
||||
if !ok {
|
||||
writeJSON(writer, http.StatusUnauthorized, apiErrorResponse{Error: "authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
nodeID := strings.TrimSpace(request.PathValue("id"))
|
||||
if nodeID == "" {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "missing node id"})
|
||||
return
|
||||
}
|
||||
|
||||
var payload patchNodeRequest
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "invalid JSON body"})
|
||||
return
|
||||
}
|
||||
if payload.HealthCheckIntervalSeconds == nil {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{
|
||||
Error: "health_check_interval_seconds is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
if *payload.HealthCheckIntervalSeconds < 0 {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{
|
||||
Error: "health_check_interval_seconds must be >= 0",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
settingsPayload, err := loadSettingsOrDefault(app.ConfigDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
store, err := settings.LoadNodesOrEmpty(app.ConfigDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
nodeIndex := -1
|
||||
var node settings.Node
|
||||
for index, candidate := range store.Nodes {
|
||||
if candidate.ID != nodeID {
|
||||
continue
|
||||
}
|
||||
node = candidate
|
||||
nodeIndex = index
|
||||
break
|
||||
}
|
||||
if nodeIndex < 0 {
|
||||
writeJSON(writer, http.StatusNotFound, apiErrorResponse{Error: "node not found"})
|
||||
return
|
||||
}
|
||||
if !userCanAccessNode(user, settingsPayload.Groups, node) {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: "permission denied"})
|
||||
return
|
||||
}
|
||||
|
||||
node.HealthCheckIntervalSeconds = *payload.HealthCheckIntervalSeconds
|
||||
store.Nodes[nodeIndex] = node
|
||||
if err := settings.SaveNodes(app.ConfigDir, store); err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_ = appendNodeAudit(
|
||||
app.ConfigDir,
|
||||
settings.NodeAuditActionUpdate,
|
||||
user.Username,
|
||||
node,
|
||||
fmt.Sprintf("health_check_interval_seconds=%d", node.HealthCheckIntervalSeconds),
|
||||
)
|
||||
writeJSON(writer, http.StatusOK, nodeResponse{Node: node})
|
||||
}
|
||||
|
||||
func (app *App) nodesHealthCheckHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := app.authorizeNodesExec(request); err != nil {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if err := app.requireKey(); err != nil {
|
||||
writeJSON(writer, http.StatusServiceUnavailable, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if app.HealthChecker == nil {
|
||||
writeJSON(writer, http.StatusServiceUnavailable, apiErrorResponse{Error: "health checker unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := UserFromContext(request.Context())
|
||||
if !ok {
|
||||
writeJSON(writer, http.StatusUnauthorized, apiErrorResponse{Error: "authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
nodeID := strings.TrimSpace(request.PathValue("id"))
|
||||
if nodeID == "" {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "missing node id"})
|
||||
return
|
||||
}
|
||||
|
||||
settingsPayload, err := loadSettingsOrDefault(app.ConfigDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
store, err := settings.LoadNodesOrEmpty(app.ConfigDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var node settings.Node
|
||||
found := false
|
||||
for _, candidate := range store.Nodes {
|
||||
if candidate.ID != nodeID {
|
||||
continue
|
||||
}
|
||||
node = candidate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
writeJSON(writer, http.StatusNotFound, apiErrorResponse{Error: "node not found"})
|
||||
return
|
||||
}
|
||||
if !userCanAccessNode(user, settingsPayload.Groups, node) {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: "permission denied"})
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := app.HealthChecker.CheckNodeByID(nodeID, user.Username)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "already running") {
|
||||
writeJSON(writer, http.StatusConflict, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, nodeResponse{Node: updated})
|
||||
}
|
||||
|
||||
func (app *App) nodesCreateHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := app.authorizeNodesExec(request); err != nil {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
|
||||
|
||||
Reference in New Issue
Block a user