Files
soft_quay/core/installer/recovery.go
T

174 lines
4.6 KiB
Go

package installer
import (
"errors"
"fmt"
"os"
)
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) {
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 := removeManagedDirectory(layout, layout.backup); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryCommitted
return result, nil
case phaseRollbackRequired:
return recoverRollbackRequired(layout, record, state, result)
case phasePrepared, phaseCurrentBackedUp, phaseStagingActivated:
return recoverUncommitted(layout, record, state, result)
default:
return RecoveryResult{}, fmt.Errorf("%w: phase=%q", ErrTransactionCorrupt, record.Phase)
}
}
func recoverUncommitted(
layout appLayout,
record transactionRecord,
state directoryState,
result RecoveryResult,
) (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 := os.Rename(layout.current, layout.staging); err != nil {
return RecoveryResult{}, fmt.Errorf("move unverified current aside: %w", err)
}
}
if err := os.Rename(layout.backup, layout.current); err != nil {
return RecoveryResult{}, fmt.Errorf("restore backup during recovery: %w", err)
}
if err := removeManagedDirectory(layout, layout.staging); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}
if record.Phase == phasePrepared && state.current && state.staging {
if err := removeManagedDirectory(layout, layout.staging); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); 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 := removeManagedDirectory(layout, layout.current); err != nil {
return RecoveryResult{}, err
}
if err := removeManagedDirectory(layout, layout.staging); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryAborted
return result, nil
}
func recoverRollbackRequired(
layout appLayout,
record transactionRecord,
state directoryState,
result RecoveryResult,
) (RecoveryResult, error) {
if record.HadCurrent && !state.backup {
if !state.current {
return RecoveryResult{}, fmt.Errorf(
"%w: rollback lost current and backup",
ErrRecoveryInconsistent,
)
}
if err := removeManagedDirectory(layout, layout.staging); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}
if err := rollbackActivated(layout, record.HadCurrent); err != nil {
return RecoveryResult{}, err
}
if err := removeTransaction(layout); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}