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