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