108 lines
4.8 KiB
Go
108 lines
4.8 KiB
Go
package backup
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
|
)
|
|
|
|
var ErrRestoreConfirmationRequired = errors.New("restore requires --yes")
|
|
|
|
// RestoreRequest describes the deliberately-confirmed archive restoration.
|
|
type RestoreRequest struct {
|
|
Archive string
|
|
Confirm bool
|
|
Drain bool
|
|
}
|
|
|
|
// RestoreResult records the retained recovery point and the final service state.
|
|
type RestoreResult struct {
|
|
Checkpoint string
|
|
Restarted bool
|
|
Verified bool
|
|
}
|
|
|
|
type restoreLock interface { Release() error }
|
|
type restoreVerify func(context.Context, config.Installation, archiveRunner) error
|
|
|
|
type restoreDependencies struct {
|
|
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
|
|
checkpoint func(context.Context, config.Installation, CreateRequest) (Result, error)
|
|
acquireLock func(config.Installation) (restoreLock, error)
|
|
runner archiveRunner
|
|
sleep func(duration time.Duration)
|
|
restoreFile func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error
|
|
restoreVolume func(context.Context, config.Installation, VolumeMetadata, io.Reader) error
|
|
verify map[string]restoreVerify
|
|
}
|
|
|
|
// Restore runs the host transaction. Concrete host dependencies are intentionally kept outside
|
|
// the deterministic core so callers cannot bypass its preflight and checkpoint boundaries.
|
|
func Restore(ctx context.Context, installation config.Installation, request RestoreRequest) (RestoreResult, error) {
|
|
return RestoreResult{}, errors.New("restore host dependencies are unavailable")
|
|
}
|
|
|
|
func restoreWithDependencies(ctx context.Context, installation config.Installation, request RestoreRequest, deps restoreDependencies) (result RestoreResult, resultErr error) {
|
|
if !request.Confirm { return RestoreResult{}, ErrRestoreConfirmationRequired }
|
|
if request.Archive == "" { return RestoreResult{}, errors.New("restore archive is required") }
|
|
if deps.preflight == nil || deps.checkpoint == nil || deps.acquireLock == nil || deps.runner == nil || deps.restoreFile == nil || deps.restoreVolume == nil || deps.verify == nil {
|
|
return RestoreResult{}, errors.New("restore dependencies are incomplete")
|
|
}
|
|
preflight, err := deps.preflight(ctx, installation, PreflightRequest{Archive: request.Archive, Confirm: true, AllowExternalSecrets: true})
|
|
if err != nil { return RestoreResult{}, err }
|
|
defer preflight.CloseArchive()
|
|
archive, err := preflight.RevalidateArchive()
|
|
if err != nil { return RestoreResult{}, err }
|
|
|
|
checkpoint, err := deps.checkpoint(ctx, installation, CreateRequest{})
|
|
if err != nil { return RestoreResult{}, fmt.Errorf("create recovery checkpoint: %w", err) }
|
|
result.Checkpoint = checkpoint.Path
|
|
lock, err := deps.acquireLock(installation)
|
|
if err != nil { return result, err }
|
|
defer func() { if releaseErr := lock.Release(); releaseErr != nil && resultErr == nil { resultErr = releaseErr } }()
|
|
|
|
wasRunning, err := installationRunning(ctx, installation, deps.runner)
|
|
if err != nil { return result, err }
|
|
mutated := false
|
|
defer func() {
|
|
if resultErr != nil && mutated { _ = runCompose(context.Background(), installation, deps.runner, "stop") }
|
|
}()
|
|
if wasRunning {
|
|
if err := maintenance(ctx, installation, deps.runner, true); err != nil { return result, err }
|
|
if err := waitForNoActiveSessions(ctx, installation, deps.runner, request.Drain, deps.sleep); err != nil { return result, err }
|
|
if err := runCompose(ctx, installation, deps.runner, "stop"); err != nil { return result, err }
|
|
}
|
|
reader, err := zip.NewReader(archive, preflight.ArchiveSize)
|
|
if err != nil { return result, fmt.Errorf("read verified restore archive: %w", err) }
|
|
members := make(map[string]*zip.File, len(reader.File))
|
|
for _, member := range reader.File { members[member.Name] = member }
|
|
for _, entry := range preflight.Entries {
|
|
if entry.Kind == EntryVolume { continue }
|
|
member := members[entry.Path]
|
|
if member == nil { return result, fmt.Errorf("verified archive is missing %q", entry.Path) }
|
|
stream, openErr := member.Open()
|
|
if openErr != nil { return result, fmt.Errorf("open verified archive member %q: %w", entry.Path, openErr) }
|
|
mutated = true
|
|
restoreErr := deps.restoreFile(ctx, installation, entry, stream)
|
|
closeErr := stream.Close()
|
|
if restoreErr != nil { return result, restoreErr }
|
|
if closeErr != nil { return result, closeErr }
|
|
}
|
|
if wasRunning {
|
|
if err := composeStartAndVerify(ctx, installation, deps.runner); err != nil { return result, err }
|
|
result.Restarted = true
|
|
}
|
|
for _, name := range []string{"health", "doctor", "pi", "workspace"} {
|
|
check := deps.verify[name]
|
|
if check == nil { return result, fmt.Errorf("restore verification %q is unavailable", name) }
|
|
if err := check(ctx, installation, deps.runner); err != nil { return result, fmt.Errorf("restore verification %s: %w", name, err) }
|
|
}
|
|
result.Verified = true
|
|
return result, nil
|
|
}
|