Files
netbird/version/update.go
Maycon Santos 5343bee7b2 [management] check and log on new management version (#4029)
This PR enhances the version checker to send a custom User-Agent header when polling for updates, and configures both the management CLI and client UI to use distinct agents. 

- NewUpdate now takes an `httpAgent` string to set the User-Agent header.
- `fetchVersion` builds a custom HTTP request (instead of `http.Get`) and sets the User-Agent.
- Management CLI and client UI now pass `"nb/management"` and `"nb/client-ui"` respectively to NewUpdate.
- Tests updated to supply an `httpAgent` constant.
- Logs if there is a new version available for management
2025-06-22 16:44:33 +02:00

197 lines
4.0 KiB
Go

package version
import (
"io"
"net/http"
"sync"
"time"
goversion "github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus"
)
const (
fetchPeriod = 30 * time.Minute
)
var (
versionURL = "https://pkgs.netbird.io/releases/latest/version"
)
// Update fetch the version info periodically and notify the onUpdateListener in case the UI version or the
// daemon version are deprecated
type Update struct {
httpAgent string
uiVersion *goversion.Version
daemonVersion *goversion.Version
latestAvailable *goversion.Version
versionsLock sync.Mutex
fetchTicker *time.Ticker
fetchDone chan struct{}
onUpdateListener func()
listenerLock sync.Mutex
}
// NewUpdate instantiate Update and start to fetch the new version information
func NewUpdate(httpAgent string) *Update {
currentVersion, err := goversion.NewVersion(version)
if err != nil {
currentVersion, _ = goversion.NewVersion("0.0.0")
}
latestAvailable, _ := goversion.NewVersion("0.0.0")
u := &Update{
httpAgent: httpAgent,
latestAvailable: latestAvailable,
uiVersion: currentVersion,
fetchTicker: time.NewTicker(fetchPeriod),
fetchDone: make(chan struct{}),
}
go u.startFetcher()
return u
}
// StopWatch stop the version info fetch loop
func (u *Update) StopWatch() {
u.fetchTicker.Stop()
select {
case u.fetchDone <- struct{}{}:
default:
}
}
// SetDaemonVersion update the currently running daemon version. If new version is available it will trigger
// the onUpdateListener
func (u *Update) SetDaemonVersion(newVersion string) bool {
daemonVersion, err := goversion.NewVersion(newVersion)
if err != nil {
daemonVersion, _ = goversion.NewVersion("0.0.0")
}
u.versionsLock.Lock()
if u.daemonVersion != nil && u.daemonVersion.Equal(daemonVersion) {
u.versionsLock.Unlock()
return false
}
u.daemonVersion = daemonVersion
u.versionsLock.Unlock()
return u.checkUpdate()
}
// SetOnUpdateListener set new update listener
func (u *Update) SetOnUpdateListener(updateFn func()) {
u.listenerLock.Lock()
defer u.listenerLock.Unlock()
u.onUpdateListener = updateFn
if u.isUpdateAvailable() {
u.onUpdateListener()
}
}
func (u *Update) startFetcher() {
if changed := u.fetchVersion(); changed {
u.checkUpdate()
}
for {
select {
case <-u.fetchDone:
return
case <-u.fetchTicker.C:
if changed := u.fetchVersion(); changed {
u.checkUpdate()
}
}
}
}
func (u *Update) fetchVersion() bool {
log.Debugf("fetching version info from %s", versionURL)
req, err := http.NewRequest("GET", versionURL, nil)
if err != nil {
log.Errorf("failed to create request for version info: %s", err)
return false
}
req.Header.Set("User-Agent", u.httpAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Errorf("failed to fetch version info: %s", err)
return false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Errorf("invalid status code: %d", resp.StatusCode)
return false
}
if resp.ContentLength > 100 {
log.Errorf("too large response: %d", resp.ContentLength)
return false
}
content, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf("failed to read content: %s", err)
return false
}
latestAvailable, err := goversion.NewVersion(string(content))
if err != nil {
log.Errorf("failed to parse the version string: %s", err)
return false
}
u.versionsLock.Lock()
defer u.versionsLock.Unlock()
if u.latestAvailable.Equal(latestAvailable) {
return false
}
u.latestAvailable = latestAvailable
return true
}
func (u *Update) checkUpdate() bool {
if !u.isUpdateAvailable() {
return false
}
u.listenerLock.Lock()
defer u.listenerLock.Unlock()
if u.onUpdateListener == nil {
return true
}
go u.onUpdateListener()
return true
}
func (u *Update) isUpdateAvailable() bool {
u.versionsLock.Lock()
defer u.versionsLock.Unlock()
if u.latestAvailable.GreaterThan(u.uiVersion) {
return true
}
if u.daemonVersion == nil {
return false
}
if u.latestAvailable.GreaterThan(u.daemonVersion) {
return true
}
return false
}