github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/statemgr/filesystem_test.go (about)

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