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 }