From 306deea03ab8b9e44b094d69bb5274513b22632d Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Mon, 24 Jul 2023 16:53:15 -0700 Subject: [PATCH] cmd/tailscale/cli,version/distro: update support for Alpine (#8701) Similar to Arch support, use the latest version info from the official `apk` repo and don't offer explicit track or version switching. Add detection for Alpine Linux in version/distro along the way. Updates #6995 Signed-off-by: Andrew Lytvynov --- cmd/tailscale/cli/update.go | 68 ++++++++++++++++++++++++++++-- cmd/tailscale/cli/update_test.go | 72 ++++++++++++++++++++++++++++++++ version/distro/distro.go | 3 ++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go index b54c3668d..7bf60b371 100644 --- a/cmd/tailscale/cli/update.go +++ b/cmd/tailscale/cli/update.go @@ -45,9 +45,10 @@ var updateCmd = &ffcli.Command{ fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts") fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts") fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)") - // These flags are not supported on Arch-based installs. Arch only - // offers one variant of tailscale and it's always the latest version. - if distro.Get() != distro.Arch { + // These flags are not supported on Arch or Alpine-based installs. + // Package repos on these distros only offer one variant of tailscale + // and it's always the latest version. + if distro.Get() != distro.Arch && distro.Get() != distro.Alpine { fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`) fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`) } @@ -145,6 +146,8 @@ func newUpdater() (*updater, error) { up.update = up.updateDebLike case distro.Arch: up.update = up.updateArchLike + case distro.Alpine: + up.update = up.updateAlpineLike } // TODO(awly): add support for Alpine switch { @@ -158,6 +161,8 @@ func newUpdater() (*updater, error) { up.update = up.updateFedoraLike("dnf") case haveExecutable("yum"): up.update = up.updateFedoraLike("yum") + case haveExecutable("apk"): + up.update = up.updateAlpineLike } case "darwin": switch { @@ -462,6 +467,63 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) { return true, os.WriteFile(repoFile, newContent.Bytes(), 0644) } +func (up *updater) updateAlpineLike() (err error) { + if err := requireRoot(); err != nil { + return err + } + + defer func() { + if err != nil && !errors.Is(err, errUserAborted) { + err = fmt.Errorf(`%w; you can try updating using "apk upgrade tailscale"`, err) + } + }() + + out, err := exec.Command("apk", "update").CombinedOutput() + if err != nil { + return fmt.Errorf("failed refresh apk repository indexes: %w, output: %q", err, out) + } + out, err = exec.Command("apk", "info", "tailscale").CombinedOutput() + if err != nil { + return fmt.Errorf("failed checking apk for latest tailscale version: %w, output: %q", err, out) + } + ver, err := parseAlpinePackageVersion(out) + if err != nil { + return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err) + } + if up.currentOrDryRun(ver) { + return nil + } + if err := up.confirm(ver); err != nil { + return err + } + + cmd := exec.Command("apk", "upgrade", "tailscale") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed tailscale update using apk: %w", err) + } + return nil +} + +func parseAlpinePackageVersion(out []byte) (string, error) { + s := bufio.NewScanner(bytes.NewReader(out)) + for s.Scan() { + // The line should look like this: + // tailscale-1.44.2-r0 description: + line := strings.TrimSpace(s.Text()) + if !strings.HasPrefix(line, "tailscale-") { + continue + } + parts := strings.SplitN(line, "-", 3) + if len(parts) < 3 { + return "", fmt.Errorf("malformed info line: %q", line) + } + return parts[1], nil + } + return "", errors.New("tailscale version not found in output") +} + func (up *updater) updateMacSys() error { // use sparkle? do we have permissions from this context? does sudo help? // We can at least fail with a command they can run to update from the shell. diff --git a/cmd/tailscale/cli/update_test.go b/cmd/tailscale/cli/update_test.go index 99152cba9..ef383916b 100644 --- a/cmd/tailscale/cli/update_test.go +++ b/cmd/tailscale/cli/update_test.go @@ -368,3 +368,75 @@ skip_if_unavailable=False }) } } + +func TestParseAlpinePackageVersion(t *testing.T) { + tests := []struct { + desc string + out string + want string + wantErr bool + }{ + { + desc: "valid version", + out: ` +tailscale-1.44.2-r0 description: +The easiest, most secure way to use WireGuard and 2FA + +tailscale-1.44.2-r0 webpage: +https://tailscale.com/ + +tailscale-1.44.2-r0 installed size: +32 MiB +`, + want: "1.44.2", + }, + { + desc: "wrong package output", + out: ` +busybox-1.36.1-r0 description: +Size optimized toolbox of many common UNIX utilities + +busybox-1.36.1-r0 webpage: +https://busybox.net/ + +busybox-1.36.1-r0 installed size: +924 KiB +`, + wantErr: true, + }, + { + desc: "missing version", + out: ` +tailscale description: +The easiest, most secure way to use WireGuard and 2FA + +tailscale webpage: +https://tailscale.com/ + +tailscale installed size: +32 MiB +`, + wantErr: true, + }, + { + desc: "empty output", + out: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + got, err := parseAlpinePackageVersion([]byte(tt.out)) + if err == nil && tt.wantErr { + t.Fatalf("got nil error and version %q, want non-nil error", got) + } + if err != nil && !tt.wantErr { + t.Fatalf("got error: %q, want nil", err) + } + if got != tt.want { + t.Fatalf("got version: %q, want %q", got, tt.want) + } + }) + } +} diff --git a/version/distro/distro.go b/version/distro/distro.go index e319d1ba7..8865a834b 100644 --- a/version/distro/distro.go +++ b/version/distro/distro.go @@ -30,6 +30,7 @@ const ( Gokrazy = Distro("gokrazy") WDMyCloud = Distro("wdmycloud") Unraid = Distro("unraid") + Alpine = Distro("alpine") ) var distro lazy.SyncValue[Distro] @@ -93,6 +94,8 @@ func linuxDistro() Distro { return WDMyCloud case have("/etc/unraid-version"): return Unraid + case have("/etc/alpine-release"): + return Alpine } return "" }