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  }