mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-28 12:02:23 +00:00
cmd/tailscale/cli: implement update for dnf/yum-based distros (#8678)
This is the Fedora family of distros, including CentOS, RHEL and others. Tested in `fedora:latest` and `centos:7` containers. Updates #6995 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
f1cc8ab3f9
commit
894b237a70
@ -144,7 +144,20 @@ func newUpdater() (*updater, error) {
|
|||||||
case distro.Debian: // includes Ubuntu
|
case distro.Debian: // includes Ubuntu
|
||||||
up.update = up.updateDebLike
|
up.update = up.updateDebLike
|
||||||
case distro.Arch:
|
case distro.Arch:
|
||||||
up.update = up.updateArch
|
up.update = up.updateArchLike
|
||||||
|
}
|
||||||
|
// TODO(awly): add support for Alpine
|
||||||
|
switch {
|
||||||
|
case haveExecutable("pacman"):
|
||||||
|
up.update = up.updateArchLike
|
||||||
|
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.
|
||||||
|
up.update = up.updateDebLike
|
||||||
|
case haveExecutable("dnf"):
|
||||||
|
up.update = up.updateFedoraLike("dnf")
|
||||||
|
case haveExecutable("yum"):
|
||||||
|
up.update = up.updateFedoraLike("yum")
|
||||||
}
|
}
|
||||||
case "darwin":
|
case "darwin":
|
||||||
switch {
|
switch {
|
||||||
@ -207,48 +220,22 @@ func (up *updater) updateSynology() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (up *updater) updateDebLike() error {
|
func (up *updater) updateDebLike() error {
|
||||||
ver := updateArgs.version
|
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||||
if ver == "" {
|
if err != nil {
|
||||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
|
return err
|
||||||
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) {
|
if up.currentOrDryRun(ver) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
track := "unstable"
|
if err := requireRoot(); err != nil {
|
||||||
if stable, ok := versionIsStable(ver); !ok {
|
return err
|
||||||
return fmt.Errorf("malformed version %q", ver)
|
|
||||||
} else if stable {
|
|
||||||
track = "stable"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Geteuid() != 0 {
|
if updated, err := updateDebianAptSourcesList(up.track); err != nil {
|
||||||
return errors.New("must be root; use sudo")
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated, err := updateDebianAptSourcesList(track); err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else if updated {
|
} else if updated {
|
||||||
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, track)
|
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, up.track)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("apt-get", "update",
|
cmd := exec.Command("apt-get", "update",
|
||||||
@ -334,9 +321,9 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (up *updater) updateArch() (err error) {
|
func (up *updater) updateArchLike() (err error) {
|
||||||
if os.Geteuid() != 0 {
|
if err := requireRoot(); err != nil {
|
||||||
return errors.New("must be root; use sudo")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -391,6 +378,90 @@ func parsePacmanVersion(out []byte) (string, error) {
|
|||||||
return "", fmt.Errorf("could not find latest version of tailscale via pacman")
|
return "", fmt.Errorf("could not find latest version of tailscale via pacman")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
|
||||||
|
|
||||||
|
// updateFedoraLike updates tailscale on any distros in the Fedora family,
|
||||||
|
// specifically anything that uses "dnf" or "yum" package managers. The actual
|
||||||
|
// package manager is passed via packageManager.
|
||||||
|
func (up *updater) updateFedoraLike(packageManager string) func() error {
|
||||||
|
return func() (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 "%s upgrade tailscale"`, err, packageManager)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if up.currentOrDryRun(ver) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := up.confirm(ver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.track); err != nil {
|
||||||
|
return err
|
||||||
|
} else if updated {
|
||||||
|
fmt.Printf("Updated %s to use the %s track\n", yumRepoConfigFile, up.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateYUMRepoTrack updates the repoFile file to make sure it has the
|
||||||
|
// provided track (stable or unstable) in it.
|
||||||
|
func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
|
||||||
|
was, err := os.ReadFile(repoFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
|
||||||
|
urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
|
||||||
|
|
||||||
|
s := bufio.NewScanner(bytes.NewReader(was))
|
||||||
|
newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
// Handle repo section name, like "[tailscale-stable]".
|
||||||
|
if len(line) > 0 && line[0] == '[' {
|
||||||
|
if !strings.HasPrefix(line, "[tailscale-") {
|
||||||
|
return false, fmt.Errorf("%q does not look like a tailscale repo file, it contains an unexpected %q section", repoFile, line)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(newContent, "[tailscale-%s]\n", dstTrack)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Update the track mentioned in repo name.
|
||||||
|
if strings.HasPrefix(line, "name=") {
|
||||||
|
fmt.Fprintf(newContent, "name=Tailscale %s\n", dstTrack)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Update the actual repo URLs.
|
||||||
|
if strings.HasPrefix(line, "baseurl=") || strings.HasPrefix(line, "gpgkey=") {
|
||||||
|
fmt.Fprintln(newContent, urlRe.ReplaceAllString(line, urlReplacement))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintln(newContent, line)
|
||||||
|
}
|
||||||
|
if bytes.Equal(was, newContent.Bytes()) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
@ -459,24 +530,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (up *updater) updateWindows() error {
|
func (up *updater) updateWindows() error {
|
||||||
ver := updateArgs.version
|
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||||
if ver == "" {
|
if err != nil {
|
||||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
|
return err
|
||||||
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
|
arch := runtime.GOARCH
|
||||||
if arch == "386" {
|
if arch == "386" {
|
||||||
@ -705,3 +761,45 @@ func (pw *progressWriter) print() {
|
|||||||
pw.lastPrint = time.Now()
|
pw.lastPrint = time.Now()
|
||||||
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func haveExecutable(name string) bool {
|
||||||
|
path, err := exec.LookPath(name)
|
||||||
|
return err == nil && path != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestedTailscaleVersion(ver, track string) (string, error) {
|
||||||
|
if ver != "" {
|
||||||
|
return ver, nil
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("fetching latest tailscale version: %w", 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)
|
||||||
|
}
|
||||||
|
if latest.Version == "" {
|
||||||
|
return "", fmt.Errorf("no version found at %q", url)
|
||||||
|
}
|
||||||
|
return latest.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireRoot() error {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
return errors.New("must be root; use sudo")
|
||||||
|
case "freebsd", "openbsd":
|
||||||
|
return errors.New("must be root; use doas")
|
||||||
|
default:
|
||||||
|
return errors.New("must be root")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -253,3 +257,114 @@ Version : 1.44.2
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateYUMRepoTrack(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
before string
|
||||||
|
track string
|
||||||
|
after string
|
||||||
|
rewrote bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "same track",
|
||||||
|
before: `
|
||||||
|
[tailscale-stable]
|
||||||
|
name=Tailscale stable
|
||||||
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||||
|
enabled=1
|
||||||
|
type=rpm
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgcheck=0
|
||||||
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||||
|
`,
|
||||||
|
track: "stable",
|
||||||
|
after: `
|
||||||
|
[tailscale-stable]
|
||||||
|
name=Tailscale stable
|
||||||
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||||
|
enabled=1
|
||||||
|
type=rpm
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgcheck=0
|
||||||
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "change track",
|
||||||
|
before: `
|
||||||
|
[tailscale-stable]
|
||||||
|
name=Tailscale stable
|
||||||
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||||
|
enabled=1
|
||||||
|
type=rpm
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgcheck=0
|
||||||
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||||
|
`,
|
||||||
|
track: "unstable",
|
||||||
|
after: `
|
||||||
|
[tailscale-unstable]
|
||||||
|
name=Tailscale unstable
|
||||||
|
baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
|
||||||
|
enabled=1
|
||||||
|
type=rpm
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgcheck=0
|
||||||
|
gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
|
||||||
|
`,
|
||||||
|
rewrote: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "non-tailscale repo file",
|
||||||
|
before: `
|
||||||
|
[fedora]
|
||||||
|
name=Fedora $releasever - $basearch
|
||||||
|
#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
|
||||||
|
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
|
||||||
|
enabled=1
|
||||||
|
countme=1
|
||||||
|
metadata_expire=7d
|
||||||
|
repo_gpgcheck=0
|
||||||
|
type=rpm
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
|
||||||
|
skip_if_unavailable=False
|
||||||
|
`,
|
||||||
|
track: "stable",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "tailscale.repo")
|
||||||
|
if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrote, err := updateYUMRepoTrack(path, tt.track)
|
||||||
|
if err == nil && tt.wantErr {
|
||||||
|
t.Fatal("got nil error, want non-nil")
|
||||||
|
}
|
||||||
|
if err != nil && !tt.wantErr {
|
||||||
|
t.Fatalf("got error %q, want nil", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rewrote != tt.rewrote {
|
||||||
|
t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
|
||||||
|
}
|
||||||
|
|
||||||
|
after, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(after) != tt.after {
|
||||||
|
t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user