github.com/chentex/terraform@v0.11.2-0.20171208003256-252e8145842e/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/json"
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"cloud.google.com/go/storage"
    12  	"github.com/hashicorp/terraform/backend"
    13  	"github.com/hashicorp/terraform/helper/pathorcontents"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  	"github.com/hashicorp/terraform/terraform"
    16  	"golang.org/x/oauth2/jwt"
    17  	"google.golang.org/api/option"
    18  )
    19  
    20  // gcsBackend 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 gcsBackend struct {
    24  	*schema.Backend
    25  
    26  	storageClient  *storage.Client
    27  	storageContext context.Context
    28  
    29  	bucketName       string
    30  	prefix           string
    31  	defaultStateFile string
    32  
    33  	projectID string
    34  	region    string
    35  }
    36  
    37  func New() backend.Backend {
    38  	be := &gcsBackend{}
    39  	be.Backend = &schema.Backend{
    40  		ConfigureFunc: be.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  			"path": {
    49  				Type:        schema.TypeString,
    50  				Optional:    true,
    51  				Description: "Path of the default state file",
    52  				Deprecated:  "Use the \"prefix\" option instead",
    53  			},
    54  
    55  			"prefix": {
    56  				Type:        schema.TypeString,
    57  				Optional:    true,
    58  				Description: "The directory where state files will be saved inside the bucket",
    59  			},
    60  
    61  			"credentials": {
    62  				Type:        schema.TypeString,
    63  				Optional:    true,
    64  				Description: "Google Cloud JSON Account Key",
    65  				Default:     "",
    66  			},
    67  
    68  			"project": {
    69  				Type:        schema.TypeString,
    70  				Optional:    true,
    71  				Description: "Google Cloud Project ID",
    72  				Default:     "",
    73  			},
    74  
    75  			"region": {
    76  				Type:        schema.TypeString,
    77  				Optional:    true,
    78  				Description: "Region / location in which to create the bucket",
    79  				Default:     "",
    80  			},
    81  		},
    82  	}
    83  
    84  	return be
    85  }
    86  
    87  func (b *gcsBackend) configure(ctx context.Context) error {
    88  	if b.storageClient != nil {
    89  		return nil
    90  	}
    91  
    92  	// ctx is a background context with the backend config added.
    93  	// Since no context is passed to remoteClient.Get(), .Lock(), etc. but
    94  	// one is required for calling the GCP API, we're holding on to this
    95  	// context here and re-use it later.
    96  	b.storageContext = ctx
    97  
    98  	data := schema.FromContextBackendConfig(b.storageContext)
    99  
   100  	b.bucketName = data.Get("bucket").(string)
   101  	b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/")
   102  	if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") {
   103  		b.prefix = b.prefix + "/"
   104  	}
   105  
   106  	b.defaultStateFile = strings.TrimLeft(data.Get("path").(string), "/")
   107  
   108  	b.projectID = data.Get("project").(string)
   109  	if id := os.Getenv("GOOGLE_PROJECT"); b.projectID == "" && id != "" {
   110  		b.projectID = id
   111  	}
   112  	b.region = data.Get("region").(string)
   113  	if r := os.Getenv("GOOGLE_REGION"); b.projectID == "" && r != "" {
   114  		b.region = r
   115  	}
   116  
   117  	var opts []option.ClientOption
   118  
   119  	creds := data.Get("credentials").(string)
   120  	if creds == "" {
   121  		creds = os.Getenv("GOOGLE_CREDENTIALS")
   122  	}
   123  
   124  	if creds != "" {
   125  		var account accountFile
   126  
   127  		// to mirror how the provider works, we accept the file path or the contents
   128  		contents, _, err := pathorcontents.Read(creds)
   129  		if err != nil {
   130  			return fmt.Errorf("Error loading credentials: %s", err)
   131  		}
   132  
   133  		if err := json.Unmarshal([]byte(contents), &account); err != nil {
   134  			return fmt.Errorf("Error parsing credentials '%s': %s", contents, err)
   135  		}
   136  
   137  		conf := jwt.Config{
   138  			Email:      account.ClientEmail,
   139  			PrivateKey: []byte(account.PrivateKey),
   140  			Scopes:     []string{storage.ScopeReadWrite},
   141  			TokenURL:   "https://accounts.google.com/o/oauth2/token",
   142  		}
   143  
   144  		opts = append(opts, option.WithHTTPClient(conf.Client(ctx)))
   145  	} else {
   146  		opts = append(opts, option.WithScopes(storage.ScopeReadWrite))
   147  	}
   148  
   149  	opts = append(opts, option.WithUserAgent(terraform.UserAgentString()))
   150  	client, err := storage.NewClient(b.storageContext, opts...)
   151  	if err != nil {
   152  		return fmt.Errorf("storage.NewClient() failed: %v", err)
   153  	}
   154  
   155  	b.storageClient = client
   156  
   157  	return nil
   158  }
   159  
   160  // accountFile represents the structure of the account file JSON file.
   161  type accountFile struct {
   162  	PrivateKeyId string `json:"private_key_id"`
   163  	PrivateKey   string `json:"private_key"`
   164  	ClientEmail  string `json:"client_email"`
   165  	ClientId     string `json:"client_id"`
   166  }