Files
soft_quay/core/installer/switcher.go
T

197 lines
5.2 KiB
Go
Raw Normal View History

2026-07-16 16:32:58 +08:00
package installer
import (
"errors"
"fmt"
"os"
)
var (
ErrHealthCheckRequired = errors.New("health check is required")
ErrHealthCheckFailed = errors.New("installed version failed health check")
ErrRollbackFailed = errors.New("install rollback failed")
)
type HealthCheck func(currentPath string) error
type switchStep string
const (
stepPrepared switchStep = "prepared"
stepCurrentRenamed switchStep = "current_renamed"
stepCurrentBackedUp switchStep = "current_backed_up"
stepStagingRenamed switchStep = "staging_renamed"
stepStagingActivated switchStep = "staging_activated"
stepRollbackRequired switchStep = "rollback_required"
stepCommitted switchStep = "committed"
)
// RollbackError reports both the health failure and rollback failure.
type RollbackError struct {
Health error
Rollback error
}
func (err *RollbackError) Error() string {
return fmt.Sprintf("%s: health=%v; rollback=%v", ErrRollbackFailed, err.Health, err.Rollback)
}
func (err *RollbackError) Unwrap() error {
return ErrRollbackFailed
}
// Switcher activates a verified staging directory and runs an injected check.
type Switcher struct {
health HealthCheck
afterStep func(switchStep) error
}
func NewSwitcher(health HealthCheck) *Switcher {
return &Switcher{health: health}
}
func (switcher *Switcher) Switch(root string) error {
if switcher.health == nil {
return ErrHealthCheckRequired
}
layout, err := inspectAppLayout(root)
if err != nil {
return err
}
if _, exists, err := loadTransaction(layout); err != nil {
return err
} else if exists {
return ErrRecoveryRequired
}
state, err := inspectDirectories(layout)
if err != nil {
return err
}
if !state.staging {
return ErrStagingMissing
}
if state.backup {
return ErrBackupExists
}
record := newTransaction(phasePrepared, state.current)
if err := writeTransaction(layout, record); err != nil {
return err
}
if err := switcher.runStep(stepPrepared); err != nil {
return err
}
if state.current {
if err := os.Rename(layout.current, layout.backup); err != nil {
return fmt.Errorf("backup current directory: %w", err)
}
if err := switcher.runStep(stepCurrentRenamed); err != nil {
return err
}
}
record.Phase = phaseCurrentBackedUp
if err := writeTransaction(layout, record); err != nil {
return err
}
if err := switcher.runStep(stepCurrentBackedUp); err != nil {
return err
}
if err := os.Rename(layout.staging, layout.current); err != nil {
return fmt.Errorf("activate staging directory: %w", err)
}
if err := switcher.runStep(stepStagingRenamed); err != nil {
return err
}
record.Phase = phaseStagingActivated
if err := writeTransaction(layout, record); err != nil {
return err
}
if err := switcher.runStep(stepStagingActivated); err != nil {
return err
}
healthErr := switcher.health(layout.current)
if healthErr != nil {
record.Phase = phaseRollbackRequired
if err := writeTransaction(layout, record); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
if err := switcher.runStep(stepRollbackRequired); err != nil {
return err
}
if err := rollbackActivated(layout, record.HadCurrent); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
if err := removeTransaction(layout); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
return fmt.Errorf("%w: %v", ErrHealthCheckFailed, healthErr)
}
record.Phase = phaseCommitted
if err := writeTransaction(layout, record); err != nil {
return err
}
if err := switcher.runStep(stepCommitted); err != nil {
return err
}
if record.HadCurrent {
if err := removeManagedDirectory(layout, layout.backup); err != nil {
return fmt.Errorf("%w: cleanup committed backup: %v", ErrRecoveryRequired, err)
}
}
if err := removeTransaction(layout); err != nil {
return fmt.Errorf("%w: %v", ErrRecoveryRequired, err)
}
return nil
}
func (switcher *Switcher) runStep(step switchStep) error {
if switcher.afterStep == nil {
return nil
}
return switcher.afterStep(step)
}
func rollbackActivated(layout appLayout, hadCurrent bool) error {
state, err := inspectDirectories(layout)
if err != nil {
return err
}
if hadCurrent {
if !state.backup {
return fmt.Errorf("%w: previous current backup is missing", ErrRollbackFailed)
}
if state.current {
if state.staging {
return fmt.Errorf("%w: current and staging both exist", ErrRollbackFailed)
}
if err := os.Rename(layout.current, layout.staging); err != nil {
return fmt.Errorf("move failed current aside: %w", err)
}
}
if err := os.Rename(layout.backup, layout.current); err != nil {
if _, statErr := os.Stat(layout.staging); statErr == nil {
_ = os.Rename(layout.staging, layout.current)
}
return fmt.Errorf("restore previous current: %w", err)
}
return removeManagedDirectory(layout, layout.staging)
}
if state.backup {
return fmt.Errorf("%w: unexpected backup without previous current", ErrRollbackFailed)
}
if state.current {
if state.staging {
return fmt.Errorf("%w: current and staging both exist", ErrRollbackFailed)
}
if err := os.Rename(layout.current, layout.staging); err != nil {
return fmt.Errorf("move failed initial install aside: %w", err)
}
}
return removeManagedDirectory(layout, layout.staging)
}