You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: resolve inline review comments on CLI proposal
Drop jsonpath for jq, client-side filter only, note preauth prefix
exists, backfill server cleanup, --list-columns, tags/users list
mockups. Abandoned text struck through, not deleted.
@@ -189,9 +192,11 @@ Error: "node" matches multiple nodes, use the ID to select one:
189
192
190
193
Because IDs are checked first, a node that is literally _named_ "12" cannot be selected by name if a node with ID 12 exists — use its ID instead.
191
194
192
-
Preauth keys are today only addressable by full key or numeric ID. To make `<id or prefix>` work everywhere, keys get a short unique prefix that is shown in listings and can be used as a target.
195
+
~~Preauth keys are today only addressable by full key or numeric ID. To make `<id or prefix>` work everywhere, keys get a short unique prefix that is shown in listings and can be used as a target.~~
193
196
194
-
Human focused error messages is very valuable here.
197
+
Preauth keys already carry a `Prefix` (the bcrypt scheme, `PreAuthKey.Prefix`) — the same lookup mechanism as API keys. Nothing new to add to the data model; the work is CLI-side: show the prefix in listings and let the resolver accept it. Legacy plaintext keys created before the bcrypt migration have an empty prefix and stay addressable by ID.
198
+
199
+
Human focused error messages is very valuable here.
195
200
196
201
## More examples
197
202
@@ -225,8 +230,8 @@ headscale nodes routes set 12 "" # clear the approved set
# Exit nodes are enabled by setting both 0.0.0.0/0 and ::/0
228
-
# If either IPv4 and IPv6, exit, or exit-node is passed, then
229
-
# we enable exit nodes.
233
+
# If either IPv4 and IPv6, exit, or exit-node is passed, then
234
+
# we enable exit nodes.
230
235
headscale nodes routes approve 12 exit
231
236
headscale nodes routes approve 12 exit-node
232
237
headscale nodes routes approve 12 0.0.0.0/0
@@ -247,7 +252,7 @@ Users, auth, policy and oauth2:
247
252
headscale users list
248
253
headscale users delete <name>
249
254
250
-
# Only available for internal user management.
255
+
# Only available for internal user management.
251
256
# OIDC managed users causes error.
252
257
headscale users create <name> # optional fields are flags
253
258
headscale users set <name> # optional fields are flags
@@ -282,9 +287,9 @@ headscale api oauth2 delete <id or prefix>
282
287
283
288
## Outputs
284
289
285
-
We will support outputting `json` and a human readable table (jsonpath is layered on top of `json`, see below). Default output mode is always the human readable table. `json` always shows all data. `yaml` and `json-line` are dropped: `json` plus jsonpath covers machine consumption without a third format to document and test.
290
+
We will support outputting `json` and a human readable table ~~(jsonpath is layered on top of `json`, see below)~~. Default output mode is always the human readable table. `json` always shows all data. `yaml` and `json-line` are dropped: ~~`json` plus jsonpath~~`json` piped to `jq` covers machine consumption without a third format to document and test.
286
291
287
-
The human readable table is **not a stable interface** — its columns and formatting may change between releases. It already carries ANSI colour and, for list columns, embedded newlines, so it is unstable by construction. Anything scripted should use `--output json`, optionally with a jsonpath expression.
292
+
The human readable table is **not a stable interface** — its columns and formatting may change between releases. It already carries ANSI colour and, for list columns, embedded newlines, so it is unstable by construction. Anything scripted should use `--output json`, piped to `jq` when you need to extract fields.
288
293
289
294
Columns come in two shapes. Short, fixed-cardinality lists — a node's IP addresses are almost always one v4 and one v6 — render comma-joined on a single line, so rows stay one-per-node. Genuinely variable-length lists such as approved routes may stack one entry per line. Today both render newline-stacked, which produces the noisy alternating rows for IP addresses that we want to avoid.
290
295
@@ -296,7 +301,20 @@ Currently the table has a fixed set of columns that the user can not change. In
296
301
headscale nodes list --columns id,hostname,online,last-seen
297
302
```
298
303
299
-
COMMENT: we need a flag or something to list all the available columns.
304
+
Because the set is dynamic and type-specific, cobra's static `--help` cannot enumerate it. A `--list-columns` flag prints the available columns for that command and exits; the same list feeds shell completion for `--columns`.
305
+
306
+
```
307
+
$ headscale nodes list --list-columns
308
+
COLUMN DESCRIPTION
309
+
id Node ID
310
+
hostname Hostname reported by the node
311
+
given-name Name assigned in headscale
312
+
user Owning user (or tag)
313
+
ip Assigned IP addresses
314
+
online Whether the node is currently connected
315
+
last-seen Time of last contact
316
+
...
317
+
```
300
318
301
319
Column names are plain and match the table headers. kubectl uses `HEADER:.json.path` pairs for its `custom-columns`; our types are flat, so we do not need paths. `--filter` uses the same names.
302
320
@@ -308,21 +326,23 @@ The human readable mode supports a `--filter` flag which allows the user to filt
308
326
309
327
`--filter-mode` selects how values are matched: `contains` (default), `prefix`, or `fuzzy`. Fuzzy matching is powered by [sahilm/fuzzy](https://github.com/sahilm/fuzzy) — importing fzf's own algorithm package works, but it is an app-internal API and drags fzf's TUI dependencies into go.sum.
310
328
311
-
kubectl has no client-side filter — its docs point at `jq` and `grep`, and its server-side field selectors are a hardcoded allowlist per resource. Our lists are small, so we filter client side. Whether the list APIs should also grow filter parameters so API users get the same thing is an open question — it needs checking against Tailscale API compatibility.
329
+
kubectl has no client-side filter — its docs point at `jq` and `grep`, and its server-side field selectors are a hardcoded allowlist per resource. Our lists are small, so we filter client side. ~~Whether the list APIs should also grow filter parameters so API users get the same thing is an open question — it needs checking against Tailscale API compatibility.~~ Filtering is client-side only: Tailscale's v2 API — which we model — has no list-filter parameters, so we do not add server-side ones.
312
330
313
331
The implementation must be generic and work across all CLI commands and types with little faff.
314
332
315
-
### jsonpath output
333
+
### Extraction with `jq`
334
+
335
+
~~For JSON output, `--output` also accepts a jsonpath expression:~~
316
336
317
-
COMMENT: Lets drop this in favour of just recommending `jq` usage.
337
+
~~kubectl's jsonpath is its own dialect from before the standard existed. We use [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535) instead, via [theory/jsonpath](https://github.com/theory/jsonpath) (MIT, actively maintained). That meets the bar of getting it mostly for free: one library wired into the output path, and the expressions are the standard ones documented everywhere.~~
318
338
319
-
For JSON output, `--output` also accepts a jsonpath expression:
339
+
We do not build a jsonpath dialect into the CLI. `--output json` plus `jq` covers extraction without a dependency or a bespoke expression syntax to document:
320
340
321
341
```
322
-
headscale nodes list --output jsonpath='$.nodes[*].name'
342
+
headscale nodes list --output json | jq -r '.nodes[].name'
323
343
```
324
344
325
-
kubectl's jsonpath is its own dialect from before the standard existed. We use [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535) instead, via [theory/jsonpath](https://github.com/theory/jsonpath) (MIT, actively maintained). That meets the bar of getting it mostly for free: one library wired into the output path, and the expressions are the standard ones documented everywhere.
345
+
The docs carry a few `jq` recipes for the common cases (names, IPs, filtering by field).
326
346
327
347
## Informative feedback
328
348
@@ -331,7 +351,7 @@ We should focus on success and error messaging that is meaningful to humans.
331
351
For example, if a user sets the expiry of a node 2h30m into the future:
Node 12 (mynode) will now expire in 2h30m (2025-08-27 10:00:00 UTC)
336
356
```
337
357
@@ -346,12 +366,32 @@ Deleting something that still has a dependency in the database, lets say deletin
346
366
347
367
## Open questions
348
368
349
-
- Should `--filter` also be implemented server side so API users benefit too, and does that conflict with staying compatible with Tailscale's API?
350
-
COMMENT: Lets not do this for now. Tailscale's V2 API does not support this and that is the API we are modeling after.
351
-
- Preauth keys need a short prefix to be addressable without the full secret — confirm we're happy adding that.
352
-
COMMENT: we already have this? all the keys are based on the same code which uses a prefix for lookup.
353
-
- Should `headscale nodes tags list` show which nodes are associated with each tag?
354
-
COMMENT: Maybe, in that case we have to be smarter, if there are 20+ nodes, it will look really difficult to interpret, so then we should truncate the result somehow.
355
-
In that case we could have a flag to also show nodes to list them all. Then we should have the same for users in a way I think. We need to mock the commands and returning UI for that.
369
+
-~~Should `--filter` also be implemented server side so API users benefit too, and does that conflict with staying compatible with Tailscale's API?~~ Resolved: no. Client-side only — Tailscale's v2 API has no list-filter parameters and we model that API.
370
+
-~~Preauth keys need a short prefix to be addressable without the full secret — confirm we're happy adding that.~~ Resolved: already done. `PreAuthKey.Prefix` exists (bcrypt scheme, same as API keys); only the CLI surfacing (show in listings, accept in the resolver) is outstanding.
371
+
372
+
### Listing related entities: `tags list` and `users list`
373
+
374
+
`tags list` and `users list` want to show associated nodes, but a raw dump gets unreadable past a handful. Default to a **count plus a truncated sample**, with `--expand` to show every node. Same pattern for both.
375
+
376
+
```
377
+
$ headscale nodes tags list
378
+
TAG NODES SAMPLE
379
+
tag:server 23 node0, node1, node2 … (+20)
380
+
tag:ci 2 builder0, builder1
381
+
tag:example 0
382
+
383
+
$ headscale nodes tags list --expand tag:server
384
+
TAG NODE USER
385
+
tag:server node0 kradalby
386
+
tag:server node1 kradalby
387
+
tag:server …
388
+
389
+
$ headscale users list
390
+
ID NAME NODES SAMPLE
391
+
1 kradalby 12 node0, node1, node2 … (+9)
392
+
2 juanfont 3 nodeA, nodeB, nodeC
393
+
```
394
+
395
+
Open: exact truncation width (fixed N vs terminal-width aware) and whether `--expand` takes an optional target to expand just one row.
356
396
357
397
[^1]: A tag is easily identifiable because it always starts with `tag:`.
0 commit comments