summaryrefslogtreecommitdiffhomepage
path: root/clientupdate/clientupdate.go
diff options
context:
space:
mode:
authorWill Hannah <willh@tailscale.com>2025-12-11 16:55:30 -0500
committerWill Hannah <willh@tailscale.com>2026-01-16 12:53:13 -0500
commitc8144ddf0f402c02cfaaf02f12223b432fea704f (patch)
treeae9ea085195e7015b160c51ab8bbbde19475d9fe /clientupdate/clientupdate.go
parent78c8d14254eab4c35dca73af2006ea1eaff19f6b (diff)
downloadtailscale-willh/rc-updates.tar.xz
tailscale-willh/rc-updates.zip
clientupdate: support updating to release candidateswillh/rc-updates
Adds a new track for release candidates which is mapped to a new Updater Arguments field: acceptReleaseCandidates. When calling update, if the "release-candidate" track is provided, both the stable and release-candidate tracks are checked for updates. The newer of the two versions is selected. When calling version with the --upstream and --accept-release-candidates flags, the latest release-candidate version is shown if it is newer than the stable version. Alpine updates to release candidates are not yet supported. updates #18193 Signed-off-by: Will Hannah <willh@tailscale.com>
Diffstat (limited to 'clientupdate/clientupdate.go')
-rw-r--r--clientupdate/clientupdate.go159
1 files changed, 130 insertions, 29 deletions
diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go
index 3a0a8d03e..1aff5b661 100644
--- a/clientupdate/clientupdate.go
+++ b/clientupdate/clientupdate.go
@@ -37,8 +37,9 @@ import (
)
const (
- StableTrack = "stable"
- UnstableTrack = "unstable"
+ StableTrack = "stable"
+ UnstableTrack = "unstable"
+ ReleaseCandidateTrack = "release-candidate"
)
var CurrentTrack = func() string {
@@ -79,6 +80,8 @@ type Arguments struct {
// running binary
// - StableTrack and UnstableTrack will use the latest versions of the
// corresponding tracks
+ // - ReleaseCandidateTrack will use the newest version from StableTrack
+ // and ReleaseCandidateTrack.
//
// Leaving this empty will use Version or fall back to CurrentTrack if both
// Track and Version are empty.
@@ -113,7 +116,7 @@ func (args Arguments) validate() error {
return fmt.Errorf("only one of Version(%q) or Track(%q) can be set", args.Version, args.Track)
}
switch args.Track {
- case StableTrack, UnstableTrack, "":
+ case StableTrack, UnstableTrack, ReleaseCandidateTrack, "":
// All valid values.
default:
return fmt.Errorf("unsupported track %q", args.Track)
@@ -131,6 +134,10 @@ type Updater struct {
// returned by version.Short(), typically "x.y.z". Used for tests to
// override the actual current version.
currentVersion string
+
+ // acceptReleaseCandidates is true when the provided track is ReleaseCandidateTrack.
+ // This allows the installation of the newer of: the latest stable and the latest RC.
+ acceptReleaseCandidates bool
}
func NewUpdater(args Arguments) (*Updater, error) {
@@ -163,6 +170,10 @@ func NewUpdater(args Arguments) (*Updater, error) {
up.Track = CurrentTrack
}
}
+ if up.Track == ReleaseCandidateTrack {
+ up.acceptReleaseCandidates = true
+ up.Track = StableTrack
+ }
if up.Arguments.PkgsAddr == "" {
up.Arguments.PkgsAddr = "https://pkgs.tailscale.com"
}
@@ -326,6 +337,19 @@ func (up *Updater) updateSynology() error {
if err != nil {
return err
}
+
+ track := up.Track
+
+ // If we're accepting release candidates, check both tracks and choose the newer of the two.
+ if up.acceptReleaseCandidates {
+ latestRC, err := latestPackages(ReleaseCandidateTrack)
+ // If an RC is found and its newer than the last up.Track version, use the RC.
+ if err == nil && cmpver.Compare(latestRC.SPKsVersion, latest.SPKsVersion) > 0 {
+ latest = latestRC
+ track = ReleaseCandidateTrack
+ }
+ }
+
spkName := latest.SPKs[osName][arch]
if spkName == "" {
return fmt.Errorf("cannot find Synology package for os=%s arch=%s, please report a bug with your device model", osName, arch)
@@ -341,7 +365,7 @@ func (up *Updater) updateSynology() error {
if err != nil {
return err
}
- pkgsPath := fmt.Sprintf("%s/%s", up.Track, spkName)
+ pkgsPath := fmt.Sprintf("%s/%s", track, spkName)
spkPath := filepath.Join(spkDir, path.Base(pkgsPath))
if err := up.downloadURLToFile(pkgsPath, spkPath); err != nil {
return err
@@ -440,7 +464,7 @@ func (up *Updater) updateDebLike() error {
// instead.
return up.updateLinuxBinary()
}
- ver, err := requestedTailscaleVersion(up.Version, up.Track)
+ ver, isRC, err := requestedTailscaleVersion(up.Version, up.Track, up.acceptReleaseCandidates)
if err != nil {
return err
}
@@ -448,12 +472,27 @@ func (up *Updater) updateDebLike() error {
return nil
}
- if updated, err := updateDebianAptSourcesList(up.Track); err != nil {
+ track := up.Track
+
+ // If the update was found in the RC track, internally update to use the RC track.
+ if isRC {
+ track = ReleaseCandidateTrack
+ }
+
+ if updated, err := updateDebianAptSourcesList(track); err != nil {
return err
} else if updated {
- up.Logf("Updated %s to use the %s track", aptSourcesFile, up.Track)
+ up.Logf("Updated %s to use the %s track", aptSourcesFile, track)
}
+ defer func() {
+ // If the update was found in the RC track, revert the sources list to
+ // the original up.Track to avoid missing subsequent patch versions.
+ if isRC {
+ updateDebianAptSourcesList(up.Track)
+ }
+ }()
+
cmd := exec.Command("apt-get", "update",
// Only update the tailscale repo, not the other ones, treating
// the tailscale.list file as the main "sources.list" file.
@@ -517,17 +556,22 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
bs := bufio.NewScanner(bytes.NewReader(was))
hadCorrect := false
commentLine := regexp.MustCompile(`^\s*\#`)
- pkgsURL := regexp.MustCompile(`\bhttps://pkgs\.tailscale\.com/((un)?stable)/`)
+ pkgsURL := regexp.MustCompile(`(^|\s)https://pkgs\.tailscale\.com/(stable|unstable|release-candidate)/`)
for bs.Scan() {
line := bs.Bytes()
if !commentLine.Match(line) {
line = pkgsURL.ReplaceAllFunc(line, func(m []byte) []byte {
- if bytes.Equal(m, trackURLPrefix) {
+ submatches := pkgsURL.FindSubmatch(m)
+ // submatches[0] is the full match, submatches[1] is the leading
+ // whitespace or start-of-line anchor, and the remainder is the URL.
+ leading := submatches[1]
+ urlPart := submatches[0][len(leading):]
+ if bytes.Equal(urlPart, trackURLPrefix) {
hadCorrect = true
} else {
changes++
}
- return trackURLPrefix
+ return append(append([]byte{}, leading...), trackURLPrefix...)
})
}
buf.Write(line)
@@ -586,7 +630,7 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
}
}()
- ver, err := requestedTailscaleVersion(up.Version, up.Track)
+ ver, isRC, err := requestedTailscaleVersion(up.Version, up.Track, up.acceptReleaseCandidates)
if err != nil {
return err
}
@@ -594,15 +638,33 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
return nil
}
- if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.Track); err != nil {
+ track := up.Track
+
+ // If the update was found in the RC track, internally update to use the RC track.
+ if isRC {
+ track = ReleaseCandidateTrack
+ }
+
+ if updated, err := updateYUMRepoTrack(yumRepoConfigFile, track); err != nil {
return err
} else if updated {
- up.Logf("Updated %s to use the %s track", yumRepoConfigFile, up.Track)
+ up.Logf("Updated %s to use the %s track", yumRepoConfigFile, track)
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
+
+ // If the update was found in the RC track, revert the package manager's config file to
+ // the original up.Track to avoid missing subsequent patch versions as they are released.
+ if isRC {
+ if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.Track); err != nil {
+ up.Logf("failed to revert %s to use the %s track: %v", yumRepoConfigFile, up.Track, err)
+ } else if updated {
+ up.Logf("Reverted %s to use the %s track", yumRepoConfigFile, up.Track)
+ }
+ }
+
if err := cmd.Run(); err != nil {
return err
}
@@ -618,8 +680,8 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
return false, err
}
- urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
- urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
+ urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(stable|unstable|release-candidate)`)
+ urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s", dstTrack)
s := bufio.NewScanner(bytes.NewReader(was))
newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
@@ -726,7 +788,7 @@ func parseAlpinePackageVersion(out []byte) (string, error) {
var apkRepoVersionRE = regexp.MustCompile(`v[0-9]+\.[0-9]+`)
func checkOutdatedAlpineRepo(logf logger.Logf, apkVer, track string) error {
- latest, err := LatestTailscaleVersion(track)
+ latest, _, err := LatestTailscaleVersion(track, false)
if err != nil {
return err
}
@@ -846,7 +908,7 @@ func (up *Updater) updateLinuxBinary() error {
if err := requireRoot(); err != nil {
return err
}
- ver, err := requestedTailscaleVersion(up.Version, up.Track)
+ ver, isRC, err := requestedTailscaleVersion(up.Version, up.Track, up.acceptReleaseCandidates)
if err != nil {
return err
}
@@ -854,6 +916,17 @@ func (up *Updater) updateLinuxBinary() error {
return nil
}
+ originalTrack := up.Track
+
+ defer func() {
+ up.Track = originalTrack
+ }()
+
+ // If an RC was found, internally update the working track to the RC track.
+ if isRC {
+ up.Track = ReleaseCandidateTrack
+ }
+
dlPath, err := up.downloadLinuxTarball(ver)
if err != nil {
return err
@@ -1148,24 +1221,56 @@ func haveExecutable(name string) bool {
return err == nil && path != ""
}
-func requestedTailscaleVersion(ver, track string) (string, error) {
+func requestedTailscaleVersion(ver, track string, acceptReleaseCandidates bool) (string, bool, error) {
if ver != "" {
- return ver, nil
+ return ver, false, nil
}
- return LatestTailscaleVersion(track)
+ return LatestTailscaleVersion(track, acceptReleaseCandidates)
}
// LatestTailscaleVersion returns the latest released version for the given
-// track from pkgs.tailscale.com.
-func LatestTailscaleVersion(track string) (string, error) {
+// track from pkgs.tailscale.com. If track is empty, CurrentTrack is used. Returns
+// the version found, whether or not it is an RC version, and any error.
+func LatestTailscaleVersion(track string, acceptReleaseCandidates bool) (string, bool, error) {
if track == "" {
track = CurrentTrack
}
- latest, err := latestPackages(track)
+ testTrack := track
+
+ // For ReleaseCandidateTrack, take the newer of StableTrack and ReleaseCandidateTrack.
+ // This avoids trapping users on an older RC after a patch stable release is made.
+ if track == ReleaseCandidateTrack {
+ testTrack = StableTrack
+ }
+ latest, err := latestPackages(testTrack)
if err != nil {
- return "", err
+ return "", false, err
+ }
+
+ // First, find the latest version on the requested track.
+ ver := latestPlatformVersion(latest)
+
+ if !acceptReleaseCandidates && ver == "" {
+ return "", false, fmt.Errorf("no latest version found for OS %q on %q track", runtime.GOOS, track)
+ } else if !acceptReleaseCandidates && ver != "" {
+ return ver, false, nil
}
+
+ // Consider the latest RC version if it's newer than the stable version just found.
+ if latestRC, err := latestPackages(ReleaseCandidateTrack); err == nil && cmpver.Compare(latestRC.Version, ver) > 0 {
+ ver = latestPlatformVersion(latestRC)
+ return ver, true, nil
+ }
+
+ if ver == "" {
+ return "", false, fmt.Errorf("no latest version or RC found for OS %q on %q track", runtime.GOOS, track)
+ }
+
+ return ver, false, nil
+}
+
+func latestPlatformVersion(latest *trackPackages) string {
ver := latest.Version
switch runtime.GOOS {
case "windows":
@@ -1178,11 +1283,7 @@ func LatestTailscaleVersion(track string) (string, error) {
ver = latest.SPKsVersion
}
}
-
- if ver == "" {
- return "", fmt.Errorf("no latest version found for OS %q on %q track", runtime.GOOS, track)
- }
- return ver, nil
+ return ver
}
type trackPackages struct {