82c7d9c5f7
Add GET /{name} and DELETE /{name} for zones, address-groups, portgroups,
fabrics, devices, and bindings, plus GET /rules/{id}, with the matching store
Get/Delete methods (deletes bump the generation and 404 on no-match). This gives
the resources full read/update/delete lifecycle so the Terraform provider can
manage them.
597 lines
17 KiB
Go
597 lines
17 KiB
Go
// Package store is the Postgres-backed repository for the fleet model. Every
|
|
// mutating method bumps the global config generation so agents can detect drift.
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
|
)
|
|
|
|
// ErrNotFound is returned when a lookup by key matches no row.
|
|
var ErrNotFound = errors.New("not found")
|
|
|
|
// Store provides CRUD over the fleet model.
|
|
type Store struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
// New constructs a Store over the given pool.
|
|
func New(pool *pgxpool.Pool) *Store { return &Store{pool: pool} }
|
|
|
|
// Generation returns the current global config generation.
|
|
func (s *Store) Generation(ctx context.Context) (int64, error) {
|
|
var g int64
|
|
err := s.pool.QueryRow(ctx, `SELECT current FROM generation WHERE id = true`).Scan(&g)
|
|
return g, err
|
|
}
|
|
|
|
// bump increments the generation within tx and returns the new value.
|
|
func bump(ctx context.Context, tx pgx.Tx) error {
|
|
_, err := tx.Exec(ctx, `UPDATE generation SET current = current + 1 WHERE id = true`)
|
|
return err
|
|
}
|
|
|
|
// jsonb marshals a value for a JSONB column, defaulting nil slices to "[]".
|
|
func jsonb(v any) ([]byte, error) {
|
|
if v == nil {
|
|
return []byte("[]"), nil
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
// ---- Fabrics ---------------------------------------------------------------
|
|
|
|
func (s *Store) ListFabrics(ctx context.Context) ([]model.Fabric, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT name, enforce_on_routers, description FROM fabrics ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Fabric
|
|
for rows.Next() {
|
|
var f model.Fabric
|
|
if err := rows.Scan(&f.Name, &f.EnforceOnRouters, &f.Description); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetFabric(ctx context.Context, name string) (model.Fabric, error) {
|
|
var f model.Fabric
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT name, enforce_on_routers, description FROM fabrics WHERE name = $1`, name,
|
|
).Scan(&f.Name, &f.EnforceOnRouters, &f.Description)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return f, ErrNotFound
|
|
}
|
|
return f, err
|
|
}
|
|
|
|
func (s *Store) UpsertFabric(ctx context.Context, f model.Fabric) error {
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO fabrics (name, enforce_on_routers, description)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
enforce_on_routers = EXCLUDED.enforce_on_routers,
|
|
description = EXCLUDED.description`,
|
|
f.Name, f.EnforceOnRouters, f.Description); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
// ---- Zones -----------------------------------------------------------------
|
|
|
|
func (s *Store) ListZones(ctx context.Context) ([]model.Zone, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT name, type, subnets, COALESCE(parent, '') FROM zones ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Zone
|
|
for rows.Next() {
|
|
var z model.Zone
|
|
var subnets []byte
|
|
if err := rows.Scan(&z.Name, &z.Type, &subnets, &z.Parent); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(subnets, &z.Subnets); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, z)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetZone(ctx context.Context, name string) (model.Zone, error) {
|
|
var z model.Zone
|
|
var subnets []byte
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT name, type, subnets, COALESCE(parent, '') FROM zones WHERE name = $1`, name,
|
|
).Scan(&z.Name, &z.Type, &subnets, &z.Parent)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return z, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return z, err
|
|
}
|
|
return z, json.Unmarshal(subnets, &z.Subnets)
|
|
}
|
|
|
|
func (s *Store) UpsertZone(ctx context.Context, z model.Zone) error {
|
|
subnets, err := jsonb(z.Subnets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if z.Type == "" {
|
|
z.Type = "ip"
|
|
}
|
|
var parent any
|
|
if z.Parent != "" {
|
|
parent = z.Parent
|
|
}
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO zones (name, type, subnets, parent)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
type = EXCLUDED.type, subnets = EXCLUDED.subnets, parent = EXCLUDED.parent`,
|
|
z.Name, z.Type, subnets, parent); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
// ---- Address groups --------------------------------------------------------
|
|
|
|
func (s *Store) ListAddressGroups(ctx context.Context) ([]model.AddressGroup, error) {
|
|
rows, err := s.pool.Query(ctx,
|
|
`SELECT name, type, members, refresh, description, resolved, resolved_at
|
|
FROM address_groups ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.AddressGroup
|
|
for rows.Next() {
|
|
var g model.AddressGroup
|
|
var members, resolved []byte
|
|
if err := rows.Scan(&g.Name, &g.Type, &members, &g.Refresh, &g.Description, &resolved, &g.ResolvedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(members, &g.Members); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(resolved, &g.Resolved); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, g)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// UpdateResolvedPrefixes stores expanded ASN prefixes for a group and bumps the
|
|
// generation so agents re-pull. Membership churn is stored separately from the
|
|
// group definition, so an UpsertAddressGroup never clobbers it.
|
|
func (s *Store) UpdateResolvedPrefixes(ctx context.Context, name string, prefixes []string) error {
|
|
resolved, err := jsonb(prefixes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
tag, err := tx.Exec(ctx,
|
|
`UPDATE address_groups SET resolved = $2, resolved_at = now() WHERE name = $1 AND type = 'asn'`,
|
|
name, resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
func (s *Store) UpsertAddressGroup(ctx context.Context, g model.AddressGroup) error {
|
|
members, err := jsonb(g.Members)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO address_groups (name, type, members, refresh, description)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
type = EXCLUDED.type, members = EXCLUDED.members,
|
|
refresh = EXCLUDED.refresh, description = EXCLUDED.description`,
|
|
g.Name, g.Type, members, g.Refresh, g.Description); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
// ---- Devices ---------------------------------------------------------------
|
|
|
|
func (s *Store) ListDevices(ctx context.Context) ([]model.Device, error) {
|
|
rows, err := s.pool.Query(ctx,
|
|
`SELECT name, class, COALESCE(fabric, ''), resolver FROM devices ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Device
|
|
for rows.Next() {
|
|
var d model.Device
|
|
var resolver []byte
|
|
if err := rows.Scan(&d.Name, &d.Class, &d.Fabric, &resolver); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(resolver, &d.Resolver); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) UpsertDevice(ctx context.Context, d model.Device) error {
|
|
resolver, err := jsonb(d.Resolver)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settings, err := jsonb(d.Settings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var fabric any
|
|
if d.Fabric != "" {
|
|
fabric = d.Fabric
|
|
}
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO devices (name, class, fabric, resolver, settings)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
class = EXCLUDED.class, fabric = EXCLUDED.fabric,
|
|
resolver = EXCLUDED.resolver, settings = EXCLUDED.settings`,
|
|
d.Name, d.Class, fabric, resolver, settings); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
// RecordDeviceStatus stores the generation an agent reports as applied.
|
|
func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation int64) error {
|
|
tag, err := s.pool.Exec(ctx,
|
|
`UPDATE devices SET reported_generation = $2, last_seen = now() WHERE name = $1`,
|
|
name, generation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
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) {
|
|
rows, err := s.pool.Query(ctx,
|
|
`SELECT device, zone, interfaces FROM bindings WHERE device = $1 ORDER BY zone`, device)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Binding
|
|
for rows.Next() {
|
|
var b model.Binding
|
|
var ifaces []byte
|
|
if err := rows.Scan(&b.Device, &b.Zone, &ifaces); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(ifaces, &b.Interfaces); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) UpsertBinding(ctx context.Context, b model.Binding) error {
|
|
ifaces, err := jsonb(b.Interfaces)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO bindings (device, zone, interfaces)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (device, zone) DO UPDATE SET interfaces = EXCLUDED.interfaces`,
|
|
b.Device, b.Zone, ifaces); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
// ---- Rules -----------------------------------------------------------------
|
|
|
|
func (s *Store) ListRules(ctx context.Context) ([]model.Rule, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id, priority, action, source, dest, proto, COALESCE(portgroup, ''), ports, log, comment
|
|
FROM rules ORDER BY priority, id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Rule
|
|
for rows.Next() {
|
|
var r model.Rule
|
|
var source, dest, ports []byte
|
|
if err := rows.Scan(&r.ID, &r.Priority, &r.Action, &source, &dest,
|
|
&r.Proto, &r.PortGroup, &ports, &r.Log, &r.Comment); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(source, &r.Source); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(dest, &r.Dest); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(ports, &r.Ports); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// CreateRule inserts a rule after validating its source/dest grammar, returning
|
|
// the assigned id.
|
|
func (s *Store) CreateRule(ctx context.Context, r model.Rule) (int64, error) {
|
|
if _, err := model.ParseElements(r.Source); err != nil {
|
|
return 0, fmt.Errorf("source: %w", err)
|
|
}
|
|
if _, err := model.ParseElements(r.Dest); err != nil {
|
|
return 0, fmt.Errorf("dest: %w", err)
|
|
}
|
|
source, _ := jsonb(r.Source)
|
|
dest, _ := jsonb(r.Dest)
|
|
ports, _ := jsonb(r.Ports)
|
|
var portgroup any
|
|
if r.PortGroup != "" {
|
|
portgroup = r.PortGroup
|
|
}
|
|
var id int64
|
|
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
if err := tx.QueryRow(ctx, `
|
|
INSERT INTO rules (priority, action, source, dest, proto, portgroup, ports, log, comment)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
|
r.Priority, r.Action, source, dest, r.Proto, portgroup, ports, r.Log, r.Comment,
|
|
).Scan(&id); err != nil {
|
|
return err
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
return id, err
|
|
}
|
|
|
|
func (s *Store) DeleteRule(ctx context.Context, id int64) error {
|
|
return s.deleteOne(ctx, `DELETE FROM rules WHERE id = $1`, id)
|
|
}
|
|
|
|
// ---- Get-single and Delete completion --------------------------------------
|
|
|
|
// deleteOne runs a single-row delete, returning ErrNotFound when nothing matched
|
|
// and bumping the generation on success.
|
|
func (s *Store) deleteOne(ctx context.Context, query string, args ...any) error {
|
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
|
tag, err := tx.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return bump(ctx, tx)
|
|
})
|
|
}
|
|
|
|
func (s *Store) DeleteZone(ctx context.Context, name string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM zones WHERE name = $1`, name)
|
|
}
|
|
|
|
func (s *Store) DeleteFabric(ctx context.Context, name string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM fabrics WHERE name = $1`, name)
|
|
}
|
|
|
|
func (s *Store) DeleteDevice(ctx context.Context, name string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM devices WHERE name = $1`, name)
|
|
}
|
|
|
|
func (s *Store) DeletePortGroup(ctx context.Context, name string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM portgroups WHERE name = $1`, name)
|
|
}
|
|
|
|
func (s *Store) DeleteAddressGroup(ctx context.Context, name string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM address_groups WHERE name = $1`, name)
|
|
}
|
|
|
|
func (s *Store) DeleteBinding(ctx context.Context, device, zone string) error {
|
|
return s.deleteOne(ctx, `DELETE FROM bindings WHERE device = $1 AND zone = $2`, device, zone)
|
|
}
|
|
|
|
func (s *Store) GetAddressGroup(ctx context.Context, name string) (model.AddressGroup, error) {
|
|
var g model.AddressGroup
|
|
var members, resolved []byte
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT name, type, members, refresh, description, resolved, resolved_at
|
|
FROM address_groups WHERE name = $1`, name,
|
|
).Scan(&g.Name, &g.Type, &members, &g.Refresh, &g.Description, &resolved, &g.ResolvedAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return g, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
if err := json.Unmarshal(members, &g.Members); err != nil {
|
|
return g, err
|
|
}
|
|
return g, json.Unmarshal(resolved, &g.Resolved)
|
|
}
|
|
|
|
func (s *Store) GetPortGroup(ctx context.Context, name string) (model.PortGroup, error) {
|
|
var p model.PortGroup
|
|
var ports []byte
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT name, proto, ports FROM portgroups WHERE name = $1`, name,
|
|
).Scan(&p.Name, &p.Proto, &ports)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return p, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
return p, json.Unmarshal(ports, &p.Ports)
|
|
}
|
|
|
|
func (s *Store) GetBinding(ctx context.Context, device, zone string) (model.Binding, error) {
|
|
var b model.Binding
|
|
var ifaces []byte
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT device, zone, interfaces FROM bindings WHERE device = $1 AND zone = $2`, device, zone,
|
|
).Scan(&b.Device, &b.Zone, &ifaces)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return b, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return b, err
|
|
}
|
|
return b, json.Unmarshal(ifaces, &b.Interfaces)
|
|
}
|
|
|
|
func (s *Store) GetRule(ctx context.Context, id int64) (model.Rule, error) {
|
|
var r model.Rule
|
|
var source, dest, ports []byte
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT id, priority, action, source, dest, proto, COALESCE(portgroup, ''), ports, log, comment
|
|
FROM rules WHERE id = $1`, id,
|
|
).Scan(&r.ID, &r.Priority, &r.Action, &source, &dest, &r.Proto, &r.PortGroup, &ports, &r.Log, &r.Comment)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return r, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
if err := json.Unmarshal(source, &r.Source); err != nil {
|
|
return r, err
|
|
}
|
|
if err := json.Unmarshal(dest, &r.Dest); err != nil {
|
|
return r, err
|
|
}
|
|
return r, json.Unmarshal(ports, &r.Ports)
|
|
}
|