opentofu/internal/states/statemgr/migrate.go

216 lines
8.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package statemgr
import (
"fmt"
"github.com/opentofu/opentofu/internal/states/statefile"
)
// Migrator is an optional interface implemented by state managers that
// are capable of direct migration of state snapshots with their associated
// metadata unchanged.
//
// This interface is used when available by function Migrate. See that
// function for more information on how it is used.
type Migrator interface {
PersistentMeta
// StateForMigration returns a full statefile representing the latest
// snapshot (as would be returned by Reader.State) and the associated
// snapshot metadata (as would be returned by
// PersistentMeta.StateSnapshotMeta).
//
// Just as with Reader.State, this must not fail.
StateForMigration() *statefile.File
// WriteStateForMigration accepts a full statefile including associated
// snapshot metadata, and atomically updates the stored file (as with
// Writer.WriteState) and the metadata.
//
// If "force" is not set, the manager must call CheckValidImport with
// the given file and the current file and complete the update only if
// that function returns nil. If force is set this may override such
// checks, but some backends do not support forcing and so will act
// as if force is always false.
WriteStateForMigration(f *statefile.File, force bool) error
}
// Migrate writes the latest transient state snapshot from src into dest,
// preserving snapshot metadata (serial and lineage) where possible.
//
// If both managers implement the optional interface Migrator then it will
// be used to copy the snapshot and its associated metadata. Otherwise,
// the normal Reader and Writer interfaces will be used instead.
//
// If the destination manager refuses the new state or fails to write it then
// its error is returned directly.
//
// For state managers that also implement Persistent, it is the caller's
// responsibility to persist the newly-written state after a successful result,
// just as with calls to Writer.WriteState.
//
// This function doesn't do any locking of its own, so if the state managers
// also implement Locker the caller should hold a lock on both managers
// for the duration of this call.
func Migrate(dst, src Transient) error {
if dstM, ok := dst.(Migrator); ok {
if srcM, ok := src.(Migrator); ok {
// Full-fidelity migration, them.
s := srcM.StateForMigration()
return dstM.WriteStateForMigration(s, true)
}
}
// Managers to not support full-fidelity migration, so migration will not
// preserve serial/lineage.
s := src.State()
return dst.WriteState(s)
}
// Import loads the given state snapshot into the given manager, preserving
// its metadata (serial and lineage) if the target manager supports metadata.
//
// A state manager must implement the optional interface Migrator to get
// access to the full metadata.
//
// Unless "force" is true, Import will check first that the metadata given
// in the file matches the current snapshot metadata for the manager, if the
// manager supports metadata. Some managers do not support forcing, so a
// write with an unsuitable lineage or serial may still be rejected even if
// "force" is set. "force" has no effect for managers that do not support
// snapshot metadata.
//
// For state managers that also implement Persistent, it is the caller's
// responsibility to persist the newly-written state after a successful result,
// just as with calls to Writer.WriteState.
//
// This function doesn't do any locking of its own, so if the state manager
// also implements Locker the caller should hold a lock on it for the
// duration of this call.
func Import(f *statefile.File, mgr Transient, force bool) error {
if mgrM, ok := mgr.(Migrator); ok {
return mgrM.WriteStateForMigration(f, force)
}
// For managers that don't implement Migrator, this is just a normal write
// of the state contained in the given file.
return mgr.WriteState(f.State)
}
// Export retrieves the latest state snapshot from the given manager, including
// its metadata (serial and lineage) where possible.
//
// A state manager must also implement either Migrator or PersistentMeta
// for the metadata to be included. Otherwise, the relevant fields will have
// zero value in the returned object.
//
// For state managers that also implement Persistent, it is the caller's
// responsibility to refresh from persistent storage first if needed.
//
// This function doesn't do any locking of its own, so if the state manager
// also implements Locker the caller should hold a lock on it for the
// duration of this call.
func Export(mgr Reader) *statefile.File {
switch mgrT := mgr.(type) {
case Migrator:
return mgrT.StateForMigration()
case PersistentMeta:
s := mgr.State()
meta := mgrT.StateSnapshotMeta()
return statefile.New(s, meta.Lineage, meta.Serial)
default:
s := mgr.State()
return statefile.New(s, "", 0)
}
}
// SnapshotMetaRel describes a relationship between two SnapshotMeta values,
// returned from the SnapshotMeta.Compare method where the "first" value
// is the receiver of that method and the "second" is the given argument.
type SnapshotMetaRel rune
//go:generate go run golang.org/x/tools/cmd/stringer -type=SnapshotMetaRel
const (
// SnapshotOlder indicates that two snapshots have a common lineage and
// that the first has a lower serial value.
SnapshotOlder SnapshotMetaRel = '<'
// SnapshotNewer indicates that two snapshots have a common lineage and
// that the first has a higher serial value.
SnapshotNewer SnapshotMetaRel = '>'
// SnapshotEqual indicates that two snapshots have a common lineage and
// the same serial value.
SnapshotEqual SnapshotMetaRel = '='
// SnapshotUnrelated indicates that two snapshots have different lineage
// and thus cannot be meaningfully compared.
SnapshotUnrelated SnapshotMetaRel = '!'
// SnapshotLegacy indicates that one or both of the snapshots
// does not have a lineage at all, and thus no comparison is possible.
SnapshotLegacy SnapshotMetaRel = '?'
)
// Compare determines the relationship, if any, between the given existing
// SnapshotMeta and the potential "new" SnapshotMeta that is the receiver.
func (m SnapshotMeta) Compare(existing SnapshotMeta) SnapshotMetaRel {
switch {
case m.Lineage == "" || existing.Lineage == "":
return SnapshotLegacy
case m.Lineage != existing.Lineage:
return SnapshotUnrelated
case m.Serial > existing.Serial:
return SnapshotNewer
case m.Serial < existing.Serial:
return SnapshotOlder
default:
// both serials are equal, by elimination
return SnapshotEqual
}
}
// CheckValidImport returns nil if the "new" snapshot can be imported as a
// successor of the "existing" snapshot without forcing.
//
// If not, an error is returned describing why.
func CheckValidImport(newFile, existingFile *statefile.File) error {
if existingFile == nil || existingFile.State.Empty() {
// It's always okay to overwrite an empty state, regardless of
// its lineage/serial.
return nil
}
new := SnapshotMeta{
Lineage: newFile.Lineage,
Serial: newFile.Serial,
}
existing := SnapshotMeta{
Lineage: existingFile.Lineage,
Serial: existingFile.Serial,
}
rel := new.Compare(existing)
switch rel {
case SnapshotNewer:
return nil // a newer snapshot is fine
case SnapshotLegacy:
return nil // anything goes for a legacy state
case SnapshotUnrelated:
return fmt.Errorf("cannot import state with lineage %q over unrelated state with lineage %q", new.Lineage, existing.Lineage)
case SnapshotEqual:
if statefile.StatesMarshalEqual(newFile.State, existingFile.State) {
// If lineage, serial, and state all match then this is fine.
return nil
}
return fmt.Errorf("cannot overwrite existing state with serial %d with a different state that has the same serial", new.Serial)
case SnapshotOlder:
return fmt.Errorf("cannot import state with serial %d over newer state with serial %d", new.Serial, existing.Serial)
default:
// Should never happen, but we'll check to make sure for safety
return fmt.Errorf("unsupported state snapshot relationship %s", rel)
}
}