github.com/codeherentuk/terraform@v0.11.12-beta1/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/backend"
    14  	"github.com/hashicorp/terraform/helper/pathorcontents"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  	"github.com/hashicorp/terraform/httpclient"
    17  	"golang.org/x/oauth2/jwt"
    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  	defaultStateFile string
    33  
    34  	encryptionKey []byte
    35  
    36  	projectID string
    37  	region    string
    38  }
    39  
    40  func New() backend.Backend {
    41  	b := &Backend{}
    42  	b.Backend = &schema.Backend{
    43  		ConfigureFunc: b.configure,
    44  		Schema: map[string]*schema.Schema{
    45  			"bucket": {
    46  				Type:        schema.TypeString,
    47  				Required:    true,
    48  				Description: "The name of the Google Cloud Storage bucket",
    49  			},
    50  
    51  			"path": {
    52  				Type:        schema.TypeString,
    53  				Optional:    true,
    54  				Description: "Path of the default state file",
    55  				Deprecated:  "Use the \"prefix\" option instead",
    56  			},
    57  
    58  			"prefix": {
    59  				Type:        schema.TypeString,
    60  				Optional:    true,
    61  				Description: "The directory where state files will be saved inside the bucket",
    62  			},
    63  
    64  			"credentials": {
    65  				Type:        schema.TypeString,
    66  				Optional:    true,
    67  				Description: "Google Cloud JSON Account Key",
    68  				Default:     "",
    69  			},
    70  
    71  			"encryption_key": {
    72  				Type:        schema.TypeString,
    73  				Optional:    true,
    74  				Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.",
    75  				Default:     "",
    76  			},
    77  
    78  			"project": {
    79  				Type:        schema.TypeString,
    80  				Optional:    true,
    81  				Description: "Google Cloud Project ID",
    82  				Default:     "",
    83  			},
    84  
    85  			"region": {
    86  				Type:        schema.TypeString,
    87  				Optional:    true,
    88  				Description: "Region / location in which to create the bucket",
    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  	b.defaultStateFile = strings.TrimLeft(data.Get("path").(string), "/")
   117  
   118  	b.projectID = data.Get("project").(string)
   119  	if id := os.Getenv("GOOGLE_PROJECT"); b.projectID == "" && id != "" {
   120  		b.projectID = id
   121  	}
   122  	b.region = data.Get("region").(string)
   123  	if r := os.Getenv("GOOGLE_REGION"); b.projectID == "" && r != "" {
   124  		b.region = r
   125  	}
   126  
   127  	var opts []option.ClientOption
   128  
   129  	creds := data.Get("credentials").(string)
   130  	if creds == "" {
   131  		creds = os.Getenv("GOOGLE_CREDENTIALS")
   132  	}
   133  
   134  	if creds != "" {
   135  		var account accountFile
   136  
   137  		// to mirror how the provider works, we accept the file path or the contents
   138  		contents, _, err := pathorcontents.Read(creds)
   139  		if err != nil {
   140  			return fmt.Errorf("Error loading credentials: %s", err)
   141  		}
   142  
   143  		if err := json.Unmarshal([]byte(contents), &account); err != nil {
   144  			return fmt.Errorf("Error parsing credentials '%s': %s", contents, err)
   145  		}
   146  
   147  		conf := jwt.Config{
   148  			Email:      account.ClientEmail,
   149  			PrivateKey: []byte(account.PrivateKey),
   150  			Scopes:     []string{storage.ScopeReadWrite},
   151  			TokenURL:   "https://accounts.google.com/o/oauth2/token",
   152  		}
   153  
   154  		opts = append(opts, option.WithHTTPClient(conf.Client(ctx)))
   155  	} else {
   156  		opts = append(opts, option.WithScopes(storage.ScopeReadWrite))
   157  	}
   158  
   159  	opts = append(opts, option.WithUserAgent(httpclient.UserAgentString()))
   160  	client, err := storage.NewClient(b.storageContext, opts...)
   161  	if err != nil {
   162  		return fmt.Errorf("storage.NewClient() failed: %v", err)
   163  	}
   164  
   165  	b.storageClient = client
   166  
   167  	key := data.Get("encryption_key").(string)
   168  	if key == "" {
   169  		key = os.Getenv("GOOGLE_ENCRYPTION_KEY")
   170  	}
   171  
   172  	if key != "" {
   173  		kc, _, err := pathorcontents.Read(key)
   174  		if err != nil {
   175  			return fmt.Errorf("Error loading encryption key: %s", err)
   176  		}
   177  
   178  		// The GCS client expects a customer supplied encryption key to be
   179  		// passed in as a 32 byte long byte slice. The byte slice is base64
   180  		// encoded before being passed to the API. We take a base64 encoded key
   181  		// to remain consistent with the GCS docs.
   182  		// https://cloud.google.com/storage/docs/encryption#customer-supplied
   183  		// https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181
   184  		k, err := base64.StdEncoding.DecodeString(kc)
   185  		if err != nil {
   186  			return fmt.Errorf("Error decoding encryption key: %s", err)
   187  		}
   188  		b.encryptionKey = k
   189  	}
   190  
   191  	return nil
   192  }
   193  
   194  // accountFile represents the structure of the account file JSON file.
   195  type accountFile struct {
   196  	PrivateKeyId string `json:"private_key_id"`
   197  	PrivateKey   string `json:"private_key"`
   198  	ClientEmail  string `json:"client_email"`
   199  	ClientId     string `json:"client_id"`
   200  }