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