github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/oss/client_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package oss 5 6 import ( 7 "fmt" 8 "strings" 9 "testing" 10 "time" 11 12 "bytes" 13 "crypto/md5" 14 15 "github.com/terramate-io/tf/backend" 16 "github.com/terramate-io/tf/states/remote" 17 "github.com/terramate-io/tf/states/statefile" 18 "github.com/terramate-io/tf/states/statemgr" 19 ) 20 21 // NOTE: Before running this testcase, please create a OTS instance called 'tf-oss-remote' 22 var RemoteTestUsedOTSEndpoint = "https://tf-oss-remote.cn-hangzhou.ots.aliyuncs.com" 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 testACC(t) 31 bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix()) 32 path := "testState" 33 34 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 35 "bucket": bucketName, 36 "prefix": path, 37 "encrypt": true, 38 })).(*Backend) 39 40 createOSSBucket(t, b.ossClient, bucketName) 41 defer deleteOSSBucket(t, b.ossClient, bucketName) 42 43 state, err := b.StateMgr(backend.DefaultStateName) 44 if err != nil { 45 t.Fatal(err) 46 } 47 48 remote.TestClient(t, state.(*remote.State).Client) 49 } 50 51 func TestRemoteClientLocks(t *testing.T) { 52 testACC(t) 53 bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix()) 54 tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix()) 55 path := "testState" 56 57 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 58 "bucket": bucketName, 59 "prefix": path, 60 "encrypt": true, 61 "tablestore_table": tableName, 62 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 63 })).(*Backend) 64 65 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 66 "bucket": bucketName, 67 "prefix": path, 68 "encrypt": true, 69 "tablestore_table": tableName, 70 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 71 })).(*Backend) 72 73 createOSSBucket(t, b1.ossClient, bucketName) 74 defer deleteOSSBucket(t, b1.ossClient, bucketName) 75 createTablestoreTable(t, b1.otsClient, tableName) 76 defer deleteTablestoreTable(t, b1.otsClient, tableName) 77 78 s1, err := b1.StateMgr(backend.DefaultStateName) 79 if err != nil { 80 t.Fatal(err) 81 } 82 83 s2, err := b2.StateMgr(backend.DefaultStateName) 84 if err != nil { 85 t.Fatal(err) 86 } 87 88 remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client) 89 } 90 91 // verify that the backend can handle more than one state in the same table 92 func TestRemoteClientLocks_multipleStates(t *testing.T) { 93 testACC(t) 94 bucketName := fmt.Sprintf("tf-remote-oss-test-force-%x", time.Now().Unix()) 95 tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix()) 96 path := "testState" 97 98 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 99 "bucket": bucketName, 100 "prefix": path, 101 "encrypt": true, 102 "tablestore_table": tableName, 103 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 104 })).(*Backend) 105 106 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 107 "bucket": bucketName, 108 "prefix": path, 109 "encrypt": true, 110 "tablestore_table": tableName, 111 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 112 })).(*Backend) 113 114 createOSSBucket(t, b1.ossClient, bucketName) 115 defer deleteOSSBucket(t, b1.ossClient, bucketName) 116 createTablestoreTable(t, b1.otsClient, tableName) 117 defer deleteTablestoreTable(t, b1.otsClient, tableName) 118 119 s1, err := b1.StateMgr("s1") 120 if err != nil { 121 t.Fatal(err) 122 } 123 if _, err := s1.Lock(statemgr.NewLockInfo()); err != nil { 124 t.Fatal("failed to get lock for s1:", err) 125 } 126 127 // s1 is now locked, s2 should not be locked as it's a different state file 128 s2, err := b2.StateMgr("s2") 129 if err != nil { 130 t.Fatal(err) 131 } 132 if _, err := s2.Lock(statemgr.NewLockInfo()); err != nil { 133 t.Fatal("failed to get lock for s2:", err) 134 } 135 } 136 137 // verify that we can unlock a state with an existing lock 138 func TestRemoteForceUnlock(t *testing.T) { 139 testACC(t) 140 bucketName := fmt.Sprintf("tf-remote-oss-test-force-%x", time.Now().Unix()) 141 tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix()) 142 path := "testState" 143 144 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 145 "bucket": bucketName, 146 "prefix": path, 147 "encrypt": true, 148 "tablestore_table": tableName, 149 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 150 })).(*Backend) 151 152 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 153 "bucket": bucketName, 154 "prefix": path, 155 "encrypt": true, 156 "tablestore_table": tableName, 157 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 158 })).(*Backend) 159 160 createOSSBucket(t, b1.ossClient, bucketName) 161 defer deleteOSSBucket(t, b1.ossClient, bucketName) 162 createTablestoreTable(t, b1.otsClient, tableName) 163 defer deleteTablestoreTable(t, b1.otsClient, tableName) 164 165 // first test with default 166 s1, err := b1.StateMgr(backend.DefaultStateName) 167 if err != nil { 168 t.Fatal(err) 169 } 170 171 info := statemgr.NewLockInfo() 172 info.Operation = "test" 173 info.Who = "clientA" 174 175 lockID, err := s1.Lock(info) 176 if err != nil { 177 t.Fatal("unable to get initial lock:", err) 178 } 179 180 // s1 is now locked, get the same state through s2 and unlock it 181 s2, err := b2.StateMgr(backend.DefaultStateName) 182 if err != nil { 183 t.Fatal("failed to get default state to force unlock:", err) 184 } 185 186 if err := s2.Unlock(lockID); err != nil { 187 t.Fatal("failed to force-unlock default state") 188 } 189 190 // now try the same thing with a named state 191 // first test with default 192 s1, err = b1.StateMgr("test") 193 if err != nil { 194 t.Fatal(err) 195 } 196 197 info = statemgr.NewLockInfo() 198 info.Operation = "test" 199 info.Who = "clientA" 200 201 lockID, err = s1.Lock(info) 202 if err != nil { 203 t.Fatal("unable to get initial lock:", err) 204 } 205 206 // s1 is now locked, get the same state through s2 and unlock it 207 s2, err = b2.StateMgr("test") 208 if err != nil { 209 t.Fatal("failed to get named state to force unlock:", err) 210 } 211 212 if err = s2.Unlock(lockID); err != nil { 213 t.Fatal("failed to force-unlock named state") 214 } 215 } 216 217 func TestRemoteClient_clientMD5(t *testing.T) { 218 testACC(t) 219 220 bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix()) 221 tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix()) 222 path := "testState" 223 224 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 225 "bucket": bucketName, 226 "prefix": path, 227 "tablestore_table": tableName, 228 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 229 })).(*Backend) 230 231 createOSSBucket(t, b.ossClient, bucketName) 232 defer deleteOSSBucket(t, b.ossClient, bucketName) 233 createTablestoreTable(t, b.otsClient, tableName) 234 defer deleteTablestoreTable(t, b.otsClient, tableName) 235 236 s, err := b.StateMgr(backend.DefaultStateName) 237 if err != nil { 238 t.Fatal(err) 239 } 240 client := s.(*remote.State).Client.(*RemoteClient) 241 242 sum := md5.Sum([]byte("test")) 243 244 if err := client.putMD5(sum[:]); err != nil { 245 t.Fatal(err) 246 } 247 248 getSum, err := client.getMD5() 249 if err != nil { 250 t.Fatal(err) 251 } 252 253 if !bytes.Equal(getSum, sum[:]) { 254 t.Fatalf("getMD5 returned the wrong checksum: expected %x, got %x", sum[:], getSum) 255 } 256 257 if err := client.deleteMD5(); err != nil { 258 t.Fatal(err) 259 } 260 261 if getSum, err := client.getMD5(); err == nil { 262 t.Fatalf("expected getMD5 error, got none. checksum: %x", getSum) 263 } 264 } 265 266 // verify that a client won't return a state with an incorrect checksum. 267 func TestRemoteClient_stateChecksum(t *testing.T) { 268 testACC(t) 269 270 bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix()) 271 tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix()) 272 path := "testState" 273 274 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 275 "bucket": bucketName, 276 "prefix": path, 277 "tablestore_table": tableName, 278 "tablestore_endpoint": RemoteTestUsedOTSEndpoint, 279 })).(*Backend) 280 281 createOSSBucket(t, b1.ossClient, bucketName) 282 defer deleteOSSBucket(t, b1.ossClient, bucketName) 283 createTablestoreTable(t, b1.otsClient, tableName) 284 defer deleteTablestoreTable(t, b1.otsClient, tableName) 285 286 s1, err := b1.StateMgr(backend.DefaultStateName) 287 if err != nil { 288 t.Fatal(err) 289 } 290 client1 := s1.(*remote.State).Client 291 292 // create an old and new state version to persist 293 s := statemgr.TestFullInitialState() 294 sf := &statefile.File{State: s} 295 var oldState bytes.Buffer 296 if err := statefile.Write(sf, &oldState); err != nil { 297 t.Fatal(err) 298 } 299 sf.Serial++ 300 var newState bytes.Buffer 301 if err := statefile.Write(sf, &newState); err != nil { 302 t.Fatal(err) 303 } 304 305 // Use b2 without a tablestore_table to bypass the lock table to write the state directly. 306 // client2 will write the "incorrect" state, simulating oss eventually consistency delays 307 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 308 "bucket": bucketName, 309 "prefix": path, 310 })).(*Backend) 311 s2, err := b2.StateMgr(backend.DefaultStateName) 312 if err != nil { 313 t.Fatal(err) 314 } 315 client2 := s2.(*remote.State).Client 316 317 // write the new state through client2 so that there is no checksum yet 318 if err := client2.Put(newState.Bytes()); err != nil { 319 t.Fatal(err) 320 } 321 322 // verify that we can pull a state without a checksum 323 if _, err := client1.Get(); err != nil { 324 t.Fatal(err) 325 } 326 327 // write the new state back with its checksum 328 if err := client1.Put(newState.Bytes()); err != nil { 329 t.Fatal(err) 330 } 331 332 // put an empty state in place to check for panics during get 333 if err := client2.Put([]byte{}); err != nil { 334 t.Fatal(err) 335 } 336 337 // remove the timeouts so we can fail immediately 338 origTimeout := consistencyRetryTimeout 339 origInterval := consistencyRetryPollInterval 340 defer func() { 341 consistencyRetryTimeout = origTimeout 342 consistencyRetryPollInterval = origInterval 343 }() 344 consistencyRetryTimeout = 0 345 consistencyRetryPollInterval = 0 346 347 // fetching an empty state through client1 should now error out due to a 348 // mismatched checksum. 349 if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) { 350 t.Fatalf("expected state checksum error: got %s", err) 351 } 352 353 // put the old state in place of the new, without updating the checksum 354 if err := client2.Put(oldState.Bytes()); err != nil { 355 t.Fatal(err) 356 } 357 358 // fetching the wrong state through client1 should now error out due to a 359 // mismatched checksum. 360 if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) { 361 t.Fatalf("expected state checksum error: got %s", err) 362 } 363 364 // update the state with the correct one after we Get again 365 testChecksumHook = func() { 366 if err := client2.Put(newState.Bytes()); err != nil { 367 t.Fatal(err) 368 } 369 testChecksumHook = nil 370 } 371 372 consistencyRetryTimeout = origTimeout 373 374 // this final Get will fail to fail the checksum verification, the above 375 // callback will update the state with the correct version, and Get should 376 // retry automatically. 377 if _, err := client1.Get(); err != nil { 378 t.Fatal(err) 379 } 380 }