feat: restart to apply update, privileged helper, rollback
This commit is contained in:
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)
|
||||
}
|
||||
Reference in new issue
Block a user