Add per-device config compiler and agent config endpoint

Project the fleet-global model through a device's bindings into a rendered,
interface-agnostic config: rules compile to saddr/daddr forward matches with no
iif/oif so they are correct under FRR/ECMP. Firewalls always enforce; routers
enforce only when their fabric opts into defense-in-depth. Referenced address
groups are emitted as named sets carrying their source (static CIDRs, dns FQDNs,
or asn numbers) so membership churns out-of-band without a rule reload. Wire
GET /devices/{name}/config to compile and serve YAML, generation-stamped. Add
portgroups/policies/settings store methods and portgroup CRUD. Pure Render is
unit-tested for enforcement gating, ASN set emission, and resolver precedence.
This commit is contained in:
benvin
2026-07-19 18:38:52 +10:00
parent c32fe8bd76
commit f80cc2cc30
7 changed files with 600 additions and 6 deletions
+41 -6
View File
@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"git.unkin.net/unkin/tomswallapi/internal/compiler"
"git.unkin.net/unkin/tomswallapi/internal/model"
"git.unkin.net/unkin/tomswallapi/internal/store"
)
@@ -36,6 +37,10 @@ func (s *Server) mountResources(r chi.Router) {
r.Get("/{name}/bindings", s.listBindings)
r.Put("/{name}/bindings/{zone}", s.putBinding)
})
r.Route("/portgroups", func(r chi.Router) {
r.Get("/", s.listPortGroups)
r.Put("/{name}", s.putPortGroup)
})
r.Route("/rules", func(r chi.Router) {
r.Get("/", s.listRules)
r.Post("/", s.createRule)
@@ -43,6 +48,24 @@ func (s *Server) mountResources(r chi.Router) {
})
}
func (s *Server) listPortGroups(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListPortGroups(r.Context())
respondList(w, list, err)
}
func (s *Server) putPortGroup(w http.ResponseWriter, r *http.Request) {
var p model.PortGroup
if !decode(w, r, &p) {
return
}
p.Name = chi.URLParam(r, "name")
if err := s.store.UpsertPortGroup(r.Context(), p); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, p)
}
func (s *Server) handleGeneration(w http.ResponseWriter, r *http.Request) {
g, err := s.store.Generation(r.Context())
if err != nil {
@@ -201,8 +224,24 @@ func (s *Server) deleteRule(w http.ResponseWriter, r *http.Request) {
// ---- Agent endpoints -------------------------------------------------------
func (s *Server) handleDeviceConfig(w http.ResponseWriter, r *http.Request) {
// Rendering is implemented by the compiler (see task: compiler + agent endpoint).
notImplemented(w)
cfg, err := compiler.Compile(r.Context(), s.store, chi.URLParam(r, "name"))
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeError(w, http.StatusNotFound, "device not found")
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
body, err := cfg.Marshal()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/yaml")
w.Header().Set("X-Tomswall-Generation", strconv.FormatInt(cfg.Generation, 10))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
func (s *Server) handleDeviceStatus(w http.ResponseWriter, r *http.Request) {
@@ -248,7 +287,3 @@ func respondList[T any](w http.ResponseWriter, list []T, err error) {
}
writeJSON(w, http.StatusOK, list)
}
func notImplemented(w http.ResponseWriter) {
writeError(w, http.StatusNotImplemented, "not implemented yet")
}