cmd/tailscale: format empty cities and countries as hyphens (#16495)

When running `tailscale exit-node list`, an empty city or country name
should be displayed as a hyphen "-". However, this only happened when
there was no location at all. If a node provides a Hostinfo.Location,
then the list would display exactly what was provided.

This patch changes the listing so that empty cities and countries will
either render the provided name or "-".

Fixes #16500

Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2025-07-08 22:14:18 -07:00
committed by GitHub
parent a60e0caf6a
commit bad17a1bfa
2 changed files with 29 additions and 21 deletions

View File

@@ -131,7 +131,7 @@ func runExitNodeList(ctx context.Context, args []string) error {
for _, country := range filteredPeers.Countries { for _, country := range filteredPeers.Countries {
for _, city := range country.Cities { for _, city := range country.Cities {
for _, peer := range city.Peers { for _, peer := range city.Peers {
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer)) fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), cmp.Or(country.Name, "-"), cmp.Or(city.Name, "-"), peerStatus(peer))
} }
} }
} }
@@ -202,23 +202,16 @@ type filteredCity struct {
Peers []*ipnstate.PeerStatus Peers []*ipnstate.PeerStatus
} }
const noLocationData = "-"
var noLocation = &tailcfg.Location{
Country: noLocationData,
CountryCode: noLocationData,
City: noLocationData,
CityCode: noLocationData,
}
// filterFormatAndSortExitNodes filters and sorts exit nodes into // filterFormatAndSortExitNodes filters and sorts exit nodes into
// alphabetical order, by country, city and then by priority if // alphabetical order, by country, city and then by priority if
// present. // present.
//
// If an exit node has location data, and the country has more than // If an exit node has location data, and the country has more than
// one city, an `Any` city is added to the country that contains the // one city, an `Any` city is added to the country that contains the
// highest priority exit node within that country. // highest priority exit node within that country.
//
// For exit nodes without location data, their country fields are // For exit nodes without location data, their country fields are
// defined as '-' to indicate that the data is not available. // defined as the empty string to indicate that the data is not available.
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes { func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
// first get peers into some fixed order, as code below doesn't break ties // first get peers into some fixed order, as code below doesn't break ties
// and our input comes from a random range-over-map. // and our input comes from a random range-over-map.
@@ -229,7 +222,10 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
countries := make(map[string]*filteredCountry) countries := make(map[string]*filteredCountry)
cities := make(map[string]*filteredCity) cities := make(map[string]*filteredCity)
for _, ps := range peers { for _, ps := range peers {
loc := cmp.Or(ps.Location, noLocation) loc := ps.Location
if loc == nil {
loc = &tailcfg.Location{}
}
if filterBy != "" && !strings.EqualFold(loc.Country, filterBy) { if filterBy != "" && !strings.EqualFold(loc.Country, filterBy) {
continue continue
@@ -259,7 +255,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
} }
for _, country := range filteredExitNodes.Countries { for _, country := range filteredExitNodes.Countries {
if country.Name == noLocationData { if country.Name == "" {
// Countries without location data should not // Countries without location data should not
// be filtered further. // be filtered further.
continue continue

View File

@@ -74,10 +74,10 @@ func TestFilterFormatAndSortExitNodes(t *testing.T) {
want := filteredExitNodes{ want := filteredExitNodes{
Countries: []*filteredCountry{ Countries: []*filteredCountry{
{ {
Name: noLocationData, Name: "",
Cities: []*filteredCity{ Cities: []*filteredCity{
{ {
Name: noLocationData, Name: "",
Peers: []*ipnstate.PeerStatus{ Peers: []*ipnstate.PeerStatus{
ps[5], ps[5],
}, },
@@ -273,14 +273,20 @@ func TestSortByCountryName(t *testing.T) {
Name: "Zimbabwe", Name: "Zimbabwe",
}, },
{ {
Name: noLocationData, Name: "",
}, },
} }
sortByCountryName(fc) sortByCountryName(fc)
if fc[0].Name != noLocationData { want := []string{"", "Albania", "Sweden", "Zimbabwe"}
t.Fatalf("sortByCountryName did not order countries by alphabetical order, got %v, want %v", fc[0].Name, noLocationData) var got []string
for _, c := range fc {
got = append(got, c.Name)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("sortByCountryName did not order countries by alphabetical order (-want +got):\n%s", diff)
} }
} }
@@ -296,13 +302,19 @@ func TestSortByCityName(t *testing.T) {
Name: "Squamish", Name: "Squamish",
}, },
{ {
Name: noLocationData, Name: "",
}, },
} }
sortByCityName(fc) sortByCityName(fc)
if fc[0].Name != noLocationData { want := []string{"", "Goteborg", "Kingston", "Squamish"}
t.Fatalf("sortByCityName did not order cities by alphabetical order, got %v, want %v", fc[0].Name, noLocationData) var got []string
for _, c := range fc {
got = append(got, c.Name)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("sortByCityName did not order countries by alphabetical order (-want +got):\n%s", diff)
} }
} }