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
+49 -26
View File
@@ -22,36 +22,44 @@ import (
// Input is the fully-resolved model needed to render one device. Keeping Render
// pure (no store access) makes it unit-testable without a database.
type Input struct {
Generation int64
Settings model.Settings
Device model.Device
Fabric *model.Fabric
Zones map[string]model.Zone
Groups map[string]model.AddressGroup
PortGroups map[string]model.PortGroup
Rules []model.Rule
Policies []model.Policy
Bindings []model.Binding
SNAT []model.SNATRule
Netmap []model.NetmapRule
NAT []model.NATRule
Generation int64
Settings model.Settings
Device model.Device
Fabric *model.Fabric
Zones map[string]model.Zone
Groups map[string]model.AddressGroup
PortGroups map[string]model.PortGroup
Rules []model.Rule
Policies []model.Policy
Bindings []model.Binding
SNAT []model.SNATRule
Netmap []model.NetmapRule
NAT []model.NATRule
Hosts []model.Host
Providers []model.Provider
Routes []model.Route
RoutingRules []model.RoutingRule
}
// RenderedConfig is the per-device output served to the agent.
type RenderedConfig struct {
Generation int64 `yaml:"generation" json:"generation"`
Device string `yaml:"device" json:"device"`
Class model.DeviceClass `yaml:"class" json:"class"`
Enforcing bool `yaml:"enforcing" json:"enforcing"`
Settings RenderedSettings `yaml:"settings" json:"settings"`
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
Generation int64 `yaml:"generation" json:"generation"`
Device string `yaml:"device" json:"device"`
Class model.DeviceClass `yaml:"class" json:"class"`
Enforcing bool `yaml:"enforcing" json:"enforcing"`
Settings RenderedSettings `yaml:"settings" json:"settings"`
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
Hosts []RenderedHost `yaml:"hosts,omitempty" json:"hosts,omitempty"`
Providers []RenderedProvider `yaml:"providers,omitempty" json:"providers,omitempty"`
Routes []RenderedRoute `yaml:"routes,omitempty" json:"routes,omitempty"`
RoutingRules []RenderedRoutingRule `yaml:"routing_rules,omitempty" json:"routing_rules,omitempty"`
}
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
@@ -197,6 +205,9 @@ func Render(in Input) (*RenderedConfig, error) {
out.Netmap = renderNetmapRules(in, out.Bindings)
out.NAT = renderNATRules(in)
// Per-device long-tail sections owned by this device.
renderPerDevice(in, out)
return out, nil
}
@@ -426,5 +437,17 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
if in.NAT, err = s.ListNAT(ctx); err != nil {
return nil, err
}
if in.Hosts, err = s.ListHosts(ctx); err != nil {
return nil, err
}
if in.Providers, err = s.ListProviders(ctx); err != nil {
return nil, err
}
if in.Routes, err = s.ListRoutes(ctx); err != nil {
return nil, err
}
if in.RoutingRules, err = s.ListRoutingRules(ctx); err != nil {
return nil, err
}
return Render(in)
}
+82
View File
@@ -0,0 +1,82 @@
package compiler
// Per-device long-tail sections rendered into a device's config. The compiler
// filters each global list to the entries owned by the device.
type RenderedHost struct {
Zone string `yaml:"zone" json:"zone"`
Interface string `yaml:"interface" json:"interface"`
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
Exclusions []string `yaml:"exclusions,omitempty" json:"exclusions,omitempty"`
Dynamic bool `yaml:"dynamic,omitempty" json:"dynamic,omitempty"`
}
type RenderedProvider struct {
Name string `yaml:"name" json:"name"`
Number int `yaml:"number" json:"number"`
Mark int `yaml:"mark,omitempty" json:"mark,omitempty"`
Duplicate string `yaml:"duplicate,omitempty" json:"duplicate,omitempty"`
Interface string `yaml:"interface" json:"interface"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
Copy []string `yaml:"copy,omitempty" json:"copy,omitempty"`
}
type RenderedRoute struct {
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
Dest string `yaml:"dest" json:"dest"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
Oif string `yaml:"oif,omitempty" json:"oif,omitempty"`
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedRoutingRule struct {
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
Provider string `yaml:"provider" json:"provider"`
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
Mark string `yaml:"mark,omitempty" json:"mark,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
// renderPerDevice projects the per-device long-tail sections owned by this device.
func renderPerDevice(in Input, out *RenderedConfig) {
dev := in.Device.Name
for _, h := range in.Hosts {
if h.Device != dev {
continue
}
out.Hosts = append(out.Hosts, RenderedHost{
Zone: h.Zone, Interface: h.Interface, Addresses: h.Addresses,
Exclusions: h.Exclusions, Dynamic: h.Dynamic,
})
}
for _, p := range in.Providers {
if p.Device != dev {
continue
}
out.Providers = append(out.Providers, RenderedProvider{
Name: p.Name, Number: p.Number, Mark: p.Mark, Duplicate: p.Duplicate,
Interface: p.Interface, Gateway: p.Gateway, Copy: p.Copy,
})
}
for _, r := range in.Routes {
if r.Device != dev {
continue
}
out.Routes = append(out.Routes, RenderedRoute{
Provider: r.Provider, Dest: r.Dest, Gateway: r.Gateway,
Oif: r.Oif, Persistent: r.Persistent, Comment: r.Comment,
})
}
for _, r := range in.RoutingRules {
if r.Device != dev {
continue
}
out.RoutingRules = append(out.RoutingRules, RenderedRoutingRule{
Source: r.Source, Dest: r.Dest, Provider: r.Provider, Priority: r.Priority,
Persistent: r.Persistent, Mark: r.Mark, Comment: r.Comment,
})
}
}
@@ -0,0 +1,48 @@
-- Per-device long-tail sections: hosts, providers (multi-ISP), static routes,
-- and routing rules (rtrules). Each is owned by a device and renders into that
-- device's config. (Nested option structs on host/provider are deferred.)
CREATE TABLE hosts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
zone TEXT NOT NULL,
interface TEXT NOT NULL,
addresses JSONB NOT NULL DEFAULT '[]'::jsonb,
exclusions JSONB NOT NULL DEFAULT '[]'::jsonb,
dynamic BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE providers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
name TEXT NOT NULL,
number INT NOT NULL,
mark INT NOT NULL DEFAULT 0,
duplicate TEXT NOT NULL DEFAULT '',
interface TEXT NOT NULL,
gateway TEXT NOT NULL DEFAULT '',
copy JSONB NOT NULL DEFAULT '[]'::jsonb
);
CREATE TABLE routes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
provider TEXT NOT NULL DEFAULT '',
dest TEXT NOT NULL,
gateway TEXT NOT NULL DEFAULT '',
oif TEXT NOT NULL DEFAULT '', -- egress interface (tomswall route "device")
persistent BOOLEAN NOT NULL DEFAULT false,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE routing_rules (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
source TEXT NOT NULL DEFAULT '',
dest TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL,
priority INT NOT NULL DEFAULT 0,
persistent BOOLEAN NOT NULL DEFAULT false,
mark TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
+54
View File
@@ -0,0 +1,54 @@
package model
// Per-device long-tail sections. Each is owned by a device (the fleet member it
// renders on) and maps to the corresponding tomswall config section.
// Host constrains a zone to specific addresses on a device's interface.
type Host struct {
ID int64 `json:"id"`
Device string `json:"device"`
Zone string `json:"zone"`
Interface string `json:"interface"`
Addresses []string `json:"addresses,omitempty"`
Exclusions []string `json:"exclusions,omitempty"`
Dynamic bool `json:"dynamic,omitempty"`
}
// Provider is a multi-ISP routing provider on a device.
type Provider struct {
ID int64 `json:"id"`
Device string `json:"device"`
Name string `json:"name"`
Number int `json:"number"`
Mark int `json:"mark,omitempty"`
Duplicate string `json:"duplicate,omitempty"`
Interface string `json:"interface"`
Gateway string `json:"gateway,omitempty"`
Copy []string `json:"copy,omitempty"`
}
// Route is a static route on a device. Oif is the egress interface (tomswall's
// route "device" field, renamed to avoid colliding with the fleet device).
type Route struct {
ID int64 `json:"id"`
Device string `json:"device"`
Provider string `json:"provider,omitempty"`
Dest string `json:"dest"`
Gateway string `json:"gateway,omitempty"`
Oif string `json:"oif,omitempty"`
Persistent bool `json:"persistent,omitempty"`
Comment string `json:"comment,omitempty"`
}
// RoutingRule directs traffic to a provider's routing table on a device.
type RoutingRule struct {
ID int64 `json:"id"`
Device string `json:"device"`
Source string `json:"source,omitempty"`
Dest string `json:"dest,omitempty"`
Provider string `json:"provider"`
Priority int `json:"priority,omitempty"`
Persistent bool `json:"persistent,omitempty"`
Mark string `json:"mark,omitempty"`
Comment string `json:"comment,omitempty"`
}
+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 {
+234
View File
@@ -0,0 +1,234 @@
package store
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// ---- Hosts -----------------------------------------------------------------
func (s *Store) ListHosts(ctx context.Context) ([]model.Host, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Host
for rows.Next() {
h, err := scanHost(rows)
if err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
func (s *Store) GetHost(ctx context.Context, id int64) (model.Host, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts WHERE id = $1`, id)
if err != nil {
return model.Host{}, err
}
defer rows.Close()
if !rows.Next() {
return model.Host{}, ErrNotFound
}
return scanHost(rows)
}
func scanHost(rows pgx.Rows) (model.Host, error) {
var h model.Host
var addrs, excl []byte
if err := rows.Scan(&h.ID, &h.Device, &h.Zone, &h.Interface, &addrs, &excl, &h.Dynamic); err != nil {
return h, err
}
if err := unmarshalStrings(addrs, &h.Addresses); err != nil {
return h, err
}
return h, unmarshalStrings(excl, &h.Exclusions)
}
func (s *Store) CreateHost(ctx context.Context, h model.Host) (int64, error) {
addrs, _ := jsonb(h.Addresses)
excl, _ := jsonb(h.Exclusions)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO hosts (device, zone, interface, addresses, exclusions, dynamic)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
h.Device, h.Zone, h.Interface, addrs, excl, h.Dynamic).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteHost(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM hosts WHERE id = $1`, id)
}
// ---- Providers -------------------------------------------------------------
func (s *Store) ListProviders(ctx context.Context) ([]model.Provider, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Provider
for rows.Next() {
p, err := scanProvider(rows)
if err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) GetProvider(ctx context.Context, id int64) (model.Provider, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers WHERE id = $1`, id)
if err != nil {
return model.Provider{}, err
}
defer rows.Close()
if !rows.Next() {
return model.Provider{}, ErrNotFound
}
return scanProvider(rows)
}
func scanProvider(rows pgx.Rows) (model.Provider, error) {
var p model.Provider
var cp []byte
if err := rows.Scan(&p.ID, &p.Device, &p.Name, &p.Number, &p.Mark, &p.Duplicate, &p.Interface, &p.Gateway, &cp); err != nil {
return p, err
}
return p, unmarshalStrings(cp, &p.Copy)
}
func (s *Store) CreateProvider(ctx context.Context, p model.Provider) (int64, error) {
cp, _ := jsonb(p.Copy)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO providers (device, name, number, mark, duplicate, interface, gateway, copy)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
p.Device, p.Name, p.Number, p.Mark, p.Duplicate, p.Interface, p.Gateway, cp).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteProvider(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM providers WHERE id = $1`, id)
}
// ---- Routes ----------------------------------------------------------------
func (s *Store) ListRoutes(ctx context.Context) ([]model.Route, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Route
for rows.Next() {
var r model.Route
if err := rows.Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func (s *Store) GetRoute(ctx context.Context, id int64) (model.Route, error) {
var r model.Route
err := s.pool.QueryRow(ctx,
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes WHERE id = $1`, id,
).Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return r, ErrNotFound
}
return r, err
}
func (s *Store) CreateRoute(ctx context.Context, r model.Route) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO routes (device, provider, dest, gateway, oif, persistent, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
r.Device, r.Provider, r.Dest, r.Gateway, r.Oif, r.Persistent, r.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteRoute(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM routes WHERE id = $1`, id)
}
// ---- Routing rules ---------------------------------------------------------
func (s *Store) ListRoutingRules(ctx context.Context) ([]model.RoutingRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules ORDER BY priority, id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.RoutingRule
for rows.Next() {
var r model.RoutingRule
if err := rows.Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func (s *Store) GetRoutingRule(ctx context.Context, id int64) (model.RoutingRule, error) {
var r model.RoutingRule
err := s.pool.QueryRow(ctx,
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules WHERE id = $1`, id,
).Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return r, ErrNotFound
}
return r, err
}
func (s *Store) CreateRoutingRule(ctx context.Context, r model.RoutingRule) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO routing_rules (device, source, dest, provider, priority, persistent, mark, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
r.Device, r.Source, r.Dest, r.Provider, r.Priority, r.Persistent, r.Mark, r.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteRoutingRule(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM routing_rules WHERE id = $1`, id)
}