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
+88
View File
@@ -262,6 +262,94 @@ func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation
return nil
}
func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error) {
var d model.Device
var resolver, settings []byte
err := s.pool.QueryRow(ctx,
`SELECT name, class, COALESCE(fabric, ''), resolver, settings FROM devices WHERE name = $1`, name,
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings)
if errors.Is(err, pgx.ErrNoRows) {
return d, ErrNotFound
}
if err != nil {
return d, err
}
if err := json.Unmarshal(resolver, &d.Resolver); err != nil {
return d, err
}
return d, json.Unmarshal(settings, &d.Settings)
}
// ---- Settings, portgroups, policies ----------------------------------------
func (s *Store) GetSettings(ctx context.Context) (model.Settings, error) {
var st model.Settings
var resolver []byte
err := s.pool.QueryRow(ctx, `
SELECT address_family, log_level, ip_forwarding, table_name, default_resolver
FROM settings WHERE id = true`,
).Scan(&st.AddressFamily, &st.LogLevel, &st.IPForwarding, &st.TableName, &resolver)
if err != nil {
return st, err
}
return st, json.Unmarshal(resolver, &st.DefaultResolver)
}
func (s *Store) ListPortGroups(ctx context.Context) ([]model.PortGroup, error) {
rows, err := s.pool.Query(ctx, `SELECT name, proto, ports FROM portgroups ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.PortGroup
for rows.Next() {
var p model.PortGroup
var ports []byte
if err := rows.Scan(&p.Name, &p.Proto, &ports); err != nil {
return nil, err
}
if err := json.Unmarshal(ports, &p.Ports); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) UpsertPortGroup(ctx context.Context, p model.PortGroup) error {
ports, err := jsonb(p.Ports)
if err != nil {
return err
}
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if _, err := tx.Exec(ctx, `
INSERT INTO portgroups (name, proto, ports) VALUES ($1, $2, $3)
ON CONFLICT (name) DO UPDATE SET proto = EXCLUDED.proto, ports = EXCLUDED.ports`,
p.Name, p.Proto, ports); err != nil {
return err
}
return bump(ctx, tx)
})
}
func (s *Store) ListPolicies(ctx context.Context) ([]model.Policy, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, priority, source, dest, action, log FROM policies ORDER BY priority, id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Policy
for rows.Next() {
var p model.Policy
if err := rows.Scan(&p.ID, &p.Priority, &p.Source, &p.Dest, &p.Action, &p.Log); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
// ---- Bindings --------------------------------------------------------------
func (s *Store) ListBindings(ctx context.Context, device string) ([]model.Binding, error) {