api/v2: return a device name as its FQDN

This commit is contained in:
Kristoffer Dalby committed 2026-11-02 17:46:54 +00:00
1 parent 822bcda76a
commit b26555ac4f
3 files changed
+27 -29

No files matched your search

+10 -4
View File
@@ -143,7 +143,7 @@ func registerDevices(api huma.API, b Backend) {
return nil, err
}
return &deviceOutput{Body: deviceFromView(node, in.Fields == "all")}, nil
return &deviceOutput{Body: deviceFromView(node, in.Fields == "all", b.Cfg.BaseDomain)}, nil
})
huma.Register(api, requireScope(huma.Operation{
@@ -167,7 +167,7 @@ func registerDevices(api huma.API, b Backend) {
out.Body.Devices = make([]Device, 0, nodes.Len())
for _, node := range nodes.All() {
out.Body.Devices = append(out.Body.Devices, deviceFromView(node, allFields))
out.Body.Devices = append(out.Body.Devices, deviceFromView(node, allFields, b.Cfg.BaseDomain))
}
return out, nil
@@ -429,12 +429,18 @@ func parseRoutes(routes []string) ([]netip.Prefix, error) {
// deviceFromView maps a Headscale node onto the Tailscale Device, reading
// through the [types.NodeView] accessors. allFields gates the route slices,
// which Tailscale only returns for fields=all.
func deviceFromView(view types.NodeView, allFields bool) Device {
func deviceFromView(view types.NodeView, allFields bool, baseDomain string) Device {
id := view.StringID()
// Tailscale names a device by its MagicDNS FQDN, without the root dot.
name := view.GivenName()
if baseDomain != "" {
name += "." + baseDomain
}
d := Device{
Addresses: emptyIfNil(view.IPsAsString()),
Name: view.GivenName(),
Name: name,
ID: id,
NodeID: id,
Authorized: true, // Headscale has no post-registration de-auth state.
+17
View File
@@ -146,6 +146,23 @@ func TestAPIv2Device_Get(t *testing.T) {
assert.Equal(t, n.IPsAsString(), dev.Addresses)
}
// TestAPIv2Device_NameIsFQDN: with a base domain, a device's name is its
// MagicDNS FQDN, in a read and in the list.
func TestAPIv2Device_NameIsFQDN(t *testing.T) {
e := newDeviceTestEnv(t)
e.app.cfg.BaseDomain = "tailnet.example.com"
assert.Equal(t, "contract-dut.tailnet.example.com", getDevice(t, e.api, e.deviceID).Name)
resp := e.api.Get("/api/v2/tailnet/-/devices")
require.Equalf(t, http.StatusOK, resp.Code, "body: %s", resp.Body)
var list struct{ Devices []apiv2.Device }
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &list))
require.Len(t, list.Devices, 1)
assert.Equal(t, "contract-dut.tailnet.example.com", list.Devices[0].Name)
}
func TestAPIv2Device_Get_UnknownID_404(t *testing.T) {
e := newDeviceTestEnv(t)
-25
View File
@@ -31,30 +31,6 @@ import (
// <test_id>/<step>/<node or action index>/<check>. A listed check that
// matches fails, so a fix must delete its entry.
var nameSkipReasons = map[string]string{
"names/N1/domain": v2NameBare,
"names/N2/domain": v2NameBare,
"names/N3/domain": v2NameBare,
"names/N4/domain": v2NameBare,
"names/N5/domain": v2NameBare,
"names/N6/domain": v2NameBare,
"names/N7/domain": v2NameBare,
"names/N8/domain": v2NameBare,
"names/N9a/domain": v2NameBare,
"names/N9b/domain": v2NameBare,
"names/N10/domain": v2NameBare,
"names/N11/domain": v2NameBare,
"names/N11b/domain": v2NameBare,
"names/N12a/domain": v2NameBare,
"names/N12b/domain": v2NameBare,
"names/N13/domain": v2NameBare,
"names-svc/S1/domain": v2NameBare,
"names-svc/S2/domain": v2NameBare,
"names-svc/S3/domain": v2NameBare,
"names-svc/S4/domain": v2NameBare,
"names-svc/S5/domain": v2NameBare,
"names-svc/S6/domain": v2NameBare,
"names-svc/S7/domain": v2NameBare,
"names/N11/pdb/label": adminNameFollowsHostname,
"names/N11b/pdb/label": adminNameFollowsHostname,
"names/N12a/pdb/label": adminNameFollowsHostname,
@@ -64,7 +40,6 @@ var nameSkipReasons = map[string]string{
const (
adminNameFollowsHostname = "ADMIN_NAME_FOLLOWS_HOSTNAME: an admin name equal to the hostname's label counts as derived from it, so a hostname change replaces it; SaaS keeps an admin name across hostname changes"
v2NameBare = "V2_NAME_BARE: headscale's v2 device name is the bare label; SaaS's is the FQDN under the tailnet domain"
)
// nameViewer is the node whose netmap every step captured.