mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
2f5dcd5c0a
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
459 lines
11 KiB
Go
459 lines
11 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 (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/go-test/deep"
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
tfversion "github.com/opentofu/opentofu/version"
|
|
)
|
|
|
|
func TestFilesystem(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
ls := testFilesystem(t)
|
|
defer os.Remove(ls.readPath)
|
|
TestFull(t, ls)
|
|
}
|
|
|
|
func TestFilesystemRace(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
ls := testFilesystem(t)
|
|
defer os.Remove(ls.readPath)
|
|
|
|
current := TestFullInitialState()
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ls.WriteState(current)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestFilesystemLocks(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
s := testFilesystem(t)
|
|
defer os.Remove(s.readPath)
|
|
|
|
// lock first
|
|
info := NewLockInfo()
|
|
info.Operation = "test"
|
|
lockID, err := s.Lock(info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
out, err := exec.Command("go", "run", "testdata/lockstate.go", s.path).CombinedOutput()
|
|
if err != nil {
|
|
t.Fatal("unexpected lock failure", err, string(out))
|
|
}
|
|
|
|
if !strings.Contains(string(out), "lock failed") {
|
|
t.Fatal("expected 'locked failed', got", string(out))
|
|
}
|
|
|
|
// check our lock info
|
|
lockInfo, err := s.lockInfo()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if lockInfo.Operation != "test" {
|
|
t.Fatalf("invalid lock info %#v\n", lockInfo)
|
|
}
|
|
|
|
// a noop, since we unlock on exit
|
|
if err := s.Unlock(lockID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// local locks can re-lock
|
|
lockID, err = s.Lock(info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := s.Unlock(lockID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// we should not be able to unlock the same lock twice
|
|
if err := s.Unlock(lockID); err == nil {
|
|
t.Fatal("unlocking an unlocked state should fail")
|
|
}
|
|
|
|
// make sure lock info is gone
|
|
lockInfoPath := s.lockInfoPath()
|
|
if _, err := os.Stat(lockInfoPath); !os.IsNotExist(err) {
|
|
t.Fatal("lock info not removed")
|
|
}
|
|
}
|
|
|
|
// Verify that we can write to the state file, as Windows' mandatory locking
|
|
// will prevent writing to a handle different than the one that hold the lock.
|
|
func TestFilesystem_writeWhileLocked(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
s := testFilesystem(t)
|
|
defer os.Remove(s.readPath)
|
|
|
|
// lock first
|
|
info := NewLockInfo()
|
|
info.Operation = "test"
|
|
lockID, err := s.Lock(info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := s.Unlock(lockID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
if err := s.WriteState(TestFullInitialState()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestFilesystem_pathOut(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
f, err := os.CreateTemp("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
f.Close()
|
|
defer os.Remove(f.Name())
|
|
|
|
ls := testFilesystem(t)
|
|
ls.path = f.Name()
|
|
defer os.Remove(ls.path)
|
|
|
|
TestFull(t, ls)
|
|
}
|
|
|
|
func TestFilesystem_backup(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
f, err := os.CreateTemp("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
f.Close()
|
|
defer os.Remove(f.Name())
|
|
|
|
ls := testFilesystem(t)
|
|
backupPath := f.Name()
|
|
ls.SetBackupPath(backupPath)
|
|
|
|
TestFull(t, ls)
|
|
|
|
// The backup functionality should've saved a copy of the original state
|
|
// prior to all of the modifications that TestFull does.
|
|
bfh, err := os.Open(backupPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bf, err := statefile.Read(bfh, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
origState := TestFullInitialState()
|
|
if !bf.State.Equal(origState) {
|
|
for _, problem := range deep.Equal(origState, bf.State) {
|
|
t.Error(problem)
|
|
}
|
|
}
|
|
}
|
|
|
|
// This test verifies a particularly tricky behavior where the input file
|
|
// is overridden and backups are enabled at the same time. This combination
|
|
// requires special care because we must ensure that when we create a backup
|
|
// it is of the original contents of the output file (which we're overwriting),
|
|
// not the contents of the input file (which is left unchanged).
|
|
func TestFilesystem_backupAndReadPath(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
|
|
workDir := t.TempDir()
|
|
|
|
markerOutput := addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)
|
|
|
|
outState := states.BuildState(func(ss *states.SyncState) {
|
|
ss.SetOutputValue(
|
|
markerOutput,
|
|
cty.StringVal("from-output-state"),
|
|
false, // not sensitive
|
|
)
|
|
})
|
|
outFile, err := os.Create(filepath.Join(workDir, "output.tfstate"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create temporary outFile %s", err)
|
|
}
|
|
defer outFile.Close()
|
|
err = statefile.Write(&statefile.File{
|
|
Lineage: "-",
|
|
Serial: 0,
|
|
TerraformVersion: version.Must(version.NewVersion("1.2.3")),
|
|
State: outState,
|
|
}, outFile, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("failed to write initial outfile state to %s: %s", outFile.Name(), err)
|
|
}
|
|
|
|
inState := states.BuildState(func(ss *states.SyncState) {
|
|
ss.SetOutputValue(
|
|
markerOutput,
|
|
cty.StringVal("from-input-state"),
|
|
false, // not sensitive
|
|
)
|
|
})
|
|
inFile, err := os.Create(filepath.Join(workDir, "input.tfstate"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create temporary inFile %s", err)
|
|
}
|
|
defer inFile.Close()
|
|
err = statefile.Write(&statefile.File{
|
|
Lineage: "-",
|
|
Serial: 0,
|
|
TerraformVersion: version.Must(version.NewVersion("1.2.3")),
|
|
State: inState,
|
|
}, inFile, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("failed to write initial infile state to %s: %s", inFile.Name(), err)
|
|
}
|
|
|
|
backupPath := outFile.Name() + ".backup"
|
|
|
|
ls := NewFilesystemBetweenPaths(inFile.Name(), outFile.Name(), encryption.StateEncryptionDisabled())
|
|
ls.SetBackupPath(backupPath)
|
|
|
|
newState := states.BuildState(func(ss *states.SyncState) {
|
|
ss.SetOutputValue(
|
|
markerOutput,
|
|
cty.StringVal("from-new-state"),
|
|
false, // not sensitive
|
|
)
|
|
})
|
|
err = WriteAndPersist(ls, newState, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to write new state: %s", err)
|
|
}
|
|
|
|
// The backup functionality should've saved a copy of the original contents
|
|
// of the _output_ file, even though the first snapshot was read from
|
|
// the _input_ file.
|
|
t.Run("backup file", func(t *testing.T) {
|
|
bfh, err := os.Open(backupPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bf, err := statefile.Read(bfh, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os := bf.State.OutputValue(markerOutput)
|
|
if got, want := os.Value, cty.StringVal("from-output-state"); !want.RawEquals(got) {
|
|
t.Errorf("wrong marker value in backup state file\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
t.Run("output file", func(t *testing.T) {
|
|
ofh, err := os.Open(outFile.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
of, err := statefile.Read(ofh, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os := of.State.OutputValue(markerOutput)
|
|
if got, want := os.Value, cty.StringVal("from-new-state"); !want.RawEquals(got) {
|
|
t.Errorf("wrong marker value in backup state file\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFilesystem_nonExist(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
ls := NewFilesystem("ishouldntexist", encryption.StateEncryptionDisabled())
|
|
if err := ls.RefreshState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if state := ls.State(); state != nil {
|
|
t.Fatalf("bad: %#v", state)
|
|
}
|
|
}
|
|
|
|
func TestFilesystem_lockUnlockWithoutWrite(t *testing.T) {
|
|
info := NewLockInfo()
|
|
info.Operation = "test"
|
|
|
|
ls := testFilesystem(t)
|
|
|
|
// Delete the just-created tempfile so that Lock recreates it
|
|
os.Remove(ls.path)
|
|
|
|
// Lock the state, and in doing so recreate the tempfile
|
|
lockID, err := ls.Lock(info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !ls.created {
|
|
t.Fatal("should have marked state as created")
|
|
}
|
|
|
|
if err := ls.Unlock(lockID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = os.Stat(ls.path)
|
|
if os.IsNotExist(err) {
|
|
// Success! Unlocking the state successfully deleted the tempfile
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error from os.Stat: %s", err)
|
|
} else {
|
|
os.Remove(ls.readPath)
|
|
t.Fatal("should have removed path, but exists")
|
|
}
|
|
}
|
|
|
|
func TestFilesystem_impl(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
var _ Reader = new(Filesystem)
|
|
var _ Writer = new(Filesystem)
|
|
var _ Persister = new(Filesystem)
|
|
var _ Refresher = new(Filesystem)
|
|
var _ OutputReader = new(Filesystem)
|
|
var _ Locker = new(Filesystem)
|
|
}
|
|
|
|
func testFilesystem(t *testing.T) *Filesystem {
|
|
f, err := os.CreateTemp("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temporary file %s", err)
|
|
}
|
|
t.Logf("temporary state file at %s", f.Name())
|
|
|
|
err = statefile.Write(&statefile.File{
|
|
Lineage: "test-lineage",
|
|
Serial: 0,
|
|
TerraformVersion: version.Must(version.NewVersion("1.2.3")),
|
|
State: TestFullInitialState(),
|
|
}, f, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("failed to write initial state to %s: %s", f.Name(), err)
|
|
}
|
|
f.Close()
|
|
|
|
ls := NewFilesystem(f.Name(), encryption.StateEncryptionDisabled())
|
|
if err := ls.RefreshState(); err != nil {
|
|
t.Fatalf("initial refresh failed: %s", err)
|
|
}
|
|
|
|
return ls
|
|
}
|
|
|
|
// Make sure we can refresh while the state is locked
|
|
func TestFilesystem_refreshWhileLocked(t *testing.T) {
|
|
defer testOverrideVersion(t, "1.2.3")()
|
|
f, err := os.CreateTemp("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
err = statefile.Write(&statefile.File{
|
|
Lineage: "test-lineage",
|
|
Serial: 0,
|
|
TerraformVersion: version.Must(version.NewVersion("1.2.3")),
|
|
State: TestFullInitialState(),
|
|
}, f, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
f.Close()
|
|
|
|
s := NewFilesystem(f.Name(), encryption.StateEncryptionDisabled())
|
|
defer os.Remove(s.path)
|
|
|
|
// lock first
|
|
info := NewLockInfo()
|
|
info.Operation = "test"
|
|
lockID, err := s.Lock(info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := s.Unlock(lockID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
if err := s.RefreshState(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
readState := s.State()
|
|
if readState == nil {
|
|
t.Fatal("missing state")
|
|
}
|
|
}
|
|
|
|
func TestFilesystem_GetRootOutputValues(t *testing.T) {
|
|
fs := testFilesystem(t)
|
|
|
|
outputs, err := fs.GetRootOutputValues()
|
|
if err != nil {
|
|
t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err)
|
|
}
|
|
|
|
if len(outputs) != 2 {
|
|
t.Errorf("Expected %d outputs, but received %d", 2, len(outputs))
|
|
}
|
|
}
|
|
|
|
func testOverrideVersion(t *testing.T, v string) func() {
|
|
oldVersionStr := tfversion.Version
|
|
oldPrereleaseStr := tfversion.Prerelease
|
|
oldSemVer := tfversion.SemVer
|
|
|
|
var newPrereleaseStr string
|
|
if dash := strings.Index(v, "-"); dash != -1 {
|
|
newPrereleaseStr = v[dash+1:]
|
|
v = v[:dash]
|
|
}
|
|
|
|
newSemVer, err := version.NewVersion(v)
|
|
if err != nil {
|
|
t.Errorf("invalid override version %q: %s", v, err)
|
|
}
|
|
newVersionStr := newSemVer.String()
|
|
|
|
tfversion.Version = newVersionStr
|
|
tfversion.Prerelease = newPrereleaseStr
|
|
tfversion.SemVer = newSemVer
|
|
|
|
return func() { // reset function
|
|
tfversion.Version = oldVersionStr
|
|
tfversion.Prerelease = oldPrereleaseStr
|
|
tfversion.SemVer = oldSemVer
|
|
}
|
|
}
|