feat: add actual state fetcher with name/ID indexing
This commit is contained in:
parent
ed12ccae77
commit
9742807f91
125
src/state/actual.test.ts
Normal file
125
src/state/actual.test.ts
Normal 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
78
src/state/actual.ts
Normal 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])),
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user