From ec37b32d4ac8432896490543d4b4a8bfe3781e88 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 11 Sep 2025 14:21:40 +0200 Subject: [PATCH 1/2] capver: generate test data too Signed-off-by: Kristoffer Dalby --- tools/capver/main.go | 193 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 186 insertions(+), 7 deletions(-) diff --git a/tools/capver/main.go b/tools/capver/main.go index 1e4512c1..2f564b9d 100644 --- a/tools/capver/main.go +++ b/tools/capver/main.go @@ -23,6 +23,7 @@ const ( releasesURL = "https://api.github.com/repos/tailscale/tailscale/releases" rawFileURL = "https://github.com/tailscale/tailscale/raw/refs/tags/%s/tailcfg/tailcfg.go" outputFile = "../../hscontrol/capver/capver_generated.go" + testFile = "../../hscontrol/capver/capver_test_data.go" ) type Release struct { @@ -63,14 +64,12 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) { rawURL := fmt.Sprintf(rawFileURL, version) resp, err := http.Get(rawURL) if err != nil { - log.Printf("Error fetching raw file for version %s: %v\n", version, err) continue } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading raw file for version %s: %v\n", version, err) continue } @@ -80,15 +79,49 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) { capabilityVersionStr := matches[1] capabilityVersion, _ := strconv.Atoi(capabilityVersionStr) versions[version] = tailcfg.CapabilityVersion(capabilityVersion) - } else { - log.Printf("Version: %s, CurrentCapabilityVersion not found\n", version) } } return versions, nil } -func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion) error { +func calculateMinSupportedCapabilityVersion(versions map[string]tailcfg.CapabilityVersion) tailcfg.CapabilityVersion { + // Get unique major.minor versions + majorMinorToCapVer := make(map[string]tailcfg.CapabilityVersion) + + for version, capVer := range versions { + // Remove 'v' prefix and split by '.' + cleanVersion := strings.TrimPrefix(version, "v") + parts := strings.Split(cleanVersion, ".") + if len(parts) >= 2 { + majorMinor := parts[0] + "." + parts[1] + // Keep the earliest (lowest) capver for each major.minor + if existing, exists := majorMinorToCapVer[majorMinor]; !exists || capVer < existing { + majorMinorToCapVer[majorMinor] = capVer + } + } + } + + // Sort major.minor versions + majorMinors := xmaps.Keys(majorMinorToCapVer) + sort.Strings(majorMinors) + + // Take the latest 10 versions + supportedCount := 10 + if len(majorMinors) < supportedCount { + supportedCount = len(majorMinors) + } + + if supportedCount == 0 { + return 90 // fallback + } + + // The minimum supported version is the oldest of the latest 10 + oldestSupportedMajorMinor := majorMinors[len(majorMinors)-supportedCount] + return majorMinorToCapVer[oldestSupportedMajorMinor] +} + +func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion, minSupportedCapVer tailcfg.CapabilityVersion) error { // Generate the Go code as a string var content strings.Builder content.WriteString("package capver\n\n") @@ -127,7 +160,12 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion for _, capVer := range capsSorted { fmt.Fprintf(&content, "\t%d:\t\t\"%s\",\n", capVer, capVarToTailscaleVer[capVer]) } - content.WriteString("}\n") + content.WriteString("}\n\n") + + // Add the MinSupportedCapabilityVersion constant + content.WriteString("// MinSupportedCapabilityVersion represents the minimum capability version\n") + content.WriteString("// supported by this Headscale instance (latest 10 minor versions)\n") + fmt.Fprintf(&content, "const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = %d\n", minSupportedCapVer) // Format the generated code formatted, err := format.Source([]byte(content.String())) @@ -144,6 +182,138 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion return nil } +func writeTestDataFile(versions map[string]tailcfg.CapabilityVersion, minSupportedCapVer tailcfg.CapabilityVersion) error { + // Get unique major.minor versions for test generation + majorMinorToCapVer := make(map[string]tailcfg.CapabilityVersion) + + for version, capVer := range versions { + cleanVersion := strings.TrimPrefix(version, "v") + parts := strings.Split(cleanVersion, ".") + if len(parts) >= 2 { + majorMinor := parts[0] + "." + parts[1] + if existing, exists := majorMinorToCapVer[majorMinor]; !exists || capVer < existing { + majorMinorToCapVer[majorMinor] = capVer + } + } + } + + // Sort major.minor versions + majorMinors := xmaps.Keys(majorMinorToCapVer) + sort.Strings(majorMinors) + + // Take latest 10 + supportedCount := 10 + if len(majorMinors) < supportedCount { + supportedCount = len(majorMinors) + } + + latest10 := majorMinors[len(majorMinors)-supportedCount:] + latest3 := majorMinors[len(majorMinors)-3:] + latest2 := majorMinors[len(majorMinors)-2:] + + // Generate test data file content + var content strings.Builder + content.WriteString("package capver\n\n") + content.WriteString("// Generated DO NOT EDIT\n\n") + content.WriteString("import \"tailscale.com/tailcfg\"\n\n") + + // Generate complete test struct for TailscaleLatestMajorMinor + content.WriteString("var tailscaleLatestMajorMinorTests = []struct {\n") + content.WriteString("\tn int\n") + content.WriteString("\tstripV bool\n") + content.WriteString("\texpected []string\n") + content.WriteString("}{\n") + + // Latest 3 with v prefix + content.WriteString("\t{3, false, []string{") + for i, version := range latest3 { + content.WriteString(fmt.Sprintf("\"v%s\"", version)) + if i < len(latest3)-1 { + content.WriteString(", ") + } + } + content.WriteString("}},\n") + + // Latest 2 without v prefix + content.WriteString("\t{2, true, []string{") + for i, version := range latest2 { + content.WriteString(fmt.Sprintf("\"%s\"", version)) + if i < len(latest2)-1 { + content.WriteString(", ") + } + } + content.WriteString("}},\n") + + // Latest 10 without v prefix (all supported) + content.WriteString("\t{10, true, []string{\n") + for _, version := range latest10 { + content.WriteString(fmt.Sprintf("\t\t\"%s\",\n", version)) + } + content.WriteString("\t}},\n") + + // Empty case + content.WriteString("\t{0, false, nil},\n") + content.WriteString("}\n\n") + + // Build capVerToTailscaleVer for test data + capVerToTailscaleVer := make(map[tailcfg.CapabilityVersion]string) + sortedVersions := xmaps.Keys(versions) + sort.Strings(sortedVersions) + for _, v := range sortedVersions { + cap := versions[v] + if _, ok := capVerToTailscaleVer[cap]; !ok { + capVerToTailscaleVer[cap] = v + } + } + + // Generate complete test struct for CapVerMinimumTailscaleVersion + content.WriteString("var capVerMinimumTailscaleVersionTests = []struct {\n") + content.WriteString("\tinput tailcfg.CapabilityVersion\n") + content.WriteString("\texpected string\n") + content.WriteString("}{\n") + + // Add minimum supported version + minVersionString := capVerToTailscaleVer[minSupportedCapVer] + content.WriteString(fmt.Sprintf("\t{%d, \"%s\"},\n", minSupportedCapVer, minVersionString)) + + // Add a few more test cases + capsSorted := xmaps.Keys(capVerToTailscaleVer) + sort.Slice(capsSorted, func(i, j int) bool { + return capsSorted[i] < capsSorted[j] + }) + + testCount := 0 + for _, capVer := range capsSorted { + if testCount >= 4 { // Limit to a few test cases + break + } + if capVer != minSupportedCapVer { // Don't duplicate the min version test + version := capVerToTailscaleVer[capVer] + content.WriteString(fmt.Sprintf("\t{%d, \"%s\"},\n", capVer, version)) + testCount++ + } + } + + // Edge cases + content.WriteString("\t{9001, \"\"}, // Test case for a version higher than any in the map\n") + content.WriteString("\t{60, \"\"}, // Test case for a version lower than any in the map\n") + content.WriteString("}\n") + + // Format the generated code + formatted, err := format.Source([]byte(content.String())) + if err != nil { + return fmt.Errorf("error formatting test data Go code: %w", err) + } + + // Write to file + err = os.WriteFile(testFile, formatted, 0644) + if err != nil { + return fmt.Errorf("error writing test data file: %w", err) + } + + return nil +} + func main() { versions, err := getCapabilityVersions() if err != nil { @@ -151,11 +321,20 @@ func main() { return } - err = writeCapabilityVersionsToFile(versions) + // Calculate the minimum supported capability version + minSupportedCapVer := calculateMinSupportedCapabilityVersion(versions) + + err = writeCapabilityVersionsToFile(versions, minSupportedCapVer) if err != nil { log.Println("Error writing to file:", err) return } + err = writeTestDataFile(versions, minSupportedCapVer) + if err != nil { + log.Println("Error writing test data file:", err) + return + } + log.Println("Capability versions written to", outputFile) } From fec338f19456a123f7dadcf5cc1c3e6689ea9350 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 11 Sep 2025 14:21:53 +0200 Subject: [PATCH 2/2] capver: generate Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 2 +- hscontrol/capver/capver.go | 7 +++-- hscontrol/capver/capver_generated.go | 33 +++++++++++++---------- hscontrol/capver/capver_test.go | 40 ++-------------------------- hscontrol/capver/capver_test_data.go | 40 ++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 55 deletions(-) create mode 100644 hscontrol/capver/capver_test_data.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e77eb3e8..c64ac204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Next -**Minimum supported Tailscale client version: v1.64.0** +**Minimum supported Tailscale client version: v1.68.0** ### Database integrity improvements diff --git a/hscontrol/capver/capver.go b/hscontrol/capver/capver.go index b6bbca5b..b471ebcc 100644 --- a/hscontrol/capver/capver.go +++ b/hscontrol/capver/capver.go @@ -12,8 +12,6 @@ import ( "tailscale.com/util/set" ) -const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 90 - // CanOldCodeBeCleanedUp is intended to be called on startup to see if // there are old code that can ble cleaned up, entries should contain // a CapVer where something can be cleaned up and a panic if it can. @@ -29,12 +27,14 @@ func CanOldCodeBeCleanedUp() { func tailscaleVersSorted() []string { vers := xmaps.Keys(tailscaleToCapVer) sort.Strings(vers) + return vers } func capVersSorted() []tailcfg.CapabilityVersion { capVers := xmaps.Keys(capVerToTailscaleVer) slices.Sort(capVers) + return capVers } @@ -48,6 +48,7 @@ func CapabilityVersion(ver string) tailcfg.CapabilityVersion { if !strings.HasPrefix(ver, "v") { ver = "v" + ver } + return tailscaleToCapVer[ver] } @@ -73,10 +74,12 @@ func TailscaleLatestMajorMinor(n int, stripV bool) []string { } majors := set.Set[string]{} + for _, vers := range tailscaleVersSorted() { if stripV { vers = strings.TrimPrefix(vers, "v") } + v := strings.Split(vers, ".") majors.Add(v[0] + "." + v[1]) } diff --git a/hscontrol/capver/capver_generated.go b/hscontrol/capver/capver_generated.go index 79590000..7f73f058 100644 --- a/hscontrol/capver/capver_generated.go +++ b/hscontrol/capver/capver_generated.go @@ -1,12 +1,10 @@ package capver -//Generated DO NOT EDIT +// Generated DO NOT EDIT import "tailscale.com/tailcfg" var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{ - "v1.64.0": 90, - "v1.64.1": 90, "v1.64.2": 90, "v1.66.0": 95, "v1.66.1": 95, @@ -35,18 +33,25 @@ var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{ "v1.84.0": 116, "v1.84.1": 116, "v1.84.2": 116, + "v1.86.0": 122, + "v1.86.2": 123, } - var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{ - 90: "v1.64.0", - 95: "v1.66.0", - 97: "v1.68.0", - 102: "v1.70.0", - 104: "v1.72.0", - 106: "v1.74.0", - 109: "v1.78.0", - 113: "v1.80.0", - 115: "v1.82.0", - 116: "v1.84.0", + 90: "v1.64.2", + 95: "v1.66.0", + 97: "v1.68.0", + 102: "v1.70.0", + 104: "v1.72.0", + 106: "v1.74.0", + 109: "v1.78.0", + 113: "v1.80.0", + 115: "v1.82.0", + 116: "v1.84.0", + 122: "v1.86.0", + 123: "v1.86.2", } + +// MinSupportedCapabilityVersion represents the minimum capability version +// supported by this Headscale instance (latest 10 minor versions) +const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 97 diff --git a/hscontrol/capver/capver_test.go b/hscontrol/capver/capver_test.go index 42f1df71..5c5d5b44 100644 --- a/hscontrol/capver/capver_test.go +++ b/hscontrol/capver/capver_test.go @@ -4,34 +4,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "tailscale.com/tailcfg" ) func TestTailscaleLatestMajorMinor(t *testing.T) { - tests := []struct { - n int - stripV bool - expected []string - }{ - {3, false, []string{"v1.80", "v1.82", "v1.84"}}, - {2, true, []string{"1.82", "1.84"}}, - // Lazy way to see all supported versions - {10, true, []string{ - "1.66", - "1.68", - "1.70", - "1.72", - "1.74", - "1.76", - "1.78", - "1.80", - "1.82", - "1.84", - }}, - {0, false, nil}, - } - - for _, test := range tests { + for _, test := range tailscaleLatestMajorMinorTests { t.Run("", func(t *testing.T) { output := TailscaleLatestMajorMinor(test.n, test.stripV) if diff := cmp.Diff(output, test.expected); diff != "" { @@ -42,19 +18,7 @@ func TestTailscaleLatestMajorMinor(t *testing.T) { } func TestCapVerMinimumTailscaleVersion(t *testing.T) { - tests := []struct { - input tailcfg.CapabilityVersion - expected string - }{ - {90, "v1.64.0"}, - {95, "v1.66.0"}, - {106, "v1.74.0"}, - {109, "v1.78.0"}, - {9001, ""}, // Test case for a version higher than any in the map - {60, ""}, // Test case for a version lower than any in the map - } - - for _, test := range tests { + for _, test := range capVerMinimumTailscaleVersionTests { t.Run("", func(t *testing.T) { output := TailscaleVersion(test.input) if output != test.expected { diff --git a/hscontrol/capver/capver_test_data.go b/hscontrol/capver/capver_test_data.go new file mode 100644 index 00000000..4c2a4c34 --- /dev/null +++ b/hscontrol/capver/capver_test_data.go @@ -0,0 +1,40 @@ +package capver + +// Generated DO NOT EDIT + +import "tailscale.com/tailcfg" + +var tailscaleLatestMajorMinorTests = []struct { + n int + stripV bool + expected []string +}{ + {3, false, []string{"v1.82", "v1.84", "v1.86"}}, + {2, true, []string{"1.84", "1.86"}}, + {10, true, []string{ + "1.68", + "1.70", + "1.72", + "1.74", + "1.76", + "1.78", + "1.80", + "1.82", + "1.84", + "1.86", + }}, + {0, false, nil}, +} + +var capVerMinimumTailscaleVersionTests = []struct { + input tailcfg.CapabilityVersion + expected string +}{ + {97, "v1.68.0"}, + {90, "v1.64.2"}, + {95, "v1.66.0"}, + {102, "v1.70.0"}, + {104, "v1.72.0"}, + {9001, ""}, // Test case for a version higher than any in the map + {60, ""}, // Test case for a version lower than any in the map +}