github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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" 18 "golang.org/x/oauth2/jwt" 19 "google.golang.org/api/option" 20 ) 21 22 // Backend implements "backend".Backend for GCS. 23 // Input(), Validate() and Configure() are implemented by embedding *schema.Backend. 24 // State(), DeleteState() and States() are implemented explicitly. 25 type Backend struct { 26 *schema.Backend 27 28 storageClient *storage.Client 29 storageContext context.Context 30 31 bucketName string 32 prefix string 33 defaultStateFile string 34 35 encryptionKey []byte 36 } 37 38 func New() backend.Backend { 39 b := &Backend{} 40 b.Backend = &schema.Backend{ 41 ConfigureFunc: b.configure, 42 Schema: map[string]*schema.Schema{ 43 "bucket": { 44 Type: schema.TypeString, 45 Required: true, 46 Description: "The name of the Google Cloud Storage bucket", 47 }, 48 49 "path": { 50 Type: schema.TypeString, 51 Optional: true, 52 Description: "Path of the default state file", 53 Deprecated: "Use the \"prefix\" option instead", 54 }, 55 56 "prefix": { 57 Type: schema.TypeString, 58 Optional: true, 59 Description: "The directory where state files will be saved inside the bucket", 60 }, 61 62 "credentials": { 63 Type: schema.TypeString, 64 Optional: true, 65 Description: "Google Cloud JSON Account Key", 66 Default: "", 67 }, 68 69 "access_token": { 70 Type: schema.TypeString, 71 Optional: true, 72 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 73 "GOOGLE_OAUTH_ACCESS_TOKEN", 74 }, nil), 75 Description: "An OAuth2 token used for GCP authentication", 76 }, 77 78 "encryption_key": { 79 Type: schema.TypeString, 80 Optional: true, 81 Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.", 82 Default: "", 83 }, 84 85 "project": { 86 Type: schema.TypeString, 87 Optional: true, 88 Description: "Google Cloud Project ID", 89 Default: "", 90 Removed: "Please remove this attribute. It is not used since the backend no longer creates the bucket if it does not yet exist.", 91 }, 92 93 "region": { 94 Type: schema.TypeString, 95 Optional: true, 96 Description: "Region / location in which to create the bucket", 97 Default: "", 98 Removed: "Please remove this attribute. It is not used since the backend no longer creates the bucket if it does not yet exist.", 99 }, 100 }, 101 } 102 103 return b 104 } 105 106 func (b *Backend) configure(ctx context.Context) error { 107 if b.storageClient != nil { 108 return nil 109 } 110 111 // ctx is a background context with the backend config added. 112 // Since no context is passed to remoteClient.Get(), .Lock(), etc. but 113 // one is required for calling the GCP API, we're holding on to this 114 // context here and re-use it later. 115 b.storageContext = ctx 116 117 data := schema.FromContextBackendConfig(b.storageContext) 118 119 b.bucketName = data.Get("bucket").(string) 120 b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/") 121 if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") { 122 b.prefix = b.prefix + "/" 123 } 124 125 b.defaultStateFile = strings.TrimLeft(data.Get("path").(string), "/") 126 127 var opts []option.ClientOption 128 129 // Add credential source 130 var creds string 131 var tokenSource oauth2.TokenSource 132 133 if v, ok := data.GetOk("access_token"); ok { 134 tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ 135 AccessToken: v.(string), 136 }) 137 } else if v, ok := data.GetOk("credentials"); ok { 138 creds = v.(string) 139 } else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { 140 creds = v 141 } else { 142 creds = os.Getenv("GOOGLE_CREDENTIALS") 143 } 144 145 if tokenSource != nil { 146 opts = append(opts, option.WithTokenSource(tokenSource)) 147 } else if creds != "" { 148 var account accountFile 149 150 // to mirror how the provider works, we accept the file path or the contents 151 contents, _, err := pathorcontents.Read(creds) 152 if err != nil { 153 return fmt.Errorf("Error loading credentials: %s", err) 154 } 155 156 if err := json.Unmarshal([]byte(contents), &account); err != nil { 157 return fmt.Errorf("Error parsing credentials '%s': %s", contents, err) 158 } 159 160 conf := jwt.Config{ 161 Email: account.ClientEmail, 162 PrivateKey: []byte(account.PrivateKey), 163 Scopes: []string{storage.ScopeReadWrite}, 164 TokenURL: "https://oauth2.googleapis.com/token", 165 } 166 167 opts = append(opts, option.WithHTTPClient(conf.Client(ctx))) 168 } else { 169 opts = append(opts, option.WithScopes(storage.ScopeReadWrite)) 170 } 171 172 opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) 173 client, err := storage.NewClient(b.storageContext, opts...) 174 if err != nil { 175 return fmt.Errorf("storage.NewClient() failed: %v", err) 176 } 177 178 b.storageClient = client 179 180 key := data.Get("encryption_key").(string) 181 if key == "" { 182 key = os.Getenv("GOOGLE_ENCRYPTION_KEY") 183 } 184 185 if key != "" { 186 kc, _, err := pathorcontents.Read(key) 187 if err != nil { 188 return fmt.Errorf("Error loading encryption key: %s", err) 189 } 190 191 // The GCS client expects a customer supplied encryption key to be 192 // passed in as a 32 byte long byte slice. The byte slice is base64 193 // encoded before being passed to the API. We take a base64 encoded key 194 // to remain consistent with the GCS docs. 195 // https://cloud.google.com/storage/docs/encryption#customer-supplied 196 // https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181 197 k, err := base64.StdEncoding.DecodeString(kc) 198 if err != nil { 199 return fmt.Errorf("Error decoding encryption key: %s", err) 200 } 201 b.encryptionKey = k 202 } 203 204 return nil 205 } 206 207 // accountFile represents the structure of the account file JSON file. 208 type accountFile struct { 209 PrivateKeyId string `json:"private_key_id"` 210 PrivateKey string `json:"private_key"` 211 ClientEmail string `json:"client_email"` 212 ClientId string `json:"client_id"` 213 }