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 <awly@tailscale.com>
pull/9946/head
Andrew Lytvynov 1 year ago committed by GitHub
parent 7df6f8736a
commit 593c086866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -86,6 +86,10 @@ type Arguments struct {
// PkgsAddr is the address of the pkgs server to fetch updates from. // PkgsAddr is the address of the pkgs server to fetch updates from.
// Defaults to "https://pkgs.tailscale.com". // Defaults to "https://pkgs.tailscale.com".
PkgsAddr string 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 { func (args Arguments) validate() error {
@ -116,10 +120,14 @@ func NewUpdater(args Arguments) (*Updater, error) {
if up.Stderr == nil { if up.Stderr == nil {
up.Stderr = os.Stderr up.Stderr = os.Stderr
} }
up.Update = up.getUpdateFunction() var canAutoUpdate bool
up.Update, canAutoUpdate = up.getUpdateFunction()
if up.Update == nil { if up.Update == nil {
return nil, errors.ErrUnsupported return nil, errors.ErrUnsupported
} }
if args.ForAutoUpdate && !canAutoUpdate {
return nil, errors.ErrUnsupported
}
switch up.Version { switch up.Version {
case StableTrack, UnstableTrack: case StableTrack, UnstableTrack:
up.track = up.Version up.track = up.Version
@ -144,52 +152,64 @@ func NewUpdater(args Arguments) (*Updater, error) {
type updateFunction func() error type updateFunction func() error
func (up *Updater) getUpdateFunction() updateFunction { func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
return up.updateWindows return up.updateWindows, true
case "linux": case "linux":
switch distro.Get() { switch distro.Get() {
case distro.Synology: case distro.Synology:
return up.updateSynology return up.updateSynology, true
case distro.Debian: // includes Ubuntu case distro.Debian: // includes Ubuntu
return up.updateDebLike return up.updateDebLike, true
case distro.Arch: 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: case distro.Alpine:
return up.updateAlpineLike return up.updateAlpineLike, true
} }
switch { switch {
case haveExecutable("pacman"): 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" case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
// The distro.Debian switch case above should catch most apt-based // The distro.Debian switch case above should catch most apt-based
// systems, but add this fallback just in case. // systems, but add this fallback just in case.
return up.updateDebLike return up.updateDebLike, true
case haveExecutable("dnf"): case haveExecutable("dnf"):
return up.updateFedoraLike("dnf") return up.updateFedoraLike("dnf"), true
case haveExecutable("yum"): case haveExecutable("yum"):
return up.updateFedoraLike("yum") return up.updateFedoraLike("yum"), true
case haveExecutable("apk"): case haveExecutable("apk"):
return up.updateAlpineLike return up.updateAlpineLike, true
} }
// If nothing matched, fall back to tarball updates. // If nothing matched, fall back to tarball updates.
if up.Update == nil { if up.Update == nil {
return up.updateLinuxBinary return up.updateLinuxBinary, true
} }
case "darwin": case "darwin":
switch { switch {
case version.IsMacAppStore(): 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(): case version.IsMacSysExt():
return up.updateMacSys return up.updateMacSys, true
default: default:
return nil return nil, false
} }
case "freebsd": 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. // 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 return buf.Bytes(), nil
} }
func (up *Updater) archPackageInstalled() bool {
err := exec.Command("pacman", "--query", "tailscale").Run()
return err == nil
}
func (up *Updater) updateArchLike() error { 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 // Arch maintainer asked us not to implement "tailscale update" or
// auto-updates on Arch-based distros: // auto-updates on Arch-based distros:
// https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106 // https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106

@ -157,7 +157,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
} }
} }
if maskedPrefs.AutoUpdateSet { if maskedPrefs.AutoUpdateSet {
_, err := clientupdate.NewUpdater(clientupdate.Arguments{}) _, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
if errors.Is(err, errors.ErrUnsupported) { if errors.Is(err, errors.ErrUnsupported) {
return errors.New("automatic updates are not supported on this platform") return errors.New("automatic updates are not supported on this platform")
} }

@ -265,7 +265,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// Note that we create the Updater solely to check for errors; we do not // 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. // invoke it here. For this purpose, it is ok to pass it a zero Arguments.
prefs := b.Prefs().AutoUpdate() prefs := b.Prefs().AutoUpdate()
_, err := clientupdate.NewUpdater(clientupdate.Arguments{}) _, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
return tailcfg.C2NUpdateResponse{ return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply, Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
Supported: err == nil, Supported: err == nil,

Loading…
Cancel
Save