From 8394e7094a4201456071885aed63e282dd5e0bf6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 12 Nov 2025 13:26:54 -0600 Subject: [PATCH] capver: update latest (#2774) --- .gitignore | 1 + 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 | 39 +++++ integration/scenario.go | 2 +- tools/capver/main.go | 239 +++++++++++++++++++++++++-- 8 files changed, 296 insertions(+), 67 deletions(-) create mode 100644 hscontrol/capver/capver_test_data.go diff --git a/.gitignore b/.gitignore index 28d23c09..4fec4f53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ ignored/ tailscale/ .vscode/ .claude/ +logs/ *.prof diff --git a/CHANGELOG.md b/CHANGELOG.md index e75a8a50..d778ecf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next +**Minimum supported Tailscale client version: v1.74.0** + ### Web registration templates redesign The OIDC callback and device registration web pages have been updated to use the 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 534ead02..3747d3ed 100644 --- a/hscontrol/capver/capver_generated.go +++ b/hscontrol/capver/capver_generated.go @@ -5,15 +5,6 @@ package capver 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, - "v1.66.2": 95, - "v1.66.3": 95, - "v1.66.4": 95, - "v1.68.0": 97, "v1.68.1": 97, "v1.68.2": 97, "v1.70.0": 102, @@ -35,12 +26,19 @@ 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, + "v1.88.1": 125, + "v1.88.3": 125, + "v1.90.1": 130, + "v1.90.2": 130, + "v1.90.3": 130, + "v1.90.4": 130, + "v1.90.6": 130, } var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{ - 90: "v1.64.0", - 95: "v1.66.0", - 97: "v1.68.0", + 97: "v1.68.1", 102: "v1.70.0", 104: "v1.72.0", 106: "v1.74.0", @@ -48,4 +46,15 @@ var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{ 113: "v1.80.0", 115: "v1.82.0", 116: "v1.84.0", + 122: "v1.86.0", + 123: "v1.86.2", + 125: "v1.88.1", + 130: "v1.90.1", } + +// SupportedMajorMinorVersions is the number of major.minor Tailscale versions supported. +const SupportedMajorMinorVersions = 9 + +// MinSupportedCapabilityVersion represents the minimum capability version +// supported by this Headscale instance (latest 10 minor versions) +const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 106 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..176b2376 --- /dev/null +++ b/hscontrol/capver/capver_test_data.go @@ -0,0 +1,39 @@ +package capver + +// Generated DO NOT EDIT + +import "tailscale.com/tailcfg" + +var tailscaleLatestMajorMinorTests = []struct { + n int + stripV bool + expected []string +}{ + {3, false, []string{"v1.86", "v1.88", "v1.90"}}, + {2, true, []string{"1.88", "1.90"}}, + {9, true, []string{ + "1.74", + "1.76", + "1.78", + "1.80", + "1.82", + "1.84", + "1.86", + "1.88", + "1.90", + }}, + {0, false, nil}, +} + +var capVerMinimumTailscaleVersionTests = []struct { + input tailcfg.CapabilityVersion + expected string +}{ + {106, "v1.74.0"}, + {97, "v1.68.1"}, + {102, "v1.70.0"}, + {104, "v1.72.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 +} diff --git a/integration/scenario.go b/integration/scenario.go index c3b5549c..fc3ce44d 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -63,7 +63,7 @@ var ( // // The rest of the version represents Tailscale versions that can be // found in Tailscale's apt repository. - AllVersions = append([]string{"head", "unstable"}, capver.TailscaleLatestMajorMinor(10, true)...) + AllVersions = append([]string{"head", "unstable"}, capver.TailscaleLatestMajorMinor(capver.SupportedMajorMinorVersions, true)...) // MustTestVersions is the minimum set of versions we should test. // At the moment, this is arbitrarily chosen as: diff --git a/tools/capver/main.go b/tools/capver/main.go index cbb5435c..0c9066ba 100644 --- a/tools/capver/main.go +++ b/tools/capver/main.go @@ -20,9 +20,16 @@ import ( ) 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" + 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" + minVersionParts = 2 + fallbackCapVer = 90 + maxTestCases = 4 + // TODO(https://github.com/tailscale/tailscale/issues/12849): Restore to 10 when v1.92 is released. + supportedMajorMinorVersions = 9 + filePermissions = 0o600 ) type Release struct { @@ -43,6 +50,7 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) { } var releases []Release + err = json.Unmarshal(body, &releases) if err != nil { return nil, fmt.Errorf("error unmarshalling JSON: %w", err) @@ -61,16 +69,15 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) { // Fetch the raw Go file 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 +87,51 @@ 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) >= minVersionParts { + 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 := supportedMajorMinorVersions + if len(majorMinors) < supportedCount { + supportedCount = len(majorMinors) + } + + if supportedCount == 0 { + return fallbackCapVer + } + + // 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") @@ -99,35 +142,50 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion sortedVersions := xmaps.Keys(versions) sort.Strings(sortedVersions) + for _, version := range sortedVersions { fmt.Fprintf(&content, "\t\"%s\": %d,\n", version, versions[version]) } + content.WriteString("}\n") content.WriteString("\n\n") content.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n") capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string) + for _, v := range sortedVersions { - cap := versions[v] + capabilityVersion := versions[v] // If it is already set, skip and continue, // we only want the first tailscale vsion per // capability vsion. - if _, ok := capVarToTailscaleVer[cap]; ok { + if _, ok := capVarToTailscaleVer[capabilityVersion]; ok { continue } - capVarToTailscaleVer[cap] = v + + capVarToTailscaleVer[capabilityVersion] = v } capsSorted := xmaps.Keys(capVarToTailscaleVer) sort.Slice(capsSorted, func(i, j int) bool { return capsSorted[i] < capsSorted[j] }) + 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 SupportedMajorMinorVersions constant + content.WriteString("// SupportedMajorMinorVersions is the number of major.minor Tailscale versions supported.\n") + fmt.Fprintf(&content, "const SupportedMajorMinorVersions = %d\n\n", supportedMajorMinorVersions) + + // 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())) @@ -136,7 +194,7 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion } // Write to file - err = os.WriteFile(outputFile, formatted, 0o644) + err = os.WriteFile(outputFile, formatted, filePermissions) if err != nil { return fmt.Errorf("error writing file: %w", err) } @@ -144,6 +202,150 @@ 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) >= minVersionParts { + 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 := supportedMajorMinorVersions + 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 N without v prefix (all supported) + content.WriteString(fmt.Sprintf("\t{%d, true, []string{\n", supportedMajorMinorVersions)) + + 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 { + capabilityVersion := versions[v] + if _, ok := capVerToTailscaleVer[capabilityVersion]; !ok { + capVerToTailscaleVer[capabilityVersion] = 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 >= maxTestCases { + 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, filePermissions) + 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 +353,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) }