github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/gcs/backend.go (about)

     1  // Package gcs implements remote storage of state on Google Cloud Storage (GCS).
     2  package gcs
     3  
     4  import (
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"strings"
    11  
    12  	"cloud.google.com/go/storage"
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/helper/pathorcontents"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  	"github.com/hashicorp/terraform/httpclient"
    17  	"golang.org/x/oauth2"
    18  	"golang.org/x/oauth2/jwt"
    19  	"google.golang.org/api/option"
    20  )
    21  
    22  // Backend implements "backend".Backend for GCS.
    23  // Input(), Validate() and Configure() are implemented by embedding *schema.Backend.
    24  // State(), DeleteState() and States() are implemented explicitly.
    25  type Backend struct {
    26  	*schema.Backend
    27  
    28  	storageClient  *storage.Client
    29  	storageContext context.Context
    30  
    31  	bucketName       string
    32  	prefix           string
    33  	defaultStateFile string
    34  
    35  	encryptionKey []byte
    36  }
    37  
    38  func New() backend.Backend {
    39  	b := &Backend{}
    40  	b.Backend = &schema.Backend{
    41  		ConfigureFunc: b.configure,
    42  		Schema: map[string]*schema.Schema{
    43  			"bucket": {
    44  				Type:        schema.TypeString,
    45  				Required:    true,
    46  				Description: "The name of the Google Cloud Storage bucket",
    47  			},
    48  
    49  			"path": {
    50  				Type:        schema.TypeString,
    51  				Optional:    true,
    52  				Description: "Path of the default state file",
    53  				Deprecated:  "Use the \"prefix\" option instead",
    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  			"encryption_key": {
    79  				Type:        schema.TypeString,
    80  				Optional:    true,
    81  				Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.",
    82  				Default:     "",
    83  			},
    84  
    85  			"project": {
    86  				Type:        schema.TypeString,
    87  				Optional:    true,
    88  				Description: "Google Cloud Project ID",
    89  				Default:     "",
    90  				Removed:     "Please remove this attribute. It is not used since the backend no longer creates the bucket if it does not yet exist.",
    91  			},
    92  
    93  			"region": {
    94  				Type:        schema.TypeString,
    95  				Optional:    true,
    96  				Description: "Region / location in which to create the bucket",
    97  				Default:     "",
    98  				Removed:     "Please remove this attribute. It is not used since the backend no longer creates the bucket if it does not yet exist.",
    99  			},
   100  		},
   101  	}
   102  
   103  	return b
   104  }
   105  
   106  func (b *Backend) configure(ctx context.Context) error {
   107  	if b.storageClient != nil {
   108  		return nil
   109  	}
   110  
   111  	// ctx is a background context with the backend config added.
   112  	// Since no context is passed to remoteClient.Get(), .Lock(), etc. but
   113  	// one is required for calling the GCP API, we're holding on to this
   114  	// context here and re-use it later.
   115  	b.storageContext = ctx
   116  
   117  	data := schema.FromContextBackendConfig(b.storageContext)
   118  
   119  	b.bucketName = data.Get("bucket").(string)
   120  	b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/")
   121  	if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") {
   122  		b.prefix = b.prefix + "/"
   123  	}
   124  
   125  	b.defaultStateFile = strings.TrimLeft(data.Get("path").(string), "/")
   126  
   127  	var opts []option.ClientOption
   128  
   129  	// Add credential source
   130  	var creds string
   131  	var tokenSource oauth2.TokenSource
   132  
   133  	if v, ok := data.GetOk("access_token"); ok {
   134  		tokenSource = oauth2.StaticTokenSource(&oauth2.Token{
   135  			AccessToken: v.(string),
   136  		})
   137  	} else if v, ok := data.GetOk("credentials"); ok {
   138  		creds = v.(string)
   139  	} else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" {
   140  		creds = v
   141  	} else {
   142  		creds = os.Getenv("GOOGLE_CREDENTIALS")
   143  	}
   144  
   145  	if tokenSource != nil {
   146  		opts = append(opts, option.WithTokenSource(tokenSource))
   147  	} else if creds != "" {
   148  		var account accountFile
   149  
   150  		// to mirror how the provider works, we accept the file path or the contents
   151  		contents, _, err := pathorcontents.Read(creds)
   152  		if err != nil {
   153  			return fmt.Errorf("Error loading credentials: %s", err)
   154  		}
   155  
   156  		if err := json.Unmarshal([]byte(contents), &account); err != nil {
   157  			return fmt.Errorf("Error parsing credentials '%s': %s", contents, err)
   158  		}
   159  
   160  		conf := jwt.Config{
   161  			Email:      account.ClientEmail,
   162  			PrivateKey: []byte(account.PrivateKey),
   163  			Scopes:     []string{storage.ScopeReadWrite},
   164  			TokenURL:   "https://oauth2.googleapis.com/token",
   165  		}
   166  
   167  		opts = append(opts, option.WithHTTPClient(conf.Client(ctx)))
   168  	} else {
   169  		opts = append(opts, option.WithScopes(storage.ScopeReadWrite))
   170  	}
   171  
   172  	opts = append(opts, option.WithUserAgent(httpclient.UserAgentString()))
   173  	client, err := storage.NewClient(b.storageContext, opts...)
   174  	if err != nil {
   175  		return fmt.Errorf("storage.NewClient() failed: %v", err)
   176  	}
   177  
   178  	b.storageClient = client
   179  
   180  	key := data.Get("encryption_key").(string)
   181  	if key == "" {
   182  		key = os.Getenv("GOOGLE_ENCRYPTION_KEY")
   183  	}
   184  
   185  	if key != "" {
   186  		kc, _, err := pathorcontents.Read(key)
   187  		if err != nil {
   188  			return fmt.Errorf("Error loading encryption key: %s", err)
   189  		}
   190  
   191  		// The GCS client expects a customer supplied encryption key to be
   192  		// passed in as a 32 byte long byte slice. The byte slice is base64
   193  		// encoded before being passed to the API. We take a base64 encoded key
   194  		// to remain consistent with the GCS docs.
   195  		// https://cloud.google.com/storage/docs/encryption#customer-supplied
   196  		// https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181
   197  		k, err := base64.StdEncoding.DecodeString(kc)
   198  		if err != nil {
   199  			return fmt.Errorf("Error decoding encryption key: %s", err)
   200  		}
   201  		b.encryptionKey = k
   202  	}
   203  
   204  	return nil
   205  }
   206  
   207  // accountFile represents the structure of the account file JSON file.
   208  type accountFile struct {
   209  	PrivateKeyId string `json:"private_key_id"`
   210  	PrivateKey   string `json:"private_key"`
   211  	ClientEmail  string `json:"client_email"`
   212  	ClientId     string `json:"client_id"`
   213  }