github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/state/remote/state_test.go (about) 1 package remote 2 3 import ( 4 "log" 5 "sync" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/hashicorp/terraform/states" 12 "github.com/hashicorp/terraform/states/statefile" 13 "github.com/hashicorp/terraform/states/statemgr" 14 "github.com/hashicorp/terraform/version" 15 ) 16 17 func TestState_impl(t *testing.T) { 18 var _ statemgr.Reader = new(State) 19 var _ statemgr.Writer = new(State) 20 var _ statemgr.Persister = new(State) 21 var _ statemgr.Refresher = new(State) 22 var _ statemgr.Locker = new(State) 23 } 24 25 func TestStateRace(t *testing.T) { 26 s := &State{ 27 Client: nilClient{}, 28 } 29 30 current := states.NewState() 31 32 var wg sync.WaitGroup 33 34 for i := 0; i < 100; i++ { 35 wg.Add(1) 36 go func() { 37 defer wg.Done() 38 s.WriteState(current) 39 s.PersistState() 40 s.RefreshState() 41 }() 42 } 43 wg.Wait() 44 } 45 46 // testCase encapsulates a test state test 47 type testCase struct { 48 name string 49 // A function to mutate state and return a cleanup function 50 mutationFunc func(*State) (*states.State, func()) 51 // The expected request to have taken place 52 expectedRequest mockClientRequest 53 // Mark this case as not having a request 54 noRequest bool 55 } 56 57 // isRequested ensures a test that is specified as not having 58 // a request doesn't have one by checking if a method exists 59 // on the expectedRequest. 60 func (tc testCase) isRequested(t *testing.T) bool { 61 hasMethod := tc.expectedRequest.Method != "" 62 if tc.noRequest && hasMethod { 63 t.Fatalf("expected no content for %q but got: %v", tc.name, tc.expectedRequest) 64 } 65 return !tc.noRequest 66 } 67 68 func TestStatePersist(t *testing.T) { 69 testCases := []testCase{ 70 // Refreshing state before we run the test loop causes a GET 71 { 72 name: "refresh state", 73 mutationFunc: func(mgr *State) (*states.State, func()) { 74 return mgr.State(), func() {} 75 }, 76 expectedRequest: mockClientRequest{ 77 Method: "Get", 78 Content: map[string]interface{}{ 79 "version": 4.0, // encoding/json decodes this as float64 by default 80 "lineage": "mock-lineage", 81 "serial": 1.0, // encoding/json decodes this as float64 by default 82 "terraform_version": "0.0.0", 83 "outputs": map[string]interface{}{}, 84 "resources": []interface{}{}, 85 }, 86 }, 87 }, 88 { 89 name: "change lineage", 90 mutationFunc: func(mgr *State) (*states.State, func()) { 91 originalLineage := mgr.lineage 92 mgr.lineage = "some-new-lineage" 93 return mgr.State(), func() { 94 mgr.lineage = originalLineage 95 } 96 }, 97 expectedRequest: mockClientRequest{ 98 Method: "Put", 99 Content: map[string]interface{}{ 100 "version": 4.0, // encoding/json decodes this as float64 by default 101 "lineage": "some-new-lineage", 102 "serial": 2.0, // encoding/json decodes this as float64 by default 103 "terraform_version": version.Version, 104 "outputs": map[string]interface{}{}, 105 "resources": []interface{}{}, 106 }, 107 }, 108 }, 109 { 110 name: "change serial", 111 mutationFunc: func(mgr *State) (*states.State, func()) { 112 originalSerial := mgr.serial 113 mgr.serial++ 114 return mgr.State(), func() { 115 mgr.serial = originalSerial 116 } 117 }, 118 expectedRequest: mockClientRequest{ 119 Method: "Put", 120 Content: map[string]interface{}{ 121 "version": 4.0, // encoding/json decodes this as float64 by default 122 "lineage": "mock-lineage", 123 "serial": 4.0, // encoding/json decodes this as float64 by default 124 "terraform_version": version.Version, 125 "outputs": map[string]interface{}{}, 126 "resources": []interface{}{}, 127 }, 128 }, 129 }, 130 { 131 name: "add output to state", 132 mutationFunc: func(mgr *State) (*states.State, func()) { 133 s := mgr.State() 134 s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) 135 return s, func() {} 136 }, 137 expectedRequest: mockClientRequest{ 138 Method: "Put", 139 Content: map[string]interface{}{ 140 "version": 4.0, // encoding/json decodes this as float64 by default 141 "lineage": "mock-lineage", 142 "serial": 3.0, // encoding/json decodes this as float64 by default 143 "terraform_version": version.Version, 144 "outputs": map[string]interface{}{ 145 "foo": map[string]interface{}{ 146 "type": "string", 147 "value": "bar", 148 }, 149 }, 150 "resources": []interface{}{}, 151 }, 152 }, 153 }, 154 { 155 name: "mutate state bar -> baz", 156 mutationFunc: func(mgr *State) (*states.State, func()) { 157 s := mgr.State() 158 s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false) 159 return s, func() {} 160 }, 161 expectedRequest: mockClientRequest{ 162 Method: "Put", 163 Content: map[string]interface{}{ 164 "version": 4.0, // encoding/json decodes this as float64 by default 165 "lineage": "mock-lineage", 166 "serial": 4.0, // encoding/json decodes this as float64 by default 167 "terraform_version": version.Version, 168 "outputs": map[string]interface{}{ 169 "foo": map[string]interface{}{ 170 "type": "string", 171 "value": "baz", 172 }, 173 }, 174 "resources": []interface{}{}, 175 }, 176 }, 177 }, 178 { 179 name: "nothing changed", 180 mutationFunc: func(mgr *State) (*states.State, func()) { 181 s := mgr.State() 182 return s, func() {} 183 }, 184 noRequest: true, 185 }, 186 { 187 name: "reset serial (force push style)", 188 mutationFunc: func(mgr *State) (*states.State, func()) { 189 mgr.serial = 2 190 return mgr.State(), func() {} 191 }, 192 expectedRequest: mockClientRequest{ 193 Method: "Put", 194 Content: map[string]interface{}{ 195 "version": 4.0, // encoding/json decodes this as float64 by default 196 "lineage": "mock-lineage", 197 "serial": 3.0, // encoding/json decodes this as float64 by default 198 "terraform_version": version.Version, 199 "outputs": map[string]interface{}{ 200 "foo": map[string]interface{}{ 201 "type": "string", 202 "value": "baz", 203 }, 204 }, 205 "resources": []interface{}{}, 206 }, 207 }, 208 }, 209 } 210 211 // Initial setup of state just to give us a fixed starting point for our 212 // test assertions below, or else we'd need to deal with 213 // random lineage. 214 mgr := &State{ 215 Client: &mockClient{ 216 current: []byte(` 217 { 218 "version": 4, 219 "lineage": "mock-lineage", 220 "serial": 1, 221 "terraform_version":"0.0.0", 222 "outputs": {}, 223 "resources": [] 224 } 225 `), 226 }, 227 } 228 229 // In normal use (during a Terraform operation) we always refresh and read 230 // before any writes would happen, so we'll mimic that here for realism. 231 // NB This causes a GET to be logged so the first item in the test cases 232 // must account for this 233 if err := mgr.RefreshState(); err != nil { 234 t.Fatalf("failed to RefreshState: %s", err) 235 } 236 237 // Our client is a mockClient which has a log we 238 // use to check that operations generate expected requests 239 mockClient := mgr.Client.(*mockClient) 240 241 // logIdx tracks the current index of the log separate from 242 // the loop iteration so we can check operations that don't 243 // cause any requests to be generated 244 logIdx := 0 245 246 // Run tests in order. 247 for _, tc := range testCases { 248 s, cleanup := tc.mutationFunc(mgr) 249 250 if err := mgr.WriteState(s); err != nil { 251 t.Fatalf("failed to WriteState for %q: %s", tc.name, err) 252 } 253 if err := mgr.PersistState(); err != nil { 254 t.Fatalf("failed to PersistState for %q: %s", tc.name, err) 255 } 256 257 if tc.isRequested(t) { 258 // Get captured request from the mock client log 259 // based on the index of the current test 260 if logIdx >= len(mockClient.log) { 261 t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) 262 } 263 loggedRequest := mockClient.log[logIdx] 264 logIdx++ 265 if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { 266 t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) 267 } 268 } 269 cleanup() 270 } 271 logCnt := len(mockClient.log) 272 if logIdx != logCnt { 273 log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) 274 } 275 } 276 277 type migrationTestCase struct { 278 name string 279 // A function to generate a statefile 280 stateFile func(*State) *statefile.File 281 // The expected request to have taken place 282 expectedRequest mockClientRequest 283 // Mark this case as not having a request 284 expectedError string 285 // force flag passed to client 286 force bool 287 } 288 289 func TestWriteStateForMigration(t *testing.T) { 290 mgr := &State{ 291 Client: &mockClient{ 292 current: []byte(` 293 { 294 "version": 4, 295 "lineage": "mock-lineage", 296 "serial": 3, 297 "terraform_version":"0.0.0", 298 "outputs": {"foo": {"value":"bar", "type": "string"}}, 299 "resources": [] 300 } 301 `), 302 }, 303 } 304 305 testCases := []migrationTestCase{ 306 // Refreshing state before we run the test loop causes a GET 307 { 308 name: "refresh state", 309 stateFile: func(mgr *State) *statefile.File { 310 return mgr.StateForMigration() 311 }, 312 expectedRequest: mockClientRequest{ 313 Method: "Get", 314 Content: map[string]interface{}{ 315 "version": 4.0, 316 "lineage": "mock-lineage", 317 "serial": 3.0, 318 "terraform_version": "0.0.0", 319 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 320 "resources": []interface{}{}, 321 }, 322 }, 323 }, 324 { 325 name: "cannot import lesser serial without force", 326 stateFile: func(mgr *State) *statefile.File { 327 return statefile.New(mgr.state, mgr.lineage, 1) 328 }, 329 expectedError: "cannot import state with serial 1 over newer state with serial 3", 330 }, 331 { 332 name: "cannot import differing lineage without force", 333 stateFile: func(mgr *State) *statefile.File { 334 return statefile.New(mgr.state, "different-lineage", mgr.serial) 335 }, 336 expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`, 337 }, 338 { 339 name: "can import lesser serial with force", 340 stateFile: func(mgr *State) *statefile.File { 341 return statefile.New(mgr.state, mgr.lineage, 1) 342 }, 343 expectedRequest: mockClientRequest{ 344 Method: "Force Put", 345 Content: map[string]interface{}{ 346 "version": 4.0, 347 "lineage": "mock-lineage", 348 "serial": 2.0, 349 "terraform_version": version.Version, 350 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 351 "resources": []interface{}{}, 352 }, 353 }, 354 force: true, 355 }, 356 { 357 name: "cannot import differing lineage without force", 358 stateFile: func(mgr *State) *statefile.File { 359 return statefile.New(mgr.state, "different-lineage", mgr.serial) 360 }, 361 expectedRequest: mockClientRequest{ 362 Method: "Force Put", 363 Content: map[string]interface{}{ 364 "version": 4.0, 365 "lineage": "different-lineage", 366 "serial": 3.0, 367 "terraform_version": version.Version, 368 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 369 "resources": []interface{}{}, 370 }, 371 }, 372 force: true, 373 }, 374 } 375 376 // In normal use (during a Terraform operation) we always refresh and read 377 // before any writes would happen, so we'll mimic that here for realism. 378 // NB This causes a GET to be logged so the first item in the test cases 379 // must account for this 380 if err := mgr.RefreshState(); err != nil { 381 t.Fatalf("failed to RefreshState: %s", err) 382 } 383 384 if err := mgr.WriteState(mgr.State()); err != nil { 385 t.Fatalf("failed to write initial state: %s", err) 386 } 387 388 // Our client is a mockClient which has a log we 389 // use to check that operations generate expected requests 390 mockClient := mgr.Client.(*mockClient) 391 392 if mockClient.force { 393 t.Fatalf("client should not default to force") 394 } 395 396 // logIdx tracks the current index of the log separate from 397 // the loop iteration so we can check operations that don't 398 // cause any requests to be generated 399 logIdx := 0 400 401 for _, tc := range testCases { 402 // Always reset client to not be force pushing 403 mockClient.force = false 404 sf := tc.stateFile(mgr) 405 err := mgr.WriteStateForMigration(sf, tc.force) 406 shouldError := tc.expectedError != "" 407 408 // If we are expecting and error check it and move on 409 if shouldError { 410 if err == nil { 411 t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError) 412 } else if err.Error() != tc.expectedError { 413 t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err) 414 } 415 continue 416 } 417 418 if err != nil { 419 t.Fatalf("test case %q failed: %v", tc.name, err) 420 } 421 422 if tc.force && !mockClient.force { 423 t.Fatalf("test case %q should have enabled force push", tc.name) 424 } 425 426 // At this point we should just do a normal write and persist 427 // as would happen from the CLI 428 mgr.WriteState(mgr.State()) 429 mgr.PersistState() 430 431 if logIdx >= len(mockClient.log) { 432 t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) 433 } 434 loggedRequest := mockClient.log[logIdx] 435 logIdx++ 436 if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { 437 t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) 438 } 439 } 440 441 logCnt := len(mockClient.log) 442 if logIdx != logCnt { 443 log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) 444 } 445 }