github.com/opentofu/opentofu@v1.7.1/internal/states/statemgr/filesystem_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package statemgr
     7  
     8  import (
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"testing"
    15  
    16  	"github.com/go-test/deep"
    17  	version "github.com/hashicorp/go-version"
    18  	"github.com/zclconf/go-cty/cty"
    19  
    20  	"github.com/opentofu/opentofu/internal/addrs"
    21  	"github.com/opentofu/opentofu/internal/encryption"
    22  	"github.com/opentofu/opentofu/internal/states"
    23  	"github.com/opentofu/opentofu/internal/states/statefile"
    24  	tfversion "github.com/opentofu/opentofu/version"
    25  )
    26  
    27  func TestFilesystem(t *testing.T) {
    28  	defer testOverrideVersion(t, "1.2.3")()
    29  	ls := testFilesystem(t)
    30  	defer os.Remove(ls.readPath)
    31  	TestFull(t, ls)
    32  }
    33  
    34  func TestFilesystemRace(t *testing.T) {
    35  	defer testOverrideVersion(t, "1.2.3")()
    36  	ls := testFilesystem(t)
    37  	defer os.Remove(ls.readPath)
    38  
    39  	current := TestFullInitialState()
    40  
    41  	var wg sync.WaitGroup
    42  	for i := 0; i < 100; i++ {
    43  		wg.Add(1)
    44  		go func() {
    45  			defer wg.Done()
    46  			ls.WriteState(current)
    47  		}()
    48  	}
    49  	wg.Wait()
    50  }
    51  
    52  func TestFilesystemLocks(t *testing.T) {
    53  	defer testOverrideVersion(t, "1.2.3")()
    54  	s := testFilesystem(t)
    55  	defer os.Remove(s.readPath)
    56  
    57  	// lock first
    58  	info := NewLockInfo()
    59  	info.Operation = "test"
    60  	lockID, err := s.Lock(info)
    61  	if err != nil {
    62  		t.Fatal(err)
    63  	}
    64  
    65  	out, err := exec.Command("go", "run", "testdata/lockstate.go", s.path).CombinedOutput()
    66  	if err != nil {
    67  		t.Fatal("unexpected lock failure", err, string(out))
    68  	}
    69  
    70  	if !strings.Contains(string(out), "lock failed") {
    71  		t.Fatal("expected 'locked failed', got", string(out))
    72  	}
    73  
    74  	// check our lock info
    75  	lockInfo, err := s.lockInfo()
    76  	if err != nil {
    77  		t.Fatal(err)
    78  	}
    79  
    80  	if lockInfo.Operation != "test" {
    81  		t.Fatalf("invalid lock info %#v\n", lockInfo)
    82  	}
    83  
    84  	// a noop, since we unlock on exit
    85  	if err := s.Unlock(lockID); err != nil {
    86  		t.Fatal(err)
    87  	}
    88  
    89  	// local locks can re-lock
    90  	lockID, err = s.Lock(info)
    91  	if err != nil {
    92  		t.Fatal(err)
    93  	}
    94  
    95  	if err := s.Unlock(lockID); err != nil {
    96  		t.Fatal(err)
    97  	}
    98  
    99  	// we should not be able to unlock the same lock twice
   100  	if err := s.Unlock(lockID); err == nil {
   101  		t.Fatal("unlocking an unlocked state should fail")
   102  	}
   103  
   104  	// make sure lock info is gone
   105  	lockInfoPath := s.lockInfoPath()
   106  	if _, err := os.Stat(lockInfoPath); !os.IsNotExist(err) {
   107  		t.Fatal("lock info not removed")
   108  	}
   109  }
   110  
   111  // Verify that we can write to the state file, as Windows' mandatory locking
   112  // will prevent writing to a handle different than the one that hold the lock.
   113  func TestFilesystem_writeWhileLocked(t *testing.T) {
   114  	defer testOverrideVersion(t, "1.2.3")()
   115  	s := testFilesystem(t)
   116  	defer os.Remove(s.readPath)
   117  
   118  	// lock first
   119  	info := NewLockInfo()
   120  	info.Operation = "test"
   121  	lockID, err := s.Lock(info)
   122  	if err != nil {
   123  		t.Fatal(err)
   124  	}
   125  	defer func() {
   126  		if err := s.Unlock(lockID); err != nil {
   127  			t.Fatal(err)
   128  		}
   129  	}()
   130  
   131  	if err := s.WriteState(TestFullInitialState()); err != nil {
   132  		t.Fatal(err)
   133  	}
   134  }
   135  
   136  func TestFilesystem_pathOut(t *testing.T) {
   137  	defer testOverrideVersion(t, "1.2.3")()
   138  	f, err := os.CreateTemp("", "tf")
   139  	if err != nil {
   140  		t.Fatalf("err: %s", err)
   141  	}
   142  	f.Close()
   143  	defer os.Remove(f.Name())
   144  
   145  	ls := testFilesystem(t)
   146  	ls.path = f.Name()
   147  	defer os.Remove(ls.path)
   148  
   149  	TestFull(t, ls)
   150  }
   151  
   152  func TestFilesystem_backup(t *testing.T) {
   153  	defer testOverrideVersion(t, "1.2.3")()
   154  	f, err := os.CreateTemp("", "tf")
   155  	if err != nil {
   156  		t.Fatalf("err: %s", err)
   157  	}
   158  	f.Close()
   159  	defer os.Remove(f.Name())
   160  
   161  	ls := testFilesystem(t)
   162  	backupPath := f.Name()
   163  	ls.SetBackupPath(backupPath)
   164  
   165  	TestFull(t, ls)
   166  
   167  	// The backup functionality should've saved a copy of the original state
   168  	// prior to all of the modifications that TestFull does.
   169  	bfh, err := os.Open(backupPath)
   170  	if err != nil {
   171  		t.Fatal(err)
   172  	}
   173  	bf, err := statefile.Read(bfh, encryption.StateEncryptionDisabled())
   174  	if err != nil {
   175  		t.Fatal(err)
   176  	}
   177  	origState := TestFullInitialState()
   178  	if !bf.State.Equal(origState) {
   179  		for _, problem := range deep.Equal(origState, bf.State) {
   180  			t.Error(problem)
   181  		}
   182  	}
   183  }
   184  
   185  // This test verifies a particularly tricky behavior where the input file
   186  // is overridden and backups are enabled at the same time. This combination
   187  // requires special care because we must ensure that when we create a backup
   188  // it is of the original contents of the output file (which we're overwriting),
   189  // not the contents of the input file (which is left unchanged).
   190  func TestFilesystem_backupAndReadPath(t *testing.T) {
   191  	defer testOverrideVersion(t, "1.2.3")()
   192  
   193  	workDir := t.TempDir()
   194  
   195  	markerOutput := addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)
   196  
   197  	outState := states.BuildState(func(ss *states.SyncState) {
   198  		ss.SetOutputValue(
   199  			markerOutput,
   200  			cty.StringVal("from-output-state"),
   201  			false, // not sensitive
   202  		)
   203  	})
   204  	outFile, err := os.Create(filepath.Join(workDir, "output.tfstate"))
   205  	if err != nil {
   206  		t.Fatalf("failed to create temporary outFile %s", err)
   207  	}
   208  	defer outFile.Close()
   209  	err = statefile.Write(&statefile.File{
   210  		Lineage:          "-",
   211  		Serial:           0,
   212  		TerraformVersion: version.Must(version.NewVersion("1.2.3")),
   213  		State:            outState,
   214  	}, outFile, encryption.StateEncryptionDisabled())
   215  	if err != nil {
   216  		t.Fatalf("failed to write initial outfile state to %s: %s", outFile.Name(), err)
   217  	}
   218  
   219  	inState := states.BuildState(func(ss *states.SyncState) {
   220  		ss.SetOutputValue(
   221  			markerOutput,
   222  			cty.StringVal("from-input-state"),
   223  			false, // not sensitive
   224  		)
   225  	})
   226  	inFile, err := os.Create(filepath.Join(workDir, "input.tfstate"))
   227  	if err != nil {
   228  		t.Fatalf("failed to create temporary inFile %s", err)
   229  	}
   230  	defer inFile.Close()
   231  	err = statefile.Write(&statefile.File{
   232  		Lineage:          "-",
   233  		Serial:           0,
   234  		TerraformVersion: version.Must(version.NewVersion("1.2.3")),
   235  		State:            inState,
   236  	}, inFile, encryption.StateEncryptionDisabled())
   237  	if err != nil {
   238  		t.Fatalf("failed to write initial infile state to %s: %s", inFile.Name(), err)
   239  	}
   240  
   241  	backupPath := outFile.Name() + ".backup"
   242  
   243  	ls := NewFilesystemBetweenPaths(inFile.Name(), outFile.Name(), encryption.StateEncryptionDisabled())
   244  	ls.SetBackupPath(backupPath)
   245  
   246  	newState := states.BuildState(func(ss *states.SyncState) {
   247  		ss.SetOutputValue(
   248  			markerOutput,
   249  			cty.StringVal("from-new-state"),
   250  			false, // not sensitive
   251  		)
   252  	})
   253  	err = WriteAndPersist(ls, newState, nil)
   254  	if err != nil {
   255  		t.Fatalf("failed to write new state: %s", err)
   256  	}
   257  
   258  	// The backup functionality should've saved a copy of the original contents
   259  	// of the _output_ file, even though the first snapshot was read from
   260  	// the _input_ file.
   261  	t.Run("backup file", func(t *testing.T) {
   262  		bfh, err := os.Open(backupPath)
   263  		if err != nil {
   264  			t.Fatal(err)
   265  		}
   266  		bf, err := statefile.Read(bfh, encryption.StateEncryptionDisabled())
   267  		if err != nil {
   268  			t.Fatal(err)
   269  		}
   270  		os := bf.State.OutputValue(markerOutput)
   271  		if got, want := os.Value, cty.StringVal("from-output-state"); !want.RawEquals(got) {
   272  			t.Errorf("wrong marker value in backup state file\ngot:  %#v\nwant: %#v", got, want)
   273  		}
   274  	})
   275  	t.Run("output file", func(t *testing.T) {
   276  		ofh, err := os.Open(outFile.Name())
   277  		if err != nil {
   278  			t.Fatal(err)
   279  		}
   280  		of, err := statefile.Read(ofh, encryption.StateEncryptionDisabled())
   281  		if err != nil {
   282  			t.Fatal(err)
   283  		}
   284  		os := of.State.OutputValue(markerOutput)
   285  		if got, want := os.Value, cty.StringVal("from-new-state"); !want.RawEquals(got) {
   286  			t.Errorf("wrong marker value in backup state file\ngot:  %#v\nwant: %#v", got, want)
   287  		}
   288  	})
   289  }
   290  
   291  func TestFilesystem_nonExist(t *testing.T) {
   292  	defer testOverrideVersion(t, "1.2.3")()
   293  	ls := NewFilesystem("ishouldntexist", encryption.StateEncryptionDisabled())
   294  	if err := ls.RefreshState(); err != nil {
   295  		t.Fatalf("err: %s", err)
   296  	}
   297  
   298  	if state := ls.State(); state != nil {
   299  		t.Fatalf("bad: %#v", state)
   300  	}
   301  }
   302  
   303  func TestFilesystem_lockUnlockWithoutWrite(t *testing.T) {
   304  	info := NewLockInfo()
   305  	info.Operation = "test"
   306  
   307  	ls := testFilesystem(t)
   308  
   309  	// Delete the just-created tempfile so that Lock recreates it
   310  	os.Remove(ls.path)
   311  
   312  	// Lock the state, and in doing so recreate the tempfile
   313  	lockID, err := ls.Lock(info)
   314  	if err != nil {
   315  		t.Fatal(err)
   316  	}
   317  
   318  	if !ls.created {
   319  		t.Fatal("should have marked state as created")
   320  	}
   321  
   322  	if err := ls.Unlock(lockID); err != nil {
   323  		t.Fatal(err)
   324  	}
   325  
   326  	_, err = os.Stat(ls.path)
   327  	if os.IsNotExist(err) {
   328  		// Success! Unlocking the state successfully deleted the tempfile
   329  		return
   330  	} else if err != nil {
   331  		t.Fatalf("unexpected error from os.Stat: %s", err)
   332  	} else {
   333  		os.Remove(ls.readPath)
   334  		t.Fatal("should have removed path, but exists")
   335  	}
   336  }
   337  
   338  func TestFilesystem_impl(t *testing.T) {
   339  	defer testOverrideVersion(t, "1.2.3")()
   340  	var _ Reader = new(Filesystem)
   341  	var _ Writer = new(Filesystem)
   342  	var _ Persister = new(Filesystem)
   343  	var _ Refresher = new(Filesystem)
   344  	var _ OutputReader = new(Filesystem)
   345  	var _ Locker = new(Filesystem)
   346  }
   347  
   348  func testFilesystem(t *testing.T) *Filesystem {
   349  	f, err := os.CreateTemp("", "tf")
   350  	if err != nil {
   351  		t.Fatalf("failed to create temporary file %s", err)
   352  	}
   353  	t.Logf("temporary state file at %s", f.Name())
   354  
   355  	err = statefile.Write(&statefile.File{
   356  		Lineage:          "test-lineage",
   357  		Serial:           0,
   358  		TerraformVersion: version.Must(version.NewVersion("1.2.3")),
   359  		State:            TestFullInitialState(),
   360  	}, f, encryption.StateEncryptionDisabled())
   361  	if err != nil {
   362  		t.Fatalf("failed to write initial state to %s: %s", f.Name(), err)
   363  	}
   364  	f.Close()
   365  
   366  	ls := NewFilesystem(f.Name(), encryption.StateEncryptionDisabled())
   367  	if err := ls.RefreshState(); err != nil {
   368  		t.Fatalf("initial refresh failed: %s", err)
   369  	}
   370  
   371  	return ls
   372  }
   373  
   374  // Make sure we can refresh while the state is locked
   375  func TestFilesystem_refreshWhileLocked(t *testing.T) {
   376  	defer testOverrideVersion(t, "1.2.3")()
   377  	f, err := os.CreateTemp("", "tf")
   378  	if err != nil {
   379  		t.Fatalf("err: %s", err)
   380  	}
   381  
   382  	err = statefile.Write(&statefile.File{
   383  		Lineage:          "test-lineage",
   384  		Serial:           0,
   385  		TerraformVersion: version.Must(version.NewVersion("1.2.3")),
   386  		State:            TestFullInitialState(),
   387  	}, f, encryption.StateEncryptionDisabled())
   388  	if err != nil {
   389  		t.Fatalf("err: %s", err)
   390  	}
   391  	f.Close()
   392  
   393  	s := NewFilesystem(f.Name(), encryption.StateEncryptionDisabled())
   394  	defer os.Remove(s.path)
   395  
   396  	// lock first
   397  	info := NewLockInfo()
   398  	info.Operation = "test"
   399  	lockID, err := s.Lock(info)
   400  	if err != nil {
   401  		t.Fatal(err)
   402  	}
   403  	defer func() {
   404  		if err := s.Unlock(lockID); err != nil {
   405  			t.Fatal(err)
   406  		}
   407  	}()
   408  
   409  	if err := s.RefreshState(); err != nil {
   410  		t.Fatal(err)
   411  	}
   412  
   413  	readState := s.State()
   414  	if readState == nil {
   415  		t.Fatal("missing state")
   416  	}
   417  }
   418  
   419  func TestFilesystem_GetRootOutputValues(t *testing.T) {
   420  	fs := testFilesystem(t)
   421  
   422  	outputs, err := fs.GetRootOutputValues()
   423  	if err != nil {
   424  		t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err)
   425  	}
   426  
   427  	if len(outputs) != 2 {
   428  		t.Errorf("Expected %d outputs, but received %d", 2, len(outputs))
   429  	}
   430  }
   431  
   432  func testOverrideVersion(t *testing.T, v string) func() {
   433  	oldVersionStr := tfversion.Version
   434  	oldPrereleaseStr := tfversion.Prerelease
   435  	oldSemVer := tfversion.SemVer
   436  
   437  	var newPrereleaseStr string
   438  	if dash := strings.Index(v, "-"); dash != -1 {
   439  		newPrereleaseStr = v[dash+1:]
   440  		v = v[:dash]
   441  	}
   442  
   443  	newSemVer, err := version.NewVersion(v)
   444  	if err != nil {
   445  		t.Errorf("invalid override version %q: %s", v, err)
   446  	}
   447  	newVersionStr := newSemVer.String()
   448  
   449  	tfversion.Version = newVersionStr
   450  	tfversion.Prerelease = newPrereleaseStr
   451  	tfversion.SemVer = newSemVer
   452  
   453  	return func() { // reset function
   454  		tfversion.Version = oldVersionStr
   455  		tfversion.Prerelease = oldPrereleaseStr
   456  		tfversion.SemVer = oldSemVer
   457  	}
   458  }