mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
375 lines
8.9 KiB
Go
375 lines
8.9 KiB
Go
//go:build windows || darwin
|
|
|
|
package updatemanager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
v "github.com/hashicorp/go-version"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
|
"github.com/netbirdio/netbird/client/internal/updatemanager/installer"
|
|
cProto "github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/version"
|
|
)
|
|
|
|
const (
|
|
latestVersion = "latest"
|
|
// this version will be ignored
|
|
developmentVersion = "development"
|
|
)
|
|
|
|
var errNoUpdateState = errors.New("no update state found")
|
|
|
|
type UpdateState struct {
|
|
PreUpdateVersion string
|
|
TargetVersion string
|
|
}
|
|
|
|
func (u UpdateState) Name() string {
|
|
return "autoUpdate"
|
|
}
|
|
|
|
type Manager struct {
|
|
statusRecorder *peer.Status
|
|
stateManager *statemanager.Manager
|
|
|
|
lastTrigger time.Time
|
|
mgmUpdateChan chan struct{}
|
|
updateChannel chan struct{}
|
|
currentVersion string
|
|
update UpdateInterface
|
|
wg sync.WaitGroup
|
|
|
|
cancel context.CancelFunc
|
|
|
|
expectedVersion *v.Version
|
|
updateToLatestVersion bool
|
|
|
|
// updateMutex protect update and expectedVersion fields
|
|
updateMutex sync.Mutex
|
|
|
|
triggerUpdateFn func(context.Context, string) error
|
|
}
|
|
|
|
func NewManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) (*Manager, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
isBrew := !installer.TypeOfInstaller(context.Background()).Downloadable()
|
|
if isBrew {
|
|
log.Warnf("auto-update disabled on Home Brew installation")
|
|
return nil, fmt.Errorf("auto-update not supported on Home Brew installation yet")
|
|
}
|
|
}
|
|
return newManager(statusRecorder, stateManager)
|
|
}
|
|
|
|
func newManager(statusRecorder *peer.Status, stateManager *statemanager.Manager) (*Manager, error) {
|
|
manager := &Manager{
|
|
statusRecorder: statusRecorder,
|
|
stateManager: stateManager,
|
|
mgmUpdateChan: make(chan struct{}, 1),
|
|
updateChannel: make(chan struct{}, 1),
|
|
currentVersion: version.NetbirdVersion(),
|
|
update: version.NewUpdate("nb/client"),
|
|
}
|
|
manager.triggerUpdateFn = manager.triggerUpdate
|
|
|
|
stateManager.RegisterState(&UpdateState{})
|
|
|
|
return manager, nil
|
|
}
|
|
|
|
// CheckUpdateSuccess checks if the update was successful and send a notification.
|
|
// It works without to start the update manager.
|
|
func (m *Manager) CheckUpdateSuccess(ctx context.Context) {
|
|
reason := m.lastResultErrReason()
|
|
if reason != "" {
|
|
m.statusRecorder.PublishEvent(
|
|
cProto.SystemEvent_ERROR,
|
|
cProto.SystemEvent_SYSTEM,
|
|
"Auto-update failed",
|
|
fmt.Sprintf("Auto-update failed: %s", reason),
|
|
nil,
|
|
)
|
|
}
|
|
|
|
updateState, err := m.loadAndDeleteUpdateState(ctx)
|
|
if err != nil {
|
|
if errors.Is(err, errNoUpdateState) {
|
|
return
|
|
}
|
|
log.Errorf("failed to load update state: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Debugf("auto-update state loaded, %v", *updateState)
|
|
|
|
if updateState.TargetVersion == m.currentVersion {
|
|
m.statusRecorder.PublishEvent(
|
|
cProto.SystemEvent_INFO,
|
|
cProto.SystemEvent_SYSTEM,
|
|
"Auto-update completed",
|
|
fmt.Sprintf("Your NetBird Client was auto-updated to version %s", m.currentVersion),
|
|
nil,
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Start(ctx context.Context) {
|
|
if m.cancel != nil {
|
|
log.Errorf("Manager already started")
|
|
return
|
|
}
|
|
|
|
m.update.SetDaemonVersion(m.currentVersion)
|
|
m.update.SetOnUpdateListener(func() {
|
|
select {
|
|
case m.updateChannel <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
go m.update.StartFetcher()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
m.cancel = cancel
|
|
|
|
m.wg.Add(1)
|
|
go m.updateLoop(ctx)
|
|
}
|
|
|
|
func (m *Manager) SetVersion(expectedVersion string) {
|
|
log.Infof("set expected agent version for upgrade: %s", expectedVersion)
|
|
if m.cancel == nil {
|
|
log.Errorf("manager not started")
|
|
return
|
|
}
|
|
|
|
m.updateMutex.Lock()
|
|
defer m.updateMutex.Unlock()
|
|
|
|
if expectedVersion == "" {
|
|
log.Errorf("empty expected version provided")
|
|
m.expectedVersion = nil
|
|
m.updateToLatestVersion = false
|
|
return
|
|
}
|
|
|
|
if expectedVersion == latestVersion {
|
|
m.updateToLatestVersion = true
|
|
m.expectedVersion = nil
|
|
} else {
|
|
expectedSemVer, err := v.NewVersion(expectedVersion)
|
|
if err != nil {
|
|
log.Errorf("error parsing version: %v", err)
|
|
return
|
|
}
|
|
if m.expectedVersion != nil && m.expectedVersion.Equal(expectedSemVer) {
|
|
return
|
|
}
|
|
m.expectedVersion = expectedSemVer
|
|
m.updateToLatestVersion = false
|
|
}
|
|
|
|
select {
|
|
case m.mgmUpdateChan <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Stop() {
|
|
if m.cancel == nil {
|
|
return
|
|
}
|
|
|
|
m.cancel()
|
|
m.updateMutex.Lock()
|
|
if m.update != nil {
|
|
m.update.StopWatch()
|
|
m.update = nil
|
|
}
|
|
m.updateMutex.Unlock()
|
|
|
|
m.wg.Wait()
|
|
}
|
|
|
|
func (m *Manager) onContextCancel() {
|
|
if m.cancel == nil {
|
|
return
|
|
}
|
|
|
|
m.updateMutex.Lock()
|
|
defer m.updateMutex.Unlock()
|
|
if m.update != nil {
|
|
m.update.StopWatch()
|
|
m.update = nil
|
|
}
|
|
}
|
|
|
|
func (m *Manager) updateLoop(ctx context.Context) {
|
|
defer m.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
m.onContextCancel()
|
|
return
|
|
case <-m.mgmUpdateChan:
|
|
case <-m.updateChannel:
|
|
log.Infof("fetched new version info")
|
|
}
|
|
|
|
m.handleUpdate(ctx)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) handleUpdate(ctx context.Context) {
|
|
var updateVersion *v.Version
|
|
|
|
m.updateMutex.Lock()
|
|
if m.update == nil {
|
|
m.updateMutex.Unlock()
|
|
return
|
|
}
|
|
|
|
expectedVersion := m.expectedVersion
|
|
useLatest := m.updateToLatestVersion
|
|
curLatestVersion := m.update.LatestVersion()
|
|
m.updateMutex.Unlock()
|
|
|
|
switch {
|
|
// Resolve "latest" to actual version
|
|
case useLatest:
|
|
if curLatestVersion == nil {
|
|
log.Tracef("latest version not fetched yet")
|
|
return
|
|
}
|
|
updateVersion = curLatestVersion
|
|
// Update to specific version
|
|
case expectedVersion != nil:
|
|
updateVersion = expectedVersion
|
|
default:
|
|
log.Debugf("no expected version information set")
|
|
return
|
|
}
|
|
|
|
log.Debugf("checking update option, current version: %s, target version: %s", m.currentVersion, updateVersion)
|
|
if !m.shouldUpdate(updateVersion) {
|
|
return
|
|
}
|
|
|
|
m.lastTrigger = time.Now()
|
|
log.Infof("Auto-update triggered, current version: %s, target version: %s", m.currentVersion, updateVersion)
|
|
m.statusRecorder.PublishEvent(
|
|
cProto.SystemEvent_CRITICAL,
|
|
cProto.SystemEvent_SYSTEM,
|
|
"Automatically updating client",
|
|
"Your client version is older than auto-update version set in Management, updating client now.",
|
|
nil,
|
|
)
|
|
|
|
m.statusRecorder.PublishEvent(
|
|
cProto.SystemEvent_CRITICAL,
|
|
cProto.SystemEvent_SYSTEM,
|
|
"",
|
|
"",
|
|
map[string]string{"progress_window": "show", "version": updateVersion.String()},
|
|
)
|
|
|
|
updateState := UpdateState{
|
|
PreUpdateVersion: m.currentVersion,
|
|
TargetVersion: updateVersion.String(),
|
|
}
|
|
|
|
if err := m.stateManager.UpdateState(updateState); err != nil {
|
|
log.Warnf("failed to update state: %v", err)
|
|
} else {
|
|
if err = m.stateManager.PersistState(ctx); err != nil {
|
|
log.Warnf("failed to persist state: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := m.triggerUpdateFn(ctx, updateVersion.String()); err != nil {
|
|
log.Errorf("Error triggering auto-update: %v", err)
|
|
m.statusRecorder.PublishEvent(
|
|
cProto.SystemEvent_ERROR,
|
|
cProto.SystemEvent_SYSTEM,
|
|
"Auto-update failed",
|
|
fmt.Sprintf("Auto-update failed: %v", err),
|
|
nil,
|
|
)
|
|
}
|
|
}
|
|
|
|
// loadAndDeleteUpdateState loads the update state, deletes it from storage, and returns it.
|
|
// Returns nil if no state exists.
|
|
func (m *Manager) loadAndDeleteUpdateState(ctx context.Context) (*UpdateState, error) {
|
|
stateType := &UpdateState{}
|
|
|
|
m.stateManager.RegisterState(stateType)
|
|
if err := m.stateManager.LoadState(stateType); err != nil {
|
|
return nil, fmt.Errorf("load state: %w", err)
|
|
}
|
|
|
|
state := m.stateManager.GetState(stateType)
|
|
if state == nil {
|
|
return nil, errNoUpdateState
|
|
}
|
|
|
|
updateState, ok := state.(*UpdateState)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to cast state to UpdateState")
|
|
}
|
|
|
|
if err := m.stateManager.DeleteState(updateState); err != nil {
|
|
return nil, fmt.Errorf("delete state: %w", err)
|
|
}
|
|
|
|
if err := m.stateManager.PersistState(ctx); err != nil {
|
|
return nil, fmt.Errorf("persist state: %w", err)
|
|
}
|
|
|
|
return updateState, nil
|
|
}
|
|
|
|
func (m *Manager) shouldUpdate(updateVersion *v.Version) bool {
|
|
if m.currentVersion == developmentVersion {
|
|
log.Debugf("skipping auto-update, running development version")
|
|
return false
|
|
}
|
|
currentVersion, err := v.NewVersion(m.currentVersion)
|
|
if err != nil {
|
|
log.Errorf("error checking for update, error parsing version `%s`: %v", m.currentVersion, err)
|
|
return false
|
|
}
|
|
if currentVersion.GreaterThanOrEqual(updateVersion) {
|
|
log.Infof("current version (%s) is equal to or higher than auto-update version (%s)", m.currentVersion, updateVersion)
|
|
return false
|
|
}
|
|
|
|
if time.Since(m.lastTrigger) < 5*time.Minute {
|
|
log.Debugf("skipping auto-update, last update was %s ago", time.Since(m.lastTrigger))
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (m *Manager) lastResultErrReason() string {
|
|
inst := installer.New()
|
|
result := installer.NewResultHandler(inst.TempDir())
|
|
return result.GetErrorResultReason()
|
|
}
|
|
|
|
func (m *Manager) triggerUpdate(ctx context.Context, targetVersion string) error {
|
|
inst := installer.New()
|
|
return inst.RunInstallation(ctx, targetVersion)
|
|
}
|