kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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 "fmt" 8 "os" 9 "strings" 10 11 "cloud.google.com/go/storage" 12 "kubeform.dev/terraform-backend-sdk/backend" 13 "kubeform.dev/terraform-backend-sdk/httpclient" 14 "kubeform.dev/terraform-backend-sdk/legacy/helper/schema" 15 "golang.org/x/oauth2" 16 "google.golang.org/api/impersonate" 17 "google.golang.org/api/option" 18 ) 19 20 // Backend 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 Backend struct { 24 *schema.Backend 25 26 storageClient *storage.Client 27 storageContext context.Context 28 29 bucketName string 30 prefix string 31 32 encryptionKey []byte 33 } 34 35 func New() backend.Backend { 36 b := &Backend{} 37 b.Backend = &schema.Backend{ 38 ConfigureFunc: b.configure, 39 Schema: map[string]*schema.Schema{ 40 "bucket": { 41 Type: schema.TypeString, 42 Required: true, 43 Description: "The name of the Google Cloud Storage bucket", 44 }, 45 46 "prefix": { 47 Type: schema.TypeString, 48 Optional: true, 49 Description: "The directory where state files will be saved inside the bucket", 50 }, 51 52 "credentials": { 53 Type: schema.TypeString, 54 Optional: true, 55 Description: "Google Cloud JSON Account Key", 56 Default: "", 57 }, 58 59 "access_token": { 60 Type: schema.TypeString, 61 Optional: true, 62 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 63 "GOOGLE_OAUTH_ACCESS_TOKEN", 64 }, nil), 65 Description: "An OAuth2 token used for GCP authentication", 66 }, 67 68 "impersonate_service_account": { 69 Type: schema.TypeString, 70 Optional: true, 71 DefaultFunc: schema.MultiEnvDefaultFunc([]string{ 72 "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", 73 }, nil), 74 Description: "The service account to impersonate for all Google API Calls", 75 }, 76 77 "impersonate_service_account_delegates": { 78 Type: schema.TypeList, 79 Optional: true, 80 Description: "The delegation chain for the impersonated service account", 81 Elem: &schema.Schema{Type: schema.TypeString}, 82 }, 83 84 "encryption_key": { 85 Type: schema.TypeString, 86 Optional: true, 87 Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.", 88 Default: "", 89 }, 90 }, 91 } 92 93 return b 94 } 95 96 func (b *Backend) configure(ctx context.Context) error { 97 if b.storageClient != nil { 98 return nil 99 } 100 101 // ctx is a background context with the backend config added. 102 // Since no context is passed to remoteClient.Get(), .Lock(), etc. but 103 // one is required for calling the GCP API, we're holding on to this 104 // context here and re-use it later. 105 b.storageContext = ctx 106 107 data := schema.FromContextBackendConfig(b.storageContext) 108 109 b.bucketName = data.Get("bucket").(string) 110 b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/") 111 if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") { 112 b.prefix = b.prefix + "/" 113 } 114 115 var opts []option.ClientOption 116 var credOptions []option.ClientOption 117 118 // Add credential source 119 var creds string 120 var tokenSource oauth2.TokenSource 121 122 if v, ok := data.GetOk("access_token"); ok { 123 tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ 124 AccessToken: v.(string), 125 }) 126 } else if v, ok := data.GetOk("credentials"); ok { 127 creds = v.(string) 128 } else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { 129 creds = v 130 } else { 131 creds = os.Getenv("GOOGLE_CREDENTIALS") 132 } 133 134 if tokenSource != nil { 135 credOptions = append(credOptions, option.WithTokenSource(tokenSource)) 136 } else if creds != "" { 137 138 // to mirror how the provider works, we accept the file path or the contents 139 contents, err := backend.ReadPathOrContents(creds) 140 if err != nil { 141 return fmt.Errorf("Error loading credentials: %s", err) 142 } 143 144 credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) 145 } 146 147 // Service Account Impersonation 148 if v, ok := data.GetOk("impersonate_service_account"); ok { 149 ServiceAccount := v.(string) 150 var delegates []string 151 152 if v, ok := data.GetOk("impersonate_service_account_delegates"); ok { 153 d := v.([]interface{}) 154 if len(delegates) > 0 { 155 delegates = make([]string, len(d)) 156 } 157 for _, delegate := range d { 158 delegates = append(delegates, delegate.(string)) 159 } 160 } 161 162 ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ 163 TargetPrincipal: ServiceAccount, 164 Scopes: []string{storage.ScopeReadWrite}, 165 Delegates: delegates, 166 }, credOptions...) 167 168 if err != nil { 169 return err 170 } 171 172 opts = append(opts, option.WithTokenSource(ts)) 173 174 } else { 175 opts = append(opts, credOptions...) 176 } 177 178 opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) 179 client, err := storage.NewClient(b.storageContext, opts...) 180 if err != nil { 181 return fmt.Errorf("storage.NewClient() failed: %v", err) 182 } 183 184 b.storageClient = client 185 186 key := data.Get("encryption_key").(string) 187 if key == "" { 188 key = os.Getenv("GOOGLE_ENCRYPTION_KEY") 189 } 190 191 if key != "" { 192 kc, err := backend.ReadPathOrContents(key) 193 if err != nil { 194 return fmt.Errorf("Error loading encryption key: %s", err) 195 } 196 197 // The GCS client expects a customer supplied encryption key to be 198 // passed in as a 32 byte long byte slice. The byte slice is base64 199 // encoded before being passed to the API. We take a base64 encoded key 200 // to remain consistent with the GCS docs. 201 // https://cloud.google.com/storage/docs/encryption#customer-supplied 202 // https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181 203 k, err := base64.StdEncoding.DecodeString(kc) 204 if err != nil { 205 return fmt.Errorf("Error decoding encryption key: %s", err) 206 } 207 b.encryptionKey = k 208 } 209 210 return nil 211 }