import * as pulumi from "@pulumi/pulumi"; import * as command from "@pulumi/command"; // ============================================================================= // NetBird API Client using Pulumi Command Provider // ============================================================================= // Since there's no TypeScript SDK for NetBird, we use the command provider // to make API calls. This demonstrates the pattern while being practical. // // Note: Delete commands use grep/sed instead of jq for CI compatibility. // The pattern extracts the ID by finding the object with matching name. export interface NetBirdConfig { url: string; token: pulumi.Output; } export interface GroupArgs { name: string; peers?: string[]; } export interface PolicyRuleArgs { name: string; description?: string; enabled: boolean; sources: pulumi.Input[]; destinations: pulumi.Input[]; bidirectional: boolean; protocol: string; action: "accept" | "drop"; } export interface PolicyArgs { name: string; description?: string; enabled: boolean; rules: PolicyRuleArgs[]; } export interface SetupKeyArgs { name: string; type: "one-off" | "reusable"; autoGroups: pulumi.Input[]; usageLimit: number; expiresIn: number; ephemeral: boolean; } // ============================================================================= // Helper: Extract ID from JSON array by name (no jq dependency) // ============================================================================= // Uses grep/sed to find an object by name and extract its ID. // This is fragile but works for simple JSON structures. // Pattern: finds "name":"" then extracts preceding "id":"" function makeDeleteScript( endpoint: string, resourceName: string, token: pulumi.Output, url: string ): pulumi.Output { // Use Python for reliable JSON parsing (available in most CI environments) return pulumi.interpolate`python3 -c " import json, urllib.request, sys req = urllib.request.Request('${url}/api/${endpoint}', headers={'Authorization': 'Token ${token}'}) data = json.loads(urllib.request.urlopen(req).read()) matches = [x['id'] for x in data if x.get('name') == '${resourceName}'] if matches: print(matches[0]) else: sys.exit(0) # Not found, nothing to delete " | while read ID; do if [ -n "$ID" ]; then curl -s -X DELETE -H "Authorization: Token ${token}" "${url}/api/${endpoint}/$ID" fi done`; } // ============================================================================= // NetBird Group Resource // ============================================================================= export class Group extends pulumi.ComponentResource { public readonly id: pulumi.Output; public readonly name: string; constructor( name: string, args: GroupArgs, config: NetBirdConfig, opts?: pulumi.ComponentResourceOptions ) { super("netbird:custom:Group", name, {}, opts); const createCmd = new command.local.Command( `${name}-create`, { create: pulumi.interpolate`curl -s -X POST \ -H "Authorization: Token ${config.token}" \ -H "Content-Type: application/json" \ -d '{"name": "${args.name}", "peers": ${JSON.stringify(args.peers || [])}}' \ ${config.url}/api/groups`, delete: makeDeleteScript("groups", args.name, config.token, config.url), }, { parent: this } ); this.id = createCmd.stdout.apply((stdout): string => { try { const result = JSON.parse(stdout); return result.id || ""; } catch { return ""; } }); this.name = args.name; this.registerOutputs({ id: this.id, name: this.name, }); } } // ============================================================================= // NetBird Policy Resource // ============================================================================= export class Policy extends pulumi.ComponentResource { public readonly id: pulumi.Output; public readonly name: string; constructor( name: string, args: PolicyArgs, config: NetBirdConfig, opts?: pulumi.ComponentResourceOptions ) { super("netbird:custom:Policy", name, {}, opts); // Build the policy payload const rules = args.rules.map((rule) => ({ name: rule.name, description: rule.description || "", enabled: rule.enabled, sources: rule.sources, destinations: rule.destinations, bidirectional: rule.bidirectional, protocol: rule.protocol, action: rule.action, })); const createCmd = new command.local.Command( `${name}-create`, { create: pulumi.all([config.token, ...args.rules.flatMap(r => [...r.sources, ...r.destinations])]).apply( ([token, ...groupIds]) => { const payload = { name: args.name, description: args.description || "", enabled: args.enabled, rules: args.rules.map((rule, i) => ({ name: rule.name, description: rule.description || "", enabled: rule.enabled, sources: rule.sources, destinations: rule.destinations, bidirectional: rule.bidirectional, protocol: rule.protocol, action: rule.action, })), }; return `curl -s -X POST \ -H "Authorization: Token ${token}" \ -H "Content-Type: application/json" \ -d '${JSON.stringify(payload)}' \ ${config.url}/api/policies`; } ), delete: makeDeleteScript("policies", args.name, config.token, config.url), }, { parent: this } ); this.id = createCmd.stdout.apply((stdout): string => { try { const result = JSON.parse(stdout); return result.id || ""; } catch { return ""; } }); this.name = args.name; this.registerOutputs({ id: this.id, name: this.name, }); } } // ============================================================================= // NetBird Setup Key Resource // ============================================================================= export class SetupKey extends pulumi.ComponentResource { public readonly id: pulumi.Output; public readonly key: pulumi.Output; public readonly name: string; constructor( name: string, args: SetupKeyArgs, config: NetBirdConfig, opts?: pulumi.ComponentResourceOptions ) { super("netbird:custom:SetupKey", name, {}, opts); const createCmd = new command.local.Command( `${name}-create`, { create: pulumi.all([config.token, ...args.autoGroups]).apply( ([token, ...groupIds]) => { const payload = { name: args.name, type: args.type, auto_groups: groupIds, usage_limit: args.usageLimit, expires_in: args.expiresIn, ephemeral: args.ephemeral, }; return `curl -s -X POST \ -H "Authorization: Token ${token}" \ -H "Content-Type: application/json" \ -d '${JSON.stringify(payload)}' \ ${config.url}/api/setup-keys`; } ), delete: makeDeleteScript("setup-keys", args.name, config.token, config.url), }, { parent: this } ); this.id = createCmd.stdout.apply((stdout): string => { try { const result = JSON.parse(stdout); return result.id || ""; } catch { return ""; } }); this.key = createCmd.stdout.apply((stdout): string => { try { const result = JSON.parse(stdout); return result.key || ""; } catch { return ""; } }); this.name = args.name; this.registerOutputs({ id: this.id, key: this.key, name: this.name, }); } }