feat: restart to apply update, privileged helper, rollback

This commit is contained in:
fsylvain committed 2026-10-01 21:36:40 +02:00
1 parent 70460e4170
commit 54e8fd5fed
63 files changed
+8635 -164

No files matched your search

@@ -0,0 +1,382 @@
package useCases
import (
"RenewCMS/internal/domain/update"
"errors"
"fmt"
"path"
"time"
)
type ApplyRestartRequestUseCase struct {
requests update.RestartRequestStore
results update.ResultStore
running update.RunningInfoStore
files update.HelperFilesystem
service update.ServiceControl
health update.HealthChecker
verifier update.SignatureVerifier
clock update.Clock
logger update.Logger
layout update.HelperLayout
platform update.Platform
}
type ApplyRestartRequestDependencies struct {
Requests update.RestartRequestStore
Results update.ResultStore
Running update.RunningInfoStore
Files update.HelperFilesystem
Service update.ServiceControl
Health update.HealthChecker
Verifier update.SignatureVerifier
Clock update.Clock
Logger update.Logger
Layout update.HelperLayout
Platform update.Platform
}
func NewApplyRestartRequestUseCase(dependencies ApplyRestartRequestDependencies) *ApplyRestartRequestUseCase {
return &ApplyRestartRequestUseCase{
requests: dependencies.Requests,
results: dependencies.Results,
running: dependencies.Running,
files: dependencies.Files,
service: dependencies.Service,
health: dependencies.Health,
verifier: dependencies.Verifier,
clock: dependencies.Clock,
logger: dependencies.Logger,
layout: dependencies.Layout,
platform: dependencies.Platform,
}
}
type helperRun struct {
apply *ApplyRestartRequestUseCase
result update.Result
tempDir string
stagedTag string
}
type verifiedCopy struct {
binaryPath string
checksum string
}
func (a *ApplyRestartRequestUseCase) Apply() (err error) {
request, readErr := a.requests.Read()
if errors.Is(readErr, update.ErrNoRestartRequest) {
a.logger.Printf("no restart request found, nothing to do")
return nil
}
run := &helperRun{apply: a, result: update.Result{
Outcome: update.OutcomeRunning,
Kind: request.Kind,
Tag: request.Tag,
StartedAt: a.clock.Now().UTC(),
}}
defer run.recoverPanic(&err)
deleteErr := a.requests.Delete()
symlinkErr := a.symlinkedDataFile()
if info, found := a.running.Read(); found {
run.result.FromVersion = info.Version
}
run.record()
for _, refusal := range []error{readErr, deleteErr, symlinkErr} {
if refusal != nil {
return run.fail(refusal)
}
}
return run.dispatch()
}
func (a *ApplyRestartRequestUseCase) symlinkedDataFile() error {
for _, candidate := range []string{a.layout.ResultPath(), a.layout.ResultTempPath()} {
if info, err := a.files.Lstat(candidate); err == nil && info.IsSymlink {
return update.SymlinkError{Path: candidate}
}
}
return nil
}
func (r *helperRun) dispatch() error {
switch r.result.Kind {
case update.RestartKindRestart:
return r.plainRestart()
case update.RestartKindUpdate:
return r.installUpdate()
}
return r.fail(fmt.Errorf("unknown restart kind %q", r.result.Kind))
}
func (r *helperRun) plainRestart() error {
if !r.apply.restartAndAwait("") {
return r.failLoudly(errors.New("the site did not come back after the restart"))
}
return r.finish(update.OutcomeSucceeded)
}
func (r *helperRun) installUpdate() error {
a := r.apply
tag := r.result.Tag
if r.result.FromVersion == "" {
return r.fail(errors.New("the running version could not be read from update-running.json"))
}
if err := update.ValidateRestartTag(tag, r.result.FromVersion); err != nil {
return r.fail(fmt.Errorf("tag %q: %w", tag, err))
}
r.stagedTag = tag
assetName := update.AssetName(tag, a.platform)
if err := a.requireStagedFiles(tag, assetName); err != nil {
return r.fail(err)
}
verified, err := r.copyAndVerify(tag, assetName)
if err != nil {
return r.fail(err)
}
if err := a.swapBinary(verified); err != nil {
return r.fail(err)
}
if a.restartAndAwait(tag) {
return r.finish(update.OutcomeSucceeded)
}
return r.rollBack()
}
func (r *helperRun) copyAndVerify(tag string, assetName string) (verifiedCopy, error) {
a := r.apply
tempDir, err := a.files.MakePrivateTempDir()
if err != nil {
return verifiedCopy{}, err
}
r.tempDir = tempDir
stagedDir := a.layout.StagedDir(tag)
for _, name := range []string{assetName, update.ChecksumsAssetName, update.SignatureAssetName} {
original := path.Join(stagedDir, name)
if err := a.files.Copy(original, path.Join(tempDir, name), update.PrivateFileMode); err != nil {
return verifiedCopy{}, fmt.Errorf("copying %s: %w", original, err)
}
}
return a.verifyCopy(tempDir, assetName)
}
func (a *ApplyRestartRequestUseCase) verifyCopy(tempDir string, assetName string) (verifiedCopy, error) {
checksumsPath := path.Join(tempDir, update.ChecksumsAssetName)
if err := a.verifier.Verify(checksumsPath, path.Join(tempDir, update.SignatureAssetName)); err != nil {
return verifiedCopy{}, fmt.Errorf("signature check: %w", err)
}
checksums, err := a.files.ReadFile(checksumsPath)
if err != nil {
return verifiedCopy{}, err
}
expected, found := update.ChecksumFor(string(checksums), assetName)
if !found {
return verifiedCopy{}, fmt.Errorf("checksums.txt has no line for %q", assetName)
}
binaryPath := path.Join(tempDir, assetName)
content, err := a.files.ReadFile(binaryPath)
if err != nil {
return verifiedCopy{}, err
}
if update.SHA256Hex(content) != expected {
return verifiedCopy{}, fmt.Errorf("sha-256 mismatch for the staged %q", assetName)
}
return verifiedCopy{binaryPath: binaryPath, checksum: expected}, nil
}
func (a *ApplyRestartRequestUseCase) requireStagedFiles(tag string, assetName string) error {
for _, directory := range []string{a.layout.UpdatesDir(), a.layout.StagedDir(tag)} {
if err := a.requireDirectory(directory); err != nil {
return err
}
}
for _, name := range []string{assetName, update.ChecksumsAssetName, update.SignatureAssetName} {
if err := a.requireRegularFile(path.Join(a.layout.StagedDir(tag), name)); err != nil {
return err
}
}
return nil
}
func (a *ApplyRestartRequestUseCase) requireDirectory(target string) error {
return a.requireEntry(target, "a directory", func(info update.FileInfo) bool { return info.IsDir })
}
func (a *ApplyRestartRequestUseCase) requireRegularFile(target string) error {
return a.requireEntry(target, "a regular file", func(info update.FileInfo) bool { return info.IsRegular })
}
func (a *ApplyRestartRequestUseCase) requireEntry(target string, description string, matches func(update.FileInfo) bool) error {
info, err := a.files.Lstat(target)
if err != nil {
return fmt.Errorf("%s: %w", target, err)
}
if info.IsSymlink {
return update.SymlinkError{Path: target}
}
if !matches(info) {
return fmt.Errorf("%s is not %s", target, description)
}
return nil
}
func (a *ApplyRestartRequestUseCase) swapBinary(verified verifiedCopy) error {
binary := a.layout.BinaryPath
if err := a.replaceViaTemp(binary, a.layout.PreviousPath(), binary+update.PreviousTempSuffix); err != nil {
return fmt.Errorf("keeping the previous binary: %w", err)
}
installTemp := binary + update.InstallTempSuffix
a.removeQuietly(installTemp)
if err := a.files.Copy(verified.binaryPath, installTemp, update.ExecutableMode); err != nil {
a.removeQuietly(installTemp)
return fmt.Errorf("writing the new binary: %w", err)
}
if err := a.installIfUnchanged(installTemp, verified.checksum); err != nil {
a.removeQuietly(installTemp)
return err
}
return nil
}
func (a *ApplyRestartRequestUseCase) installIfUnchanged(installTemp string, checksum string) error {
content, err := a.files.ReadFile(installTemp)
if err != nil {
return err
}
if update.SHA256Hex(content) != checksum {
return fmt.Errorf("%s changed after it was verified", installTemp)
}
if err := a.files.Chmod(installTemp, update.ExecutableMode); err != nil {
return err
}
if err := a.files.Rename(installTemp, a.layout.BinaryPath); err != nil {
return fmt.Errorf("installing the new binary: %w", err)
}
return nil
}
func (a *ApplyRestartRequestUseCase) replaceViaTemp(source string, destination string, temporary string) error {
a.removeQuietly(temporary)
if err := a.files.Copy(source, temporary, update.ExecutableMode); err != nil {
a.removeQuietly(temporary)
return err
}
if err := a.files.Rename(temporary, destination); err != nil {
a.removeQuietly(temporary)
return err
}
return nil
}
func (r *helperRun) rollBack() error {
a := r.apply
a.logger.Printf("version %s did not come up, restoring %s", r.result.Tag, a.layout.PreviousPath())
restoreTemp := a.layout.BinaryPath + update.RestoreTempSuffix
if err := a.replaceViaTemp(a.layout.PreviousPath(), a.layout.BinaryPath, restoreTemp); err != nil {
return r.failLoudly(fmt.Errorf("the previous binary could not be restored, the site may be down: %w", err))
}
if !a.restartAndAwait(r.result.FromVersion) {
return r.failLoudly(errors.New("the previous version did not come back after the rollback"))
}
return r.finish(update.OutcomeRolledBack)
}
func (a *ApplyRestartRequestUseCase) restartAndAwait(expectedVersion string) bool {
baseline, _ := a.running.Read()
restartedAt := a.clock.Now()
if err := a.service.Restart(); err != nil {
a.logger.Printf("restarting the service failed: %v", err)
return false
}
for a.clock.Now().Sub(restartedAt) < update.RestartWindow {
if a.isBackUp(baseline, restartedAt, expectedVersion) {
return true
}
a.clock.Sleep(update.PollInterval)
}
return false
}
func (a *ApplyRestartRequestUseCase) isBackUp(baseline update.RunningInfo, restartedAt time.Time, expectedVersion string) bool {
if !a.health.Healthy() {
return false
}
info, found := a.running.Read()
if !found || !info.IsNewerThan(baseline, restartedAt) {
return false
}
return expectedVersion == "" || info.Version == expectedVersion
}
func (r *helperRun) record() {
if err := r.apply.results.Write(r.result); err != nil {
r.apply.logger.Printf("update-result.json could not be written: %v", err)
}
}
func (r *helperRun) finish(outcome string) error {
finishedAt := r.apply.clock.Now().UTC()
r.result.Outcome = outcome
r.result.FinishedAt = &finishedAt
r.record()
r.cleanUp()
return nil
}
func (r *helperRun) fail(reason error) error {
r.apply.logger.Printf("restart request refused or failed: %v", reason)
return r.finish(update.OutcomeFailed)
}
func (r *helperRun) failLoudly(reason error) error {
r.apply.logger.Printf("restart request failed: %v", reason)
_ = r.finish(update.OutcomeFailed)
return reason
}
func (r *helperRun) recoverPanic(err *error) {
if recovered := recover(); recovered != nil {
*err = r.failLoudly(fmt.Errorf("panic: %v", recovered))
}
}
func (r *helperRun) cleanUp() {
a := r.apply
if r.tempDir != "" {
a.removeTreeQuietly(r.tempDir)
}
if r.stagedTag != "" {
a.removeStaged(r.stagedTag)
}
}
func (a *ApplyRestartRequestUseCase) removeStaged(tag string) {
if err := a.requireDirectory(a.layout.UpdatesDir()); err != nil {
a.logger.Printf("staged files left in place: %v", err)
return
}
staged := a.layout.StagedDir(tag)
info, err := a.files.Lstat(staged)
if err != nil {
return
}
if info.IsSymlink {
a.removeQuietly(staged)
return
}
a.removeTreeQuietly(staged)
}
func (a *ApplyRestartRequestUseCase) removeQuietly(target string) {
_ = a.files.Remove(target)
}
func (a *ApplyRestartRequestUseCase) removeTreeQuietly(target string) {
if err := a.files.RemoveAll(target); err != nil {
a.logger.Printf("could not remove %s: %v", target, err)
}
}
@@ -1,32 +1,22 @@
package useCases
import "RenewCMS/internal/domain/update"
import (
"RenewCMS/internal/domain/update"
"time"
)
type GetUpdateStatusUseCase struct {
store update.StatusStore
jobs update.JobStore
helper update.HelperDetector
currentVersion string
status updateStatusBuilder
helper update.HelperDetector
}
func NewGetUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, currentVersion string) *GetUpdateStatusUseCase {
return &GetUpdateStatusUseCase{store: store, jobs: jobs, helper: helper, currentVersion: currentVersion}
func NewGetUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, results update.ResultStore, requests update.RestartRequestStore, currentVersion string, now func() time.Time) *GetUpdateStatusUseCase {
return &GetUpdateStatusUseCase{
status: updateStatusBuilder{store: store, jobs: jobs, results: results, requests: requests, currentVersion: currentVersion, now: now},
helper: helper,
}
}
func (g *GetUpdateStatusUseCase) GetStatus(isOwner bool) update.UpdateStatus {
return buildUpdateStatus(g.store, g.jobs, g.currentVersion, isOwner && g.helper.Installed())
}
func buildUpdateStatus(store update.StatusStore, jobs update.JobStore, currentVersion string, canApply bool) update.UpdateStatus {
var status update.UpdateStatus
if result, found := store.Load(); found {
status = update.NewStatus(currentVersion, &result)
} else {
status = update.NewStatus(currentVersion, nil)
}
if job, found := jobs.Load(); found {
status = status.WithJob(job)
}
status.CanApply = canApply
return status
return g.status.build(isOwner && g.helper.Installed())
}
@@ -0,0 +1,175 @@
package useCases
import (
"RenewCMS/internal/domain/update"
"strings"
"time"
)
type ManageBrowserUpdatesDependencies struct {
Probe update.MachineProbe
Locator update.ServiceLocator
Units update.UnitWriter
Manager update.UnitManager
Status update.HelperStatusWriter
Now func() time.Time
}
type EnableBrowserUpdatesUseCase struct {
dependencies ManageBrowserUpdatesDependencies
}
type DisableBrowserUpdatesUseCase struct {
dependencies ManageBrowserUpdatesDependencies
}
func NewEnableBrowserUpdatesUseCase(dependencies ManageBrowserUpdatesDependencies) *EnableBrowserUpdatesUseCase {
return &EnableBrowserUpdatesUseCase{dependencies: dependencies}
}
func NewDisableBrowserUpdatesUseCase(dependencies ManageBrowserUpdatesDependencies) *DisableBrowserUpdatesUseCase {
return &DisableBrowserUpdatesUseCase{dependencies: dependencies}
}
func (e *EnableBrowserUpdatesUseCase) Enable(options update.InstallOptions) (string, error) {
d := e.dependencies
if err := update.CheckInstallEnvironment(d.Probe); err != nil {
return "", err
}
resolved, err := e.resolve(options)
if err != nil {
return "", err
}
if err := e.install(resolved); err != nil {
e.undo()
return "", err
}
status := update.HelperStatus{
Version: update.HelperStatusVersion,
Service: resolved.Service,
Binary: resolved.Binary,
DataDir: resolved.DataDir,
InstalledAt: d.Now().UTC(),
}
if err := d.Status.Write(resolved.DataDir, status); err != nil {
e.undo()
return "", err
}
return enabledMessage(resolved), nil
}
func (e *EnableBrowserUpdatesUseCase) resolve(options update.InstallOptions) (update.InstallOptions, error) {
service := firstNonEmpty(options.Service, update.DefaultService)
description, found := e.dependencies.Locator.Locate(service)
resolved := update.InstallOptions{
Service: service,
Binary: firstNonEmpty(options.Binary, description.BinaryPath),
DataDir: firstNonEmpty(options.DataDir, description.DataDir),
}
if resolved.Binary == "" || (resolved.DataDir == "" && !found) {
return resolved, update.ErrServiceNotFound
}
if resolved.DataDir == "" {
return resolved, update.ErrDataDirNotFound
}
resolved.Port = e.port(options.Port, description)
return resolved, update.ValidatePort(resolved.Port)
}
func (e *EnableBrowserUpdatesUseCase) port(flag string, description update.ServiceDescription) string {
if flag != "" {
return flag
}
if port, found := e.dependencies.Locator.PortFromEnvironmentFile(description.EnvironmentFile); found {
return port
}
return update.DefaultPort
}
func (e *EnableBrowserUpdatesUseCase) install(options update.InstallOptions) error {
d := e.dependencies
layout := update.HelperLayout{DataDir: options.DataDir, BinaryPath: options.Binary}
if err := d.Units.Write(update.ServiceUnitName, update.RenderServiceUnit(options)); err != nil {
return err
}
if err := d.Units.Write(update.PathUnitName, update.RenderPathUnit(layout)); err != nil {
return err
}
if err := d.Manager.DaemonReload(); err != nil {
return err
}
return d.Manager.EnableNow(update.PathUnitName)
}
func (e *EnableBrowserUpdatesUseCase) undo() {
d := e.dependencies
_ = d.Manager.DisableNow(update.PathUnitName)
_, _ = d.Units.Remove(update.ServiceUnitName)
_, _ = d.Units.Remove(update.PathUnitName)
_ = d.Manager.DaemonReload()
}
func enabledMessage(options update.InstallOptions) string {
lines := []string{
"Browser updates are enabled for " + options.Service + ".",
"Installed " + update.PathUnitName + " and " + update.ServiceUnitName + " (port " + options.Port + ") and wrote " + update.HelperStatusFileName + " in " + options.DataDir + ".",
"To undo it, run: renewcms disable-browser-updates",
}
return strings.Join(lines, "\n")
}
func (u *DisableBrowserUpdatesUseCase) Disable(options update.InstallOptions) (string, error) {
d := u.dependencies
if d.Probe.OS() != update.OSLinux || !d.Probe.SystemdRunning() {
return notEnabledMessage, nil
}
if err := update.CheckInstallEnvironment(d.Probe); err != nil {
return "", err
}
dataDir := firstNonEmpty(options.DataDir, u.locatedDataDir(options.Service))
_ = d.Manager.DisableNow(update.PathUnitName)
removed := u.removeUnits()
if dataDir == "" {
return "", update.ErrDataDirNotFound
}
statusRemoved, err := d.Status.Remove(dataDir)
if err != nil {
return "", err
}
if !removed && !statusRemoved {
return notEnabledMessage, nil
}
if err := d.Manager.DaemonReload(); err != nil {
return "", err
}
return disabledMessage, nil
}
func (u *DisableBrowserUpdatesUseCase) locatedDataDir(service string) string {
description, _ := u.dependencies.Locator.Locate(firstNonEmpty(service, update.DefaultService))
return description.DataDir
}
func (u *DisableBrowserUpdatesUseCase) removeUnits() bool {
removedAny := false
for _, name := range []string{update.PathUnitName, update.ServiceUnitName} {
removed, err := u.dependencies.Units.Remove(name)
removedAny = removedAny || (removed && err == nil)
}
return removedAny
}
const (
notEnabledMessage = "Browser updates were not enabled, nothing to remove."
disabledMessage = "Browser updates are disabled. The restart request, the result, the staged files and the backups were left in place."
)
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
@@ -0,0 +1,21 @@
package useCases
import (
"RenewCMS/internal/domain/update"
"time"
)
type RecordRunningInfoUseCase struct {
store update.RunningInfoStore
version string
pid int
now func() time.Time
}
func NewRecordRunningInfoUseCase(store update.RunningInfoStore, version string, pid int, now func() time.Time) *RecordRunningInfoUseCase {
return &RecordRunningInfoUseCase{store: store, version: version, pid: pid, now: now}
}
func (r *RecordRunningInfoUseCase) Record() error {
return r.store.Write(update.RunningInfo{Version: r.version, Pid: r.pid, StartedAt: r.now().UTC()})
}
@@ -0,0 +1,79 @@
package useCases
import (
"RenewCMS/internal/domain/update"
"sync"
"time"
)
type RequestRestartUseCase struct {
jobs update.JobStore
helper update.HelperDetector
results update.ResultStore
requests update.RestartRequestStore
status updateStatusBuilder
now func() time.Time
mu sync.Mutex
}
func NewRequestRestartUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, results update.ResultStore, requests update.RestartRequestStore, currentVersion string, now func() time.Time) *RequestRestartUseCase {
return &RequestRestartUseCase{
jobs: jobs,
helper: helper,
results: results,
requests: requests,
status: updateStatusBuilder{store: store, jobs: jobs, results: results, requests: requests, currentVersion: currentVersion, now: now},
now: now,
}
}
func (r *RequestRestartUseCase) Request(userId uint32, kind string) (update.UpdateStatus, error) {
if !r.helper.Installed() {
return update.UpdateStatus{}, update.ErrNotPrepared
}
r.mu.Lock()
defer r.mu.Unlock()
current := r.status.build(true)
if err := refuseWhileBusy(current); err != nil {
return update.UpdateStatus{}, err
}
request, err := r.requestFor(kind, userId)
if err != nil {
return update.UpdateStatus{}, err
}
if err := r.results.Delete(); err != nil {
return update.UpdateStatus{}, err
}
if err := r.requests.Write(request); err != nil {
return update.UpdateStatus{}, err
}
return r.status.build(true), nil
}
func refuseWhileBusy(status update.UpdateStatus) error {
switch status.State {
case update.StateDownloading, update.StateVerifying:
return update.ErrJobInProgress
case update.StateRestarting:
return update.ErrRestartPending
}
return nil
}
func (r *RequestRestartUseCase) requestFor(kind string, userId uint32) (update.RestartRequest, error) {
request := update.RestartRequest{
Kind: update.RestartKindRestart,
RequestedAt: r.now().UTC(),
RequestedBy: userId,
}
if job, found := r.jobs.Load(); found && job.State == update.StateReady {
request.Kind = update.RestartKindUpdate
request.Tag = job.Tag
}
if kind != "" && kind != request.Kind {
return update.RestartRequest{}, update.ErrRestartKindMismatch
}
return request, nil
}
@@ -12,6 +12,8 @@ type StartUpdateDownloadUseCase struct {
store update.StatusStore
jobs update.JobStore
helper update.HelperDetector
results update.ResultStore
status updateStatusBuilder
fetcher update.AssetFetcher
verifier update.SignatureVerifier
runner update.StagedBinaryRunner
@@ -29,6 +31,8 @@ type StartUpdateDownloadDependencies struct {
Store update.StatusStore
Jobs update.JobStore
Helper update.HelperDetector
Results update.ResultStore
Requests update.RestartRequestStore
Fetcher update.AssetFetcher
Verifier update.SignatureVerifier
Runner update.StagedBinaryRunner
@@ -52,9 +56,18 @@ func (f jobFailure) Error() string {
func NewStartUpdateDownloadUseCase(dependencies StartUpdateDownloadDependencies) *StartUpdateDownloadUseCase {
return &StartUpdateDownloadUseCase{
store: dependencies.Store,
jobs: dependencies.Jobs,
helper: dependencies.Helper,
store: dependencies.Store,
jobs: dependencies.Jobs,
helper: dependencies.Helper,
results: dependencies.Results,
status: updateStatusBuilder{
store: dependencies.Store,
jobs: dependencies.Jobs,
results: dependencies.Results,
requests: dependencies.Requests,
currentVersion: dependencies.CurrentVersion,
now: dependencies.Now,
},
fetcher: dependencies.Fetcher,
verifier: dependencies.Verifier,
runner: dependencies.Runner,
@@ -79,6 +92,9 @@ func (s *StartUpdateDownloadUseCase) Start() (update.UpdateStatus, error) {
if job, found := s.jobs.Load(); found && job.Occupies() {
return update.UpdateStatus{}, update.ErrJobInProgress
}
if s.status.build(true).State == update.StateRestarting {
return update.UpdateStatus{}, update.ErrRestartPending
}
result, found := s.store.Load()
if !found || !update.IsNewer(s.currentVersion, result.Latest) {
return update.UpdateStatus{}, update.ErrNothingToUpdate
@@ -95,10 +111,13 @@ func (s *StartUpdateDownloadUseCase) Start() (update.UpdateStatus, error) {
StartedAt: s.now().UTC(),
TotalBytes: binary.Size,
}
if err := s.results.Delete(); err != nil {
return update.UpdateStatus{}, err
}
if err := s.jobs.Save(job); err != nil {
return update.UpdateStatus{}, err
}
status := buildUpdateStatus(s.store, s.jobs, s.currentVersion, true)
status := s.status.build(true)
go s.run(job, binary, checksums, signature)
return status, nil
@@ -0,0 +1,44 @@
package useCases
import (
"RenewCMS/internal/domain/update"
"errors"
"time"
)
type updateStatusBuilder struct {
store update.StatusStore
jobs update.JobStore
results update.ResultStore
requests update.RestartRequestStore
currentVersion string
now func() time.Time
}
func (b updateStatusBuilder) build(canApply bool) update.UpdateStatus {
status := b.baseStatus()
if job, found := b.jobs.Load(); found {
status = status.WithJob(job)
}
status.CanApply = canApply
request, requestErr := b.requests.Read()
result, hasResult := b.results.Read()
if hasResult {
status.FinishedAt = result.FinishedAt
}
switch {
case !errors.Is(requestErr, update.ErrNoRestartRequest):
return status.WithRestartRequest(request)
case hasResult:
return status.WithResult(result, b.currentVersion, b.now())
}
return status
}
func (b updateStatusBuilder) baseStatus() update.UpdateStatus {
if result, found := b.store.Load(); found {
return update.NewStatus(b.currentVersion, &result)
}
return update.NewStatus(b.currentVersion, nil)
}