capver: update latest (#2774)

This commit is contained in:
Kristoffer Dalby
2025-11-12 13:26:54 -06:00
committed by GitHub
parent da9018a0eb
commit 8394e7094a
8 changed files with 296 additions and 67 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ ignored/
tailscale/ tailscale/
.vscode/ .vscode/
.claude/ .claude/
logs/
*.prof *.prof

View File

@@ -2,6 +2,8 @@
## Next ## Next
**Minimum supported Tailscale client version: v1.74.0**
### Web registration templates redesign ### Web registration templates redesign
The OIDC callback and device registration web pages have been updated to use the The OIDC callback and device registration web pages have been updated to use the

View File

@@ -12,8 +12,6 @@ import (
"tailscale.com/util/set" "tailscale.com/util/set"
) )
const MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 90
// CanOldCodeBeCleanedUp is intended to be called on startup to see if // CanOldCodeBeCleanedUp is intended to be called on startup to see if
// there are old code that can ble cleaned up, entries should contain // 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. // a CapVer where something can be cleaned up and a panic if it can.
@@ -29,12 +27,14 @@ func CanOldCodeBeCleanedUp() {
func tailscaleVersSorted() []string { func tailscaleVersSorted() []string {
vers := xmaps.Keys(tailscaleToCapVer) vers := xmaps.Keys(tailscaleToCapVer)
sort.Strings(vers) sort.Strings(vers)
return vers return vers
} }
func capVersSorted() []tailcfg.CapabilityVersion { func capVersSorted() []tailcfg.CapabilityVersion {
capVers := xmaps.Keys(capVerToTailscaleVer) capVers := xmaps.Keys(capVerToTailscaleVer)
slices.Sort(capVers) slices.Sort(capVers)
return capVers return capVers
} }
@@ -48,6 +48,7 @@ func CapabilityVersion(ver string) tailcfg.CapabilityVersion {
if !strings.HasPrefix(ver, "v") { if !strings.HasPrefix(ver, "v") {
ver = "v" + ver ver = "v" + ver
} }
return tailscaleToCapVer[ver] return tailscaleToCapVer[ver]
} }
@@ -73,10 +74,12 @@ func TailscaleLatestMajorMinor(n int, stripV bool) []string {
} }
majors := set.Set[string]{} majors := set.Set[string]{}
for _, vers := range tailscaleVersSorted() { for _, vers := range tailscaleVersSorted() {
if stripV { if stripV {
vers = strings.TrimPrefix(vers, "v") vers = strings.TrimPrefix(vers, "v")
} }
v := strings.Split(vers, ".") v := strings.Split(vers, ".")
majors.Add(v[0] + "." + v[1]) majors.Add(v[0] + "." + v[1])
} }

View File

