github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/gcs/backend.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 implements remote storage of state on Google Cloud Storage (GCS).
     7  package gcs
     8  
     9  import (
    10  	"context"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"fmt"
    14  	"os"
    15  	"strings"
    16  
    17  	"cloud.google.com/go/storage"
    18  	"github.com/opentofu/opentofu/internal/backend"
    19  	"github.com/opentofu/opentofu/internal/encryption"
    20  	"github.com/opentofu/opentofu/internal/httpclient"
    21  	"github.com/opentofu/opentofu/internal/legacy/helper/schema"
    22  	"github.com/opentofu/opentofu/version"
    23  	"golang.org/x/oauth2"
    24  	"google.golang.org/api/impersonate"
    25  	"google.golang.org/api/option"
    26  )
    27  
    28  // Backend implements "backend".Backend for GCS.
    29  // Input(), Validate() and Configure() are implemented by embedding *schema.Backend.
    30  // State(), DeleteState() and States() are implemented explicitly.
    31  type Backend struct {
    32  	*schema.Backend
    33  	encryption encryption.StateEncryption
    34  
    35  	storageClient  *storage.Client
    36  	storageContext context.Context
    37  
    38  	bucketName string
    39  	prefix     string
    40  
    41  	encryptionKey []byte
    42  	kmsKeyName    string
    43  }
    44  
    45  func New(enc encryption.StateEncryption) backend.Backend {
    46  	b := &Backend{encryption: enc}
    47  	b.Backend = &schema.Backend{
    48  		ConfigureFunc: b.configure,
    49  		Schema: map[string]*schema.Schema{
    50  			"bucket": {
    51  				Type:        schema.TypeString,
    52  				Required:    true,
    53  				Description: "The name of the Google Cloud Storage bucket",
    54  			},
    55  
    56  			"prefix": {
    57  				Type:        schema.TypeString,
    58  				Optional:    true,
    59  				Description: "The directory where state files will be saved inside the bucket",
    60  			},
    61  
    62  			"credentials": {
    63  				Type:        schema.TypeString,
    64  				Optional:    true,
    65  				Description: "Google Cloud JSON Account Key",
    66  				Default:     "",
    67  			},
    68  
    69  			"access_token": {
    70  				Type:     schema.TypeString,
    71  				Optional: true,
    72  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
    73  					"GOOGLE_OAUTH_ACCESS_TOKEN",
    74  				}, nil),
    75  				Description: "An OAuth2 token used for GCP authentication",
    76  			},
    77  
    78  			"impersonate_service_account": {
    79  				Type:     schema.TypeString,
    80  				Optional: true,
    81  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
    82  					"GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT",
    83  					"GOOGLE_IMPERSONATE_SERVICE_ACCOUNT",
    84  				}, nil),
    85  				Description: "The service account to impersonate for all Google API Calls",
    86  			},
    87  
    88  			"impersonate_service_account_delegates": {
    89  				Type:        schema.TypeList,
    90  				Optional:    true,
    91  				Description: "The delegation chain for the impersonated service account",
    92  				Elem:        &schema.Schema{Type: schema.TypeString},
    93  			},
    94  
    95  			"encryption_key": {
    96  				Type:     schema.TypeString,
    97  				Optional: true,
    98  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
    99  					"GOOGLE_ENCRYPTION_KEY",
   100  				}, nil),
   101  				Description:   "A 32 byte base64 encoded 'customer supplied encryption key' used when reading and writing state files in the bucket.",
   102  				ConflictsWith: []string{"kms_encryption_key"},
   103  			},
   104  
   105  			"kms_encryption_key": {
   106  				Type:     schema.TypeString,
   107  				Optional: true,
   108  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
   109  					"GOOGLE_KMS_ENCRYPTION_KEY",
   110  				}, nil),
   111  				Description:   "A Cloud KMS key ('customer managed encryption key') used when reading and writing state files in the bucket. Format should be 'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}'.",
   112  				ConflictsWith: []string{"encryption_key"},
   113  			},
   114  
   115  			"storage_custom_endpoint": {
   116  				Type:     schema.TypeString,
   117  				Optional: true,
   118  				DefaultFunc: schema.MultiEnvDefaultFunc([]string{
   119  					"GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT",
   120  					"GOOGLE_STORAGE_CUSTOM_ENDPOINT",
   121  				}, nil),
   122  			},
   123  		},
   124  	}
   125  
   126  	return b
   127  }
   128  
   129  func (b *Backend) configure(ctx context.Context) error {
   130  	if b.storageClient != nil {
   131  		return nil
   132  	}
   133  
   134  	// ctx is a background context with the backend config added.
   135  	// Since no context is passed to remoteClient.Get(), .Lock(), etc. but
   136  	// one is required for calling the GCP API, we're holding on to this
   137  	// context here and re-use it later.
   138  	b.storageContext = ctx
   139  
   140  	data := schema.FromContextBackendConfig(b.storageContext)
   141  
   142  	b.bucketName = data.Get("bucket").(string)
   143  	b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/")
   144  	if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") {
   145  		b.prefix = b.prefix + "/"
   146  	}
   147  
   148  	var opts []option.ClientOption
   149  	var credOptions []option.ClientOption
   150  
   151  	// Add credential source
   152  	var creds string
   153  	var tokenSource oauth2.TokenSource
   154  
   155  	if v, ok := data.GetOk("access_token"); ok {
   156  		tokenSource = oauth2.StaticTokenSource(&oauth2.Token{
   157  			AccessToken: v.(string),
   158  		})
   159  	} else if v, ok := data.GetOk("credentials"); ok {
   160  		creds = v.(string)
   161  	} else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" {
   162  		creds = v
   163  	} else {
   164  		creds = os.Getenv("GOOGLE_CREDENTIALS")
   165  	}
   166  
   167  	if tokenSource != nil {
   168  		credOptions = append(credOptions, option.WithTokenSource(tokenSource))
   169  	} else if creds != "" {
   170  
   171  		// to mirror how the provider works, we accept the file path or the contents
   172  		contents, err := backend.ReadPathOrContents(creds)
   173  		if err != nil {
   174  			return fmt.Errorf("Error loading credentials: %w", err)
   175  		}
   176  
   177  		if !json.Valid([]byte(contents)) {
   178  			return fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path")
   179  		}
   180  
   181  		credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents)))
   182  	}
   183  
   184  	// Service Account Impersonation
   185  	if v, ok := data.GetOk("impersonate_service_account"); ok {
   186  		ServiceAccount := v.(string)
   187  		var delegates []string
   188  
   189  		if v, ok := data.GetOk("impersonate_service_account_delegates"); ok {
   190  			d := v.([]interface{})
   191  			if len(delegates) > 0 {
   192  				delegates = make([]string, 0, len(d))
   193  			}
   194  			for _, delegate := range d {
   195  				delegates = append(delegates, delegate.(string))
   196  			}
   197  		}
   198  
   199  		ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
   200  			TargetPrincipal: ServiceAccount,
   201  			Scopes:          []string{storage.ScopeReadWrite},
   202  			Delegates:       delegates,
   203  		}, credOptions...)
   204  
   205  		if err != nil {
   206  			return err
   207  		}
   208  
   209  		opts = append(opts, option.WithTokenSource(ts))
   210  
   211  	} else {
   212  		opts = append(opts, credOptions...)
   213  	}
   214  
   215  	opts = append(opts, option.WithUserAgent(httpclient.OpenTofuUserAgent(version.Version)))
   216  
   217  	// Custom endpoint for storage API
   218  	if storageEndpoint, ok := data.GetOk("storage_custom_endpoint"); ok {
   219  		endpoint := option.WithEndpoint(storageEndpoint.(string))
   220  		opts = append(opts, endpoint)
   221  	}
   222  	client, err := storage.NewClient(b.storageContext, opts...)
   223  	if err != nil {
   224  		return fmt.Errorf("storage.NewClient() failed: %w", err)
   225  	}
   226  
   227  	b.storageClient = client
   228  
   229  	// Customer-supplied encryption
   230  	key := data.Get("encryption_key").(string)
   231  	if key != "" {
   232  		kc, err := backend.ReadPathOrContents(key)
   233  		if err != nil {
   234  			return fmt.Errorf("Error loading encryption key: %w", err)
   235  		}
   236  
   237  		// The GCS client expects a customer supplied encryption key to be
   238  		// passed in as a 32 byte long byte slice. The byte slice is base64
   239  		// encoded before being passed to the API. We take a base64 encoded key
   240  		// to remain consistent with the GCS docs.
   241  		// https://cloud.google.com/storage/docs/encryption#customer-supplied
   242  		// https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181
   243  		k, err := base64.StdEncoding.DecodeString(kc)
   244  		if err != nil {
   245  			return fmt.Errorf("Error decoding encryption key: %w", err)
   246  		}
   247  		b.encryptionKey = k
   248  	}
   249  
   250  	// Customer-managed encryption
   251  	kmsName := data.Get("kms_encryption_key").(string)
   252  	if kmsName != "" {
   253  		b.kmsKeyName = kmsName
   254  	}
   255  
   256  	return nil
   257  }