192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
package installer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
var ErrRecoveryInconsistent = errors.New("install recovery state is inconsistent")
|
|
|
|
type RecoveryAction string
|
|
|
|
const (
|
|
RecoveryNone RecoveryAction = "none"
|
|
RecoveryAborted RecoveryAction = "aborted"
|
|
RecoveryRolledBack RecoveryAction = "rolled_back"
|
|
RecoveryCommitted RecoveryAction = "committed"
|
|
)
|
|
|
|
type RecoveryResult struct {
|
|
Action RecoveryAction
|
|
Phase string
|
|
}
|
|
|
|
// Recover resolves an interrupted transaction from journal and directory state.
|
|
func Recover(root string) (RecoveryResult, error) {
|
|
return recoverWithFence(root, defaultDurability())
|
|
}
|
|
|
|
func recoverWithFence(root string, fence durabilityFence) (RecoveryResult, error) {
|
|
fence = effectiveDurability(fence)
|
|
layout, err := inspectAppLayout(root)
|
|
if err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
record, exists, err := loadTransaction(layout)
|
|
if err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
state, err := inspectDirectories(layout)
|
|
if err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if !exists {
|
|
if state.backup {
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: backup exists without transaction",
|
|
ErrRecoveryInconsistent,
|
|
)
|
|
}
|
|
return RecoveryResult{Action: RecoveryNone}, nil
|
|
}
|
|
|
|
result := RecoveryResult{Phase: string(record.Phase)}
|
|
switch record.Phase {
|
|
case phaseCommitted:
|
|
if !state.current || state.staging {
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: committed current=%t staging=%t",
|
|
ErrRecoveryInconsistent,
|
|
state.current,
|
|
state.staging,
|
|
)
|
|
}
|
|
if err := removeManagedDirectoryWithFence(layout, layout.backup, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryCommitted
|
|
return result, nil
|
|
case phaseRollbackRequired:
|
|
return recoverRollbackRequired(layout, record, state, result, fence)
|
|
case phasePrepared, phaseCurrentBackedUp, phaseStagingActivated:
|
|
return recoverUncommitted(layout, record, state, result, fence)
|
|
default:
|
|
return RecoveryResult{}, fmt.Errorf("%w: phase=%q", ErrTransactionCorrupt, record.Phase)
|
|
}
|
|
}
|
|
|
|
func recoverUncommitted(
|
|
layout appLayout,
|
|
record transactionRecord,
|
|
state directoryState,
|
|
result RecoveryResult,
|
|
fence durabilityFence,
|
|
) (RecoveryResult, error) {
|
|
if record.HadCurrent {
|
|
if state.backup {
|
|
if state.current && state.staging {
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: current, staging and backup all exist",
|
|
ErrRecoveryInconsistent,
|
|
)
|
|
}
|
|
if state.current {
|
|
if err := renameManagedDirectory(
|
|
layout,
|
|
layout.current,
|
|
layout.staging,
|
|
fence,
|
|
"app root after recovery staging rename",
|
|
); err != nil {
|
|
return RecoveryResult{}, fmt.Errorf("move unverified current aside: %w", err)
|
|
}
|
|
}
|
|
if err := renameManagedDirectory(
|
|
layout,
|
|
layout.backup,
|
|
layout.current,
|
|
fence,
|
|
"app root after recovery current restore",
|
|
); err != nil {
|
|
return RecoveryResult{}, fmt.Errorf("restore backup during recovery: %w", err)
|
|
}
|
|
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryRolledBack
|
|
return result, nil
|
|
}
|
|
if record.Phase == phasePrepared && state.current && state.staging {
|
|
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryAborted
|
|
return result, nil
|
|
}
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: old current has no recoverable backup",
|
|
ErrRecoveryInconsistent,
|
|
)
|
|
}
|
|
|
|
if state.backup || (state.current && state.staging) {
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: initial install has conflicting directories",
|
|
ErrRecoveryInconsistent,
|
|
)
|
|
}
|
|
if err := removeManagedDirectoryWithFence(layout, layout.current, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryAborted
|
|
return result, nil
|
|
}
|
|
|
|
func recoverRollbackRequired(
|
|
layout appLayout,
|
|
record transactionRecord,
|
|
state directoryState,
|
|
result RecoveryResult,
|
|
fence durabilityFence,
|
|
) (RecoveryResult, error) {
|
|
if record.HadCurrent && !state.backup {
|
|
if !state.current {
|
|
return RecoveryResult{}, fmt.Errorf(
|
|
"%w: rollback lost current and backup",
|
|
ErrRecoveryInconsistent,
|
|
)
|
|
}
|
|
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryRolledBack
|
|
return result, nil
|
|
}
|
|
if err := rollbackActivated(layout, record.HadCurrent, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
if err := removeTransactionWithFence(layout, fence); err != nil {
|
|
return RecoveryResult{}, err
|
|
}
|
|
result.Action = RecoveryRolledBack
|
|
return result, nil
|
|
}
|