4c8de033
server: a --cidr-pool that lost says so
a73x 2026-08-23 10:36
Commit message
internal/server/store/store.go
| Old | New | ||
|---|---|---|---|
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | "encoding/json" | 12 | "encoding/json" |
| 13 | "errors" | 13 | "errors" |
| 14 | "fmt" | 14 | "fmt" |
| 15 | "log/slog" | ||
| 15 | "net/netip" | 16 | "net/netip" |
| 16 | "os" | 17 | "os" |
| 17 | "path/filepath" | 18 | "path/filepath" |
| @@ -444,6 +445,25 @@ func Open(path, cidrPool string) (*Store, error) { | |||
| 444 | db.Close() | 445 | db.Close() |
| 445 | return nil, fmt.Errorf("seed cidr_pool: %w", err) | 446 | return nil, fmt.Errorf("seed cidr_pool: %w", err) |
| 446 | } | 447 | } |
| 448 | // The flag lost, so say so. A host holds the subnet it was allocated for | ||
| 449 | // life, which is why the stored pool wins and must — re-planning subnets | ||
| 450 | // under a fleet already routing on the old ones is not something a restart | ||
| 451 | // gets to do. But an operator who edited the flag deliberately otherwise | ||
| 452 | // sees a clean startup and a control plane that quietly kept the old value, | ||
| 453 | // and only learns the flag was inert when a newly-enrolled host lands on a | ||
| 454 | // subnet nobody expected. A warning rather than a refusal because a unit | ||
| 455 | // file that has already drifted must still be able to start the plane. | ||
| 456 | var stored string | ||
| 457 | if err := db.QueryRow(`SELECT value FROM meta WHERE key='cidr_pool'`).Scan(&stored); err != nil { | ||
| 458 | db.Close() | ||
| 459 | return nil, fmt.Errorf("read cidr_pool: %w", err) | ||
| 460 | } | ||
| 461 | if stored != cidrPool { | ||
| 462 | slog.Warn("--cidr-pool was ignored: this database was created with a different guest-subnet pool, "+ | ||
| 463 | "and hosts keep the subnet they were allocated, so the stored pool stands and every host "+ | ||
| 464 | "enrolled from now on is still allocated out of it", | ||
| 465 | "stored", stored, "passed", cidrPool) | ||
| 466 | } | ||
| 447 | 467 | ||
| 448 | s := &Store{db: db, dbDir: filepath.Dir(path)} | 468 | s := &Store{db: db, dbDir: filepath.Dir(path)} |
| 449 | 469 | ||
internal/server/store/store_test.go
| Old | New | ||
|---|---|---|---|
| @@ -1,8 +1,10 @@ | |||
| 1 | package store | 1 | package store |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | ||
| 4 | "context" | 5 | "context" |
| 5 | "database/sql" | 6 | "database/sql" |
| 7 | "log/slog" | ||
| 6 | "path/filepath" | 8 | "path/filepath" |
| 7 | "strings" | 9 | "strings" |
| 8 | "testing" | 10 | "testing" |
| @@ -1356,10 +1358,9 @@ func TestCidrPoolFirstOpenWins(t *testing.T) { | |||
| 1356 | require.Equal(t, testTenant, tn.ID) | 1358 | require.Equal(t, testTenant, tn.ID) |
| 1357 | require.NoError(t, first.Close()) | 1359 | require.NoError(t, first.Close()) |
| 1358 | 1360 | ||
| 1359 | // The operator restarts the server with a different pool. Note this is | 1361 | // The operator restarts the server with a different pool. It is ignored — |
| 1360 | // silently ignored, with no signal either way — a footgun in its own right, | 1362 | // but no longer in silence; TestChangedCidrPoolWarns owns the signal. What |
| 1361 | // tracked separately; what the test pins is that the fleet's subnets do not | 1363 | // this test pins is that the fleet's subnets do not move underneath it. |
| 1362 | // move underneath it. | ||
| 1363 | second, err := Open(path, "10.99.0.0/16") | 1364 | second, err := Open(path, "10.99.0.0/16") |
| 1364 | require.NoError(t, err) | 1365 | require.NoError(t, err) |
| 1365 | t.Cleanup(func() { second.Close() }) | 1366 | t.Cleanup(func() { second.Close() }) |
| @@ -1368,3 +1369,34 @@ func TestCidrPoolFirstOpenWins(t *testing.T) { | |||
| 1368 | assert.True(t, strings.HasPrefix(h.BridgeCIDR, "10.77."), | 1369 | assert.True(t, strings.HasPrefix(h.BridgeCIDR, "10.77."), |
| 1369 | "the cidr_pool is fixed at first Open: this host got %s, allocated from the pool passed at restart, so a fleet already routing 10.77.x has a peer on a subnet nothing routes to", h.BridgeCIDR) | 1370 | "the cidr_pool is fixed at first Open: this host got %s, allocated from the pool passed at restart, so a fleet already routing 10.77.x has a peer on a subnet nothing routes to", h.BridgeCIDR) |
| 1370 | } | 1371 | } |
| 1372 | |||
| 1373 | // TestChangedCidrPoolWarns pins the signal, not the behaviour: the stored pool | ||
| 1374 | // still wins (TestCidrPoolFirstOpenWins says why). What this asserts is that | ||
| 1375 | // the operator is told. Without the line, editing --cidr-pool produces a clean | ||
| 1376 | // startup, no complaint, and a plane that quietly kept the old value — and the | ||
| 1377 | // flag is discovered to have been inert only when a newly-enrolled host lands | ||
| 1378 | // on a subnet nobody routes to, or never. | ||
| 1379 | func TestChangedCidrPoolWarns(t *testing.T) { | ||
| 1380 | path := t.TempDir() + "/eitri.db" | ||
| 1381 | openLogging := func(t *testing.T, pool string) string { | ||
| 1382 | t.Helper() | ||
| 1383 | var buf bytes.Buffer | ||
| 1384 | prev := slog.Default() | ||
| 1385 | t.Cleanup(func() { slog.SetDefault(prev) }) | ||
| 1386 | slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))) | ||
| 1387 | s, err := Open(path, pool) | ||
| 1388 | require.NoError(t, err, "a drifted pool must never stop the plane booting: refusing would brick a "+ | ||
| 1389 | "restart for anyone whose unit file already carries one, and their fleet is routing fine") | ||
| 1390 | t.Cleanup(func() { s.Close() }) | ||
| 1391 | return buf.String() | ||
| 1392 | } | ||
| 1393 | |||
| 1394 | assert.Empty(t, openLogging(t, "10.77.0.0/16"), "the first Open is what seeded the pool and has nothing to report") | ||
| 1395 | assert.Empty(t, openLogging(t, "10.77.0.0/16"), "a restart passing the pool that is already stored is not a misconfiguration") | ||
| 1396 | |||
| 1397 | out := openLogging(t, "10.99.0.0/16") | ||
| 1398 | assert.Contains(t, out, "--cidr-pool was ignored", | ||
| 1399 | "a restart with a changed pool must say the flag did nothing, or the operator goes on believing it took") | ||
| 1400 | assert.Contains(t, out, "10.77.0.0/16", "the warning must name the pool that actually governs") | ||
| 1401 | assert.Contains(t, out, "10.99.0.0/16", "and the one that was discarded") | ||
| 1402 | } | ||