Report committed comparison cleanup failures
This commit is contained in:
@@ -262,43 +262,72 @@ func unrecognizedBundleError(action string, err error) error {
|
||||
return fmt.Errorf("%w: %s: %v", ErrUnrecognizedBundle, action, err)
|
||||
}
|
||||
|
||||
// PublicationResult describes the durable state of a publication attempt.
|
||||
type PublicationResult struct {
|
||||
Committed bool
|
||||
RetainedBackupPath string
|
||||
}
|
||||
|
||||
// PublicationCleanupError reports that a committed bundle could not remove its
|
||||
// prior sibling backup. The new bundle remains installed and the backup path
|
||||
// is retained for operator recovery.
|
||||
type PublicationCleanupError struct {
|
||||
RetainedBackupPath string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (err *PublicationCleanupError) Error() string {
|
||||
return fmt.Sprintf("remove comparison backup %q: %v", err.RetainedBackupPath, err.Err)
|
||||
}
|
||||
|
||||
func (err *PublicationCleanupError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// Publish writes a complete logical bundle through a private sibling directory
|
||||
// and atomically installs it at a preflighted destination.
|
||||
func Publish(ctx context.Context, plan DestinationPlan, bundle LogicalBundle) error {
|
||||
return publish(ctx, plan, bundle, publishOperations{rename: os.Rename})
|
||||
func Publish(ctx context.Context, plan DestinationPlan, bundle LogicalBundle) (PublicationResult, error) {
|
||||
return publish(ctx, plan, bundle, publishOperations{rename: os.Rename, removeAll: os.RemoveAll})
|
||||
}
|
||||
|
||||
type publishOperations struct {
|
||||
rename func(string, string) error
|
||||
removeAll func(string) error
|
||||
beforeCommit func()
|
||||
}
|
||||
|
||||
func publish(ctx context.Context, plan DestinationPlan, bundle LogicalBundle, operations publishOperations) error {
|
||||
func publish(ctx context.Context, plan DestinationPlan, bundle LogicalBundle, operations publishOperations) (PublicationResult, error) {
|
||||
if operations.rename == nil {
|
||||
operations.rename = os.Rename
|
||||
}
|
||||
if operations.removeAll == nil {
|
||||
operations.removeAll = os.RemoveAll
|
||||
}
|
||||
if err := bundle.Validate(); err != nil {
|
||||
return fmt.Errorf("validate comparison bundle: %w", err)
|
||||
return PublicationResult{}, fmt.Errorf("validate comparison bundle: %w", err)
|
||||
}
|
||||
manifestData, err := EncodeManifest(bundle.Manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
plan, err = PlanDestination(plan.WorkingDirectory, plan.Target, plan.Replace)
|
||||
if err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(plan.Target), 0o755); err != nil {
|
||||
return fmt.Errorf("create comparison destination parent %q: %w", filepath.Dir(plan.Target), err)
|
||||
return PublicationResult{}, fmt.Errorf("create comparison destination parent %q: %w", filepath.Dir(plan.Target), err)
|
||||
}
|
||||
temporaryDirectory, err := os.MkdirTemp(filepath.Dir(plan.Target), "."+filepath.Base(plan.Target)+".staging-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create comparison staging directory: %w", err)
|
||||
return PublicationResult{}, fmt.Errorf("create comparison staging directory: %w", err)
|
||||
}
|
||||
if err := os.Chmod(temporaryDirectory, 0o700); err != nil {
|
||||
os.RemoveAll(temporaryDirectory)
|
||||
return fmt.Errorf("secure comparison staging directory: %w", err)
|
||||
return PublicationResult{}, fmt.Errorf("secure comparison staging directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if temporaryDirectory != "" {
|
||||
@@ -307,45 +336,46 @@ func publish(ctx context.Context, plan DestinationPlan, bundle LogicalBundle, op
|
||||
}()
|
||||
|
||||
if err := writeLogicalBundle(ctx, temporaryDirectory, bundle, manifestData); err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
|
||||
currentPlan, err := PlanDestination(plan.WorkingDirectory, plan.Target, plan.Replace)
|
||||
if err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
if operations.beforeCommit != nil {
|
||||
operations.beforeCommit()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
if currentPlan.state == destinationAbsent {
|
||||
if err := operations.rename(temporaryDirectory, currentPlan.Target); err != nil {
|
||||
return fmt.Errorf("publish comparison bundle to %q: %w", currentPlan.Target, err)
|
||||
return PublicationResult{}, fmt.Errorf("publish comparison bundle to %q: %w", currentPlan.Target, err)
|
||||
}
|
||||
temporaryDirectory = ""
|
||||
return nil
|
||||
return PublicationResult{Committed: true}, nil
|
||||
}
|
||||
|
||||
backupDirectory, err := uniqueSiblingPath(filepath.Dir(currentPlan.Target), "."+filepath.Base(currentPlan.Target)+".backup-")
|
||||
if err != nil {
|
||||
return err
|
||||
return PublicationResult{}, err
|
||||
}
|
||||
if err := operations.rename(currentPlan.Target, backupDirectory); err != nil {
|
||||
return fmt.Errorf("back up comparison destination %q: %w", currentPlan.Target, err)
|
||||
return PublicationResult{}, fmt.Errorf("back up comparison destination %q: %w", currentPlan.Target, err)
|
||||
}
|
||||
if err := authorizeMovedDestination(currentPlan, backupDirectory); err != nil {
|
||||
return restoreMovedDestination(operations, backupDirectory, currentPlan.Target, err)
|
||||
return PublicationResult{}, restoreMovedDestination(operations, backupDirectory, currentPlan.Target, err)
|
||||
}
|
||||
if err := operations.rename(temporaryDirectory, currentPlan.Target); err != nil {
|
||||
return restoreMovedDestination(operations, backupDirectory, currentPlan.Target, fmt.Errorf("replace comparison destination %q: %w", currentPlan.Target, err))
|
||||
return PublicationResult{}, restoreMovedDestination(operations, backupDirectory, currentPlan.Target, fmt.Errorf("replace comparison destination %q: %w", currentPlan.Target, err))
|
||||
}
|
||||
temporaryDirectory = ""
|
||||
if err := os.RemoveAll(backupDirectory); err != nil {
|
||||
return fmt.Errorf("remove comparison backup %q: %w", backupDirectory, err)
|
||||
if err := operations.removeAll(backupDirectory); err != nil {
|
||||
cleanupErr := &PublicationCleanupError{RetainedBackupPath: backupDirectory, Err: err}
|
||||
return PublicationResult{Committed: true, RetainedBackupPath: backupDirectory}, cleanupErr
|
||||
}
|
||||
return nil
|
||||
return PublicationResult{Committed: true}, nil
|
||||
}
|
||||
|
||||
func authorizeMovedDestination(plan DestinationPlan, backupDirectory string) error {
|
||||
|
||||
Reference in New Issue
Block a user