Control Plane HTTP API가 fleet polling과 client 상태 확장 전에 더 커지지 않도록 server lifecycle, DTO conversion, Edge/fleet handler 책임을 파일 단위로 나눈다.
149 lines
4.7 KiB
Go
149 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
|
|
"iop/apps/control-plane/internal/wire"
|
|
)
|
|
|
|
// edgeStatusRequester asks a connected Edge for its Edge-owned node snapshot.
|
|
type edgeStatusRequester func(edgeID string, timeout time.Duration) (*iop.EdgeStatusResponse, error)
|
|
|
|
// edgeCommandSender dispatches a command to a connected Edge and returns its
|
|
// typed response.
|
|
type edgeCommandSender func(edgeID, operation, targetSelector string, parameters map[string]string, timeout time.Duration) (*iop.EdgeCommandResponse, error)
|
|
|
|
func registerEdgeRegistryHandlers(mux *http.ServeMux, registry *wire.EdgeRegistry, requestStatus edgeStatusRequester, sendCommand edgeCommandSender) {
|
|
mux.HandleFunc("/edges", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/edges" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
methodNotAllowed(w, http.MethodGet)
|
|
return
|
|
}
|
|
states := registry.Snapshots()
|
|
views := make([]edgeRegistryView, 0, len(states))
|
|
for _, state := range states {
|
|
views = append(views, edgeRegistryViewFromState(state))
|
|
}
|
|
writeJSON(w, http.StatusOK, edgeRegistryResponse{Edges: views})
|
|
})
|
|
mux.HandleFunc("/edges/", func(w http.ResponseWriter, r *http.Request) {
|
|
edgePath := strings.TrimPrefix(r.URL.Path, "/edges/")
|
|
edgeID, rest, hasRest := strings.Cut(edgePath, "/")
|
|
if edgeID == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if !hasRest {
|
|
if r.Method != http.MethodGet {
|
|
methodNotAllowed(w, http.MethodGet)
|
|
return
|
|
}
|
|
state, ok := registry.Snapshot(edgeID)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, edgeRegistryViewFromState(state))
|
|
return
|
|
}
|
|
switch rest {
|
|
case "status":
|
|
if r.Method != http.MethodGet {
|
|
methodNotAllowed(w, http.MethodGet)
|
|
return
|
|
}
|
|
if _, ok := registry.Snapshot(edgeID); !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
resp, err := requestStatus(edgeID, 5*time.Second)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, edgeStatusResponseViewFromProto(resp))
|
|
case "events":
|
|
if r.Method != http.MethodGet {
|
|
methodNotAllowed(w, http.MethodGet)
|
|
return
|
|
}
|
|
if _, ok := registry.Snapshot(edgeID); !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
records := registry.NodeEvents(edgeID, r.URL.Query().Get("node_id"))
|
|
views := make([]edgeNodeEventView, 0, len(records))
|
|
for _, record := range records {
|
|
views = append(views, edgeNodeEventViewFromRecord(record))
|
|
}
|
|
writeJSON(w, http.StatusOK, edgeNodeEventsResponse{EdgeID: edgeID, Events: views})
|
|
case "operations":
|
|
if r.Method != http.MethodGet {
|
|
methodNotAllowed(w, http.MethodGet)
|
|
return
|
|
}
|
|
if _, ok := registry.Snapshot(edgeID); !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
records := registry.Commands(edgeID)
|
|
views := make([]edgeCommandRecordView, 0, len(records))
|
|
for _, record := range records {
|
|
views = append(views, edgeCommandRecordViewFromRecord(record))
|
|
}
|
|
writeJSON(w, http.StatusOK, edgeOperationsResponse{EdgeID: edgeID, Operations: views})
|
|
case "commands":
|
|
if r.Method != http.MethodPost {
|
|
methodNotAllowed(w, http.MethodPost)
|
|
return
|
|
}
|
|
handleEdgeCommandPost(w, r, edgeID, sendCommand)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
}
|
|
|
|
// handleEdgeCommandPost decodes a command request body and dispatches it to a
|
|
// single target Edge. A not-connected Edge yields a 502 with the wire error.
|
|
func handleEdgeCommandPost(w http.ResponseWriter, r *http.Request, edgeID string, sendCommand edgeCommandSender) {
|
|
var body edgeCommandRequestBody
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
|
return
|
|
}
|
|
if body.Operation == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "operation is required"})
|
|
return
|
|
}
|
|
if sendCommand == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "command dispatch is not configured"})
|
|
return
|
|
}
|
|
resp, err := sendCommand(edgeID, body.Operation, body.TargetSelector, body.Parameters, 10*time.Second)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, edgeCommandResponseViewFromProto(resp))
|
|
}
|
|
|
|
func methodNotAllowed(w http.ResponseWriter, allow ...string) {
|
|
w.Header().Set("Allow", strings.Join(allow, ", "))
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|