github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/gcs/backend_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 gcs 7 8 import ( 9 "context" 10 "encoding/json" 11 "fmt" 12 "log" 13 "os" 14 "strings" 15 "testing" 16 "time" 17 18 kms "cloud.google.com/go/kms/apiv1" 19 "cloud.google.com/go/storage" 20 "github.com/opentofu/opentofu/internal/backend" 21 "github.com/opentofu/opentofu/internal/encryption" 22 "github.com/opentofu/opentofu/internal/httpclient" 23 "github.com/opentofu/opentofu/internal/states/remote" 24 "github.com/opentofu/opentofu/version" 25 "google.golang.org/api/option" 26 kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" 27 ) 28 29 const ( 30 noPrefix = "" 31 noEncryptionKey = "" 32 noKmsKeyName = "" 33 ) 34 35 // See https://cloud.google.com/storage/docs/using-encryption-keys#generating_your_own_encryption_key 36 const encryptionKey = "yRyCOikXi1ZDNE0xN3yiFsJjg7LGimoLrGFcLZgQoVk=" 37 38 // KMS key ring name and key name are hardcoded here and re-used because key rings (and keys) cannot be deleted 39 // Test code asserts their presence and creates them if they're absent. They're not deleted at the end of tests. 40 // See: https://cloud.google.com/kms/docs/faq#cannot_delete 41 const ( 42 keyRingName = "tf-gcs-backend-acc-tests" 43 keyName = "tf-test-key-1" 44 kmsRole = "roles/cloudkms.cryptoKeyEncrypterDecrypter" // GCS service account needs this binding on the created key 45 ) 46 47 var keyRingLocation = os.Getenv("GOOGLE_REGION") 48 49 func TestStateFile(t *testing.T) { 50 t.Parallel() 51 52 cases := []struct { 53 prefix string 54 name string 55 wantStateFile string 56 wantLockFile string 57 }{ 58 {"state", "default", "state/default.tfstate", "state/default.tflock"}, 59 {"state", "test", "state/test.tfstate", "state/test.tflock"}, 60 {"state", "test", "state/test.tfstate", "state/test.tflock"}, 61 {"state", "test", "state/test.tfstate", "state/test.tflock"}, 62 } 63 for _, c := range cases { 64 b := &Backend{ 65 prefix: c.prefix, 66 } 67 68 if got := b.stateFile(c.name); got != c.wantStateFile { 69 t.Errorf("stateFile(%q) = %q, want %q", c.name, got, c.wantStateFile) 70 } 71 72 if got := b.lockFile(c.name); got != c.wantLockFile { 73 t.Errorf("lockFile(%q) = %q, want %q", c.name, got, c.wantLockFile) 74 } 75 } 76 } 77 78 func TestRemoteClient(t *testing.T) { 79 t.Parallel() 80 81 bucket := bucketName(t) 82 be := setupBackend(t, bucket, noPrefix, noEncryptionKey, noKmsKeyName) 83 defer teardownBackend(t, be, noPrefix) 84 85 ss, err := be.StateMgr(backend.DefaultStateName) 86 if err != nil { 87 t.Fatalf("be.StateMgr(%q) = %v", backend.DefaultStateName, err) 88 } 89 90 rs, ok := ss.(*remote.State) 91 if !ok { 92 t.Fatalf("be.StateMgr(): got a %T, want a *remote.State", ss) 93 } 94 95 remote.TestClient(t, rs.Client) 96 } 97 func TestRemoteClientWithEncryption(t *testing.T) { 98 t.Parallel() 99 100 bucket := bucketName(t) 101 be := setupBackend(t, bucket, noPrefix, encryptionKey, noKmsKeyName) 102 defer teardownBackend(t, be, noPrefix) 103 104 ss, err := be.StateMgr(backend.DefaultStateName) 105 if err != nil { 106 t.Fatalf("be.StateMgr(%q) = %v", backend.DefaultStateName, err) 107 } 108 109 rs, ok := ss.(*remote.State) 110 if !ok { 111 t.Fatalf("be.StateMgr(): got a %T, want a *remote.State", ss) 112 } 113 114 remote.TestClient(t, rs.Client) 115 } 116 117 func TestRemoteLocks(t *testing.T) { 118 t.Parallel() 119 120 bucket := bucketName(t) 121 be := setupBackend(t, bucket, noPrefix, noEncryptionKey, noKmsKeyName) 122 defer teardownBackend(t, be, noPrefix) 123 124 remoteClient := func() (remote.Client, error) { 125 ss, err := be.StateMgr(backend.DefaultStateName) 126 if err != nil { 127 return nil, err 128 } 129 130 rs, ok := ss.(*remote.State) 131 if !ok { 132 return nil, fmt.Errorf("be.StateMgr(): got a %T, want a *remote.State", ss) 133 } 134 135 return rs.Client, nil 136 } 137 138 c0, err := remoteClient() 139 if err != nil { 140 t.Fatalf("remoteClient(0) = %v", err) 141 } 142 c1, err := remoteClient() 143 if err != nil { 144 t.Fatalf("remoteClient(1) = %v", err) 145 } 146 147 remote.TestRemoteLocks(t, c0, c1) 148 } 149 150 func TestBackend(t *testing.T) { 151 t.Parallel() 152 153 bucket := bucketName(t) 154 155 be0 := setupBackend(t, bucket, noPrefix, noEncryptionKey, noKmsKeyName) 156 defer teardownBackend(t, be0, noPrefix) 157 158 be1 := setupBackend(t, bucket, noPrefix, noEncryptionKey, noKmsKeyName) 159 160 backend.TestBackendStates(t, be0) 161 backend.TestBackendStateLocks(t, be0, be1) 162 backend.TestBackendStateForceUnlock(t, be0, be1) 163 } 164 165 func TestBackendWithPrefix(t *testing.T) { 166 t.Parallel() 167 168 prefix := "test/prefix" 169 bucket := bucketName(t) 170 171 be0 := setupBackend(t, bucket, prefix, noEncryptionKey, noKmsKeyName) 172 defer teardownBackend(t, be0, prefix) 173 174 be1 := setupBackend(t, bucket, prefix+"/", noEncryptionKey, noKmsKeyName) 175 176 backend.TestBackendStates(t, be0) 177 backend.TestBackendStateLocks(t, be0, be1) 178 } 179 func TestBackendWithCustomerSuppliedEncryption(t *testing.T) { 180 t.Parallel() 181 182 bucket := bucketName(t) 183 184 be0 := setupBackend(t, bucket, noPrefix, encryptionKey, noKmsKeyName) 185 defer teardownBackend(t, be0, noPrefix) 186 187 be1 := setupBackend(t, bucket, noPrefix, encryptionKey, noKmsKeyName) 188 189 backend.TestBackendStates(t, be0) 190 backend.TestBackendStateLocks(t, be0, be1) 191 } 192 193 func TestBackendWithCustomerManagedKMSEncryption(t *testing.T) { 194 t.Parallel() 195 196 projectID := os.Getenv("GOOGLE_PROJECT") 197 bucket := bucketName(t) 198 199 // Taken from global variables in test file 200 kmsDetails := map[string]string{ 201 "project": projectID, 202 "location": keyRingLocation, 203 "ringName": keyRingName, 204 "keyName": keyName, 205 } 206 207 kmsName := setupKmsKey(t, kmsDetails) 208 209 be0 := setupBackend(t, bucket, noPrefix, noEncryptionKey, kmsName) 210 defer teardownBackend(t, be0, noPrefix) 211 212 be1 := setupBackend(t, bucket, noPrefix, noEncryptionKey, kmsName) 213 214 backend.TestBackendStates(t, be0) 215 backend.TestBackendStateLocks(t, be0, be1) 216 } 217 218 // setupBackend returns a new GCS backend. 219 func setupBackend(t *testing.T, bucket, prefix, key, kmsName string) backend.Backend { 220 t.Helper() 221 222 projectID := os.Getenv("GOOGLE_PROJECT") 223 if projectID == "" || os.Getenv("TF_ACC") == "" { 224 t.Skip("This test creates a bucket in GCS and populates it. " + 225 "Since this may incur costs, it will only run if " + 226 "the TF_ACC and GOOGLE_PROJECT environment variables are set.") 227 } 228 229 config := map[string]interface{}{ 230 "bucket": bucket, 231 "prefix": prefix, 232 } 233 // Only add encryption keys to config if non-zero value set 234 // If not set here, default values are supplied in `TestBackendConfig` by `PrepareConfig` function call 235 if len(key) > 0 { 236 config["encryption_key"] = key 237 } 238 if len(kmsName) > 0 { 239 config["kms_encryption_key"] = kmsName 240 } 241 242 b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)) 243 be := b.(*Backend) 244 245 // create the bucket if it doesn't exist 246 bkt := be.storageClient.Bucket(bucket) 247 _, err := bkt.Attrs(be.storageContext) 248 if err != nil { 249 if err != storage.ErrBucketNotExist { 250 t.Fatal(err) 251 } 252 253 attrs := &storage.BucketAttrs{ 254 Location: os.Getenv("GOOGLE_REGION"), 255 } 256 err := bkt.Create(be.storageContext, projectID, attrs) 257 if err != nil { 258 t.Fatal(err) 259 } 260 } 261 262 return b 263 } 264 265 // setupKmsKey asserts that a KMS key chain and key exist and necessary IAM bindings are in place 266 // If the key ring or key do not exist they are created and permissions are given to the GCS Service account 267 func setupKmsKey(t *testing.T, keyDetails map[string]string) string { 268 t.Helper() 269 270 projectID := os.Getenv("GOOGLE_PROJECT") 271 if projectID == "" || os.Getenv("TF_ACC") == "" { 272 t.Skip("This test creates a KMS key ring and key in Cloud KMS. " + 273 "Since this may incur costs, it will only run if " + 274 "the TF_ACC and GOOGLE_PROJECT environment variables are set.") 275 } 276 277 // KMS Client 278 ctx := context.Background() 279 opts, err := testGetClientOptions(t) 280 if err != nil { 281 e := fmt.Errorf("testGetClientOptions() failed: %w", err) 282 t.Fatal(e) 283 } 284 c, err := kms.NewKeyManagementClient(ctx, opts...) 285 if err != nil { 286 e := fmt.Errorf("kms.NewKeyManagementClient() failed: %w", err) 287 t.Fatal(e) 288 } 289 defer c.Close() 290 291 // Get KMS key ring, create if doesn't exist 292 reqGetKeyRing := &kmspb.GetKeyRingRequest{ 293 Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", keyDetails["project"], keyDetails["location"], keyDetails["ringName"]), 294 } 295 var keyRing *kmspb.KeyRing 296 keyRing, err = c.GetKeyRing(ctx, reqGetKeyRing) 297 if err != nil { 298 if !strings.Contains(err.Error(), "NotFound") { 299 // Handle unexpected error that isn't related to the key ring not being made yet 300 t.Fatal(err) 301 } 302 // Create key ring that doesn't exist 303 t.Logf("Cloud KMS key ring `%s` not found: creating key ring", 304 fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", keyDetails["project"], keyDetails["location"], keyDetails["ringName"]), 305 ) 306 reqCreateKeyRing := &kmspb.CreateKeyRingRequest{ 307 Parent: fmt.Sprintf("projects/%s/locations/%s", keyDetails["project"], keyDetails["location"]), 308 KeyRingId: keyDetails["ringName"], 309 } 310 keyRing, err = c.CreateKeyRing(ctx, reqCreateKeyRing) 311 if err != nil { 312 t.Fatal(err) 313 } 314 t.Logf("Cloud KMS key ring `%s` created successfully", keyRing.Name) 315 } 316 317 // Get KMS key, create if doesn't exist (and give GCS service account permission to use) 318 reqGetKey := &kmspb.GetCryptoKeyRequest{ 319 Name: fmt.Sprintf("%s/cryptoKeys/%s", keyRing.Name, keyDetails["keyName"]), 320 } 321 var key *kmspb.CryptoKey 322 key, err = c.GetCryptoKey(ctx, reqGetKey) 323 if err != nil { 324 if !strings.Contains(err.Error(), "NotFound") { 325 // Handle unexpected error that isn't related to the key not being made yet 326 t.Fatal(err) 327 } 328 // Create key that doesn't exist 329 t.Logf("Cloud KMS key `%s` not found: creating key", 330 fmt.Sprintf("%s/cryptoKeys/%s", keyRing.Name, keyDetails["keyName"]), 331 ) 332 reqCreateKey := &kmspb.CreateCryptoKeyRequest{ 333 Parent: keyRing.Name, 334 CryptoKeyId: keyDetails["keyName"], 335 CryptoKey: &kmspb.CryptoKey{ 336 Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, 337 }, 338 } 339 key, err = c.CreateCryptoKey(ctx, reqCreateKey) 340 if err != nil { 341 t.Fatal(err) 342 } 343 t.Logf("Cloud KMS key `%s` created successfully", key.Name) 344 } 345 346 // Get GCS Service account email, check has necessary permission on key 347 // Note: we cannot reuse the backend's storage client (like in the setupBackend function) 348 // because the KMS key needs to exist before the backend buckets are made in the test. 349 sc, err := storage.NewClient(ctx, opts...) //reuse opts from KMS client 350 if err != nil { 351 e := fmt.Errorf("storage.NewClient() failed: %w", err) 352 t.Fatal(e) 353 } 354 defer sc.Close() 355 gcsServiceAccount, err := sc.ServiceAccount(ctx, keyDetails["project"]) 356 if err != nil { 357 t.Fatal(err) 358 } 359 360 // Assert Cloud Storage service account has permission to use this key. 361 member := fmt.Sprintf("serviceAccount:%s", gcsServiceAccount) 362 iamHandle := c.ResourceIAM(key.Name) 363 policy, err := iamHandle.Policy(ctx) 364 if err != nil { 365 t.Fatal(err) 366 } 367 if ok := policy.HasRole(member, kmsRole); !ok { 368 // Add the missing permissions 369 t.Logf("Granting GCS service account %s %s role on key %s", gcsServiceAccount, kmsRole, key.Name) 370 policy.Add(member, kmsRole) 371 err = iamHandle.SetPolicy(ctx, policy) 372 if err != nil { 373 t.Fatal(err) 374 } 375 } 376 return key.Name 377 } 378 379 // teardownBackend deletes all states from be except the default state. 380 func teardownBackend(t *testing.T, be backend.Backend, prefix string) { 381 t.Helper() 382 gcsBE, ok := be.(*Backend) 383 if !ok { 384 t.Fatalf("be is a %T, want a *gcsBackend", be) 385 } 386 ctx := gcsBE.storageContext 387 388 bucket := gcsBE.storageClient.Bucket(gcsBE.bucketName) 389 objs := bucket.Objects(ctx, nil) 390 391 for o, err := objs.Next(); err == nil; o, err = objs.Next() { 392 if err := bucket.Object(o.Name).Delete(ctx); err != nil { 393 log.Printf("Error trying to delete object: %s %s\n\n", o.Name, err) 394 } else { 395 log.Printf("Object deleted: %s", o.Name) 396 } 397 } 398 399 // Delete the bucket itself. 400 if err := bucket.Delete(ctx); err != nil { 401 t.Errorf("deleting bucket %q failed, manual cleanup may be required: %v", gcsBE.bucketName, err) 402 } 403 } 404 405 // bucketName returns a valid bucket name for this test. 406 func bucketName(t *testing.T) string { 407 name := fmt.Sprintf("tf-%x-%s", time.Now().UnixNano(), t.Name()) 408 409 // Bucket names must contain 3 to 63 characters. 410 if len(name) > 63 { 411 name = name[:63] 412 } 413 414 return strings.ToLower(name) 415 } 416 417 // getClientOptions returns the []option.ClientOption needed to configure Google API clients 418 // that are required in acceptance tests but are not part of the gcs backend itself 419 func testGetClientOptions(t *testing.T) ([]option.ClientOption, error) { 420 t.Helper() 421 422 var creds string 423 if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { 424 creds = v 425 } else { 426 creds = os.Getenv("GOOGLE_CREDENTIALS") 427 } 428 if creds == "" { 429 t.Skip("This test required credentials to be supplied via" + 430 "the GOOGLE_CREDENTIALS or GOOGLE_BACKEND_CREDENTIALS environment variables.") 431 } 432 433 var opts []option.ClientOption 434 var credOptions []option.ClientOption 435 436 contents, err := backend.ReadPathOrContents(creds) 437 if err != nil { 438 return nil, fmt.Errorf("error loading credentials: %w", err) 439 } 440 if !json.Valid([]byte(contents)) { 441 return nil, fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path") 442 } 443 credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) 444 opts = append(opts, credOptions...) 445 opts = append(opts, option.WithUserAgent(httpclient.OpenTofuUserAgent(version.Version))) 446 447 return opts, nil 448 }