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  }