package db import ( "context" "encoding/json" "fmt" "cloudipvalidator/internal/config" ) // BootstrapFromConfig ensures the database's baseline state matches the // YAML config on a fresh install, then gets out of the way. // // - ip_addresses -> SeedQueue: unchanged, always additive. This is a // separate concept from the runtime admin queue API (SubmitIPs) — every // control-api startup re-adds any address from cfg.IPAddresses that // isn't already in the queue. // - validators / sites / target_groups / check_types: applied from YAML // only if the corresponding table is currently empty. Once any row // exists (via this bootstrap or the admin API), YAML for that section // is ignored on every subsequent startup — the database is the source // of truth from that point on. This is what makes admin API changes to // these four entities survive a restart. func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error { if err := d.bootstrapValidators(ctx, cfg.Validators); err != nil { return fmt.Errorf("bootstrap validators: %w", err) } if err := d.bootstrapSites(ctx, cfg.Sites); err != nil { return fmt.Errorf("bootstrap sites: %w", err) } if err := d.bootstrapTargetGroups(ctx, cfg.Targets); err != nil { return fmt.Errorf("bootstrap target groups: %w", err) } if err := d.bootstrapCheckTypes(ctx, cfg.CheckTypes); err != nil { return fmt.Errorf("bootstrap check types: %w", err) } if err := d.bootstrapSettings(ctx, cfg.Orchestrator.FIPSettleSeconds); err != nil { return fmt.Errorf("bootstrap settings: %w", err) } if err := d.bootstrapInboundChecks(ctx, cfg.Inbound.Ports, cfg.Inbound.ICMP); err != nil { return fmt.Errorf("bootstrap inbound checks: %w", err) } if err := d.SeedQueue(ctx, cfg.IPAddresses); err != nil { return fmt.Errorf("seed ip queue: %w", err) } return nil } func (d *DB) bootstrapValidators(ctx context.Context, validators []config.ValidatorConfig) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM validators`).Scan(&count); err != nil { return err } if count > 0 { return nil } for _, v := range validators { if err := d.RegisterValidator(ctx, v.ValidatorID, "", v.OSPortID, ""); err != nil { return fmt.Errorf("seed validator %s: %w", v.ValidatorID, err) } } return nil } func (d *DB) bootstrapSites(ctx context.Context, sites []config.SiteConfig) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM sites`).Scan(&count); err != nil { return err } if count > 0 { return nil } for _, s := range sites { if err := d.UpsertSite(ctx, s.Index, s.SiteID); err != nil { return fmt.Errorf("seed site %s: %w", s.SiteID, err) } } return nil } func (d *DB) bootstrapTargetGroups(ctx context.Context, targets map[string][]string) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM target_groups`).Scan(&count); err != nil { return err } if count > 0 { return nil } for name, addrs := range targets { if err := d.UpsertTargetGroup(ctx, name, addrs); err != nil { return fmt.Errorf("seed target group %s: %w", name, err) } } return nil } func (d *DB) bootstrapCheckTypes(ctx context.Context, checkTypes []config.CheckTypeConfig) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM check_types`).Scan(&count); err != nil { return err } if count > 0 { return nil } for _, ct := range checkTypes { if err := d.UpsertCheckType(ctx, ct.Name, ct.Enabled, ct.Targets); err != nil { return fmt.Errorf("seed check type %s: %w", ct.Name, err) } } return nil } func (d *DB) bootstrapSettings(ctx context.Context, fipSettleSeconds int) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM settings`).Scan(&count); err != nil { return err } if count > 0 { return nil } now := timeToDB(Now()) _, err := d.ExecContext(ctx, ` INSERT INTO settings (id, fip_settle_seconds, created_at, updated_at) VALUES (1, ?, ?, ?) `, fipSettleSeconds, now, now) return err } func (d *DB) bootstrapInboundChecks(ctx context.Context, ports []int, icmp bool) error { var count int if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM inbound_checks_settings`).Scan(&count); err != nil { return err } if count > 0 { return nil } if ports == nil { ports = []int{} } payload, err := json.Marshal(ports) if err != nil { return err } now := timeToDB(Now()) _, err = d.ExecContext(ctx, ` INSERT INTO inbound_checks_settings (id, ports, icmp, created_at, updated_at) VALUES (1, ?, ?, ?, ?) `, string(payload), icmp, now, now) return err }