Manage external site-prober actions va API and Dashboard
This commit is contained in:
1 parent
f22ad569b6
commit
42f584dd3c
28 files changed
+717
-32
No files matched your search
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"cloudipvalidator/internal/config"
|
||||
@@ -36,6 +37,9 @@ func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) er
|
||||
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)
|
||||
}
|
||||
@@ -120,3 +124,25 @@ func (d *DB) bootstrapSettings(ctx context.Context, fipSettleSeconds int) error
|
||||
`, 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
|
||||
}
|
||||
@@ -22,6 +22,9 @@ var dynamicConfigSchema string
|
||||
//go:embed migrations/0003_fip_settle_delay.sql
|
||||
var fipSettleDelaySchema string
|
||||
|
||||
//go:embed migrations/0004_inbound_checks_admin.sql
|
||||
var inboundChecksAdminSchema string
|
||||
|
||||
// migrations is the ordered list of schema versions. Each entry's SQL is
|
||||
// applied, in order, for any version greater than the database's current
|
||||
// PRAGMA user_version — so a fresh database walks the whole list and an
|
||||
@@ -33,6 +36,7 @@ var migrations = []struct {
|
||||
{1, initSchema},
|
||||
{2, dynamicConfigSchema},
|
||||
{3, fipSettleDelaySchema},
|
||||
{4, inboundChecksAdminSchema},
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Support for admin-API-configurable prober check types (see docs/USAGE.md,
|
||||
-- "Управление типами проверок пробера"). Previously orchestrator.inbound_checks
|
||||
-- was a YAML-only value baked into the process at startup.
|
||||
|
||||
-- Singleton row, deliberately separate from `settings`: unlike
|
||||
-- fip_settle_seconds, inbound checks need no cross-field validation against
|
||||
-- other orchestrator config, and ports is a list rather than a scalar, so it
|
||||
-- doesn't fit the "add a column to settings" guidance left in
|
||||
-- 0003_fip_settle_delay.sql.
|
||||
CREATE TABLE inbound_checks_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
ports TEXT NOT NULL, -- JSON array of int, e.g. "[22,80,443,8080]"
|
||||
icmp BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
@@ -171,3 +171,13 @@ type Settings struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// InboundChecksSettings is the singleton row describing what the prober
|
||||
// checks on every site for every in-flight IP (TCP ports + optional ICMP).
|
||||
// Admin-configurable at runtime (see queries_inbound.go).
|
||||
type InboundChecksSettings struct {
|
||||
Ports []int
|
||||
ICMP bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GetInboundChecks returns the singleton inbound-checks settings row.
|
||||
// BootstrapFromConfig guarantees it exists before any other code path can
|
||||
// observe it, so sql.ErrNoRows here would indicate a bootstrap bug, not a
|
||||
// normal condition.
|
||||
func (d *DB) GetInboundChecks(ctx context.Context) (InboundChecksSettings, error) {
|
||||
var s InboundChecksSettings
|
||||
var portsJSON, createdAt, updatedAt string
|
||||
err := d.QueryRowContext(ctx, `
|
||||
SELECT ports, icmp, created_at, updated_at FROM inbound_checks_settings WHERE id=1
|
||||
`).Scan(&portsJSON, &s.ICMP, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
return InboundChecksSettings{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(portsJSON), &s.Ports); err != nil {
|
||||
return InboundChecksSettings{}, fmt.Errorf("decode inbound check ports: %w", err)
|
||||
}
|
||||
if s.Ports == nil {
|
||||
s.Ports = []int{}
|
||||
}
|
||||
if s.CreatedAt, err = dbToTime(createdAt); err != nil {
|
||||
return InboundChecksSettings{}, err
|
||||
}
|
||||
if s.UpdatedAt, err = dbToTime(updatedAt); err != nil {
|
||||
return InboundChecksSettings{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// SetInboundChecks persists a new prober check configuration. Validation is
|
||||
// purely local (port range, no duplicates) — unlike fip_settle_seconds,
|
||||
// there's no cross-field dependency on other orchestrator config, so this
|
||||
// is called directly by the HTTP handler without going through
|
||||
// orchestrator.Orchestrator.
|
||||
func (d *DB) SetInboundChecks(ctx context.Context, ports []int, icmp bool) error {
|
||||
seen := make(map[int]bool, len(ports))
|
||||
for _, p := range ports {
|
||||
if p < 1 || p > 65535 {
|
||||
return fmt.Errorf("port %d out of range 1..65535: %w", p, ErrValidation)
|
||||
}
|
||||
if seen[p] {
|
||||
return fmt.Errorf("duplicate port %d: %w", p, ErrValidation)
|
||||
}
|
||||
seen[p] = true
|
||||
}
|
||||
if ports == nil {
|
||||
ports = []int{}
|
||||
}
|
||||
payload, err := json.Marshal(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := timeToDB(Now())
|
||||
_, err = d.ExecContext(ctx, `
|
||||
UPDATE inbound_checks_settings SET ports=?, icmp=?, updated_at=? WHERE id=1
|
||||
`, string(payload), icmp, now)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"cloudipvalidator/internal/config"
|
||||
)
|
||||
|
||||
func TestBootstrapInboundChecksSeedsOnceFromConfig(t *testing.T) {
|
||||
d, ctx := newTestDB(t)
|
||||
|
||||
cfg := &config.ControlAPI{Inbound: config.InboundConfig{Ports: []int{22, 80}, ICMP: true}}
|
||||
if err := d.BootstrapFromConfig(ctx, cfg); err != nil {
|
||||
t.Fatalf("first bootstrap: %v", err)
|
||||
}
|
||||
settings, err := d.GetInboundChecks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get inbound checks: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(settings.Ports, []int{22, 80}) || !settings.ICMP {
|
||||
t.Fatalf("expected seeded {[22 80] true}, got %+v", settings)
|
||||
}
|
||||
|
||||
// A second bootstrap with a different YAML value must not overwrite the
|
||||
// now-non-empty table — same "DB is source of truth once seeded"
|
||||
// semantics as validators/sites/targets/check_types/settings.
|
||||
cfg2 := &config.ControlAPI{Inbound: config.InboundConfig{Ports: []int{443}, ICMP: false}}
|
||||
if err := d.BootstrapFromConfig(ctx, cfg2); err != nil {
|
||||
t.Fatalf("second bootstrap: %v", err)
|
||||
}
|
||||
settings, err = d.GetInboundChecks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get inbound checks after second bootstrap: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(settings.Ports, []int{22, 80}) || !settings.ICMP {
|
||||
t.Fatalf("expected YAML to be ignored on non-empty table, got %+v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetInboundChecksRejectsPortOutOfRange(t *testing.T) {
|
||||
d, ctx := newTestDB(t)
|
||||
if err := d.BootstrapFromConfig(ctx, &config.ControlAPI{}); err != nil {
|
||||
t.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
for _, p := range []int{0, -1, 65536, 70000} {
|
||||
if err := d.SetInboundChecks(ctx, []int{p}, false); !errors.Is(err, ErrValidation) {
|
||||
t.Fatalf("port %d: expected ErrValidation, got %v", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetInboundChecksRejectsDuplicatePorts(t *testing.T) {
|
||||
d, ctx := newTestDB(t)
|
||||
if err := d.BootstrapFromConfig(ctx, &config.ControlAPI{}); err != nil {
|
||||
t.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
if err := d.SetInboundChecks(ctx, []int{22, 80, 22}, false); !errors.Is(err, ErrValidation) {
|
||||
t.Fatalf("expected ErrValidation for duplicate port, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSetInboundChecksRoundTrip(t *testing.T) {
|
||||
d, ctx := newTestDB(t)
|
||||
if err := d.BootstrapFromConfig(ctx, &config.ControlAPI{}); err != nil {
|
||||
t.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
before, err := d.GetInboundChecks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get inbound checks: %v", err)
|
||||
}
|
||||
if len(before.Ports) != 0 || before.ICMP {
|
||||
t.Fatalf("expected default {[] false}, got %+v", before)
|
||||
}
|
||||
|
||||
if err := d.SetInboundChecks(ctx, []int{22, 443, 8080}, true); err != nil {
|
||||
t.Fatalf("set inbound checks: %v", err)
|
||||
}
|
||||
after, err := d.GetInboundChecks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get inbound checks after set: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(after.Ports, []int{22, 443, 8080}) || !after.ICMP {
|
||||
t.Fatalf("expected {[22 443 8080] true}, got %+v", after)
|
||||
}
|
||||
if after.UpdatedAt.Before(before.UpdatedAt) {
|
||||
t.Fatalf("expected updated_at not to go backwards, before=%v after=%v", before.UpdatedAt, after.UpdatedAt)
|
||||
}
|
||||
|
||||
// Setting an empty port list + icmp:false is legal — it's the "disable
|
||||
// all inbound checks without touching sites" configuration.
|
||||
if err := d.SetInboundChecks(ctx, nil, false); err != nil {
|
||||
t.Fatalf("set empty inbound checks: %v", err)
|
||||
}
|
||||
cleared, err := d.GetInboundChecks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get inbound checks after clear: %v", err)
|
||||
}
|
||||
if len(cleared.Ports) != 0 || cleared.ICMP {
|
||||
t.Fatalf("expected {[] false} after clearing, got %+v", cleared)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user