Files
EvoBGP/internal/httpapi/peers_discovery.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

126 lines
3.5 KiB
Go

package httpapi
import (
"encoding/json"
"net/http"
"strings"
"time"
"evobgp/internal/store"
)
func (s *Server) registerPeerDiscoveryRoutes(m *http.ServeMux) {
m.HandleFunc("GET /peers/discovered", s.handleListPeerDiscoveries)
m.HandleFunc("POST /peers/discovered/{id}/approve", s.handleApprovePeerDiscovery)
m.HandleFunc("POST /peers/discovered/{id}/reject", s.handleRejectPeerDiscovery)
}
func peerDiscoveryJSON(d *store.BGPPeerDiscovery) map[string]any {
if d == nil {
return nil
}
m := map[string]any{
"id": d.ID,
"neighbor_id": d.NeighborID,
"neighbor": d.Neighbor,
"remote_asn": d.RemoteASN,
"protocol_name": d.ProtocolName,
"session_state": d.SessionState,
"status": d.Status,
"first_seen_at": d.FirstSeenAt.UTC().Format(time.RFC3339Nano),
"last_seen_at": d.LastSeenAt.UTC().Format(time.RFC3339Nano),
}
if d.SpeakerID != "" {
m["speaker_id"] = d.SpeakerID
} else {
m["speaker_id"] = nil
}
if d.ApprovedPeerID != "" {
m["approved_peer_id"] = d.ApprovedPeerID
} else {
m["approved_peer_id"] = nil
}
return m
}
func (s *Server) handleListPeerDiscoveries(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requirePerm(w, a, "bgp:network:read") {
return
}
status := strings.TrimSpace(r.URL.Query().Get("status"))
if status == "" {
status = store.PeerDiscoveryPending
}
listStatus := status
if status == "all" {
listStatus = ""
}
items, err := s.store.ListPeerDiscoveries(a.TenantID, listStatus)
if err != nil {
writeStoreErr(w, err)
return
}
out := make([]map[string]any, 0, len(items))
for _, d := range items {
out = append(out, peerDiscoveryJSON(d))
}
writeJSON(w, http.StatusOK, map[string]any{"items": out})
}
func (s *Server) handleApprovePeerDiscovery(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:network:write") {
return
}
var body struct {
Name string `json:"name"`
BGPSpeakerID *string `json:"bgp_speaker_id"`
Enabled *bool `json:"enabled"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
peer, disc, err := s.store.ApprovePeerDiscovery(a.TenantID, r.PathValue("id"), &store.PeerDiscoveryApproveInput{
Name: body.Name,
SpeakerID: body.BGPSpeakerID,
Enabled: body.Enabled,
})
if err != nil {
writeStoreErr(w, err)
return
}
s.enqueuePeerReconcile(w, r, a.TenantID, "peer_discovery_approve")
s.recordCRUDAudit(r, a, "bgp.peer.discovery.approve", "Approved discovered peer "+peer.Neighbor, peer.ID, map[string]any{
"peer_id": peer.ID,
"discovery_id": disc.ID,
"neighbor": peer.Neighbor,
"neighbor_id": disc.NeighborID,
"remote_asn": peer.RemoteASN,
})
writeJSON(w, http.StatusOK, map[string]any{
"peer": peerJSON(peer),
"discovery": peerDiscoveryJSON(disc),
})
}
func (s *Server) handleRejectPeerDiscovery(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:network:write") {
return
}
disc, err := s.store.RejectPeerDiscovery(a.TenantID, r.PathValue("id"))
if err != nil {
writeStoreErr(w, err)
return
}
s.recordCRUDAudit(r, a, "bgp.peer.discovery.reject", "Rejected discovered peer "+disc.Neighbor, disc.ID, map[string]any{
"discovery_id": disc.ID,
"neighbor": disc.Neighbor,
"neighbor_id": disc.NeighborID,
})
writeJSON(w, http.StatusOK, peerDiscoveryJSON(disc))
}