github.com/trawler/terraform@v0.10.8-0.20171106022149-4b1c7a1d9b48/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  	"fmt"
     7  	"os"
     8  	"strings"
     9  
    10  	"cloud.google.com/go/storage"
    11  	"github.com/hashicorp/terraform/backend"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  	"github.com/hashicorp/terraform/terraform"
    14  	"google.golang.org/api/option"
    15  )
    16  
    17  // gcsBackend implements "backend".Backend for GCS.
    18  // Input(), Validate() and Configure() are implemented by embedding *schema.Backend.
    19  // State(), DeleteState() and States() are implemented explicitly.
    20  type gcsBackend struct {
    21  	*schema.Backend
    22  
    23  	storageClient  *storage.Client
    24  	storageContext context.Context
    25  
    26  	bucketName       string
    27  	prefix           string
    28  	defaultStateFile string
    29  
    30  	projectID string
    31  	region    string
    32  }
    33  
    34  func New() backend.Backend {
    35  	be := &gcsBackend{}
    36  	be.Backend = &schema.Backend{
    37  		ConfigureFunc: be.configure,
    38  		Schema: map[string]*schema.Schema{
    39  			"bucket": {
    40  				Type:        schema.TypeString,
    41  				Required:    true,
    42  				Description: "The name of the Google Cloud Storage bucket",
    43  			},
    44  
    45  			"path": {
    46  				Type:        schema.TypeString,
    47  				Optional:    true,
    48  				Description: "Path of the default state file",
    49  				Deprecated:  "Use the \"prefix\" option instead",
    50  			},
    51  
    52  			"prefix": {
    53  				Type:        schema.TypeString,
    54  				Optional:    true,
    55  				Description: "The directory where state files will be saved inside the bucket",
    56  			},
    57  
    58  			"credentials": {
    59  				Type:        schema.TypeString,
    60  				Optional:    true,
    61  				Description: "Google Cloud JSON Account Key",
    62  				Default:     "",
    63  			},
    64  
    65  			"project": {
    66  				Type:        schema.TypeString,
    67  				Optional:    true,
    68  				Description: "Google Cloud Project ID",
    69  				Default:     "",
    70  			},
    71  
    72  			"region": {
    73  				Type:        schema.TypeString,
    74  				Optional:    true,
    75  				Description: "Region / location in which to create the bucket",
    76  				Default:     "",
    77  			},
    78  		},
    79  	}
    80  
    81  	return be
    82  }
    83  
    84  func (b *gcsBackend) configure(ctx context.Context) error {
    85  	if b.storageClient != nil {
    86  		return nil
    87  	}
    88  
    89  	// ctx is a background context with the backend config added.
    90  	// Since no context is passed to remoteClient.Get(), .Lock(), etc. but
    91  	// one is required for calling the GCP API, we're holding on to this
    92  	// context here and re-use it later.
    93  	b.storageContext = ctx
    94  
    95  	data := schema.FromContextBackendConfig(b.storageContext)
    96  
    97  	b.bucketName = data.Get("bucket").(string)
    98  	b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/")
    99  
   100  	b.defaultStateFile = strings.TrimLeft(data.Get("path").(string), "/")
   101  
   102  	b.projectID = data.Get("project").(string)
   103  	if id := os.Getenv("GOOGLE_PROJECT"); b.projectID == "" && id != "" {
   104  		b.projectID = id
   105  	}
   106  	b.region = data.Get("region").(string)
   107  	if r := os.Getenv("GOOGLE_REGION"); b.projectID == "" && r != "" {
   108  		b.region = r
   109  	}
   110  
   111  	opts := []option.ClientOption{
   112  		option.WithScopes(storage.ScopeReadWrite),
   113  		option.WithUserAgent(terraform.UserAgentString()),
   114  	}
   115  	if credentialsFile := data.Get("credentials").(string); credentialsFile != "" {
   116  		opts = append(opts, option.WithCredentialsFile(credentialsFile))
   117  	} else if credentialsFile := os.Getenv("GOOGLE_CREDENTIALS"); credentialsFile != "" {
   118  		opts = append(opts, option.WithCredentialsFile(credentialsFile))
   119  	}
   120  
   121  	client, err := storage.NewClient(b.storageContext, opts...)
   122  	if err != nil {
   123  		return fmt.Errorf("storage.NewClient() failed: %v", err)
   124  	}
   125  
   126  	b.storageClient = client
   127  
   128  	return b.ensureBucketExists()
   129  }
   130  
   131  func (b *gcsBackend) ensureBucketExists() error {
   132  	_, err := b.storageClient.Bucket(b.bucketName).Attrs(b.storageContext)
   133  	if err != storage.ErrBucketNotExist {
   134  		return err
   135  	}
   136  
   137  	if b.projectID == "" {
   138  		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)
   139  	}
   140  
   141  	attrs := &storage.BucketAttrs{
   142  		Location: b.region,
   143  	}
   144  
   145  	return b.storageClient.Bucket(b.bucketName).Create(b.storageContext, b.projectID, attrs)
   146  }