milestone 20: declarative configuration for iac
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
//! `listClients` returns only `hand_edited = 1` rows. A client the server
|
||||
//! materialised from live traffic is runtime state, not configuration, and must
|
||||
//! not appear in an export. `hand_edited` is the only marker of operator intent
|
||||
//! in this table, so it also decides what `import.isEmpty` counts: a database
|
||||
//! carrying nothing but materialised rows has never been configured, and a seed
|
||||
//! file must still be able to fill it. `countClients` counts **all** rows and is
|
||||
//! a test helper — it deliberately does not answer that question.
|
||||
//! in this table, so it also decides what the reconcile engine may delete: a
|
||||
//! declared row the file drops is removed, an observed row is kept whatever the
|
||||
//! file says, and declaring an observed address promotes that row in place.
|
||||
//! `countClients` counts **all** rows and is a test helper.
|
||||
//!
|
||||
//! The import path is list / insert / deleteAll / count, plus the two runtime
|
||||
//! calls `upsertSeen` and `pruneStale` that `server/clients.zig`'s tracker owns.
|
||||
//! The configuration path is list / insert / update / delete / count, plus the
|
||||
//! two runtime calls `upsertSeen` and `pruneStale` that `server/clients.zig`'s
|
||||
//! tracker owns.
|
||||
//! The REST surface is the third section: it speaks row ids and shows
|
||||
//! every client, materialised ones included.
|
||||
|
||||
@@ -126,7 +127,7 @@ pub fn deleteAllClients(database: *db.Db) db.Error!void {
|
||||
}
|
||||
|
||||
/// Counts every row, including the materialised ones `listClients` filters out.
|
||||
/// Used by tests; `import.isEmpty` counts operator intent instead.
|
||||
/// Used by tests.
|
||||
pub fn countClients(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM clients");
|
||||
}
|
||||
@@ -407,6 +408,67 @@ pub fn replaceClientPrefixes(database: *db.Db, items: []const ClientPrefixInput)
|
||||
try tx.commit();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reconcile surface (milestone 20)
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// `replaceClientPrefixes` above is the REST list resource: one atomic swap of
|
||||
// the whole table, in a transaction of its own. The reconcile engine cannot use
|
||||
// it — it runs inside a transaction already, and rewriting every row would
|
||||
// forfeit the row ids and the zero-writes property the engine exists for — so
|
||||
// it edits and removes prefixes one at a time instead.
|
||||
|
||||
/// Writes the two columns a prefix row carries besides its identity.
|
||||
///
|
||||
/// `error.NotFound`: no prefix holds `id`. `error.Constraint`:
|
||||
/// `client_prefixes.prefix` is UNIQUE, or `group_id` names no group.
|
||||
pub fn updateClientPrefix(database: *db.Db, id: i64, item: ClientPrefixInput) db.Error!void {
|
||||
var stmt = try database.prepare(
|
||||
"UPDATE client_prefixes SET prefix = ?2, group_id = ?3, priority = ?4 WHERE id = ?1",
|
||||
);
|
||||
defer stmt.deinit();
|
||||
try stmt.bindInt(1, id);
|
||||
try stmt.bindText(2, item.prefix);
|
||||
try stmt.bindInt(3, item.group_id);
|
||||
try stmt.bindInt(4, item.priority);
|
||||
return crud.execStrict(database, &stmt);
|
||||
}
|
||||
|
||||
/// `error.NotFound`: no prefix holds `id`. Nothing references
|
||||
/// `client_prefixes`, so a delete cannot violate a constraint.
|
||||
pub fn deleteClientPrefix(database: *db.Db, id: i64) db.Error!void {
|
||||
var stmt = try database.prepare("DELETE FROM client_prefixes WHERE id = ?1");
|
||||
defer stmt.deinit();
|
||||
try stmt.bindInt(1, id);
|
||||
return crud.execStrict(database, &stmt);
|
||||
}
|
||||
|
||||
/// Moves the observed clients of one group to another, and reports how many
|
||||
/// rows moved.
|
||||
///
|
||||
/// `clients.group_id` references `groups(id)` with no `ON DELETE` action
|
||||
/// (config_schema.zig:26), so a group that any client still sits in cannot be
|
||||
/// deleted. When a configuration stops declaring a group, its *declared*
|
||||
/// clients go with it, but the devices the DNS path materialised into it did
|
||||
/// not come from the configuration and must not be deleted for a decision that
|
||||
/// was never about them. They move to the default group, which is also the
|
||||
/// semantics the operator asked for: they un-declared the group, not the
|
||||
/// devices.
|
||||
///
|
||||
/// `hand_edited = 1` rows are untouched — those are configuration, and the
|
||||
/// reconcile engine has already accounted for them.
|
||||
pub fn reassignObservedClients(database: *db.Db, from_group_id: i64, to_group_id: i64) db.Error!u32 {
|
||||
var stmt = try database.prepare(
|
||||
"UPDATE clients SET group_id = ?2 WHERE hand_edited = 0 AND group_id = ?1",
|
||||
);
|
||||
defer stmt.deinit();
|
||||
try stmt.bindInt(1, from_group_id);
|
||||
try stmt.bindInt(2, to_group_id);
|
||||
try stmt.exec();
|
||||
const moved = database.changes();
|
||||
return @intCast(@min(moved, std.math.maxInt(u32)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user