Skip to content

Instantly share code, notes, and snippets.

@raihankhan
Created October 20, 2025 09:24
Show Gist options
  • Select an option

  • Save raihankhan/81a3b072a2ebcc32a23ab5a70e4b3720 to your computer and use it in GitHub Desktop.

Select an option

Save raihankhan/81a3b072a2ebcc32a23ab5a70e4b3720 to your computer and use it in GitHub Desktop.
Netbird_resource_ingestor
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"time"
)
/*
Production-ready example: create a Netbird network, create a resource with a specific IP,
and assign that resource to a group.
Notes:
- The exact control-plane API paths and payloads may differ depending on the Netbird deployment/version.
This program uses sensible defaults for REST endpoints:
- POST {baseURL}/api/v1/networks
- POST {baseURL}/api/v1/resources
- GET {baseURL}/api/v1/groups
- POST {baseURL}/api/v1/groups/{group_id}/resources (to assign)
If your control plane uses different endpoints or GraphQL/gRPC, adapt the paths/payloads
by setting the *_PATH environment variables below or changing the payload composition.
- Authenticate using a bearer token provided in NETBIRD_API_TOKEN environment variable.
- Use the environment variables documented in `main()` for configuration.
- The code uses retries with exponential backoff for idempotent operations (configurable).
- The code does basic validation (e.g., IP address).
*/
// Default endpoints (adjust if your control plane differs)
const (
defaultNetworksPath = "/api/v1/networks"
defaultResourcesPath = "/api/v1/resources"
defaultGroupsPath = "/api/v1/groups"
defaultGroupAssignFormat = "/api/v1/groups/%s/resources" // POST with payload to assign resource
)
// Config holds runtime configuration loaded from environment
type Config struct {
BaseURL string
APIToken string
NetworkName string
NetworkDesc string
ResourceName string
ResourceIP string
GroupName string // used to lookup group
GroupID string // can be provided directly; if set, will be used
NetworksPath string
ResourcesPath string
GroupsPath string
GroupAssign string
HTTPTimeout time.Duration
MaxRetries int
}
func main() {
ctx := context.Background()
// Load config from environment
cfg := loadConfigFromEnv()
logger := log.New(os.Stdout, "", log.LstdFlags|log.Lmsgprefix)
logger.SetPrefix("[netbird-client] ")
// Basic validation
if cfg.BaseURL == "" || cfg.APIToken == "" {
logger.Fatal("NETBIRD_BASE_URL and NETBIRD_API_TOKEN MUST be set")
}
if cfg.NetworkName == "" || cfg.ResourceName == "" || cfg.ResourceIP == "" || (cfg.GroupName == "" && cfg.GroupID == "") {
logger.Fatal("NETWORK_NAME, RESOURCE_NAME, RESOURCE_IP and either GROUP_NAME or GROUP_ID must be provided")
}
if net.ParseIP(cfg.ResourceIP) == nil {
logger.Fatalf("RESOURCE_IP (%s) is not a valid IP address", cfg.ResourceIP)
}
httpClient := &http.Client{
Timeout: cfg.HTTPTimeout,
}
// Create network
logger.Printf("Creating network %q...", cfg.NetworkName)
networkID, err := createNetwork(ctx, httpClient, cfg, logger)
if err != nil {
logger.Fatalf("failed to create network: %v", err)
}
logger.Printf("Network created with ID: %s", networkID)
// Create resource in the network with the specific IP
logger.Printf("Creating resource %q with IP %s in network %s...", cfg.ResourceName, cfg.ResourceIP, networkID)
resourceID, err := createResource(ctx, httpClient, cfg, networkID, logger)
if err != nil {
logger.Fatalf("failed to create resource: %v", err)
}
logger.Printf("Resource created with ID: %s", resourceID)
// Determine group ID (if not provided)
groupID := cfg.GroupID
if groupID == "" {
logger.Printf("Looking up group %q...", cfg.GroupName)
foundID, err := getGroupIDByName(ctx, httpClient, cfg, cfg.GroupName, logger)
if err != nil {
logger.Fatalf("failed to find group: %v", err)
}
groupID = foundID
logger.Printf("Found group ID: %s", groupID)
}
// Assign resource to group
logger.Printf("Assigning resource %s to group %s...", resourceID, groupID)
if err := assignResourceToGroup(ctx, httpClient, cfg, groupID, resourceID, logger); err != nil {
logger.Fatalf("failed to assign resource to group: %v", err)
}
logger.Printf("Resource %s assigned to group %s successfully", resourceID, groupID)
}
// loadConfigFromEnv reads configuration from environment and returns a Config with defaults applied.
func loadConfigFromEnv() *Config {
base := os.Getenv("NETBIRD_BASE_URL")
if base != "" {
// trim trailing slash for consistent path building
base = strings.TrimRight(base, "/")
}
networksPath := getEnvOrDefault("NETBIRD_NETWORKS_PATH", defaultNetworksPath)
resourcesPath := getEnvOrDefault("NETBIRD_RESOURCES_PATH", defaultResourcesPath)
groupsPath := getEnvOrDefault("NETBIRD_GROUPS_PATH", defaultGroupsPath)
groupAssign := getEnvOrDefault("NETBIRD_GROUP_ASSIGN_PATH", defaultGroupAssignFormat)
// HTTP timeout and retry defaults (production-friendly)
timeout := 20 * time.Second
retries := 5
if v := os.Getenv("NETBIRD_HTTP_TIMEOUT_SECONDS"); v != "" {
if t, err := time.ParseDuration(v + "s"); err == nil {
timeout = t
}
}
if v := os.Getenv("NETBIRD_MAX_RETRIES"); v != "" {
if n, err := parseInt(v); err == nil && n >= 0 {
retries = n
}
}
return &Config{
BaseURL: base,
APIToken: os.Getenv("NETBIRD_API_TOKEN"),
NetworkName: os.Getenv("NETWORK_NAME"),
NetworkDesc: os.Getenv("NETWORK_DESCRIPTION"),
ResourceName: os.Getenv("RESOURCE_NAME"),
ResourceIP: os.Getenv("RESOURCE_IP"),
GroupName: os.Getenv("GROUP_NAME"),
GroupID: os.Getenv("GROUP_ID"),
NetworksPath: networksPath,
ResourcesPath: resourcesPath,
GroupsPath: groupsPath,
GroupAssign: groupAssign,
HTTPTimeout: timeout,
MaxRetries: retries,
}
}
func getEnvOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func parseInt(s string) (int, error) {
var n int
_, err := fmt.Sscanf(s, "%d", &n)
return n, err
}
// doJSONRequest sends an HTTP request with JSON body and decodes the JSON response into out (if non-nil).
// Implements a simple retry with exponential backoff for idempotent operations.
func doJSONRequest(ctx context.Context, client *http.Client, method, url string, token string, body interface{}, out interface{}, maxRetries int) error {
var reqBody []byte
var err error
if body != nil {
reqBody, err = json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request body: %w", err)
}
}
backoffBase := 300 * time.Millisecond
attempt := 0
for {
attempt++
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(reqBody))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
// Retry on transient transport errors
if attempt <= maxRetries {
sleep := backoffBase * time.Duration(1<<(attempt-1))
time.Sleep(sleep)
continue
}
return fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
if out != nil {
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(out); err != nil {
return fmt.Errorf("decode response JSON: %w", err)
}
}
return nil
}
// Non-successful status codes
var errResp map[string]interface{}
dec := json.NewDecoder(resp.Body)
_ = dec.Decode(&errResp) // try to decode error payload, ignore decode errors
// Retry on server-side errors (5xx)
if resp.StatusCode >= 500 && attempt <= maxRetries {
sleep := backoffBase * time.Duration(1<<(attempt-1))
time.Sleep(sleep)
continue
}
// Build helpful error message
return fmt.Errorf("request failed: %s %s status=%d body=%v", method, url, resp.StatusCode, errResp)
}
}
// createNetwork attempts to create a network and returns the created network ID.
func createNetwork(ctx context.Context, client *http.Client, cfg *Config, logger *log.Logger) (string, error) {
url := cfg.BaseURL + cfg.NetworksPath
// Example payload - adjust fields to match your control plane API.
payload := map[string]interface{}{
"name": cfg.NetworkName,
"description": cfg.NetworkDesc,
}
var resp map[string]interface{}
if err := doJSONRequest(ctx, client, http.MethodPost, url, cfg.APIToken, payload, &resp, cfg.MaxRetries); err != nil {
return "", err
}
id := extractID(resp)
if id == "" {
// Sometimes the API returns { "network": { "id": "..." } }
if nested, ok := resp["network"].(map[string]interface{}); ok {
id = extractID(nested)
}
}
if id == "" {
logger.Printf("warning: response didn't include an id field; full response: %+v", resp)
return "", errors.New("no id in network creation response")
}
return id, nil
}
// createResource creates a resource attached to the given network and returns the resource ID.
// The payload includes an IP address for the resource, mapped according to a typical control-plane schema.
func createResource(ctx context.Context, client *http.Client, cfg *Config, networkID string, logger *log.Logger) (string, error) {
url := cfg.BaseURL + cfg.ResourcesPath
// Payload – adapt as necessary for your control plane. Many control planes accept "network_id" or "network" relation.
payload := map[string]interface{}{
"name": cfg.ResourceName,
"network_id": networkID,
"addresses": []string{cfg.ResourceIP}, // common field name: addresses or ips
"metadata": map[string]string{
"created_by": "netbird-go-client",
"time": time.Now().UTC().Format(time.RFC3339),
},
}
var resp map[string]interface{}
if err := doJSONRequest(ctx, client, http.MethodPost, url, cfg.APIToken, payload, &resp, cfg.MaxRetries); err != nil {
return "", err
}
id := extractID(resp)
// Fallback: resp["resource"].(map)["id"]
if id == "" {
if nested, ok := resp["resource"].(map[string]interface{}); ok {
id = extractID(nested)
}
}
if id == "" {
logger.Printf("warning: response didn't include an id field; full response: %+v", resp)
return "", errors.New("no id in resource creation response")
}
return id, nil
}
// getGroupIDByName retrieves the list of groups and returns the ID for the provided group name.
// This function paginates if necessary (basic single-page implementation).
func getGroupIDByName(ctx context.Context, client *http.Client, cfg *Config, groupName string, logger *log.Logger) (string, error) {
url := cfg.BaseURL + cfg.GroupsPath
var resp []map[string]interface{}
if err := doJSONRequest(ctx, client, http.MethodGet, url, cfg.APIToken, nil, &resp, cfg.MaxRetries); err != nil {
return "", err
}
for _, g := range resp {
// try common name keys
if name, ok := g["name"].(string); ok && name == groupName {
if id := extractID(g); id != "" {
return id, nil
}
}
// sometimes group payloads use "display_name"
if name, ok := g["display_name"].(string); ok && name == groupName {
if id := extractID(g); id != "" {
return id, nil
}
}
}
return "", fmt.Errorf("group %q not found", groupName)
}
// assignResourceToGroup associates the resource with the given group.
// Expected endpoint: POST {baseURL}/api/v1/groups/{group_id}/resources
func assignResourceToGroup(ctx context.Context, client *http.Client, cfg *Config, groupID, resourceID string, logger *log.Logger) error {
// Compose assign URL. cfg.GroupAssign can be a format string with %s for group id.
assignPath := cfg.GroupAssign
if strings.Contains(assignPath, "%s") {
assignPath = fmt.Sprintf(assignPath, groupID)
} else {
// If user supplied a direct path, try to replace {group_id}
assignPath = strings.ReplaceAll(assignPath, "{group_id}", groupID)
}
url := cfg.BaseURL + assignPath
// Common payload: { "resource_id": "..." } or { "resources": ["id1", ...] }
payload := map[string]interface{}{
"resource_id": resourceID,
}
var resp map[string]interface{}
if err := doJSONRequest(ctx, client, http.MethodPost, url, cfg.APIToken, payload, &resp, cfg.MaxRetries); err != nil {
return err
}
// Optionally validate response for success
// If your API returns 204 No Content, doJSONRequest would have returned success already with no decoding.
return nil
}
// extractID extracts an "id" or "ID" or "Id" string from a generic JSON object.
func extractID(obj map[string]interface{}) string {
for _, key := range []string{"id", "ID", "Id"} {
if v, ok := obj[key]; ok {
switch t := v.(type) {
case string:
return t
case float64:
// some APIs encode numeric IDs
return fmt.Sprintf("%.0f", t)
}
}
}
return ""
}
/*
- A single, production-minded Go program (main.go) that:
- Reads configuration (endpoints, token, network/resource/group names) from environment variables.
- Creates a network via a POST to the control-plane networks endpoint.
- Creates a resource attached to that network with a specified IP address.
- Looks up the named group (or uses a provided group ID) and assigns the resource to that group.
- Uses a real HTTP client with timeout, retries and exponential backoff, and provides helpful logging.
- Includes defensive checks (IP validation, response ID extraction fallbacks) and clear comments.
How to run
1. Set environment variables (example):
- NETBIRD_BASE_URL=https://controlplane.example.com
- NETBIRD_API_TOKEN=eyJ...
- NETWORK_NAME="my-network"
- NETWORK_DESCRIPTION="Managed-by-go-client"
- RESOURCE_NAME="resource-01"
- RESOURCE_IP="10.100.0.5"
- GROUP_NAME="workers" (or set GROUP_ID directly)
- Optional: NETBIRD_NETWORKS_PATH, NETBIRD_RESOURCES_PATH, NETBIRD_GROUPS_PATH, NETBIRD_GROUP_ASSIGN_PATH
2. Build and run:
go build -o nb-client main.go
./nb-client
Notes and next steps
- Verify the control-plane API schema used by your Netbird installation. If it uses different field names or GraphQL/gRPC, adapt payloads and endpoints accordingly.
- For production, consider:
- Using a structured logger (e.g., zap, zerolog).
- Secrets management for the API token (not plain env vars).
- Better pagination/filters for group lookup if you have many groups.
- TLS config and CA verification customization if your control plane uses private certs.
- If you want, I can adapt this example to the exact Netbird control-plane schema (show me sample API request/response examples or the control-plane OpenAPI/Swagger schema and I'll generate exact payloads).
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment