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