github.com/pdecat/terraform@v0.11.9-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 }