feat: add actual state fetcher with name/ID indexing

This commit is contained in:
Prox 2026-03-04 00:07:23 +02:00
parent ed12ccae77
commit 9742807f91
2 changed files with 203 additions and 0 deletions

125
src/state/actual.test.ts Normal file
View File

@ -0,0 +1,125 @@
import { assertEquals } from "@std/assert";
import { fetchActualState } from "./actual.ts";
import type {
NbDnsNameserverGroup,
NbGroup,
NbPeer,
NbPolicy,
NbRoute,
NbSetupKey,
} from "../netbird/types.ts";
/** Minimal mock NetBird client that returns predetermined data */
function mockClient(data: {
groups?: NbGroup[];
setupKeys?: NbSetupKey[];
peers?: NbPeer[];
policies?: NbPolicy[];
routes?: NbRoute[];
dns?: NbDnsNameserverGroup[];
}) {
return {
listGroups: () => Promise.resolve(data.groups ?? []),
listSetupKeys: () => Promise.resolve(data.setupKeys ?? []),
listPeers: () => Promise.resolve(data.peers ?? []),
listPolicies: () => Promise.resolve(data.policies ?? []),
listRoutes: () => Promise.resolve(data.routes ?? []),
listDnsNameserverGroups: () => Promise.resolve(data.dns ?? []),
};
}
Deno.test("fetchActualState builds name-to-id maps", async () => {
const actual = await fetchActualState(
mockClient({
groups: [
{ id: "g1", name: "pilots", peers_count: 0, peers: [], issued: "api" },
],
setupKeys: [
{
id: 1,
name: "Pilot-hawk-72",
type: "one-off",
key: "masked",
expires: "2026-04-01T00:00:00Z",
valid: true,
revoked: false,
used_times: 0,
state: "valid",
auto_groups: ["g1"],
usage_limit: 1,
},
],
}),
);
assertEquals(actual.groupsByName.get("pilots")?.id, "g1");
assertEquals(actual.setupKeysByName.get("Pilot-hawk-72")?.id, 1);
});
Deno.test("fetchActualState returns empty maps for empty input", async () => {
const actual = await fetchActualState(mockClient({}));
assertEquals(actual.groups.length, 0);
assertEquals(actual.groupsByName.size, 0);
assertEquals(actual.groupsById.size, 0);
assertEquals(actual.setupKeys.length, 0);
assertEquals(actual.peers.length, 0);
assertEquals(actual.policies.length, 0);
assertEquals(actual.routes.length, 0);
assertEquals(actual.dns.length, 0);
});
Deno.test("fetchActualState indexes all resource types", async () => {
const actual = await fetchActualState(
mockClient({
groups: [
{ id: "g1", name: "ops", peers_count: 1, peers: [{ id: "p1", name: "drone-1" }], issued: "api" },
],
peers: [
{
id: "p1", name: "drone-1", ip: "100.64.0.1", connected: true,
hostname: "drone-1", os: "linux", version: "0.28.0",
groups: [{ id: "g1", name: "ops" }], last_seen: "2026-03-01T00:00:00Z",
dns_label: "drone-1", login_expiration_enabled: false,
ssh_enabled: false, inactivity_expiration_enabled: false,
},
],
policies: [
{
id: "pol1", name: "allow-ops", description: "ops traffic",
enabled: true, rules: [],
},
],
routes: [
{
id: "r1", description: "lan", network_id: "lan-net",
enabled: true, network: "10.0.0.0/24", metric: 100,
masquerade: true, groups: ["g1"], keep_route: false,
},
],
dns: [
{
id: "d1", name: "internal-dns", description: "internal",
nameservers: [{ ip: "1.1.1.1", ns_type: "udp", port: 53 }],
enabled: true, groups: ["g1"], primary: true,
domains: ["internal."], search_domains_enabled: false,
},
],
}),
);
// Groups indexed both ways
assertEquals(actual.groupsByName.get("ops")?.id, "g1");
assertEquals(actual.groupsById.get("g1")?.name, "ops");
// Peers indexed both ways
assertEquals(actual.peersByName.get("drone-1")?.id, "p1");
assertEquals(actual.peersById.get("p1")?.name, "drone-1");
// Policies by name
assertEquals(actual.policiesByName.get("allow-ops")?.id, "pol1");
// Routes by network_id
assertEquals(actual.routesByNetworkId.get("lan-net")?.id, "r1");
// DNS by name
assertEquals(actual.dnsByName.get("internal-dns")?.id, "d1");
});

78
src/state/actual.ts Normal file
View File

@ -0,0 +1,78 @@
import type { NetbirdClient } from "../netbird/client.ts";
import type {
NbDnsNameserverGroup,
NbGroup,
NbPeer,
NbPolicy,
NbRoute,
NbSetupKey,
} from "../netbird/types.ts";
/** Indexed view of all current NetBird state */
export interface ActualState {
groups: NbGroup[];
groupsByName: Map<string, NbGroup>;
groupsById: Map<string, NbGroup>;
setupKeys: NbSetupKey[];
setupKeysByName: Map<string, NbSetupKey>;
peers: NbPeer[];
peersByName: Map<string, NbPeer>;
peersById: Map<string, NbPeer>;
policies: NbPolicy[];
policiesByName: Map<string, NbPolicy>;
routes: NbRoute[];
routesByNetworkId: Map<string, NbRoute>;
dns: NbDnsNameserverGroup[];
dnsByName: Map<string, NbDnsNameserverGroup>;
}
/**
* Subset of NetbirdClient needed for fetching state.
*
* Using a structural pick rather than the full class keeps this module
* testable with plain object mocks and avoids pulling in fetch/auth deps.
*/
type ClientLike = Pick<
NetbirdClient,
| "listGroups"
| "listSetupKeys"
| "listPeers"
| "listPolicies"
| "listRoutes"
| "listDnsNameserverGroups"
>;
/**
* Fetches all resource collections from the NetBird API in parallel and
* returns them with bidirectional name<->ID indexes for O(1) lookup
* during diff/reconciliation.
*/
export async function fetchActualState(
client: ClientLike,
): Promise<ActualState> {
const [groups, setupKeys, peers, policies, routes, dns] = await Promise.all([
client.listGroups(),
client.listSetupKeys(),
client.listPeers(),
client.listPolicies(),
client.listRoutes(),
client.listDnsNameserverGroups(),
]);
return {
groups,
groupsByName: new Map(groups.map((g) => [g.name, g])),
groupsById: new Map(groups.map((g) => [g.id, g])),
setupKeys,
setupKeysByName: new Map(setupKeys.map((k) => [k.name, k])),
peers,
peersByName: new Map(peers.map((p) => [p.name, p])),
peersById: new Map(peers.map((p) => [p.id, p])),
policies,
policiesByName: new Map(policies.map((p) => [p.name, p])),
routes,
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
dns,
dnsByName: new Map(dns.map((d) => [d.name, d])),
};
}