@@ -5,15 +5,6 @@ package capver
import "tailscale.com/tailcfg" import "tailscale.com/tailcfg"
var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{ 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.1": 97,
"v1.68.2": 97, "v1.68.2": 97,
"v1.70.0": 102, "v1.70.0": 102,
@@ -35,12 +26,19 @@ var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{
"v1.84.0": 116, "v1.84.0": 116,
"v1.84.1": 116, "v1.84.1": 116,
"v1.84.2": 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{ var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{
90: "v1.64.0", 97: "v1.68.1",
95: "v1.66.0",
97: "v1.68.0",
102: "v1.70.0", 102: "v1.70.0",
104: "v1.72.0", 104: "v1.72.0",
106: "v1.74.0", 106: "v1.74.0",
@@ -48,4 +46,15 @@ var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{
113: "v1.80.0", 113: "v1.80.0",
115: "v1.82.0", 115: "v1.82.0",
116: "v1.84.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

View File

@@ -4,34 +4,10 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"tailscale.com/tailcfg"
) )
func TestTailscaleLatestMajorMinor(t *testing.T) { func TestTailscaleLatestMajorMinor(t *testing.T) {
tests := []struct { for _, test := range tailscaleLatestMajorMinorTests {
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 {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
output := TailscaleLatestMajorMinor(test.n, test.stripV) output := TailscaleLatestMajorMinor(test.n, test.stripV)
if diff := cmp.Diff(output, test.expected); diff != "" { if diff := cmp.Diff(output, test.expected); diff != "" {
@@ -42,19 +18,7 @@ func TestTailscaleLatestMajorMinor(t *testing.T) {
} }
func TestCapVerMinimumTailscaleVersion(t *testing.T) { func TestCapVerMinimumTailscaleVersion(t *testing.T) {
tests := []struct { for _, test := range capVerMinimumTailscaleVersionTests {
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 {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
output := TailscaleVersion(test.input) output := TailscaleVersion(test.input)
if output != test.expected { if output != test.expected {

View File

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

View File

@@ -63,7 +63,7 @@ var (
// //
// The rest of the version represents Tailscale versions that can be // The rest of the version represents Tailscale versions that can be
// found in Tailscale's apt repository. // 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. // MustTestVersions is the minimum set of versions we should test.
// At the moment, this is arbitrarily chosen as: // At the moment, this is arbitrarily chosen as:

View File

@@ -20,9 +20,16 @@ import (
) )
const ( const (
releasesURL = "https://api.github.com/repos/tailscale/tailscale/releases" releasesURL = "https://api.github.com/repos/tailscale/tailscale/releases"
rawFileURL = "https://github.com/tailscale/tailscale/raw/refs/tags/%s/tailcfg/tailcfg.go" rawFileURL = "https://github.com/tailscale/tailscale/raw/refs/tags/%s/tailcfg/tailcfg.go"
outputFile = "../../hscontrol/capver/capver_generated.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 { type Release struct {
@@ -43,6 +50,7 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) {
} }
var releases []Release var releases []Release
err = json.Unmarshal(body, &releases) err = json.Unmarshal(body, &releases)
if err != nil { if err != nil {
return nil, fmt.Errorf("error unmarshalling JSON: %w", err) 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 // Fetch the raw Go file
rawURL := fmt.Sprintf(rawFileURL, version) rawURL := fmt.Sprintf(rawFileURL, version)
resp, err := http.Get(rawURL) resp, err := http.Get(rawURL)
if err != nil { if err != nil {
log.Printf("Error fetching raw file for version %s: %v\n", version, err)
continue continue
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Printf("Error reading raw file for version %s: %v\n", version, err)
continue continue
} }
@@ -80,15 +87,51 @@ func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) {
capabilityVersionStr := matches[1] capabilityVersionStr := matches[1]
capabilityVersion, _ := strconv.Atoi(capabilityVersionStr) capabilityVersion, _ := strconv.Atoi(capabilityVersionStr)
versions[version] = tailcfg.CapabilityVersion(capabilityVersion) versions[version] = tailcfg.CapabilityVersion(capabilityVersion)
} else {
log.Printf("Version: %s, CurrentCapabilityVersion not found\n", version)
} }
} }
return versions, nil 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 // Generate the Go code as a string
var content strings.Builder var content strings.Builder
content.WriteString("package capver\n\n") content.WriteString("package capver\n\n")
@@ -99,35 +142,50 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion
sortedVersions := xmaps.Keys(versions) sortedVersions := xmaps.Keys(versions)
sort.Strings(sortedVersions) sort.Strings(sortedVersions)
for _, version := range sortedVersions { for _, version := range sortedVersions {
fmt.Fprintf(&content, "\t\"%s\": %d,\n", version, versions[version]) fmt.Fprintf(&content, "\t\"%s\": %d,\n", version, versions[version])
} }
content.WriteString("}\n") content.WriteString("}\n")
content.WriteString("\n\n") content.WriteString("\n\n")
content.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n") content.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n")
capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string) capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
for _, v := range sortedVersions { for _, v := range sortedVersions {
cap := versions[v] capabilityVersion := versions[v]
// If it is already set, skip and continue, // If it is already set, skip and continue,
// we only want the first tailscale vsion per // we only want the first tailscale vsion per
// capability vsion. // capability vsion.
if _, ok := capVarToTailscaleVer[cap]; ok { if _, ok := capVarToTailscaleVer[capabilityVersion]; ok {
continue continue
} }
capVarToTailscaleVer[cap] = v
capVarToTailscaleVer[capabilityVersion] = v
} }
capsSorted := xmaps.Keys(capVarToTailscaleVer) capsSorted := xmaps.Keys(capVarToTailscaleVer)
sort.Slice(capsSorted, func(i, j int) bool { sort.Slice(capsSorted, func(i, j int) bool {
return capsSorted[i] < capsSorted[j] return capsSorted[i] < capsSorted[j]
}) })
for _, capVer := range capsSorted { for _, capVer := range capsSorted {
fmt.Fprintf(&content, "\t%d:\t\t\"%s\",\n", capVer, capVarToTailscaleVer[capVer]) 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 // Format the generated code
formatted, err := format.Source([]byte(content.String())) formatted, err := format.Source([]byte(content.String()))
@@ -136,7 +194,7 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion
} }
// Write to file // Write to file
err = os.WriteFile(outputFile, formatted, 0o644) err = os.WriteFile(outputFile, formatted, filePermissions)
if err != nil { if err != nil {
return fmt.Errorf("error writing file: %w", err) return fmt.Errorf("error writing file: %w", err)
} }
@@ -144,6 +202,150 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion
return nil 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() { func main() {
versions, err := getCapabilityVersions() versions, err := getCapabilityVersions()
if err != nil { if err != nil {
@@ -151,11 +353,20 @@ func main() {
return return
} }
err = writeCapabilityVersionsToFile(versions) // Calculate the minimum supported capability version
minSupportedCapVer := calculateMinSupportedCapabilityVersion(versions)
err = writeCapabilityVersionsToFile(versions, minSupportedCapVer)
if err != nil { if err != nil {
log.Println("Error writing to file:", err) log.Println("Error writing to file:", err)
return return
} }
err = writeTestDataFile(versions, minSupportedCapVer)
if err != nil {
log.Println("Error writing test data file:", err)
return
}
log.Println("Capability versions written to", outputFile) log.Println("Capability versions written to", outputFile)
} }