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 }