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