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