Add per-device routing long-tail: hosts, providers, routes, routing_rules
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Storage + CRUD (migration 0006, model, id-keyed store, REST handlers) plus
compiler rendering: each section is owned by a device and projected into that
device's rendered config (hosts/providers/routes/routing_rules).
This commit is contained in:
benvin
2026-07-26 00:12:19 +10:00
committed by Ben Vincent
parent bd97b13ff7
commit 721f4c1af3
7 changed files with 667 additions and 26 deletions
+190
View File
@@ -0,0 +1,190 @@
package server
import (
"net/http"
"github.com/go-chi/chi/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// mountPerDevice wires the per-device long-tail sections: hosts, providers,
// routes, and routing-rules.
func (s *Server) mountPerDevice(r chi.Router) {
r.Route("/hosts", func(r chi.Router) {
r.Get("/", s.listHosts)
r.Post("/", s.createHost)
r.Get("/{id}", s.getHost)
r.Delete("/{id}", s.deleteHost)
})
r.Route("/providers", func(r chi.Router) {
r.Get("/", s.listProviders)
r.Post("/", s.createProvider)
r.Get("/{id}", s.getProvider)
r.Delete("/{id}", s.deleteProvider)
})
r.Route("/routes", func(r chi.Router) {
r.Get("/", s.listRoutes)
r.Post("/", s.createRoute)
r.Get("/{id}", s.getRoute)
r.Delete("/{id}", s.deleteRoute)
})
r.Route("/routing-rules", func(r chi.Router) {
r.Get("/", s.listRoutingRules)
r.Post("/", s.createRoutingRule)
r.Get("/{id}", s.getRoutingRule)
r.Delete("/{id}", s.deleteRoutingRule)
})
}
func (s *Server) listHosts(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListHosts(r.Context())
respondList(w, list, err)
}
func (s *Server) createHost(w http.ResponseWriter, r *http.Request) {
var v model.Host
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Zone == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device, zone, and interface are required")
return
}
id, err := s.store.CreateHost(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getHost(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetHost(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteHost(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteHost(r.Context(), id))
}
func (s *Server) listProviders(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListProviders(r.Context())
respondList(w, list, err)
}
func (s *Server) createProvider(w http.ResponseWriter, r *http.Request) {
var v model.Provider
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Name == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device, name, and interface are required")
return
}
id, err := s.store.CreateProvider(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getProvider(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetProvider(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteProvider(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteProvider(r.Context(), id))
}
func (s *Server) listRoutes(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListRoutes(r.Context())
respondList(w, list, err)
}
func (s *Server) createRoute(w http.ResponseWriter, r *http.Request) {
var v model.Route
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Dest == "" {
writeError(w, http.StatusBadRequest, "device and dest are required")
return
}
id, err := s.store.CreateRoute(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getRoute(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetRoute(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteRoute(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteRoute(r.Context(), id))
}
func (s *Server) listRoutingRules(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListRoutingRules(r.Context())
respondList(w, list, err)
}
func (s *Server) createRoutingRule(w http.ResponseWriter, r *http.Request) {
var v model.RoutingRule
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Provider == "" {
writeError(w, http.StatusBadRequest, "device and provider are required")
return
}
id, err := s.store.CreateRoutingRule(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getRoutingRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetRoutingRule(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteRoutingRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteRoutingRule(r.Context(), id))
}
+10
View File
@@ -61,6 +61,7 @@ func (s *Server) mountResources(r chi.Router) {
})
s.mountNAT(r)
s.mountLongtail(r)
s.mountPerDevice(r)
}
// respondOne writes a single resource, mapping ErrNotFound to 404.
@@ -76,6 +77,15 @@ func respondOne(w http.ResponseWriter, v any, err error) {
writeJSON(w, http.StatusOK, v)
}
// respondCreated writes a 201 with the created resource, or 500 on error.
func respondCreated(w http.ResponseWriter, v any, err error) {
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, v)
}
// respondDelete maps a delete result to 204/404/500.
func respondDelete(w http.ResponseWriter, err error) {
if err != nil {