Skip to content

Commit 76b9397

Browse files
committed
Feature: Auto-update client
1 parent 541e258 commit 76b9397

File tree

19 files changed

+910
-512
lines changed

19 files changed

+910
-512
lines changed

client/internal/engine.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/netbirdio/netbird/client/internal/routemanager"
4949
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
5050
"github.com/netbirdio/netbird/client/internal/statemanager"
51+
"github.com/netbirdio/netbird/client/internal/updatemanager"
5152
cProto "github.com/netbirdio/netbird/client/proto"
5253
"github.com/netbirdio/netbird/management/domain"
5354
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
@@ -194,6 +195,9 @@ type Engine struct {
194195
latestNetworkMap *mgmProto.NetworkMap
195196
connSemaphore *semaphoregroup.SemaphoreGroup
196197
flowManager nftypes.FlowManager
198+
199+
// auto-update
200+
updateManager *updatemanager.UpdateManager
197201
}
198202

199203
// Peer is an instance of the Connection Peer
@@ -236,6 +240,7 @@ func NewEngine(
236240
statusRecorder: statusRecorder,
237241
checks: checks,
238242
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
243+
updateManager: updatemanager.NewUpdateManager(statusRecorder),
239244
}
240245

241246
sm := profilemanager.ServiceManager{}
@@ -658,7 +663,11 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
658663
e.syncMsgMux.Lock()
659664
defer e.syncMsgMux.Unlock()
660665

