mirror of https://github.com/jetkvm/kvm.git
212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
package ota
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
)
|
|
|
|
// UpdateReleaseAPIEndpoint updates the release API endpoint
|
|
func (s *State) UpdateReleaseAPIEndpoint(endpoint string) {
|
|
s.releaseAPIEndpoint = endpoint
|
|
}
|
|
|
|
// GetReleaseAPIEndpoint returns the release API endpoint
|
|
func (s *State) GetReleaseAPIEndpoint() string {
|
|
return s.releaseAPIEndpoint
|
|
}
|
|
|
|
func (s *State) fetchUpdateMetadata(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateMetadata, error) {
|
|
metadata := &UpdateMetadata{}
|
|
|
|
updateURL, err := url.Parse(s.releaseAPIEndpoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing update metadata URL: %w", err)
|
|
}
|
|
|
|
query := updateURL.Query()
|
|
query.Set("deviceId", deviceID)
|
|
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
|
updateURL.RawQuery = query.Encode()
|
|
|
|
logger.Info().Str("url", updateURL.String()).Msg("Checking for updates")
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", updateURL.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %w", err)
|
|
}
|
|
|
|
client := s.client()
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding response: %w", err)
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
func (s *State) TryUpdate(ctx context.Context, deviceID string, includePreRelease bool) error {
|
|
scopedLogger := s.l.With().
|
|
Str("deviceID", deviceID).
|
|
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
|
Logger()
|
|
|
|
scopedLogger.Info().Msg("Trying to update...")
|
|
if s.updating {
|
|
return fmt.Errorf("update already in progress")
|
|
}
|
|
|
|
s.updating = true
|
|
s.onProgressUpdate()
|
|
|
|
defer func() {
|
|
s.updating = false
|
|
s.onProgressUpdate()
|
|
}()
|
|
|
|
appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, deviceID, includePreRelease)
|
|
if err != nil {
|
|
return s.componentUpdateError("Error checking for updates", err, &scopedLogger)
|
|
}
|
|
|
|
s.metadataFetchedAt = time.Now()
|
|
s.onProgressUpdate()
|
|
|
|
if appUpdate.available {
|
|
appUpdate.pending = true
|
|
}
|
|
|
|
if systemUpdate.available {
|
|
systemUpdate.pending = true
|
|
}
|
|
|
|
if appUpdate.pending {
|
|
scopedLogger.Info().
|
|
Str("url", appUpdate.url).
|
|
Str("hash", appUpdate.hash).
|
|
Msg("App update available")
|
|
|
|
if err := s.updateApp(ctx, appUpdate); err != nil {
|
|
return s.componentUpdateError("Error updating app", err, &scopedLogger)
|
|
}
|
|
} else {
|
|
scopedLogger.Info().Msg("App is up to date")
|
|
}
|
|
|
|
if systemUpdate.pending {
|
|
if err := s.updateSystem(ctx, systemUpdate); err != nil {
|
|
return s.componentUpdateError("Error updating system", err, &scopedLogger)
|
|
}
|
|
} else {
|
|
scopedLogger.Info().Msg("System is up to date")
|
|
}
|
|
|
|
if s.rebootNeeded {
|
|
scopedLogger.Info().Msg("System Rebooting due to OTA update")
|
|
|
|
postRebootAction := &PostRebootAction{
|
|
HealthCheck: "/device/status",
|
|
RedirectUrl: fmt.Sprintf("/settings/general/update?version=%s", systemUpdate.version),
|
|
}
|
|
|
|
if err := s.reboot(true, postRebootAction, 10*time.Second); err != nil {
|
|
return s.componentUpdateError("Error requesting reboot", err, &scopedLogger)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *State) getUpdateStatus(
|
|
ctx context.Context,
|
|
deviceID string,
|
|
includePreRelease bool,
|
|
) (
|
|
appUpdate *componentUpdateStatus,
|
|
systemUpdate *componentUpdateStatus,
|
|
err error,
|
|
) {
|
|
appUpdate = &componentUpdateStatus{}
|
|
systemUpdate = &componentUpdateStatus{}
|
|
err = nil
|
|
|
|
// Get local versions
|
|
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error getting local version: %w", err)
|
|
}
|
|
appUpdate.localVersion = appVersionLocal.String()
|
|
systemUpdate.localVersion = systemVersionLocal.String()
|
|
|
|
// Get remote metadata
|
|
remoteMetadata, err := s.fetchUpdateMetadata(ctx, deviceID, includePreRelease)
|
|
if err != nil {
|
|
err = fmt.Errorf("error checking for updates: %w", err)
|
|
return
|
|
}
|
|
appUpdate.url = remoteMetadata.AppURL
|
|
appUpdate.hash = remoteMetadata.AppHash
|
|
appUpdate.version = remoteMetadata.AppVersion
|
|
|
|
systemUpdate.url = remoteMetadata.SystemURL
|
|
systemUpdate.hash = remoteMetadata.SystemHash
|
|
systemUpdate.version = remoteMetadata.SystemVersion
|
|
|
|
// Get remote versions
|
|
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
|
if err != nil {
|
|
err = fmt.Errorf("error parsing remote system version: %w", err)
|
|
return
|
|
}
|
|
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
|
|
|
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
|
if err != nil {
|
|
err = fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
|
return
|
|
}
|
|
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
|
|
|
|
// Handle pre-release updates
|
|
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
|
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
|
|
|
if isRemoteSystemPreRelease && !includePreRelease {
|
|
systemUpdate.available = false
|
|
}
|
|
if isRemoteAppPreRelease && !includePreRelease {
|
|
appUpdate.available = false
|
|
}
|
|
|
|
s.componentUpdateStatuses["app"] = *appUpdate
|
|
s.componentUpdateStatuses["system"] = *systemUpdate
|
|
|
|
return
|
|
}
|
|
|
|
// GetUpdateStatus returns the current update status (for backwards compatibility)
|
|
func (s *State) GetUpdateStatus(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateStatus, error) {
|
|
_, _, err := s.getUpdateStatus(ctx, deviceID, includePreRelease)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting update status: %w", err)
|
|
}
|
|
|
|
return s.ToUpdateStatus(), nil
|
|
}
|