Replace dataroot module with BinDir and DataDir on instance, adapt modules

This commit is contained in:
Daniel
2024-11-06 10:48:02 +01:00
parent 0f3f3c360f
commit 7bc1c3b764
39 changed files with 819 additions and 482 deletions

View File

@@ -55,7 +55,7 @@ func (d *Downloader) updateIndex(ctx context.Context) error {
break
}
log.Warningf("updates: failed to update index from %q: %s", url, err)
log.Warningf("updates/%s: failed to update index from %q: %s", d.u.cfg.Name, url, err)
err = fmt.Errorf("update index file from %q: %s", url, err)
}
if err != nil {
@@ -111,7 +111,7 @@ func (d *Downloader) gatherExistingFiles(dir string) error {
// Read full file.
fileData, err := os.ReadFile(fullpath)
if err != nil {
log.Debugf("updates: failed to read file %q while searching for existing files: %w", fullpath, err)
log.Debugf("updates/%s: failed to read file %q while searching for existing files: %w", d.u.cfg.Name, fullpath, err)
return fmt.Errorf("failed to read file %s: %w", fullpath, err)
}
@@ -150,7 +150,7 @@ artifacts:
if err == nil {
continue artifacts
}
log.Debugf("updates: failed to copy existing file %s: %w", artifact.Filename, err)
log.Debugf("updates/%s: failed to copy existing file %s: %w", d.u.cfg.Name, artifact.Filename, err)
}
// Try to download the artifact from one of the URLs.
@@ -182,7 +182,7 @@ artifacts:
return fmt.Errorf("rename %s after write: %w", artifact.Filename, err)
}
log.Infof("updates: downloaded and verified %s", artifact.Filename)
log.Infof("updates/%s: downloaded and verified %s", d.u.cfg.Name, artifact.Filename)
}
return nil
}

View File

@@ -35,7 +35,8 @@ type Artifact struct {
Unpack string `json:"Unpack,omitempty"`
Version string `json:"Version,omitempty"`
localFile string
localFile string
versionNum *semver.Version
}
// GetFileMode returns the required filesystem permission for the artifact.
@@ -52,6 +53,67 @@ func (a *Artifact) GetFileMode() os.FileMode {
return defaultFileMode
}
// Path returns the absolute path to the local file.
func (a *Artifact) Path() string {
return a.localFile
}
// SemVer returns the version of the artifact.
func (a *Artifact) SemVer() *semver.Version {
return a.versionNum
}
// IsNewerThan returns whether the artifact is newer than the given artifact.
// Returns true if the given artifact is nil.
// The second return value "ok" is false when version could not be compared.
// In this case, it is up to the caller to decide how to proceed.
func (a *Artifact) IsNewerThan(b *Artifact) (newer, ok bool) {
switch {
case a == nil:
return false, false
case b == nil:
return true, true
case a.versionNum == nil:
return false, false
case b.versionNum == nil:
return false, false
case a.versionNum.GreaterThan(b.versionNum):
return true, true
default:
return false, true
}
}
func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact {
copy := &Artifact{
Filename: a.Filename,
SHA256: a.SHA256,
URLs: a.URLs,
Platform: a.Platform,
Unpack: a.Unpack,
Version: a.Version,
localFile: filepath.Join(dir, a.Filename),
versionNum: a.versionNum,
}
// Make sure we have a version number.
switch {
case copy.versionNum != nil:
// Version already parsed.
case copy.Version != "":
// Need to parse version.
v, err := semver.NewVersion(copy.Version)
if err == nil {
copy.versionNum = v
}
default:
// No version defined, inherit index version.
copy.versionNum = indexVersion
}
return copy
}
// Index represents a collection of artifacts with metadata.
type Index struct {
Name string `json:"Name"`
@@ -90,16 +152,26 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error)
return nil, fmt.Errorf("parse index: %w", err)
}
// Initialize data.
err = index.init()
if err != nil {
return nil, err
}
return &index, nil
}
func (index *Index) init() error {
// Parse version number, if set.
if index.Version != "" {
versionNum, err := semver.NewVersion(index.Version)
if err != nil {
return nil, fmt.Errorf("invalid index version %q: %w", index.Version, err)
return fmt.Errorf("invalid index version %q: %w", index.Version, err)
}
index.versionNum = versionNum
}
// Filter artifacts by currnet platform.
// Filter artifacts by current platform.
filtered := make([]Artifact, 0)
for _, a := range index.Artifacts {
if a.Platform == "" || a.Platform == currentPlatform {
@@ -108,7 +180,19 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error)
}
index.Artifacts = filtered
return &index, nil
// Parse artifact version numbers.
for _, a := range index.Artifacts {
if a.Version != "" {
v, err := semver.NewVersion(a.Version)
if err == nil {
a.versionNum = v
}
} else {
a.versionNum = index.versionNum
}
}
return nil
}
// CanDoUpgrades returns whether the index is able to follow a secure upgrade path.