666+
if update.GetAutoUpdateVersion() != "skip" {
667+
e.updateManager.SetVersion(update.GetAutoUpdateVersion())
668+
}
661669
if update.GetNetbirdConfig() != nil {
670+
662671
wCfg := update.GetNetbirdConfig()
663672
err := e.updateTURNs(wCfg.GetTurns())
664673
if err != nil {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package updatemanager
2+
3+
import (
4+
"github.com/netbirdio/netbird/client/internal/peer"
5+
cProto "github.com/netbirdio/netbird/client/proto"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
log "github.com/sirupsen/logrus"
14+
15+
v "github.com/hashicorp/go-version"
16+
"github.com/netbirdio/netbird/version"
17+
)
18+
19+
type UpdateManager struct {
20+
version string
21+
update *version.Update
22+
lastTrigger time.Time
23+
statusRecorder *peer.Status
24+
}
25+
26+
func NewUpdateManager(statusRecorder *peer.Status) *UpdateManager {
27+
update := version.NewUpdate("nb/client")
28+
manager := &UpdateManager{
29+
update: update,
30+
lastTrigger: time.Now().Add(-10 * time.Minute),
31+
statusRecorder: statusRecorder,
32+
}
33+
manager.version = "disabled"
34+
update.SetDaemonVersion(version.NetbirdVersion())
35+
update.SetOnUpdateListener(manager.CheckForUpdates)
36+
return manager
37+
}
38+
39+
func (u *UpdateManager) SetVersion(v string) {
40+
if u.version != v {
41+
log.Errorf("############## Version set to %s", v)
42+
u.version = v
43+
go u.CheckForUpdates()
44+
}
45+
}
46+
47+
func (u *UpdateManager) CheckForUpdates() {
48+
if u.version == "disabled" {
49+
log.Trace("Skipped checking for updates, auto-update is disabled")
50+
return
51+
}
52+
currentVersionString := version.NetbirdVersion()
53+
updateVersionString := u.version
54+
if updateVersionString == "latest" || updateVersionString == "" {
55+
if u.update.LatestAvailable == nil {
56+
log.Tracef("Latest version not fetched yet")
57+
return
58+
}
59+
updateVersionString = u.update.LatestAvailable.String()
60+
}
61+
currentVersion, err := v.NewVersion(currentVersionString)
62+
if err != nil {
63+
log.Errorf("Error checking for update, error parsing version `%s`: %v", currentVersionString, err)
64+
return
65+
}
66+
updateVersion, err := v.NewVersion(updateVersionString)
67+
if err != nil {
68+
log.Errorf("Error checking for update, error parsing version `%s`: %v", updateVersionString, err)
69+
return
70+
}
71+
if currentVersion.LessThan(updateVersion) {
72+
if u.lastTrigger.Add(5 * time.Minute).Before(time.Now()) {
73+
u.lastTrigger = time.Now()
74+
log.Debugf("Auto-update triggered, current version: %s, target version: %s", currentVersionString, updateVersionString)
75+
u.statusRecorder.PublishEvent(
76+
cProto.SystemEvent_INFO,
77+
cProto.SystemEvent_SYSTEM,
78+
"Automatically updating client",
79+
"Your client version is older than auto-update version set in Management, updating client now.",
80+
nil,
81+
)
82+
err = u.triggerUpdate(updateVersionString)
83+
if err != nil {
84+
log.Errorf("Error triggering auto-update: %v", err)
85+
}
86+
}
87+
} else {
88+
log.Trace("Current version is equal to or higher than auto-update version")
89+
}
90+
}
91+
92+
func downloadFileToTemporaryDir(fileURL string) (string, error) { //nolint:unused
93+
tempDir, err := os.MkdirTemp("", "netbird-installer-*")
94+
if err != nil {
95+
return "", err
96+
}
97+
fileNameParts := strings.Split(fileURL, "/")
98+
out, err := os.Create(filepath.Join(tempDir, fileNameParts[len(fileNameParts)-1]))
99+
if err != nil {
100+
return "", err
101+
}
102+
defer out.Close()
103+
104+
resp, err := http.Get(fileURL)
105+
if err != nil {
106+
return "", err
107+
}
108+
defer resp.Body.Close()
109+
110+
_, err = io.Copy(out, resp.Body)
111+
if err != nil {
112+
return "", err
113+
}
114+
115+
log.Tracef("Downloaded update file to %s", out.Name())
116+
117+
return out.Name(), nil
118+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//go:build darwin
2+
3+
package updatemanager
4+
5+
import (
6+
"fmt"
7+
log "github.com/sirupsen/logrus"
8+
"os"
9+
"os/exec"
10+
"runtime"
11+
"strings"
12+
"syscall"
13+
)
14+
15+
const (
16+
pkgDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%version/netbird_%version_darwin_%arch.pkg"
17+
)
18+
19+
func (u *UpdateManager) triggerUpdate(targetVersion string) error {
20+
cmd := exec.Command("pkgutil", "--pkg-info", "io.netbird.client")
21+
outBytes, err := cmd.Output()
22+
if err != nil && cmd.ProcessState.ExitCode() == 1 {
23+
// Not installed using pkg file, thus installed using Homebrew
24+
25+
return u.updateHomeBrew()
26+
}
27+
// Installed using pkg file
28+
url := strings.ReplaceAll(pkgDownloadURL, "%version", targetVersion)
29+
url = strings.ReplaceAll(url, "%arch", runtime.GOARCH)
30+
path, err := downloadFileToTemporaryDir(url)
31+
if err != nil {
32+
return err
33+
}
34+
35+
volume := "/"
36+
for _, v := range strings.Split(string(outBytes), "\n") {
37+
trimmed := strings.TrimSpace(v)
38+
if strings.HasPrefix(trimmed, "volume: ") {
39+
volume = strings.Split(trimmed, ": ")[1]
40+
}
41+
}
42+
43+
cmd = exec.Command("installer", "-pkg", path, "-target", volume)
44+
45+
err = cmd.Start()
46+
if err != nil {
47+
return err
48+
}
49+
err = cmd.Process.Release()
50+
51+
return err
52+
}
53+
54+
func (u *UpdateManager) updateHomeBrew() error {
55+
// Homebrew must be run as a non-root user
56+
// To find out which user installed NetBird using HomeBrew we can check the owner of our brew tap directory
57+
fileInfo, err := os.Stat("/opt/homebrew/Library/Taps/netbirdio/homebrew-tap/")
58+
if err != nil {
59+
return err
60+
}
61+
62+
fileSysInfo, ok := fileInfo.Sys().(*syscall.Stat_t)
63+
if !ok {
64+
return fmt.Errorf("Error checking file owner, sysInfo type is %T not *syscall.Stat_t", fileInfo.Sys())
65+
}
66+
67+
// Get user name from UID
68+
cmd := exec.Command("id", "-nu", fmt.Sprintf("%d", fileSysInfo.Uid))
69+
out, err := cmd.CombinedOutput()
70+
if err != nil {
71+
return err
72+
}
73+
userName := strings.TrimSpace(string(out))
74+
75+
// Get user HOME, required for brew to run correctly
76+
// https://github.com/Homebrew/brew/issues/15833
77+
cmd = exec.Command("sudo", "-u", userName, "sh", "-c", "echo $HOME")
78+
out, err = cmd.CombinedOutput()
79+
if err != nil {
80+
return err
81+
}
82+
83+
homeDir := strings.TrimSpace(string(out))
84+
// Homebrew does not support installing specific versions
85+
// Thus it will always update to latest and ignore targetVersion
86+
cmd = exec.Command("sudo", "-u", userName, "/opt/homebrew/bin/brew", "upgrade", "netbirdio/tap/netbird")
87+
cmd.Env = append(cmd.Env, "HOME="+homeDir)
88+
89+
// Homebrew upgrade doesn't restart the client on its own
90+
// So we have to wait for it to finish running and ensure it's done
91+
// And then basically restart the netbird service
92+
out, err = cmd.CombinedOutput()
93+
if err != nil {
94+
log.Errorf("Error running brew upgrade, output: %v", string(out))
95+
return err
96+
}
97+
98+
currentPID := os.Getpid()
99+
100+
// Restart netbird service after the fact
101+
// This is a workaround since attempting to restart using launchctl will kill the service and die before starting
102+
// the service again as it's a child process
103+
// using SigTerm should ensure a clean shutdown
104+
cmd = exec.Command("kill", "-15", fmt.Sprintf("%d", currentPID))
105+
err = cmd.Run()
106+
// We're dying now, which should restart us
107+
108+
return err
109+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build freebsd
2+
3+
package updatemanager
4+
5+
func (u *UpdateManager) triggerUpdate(targetVersion string) error {
6+
// TODO: Implement
7+
return nil
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build linux
2+
3+
package updatemanager
4+
5+
func (u *UpdateManager) triggerUpdate(targetVersion string) error {
6+
// TODO: Implement
7+
return nil
8+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//go:build windows
2+
3+
package updatemanager
4+
5+
import (
6+
"os/exec"
7+
"strings"
8+
9+
log "github.com/sirupsen/logrus"
10+
11+
"golang.org/x/sys/windows/registry"
12+
)
13+
14+
const (
15+
msiDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%s/netbird_installer_%s_windows_amd64.msi"
16+
exeDownloadURL = "https://github.com/netbirdio/netbird/releases/download/v%s/netbird_installer_%s_windows_amd64.exe"
17+
)
18+
19+
func (u *UpdateManager) triggerUpdate(targetVersion string) error {
20+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Netbird`, registry.QUERY_VALUE)
21+
if err != nil && strings.Contains(err.Error(), "system cannot find the file specified") {
22+
// Installed using MSI installer
23+
path, err := downloadFileToTemporaryDir(strings.ReplaceAll(msiDownloadURL, "%s", targetVersion))
24+
if err != nil {
25+
return err
26+
}
27+
cmd := exec.Command("msiexec", "/quiet", "/i", path)
28+
err = cmd.Run()
29+
return err
30+
} else if err != nil {
31+
return err
32+
}
33+
err = k.Close()
34+
if err != nil {
35+
log.Warnf("Error closing registry key: %v", err)
36+
}
37+
38+
// Installed using EXE installer
39+
path, err := downloadFileToTemporaryDir(strings.ReplaceAll(exeDownloadURL, "%s", targetVersion))
40+
if err != nil {
41+
return err
42+
}
43+
cmd := exec.Command(path, "/S")
44+
err = cmd.Start()
45+
if err != nil {
46+
return err
47+
}
48+
err = cmd.Process.Release()
49+
50+
return err
51+
}

0 commit comments

Comments
 (0)