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 }