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