// 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 } }