github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/gcs/backend.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 // Package gcs implements remote storage of state on Google Cloud Storage (GCS). 5 package gcs 6 7 import ( 8 "context" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "os" 13 "strings" 14 15 "cloud.google.com/go/storage" 16 "github.com/terramate-io/tf/backend" 17 "github.com/terramate-io/tf/httpclient" 18 "github.com/terramate-io/tf/legacy/helper/schema" 19 "golang.org/x/oauth2" 20 "google.golang.org/api/impersonate" 21 "google.golang.org/api/option" 22 ) 23 24 // Backend implements "backend".Backend for GCS. 25 // Input(), Validate() and Configure() are implemented by embedding *schema.Backend. 26 // State(), DeleteState() and States() are implemented explicitly. 27 type Backend struct { 28 *schema.Backend 29 30 storageClient *storage.Client 31 storageContext context.Context 32 33 bucketName string 34 prefix string 35 36 encryptionKey []byte 37 kmsKeyName 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 "prefix": { 52 Type: schema.TypeString, 53 Optional: true, 54 Description: "The directory where state files will be saved inside the bucket", 55 }, 56 57 "credentials": { 58 Type: schema.TypeString, 59 Optional: true, 60 Description: "Google Cloud JSON Account Key", 61 Default: "", 62 }, 63 64 "access_token": { 65 Type: schema.TypeString, 66 Optional: true, 67 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 68 "GOOGLE_OAUTH_ACCESS_TOKEN", 69 }, nil), 70 Description: "An OAuth2 token used for GCP authentication", 71 }, 72 73 "impersonate_service_account": { 74 Type: schema.TypeString, 75 Optional: true, 76 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 77 "GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT", 78 "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", 79 }, nil), 80 Description: "The service account to impersonate for all Google API Calls", 81 }, 82 83 "impersonate_service_account_delegates": { 84 Type: schema.TypeList, 85 Optional: true, 86 Description: "The delegation chain for the impersonated service account", 87 Elem: &schema.Schema{Type: schema.TypeString}, 88 }, 89 90 "encryption_key": { 91 Type: schema.TypeString, 92 Optional: true, 93 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 94 "GOOGLE_ENCRYPTION_KEY", 95 }, nil), 96 Description: "A 32 byte base64 encoded 'customer supplied encryption key' used when reading and writing state files in the bucket.", 97 ConflictsWith: []string{"kms_encryption_key"}, 98 }, 99 100 "kms_encryption_key": { 101 Type: schema.TypeString, 102 Optional: true, 103 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 104 "GOOGLE_KMS_ENCRYPTION_KEY", 105 }, nil), 106 Description: "A Cloud KMS key ('customer managed encryption key') used when reading and writing state files in the bucket. Format should be 'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}'.", 107 ConflictsWith: []string{"encryption_key"}, 108 }, 109 110 "storage_custom_endpoint": { 111 Type: schema.TypeString, 112 Optional: true, 113 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 114 "GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT", 115 "GOOGLE_STORAGE_CUSTOM_ENDPOINT", 116 }, nil), 117 }, 118 }, 119 } 120 121 return b 122 } 123 124 func (b *Backend) configure(ctx context.Context) error { 125 if b.storageClient != nil { 126 return nil 127 } 128 129 // ctx is a background context with the backend config added. 130 // Since no context is passed to remoteClient.Get(), .Lock(), etc. but 131 // one is required for calling the GCP API, we're holding on to this 132 // context here and re-use it later. 133 b.storageContext = ctx 134 135 data := schema.FromContextBackendConfig(b.storageContext) 136 137 b.bucketName = data.Get("bucket").(string) 138 b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/") 139 if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") { 140 b.prefix = b.prefix + "/" 141 } 142 143 var opts []option.ClientOption 144 var credOptions []option.ClientOption 145 146 // Add credential source 147 var creds string 148 var tokenSource oauth2.TokenSource 149 150 if v, ok := data.GetOk("access_token"); ok { 151 tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ 152 AccessToken: v.(string), 153 }) 154 } else if v, ok := data.GetOk("credentials"); ok { 155 creds = v.(string) 156 } else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { 157 creds = v 158 } else { 159 creds = os.Getenv("GOOGLE_CREDENTIALS") 160 } 161 162 if tokenSource != nil { 163 credOptions = append(credOptions, option.WithTokenSource(tokenSource)) 164 } else if creds != "" { 165 166 // to mirror how the provider works, we accept the file path or the contents 167 contents, err := backend.ReadPathOrContents(creds) 168 if err != nil { 169 return fmt.Errorf("Error loading credentials: %s", err) 170 } 171 172 if !json.Valid([]byte(contents)) { 173 return fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path") 174 } 175 176 credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) 177 } 178 179 // Service Account Impersonation 180 if v, ok := data.GetOk("impersonate_service_account"); ok { 181 ServiceAccount := v.(string) 182 var delegates []string 183 184 if v, ok := data.GetOk("impersonate_service_account_delegates"); ok { 185 d := v.([]interface{}) 186 if len(delegates) > 0 { 187 delegates = make([]string, 0, len(d)) 188 } 189 for _, delegate := range d { 190 delegates = append(delegates, delegate.(string)) 191 } 192 } 193 194 ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ 195 TargetPrincipal: ServiceAccount, 196 Scopes: []string{storage.ScopeReadWrite}, 197 Delegates: delegates, 198 }, credOptions...) 199 200 if err != nil { 201 return err 202 } 203 204 opts = append(opts, option.WithTokenSource(ts)) 205 206 } else { 207 opts = append(opts, credOptions...) 208 } 209 210 opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) 211 212 // Custom endpoint for storage API 213 if storageEndpoint, ok := data.GetOk("storage_custom_endpoint"); ok { 214 endpoint := option.WithEndpoint(storageEndpoint.(string)) 215 opts = append(opts, endpoint) 216 } 217 client, err := storage.NewClient(b.storageContext, opts...) 218 if err != nil { 219 return fmt.Errorf("storage.NewClient() failed: %v", err) 220 } 221 222 b.storageClient = client 223 224 // Customer-supplied encryption 225 key := data.Get("encryption_key").(string) 226 if key != "" { 227 kc, err := backend.ReadPathOrContents(key) 228 if err != nil { 229 return fmt.Errorf("Error loading encryption key: %s", err) 230 } 231 232 // The GCS client expects a customer supplied encryption key to be 233 // passed in as a 32 byte long byte slice. The byte slice is base64 234 // encoded before being passed to the API. We take a base64 encoded key 235 // to remain consistent with the GCS docs. 236 // https://cloud.google.com/storage/docs/encryption#customer-supplied 237 // https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181 238 k, err := base64.StdEncoding.DecodeString(kc) 239 if err != nil { 240 return fmt.Errorf("Error decoding encryption key: %s", err) 241 } 242 b.encryptionKey = k 243 } 244 245 // Customer-managed encryption 246 kmsName := data.Get("kms_encryption_key").(string) 247 if kmsName != "" { 248 b.kmsKeyName = kmsName 249 } 250 251 return nil 252 }