Healthcheck updates, UI to remove wierd bar

This commit is contained in:
2026-07-19 23:11:00 +02:00
parent aac9e82766
commit e047499fed
6 changed files with 402 additions and 2 deletions
+9
View File
@@ -317,6 +317,15 @@ func (app *App) nodesPatchHandler(writer http.ResponseWriter, request *http.Requ
node,
fmt.Sprintf("health_check_interval_seconds=%d", node.HealthCheckIntervalSeconds),
)
if node.HealthCheckIntervalSeconds > 0 && app.HealthChecker != nil {
nodeID := node.ID
actor := user.Username
go func() {
_, _ = app.HealthChecker.CheckNodeByID(nodeID, actor)
}()
}
writeJSON(writer, http.StatusOK, nodeResponse{Node: node})
}
@@ -6,7 +6,9 @@ import (
"errors"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/auth"
)
@@ -171,6 +173,154 @@ func TestNodesPatchHealthInterval(t *testing.T) {
}
}
func TestNodesPatchHealthIntervalTriggersBackgroundCheck(t *testing.T) {
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router, app := NewRouterWithApp(configDir)
app.HealthChecker.PingFn = func(hostIP string) auth.PingResult {
return auth.PingResult{OK: true, Method: auth.PingMethodICMP}
}
app.HealthChecker.SSHFn = func(hostIP string, username string, privateKeyPEM string, passphrase string) error {
return nil
}
const nodeID = "ffffffff-bbbb-4ccc-8ddd-eeeeeeeeeeee"
createBody := []byte(`{
"id":"` + nodeID + `",
"kind":"container",
"name":"bg-check-node",
"host_ip":"10.20.30.44",
"group_name":"Administrators",
"generate":{"algorithm":"ed25519"}
}`)
createRequest := withSession(
httptest.NewRequest(http.MethodPost, "/api/v1/nodes", bytes.NewReader(createBody)),
cookie,
)
createRecorder := httptest.NewRecorder()
router.ServeHTTP(createRecorder, createRequest)
if createRecorder.Code != http.StatusCreated {
t.Fatalf("create status = %d body=%s", createRecorder.Code, createRecorder.Body.String())
}
patchBody := []byte(`{"health_check_interval_seconds":60}`)
patchRequest := withSession(
httptest.NewRequest(
http.MethodPatch,
"/api/v1/nodes/"+nodeID,
bytes.NewReader(patchBody),
),
cookie,
)
patchRecorder := httptest.NewRecorder()
router.ServeHTTP(patchRecorder, patchRequest)
if patchRecorder.Code != http.StatusOK {
t.Fatalf("patch status = %d body=%s", patchRecorder.Code, patchRecorder.Body.String())
}
deadline := time.Now().Add(2 * time.Second)
for {
getRequest := withSession(
httptest.NewRequest(http.MethodGet, "/api/v1/nodes/"+nodeID, nil),
cookie,
)
getRecorder := httptest.NewRecorder()
router.ServeHTTP(getRecorder, getRequest)
if getRecorder.Code != http.StatusOK {
t.Fatalf("get status = %d body=%s", getRecorder.Code, getRecorder.Body.String())
}
var response nodeResponse
if err := json.Unmarshal(getRecorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode: %v", err)
}
if response.Node.HealthOK != nil && *response.Node.HealthOK {
return
}
if time.Now().After(deadline) {
t.Fatalf("timed out waiting for background health check, node=%#v", response.Node)
}
time.Sleep(20 * time.Millisecond)
}
}
func TestNodesPatchHealthIntervalZeroSkipsBackgroundCheck(t *testing.T) {
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router, app := NewRouterWithApp(configDir)
checked := atomic.Bool{}
app.HealthChecker.PingFn = func(hostIP string) auth.PingResult {
checked.Store(true)
return auth.PingResult{OK: true, Method: auth.PingMethodICMP}
}
app.HealthChecker.SSHFn = func(hostIP string, username string, privateKeyPEM string, passphrase string) error {
checked.Store(true)
return nil
}
const nodeID = "aaaaaaaa-cccc-4ddd-8eee-ffffffffffff"
createBody := []byte(`{
"id":"` + nodeID + `",
"kind":"vm",
"name":"no-bg-check-node",
"host_ip":"10.20.30.45",
"group_name":"Administrators",
"generate":{"algorithm":"ed25519"}
}`)
createRequest := withSession(
httptest.NewRequest(http.MethodPost, "/api/v1/nodes", bytes.NewReader(createBody)),
cookie,
)
createRecorder := httptest.NewRecorder()
router.ServeHTTP(createRecorder, createRequest)
if createRecorder.Code != http.StatusCreated {
t.Fatalf("create status = %d body=%s", createRecorder.Code, createRecorder.Body.String())
}
patchBody := []byte(`{"health_check_interval_seconds":0}`)
patchRequest := withSession(
httptest.NewRequest(
http.MethodPatch,
"/api/v1/nodes/"+nodeID,
bytes.NewReader(patchBody),
),
cookie,
)
patchRecorder := httptest.NewRecorder()
router.ServeHTTP(patchRecorder, patchRequest)
if patchRecorder.Code != http.StatusOK {
t.Fatalf("patch status = %d body=%s", patchRecorder.Code, patchRecorder.Body.String())
}
time.Sleep(100 * time.Millisecond)
getRequest := withSession(
httptest.NewRequest(http.MethodGet, "/api/v1/nodes/"+nodeID, nil),
cookie,
)
getRecorder := httptest.NewRecorder()
router.ServeHTTP(getRecorder, getRequest)
if getRecorder.Code != http.StatusOK {
t.Fatalf("get status = %d body=%s", getRecorder.Code, getRecorder.Body.String())
}
var response nodeResponse
if err := json.Unmarshal(getRecorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode: %v", err)
}
if checked.Load() {
t.Fatal("expected no background health check when interval is 0")
}
if response.Node.HealthOK != nil {
t.Fatalf("expected health_ok unset, got %#v", response.Node.HealthOK)
}
if response.Node.HealthLastCheckedAt != nil {
t.Fatalf("expected health_last_checked_at unset, got %#v", response.Node.HealthLastCheckedAt)
}
}
func TestNodesPatchHealthIntervalRejectsNegative(t *testing.T) {
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)