Browse Source

clientupdate, cmd/tailscale/cli: support updating to release-candidates (#18632)

Adds a new track for release candidates. Supports querying by track in
version and updating to RCs in update for supported platforms.

updates #18193

Signed-off-by: Will Hannah <[email protected]>
Will Hannah 3 weeks ago
parent
commit
36d359e585

+ 13 - 10
clientupdate/clientupdate.go

@@ -38,8 +38,9 @@ import (
 )
 
 const (
-	StableTrack   = "stable"
-	UnstableTrack = "unstable"
+	StableTrack           = "stable"
+	UnstableTrack         = "unstable"
+	ReleaseCandidateTrack = "release-candidate"
 )
 
 var CurrentTrack = func() string {
@@ -80,6 +81,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.
@@ -114,7 +117,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)
@@ -496,10 +499,10 @@ func (up *Updater) updateDebLike() error {
 const aptSourcesFile = "/etc/apt/sources.list.d/tailscale.list"
 
 // updateDebianAptSourcesList updates the /etc/apt/sources.list.d/tailscale.list
-// file to make sure it has the provided track (stable or unstable) in it.
+// file to make sure it has the provided track (stable, unstable, or release-candidate) in it.
 //
-// If it already has the right track (including containing both stable and
-// unstable), it does nothing.
+// If it already has the right track (including containing both stable,
+// unstable, and release-candidate), it does nothing.
 func updateDebianAptSourcesList(dstTrack string) (rewrote bool, err error) {
 	was, err := os.ReadFile(aptSourcesFile)
 	if err != nil {
@@ -522,7 +525,7 @@ 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(`\bhttps://pkgs\.tailscale\.com/(stable|unstable|release-candidate)/`)
 	for bs.Scan() {
 		line := bs.Bytes()
 		if !commentLine.Match(line) {
@@ -616,15 +619,15 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
 }
 
 // updateYUMRepoTrack updates the repoFile file to make sure it has the
-// provided track (stable or unstable) in it.
+// provided track (stable, unstable, or release-candidate) in it.
 func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
 	was, err := os.ReadFile(repoFile)
 	if err != nil {
 		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)))

+ 51 - 44
clientupdate/clientupdate_test.go

@@ -86,29 +86,8 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
 	}
 }
 
-func TestUpdateYUMRepoTrack(t *testing.T) {
-	tests := []struct {
-		desc    string
-		before  string
-		track   string
-		after   string
-		rewrote bool
-		wantErr bool
-	}{
-		{
-			desc: "same track",
-			before: `
-[tailscale-stable]
-name=Tailscale stable
-baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
-enabled=1
-type=rpm
-repo_gpgcheck=1
-gpgcheck=0
-gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
-`,
-			track: StableTrack,
-			after: `
+var YUMRepos = map[string]string{
+	StableTrack: `
 [tailscale-stable]
 name=Tailscale stable
 baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
@@ -118,35 +97,30 @@ repo_gpgcheck=1
 gpgcheck=0
 gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
 `,
-		},
-		{
-			desc: "change track",
-			before: `
-[tailscale-stable]
-name=Tailscale stable
-baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
+
+	UnstableTrack: `
+[tailscale-unstable]
+name=Tailscale unstable
+baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
 enabled=1
 type=rpm
 repo_gpgcheck=1
 gpgcheck=0
-gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
+gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
 `,
-			track: UnstableTrack,
-			after: `
-[tailscale-unstable]
-name=Tailscale unstable
-baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
+
+	ReleaseCandidateTrack: `
+[tailscale-release-candidate]
+name=Tailscale release-candidate
+baseurl=https://pkgs.tailscale.com/release-candidate/fedora/$basearch
 enabled=1
 type=rpm
 repo_gpgcheck=1
 gpgcheck=0
-gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
+gpgkey=https://pkgs.tailscale.com/release-candidate/fedora/repo.gpg
 `,
-			rewrote: true,
-		},
-		{
-			desc: "non-tailscale repo file",
-			before: `
+
+	"FakeRepo": `
 [fedora]
 name=Fedora $releasever - $basearch
 #baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
@@ -158,8 +132,41 @@ repo_gpgcheck=0
 type=rpm
 gpgcheck=1
 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
-skip_if_unavailable=False
-`,
+skip_if_unavailable=False`,
+}
+
+func TestUpdateYUMRepoTrack(t *testing.T) {
+	tests := []struct {
+		desc    string
+		before  string
+		track   string
+		after   string
+		rewrote bool
+		wantErr bool
+	}{
+		{
+			desc:   "same track",
+			before: YUMRepos[StableTrack],
+			track:  StableTrack,
+			after:  YUMRepos[StableTrack],
+		},
+		{
+			desc:    "change track",
+			before:  YUMRepos[StableTrack],
+			track:   UnstableTrack,
+			after:   YUMRepos[UnstableTrack],
+			rewrote: true,
+		},
+		{
+			desc:    "change track RC",
+			before:  YUMRepos[StableTrack],
+			track:   ReleaseCandidateTrack,
+			after:   YUMRepos[ReleaseCandidateTrack],
+			rewrote: true,
+		},
+		{
+			desc:    "non-tailscale repo file",
+			before:  YUMRepos["FakeRepo"],
 			track:   StableTrack,
 			wantErr: true,
 		},

+ 6 - 3
cmd/tailscale/cli/update.go

@@ -22,8 +22,11 @@ import (
 func init() {
 	maybeUpdateCmd = func() *ffcli.Command { return updateCmd }
 
-	clientupdateLatestTailscaleVersion.Set(func() (string, error) {
-		return clientupdate.LatestTailscaleVersion(clientupdate.CurrentTrack)
+	clientupdateLatestTailscaleVersion.Set(func(track string) (string, error) {
+		if track == "" {
+			return clientupdate.LatestTailscaleVersion(clientupdate.CurrentTrack)
+		}
+		return clientupdate.LatestTailscaleVersion(track)
 	})
 }
 
@@ -50,7 +53,7 @@ var updateCmd = &ffcli.Command{
 			distro.Get() != distro.Synology &&
 			runtime.GOOS != "freebsd" &&
 			runtime.GOOS != "darwin" {
-			fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
+			fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable", "release-candidate", or "unstable" (dev); empty means same as current`)
 			fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
 		}
 		return fs

+ 4 - 2
cmd/tailscale/cli/version.go

@@ -24,6 +24,7 @@ var versionCmd = &ffcli.Command{
 		fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
 		fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format")
 		fs.BoolVar(&versionArgs.upstream, "upstream", false, "fetch and print the latest upstream release version from pkgs.tailscale.com")
+		fs.StringVar(&versionArgs.track, "track", "", `which track to check for updates: "stable", "release-candidate", or "unstable" (dev); empty means same as current`)
 		return fs
 	})(),
 	Exec: runVersion,
@@ -33,9 +34,10 @@ var versionArgs struct {
 	daemon   bool // also check local node's daemon version
 	json     bool
 	upstream bool
+	track    string
 }
 
-var clientupdateLatestTailscaleVersion feature.Hook[func() (string, error)]
+var clientupdateLatestTailscaleVersion feature.Hook[func(string) (string, error)]
 
 func runVersion(ctx context.Context, args []string) error {
 	if len(args) > 0 {
@@ -57,7 +59,7 @@ func runVersion(ctx context.Context, args []string) error {
 		if !ok {
 			return fmt.Errorf("fetching latest version not supported in this build")
 		}
-		upstreamVer, err = f()
+		upstreamVer, err = f(versionArgs.track)
 		if err != nil {
 			return err
 		}