package store import ( "context" "database/sql" "encoding/json" "errors" "reflect" "jarl/internal/model" ) const settingsKey = "settings" // LoadState loads the complete configuration. func (s *Store) LoadState(ctx context.Context) (*model.State, error) { return loadState(ctx, s.db) } func loadState(ctx context.Context, e execer) (*model.State, error) { st := model.NewState() var raw string err := e.QueryRowContext(ctx, `SELECT value FROM meta WHERE key = ?`, settingsKey).Scan(&raw) switch { case errors.Is(err, sql.ErrNoRows): case err != nil: return nil, err default: if err := json.Unmarshal([]byte(raw), &st.Settings); err != nil { return nil, err } } rows, err := e.QueryContext(ctx, `SELECT id, name, position, match_clients, match_destinations, match_recursive_only, comment, version, created_at, updated_at, created_by, updated_by FROM views`) if err != nil { return nil, err } for rows.Next() { v := &model.View{} var mc, md, ca, ua string var mro sql.NullInt64 if err := rows.Scan(&v.ID, &v.Name, &v.Position, &mc, &md, &mro, &v.Comment, &v.Version, &ca, &ua, &v.CreatedBy, &v.UpdatedBy); err != nil { rows.Close() return nil, err } json.Unmarshal([]byte(mc), &v.MatchClients) json.Unmarshal([]byte(md), &v.MatchDestinations) if mro.Valid { v.MatchRecursiveOnly = model.Ptr(mro.Int64 != 0) } v.CreatedAt, v.UpdatedAt = parseTS(ca), parseTS(ua) st.Views[v.ID] = v } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, name, elements, comment, version, created_at, updated_at, created_by, updated_by FROM acls`) if err != nil { return nil, err } for rows.Next() { a := &model.ACL{} var el, ca, ua string if err := rows.Scan(&a.ID, &a.Name, &el, &a.Comment, &a.Version, &ca, &ua, &a.CreatedBy, &a.UpdatedBy); err != nil { rows.Close() return nil, err } json.Unmarshal([]byte(el), &a.Elements) a.CreatedAt, a.UpdatedAt = parseTS(ca), parseTS(ua) st.ACLs[a.ID] = a } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, name, algorithm, secret, comment, version, created_at, updated_at, created_by, updated_by FROM tsig_keys`) if err != nil { return nil, err } for rows.Next() { k := &model.TSIGKey{} var ca, ua string if err := rows.Scan(&k.ID, &k.Name, &k.Algorithm, &k.Secret, &k.Comment, &k.Version, &ca, &ua, &k.CreatedBy, &k.UpdatedBy); err != nil { rows.Close() return nil, err } k.CreatedAt, k.UpdatedAt = parseTS(ca), parseTS(ua) st.Keys[k.ID] = k } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, name, view_id, type, in_view, file, soa_mname, soa_rname, soa_serial, soa_refresh, soa_retry, soa_expire, soa_minimum, default_ttl, serial_policy, dnssec_policy, comment, version, created_at, updated_at, created_by, updated_by FROM zones`) if err != nil { return nil, err } for rows.Next() { z := &model.Zone{} var ca, ua string if err := rows.Scan(&z.ID, &z.Name, &z.ViewID, &z.Type, &z.InView, &z.File, &z.SOA.MName, &z.SOA.RName, &z.SOA.Serial, &z.SOA.Refresh, &z.SOA.Retry, &z.SOA.Expire, &z.SOA.Minimum, &z.DefaultTTL, &z.SerialPolicy, &z.DNSSECPolicy, &z.Comment, &z.Version, &ca, &ua, &z.CreatedBy, &z.UpdatedBy); err != nil { rows.Close() return nil, err } z.CreatedAt, z.UpdatedAt = parseTS(ca), parseTS(ua) st.Zones[z.ID] = z } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, zone_id, name, type, ttl, data, comment, disabled, version, created_at, updated_at, created_by, updated_by FROM records`) if err != nil { return nil, err } for rows.Next() { r := &model.Record{} var ttl sql.NullInt64 var ca, ua string if err := rows.Scan(&r.ID, &r.ZoneID, &r.Name, &r.Type, &ttl, &r.Data, &r.Comment, &r.Disabled, &r.Version, &ca, &ua, &r.CreatedBy, &r.UpdatedBy); err != nil { rows.Close() return nil, err } if ttl.Valid { r.TTL = model.Ptr(uint32(ttl.Int64)) } r.CreatedAt, r.UpdatedAt = parseTS(ca), parseTS(ua) st.Records[r.ID] = r } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, scope, scope_id, name, value, position, comment, version, created_at, updated_at, created_by, updated_by FROM options`) if err != nil { return nil, err } for rows.Next() { o := &model.Option{} var ca, ua string if err := rows.Scan(&o.ID, &o.Scope, &o.ScopeID, &o.Name, &o.Value, &o.Position, &o.Comment, &o.Version, &ca, &ua, &o.CreatedBy, &o.UpdatedBy); err != nil { rows.Close() return nil, err } o.CreatedAt, o.UpdatedAt = parseTS(ca), parseTS(ua) st.Options[o.ID] = o } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, name, url, view_ids, disabled, comment, version, created_at, updated_at, created_by, updated_by FROM blocklists`) if err != nil { return nil, err } for rows.Next() { b := &model.Blocklist{} var views, ca, ua string if err := rows.Scan(&b.ID, &b.Name, &b.URL, &views, &b.Disabled, &b.Comment, &b.Version, &ca, &ua, &b.CreatedBy, &b.UpdatedBy); err != nil { rows.Close() return nil, err } json.Unmarshal([]byte(views), &b.ViewIDs) b.CreatedAt, b.UpdatedAt = parseTS(ca), parseTS(ua) st.Blocklists[b.ID] = b } if err := closeRows(rows); err != nil { return nil, err } rows, err = e.QueryContext(ctx, `SELECT id, name, action, exact, view_ids, comment, version, created_at, updated_at, created_by, updated_by FROM block_rules`) if err != nil { return nil, err } for rows.Next() { r := &model.BlockRule{} var views, ca, ua string if err := rows.Scan(&r.ID, &r.Name, &r.Action, &r.Exact, &views, &r.Comment, &r.Version, &ca, &ua, &r.CreatedBy, &r.UpdatedBy); err != nil { rows.Close() return nil, err } json.Unmarshal([]byte(views), &r.ViewIDs) r.CreatedAt, r.UpdatedAt = parseTS(ca), parseTS(ua) st.BlockRules[r.ID] = r } if err := closeRows(rows); err != nil { return nil, err } return st, nil } func closeRows(rows *sql.Rows) error { err := rows.Err() rows.Close() return err } // SaveState writes the differences between old and new. func (t *Tx) SaveState(old, new *model.State) error { return saveState(t.ctx, t.tx, old, new) } // ReplaceState replaces the stored configuration with st (used by import // and tests). func (t *Tx) ReplaceState(st *model.State) error { for _, tbl := range []string{"views", "acls", "tsig_keys", "zones", "records", "options", "blocklists", "block_rules"} { if _, err := t.tx.ExecContext(t.ctx, "DELETE FROM "+tbl); err != nil { return err } } return saveState(t.ctx, t.tx, model.NewState(), st) } // LoadState loads the configuration inside the transaction. func (t *Tx) LoadState() (*model.State, error) { return loadState(t.ctx, t.tx) } func saveState(ctx context.Context, e execer, old, new *model.State) error { if !reflect.DeepEqual(old.Settings, new.Settings) { if err := setMeta(ctx, e, settingsKey, jsonText(new.Settings)); err != nil { return err } } if err := diffMap(old.Views, new.Views, func(v *model.View) error { var mro any if v.MatchRecursiveOnly != nil { mro = boolInt(*v.MatchRecursiveOnly) } _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO views (id, name, position, match_clients, match_destinations, match_recursive_only, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, v.ID, v.Name, v.Position, jsonText(nonNil(v.MatchClients)), jsonText(nonNil(v.MatchDestinations)), mro, v.Comment, v.Version, ts(v.CreatedAt), ts(v.UpdatedAt), v.CreatedBy, v.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "views", id) }); err != nil { return err } if err := diffMap(old.ACLs, new.ACLs, func(a *model.ACL) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO acls (id, name, elements, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?)`, a.ID, a.Name, jsonText(nonNil(a.Elements)), a.Comment, a.Version, ts(a.CreatedAt), ts(a.UpdatedAt), a.CreatedBy, a.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "acls", id) }); err != nil { return err } if err := diffMap(old.Keys, new.Keys, func(k *model.TSIGKey) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO tsig_keys (id, name, algorithm, secret, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?)`, k.ID, k.Name, k.Algorithm, k.Secret, k.Comment, k.Version, ts(k.CreatedAt), ts(k.UpdatedAt), k.CreatedBy, k.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "tsig_keys", id) }); err != nil { return err } if err := diffMap(old.Zones, new.Zones, func(z *model.Zone) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO zones (id, name, view_id, type, in_view, file, soa_mname, soa_rname, soa_serial, soa_refresh, soa_retry, soa_expire, soa_minimum, default_ttl, serial_policy, dnssec_policy, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, z.ID, z.Name, z.ViewID, z.Type, z.InView, z.File, z.SOA.MName, z.SOA.RName, z.SOA.Serial, z.SOA.Refresh, z.SOA.Retry, z.SOA.Expire, z.SOA.Minimum, z.DefaultTTL, z.SerialPolicy, z.DNSSECPolicy, z.Comment, z.Version, ts(z.CreatedAt), ts(z.UpdatedAt), z.CreatedBy, z.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "zones", id) }); err != nil { return err } if err := diffMap(old.Records, new.Records, func(r *model.Record) error { var ttl any if r.TTL != nil { ttl = int64(*r.TTL) } _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO records (id, zone_id, name, type, ttl, data, comment, disabled, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, r.ID, r.ZoneID, r.Name, r.Type, ttl, r.Data, r.Comment, r.Disabled, r.Version, ts(r.CreatedAt), ts(r.UpdatedAt), r.CreatedBy, r.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "records", id) }); err != nil { return err } if err := diffMap(old.Options, new.Options, func(o *model.Option) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO options (id, scope, scope_id, name, value, position, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, o.ID, o.Scope, o.ScopeID, o.Name, o.Value, o.Position, o.Comment, o.Version, ts(o.CreatedAt), ts(o.UpdatedAt), o.CreatedBy, o.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "options", id) }); err != nil { return err } if err := diffMap(old.Blocklists, new.Blocklists, func(b *model.Blocklist) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO blocklists (id, name, url, view_ids, disabled, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, b.ID, b.Name, b.URL, jsonText(nonNilIDs(b.ViewIDs)), b.Disabled, b.Comment, b.Version, ts(b.CreatedAt), ts(b.UpdatedAt), b.CreatedBy, b.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "blocklists", id) }); err != nil { return err } return diffMap(old.BlockRules, new.BlockRules, func(r *model.BlockRule) error { _, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO block_rules (id, name, action, exact, view_ids, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, r.ID, r.Name, r.Action, r.Exact, jsonText(nonNilIDs(r.ViewIDs)), r.Comment, r.Version, ts(r.CreatedAt), ts(r.UpdatedAt), r.CreatedBy, r.UpdatedBy) return err }, func(id int64) error { return del(ctx, e, "block_rules", id) }) } func nonNilIDs(ids []int64) []int64 { if ids == nil { return []int64{} } return ids } func nonNil(s []string) []string { if s == nil { return []string{} } return s } func del(ctx context.Context, e execer, table string, id int64) error { _, err := e.ExecContext(ctx, "DELETE FROM "+table+" WHERE id = ?", id) return err } // diffMap calls put for new or changed objects and remove for deleted ones. func diffMap[T any](old, new map[int64]*T, put func(*T) error, remove func(int64) error) error { for id, n := range new { if o, ok := old[id]; ok && reflect.DeepEqual(o, n) { continue } if err := put(n); err != nil { return err } } for id := range old { if _, ok := new[id]; !ok { if err := remove(id); err != nil { return err } } } return nil }