github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/consul/client_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package consul 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "math/rand" 12 "net" 13 "reflect" 14 "strings" 15 "sync" 16 "testing" 17 "time" 18 19 "github.com/terramate-io/tf/backend" 20 "github.com/terramate-io/tf/states/remote" 21 "github.com/terramate-io/tf/states/statemgr" 22 ) 23 24 func TestRemoteClient_impl(t *testing.T) { 25 var _ remote.Client = new(RemoteClient) 26 var _ remote.ClientLocker = new(RemoteClient) 27 } 28 29 func TestRemoteClient(t *testing.T) { 30 srv := newConsulTestServer(t) 31 32 testCases := []string{ 33 fmt.Sprintf("tf-unit/%s", time.Now().String()), 34 fmt.Sprintf("tf-unit/%s/", time.Now().String()), 35 } 36 37 for _, path := range testCases { 38 t.Run(path, func(*testing.T) { 39 // Get the backend 40 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 41 "address": srv.HTTPAddr, 42 "path": path, 43 })) 44 45 // Grab the client 46 state, err := b.StateMgr(backend.DefaultStateName) 47 if err != nil { 48 t.Fatalf("err: %s", err) 49 } 50 51 // Test 52 remote.TestClient(t, state.(*remote.State).Client) 53 }) 54 } 55 } 56 57 // test the gzip functionality of the client 58 func TestRemoteClient_gzipUpgrade(t *testing.T) { 59 srv := newConsulTestServer(t) 60 61 statePath := fmt.Sprintf("tf-unit/%s", time.Now().String()) 62 63 // Get the backend 64 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 65 "address": srv.HTTPAddr, 66 "path": statePath, 67 })) 68 69 // Grab the client 70 state, err := b.StateMgr(backend.DefaultStateName) 71 if err != nil { 72 t.Fatalf("err: %s", err) 73 } 74 75 // Test 76 remote.TestClient(t, state.(*remote.State).Client) 77 78 // create a new backend with gzip 79 b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 80 "address": srv.HTTPAddr, 81 "path": statePath, 82 "gzip": true, 83 })) 84 85 // Grab the client 86 state, err = b.StateMgr(backend.DefaultStateName) 87 if err != nil { 88 t.Fatalf("err: %s", err) 89 } 90 91 // Test 92 remote.TestClient(t, state.(*remote.State).Client) 93 } 94 95 // TestConsul_largeState tries to write a large payload using the Consul state 96 // manager, as there is a limit to the size of the values in the KV store it 97 // will need to be split up before being saved and put back together when read. 98 func TestConsul_largeState(t *testing.T) { 99 srv := newConsulTestServer(t) 100 101 path := "tf-unit/test-large-state" 102 103 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 104 "address": srv.HTTPAddr, 105 "path": path, 106 })) 107 108 s, err := b.StateMgr(backend.DefaultStateName) 109 if err != nil { 110 t.Fatal(err) 111 } 112 113 c := s.(*remote.State).Client.(*RemoteClient) 114 c.Path = path 115 116 // testPaths fails the test if the keys found at the prefix don't match 117 // what is expected 118 testPaths := func(t *testing.T, expected []string) { 119 kv := c.Client.KV() 120 pairs, _, err := kv.List(c.Path, nil) 121 if err != nil { 122 t.Fatal(err) 123 } 124 res := make([]string, 0) 125 for _, p := range pairs { 126 res = append(res, p.Key) 127 } 128 if !reflect.DeepEqual(res, expected) { 129 t.Fatalf("Wrong keys: %#v", res) 130 } 131 } 132 133 testPayload := func(t *testing.T, data map[string]string, keys []string) { 134 payload, err := json.Marshal(data) 135 if err != nil { 136 t.Fatal(err) 137 } 138 err = c.Put(payload) 139 if err != nil { 140 t.Fatal("could not put payload", err) 141 } 142 143 remote, err := c.Get() 144 if err != nil { 145 t.Fatal(err) 146 } 147 148 if !bytes.Equal(payload, remote.Data) { 149 t.Fatal("the data do not match") 150 } 151 152 testPaths(t, keys) 153 } 154 155 // The default limit for the size of the value in Consul is 524288 bytes 156 testPayload( 157 t, 158 map[string]string{ 159 "foo": strings.Repeat("a", 524288+2), 160 }, 161 []string{ 162 "tf-unit/test-large-state", 163 "tf-unit/test-large-state/tfstate.2cb96f52c9fff8e0b56cb786ec4d2bed/0", 164 "tf-unit/test-large-state/tfstate.2cb96f52c9fff8e0b56cb786ec4d2bed/1", 165 }, 166 ) 167 168 // This payload is just short enough to be stored but will be bigger when 169 // going through the Transaction API as it will be base64 encoded 170 testPayload( 171 t, 172 map[string]string{ 173 "foo": strings.Repeat("a", 524288-10), 174 }, 175 []string{ 176 "tf-unit/test-large-state", 177 "tf-unit/test-large-state/tfstate.4f407ace136a86521fd0d366972fe5c7/0", 178 }, 179 ) 180 181 // We try to replace the payload with a small one, the old chunks should be removed 182 testPayload( 183 t, 184 map[string]string{"var": "a"}, 185 []string{"tf-unit/test-large-state"}, 186 ) 187 188 // Test with gzip and chunks 189 b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 190 "address": srv.HTTPAddr, 191 "path": path, 192 "gzip": true, 193 })) 194 195 s, err = b.StateMgr(backend.DefaultStateName) 196 if err != nil { 197 t.Fatal(err) 198 } 199 200 c = s.(*remote.State).Client.(*RemoteClient) 201 c.Path = path 202 203 // We need a long random string so it results in multiple chunks even after 204 // being gziped 205 206 // We use a fixed seed so the test can be reproductible 207 rand.Seed(1234) 208 RandStringRunes := func(n int) string { 209 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 210 b := make([]rune, n) 211 for i := range b { 212 b[i] = letterRunes[rand.Intn(len(letterRunes))] 213 } 214 return string(b) 215 } 216 217 testPayload( 218 t, 219 map[string]string{ 220 "bar": RandStringRunes(5 * (524288 + 2)), 221 }, 222 []string{ 223 "tf-unit/test-large-state", 224 "tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/0", 225 "tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/1", 226 "tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/2", 227 "tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/3", 228 }, 229 ) 230 231 // Deleting the state should remove all chunks 232 err = c.Delete() 233 if err != nil { 234 t.Fatal(err) 235 } 236 testPaths(t, []string{}) 237 } 238 239 func TestConsul_stateLock(t *testing.T) { 240 srv := newConsulTestServer(t) 241 242 testCases := []string{ 243 fmt.Sprintf("tf-unit/%s", time.Now().String()), 244 fmt.Sprintf("tf-unit/%s/", time.Now().String()), 245 } 246 247 for _, path := range testCases { 248 t.Run(path, func(*testing.T) { 249 // create 2 instances to get 2 remote.Clients 250 sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 251 "address": srv.HTTPAddr, 252 "path": path, 253 })).StateMgr(backend.DefaultStateName) 254 if err != nil { 255 t.Fatal(err) 256 } 257 258 sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 259 "address": srv.HTTPAddr, 260 "path": path, 261 })).StateMgr(backend.DefaultStateName) 262 if err != nil { 263 t.Fatal(err) 264 } 265 266 remote.TestRemoteLocks(t, sA.(*remote.State).Client, sB.(*remote.State).Client) 267 }) 268 } 269 } 270 271 func TestConsul_destroyLock(t *testing.T) { 272 srv := newConsulTestServer(t) 273 274 testCases := []string{ 275 fmt.Sprintf("tf-unit/%s", time.Now().String()), 276 fmt.Sprintf("tf-unit/%s/", time.Now().String()), 277 } 278 279 testLock := func(client *RemoteClient, lockPath string) { 280 // get the lock val 281 pair, _, err := client.Client.KV().Get(lockPath, nil) 282 if err != nil { 283 t.Fatal(err) 284 } 285 if pair != nil { 286 t.Fatalf("lock key not cleaned up at: %s", pair.Key) 287 } 288 } 289 290 for _, path := range testCases { 291 t.Run(path, func(*testing.T) { 292 // Get the backend 293 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 294 "address": srv.HTTPAddr, 295 "path": path, 296 })) 297 298 // Grab the client 299 s, err := b.StateMgr(backend.DefaultStateName) 300 if err != nil { 301 t.Fatalf("err: %s", err) 302 } 303 304 clientA := s.(*remote.State).Client.(*RemoteClient) 305 306 info := statemgr.NewLockInfo() 307 id, err := clientA.Lock(info) 308 if err != nil { 309 t.Fatal(err) 310 } 311 312 lockPath := clientA.Path + lockSuffix 313 314 if err := clientA.Unlock(id); err != nil { 315 t.Fatal(err) 316 } 317 318 testLock(clientA, lockPath) 319 320 // The release the lock from a second client to test the 321 // `terraform force-unlock <lock_id>` functionnality 322 s, err = b.StateMgr(backend.DefaultStateName) 323 if err != nil { 324 t.Fatalf("err: %s", err) 325 } 326 327 clientB := s.(*remote.State).Client.(*RemoteClient) 328 329 info = statemgr.NewLockInfo() 330 id, err = clientA.Lock(info) 331 if err != nil { 332 t.Fatal(err) 333 } 334 335 if err := clientB.Unlock(id); err != nil { 336 t.Fatal(err) 337 } 338 339 testLock(clientA, lockPath) 340 341 err = clientA.Unlock(id) 342 343 if err == nil { 344 t.Fatal("consul lock should have been lost") 345 } 346 if err.Error() != "consul lock was lost" { 347 t.Fatal("got wrong error", err) 348 } 349 }) 350 } 351 } 352 353 func TestConsul_lostLock(t *testing.T) { 354 srv := newConsulTestServer(t) 355 356 path := fmt.Sprintf("tf-unit/%s", time.Now().String()) 357 358 // create 2 instances to get 2 remote.Clients 359 sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 360 "address": srv.HTTPAddr, 361 "path": path, 362 })).StateMgr(backend.DefaultStateName) 363 if err != nil { 364 t.Fatal(err) 365 } 366 367 sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 368 "address": srv.HTTPAddr, 369 "path": path + "-not-used", 370 })).StateMgr(backend.DefaultStateName) 371 if err != nil { 372 t.Fatal(err) 373 } 374 375 info := statemgr.NewLockInfo() 376 info.Operation = "test-lost-lock" 377 id, err := sA.Lock(info) 378 if err != nil { 379 t.Fatal(err) 380 } 381 382 reLocked := make(chan struct{}) 383 testLockHook = func() { 384 close(reLocked) 385 testLockHook = nil 386 } 387 388 // now we use the second client to break the lock 389 kv := sB.(*remote.State).Client.(*RemoteClient).Client.KV() 390 _, err = kv.Delete(path+lockSuffix, nil) 391 if err != nil { 392 t.Fatal(err) 393 } 394 395 <-reLocked 396 397 if err := sA.Unlock(id); err != nil { 398 t.Fatal(err) 399 } 400 } 401 402 func TestConsul_lostLockConnection(t *testing.T) { 403 srv := newConsulTestServer(t) 404 405 // create an "unreliable" network by closing all the consul client's 406 // network connections 407 conns := &unreliableConns{} 408 origDialFn := dialContext 409 defer func() { 410 dialContext = origDialFn 411 }() 412 dialContext = conns.DialContext 413 414 path := fmt.Sprintf("tf-unit/%s", time.Now().String()) 415 416 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 417 "address": srv.HTTPAddr, 418 "path": path, 419 })) 420 421 s, err := b.StateMgr(backend.DefaultStateName) 422 if err != nil { 423 t.Fatal(err) 424 } 425 426 info := statemgr.NewLockInfo() 427 info.Operation = "test-lost-lock-connection" 428 id, err := s.Lock(info) 429 if err != nil { 430 t.Fatal(err) 431 } 432 433 // kill the connection a few times 434 for i := 0; i < 3; i++ { 435 dialed := conns.dialedDone() 436 // kill any open connections 437 conns.Kill() 438 // wait for a new connection to be dialed, and kill it again 439 <-dialed 440 } 441 442 if err := s.Unlock(id); err != nil { 443 t.Fatal("unlock error:", err) 444 } 445 } 446 447 type unreliableConns struct { 448 sync.Mutex 449 conns []net.Conn 450 dialCallback func() 451 } 452 453 func (u *unreliableConns) DialContext(ctx context.Context, netw, addr string) (net.Conn, error) { 454 u.Lock() 455 defer u.Unlock() 456 457 dialer := &net.Dialer{} 458 conn, err := dialer.DialContext(ctx, netw, addr) 459 if err != nil { 460 return nil, err 461 } 462 463 u.conns = append(u.conns, conn) 464 465 if u.dialCallback != nil { 466 u.dialCallback() 467 } 468 469 return conn, nil 470 } 471 472 func (u *unreliableConns) dialedDone() chan struct{} { 473 u.Lock() 474 defer u.Unlock() 475 dialed := make(chan struct{}) 476 u.dialCallback = func() { 477 defer close(dialed) 478 u.dialCallback = nil 479 } 480 481 return dialed 482 } 483 484 // Kill these with a deadline, just to make sure we don't end up with any EOFs 485 // that get ignored. 486 func (u *unreliableConns) Kill() { 487 u.Lock() 488 defer u.Unlock() 489 490 for _, conn := range u.conns { 491 conn.(*net.TCPConn).SetDeadline(time.Now()) 492 } 493 u.conns = nil 494 }