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