mirror of https://github.com/tailscale/tailscale/
cmd/tailscale: add start of "tailscale update" command
Goal: one way for users to update Tailscale, downgrade, switch tracks, regardless of platform (Windows, most Linux distros, macOS, Synology). This is a start. Updates #755, etc Change-Id: I23466da1ba41b45f0029ca79a17f5796c2eedd92 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>pull/6964/head
parent
67f82e62a1
commit
d9144c73a8
@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortHelp: "Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("update")
|
||||
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.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`)
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var updateArgs struct {
|
||||
yes bool
|
||||
dryRun bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
}
|
||||
|
||||
func runUpdate(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
if updateArgs.version != "" && updateArgs.track != "" {
|
||||
return errors.New("cannot specify both --version and --track")
|
||||
}
|
||||
up, err := newUpdater()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return up.update()
|
||||
}
|
||||
|
||||
func newUpdater() (*updater, error) {
|
||||
up := &updater{
|
||||
track: updateArgs.track,
|
||||
}
|
||||
switch up.track {
|
||||
case "stable", "unstable":
|
||||
case "":
|
||||
if version.IsUnstableBuild() {
|
||||
up.track = "unstable"
|
||||
} else {
|
||||
up.track = "stable"
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown track %q; must be 'stable' or 'unstable'", up.track)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
up.update = up.updateWindows
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
up.update = up.updateSynology
|
||||
case distro.Debian: // includes Ubuntu
|
||||
up.update = up.updateDebLike
|
||||
}
|
||||
case "darwin":
|
||||
switch {
|
||||
case !version.IsSandboxedMacOS():
|
||||
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
|
||||
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
up.update = up.updateMacSys
|
||||
default:
|
||||
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version")
|
||||
}
|
||||
}
|
||||
if up.update == nil {
|
||||
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/")
|
||||
}
|
||||
return up, nil
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
track string
|
||||
update func() error
|
||||
}
|
||||
|
||||
func (up *updater) currentOrDryRun(ver string) bool {
|
||||
if version.Short == ver {
|
||||
fmt.Printf("already running %v; no update needed\n", ver)
|
||||
return true
|
||||
}
|
||||
if updateArgs.dryRun {
|
||||
fmt.Printf("Current: %v, Latest: %v\n", version.Short, ver)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (up *updater) updateSynology() error {
|
||||
// TODO(bradfitz): detect, map GOARCH+CPU to the right Synology arch.
|
||||
// TODO(bradfitz): add pkgs.tailscale.com endpoint to get release info
|
||||
// TODO(bradfitz): require root/sudo
|
||||
// TODO(bradfitz): run /usr/syno/bin/synopkg install tailscale.spk
|
||||
return errors.New("The 'update' command is not yet implemented on Synology.")
|
||||
}
|
||||
|
||||
func (up *updater) updateDebLike() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz"
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
f, ok := latest.Tarballs[runtime.GOARCH]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't update architecture %q", runtime.GOARCH)
|
||||
}
|
||||
ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_")
|
||||
if !ok {
|
||||
return fmt.Errorf("can't parse version from %q", f)
|
||||
}
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/debian/pool/tailscale_%s_%s.deb", up.track, ver, runtime.GOARCH)
|
||||
// TODO(bradfitz): require root/sudo
|
||||
// TODO(bradfitz): check https://pkgs.tailscale.com/stable/debian/dists/sid/InRelease, check gpg, get sha256
|
||||
// And https://pkgs.tailscale.com/stable/debian/dists/sid/main/binary-amd64/Packages.gz and sha256 of it
|
||||
//
|
||||
|
||||
return errors.New("TODO: Debian/Ubuntu deb download of " + url)
|
||||
}
|
||||
|
||||
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.
|
||||
// Like "tailscale update --macsys | sudo sh" or something.
|
||||
//
|
||||
// TODO(bradfitz,mihai): implement. But for now:
|
||||
return errors.New("The 'update' command is not yet implemented on macOS.")
|
||||
}
|
||||
|
||||
func (up *updater) updateWindows() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Version string
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
ver = latest.Version
|
||||
if ver == "" {
|
||||
return errors.New("no version found")
|
||||
}
|
||||
}
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
arch = "x86"
|
||||
}
|
||||
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", up.track, ver, arch)
|
||||
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New("must be run as Administrator")
|
||||
}
|
||||
// TODO(bradfitz): require elevated mode
|
||||
return errors.New("TODO: download + msiexec /i /quiet " + url)
|
||||
}
|
Loading…
Reference in New Issue