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