opentofu/internal/states/statemgr/filesystem_test.go
Martin Atkins f40800b3a4 Move states/ to internal/states/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

444 lines
10 KiB
Go

package statemgr
import (
"io/ioutil"
"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/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
tfversion "github.com/hashicorp/terraform/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 := ioutil.TempFile("", "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 := ioutil.TempFile("", "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)
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, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
defer os.RemoveAll(workDir)
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)
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)
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())
ls.SetBackupPath(backupPath)
newState := states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(
markerOutput,
cty.StringVal("from-new-state"),
false, // not sensitive
)
})
err = ls.WriteState(newState)
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)
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)
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")
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 _ Locker = new(Filesystem)
}
func testFilesystem(t *testing.T) *Filesystem {
f, err := ioutil.TempFile("", "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)
if err != nil {
t.Fatalf("failed to write initial state to %s: %s", f.Name(), err)
}
f.Close()
ls := NewFilesystem(f.Name())
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 := ioutil.TempFile("", "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)
if err != nil {
t.Fatalf("err: %s", err)
}
f.Close()
s := NewFilesystem(f.Name())
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 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
}
}