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, ) } if state.staging { if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil { return RecoveryResult{}, err } return RecoveryResult{Action: RecoveryAborted}, nil } 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 }