mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
d869923103
Signed-off-by: 1garo <alevardai427@gmail.com> Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Co-authored-by: Christian Mesh <christianmesh1@gmail.com>
560 lines
15 KiB
Go
560 lines
15 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package statemgr
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
)
|
|
|
|
// Filesystem is a full state manager that uses a file in the local filesystem
|
|
// for persistent storage.
|
|
//
|
|
// The transient storage for Filesystem is always in-memory.
|
|
type Filesystem struct {
|
|
// path is the location where a file will be created or replaced for
|
|
// each persistent snapshot.
|
|
path string
|
|
|
|
// readPath is read by RefreshState instead of "path" until the first
|
|
// call to PersistState, after which it is ignored.
|
|
//
|
|
// The file at readPath must never be written to by this manager.
|
|
readPath string
|
|
|
|
// backupPath is an optional extra path which, if non-empty, will be
|
|
// created or overwritten with the first state snapshot we read if there
|
|
// is a subsequent call to write a different state.
|
|
backupPath string
|
|
|
|
// the file handle corresponding to PathOut
|
|
stateFileOut *os.File
|
|
|
|
// While the stateFileOut will correspond to the lock directly,
|
|
// store and check the lock ID to maintain a strict statemgr.Locker
|
|
// implementation.
|
|
lockID string
|
|
|
|
// created is set to true if stateFileOut didn't exist before we created it.
|
|
// This is mostly so we can clean up empty files during tests, but doesn't
|
|
// hurt to remove file we never wrote to.
|
|
created bool
|
|
|
|
mu sync.Mutex
|
|
file, readFile *statefile.File
|
|
backupFile *statefile.File
|
|
writtenBackup bool
|
|
|
|
encryption encryption.StateEncryption
|
|
}
|
|
|
|
var (
|
|
_ Full = (*Filesystem)(nil)
|
|
_ PersistentMeta = (*Filesystem)(nil)
|
|
_ Migrator = (*Filesystem)(nil)
|
|
)
|
|
|
|
// NewFilesystem creates a filesystem-based state manager that reads and writes
|
|
// state snapshots at the given filesystem path.
|
|
//
|
|
// This is equivalent to calling NewFileSystemBetweenPaths with statePath as
|
|
// both of the path arguments.
|
|
func NewFilesystem(statePath string, enc encryption.StateEncryption) *Filesystem {
|
|
return &Filesystem{
|
|
path: statePath,
|
|
readPath: statePath,
|
|
encryption: enc,
|
|
}
|
|
}
|
|
|
|
// NewFilesystemBetweenPaths creates a filesystem-based state manager that
|
|
// reads an initial snapshot from readPath and then writes all new snapshots to
|
|
// writePath.
|
|
func NewFilesystemBetweenPaths(readPath, writePath string, enc encryption.StateEncryption) *Filesystem {
|
|
return &Filesystem{
|
|
path: writePath,
|
|
readPath: readPath,
|
|
encryption: enc,
|
|
}
|
|
}
|
|
|
|
// SetBackupPath configures the receiever so that it will create a local
|
|
// backup file of the next state snapshot it reads (in State) if a different
|
|
// snapshot is subsequently written (in WriteState). Only one backup is
|
|
// written for the lifetime of the object, unless reset as described below.
|
|
//
|
|
// For correct operation, this must be called before any other state methods
|
|
// are called. If called multiple times, each call resets the backup
|
|
// function so that the next read will become the backup snapshot and a
|
|
// following write will save a backup of it.
|
|
func (s *Filesystem) SetBackupPath(path string) {
|
|
s.backupPath = path
|
|
s.backupFile = nil
|
|
s.writtenBackup = false
|
|
}
|
|
|
|
// BackupPath returns the manager's backup path if backup files are enabled,
|
|
// or an empty string otherwise.
|
|
func (s *Filesystem) BackupPath() string {
|
|
return s.backupPath
|
|
}
|
|
|
|
// State is an implementation of Reader.
|
|
func (s *Filesystem) State() *states.State {
|
|
defer s.mutex()()
|
|
if s.file == nil {
|
|
return nil
|
|
}
|
|
return s.file.DeepCopy().State
|
|
}
|
|
|
|
// WriteState is an implementation of Writer.
|
|
func (s *Filesystem) WriteState(state *states.State) error {
|
|
defer s.mutex()()
|
|
|
|
if s.readFile == nil {
|
|
err := s.refreshState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.writeState(state, nil)
|
|
}
|
|
|
|
func (s *Filesystem) writeState(state *states.State, meta *SnapshotMeta) error {
|
|
s.file = s.file.DeepCopy()
|
|
if s.file == nil {
|
|
s.file = NewStateFile()
|
|
}
|
|
s.file.State = state.DeepCopy()
|
|
|
|
if meta != nil {
|
|
// Force new metadata
|
|
s.file.Lineage = meta.Lineage
|
|
s.file.Serial = meta.Serial
|
|
log.Printf("[TRACE] statemgr.Filesystem: forcing lineage %q serial %d for migration/import", s.file.Lineage, s.file.Serial)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PersistState writes state to a tfstate file.
|
|
func (s *Filesystem) PersistState(schemas *tofu.Schemas) error {
|
|
defer s.mutex()()
|
|
|
|
return s.persistState(schemas)
|
|
}
|
|
|
|
func (s *Filesystem) persistState(schemas *tofu.Schemas) error {
|
|
// TODO: this should use a more robust method of writing state, by first
|
|
// writing to a temp file on the same filesystem, and renaming the file over
|
|
// the original.
|
|
if s.stateFileOut == nil {
|
|
if err := s.createStateFiles(); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
defer s.stateFileOut.Sync()
|
|
|
|
if s.file == nil {
|
|
s.file = NewStateFile()
|
|
}
|
|
state := s.file.State
|
|
|
|
// We'll try to write our backup first, so we can be sure we've created
|
|
// it successfully before clobbering the original file it came from.
|
|
if !s.writtenBackup && s.backupFile != nil && s.backupPath != "" {
|
|
if !statefile.StatesMarshalEqual(state, s.backupFile.State) {
|
|
log.Printf("[TRACE] statemgr.Filesystem: creating backup snapshot at %s", s.backupPath)
|
|
bfh, err := os.Create(s.backupPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create local state backup file: %w", err)
|
|
}
|
|
defer bfh.Close()
|
|
|
|
err = statefile.Write(s.backupFile, bfh, s.encryption)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write to local state backup file: %w", err)
|
|
}
|
|
|
|
s.writtenBackup = true
|
|
} else {
|
|
log.Print("[TRACE] statemgr.Filesystem: not making a backup, because the new snapshot is identical to the old")
|
|
}
|
|
} else {
|
|
// This branch is all just logging, to help understand why we didn't make a backup.
|
|
switch {
|
|
case s.backupPath == "":
|
|
log.Print("[TRACE] statemgr.Filesystem: state file backups are disabled")
|
|
case s.writtenBackup:
|
|
log.Printf("[TRACE] statemgr.Filesystem: have already backed up original %s to %s on a previous write", s.path, s.backupPath)
|
|
case s.backupFile == nil:
|
|
log.Printf("[TRACE] statemgr.Filesystem: no original state snapshot to back up")
|
|
default:
|
|
log.Printf("[TRACE] statemgr.Filesystem: not creating a backup for an unknown reason")
|
|
}
|
|
}
|
|
|
|
if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
if err := s.stateFileOut.Truncate(0); err != nil {
|
|
return err
|
|
}
|
|
|
|
if state == nil {
|
|
// if we have no state, don't write anything else.
|
|
log.Print("[TRACE] statemgr.Filesystem: state is nil, so leaving the file empty")
|
|
return nil
|
|
}
|
|
|
|
if s.readFile == nil || !statefile.StatesMarshalEqual(s.file.State, s.readFile.State) {
|
|
s.file.Serial++
|
|
log.Printf("[TRACE] statemgr.Filesystem: state has changed since last snapshot, so incrementing serial to %d", s.file.Serial)
|
|
} else {
|
|
log.Print("[TRACE] statemgr.Filesystem: no state changes since last snapshot")
|
|
}
|
|
|
|
log.Printf("[TRACE] statemgr.Filesystem: writing snapshot at %s", s.path)
|
|
if err := statefile.Write(s.file, s.stateFileOut, s.encryption); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Any future reads must come from the file we've now updated
|
|
s.readPath = s.path
|
|
return nil
|
|
}
|
|
|
|
// RefreshState is an implementation of Refresher.
|
|
func (s *Filesystem) RefreshState() error {
|
|
defer s.mutex()()
|
|
return s.refreshState()
|
|
}
|
|
|
|
func (s *Filesystem) GetRootOutputValues() (map[string]*states.OutputValue, error) {
|
|
err := s.RefreshState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state := s.State()
|
|
if state == nil {
|
|
state = states.NewState()
|
|
}
|
|
|
|
return state.RootModule().OutputValues, nil
|
|
}
|
|
|
|
func (s *Filesystem) refreshState() error {
|
|
var reader io.Reader
|
|
|
|
// The s.readPath file is only OK to read if we have not written any state out
|
|
// (in which case the same state needs to be read in), and no state output file
|
|
// has been opened (possibly via a lock) or the input path is different
|
|
// than the output path.
|
|
// This is important for Windows, as if the input file is the same as the
|
|
// output file, and the output file has been locked already, we can't open
|
|
// the file again.
|
|
if s.stateFileOut == nil || s.readPath != s.path {
|
|
// we haven't written a state file yet, so load from readPath
|
|
log.Printf("[TRACE] statemgr.Filesystem: reading initial snapshot from %s", s.readPath)
|
|
f, err := os.Open(s.readPath)
|
|
if err != nil {
|
|
// It is okay if the file doesn't exist; we'll treat that as a nil state.
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// we need a non-nil reader for ReadState and an empty buffer works
|
|
// to return EOF immediately
|
|
reader = bytes.NewBuffer(nil)
|
|
|
|
} else {
|
|
defer f.Close()
|
|
reader = f
|
|
}
|
|
} else {
|
|
log.Printf("[TRACE] statemgr.Filesystem: reading latest snapshot from %s", s.path)
|
|
// no state to refresh
|
|
if s.stateFileOut == nil {
|
|
return nil
|
|
}
|
|
|
|
// we have a state file, make sure we're at the start
|
|
_, err := s.stateFileOut.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader = s.stateFileOut
|
|
}
|
|
|
|
f, err := statefile.Read(reader, s.encryption)
|
|
// if there's no state then a nil file is fine
|
|
if err != nil {
|
|
if err != statefile.ErrNoState {
|
|
return err
|
|
}
|
|
log.Printf("[TRACE] statemgr.Filesystem: snapshot file has nil snapshot, but that's okay")
|
|
}
|
|
|
|
s.file = f
|
|
s.readFile = s.file.DeepCopy()
|
|
if s.file != nil {
|
|
log.Printf("[TRACE] statemgr.Filesystem: read snapshot with lineage %q serial %d", s.file.Lineage, s.file.Serial)
|
|
} else {
|
|
log.Print("[TRACE] statemgr.Filesystem: read nil snapshot")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Lock implements Locker using filesystem discretionary locks.
|
|
func (s *Filesystem) Lock(info *LockInfo) (string, error) {
|
|
defer s.mutex()()
|
|
|
|
if s.stateFileOut == nil {
|
|
if err := s.createStateFiles(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if s.lockID != "" {
|
|
return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name())
|
|
}
|
|
|
|
if err := s.lock(); err != nil {
|
|
info, infoErr := s.lockInfo()
|
|
if infoErr != nil {
|
|
err = multierror.Append(err, infoErr)
|
|
}
|
|
|
|
lockErr := &LockError{
|
|
Info: info,
|
|
Err: err,
|
|
}
|
|
|
|
return "", lockErr
|
|
}
|
|
|
|
s.lockID = info.ID
|
|
return s.lockID, s.writeLockInfo(info)
|
|
}
|
|
|
|
// Unlock is the companion to Lock, completing the implemention of Locker.
|
|
func (s *Filesystem) Unlock(id string) error {
|
|
defer s.mutex()()
|
|
|
|
if s.lockID == "" {
|
|
return fmt.Errorf("LocalState not locked")
|
|
}
|
|
|
|
if id != s.lockID {
|
|
idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
|
|
info, err := s.lockInfo()
|
|
if err != nil {
|
|
idErr = multierror.Append(idErr, err)
|
|
}
|
|
|
|
return &LockError{
|
|
Err: idErr,
|
|
Info: info,
|
|
}
|
|
}
|
|
|
|
lockInfoPath := s.lockInfoPath()
|
|
err := os.Remove(lockInfoPath)
|
|
if err != nil {
|
|
log.Printf(
|
|
"[ERROR] statemgr.Filesystem: error removing lock metadata file %q: %s",
|
|
lockInfoPath,
|
|
err,
|
|
)
|
|
} else {
|
|
log.Printf("[TRACE] statemgr.Filesystem: removed lock metadata file %s", lockInfoPath)
|
|
}
|
|
fileName := s.stateFileOut.Name()
|
|
|
|
unlockErr := s.unlock()
|
|
|
|
s.stateFileOut.Close()
|
|
s.stateFileOut = nil
|
|
s.lockID = ""
|
|
|
|
// clean up the state file if we created it an never wrote to it
|
|
stat, err := os.Stat(fileName)
|
|
if err == nil && stat.Size() == 0 && s.created {
|
|
err = os.Remove(fileName)
|
|
if err != nil {
|
|
log.Printf("[ERROR] stagemgr.Filesystem: error removing empty state file %q: %s", fileName, err)
|
|
}
|
|
}
|
|
|
|
return unlockErr
|
|
}
|
|
|
|
// StateSnapshotMeta returns the metadata from the most recently persisted
|
|
// or refreshed persistent state snapshot.
|
|
//
|
|
// This is an implementation of PersistentMeta.
|
|
func (s *Filesystem) StateSnapshotMeta() SnapshotMeta {
|
|
if s.file == nil {
|
|
return SnapshotMeta{} // placeholder
|
|
}
|
|
|
|
return SnapshotMeta{
|
|
Lineage: s.file.Lineage,
|
|
Serial: s.file.Serial,
|
|
|
|
TerraformVersion: s.file.TerraformVersion,
|
|
}
|
|
}
|
|
|
|
// StateForMigration is part of our implementation of Migrator.
|
|
func (s *Filesystem) StateForMigration() *statefile.File {
|
|
return s.file.DeepCopy()
|
|
}
|
|
|
|
// WriteStateForMigration is part of our implementation of Migrator.
|
|
func (s *Filesystem) WriteStateForMigration(f *statefile.File, force bool) error {
|
|
defer s.mutex()()
|
|
|
|
if s.readFile == nil {
|
|
err := s.refreshState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !force {
|
|
err := CheckValidImport(f, s.readFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if s.readFile != nil {
|
|
log.Printf(
|
|
"[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d over snapshot with lineage %q serial %d at %s",
|
|
f.Lineage, f.Serial,
|
|
s.readFile.Lineage, s.readFile.Serial,
|
|
s.path,
|
|
)
|
|
} else {
|
|
log.Printf(
|
|
"[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d as the initial state snapshot at %s",
|
|
f.Lineage, f.Serial,
|
|
s.path,
|
|
)
|
|
}
|
|
|
|
err := s.writeState(f.State, &SnapshotMeta{Lineage: f.Lineage, Serial: f.Serial})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.persistState(nil)
|
|
}
|
|
|
|
// Open the state file, creating the directories and file as needed.
|
|
func (s *Filesystem) createStateFiles() error {
|
|
log.Printf("[TRACE] statemgr.Filesystem: preparing to manage state snapshots at %s", s.path)
|
|
|
|
// This could race, but we only use it to clean up empty files
|
|
if _, err := os.Stat(s.path); os.IsNotExist(err) {
|
|
s.created = true
|
|
}
|
|
|
|
// Create all the directories
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.stateFileOut = f
|
|
|
|
// If the file already existed with content then that'll be the content
|
|
// of our backup file if we write a change later.
|
|
s.backupFile, err = statefile.Read(s.stateFileOut, s.encryption)
|
|
if err != nil {
|
|
if err != statefile.ErrNoState {
|
|
return err
|
|
}
|
|
log.Printf("[TRACE] statemgr.Filesystem: no previously-stored snapshot exists")
|
|
} else {
|
|
log.Printf("[TRACE] statemgr.Filesystem: existing snapshot has lineage %q serial %d", s.backupFile.Lineage, s.backupFile.Serial)
|
|
}
|
|
|
|
// Refresh now, to load in the snapshot if the file already existed
|
|
return nil
|
|
}
|
|
|
|
// return the path for the lockInfo metadata.
|
|
func (s *Filesystem) lockInfoPath() string {
|
|
stateDir, stateName := filepath.Split(s.path)
|
|
if stateName == "" {
|
|
panic("empty state file path")
|
|
}
|
|
|
|
if stateName[0] == '.' {
|
|
stateName = stateName[1:]
|
|
}
|
|
|
|
return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName))
|
|
}
|
|
|
|
// lockInfo returns the data in a lock info file
|
|
func (s *Filesystem) lockInfo() (*LockInfo, error) {
|
|
path := s.lockInfoPath()
|
|
infoData, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := LockInfo{}
|
|
err = json.Unmarshal(infoData, &info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %w", s.readPath, err)
|
|
}
|
|
return &info, nil
|
|
}
|
|
|
|
// write a new lock info file
|
|
func (s *Filesystem) writeLockInfo(info *LockInfo) error {
|
|
path := s.lockInfoPath()
|
|
info.Path = s.readPath
|
|
info.Created = time.Now().UTC()
|
|
|
|
log.Printf("[TRACE] statemgr.Filesystem: writing lock metadata to %s", path)
|
|
err := os.WriteFile(path, info.Marshal(), 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("could not write lock info for %q: %w", s.readPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Filesystem) mutex() func() {
|
|
s.mu.Lock()
|
|
return s.mu.Unlock
|
|
}
|