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