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 <awly@tailscale.com>
pull/5086/merge
Andrew Lytvynov 1 year ago committed by GitHub
parent 6afffece8a
commit 306deea03a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -45,9 +45,10 @@ var updateCmd = &ffcli.Command{
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts") 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.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)") 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 // These flags are not supported on Arch or Alpine-based installs.
// offers one variant of tailscale and it's always the latest version. // Package repos on these distros only offer one variant of tailscale
if distro.Get() != distro.Arch { // 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.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`) fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
} }
@ -145,6 +146,8 @@ func newUpdater() (*updater, error) {
up.update = up.updateDebLike up.update = up.updateDebLike
case distro.Arch: case distro.Arch:
up.update = up.updateArchLike up.update = up.updateArchLike
case distro.Alpine:
up.update = up.updateAlpineLike
} }
// TODO(awly): add support for Alpine // TODO(awly): add support for Alpine
switch { switch {
@ -158,6 +161,8 @@ func newUpdater() (*updater, error) {
up.update = up.updateFedoraLike("dnf") up.update = up.updateFedoraLike("dnf")
case haveExecutable("yum"): case haveExecutable("yum"):
up.update = up.updateFedoraLike("yum") up.update = up.updateFedoraLike("yum")
case haveExecutable("apk"):
up.update = up.updateAlpineLike
} }
case "darwin": case "darwin":
switch { switch {
@ -462,6 +467,63 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644) 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 { func (up *updater) updateMacSys() error {
// use sparkle? do we have permissions from this context? does sudo help? // 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. // We can at least fail with a command they can run to update from the shell.

@ -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)
}
})
}
}

@ -30,6 +30,7 @@ const (
Gokrazy = Distro("gokrazy") Gokrazy = Distro("gokrazy")
WDMyCloud = Distro("wdmycloud") WDMyCloud = Distro("wdmycloud")
Unraid = Distro("unraid") Unraid = Distro("unraid")
Alpine = Distro("alpine")
) )
var distro lazy.SyncValue[Distro] var distro lazy.SyncValue[Distro]
@ -93,6 +94,8 @@ func linuxDistro() Distro {
return WDMyCloud return WDMyCloud
case have("/etc/unraid-version"): case have("/etc/unraid-version"):
return Unraid return Unraid
case have("/etc/alpine-release"):
return Alpine
} }
return "" return ""
} }

Loading…
Cancel
Save