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) }