View File

@@ -45,6 +45,8 @@ var (
// Config holds the configuration for the updates module.
type Config struct {
// Name of the updater.
Name string
// Directory is the main directory where the currently to-be-used artifacts live.
Directory string
// DownloadDirectory is the directory where new artifacts are downloaded to and prepared for upgrading.
@@ -80,6 +82,8 @@ type Config struct {
func (cfg *Config) Check() error {
// Check if required fields are set.
switch {
case cfg.Name == "":
return errors.New("name must be set")
case cfg.Directory == "":
return errors.New("directory must be set")
case cfg.DownloadDirectory == "":
@@ -157,19 +161,22 @@ func New(instance instance, name string, cfg Config) (*Updater, error) {
// Load index.
index, err := LoadIndex(filepath.Join(cfg.Directory, cfg.IndexFile), cfg.Verify)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Errorf("updates: invalid index file, falling back to dir scan: %w", err)
}
// Fall back to scanning the directory.
index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"})
if err != nil {
return nil, fmt.Errorf("updates index load and dir scan failed: %w", err)
}
if err == nil {
module.index = index
return module, nil
}
module.index = index
// Fall back to scanning the directory.
if !errors.Is(err, os.ErrNotExist) {
log.Errorf("updates/%s: invalid index file, falling back to dir scan: %w", cfg.Name, err)
}
index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"})
if err == nil && index.init() == nil {
module.index = index
return module, nil
}
// Fall back to empty index.
return module, nil
}
@@ -207,7 +214,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
u.indexLock.Unlock()
// Check with local pointer to index.
if err := index.ShouldUpgradeTo(downloader.index); err != nil {
log.Infof("updates: no new or eligible update: %s", err)
log.Infof("updates/%s: no new or eligible update: %s", u.cfg.Name, err)
if u.cfg.Notify && u.instance.Notifications() != nil {
u.instance.Notifications().NotifyInfo(
noNewUpdateNotificationID,
@@ -247,12 +254,12 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
// Download any remaining needed files.
// If everything is already found in the download directory, then this is a no-op.
log.Infof("updates: downloading new version: %s %s", downloader.index.Name, downloader.index.Version)
log.Infof("updates/%s: downloading new version: %s %s", u.cfg.Name, downloader.index.Name, downloader.index.Version)
err = downloader.downloadArtifacts(w.Ctx())
if err != nil {
log.Errorf("updates: failed to download update: %s", err)
log.Errorf("updates/%s: failed to download update: %s", u.cfg.Name, err)
if err := u.deleteUnfinishedFiles(u.cfg.DownloadDirectory); err != nil {
log.Debugf("updates: failed to delete unfinished files in download directory %s", u.cfg.DownloadDirectory)
log.Debugf("updates/%s: failed to delete unfinished files in download directory %s", u.cfg.Name, u.cfg.DownloadDirectory)
}
return fmt.Errorf("downloading failed: %w", err)
}
@@ -282,7 +289,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
err = u.upgrade(downloader, ignoreVersion)
if err != nil {
if err := u.deleteUnfinishedFiles(u.cfg.PurgeDirectory); err != nil {
log.Debugf("updates: failed to delete unfinished files in purge directory %s", u.cfg.PurgeDirectory)
log.Debugf("updates/%s: failed to delete unfinished files in purge directory %s", u.cfg.Name, u.cfg.PurgeDirectory)
}
return err
}
@@ -334,6 +341,14 @@ func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error {
return nil
}
// ForceUpdate executes a forced update and upgrade directly and synchronously
// and is intended to be used only within a tool, not a service.
func (u *Updater) ForceUpdate() error {
return u.m.Do("update and upgrade", func(w *mgr.WorkerCtx) error {
return u.updateAndUpgrade(w, u.cfg.IndexURLs, true, true)
})
}
// UpdateFromURL installs an update from the provided url.
func (u *Updater) UpdateFromURL(url string) error {
u.m.Go("custom update from url", func(w *mgr.WorkerCtx) error {
@@ -383,10 +398,15 @@ func (u *Updater) GetMainDir() string {
}
// GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found.
func (u *Updater) GetFile(name string) (string, error) {
func (u *Updater) GetFile(name string) (*Artifact, error) {
u.indexLock.Lock()
defer u.indexLock.Unlock()
// Check if any index is active.
if u.index == nil {
return nil, ErrNotFound
}
for _, artifact := range u.index.Artifacts {
switch {
case artifact.Filename != name:
@@ -396,11 +416,11 @@ func (u *Updater) GetFile(name string) (string, error) {
// Platforms are usually pre-filtered, but just to be sure.
default:
// Artifact matches!
return filepath.Join(u.cfg.Directory, artifact.Filename), nil
return artifact.export(u.cfg.Directory, u.index.versionNum), nil
}
}
return "", ErrNotFound
return nil, ErrNotFound
}
// Stop stops the module.

View File

@@ -43,7 +43,7 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
}
// Recovery failed too.
return fmt.Errorf("upgrade (including recovery) failed: %s", upgradeError)
return fmt.Errorf("upgrade (including recovery) failed: %s", u.cfg.Name, upgradeError)
}
func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) error {
@@ -60,7 +60,9 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e
}
// Move current version files into purge folder.
log.Debugf("updates: removing the old version (v%s from %s)", u.index.Version, u.index.Published)
if u.index != nil {
log.Debugf("updates/%s: removing the old version (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
}
files, err := os.ReadDir(u.cfg.Directory)
if err != nil {
return fmt.Errorf("read current directory: %w", err)
@@ -74,17 +76,17 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e
// Otherwise, move file to purge dir.
src := filepath.Join(u.cfg.Directory, file.Name())
dst := filepath.Join(u.cfg.PurgeDirectory, file.Name())
err := moveFile(src, dst, "", file.Type().Perm())
err := u.moveFile(src, dst, "", file.Type().Perm())
if err != nil {
return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err)
}
}
// Move the new index file into main directory.
log.Debugf("updates: installing the new version (v%s from %s)", u.index.Version, u.index.Published)
log.Debugf("updates/%s: installing the new version (v%s from %s)", u.cfg.Name, downloader.index.Version, downloader.index.Published)
src := filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile)
dst := filepath.Join(u.cfg.Directory, u.cfg.IndexFile)
err = moveFile(src, dst, "", defaultFileMode)
err = u.moveFile(src, dst, "", defaultFileMode)
if err != nil {
return fmt.Errorf("failed to move index file to %s: %w", dst, err)
}
@@ -93,30 +95,30 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e
for _, artifact := range downloader.index.Artifacts {
src = filepath.Join(u.cfg.DownloadDirectory, artifact.Filename)
dst = filepath.Join(u.cfg.Directory, artifact.Filename)
err = moveFile(src, dst, artifact.SHA256, artifact.GetFileMode())
err = u.moveFile(src, dst, artifact.SHA256, artifact.GetFileMode())
if err != nil {
return fmt.Errorf("failed to move file %s: %w", artifact.Filename, err)
} else {
log.Debugf("updates: %s moved", artifact.Filename)
log.Debugf("updates/%s: %s moved", u.cfg.Name, artifact.Filename)
}
}
// Set new index on module.
u.index = downloader.index
log.Infof("updates: update complete (v%s from %s)", u.index.Version, u.index.Published)
log.Infof("updates/%s: update complete (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
return nil
}
// moveFile moves a file and falls back to copying if it fails.
func moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error {
func (u *Updater) moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error {
// Try to simply move file.
err := os.Rename(currentPath, newPath)
if err == nil {
// Moving was successful, return.
return nil
}
log.Tracef("updates: failed to move to %q, falling back to copy+delete: %w", newPath, err)
log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %w", u.cfg.Name, newPath, err)
// Copy and check the checksum while we are at it.
err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, fileMode)
@@ -139,10 +141,10 @@ func (u *Updater) recoverFromFailedUpgrade() error {
for _, file := range files {
purgedFile := filepath.Join(u.cfg.PurgeDirectory, file.Name())
activeFile := filepath.Join(u.cfg.Directory, file.Name())
err := moveFile(purgedFile, activeFile, "", file.Type().Perm())
err := u.moveFile(purgedFile, activeFile, "", file.Type().Perm())
if err != nil {
// Only warn and continue to recover as many files as possible.
log.Warningf("updates: failed to roll back file %s: %w", file.Name(), err)
log.Warningf("updates/%s: failed to roll back file %s: %w", u.cfg.Name, file.Name(), err)
}
}
@@ -176,18 +178,18 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error {
case strings.HasSuffix(e.Name(), ".download"):
path := filepath.Join(dir, e.Name())
log.Warningf("updates: deleting unfinished download file: %s\n", path)
log.Warningf("updates/%s: deleting unfinished download file: %s", u.cfg.Name, path)
err := os.Remove(path)
if err != nil {
log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err)
log.Errorf("updates/%s: failed to delete unfinished download file %s: %s", u.cfg.Name, path, err)
}
case strings.HasSuffix(e.Name(), ".copy"):
path := filepath.Join(dir, e.Name())
log.Warningf("updates: deleting unfinished copied file: %s\n", path)
log.Warningf("updates/%s: deleting unfinished copied file: %s", u.cfg.Name, path)
err := os.Remove(path)
if err != nil {
log.Errorf("updates: failed to delete unfinished copied file %s: %s", path, err)
log.Errorf("updates/%s: failed to delete unfinished copied file %s: %s", u.cfg.Name, path, err)
}
}
}