Files
tomswallapi/internal/store/perdevice.go
T
benvin 721f4c1af3
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add per-device routing long-tail: hosts, providers, routes, routing_rules
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).
2026-07-26 15:10:47 +10:00

235 lines
7.1 KiB
Go

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)
}