2017-09-08 16:11:41 +02:00
// Package gcs implements remote storage of state on Google Cloud Storage (GCS).
package gcs
2017-07-19 11:07:24 +02:00
import (
"context"
"fmt"
2017-09-12 12:15:13 +02:00
"os"
2017-09-08 09:25:20 +02:00
"strings"
2017-09-07 13:10:56 +02:00
"cloud.google.com/go/storage"
2017-07-19 11:07:24 +02:00
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
2017-09-12 12:15:13 +02:00
"github.com/hashicorp/terraform/terraform"
2017-07-19 11:07:24 +02:00
"google.golang.org/api/option"
)
2017-09-08 13:50:07 +02:00
// gcsBackend implements "backend".Backend for GCS.
// Input(), Validate() and Configure() are implemented by embedding *schema.Backend.
// State(), DeleteState() and States() are implemented explicitly.
type gcsBackend struct {
2017-09-07 10:08:39 +02:00
* schema . Backend
storageClient * storage . Client
2017-09-08 14:14:00 +02:00
storageContext context . Context
2017-09-07 10:08:39 +02:00
2017-09-08 14:32:00 +02:00
bucketName string
2017-09-11 08:25:42 +02:00
prefix string
2017-09-08 14:32:00 +02:00
defaultStateFile string
2017-09-13 15:48:36 +02:00
projectID string
2017-09-13 15:52:39 +02:00
region string
2017-09-07 10:08:39 +02:00
}
2017-07-19 11:07:24 +02:00
func New ( ) backend . Backend {
2017-09-08 13:50:07 +02:00
be := & gcsBackend { }
2017-09-07 13:54:12 +02:00
be . Backend = & schema . Backend {
ConfigureFunc : be . configure ,
2017-07-19 11:07:24 +02:00
Schema : map [ string ] * schema . Schema {
"bucket" : {
Type : schema . TypeString ,
Required : true ,
Description : "The name of the Google Cloud Storage bucket" ,
} ,
2017-09-08 14:32:00 +02:00
"path" : {
Type : schema . TypeString ,
Optional : true ,
2017-09-12 10:47:10 +02:00
Description : "Path of the default state file" ,
Deprecated : "Use the \"prefix\" option instead" ,
2017-09-08 14:32:00 +02:00
} ,
2017-09-11 08:25:42 +02:00
"prefix" : {
2017-07-19 11:07:24 +02:00
Type : schema . TypeString ,
Optional : true ,
Description : "The directory where state files will be saved inside the bucket" ,
} ,
"credentials" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Google Cloud JSON Account Key" ,
Default : "" ,
} ,
2017-09-12 13:57:42 +02:00
"project" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Google Cloud Project ID" ,
Default : "" ,
} ,
2017-09-13 15:52:39 +02:00
"region" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Region / location in which to create the bucket" ,
Default : "" ,
} ,
2017-07-19 11:07:24 +02:00
} ,
}
2017-09-07 13:54:12 +02:00
return be
2017-07-19 11:07:24 +02:00
}
2017-09-08 13:50:07 +02:00
func ( b * gcsBackend ) configure ( ctx context . Context ) error {
2017-07-19 11:07:24 +02:00
if b . storageClient != nil {
return nil
}
2017-09-07 14:09:14 +02:00
// ctx is a background context with the backend config added.
2017-09-08 13:36:40 +02:00
// Since no context is passed to remoteClient.Get(), .Lock(), etc. but
2017-09-07 14:09:14 +02:00
// one is required for calling the GCP API, we're holding on to this
// context here and re-use it later.
b . storageContext = ctx
data := schema . FromContextBackendConfig ( b . storageContext )
2017-07-19 11:07:24 +02:00
2017-10-04 14:19:16 +02:00
b . bucketName = toBucketName ( data . Get ( "bucket" ) . ( string ) )
2017-09-11 08:25:42 +02:00
b . prefix = strings . TrimLeft ( data . Get ( "prefix" ) . ( string ) , "/" )
2017-07-19 11:07:24 +02:00
2017-09-08 14:32:00 +02:00
b . defaultStateFile = strings . TrimLeft ( data . Get ( "path" ) . ( string ) , "/" )
2017-09-13 15:48:36 +02:00
b . projectID = data . Get ( "project" ) . ( string )
if id := os . Getenv ( "GOOGLE_PROJECT" ) ; b . projectID == "" && id != "" {
b . projectID = id
}
2017-09-13 15:52:39 +02:00
b . region = data . Get ( "region" ) . ( string )
if r := os . Getenv ( "GOOGLE_REGION" ) ; b . projectID == "" && r != "" {
b . region = r
}
2017-09-13 15:48:36 +02:00
2017-09-12 12:15:13 +02:00
opts := [ ] option . ClientOption {
option . WithScopes ( storage . ScopeReadWrite ) ,
option . WithUserAgent ( terraform . UserAgentString ( ) ) ,
}
if credentialsFile := data . Get ( "credentials" ) . ( string ) ; credentialsFile != "" {
opts = append ( opts , option . WithCredentialsFile ( credentialsFile ) )
} else if credentialsFile := os . Getenv ( "GOOGLE_CREDENTIALS" ) ; credentialsFile != "" {
opts = append ( opts , option . WithCredentialsFile ( credentialsFile ) )
2017-07-19 11:07:24 +02:00
}
2017-09-12 12:15:13 +02:00
client , err := storage . NewClient ( b . storageContext , opts ... )
2017-07-19 11:07:24 +02:00
if err != nil {
2017-09-12 12:15:13 +02:00
return fmt . Errorf ( "storage.NewClient() failed: %v" , err )
2017-07-19 11:07:24 +02:00
}
b . storageClient = client
2017-09-12 13:57:42 +02:00
return b . ensureBucketExists ( )
}
func ( b * gcsBackend ) ensureBucketExists ( ) error {
_ , err := b . storageClient . Bucket ( b . bucketName ) . Attrs ( b . storageContext )
if err != storage . ErrBucketNotExist {
return err
}
if b . projectID == "" {
return fmt . Errorf ( "bucket %q does not exist; specify the \"project\" option or create the bucket manually using `gsutil mb gs://%s`" , b . bucketName , b . bucketName )
}
2017-09-13 11:36:36 +02:00
attrs := & storage . BucketAttrs {
2017-10-04 10:56:33 +02:00
Location : b . region ,
2017-09-13 11:36:36 +02:00
}
return b . storageClient . Bucket ( b . bucketName ) . Create ( b . storageContext , b . projectID , attrs )
2017-07-19 11:07:24 +02:00
}
2017-10-04 14:19:16 +02:00
// toBucketName returns a copy of in that is suitable for use as a bucket name.
// All upper case characters are converted to lower case, other invalid
// characters are replaced by '_'.
func toBucketName ( in string ) string {
// Bucket names must contain only lowercase letters, numbers, dashes
// (-), and underscores (_).
isValid := func ( r rune ) bool {
switch {
case r >= 'a' && r <= 'z' :
return true
case r >= '0' && r <= '9' :
return true
case r == '-' || r == '_' :
return true
default :
return false
}
}
out := make ( [ ] rune , 0 , len ( in ) )
for _ , r := range strings . ToLower ( in ) {
if ! isValid ( r ) {
r = '_'
}
out = append ( out , r )
}
// Bucket names must contain 3 to 63 characters.
if len ( out ) > 63 {
out = out [ : 63 ]
}
return string ( out )
}