[PR #6215] [client] add new --json / --yaml output flags to client commands #25096

Open
opened 2026-08-05 06:09:31 -04:00 by saavagebueno · 0 comments
Owner

Original Pull Request: https://github.com/netbirdio/netbird/pull/6215

State: closed
Merged: No


Describe your changes

Extends the structured-output pattern already used by netbird status (OutputOverview.JSON() / YAML()) to the rest of the client CLI surface that benefits from machine-readable output. Automation use cases (provisioning scripts, CI runners, OpenWRT-style packaging, NetworkManager integrations) no longer need to scrape free-form text or grep for English status messages.

All new flags follow the same pattern: --json / -j and --yaml / -y are registered per leaf command, marked mutually exclusive, and the default text path is preserved unchanged. Each command emits a typed struct from client/status so the on-the-wire schema is visible in one place.

New flags

  • netbird up — daemon-mode emits a final UpOutput (status: "connected" | "already_connected", profileName). When SSO is required, openURL emits an NDJSON SSOEvent (event: "sso_required", verificationUriComplete, userCode) before the final result, and skips browser-opening since automation contexts are typically headless. --json / --yaml combined with --foreground-mode is rejected upfront — foreground mode is a long-running logrus stream that doesn't fit a single-object contract.
  • netbird down — emits DownOutput (status: "Disconnected").
  • netbird version — emits VersionOutput (version). Run: converted to RunE: so marshal errors can propagate.
  • netbird deregister (alias logout) — emits DeregisterOutput (status: "deregistered", optional profileName when --profile was used).
  • netbird login — emits LoginOutput (status: "logged_in", profileName) on both daemon-mode and foreground-mode paths. SSO event flows through the same openURL gating as up.
  • netbird networks list — emits NetworksListOutput (networks: [{id, range, domains, resolvedIps, selected}]).
  • netbird networks select / deselect — emit NetworksMutationOutput (status: "selected" | "deselected", networks list, all: true when special all arg was used).
  • netbird forwarding list — emits ForwardingListOutput (rules: [{translatedAddress, translatedHostname, protocol, destinationPort, translatedPort}]). Sort moved out of the printer into a shared helper so JSON and text output have the same ordering. Port fields are *string with omitempty — when the underlying PortInfo oneof is unset, the field is omitted entirely rather than emitted as the human-readable sentinel "No port specified".
  • netbird profile list — emits ProfileListOutput (profiles: [{name, active}]).
  • netbird profile add / remove / select — emit ProfileMutationOutput (status: "added" | "removed" | "selected", profileName).
  • netbird state list — emits StateListOutput (states: [{name}]).
  • netbird debug bundle — emits DebugBundleOutput (path, optional uploadedKey when --upload-bundle is set).

Schema

All output structs live in client/status/status.go alongside the existing OutputOverview. Each struct exposes JSON() and YAML() methods that marshal via encoding/json and gopkg.in/yaml.v3 respectively — the same shape as the existing OutputOverview.JSON() / YAML(). Field tags use camelCase (consistent with OutputOverview).

Behavior preserved

  • All text-mode (no flag) output is byte-for-byte identical to before.
  • Errors still return through cobra and print plain-text to stderr — same as status and the rest of the CLI today. Structured error JSON is a deliberate follow-up, not part of this PR.
  • log.Errorf / log.Warnf writes via logrus continue to land on stderr, so stdout remains clean for JSON consumers.

Behavior changes worth noting

  • Empty-result list commands emit empty arrays under --json instead of the human-readable "No X available." message. e.g. netbird networks list --json with no networks now emits {"networks":[]}. This is intentional — zero-length is meaningful to automation — but is a behavior change for any caller that was checking text output.
  • openURL now suppresses browser-opening when --json/--yaml is set. Automation contexts are typically headless. The SSO event still carries the verification URL so the caller can surface it.
  • netbird up --json --foreground-mode returns an error. Foreground mode is a long-running logrus stream; supporting it would require a logrus JSON formatter swap and an event schema for every log category — out of scope.

Testing

Manual

# Connection lifecycle
netbird up --json
netbird up --yaml
netbird up --json --foreground-mode   # expect: error
netbird status --json                  # unchanged, existing behavior
netbird down --json

# Listing
netbird networks list --json
netbird networks list --json           # with zero networks: expect {"networks":[]}
netbird forwarding list --json
netbird forwarding list --yaml
netbird profile list --json
netbird state list --json
netbird version --json

# Mutations
netbird networks select route1 route2 --json
netbird networks select all --json     # expect "all":true, no "networks" field
netbird networks deselect all --json
netbird profile add work --json
netbird profile select work --json
netbird profile remove work --json
netbird deregister --json
netbird deregister --profile work --json

# Auth
netbird login --json                   # with setup key: one object
netbird login --json                   # SSO flow: NDJSON, sso_required then logged_in

# Debug bundle
netbird debug bundle --json
netbird debug bundle --upload-bundle --json  # expect uploadedKey field

Parser sanity

For each command above, pipe stdout through jq . and confirm it parses without error. Confirm stderr stays free of JSON pollution (e.g. logrus warnings going to stderr should not appear in jq output).

For NDJSON-shaped output (up --json with SSO, login --json with SSO), confirm each line parses as standalone JSON:

netbird login --json | while read -r line; do echo "$line" | jq .; done

Edge cases

  • --json and --yaml together: confirm cobra rejects via MarkFlagsMutuallyExclusive.
  • forwarding list with a rule that has no destination/translated port: confirm the field is omitted, not emitted as "No port specified".
  • networks list --json when daemon is not running: current behavior is plain-text error from cobra — confirm this matches status behavior (it does; addressing it across the CLI is tracked separately under netbirdio/netbird#2780).
  • up --json happy path with setup key: single object on stdout.
  • up --json SSO path: two objects on stdout (sso_required, then connected).
  • up --json "already connected" path: single object, status: "already_connected".

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Not needed yet, this is mostly for testing.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Out of scope (deliberate follow-ups)

  • Structured JSON errors (would require cmd.SilenceErrors = true and threading a JSON error path through every return fmt.Errorf(...)).
  • netbird trace (rich per-rule structured output, larger schema design).
  • netbird expose, netbird ssh, netbird capture, netbird service install/start/stop/... (interactive or long-running streams that don't fit the single-object contract).
  • The smaller debug subcommands (debug log level, debug for, debug persistence).
  • The schema-consistency fix for netbird status when daemon is unreachable (issue #2780 — separate proposal in progress).
  • Test coverage for the new flag paths (no tests added in this PR).
  • Documentation / --help text consistency pass (the original status flag description differs slightly from the new ones).

Summary by CodeRabbit

Release Notes

  • New Features
    • CLI commands now support JSON and YAML output formats via --json/-j and --yaml/-y flags
    • Applicable to: up, down, login, logout, networks, profile, state, version, debug bundle, and forwarding rules commands

Review Change Stack

**Original Pull Request:** https://github.com/netbirdio/netbird/pull/6215 **State:** closed **Merged:** No --- ## Describe your changes Extends the structured-output pattern already used by `netbird status` (`OutputOverview.JSON()` / `YAML()`) to the rest of the client CLI surface that benefits from machine-readable output. Automation use cases (provisioning scripts, CI runners, OpenWRT-style packaging, NetworkManager integrations) no longer need to scrape free-form text or `grep` for English status messages. All new flags follow the same pattern: `--json` / `-j` and `--yaml` / `-y` are registered per leaf command, marked mutually exclusive, and the default text path is preserved unchanged. Each command emits a typed struct from `client/status` so the on-the-wire schema is visible in one place. ### New flags - **`netbird up`** — daemon-mode emits a final `UpOutput` (`status: "connected" | "already_connected"`, `profileName`). When SSO is required, `openURL` emits an NDJSON `SSOEvent` (`event: "sso_required"`, `verificationUriComplete`, `userCode`) before the final result, and skips browser-opening since automation contexts are typically headless. `--json` / `--yaml` combined with `--foreground-mode` is rejected upfront — foreground mode is a long-running logrus stream that doesn't fit a single-object contract. - **`netbird down`** — emits `DownOutput` (`status: "Disconnected"`). - **`netbird version`** — emits `VersionOutput` (`version`). `Run:` converted to `RunE:` so marshal errors can propagate. - **`netbird deregister`** (alias `logout`) — emits `DeregisterOutput` (`status: "deregistered"`, optional `profileName` when `--profile` was used). - **`netbird login`** — emits `LoginOutput` (`status: "logged_in"`, `profileName`) on both daemon-mode and foreground-mode paths. SSO event flows through the same `openURL` gating as `up`. - **`netbird networks list`** — emits `NetworksListOutput` (`networks: [{id, range, domains, resolvedIps, selected}]`). - **`netbird networks select` / `deselect`** — emit `NetworksMutationOutput` (`status: "selected" | "deselected"`, `networks` list, `all: true` when special `all` arg was used). - **`netbird forwarding list`** — emits `ForwardingListOutput` (`rules: [{translatedAddress, translatedHostname, protocol, destinationPort, translatedPort}]`). Sort moved out of the printer into a shared helper so JSON and text output have the same ordering. Port fields are `*string` with `omitempty` — when the underlying `PortInfo` oneof is unset, the field is omitted entirely rather than emitted as the human-readable sentinel `"No port specified"`. - **`netbird profile list`** — emits `ProfileListOutput` (`profiles: [{name, active}]`). - **`netbird profile add` / `remove` / `select`** — emit `ProfileMutationOutput` (`status: "added" | "removed" | "selected"`, `profileName`). - **`netbird state list`** — emits `StateListOutput` (`states: [{name}]`). - **`netbird debug bundle`** — emits `DebugBundleOutput` (`path`, optional `uploadedKey` when `--upload-bundle` is set). ### Schema All output structs live in `client/status/status.go` alongside the existing `OutputOverview`. Each struct exposes `JSON()` and `YAML()` methods that marshal via `encoding/json` and `gopkg.in/yaml.v3` respectively — the same shape as the existing `OutputOverview.JSON()` / `YAML()`. Field tags use camelCase (consistent with `OutputOverview`). ### Behavior preserved - All text-mode (no flag) output is byte-for-byte identical to before. - Errors still return through cobra and print plain-text to stderr — same as `status` and the rest of the CLI today. Structured error JSON is a deliberate follow-up, not part of this PR. - `log.Errorf` / `log.Warnf` writes via logrus continue to land on stderr, so stdout remains clean for JSON consumers. ### Behavior changes worth noting - **Empty-result list commands emit empty arrays under `--json`** instead of the human-readable "No X available." message. e.g. `netbird networks list --json` with no networks now emits `{"networks":[]}`. This is intentional — zero-length is meaningful to automation — but is a behavior change for any caller that was checking text output. - **`openURL` now suppresses browser-opening when `--json`/`--yaml` is set.** Automation contexts are typically headless. The SSO event still carries the verification URL so the caller can surface it. - **`netbird up --json --foreground-mode` returns an error.** Foreground mode is a long-running logrus stream; supporting it would require a logrus JSON formatter swap and an event schema for every log category — out of scope. ## Testing ### Manual ```bash # Connection lifecycle netbird up --json netbird up --yaml netbird up --json --foreground-mode # expect: error netbird status --json # unchanged, existing behavior netbird down --json # Listing netbird networks list --json netbird networks list --json # with zero networks: expect {"networks":[]} netbird forwarding list --json netbird forwarding list --yaml netbird profile list --json netbird state list --json netbird version --json # Mutations netbird networks select route1 route2 --json netbird networks select all --json # expect "all":true, no "networks" field netbird networks deselect all --json netbird profile add work --json netbird profile select work --json netbird profile remove work --json netbird deregister --json netbird deregister --profile work --json # Auth netbird login --json # with setup key: one object netbird login --json # SSO flow: NDJSON, sso_required then logged_in # Debug bundle netbird debug bundle --json netbird debug bundle --upload-bundle --json # expect uploadedKey field ``` ### Parser sanity For each command above, pipe stdout through `jq .` and confirm it parses without error. Confirm stderr stays free of JSON pollution (e.g. logrus warnings going to stderr should not appear in `jq` output). For NDJSON-shaped output (`up --json` with SSO, `login --json` with SSO), confirm each line parses as standalone JSON: ```bash netbird login --json | while read -r line; do echo "$line" | jq .; done ``` ### Edge cases - [ ] `--json` and `--yaml` together: confirm cobra rejects via `MarkFlagsMutuallyExclusive`. - [ ] `forwarding list` with a rule that has no destination/translated port: confirm the field is omitted, not emitted as `"No port specified"`. - [ ] `networks list --json` when daemon is not running: current behavior is plain-text error from cobra — confirm this matches `status` behavior (it does; addressing it across the CLI is tracked separately under netbirdio/netbird#2780). - [ ] `up --json` happy path with setup key: single object on stdout. - [ ] `up --json` SSO path: two objects on stdout (sso_required, then connected). - [ ] `up --json` "already connected" path: single object, `status: "already_connected"`. ## Issue ticket number and link ## Stack <!-- branch-stack --> ### Checklist - [ ] Is it a bug fix - [ ] Is a typo/documentation fix - [X] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [X] Documentation is **not needed** for this change (explain why) Not needed yet, this is mostly for testing. ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: https://github.com/netbirdio/docs/pull/__ ## Out of scope (deliberate follow-ups) - Structured JSON errors (would require `cmd.SilenceErrors = true` and threading a JSON error path through every `return fmt.Errorf(...)`). - `netbird trace` (rich per-rule structured output, larger schema design). - `netbird expose`, `netbird ssh`, `netbird capture`, `netbird service install/start/stop/...` (interactive or long-running streams that don't fit the single-object contract). - The smaller debug subcommands (`debug log level`, `debug for`, `debug persistence`). - The schema-consistency fix for `netbird status` when daemon is unreachable (issue #2780 — separate proposal in progress). - Test coverage for the new flag paths (no tests added in this PR). - Documentation / `--help` text consistency pass (the original `status` flag description differs slightly from the new ones). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * CLI commands now support JSON and YAML output formats via `--json/-j` and `--yaml/-y` flags * Applicable to: up, down, login, logout, networks, profile, state, version, debug bundle, and forwarding rules commands <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/netbirdio/netbird/pull/6215?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
saavagebueno added the pull-request label 2026-08-05 06:09:31 -04:00
Sign in to join this conversation.
No Label pull-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#25096