From 593c0868667400e89d2179c5c311bbc03adc6c86 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Mon, 23 Oct 2023 18:21:54 -0700 Subject: [PATCH] clientupdate: distinguish when auto-updates are possible (#9896) clientupdate.Updater will have a non-nil Update func in a few cases where it doesn't actually perform an update: * on Arch-like distros, where it prints instructions on how to update * on macOS app store version, where it opens the app store page Add a new clientupdate.Arguments field to cause NewUpdater to fail when we hit one of these cases. This results in c2n updates being "not supported" and `tailscale set --auto-update` returning an error. Updates #755 Signed-off-by: Andrew Lytvynov --- clientupdate/clientupdate.go | 66 +++++++++++++++++++++++------------- cmd/tailscale/cli/set.go | 2 +- ipn/ipnlocal/c2n.go | 2 +- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go index 85a826108..a12b4e37f 100644 --- a/clientupdate/clientupdate.go +++ b/clientupdate/clientupdate.go @@ -86,6 +86,10 @@ type Arguments struct { // PkgsAddr is the address of the pkgs server to fetch updates from. // Defaults to "https://pkgs.tailscale.com". PkgsAddr string + // ForAutoUpdate should be true when Updater is created in auto-update + // context. When true, NewUpdater returns an error if it cannot be used for + // auto-updates (even if Updater.Update field is non-nil). + ForAutoUpdate bool } func (args Arguments) validate() error { @@ -116,10 +120,14 @@ func NewUpdater(args Arguments) (*Updater, error) { if up.Stderr == nil { up.Stderr = os.Stderr } - up.Update = up.getUpdateFunction() + var canAutoUpdate bool + up.Update, canAutoUpdate = up.getUpdateFunction() if up.Update == nil { return nil, errors.ErrUnsupported } + if args.ForAutoUpdate && !canAutoUpdate { + return nil, errors.ErrUnsupported + } switch up.Version { case StableTrack, UnstableTrack: up.track = up.Version @@ -144,52 +152,64 @@ func NewUpdater(args Arguments) (*Updater, error) { type updateFunction func() error -func (up *Updater) getUpdateFunction() updateFunction { +func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) { switch runtime.GOOS { case "windows": - return up.updateWindows + return up.updateWindows, true case "linux": switch distro.Get() { case distro.Synology: - return up.updateSynology + return up.updateSynology, true case distro.Debian: // includes Ubuntu - return up.updateDebLike + return up.updateDebLike, true case distro.Arch: - return up.updateArchLike + if up.archPackageInstalled() { + // Arch update func just prints a message about how to update, + // it doesn't support auto-updates. + return up.updateArchLike, false + } + return up.updateLinuxBinary, true case distro.Alpine: - return up.updateAlpineLike + return up.updateAlpineLike, true } switch { case haveExecutable("pacman"): - return up.updateArchLike + if up.archPackageInstalled() { + // Arch update func just prints a message about how to update, + // it doesn't support auto-updates. + return up.updateArchLike, false + } + return up.updateLinuxBinary, true case haveExecutable("apt-get"): // TODO(awly): add support for "apt" // The distro.Debian switch case above should catch most apt-based // systems, but add this fallback just in case. - return up.updateDebLike + return up.updateDebLike, true case haveExecutable("dnf"): - return up.updateFedoraLike("dnf") + return up.updateFedoraLike("dnf"), true case haveExecutable("yum"): - return up.updateFedoraLike("yum") + return up.updateFedoraLike("yum"), true case haveExecutable("apk"): - return up.updateAlpineLike + return up.updateAlpineLike, true } // If nothing matched, fall back to tarball updates. if up.Update == nil { - return up.updateLinuxBinary + return up.updateLinuxBinary, true } case "darwin": switch { case version.IsMacAppStore(): - return up.updateMacAppStore + // App store update func just opens the store page, it doesn't + // support auto-updates. + return up.updateMacAppStore, false case version.IsMacSysExt(): - return up.updateMacSys + return up.updateMacSys, true default: - return nil + return nil, false } case "freebsd": - return up.updateFreeBSD + return up.updateFreeBSD, true } - return nil + return nil, false } // Update runs a single update attempt using the platform-specific mechanism. @@ -454,12 +474,12 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent [] return buf.Bytes(), nil } +func (up *Updater) archPackageInstalled() bool { + err := exec.Command("pacman", "--query", "tailscale").Run() + return err == nil +} + func (up *Updater) updateArchLike() error { - if err := exec.Command("pacman", "--query", "tailscale").Run(); err != nil && isExitError(err) { - // Tailscale was not installed via pacman, update via tarball download - // instead. - return up.updateLinuxBinary() - } // Arch maintainer asked us not to implement "tailscale update" or // auto-updates on Arch-based distros: // https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106 diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index faa2a1ec2..a10fd771f 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -157,7 +157,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { } } if maskedPrefs.AutoUpdateSet { - _, err := clientupdate.NewUpdater(clientupdate.Arguments{}) + _, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true}) if errors.Is(err, errors.ErrUnsupported) { return errors.New("automatic updates are not supported on this platform") } diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index d95c3e7cc..300abff55 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -265,7 +265,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse { // Note that we create the Updater solely to check for errors; we do not // invoke it here. For this purpose, it is ok to pass it a zero Arguments. prefs := b.Prefs().AutoUpdate() - _, err := clientupdate.NewUpdater(clientupdate.Arguments{}) + _, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true}) return tailcfg.C2NUpdateResponse{ Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply, Supported: err == nil,