Manage external site-prober actions va API and Dashboard

This commit is contained in:
ayurishchev committed 2026-08-26 19:45:48 +03:00
1 parent f22ad569b6
commit 42f584dd3c
28 files changed
+717 -32

No files matched your search

+8
View File
@@ -80,3 +80,11 @@ type putCheckTypeRequest struct {
type orchestratorSettingsDTO struct {
FIPSettleSeconds int `json:"fip_settle_seconds"`
}
// inboundChecksDTO doubles as both the GET response and the PUT request
// body for /api/v1/admin/config/inbound-checks. Same field shape as
// proberAssignment.Ports/ICMP (dto.go).
type inboundChecksDTO struct {
Ports []int `json:"ports"`
ICMP bool `json:"icmp"`
}
+27
View File
@@ -209,3 +209,30 @@ func (s *Server) handleConfigPutOrchestratorSettings(w http.ResponseWriter, r *h
}
writeJSON(w, http.StatusOK, orchestratorSettingsDTO{FIPSettleSeconds: req.FIPSettleSeconds})
}
// --- prober inbound checks ---
func (s *Server) handleConfigGetInboundChecks(w http.ResponseWriter, r *http.Request) {
settings, err := s.DB.GetInboundChecks(r.Context())
if err != nil {
writeDBError(w, err)
return
}
writeJSON(w, http.StatusOK, inboundChecksDTO{Ports: settings.Ports, ICMP: settings.ICMP})
}
// handleConfigPutInboundChecks writes directly to the DB (unlike the
// orchestrator-settings PUT above) because port-range/duplicate validation
// is purely local — no cross-field dependency on other orchestrator config.
func (s *Server) handleConfigPutInboundChecks(w http.ResponseWriter, r *http.Request) {
var req inboundChecksDTO
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
return
}
if err := s.DB.SetInboundChecks(r.Context(), req.Ports, req.ICMP); err != nil {
writeDBError(w, err)
return
}
writeJSON(w, http.StatusOK, inboundChecksDTO{Ports: req.Ports, ICMP: req.ICMP})
}
+123
View File
@@ -500,3 +500,126 @@ func TestFIPSettleDelayGatesAssignmentEndpoint(t *testing.T) {
t.Fatalf("expected 200 after settle window elapsed, status=%d body=%s", resp.StatusCode, body)
}
}
// TestInboundChecksGetPut proves the prober check config round-trips
// through GET/PUT /api/v1/admin/config/inbound-checks.
func TestInboundChecksGetPut(t *testing.T) {
fc, _, _, _ := newConfigTestHarness(t)
resp, body := fc.do(http.MethodGet, "/api/v1/admin/config/inbound-checks", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("get inbound checks: status=%d body=%s", resp.StatusCode, body)
}
var got inboundChecksDTO
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("unmarshal get response: %v", err)
}
// newConfigTestHarness seeds Ports:[22,80], ICMP:true.
if len(got.Ports) != 2 || !got.ICMP {
t.Fatalf("expected seeded {[22 80] true}, got %+v", got)
}
resp, body = fc.do(http.MethodPut, "/api/v1/admin/config/inbound-checks", inboundChecksDTO{Ports: []int{443, 8080}, ICMP: false})
if resp.StatusCode != http.StatusOK {
t.Fatalf("put inbound checks: status=%d body=%s", resp.StatusCode, body)
}
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("unmarshal put response: %v", err)
}
if len(got.Ports) != 2 || got.ICMP {
t.Fatalf("expected {[443 8080] false}, got %+v", got)
}
resp, body = fc.do(http.MethodGet, "/api/v1/admin/config/inbound-checks", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("get inbound checks after put: status=%d body=%s", resp.StatusCode, body)
}
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("unmarshal get-after-put response: %v", err)
}
if got.Ports[0] != 443 || got.Ports[1] != 8080 || got.ICMP {
t.Fatalf("expected {[443 8080] false} to persist, got %+v", got)
}
}
// TestInboundChecksPutValidation proves out-of-range and duplicate ports
// are rejected with 400.
func TestInboundChecksPutValidation(t *testing.T) {
fc, _, _, _ := newConfigTestHarness(t)
resp, body := fc.do(http.MethodPut, "/api/v1/admin/config/inbound-checks", inboundChecksDTO{Ports: []int{0}, ICMP: false})
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400 for port 0, status=%d body=%s", resp.StatusCode, body)
}
resp, body = fc.do(http.MethodPut, "/api/v1/admin/config/inbound-checks", inboundChecksDTO{Ports: []int{70000}, ICMP: false})
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400 for port 70000, status=%d body=%s", resp.StatusCode, body)
}
resp, body = fc.do(http.MethodPut, "/api/v1/admin/config/inbound-checks", inboundChecksDTO{Ports: []int{22, 22}, ICMP: false})
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400 for duplicate port, status=%d body=%s", resp.StatusCode, body)
}
}
// TestInboundChecksReflectedInProberAssignmentsWithoutRestart proves the
// fix to the formerly-static handlers_prober.go read: a PUT to
// /api/v1/admin/config/inbound-checks changes what GET
// /api/v1/probers/{site_id}/assignments hands back to an already-registered
// prober, for an IP already in `checking`, with no control-api restart.
func TestInboundChecksReflectedInProberAssignmentsWithoutRestart(t *testing.T) {
fc, _, orch, mock := newConfigTestHarness(t)
ctx := context.Background()
mock.Seed("fip-1", "9.9.9.9", "svc-project")
fc.do(http.MethodPut, "/api/v1/admin/config/sites/1", putSiteRequest{SiteID: "site-1"})
fc.do(http.MethodPost, "/api/v1/admin/config/validators", createValidatorRequest{ValidatorID: "validator-1", OSPortID: "port-1"})
fc.do(http.MethodPost, "/api/v1/agents/register", registerAgentRequest{ValidatorID: "validator-1"})
fc.do(http.MethodPost, "/api/v1/admin/ips", submitIPsRequest{Addresses: []string{"9.9.9.9"}})
orch.Tick(ctx) // claim + associate -> awaiting_self_check
resp, body := fc.do(http.MethodGet, "/api/v1/agents/validator-1/assignment", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("assignment: status=%d body=%s", resp.StatusCode, body)
}
var assignment assignmentResponse
if err := json.Unmarshal(body, &assignment); err != nil {
t.Fatalf("unmarshal assignment: %v", err)
}
fc.do(http.MethodPost, "/api/v1/agents/validator-1/self-check", selfCheckRequest{
IPID: assignment.IPID, DetectedEgress: "9.9.9.9", Success: true, Detail: "matched",
})
// Now item.State == "checking" — a prober assignment target.
fc.do(http.MethodPost, "/api/v1/probers/register", registerProberRequest{SiteID: "site-1"})
resp, body = fc.do(http.MethodGet, "/api/v1/probers/site-1/assignments", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("prober assignments: status=%d body=%s", resp.StatusCode, body)
}
var assignments []proberAssignment
if err := json.Unmarshal(body, &assignments); err != nil {
t.Fatalf("unmarshal assignments: %v", err)
}
if len(assignments) != 1 || len(assignments[0].Ports) != 2 || !assignments[0].ICMP {
t.Fatalf("expected seeded {[22 80] true} before PUT, got %+v", assignments)
}
resp, body = fc.do(http.MethodPut, "/api/v1/admin/config/inbound-checks", inboundChecksDTO{Ports: []int{8080}, ICMP: false})
if resp.StatusCode != http.StatusOK {
t.Fatalf("put inbound checks: status=%d body=%s", resp.StatusCode, body)
}
resp, body = fc.do(http.MethodGet, "/api/v1/probers/site-1/assignments", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("prober assignments after put: status=%d body=%s", resp.StatusCode, body)
}
if err := json.Unmarshal(body, &assignments); err != nil {
t.Fatalf("unmarshal assignments after put: %v", err)
}
if len(assignments) != 1 || len(assignments[0].Ports) != 1 || assignments[0].Ports[0] != 8080 || assignments[0].ICMP {
t.Fatalf("expected updated {[8080] false} without restart, got %+v", assignments)
}
}
+6 -1
View File
@@ -45,11 +45,16 @@ func (s *Server) handleProberAssignments(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
inbound, err := s.DB.GetInboundChecks(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
out := make([]proberAssignment, 0, len(items))
for _, item := range items {
out = append(out, proberAssignment{
IPID: item.ID, IPAddress: item.IPAddress,
Ports: s.Orch.Inbound.Ports, ICMP: s.Orch.Inbound.ICMP,
Ports: inbound.Ports, ICMP: inbound.ICMP,
})
}
writeJSON(w, http.StatusOK, out)
+3
View File
@@ -46,4 +46,7 @@ func (s *Server) routes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/admin/config/orchestrator", s.handleConfigGetOrchestratorSettings)
mux.HandleFunc("PUT /api/v1/admin/config/orchestrator", s.handleConfigPutOrchestratorSettings)
mux.HandleFunc("GET /api/v1/admin/config/inbound-checks", s.handleConfigGetInboundChecks)
mux.HandleFunc("PUT /api/v1/admin/config/inbound-checks", s.handleConfigPutInboundChecks)
}