Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ playwright-report/
test-results/
site/
deploy/dev/.headscale-key
deploy/dev/.netbird-key
deploy/dev/.kube/

# Override global gitignore patterns (e.g. a personal `debug.*` rule)
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ dev: dev-up assets
echo "air not found on PATH; install with: go install github.com/air-verse/air@latest"; \
exit 1; \
}
@HEADSCALE_API_KEY="$$(cat deploy/dev/.headscale-key/apikey.txt)" air
@HEADSCALE_API_KEY="$$(cat deploy/dev/.headscale-key/apikey.txt)" \
NETBIRD_API_TOKEN="$$(cat deploy/dev/.netbird-key/token.txt)" air

.PHONY: dev-up
dev-up:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

Add a source and Compass renders what is exposed in a server-driven UI with source status, tags, metadata, and operator context. Search, filter, debug, and jump into services without maintaining a pile of hand-written cards.

- Docker, Kubernetes, Tailscale, Headscale, static config, and JSON API sources.
- Docker, Kubernetes, Tailscale, Headscale, NetBird, static config, and JSON API sources.
- Source health, refresh status, service metadata, panels, tags, and debug views.
- Server-rendered HTML with HTMX, Alpine.js, Tailwind, and daisyUI.

Expand Down
2 changes: 1 addition & 1 deletion cmd/compass/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var (
var rootCmd = &cobra.Command{
Use: "compass",
Short: "Service-discovery dashboard for homelabs",
Long: `Compass discovers services from your homelab (Docker, Kubernetes, Tailscale, Headscale, JSON APIs) and renders them with operational context.`,
Long: `Compass discovers services from your homelab (Docker, Kubernetes, Tailscale, Headscale, NetBird, JSON APIs) and renders them with operational context.`,
RunE: runCompass,
}

Expand Down
10 changes: 10 additions & 0 deletions compass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ services:
url_scheme: http
device_tags: [node]

# NetBird (Cloud or self-hosted). PAT from a NetBird service user.
- type: netbird
name: netbird
tags: [netbird, vpn]
netbird:
# api_url: https://netbird.example.com # self-hosted; omit for NetBird Cloud
api_token: ${NETBIRD_API_TOKEN}
url_scheme: http
device_tags: [node]

# Docker — container labels (or Traefik Host(...) rule fallback).
# - type: docker
# name: local
Expand Down
4 changes: 4 additions & 0 deletions deploy/dev/Dockerfile.netbird-init
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Shell + curl + jq for the netbird-init compose service, which drives
# NetBird's REST API to mint an owner PAT and a setup key.
FROM alpine:3.20
RUN apk add --no-cache curl jq
11 changes: 11 additions & 0 deletions deploy/dev/compass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ services:
url_scheme: http
device_tags: [node]

# Exercises the netbird source over REST. `make dev` exports
# NETBIRD_API_TOKEN from the netbird-init bootstrap output.
- type: netbird
name: netbird
tags: [netbird, vpn]
netbird:
api_url: http://localhost:8086
api_token: ${NETBIRD_API_TOKEN}
url_scheme: http
device_tags: [node]

# Talks to whatever cluster KUBECONFIG / ~/.kube/config points at.
# Exercises the kubeconfig code path. Skipped automatically when no
# kubeconfig is present.
Expand Down
108 changes: 107 additions & 1 deletion deploy/dev/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Dev stack for exercising every Compass source end-to-end.
#
# docker compose -f deploy/dev/docker-compose.yml up -d
# HEADSCALE_API_KEY=$(cat deploy/dev/.headscale-key/apikey.txt) go run ./cmd/compass -c deploy/dev/compass.yaml
# HEADSCALE_API_KEY=$(cat deploy/dev/.headscale-key/apikey.txt) \
# NETBIRD_API_TOKEN=$(cat deploy/dev/.netbird-key/token.txt) \
# go run ./cmd/compass -c deploy/dev/compass.yaml
#
# Compass runs on the host so it can read /var/run/docker.sock and
# reach Caddy's admin API on localhost:2019.
Expand Down Expand Up @@ -219,6 +221,109 @@ services:
fi
exec sleep infinity

# Self-hosted NetBird combined server. NB_SETUP_PAT_ENABLED lets
# netbird-init create the owner and get a PAT back in one unauthenticated
# /api/setup call, the same path NetBird's own e2e harness uses.
netbird:
image: netbirdio/netbird-server:0.81.3
command: ["--config", "/etc/netbird/config.yaml"]
restart: unless-stopped
environment:
NB_SETUP_PAT_ENABLED: "true"
NB_DISABLE_GEOLOCATION: "true"
ports:
- "8086:8080" # REST API consumed by the netbird source
volumes:
- ./netbird-config.yaml:/etc/netbird/config.yaml:ro
- netbird-data:/var/lib/netbird
healthcheck:
# The image is plain ubuntu (no curl/wget); bash's /dev/tcp is enough
# to see the healthcheck listener come up.
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/9000"]
interval: 2s
timeout: 2s
retries: 30
start_period: 3s

# One-shot bootstrap: owner + PAT, a `servers` group, and a reusable
# setup key that auto-assigns it. Writes the PAT and setup key into
# deploy/dev/.netbird-key so `make dev` can export NETBIRD_API_TOKEN and
# the demo peers can join. Sleeps afterwards so `compose up --wait` sees
# it running; the healthcheck gates the peers on the setup key existing.
netbird-init:
build:
context: .
dockerfile: Dockerfile.netbird-init
image: compass-dev-netbird-init:local
depends_on:
netbird:
condition: service_healthy
volumes:
- ./.netbird-key:/state
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
API=http://netbird:8080/api
TOKEN_FILE=/state/token.txt
SETUP_KEY_FILE=/state/setupkey.txt
until curl -fsS "$$API/instance" >/tmp/instance.json; do sleep 1; done
# setup_required is true only on a fresh netbird-data volume, so a
# cached PAT / setup key from a previous generation is stale.
if jq -e '.setup_required' /tmp/instance.json >/dev/null; then
rm -f "$$TOKEN_FILE" "$$SETUP_KEY_FILE"
curl -fsS -X POST "$$API/setup" -H 'Content-Type: application/json' \
-d '{"email":"dev@compass.local","password":"compass-dev-password","name":"Compass Dev","create_pat":true,"pat_expire_in":365}' \
| jq -r '.personal_access_token' > "$$TOKEN_FILE"
fi
if [ ! -s "$$TOKEN_FILE" ]; then
echo "netbird is set up but $$TOKEN_FILE is missing; reset with: docker compose -f deploy/dev/docker-compose.yml down -v" >&2
exit 1
fi
AUTH="Authorization: Token $$(cat "$$TOKEN_FILE")"
if [ ! -s "$$SETUP_KEY_FILE" ]; then
GROUP_ID=$$(curl -fsS -X POST "$$API/groups" -H "$$AUTH" -H 'Content-Type: application/json' \
-d '{"name":"servers"}' | jq -r '.id')
curl -fsS -X POST "$$API/setup-keys" -H "$$AUTH" -H 'Content-Type: application/json' \
-d "{\"name\":\"compass-dev\",\"type\":\"reusable\",\"expires_in\":31536000,\"auto_groups\":[\"$$GROUP_ID\"],\"usage_limit\":0}" \
| jq -r '.key' > "$$SETUP_KEY_FILE"
fi
# Compass runs on the host as the invoking user and must read the PAT.
chmod 644 "$$TOKEN_FILE" "$$SETUP_KEY_FILE"
echo "netbird bootstrap complete"
exec sleep infinity
healthcheck:
test: ["CMD", "test", "-s", "/state/setupkey.txt"]
interval: 2s
timeout: 2s
retries: 30

# Two demo peers so the netbird source has something to list. Netstack
# mode runs WireGuard in userspace without a TUN device, so no extra
# capabilities are needed.
netbird-peer-laptop: &netbird-peer
image: netbirdio/netbird:0.81.3
restart: unless-stopped
depends_on:
netbird-init:
condition: service_healthy
environment:
NB_MANAGEMENT_URL: http://netbird:8080
NB_HOSTNAME: demo-laptop
NB_USE_NETSTACK_MODE: "true"
volumes:
- ./.netbird-key:/state:ro
entrypoint: ["/bin/sh", "-c"]
command:
- NB_SETUP_KEY="$$(cat /state/setupkey.txt)" exec /usr/local/bin/netbird-entrypoint.sh

netbird-peer-server:
<<: *netbird-peer
environment:
NB_MANAGEMENT_URL: http://netbird:8080
NB_HOSTNAME: demo-server
NB_USE_NETSTACK_MODE: "true"

# Local k3s cluster for the kubernetes source's inline-credentials code
# path (cluster_url + bearer_token_file + cluster_ca_file). The
# kubeconfig source already in compass.yaml exercises the kubeconfig
Expand Down Expand Up @@ -306,4 +411,5 @@ services:
volumes:
headscale-socket:
headscale-data:
netbird-data:
k3s-data:
21 changes: 21 additions & 0 deletions deploy/dev/netbird-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Minimal NetBird combined server (management + signal + relay + STUN +
# embedded IdP) for the dev stack. Plain HTTP, no TLS: peers join over
# the compose network and only the REST API is exposed to the host.
server:
listenAddress: ":8080"
# Peers reach management/signal/relay at the compose service name.
exposedAddress: "http://netbird:8080"
healthcheckAddress: ":9000"
metricsPort: 9090
logLevel: "info"
logFile: "console"
authSecret: "compass-dev-relay-secret"
dataDir: "/var/lib/netbird"
disableAnonymousMetrics: true
disableGeoliteUpdate: true
auth:
# Only used for internal JWT validation; peers authenticate with a
# setup key and Compass with a PAT, so nothing hits this from outside.
issuer: "http://localhost:8080/oauth2"
store:
engine: "sqlite"
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ type-specific config:

| Field | Notes |
| ------------------ | -------------------------------------------------------------------- |
| `type` | Required. One of `static`, `docker`, `kubernetes`, `tailscale`, `headscale`, `api`. |
| `type` | Required. One of `static`, `docker`, `kubernetes`, `tailscale`, `headscale`, `netbird`, `dns_sd`, `api`. |
| `name` | Required. Used as the source instance name (shown in the UI and `/debug`). |
| `access.required_groups` | Optional. When set, services, details, command search entries, page shortcodes, and debug rows from this source are visible only to users in at least one listed group. Omit it or leave it empty for a public source. |
| `tags` | Applied to every service from this source. Per-service tags are appended after these. |
Expand Down Expand Up @@ -356,7 +356,7 @@ The binary takes two flags. Anything else lives in `compass.yaml`.
| `-c`, `--config` | `COMPASS_CONFIG` | `compass.yaml` | Path to the config file. Relative or absolute. |

Everything else, including provider credentials, Tailscale OAuth,
Headscale API keys, Docker host, and Kubernetes bearer tokens, is set in
Headscale API keys, NetBird access tokens, Docker host, and Kubernetes bearer tokens, is set in
`compass.yaml`, optionally with `${VAR}` interpolation so secrets come from
the process environment without templating tooling:

Expand Down
3 changes: 2 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ source something real to talk to:
| `traefik` + `whoami` | Lets the Docker source exercise its Traefik label fallback. |
| `consul` | Two services pre-registered from `consul-services.json`; exercises the `api` source's `items_mode: values`. |
| `headscale` + `headscale-init` | Self-hosted tailnet. The init sidecar bootstraps a user, two demo nodes, and an API key on first run. |
| `netbird` + `netbird-init` + `netbird-peer-*` | Self-hosted NetBird combined server. The init sidecar bootstraps an owner, a PAT, and a setup key on first run, and two userspace (netstack) peers join with that setup key. |

`deploy/dev/compass.yaml` then wires up six sources (static, docker, caddy
via api, consul via api, headscale, plus optional kubernetes/tailscale if
via api, consul via api, headscale, netbird, plus optional kubernetes/tailscale if
their creds are available) against that stack. After `make dev`, useful
URLs:

Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Multiple sources run side-by-side; the registry merges and deduplicates.
| Kubernetes | `kubernetes` | [Sources → Kubernetes](sources.md#kubernetes) |
| Tailscale | `tailscale` | [Sources → Tailscale](sources.md#tailscale) |
| Headscale | `headscale` | [Sources → Headscale](sources.md#headscale) |
| NetBird | `netbird` | [Sources → NetBird](sources.md#netbird) |
| Anything HTTP+JSON | `api` | [Sources → API source](sources.md#api-source) |
| Hand-curated | `static` | [Sources → Static](sources.md#static) |

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Add a source and Compass renders what is exposed in a server-driven UI with
source status, tags, metadata, and operator context. Search, filter, debug,
and jump into services without maintaining a pile of hand-written cards.

- Docker, Kubernetes, Tailscale, Headscale, static config, and JSON API
- Docker, Kubernetes, Tailscale, Headscale, NetBird, static config, and JSON API
sources.
- Source health, refresh status, service metadata, panels, tags, and debug
views.
Expand Down
54 changes: 52 additions & 2 deletions docs/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Where each source's keys live:
| `kubernetes` | Resource annotations (`enabled` is a label) |
| `tailscale` | Tailscale Service annotations |
| `headscale` | Native gRPC node fields; no annotation surface |
| `netbird` | Native REST peer fields; no annotation surface |
| `api` | `mapping.fields` map keys (values are `gjson` paths) |
| `dns_sd` | DNS SRV record fields; no annotation surface |
| `static` | Top-level YAML keys per service |
Expand Down Expand Up @@ -121,6 +122,7 @@ Source-side tag conventions:
- `docker` appends the Compose project name when present.
- `tailscale` appends Tailscale's own tags after stripping the `tag:` prefix.
- `headscale` applies configured source/device tags, appends node tags after stripping `tag:`, and marks offline nodes with `offline`.
- `netbird` applies configured source/device tags, appends the peer's NetBird group names (except the built-in `All`), and marks disconnected peers with `offline`.

#### Metadata

Expand All @@ -133,11 +135,14 @@ the detail page. URLs in the metadata table auto-render as links.
| `kubernetes` | `namespace`, `kind`, `labels`, route `hostnames` / `hostname` |
| `tailscale` | `name`, `tailnet`, `addresses`, `ports`, `tags`, device state fields |
| `headscale` | `id`, `name`, `given_name`, `addresses`, `online`, `last_seen`, route maps |
| `netbird` | `id`, `name`, `hostname`, `ip`, `dns_label`, `connected`, `os`, `version`, `groups`, `last_seen` |
| `api` | none |
| `static` | Whatever the user wrote under `metadata:` |

Sensitive identifiers such as API keys, bearer tokens, Tailscale machine
keys, and Headscale node/machine keys are not rendered as metadata.
keys, and Headscale node/machine keys are not rendered as metadata. The
`netbird` source also leaves out each peer's public connection IP,
location, and serial number.

#### Links

Expand Down Expand Up @@ -285,10 +290,12 @@ integration:
| `kubernetes` | Kubernetes API (HTTPRoute, GRPCRoute, Ingress) |
| `tailscale` | Tailscale API (devices by default, Services opt-in) |
| `headscale` | Headscale gRPC API (self-hosted tailnet nodes) |
| `netbird` | NetBird management REST API (Cloud or self-hosted peers) |

The split is intentional. First-class sources earn a dedicated Go client
when they need non-trivial logic: Kubernetes route expansion, Tailscale
OAuth refresh, Docker's Traefik-rule fallback, headscale's gRPC transport.
OAuth refresh, Docker's Traefik-rule fallback, headscale's gRPC transport,
NetBird's offline tagging and group-to-tag mapping.
Anything that's just "GET a JSON endpoint" stays in `api`; adding a source
type per recipe would balloon the codebase without buying anything.

Expand Down Expand Up @@ -629,6 +636,49 @@ A working dev example lives in `deploy/dev/compass.yaml`. The
`headscale-init` sidecar in `deploy/dev/docker-compose.yml` bootstraps a
demo user, two nodes, and an API key on `make dev-up`.

#### NetBird

Reads peers from a [NetBird][netbird] management server's REST API,
NetBird Cloud or self-hosted. Each peer becomes a service; its NetBird
groups become tags.

```yaml
- type: netbird
name: netbird
tags: [vpn, netbird]
netbird:
api_url: https://netbird.example.com # omit for NetBird Cloud
api_token: ${NETBIRD_API_TOKEN}
url_scheme: http # prefixed onto each peer's IP
include_devices: true # default true; peers become services
device_tags: [node] # appended to every peer's tags
```

[netbird]: https://netbird.io

Field reference:

| Field | Notes |
| ----------------- | ----- |
| `api_url` | Management server root. Falls back to `NETBIRD_API_URL` env, then `https://api.netbird.io` (NetBird Cloud). |
| `api_token` | Personal access token. Falls back to `NETBIRD_API_TOKEN` env. Use `${VAR}` interpolation. |
| `url_scheme` | Scheme prefixed onto each peer's NetBird IP (or DNS label when it has no IP). Defaults to `http`. |
| `tags` | Tags applied to every discovered peer. |
| `include_devices` | Toggle peer discovery. Defaults to `true`. |
| `device_tags` | Extra tags appended on top of `tags` for every peer. |

Create the token on a [service user][netbird-service-users] rather than a
personal account, so it survives people leaving and can be revoked on its
own. Compass only calls `GET /api/peers`, so a read-only service user is
enough.

[netbird-service-users]: https://docs.netbird.io/manage/public-api

A working dev example lives in `deploy/dev/compass.yaml`. The
`netbird-init` sidecar in `deploy/dev/docker-compose.yml` bootstraps an
owner, a personal access token, and a setup key on `make dev-up`. Two
demo peers then join with that setup key.

### Second-class

Second-class integrations use the generic `api` source. They still produce
Expand Down
1 change: 1 addition & 0 deletions internal/compass/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
SourceTypeDocker = "docker"
SourceTypeHeadscale = "headscale"
SourceTypeKubernetes = "kubernetes"
SourceTypeNetbird = "netbird"
SourceTypeStatic = "static"
SourceTypeTailscale = "tailscale"
)
Expand Down
Loading
Loading