github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/http/backend.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package http 5 6 import ( 7 "context" 8 "crypto/tls" 9 "crypto/x509" 10 "errors" 11 "fmt" 12 "log" 13 "net/http" 14 "net/url" 15 "time" 16 17 "github.com/hashicorp/go-retryablehttp" 18 19 "github.com/terramate-io/tf/backend" 20 "github.com/terramate-io/tf/legacy/helper/schema" 21 "github.com/terramate-io/tf/logging" 22 "github.com/terramate-io/tf/states/remote" 23 "github.com/terramate-io/tf/states/statemgr" 24 ) 25 26 func New() backend.Backend { 27 s := &schema.Backend{ 28 Schema: map[string]*schema.Schema{ 29 "address": &schema.Schema{ 30 Type: schema.TypeString, 31 Required: true, 32 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil), 33 Description: "The address of the REST endpoint", 34 }, 35 "update_method": &schema.Schema{ 36 Type: schema.TypeString, 37 Optional: true, 38 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"), 39 Description: "HTTP method to use when updating state", 40 }, 41 "lock_address": &schema.Schema{ 42 Type: schema.TypeString, 43 Optional: true, 44 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil), 45 Description: "The address of the lock REST endpoint", 46 }, 47 "unlock_address": &schema.Schema{ 48 Type: schema.TypeString, 49 Optional: true, 50 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil), 51 Description: "The address of the unlock REST endpoint", 52 }, 53 "lock_method": &schema.Schema{ 54 Type: schema.TypeString, 55 Optional: true, 56 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"), 57 Description: "The HTTP method to use when locking", 58 }, 59 "unlock_method": &schema.Schema{ 60 Type: schema.TypeString, 61 Optional: true, 62 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"), 63 Description: "The HTTP method to use when unlocking", 64 }, 65 "username": &schema.Schema{ 66 Type: schema.TypeString, 67 Optional: true, 68 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil), 69 Description: "The username for HTTP basic authentication", 70 }, 71 "password": &schema.Schema{ 72 Type: schema.TypeString, 73 Optional: true, 74 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil), 75 Description: "The password for HTTP basic authentication", 76 }, 77 "skip_cert_verification": &schema.Schema{ 78 Type: schema.TypeBool, 79 Optional: true, 80 Default: false, 81 Description: "Whether to skip TLS verification.", 82 }, 83 "retry_max": &schema.Schema{ 84 Type: schema.TypeInt, 85 Optional: true, 86 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2), 87 Description: "The number of HTTP request retries.", 88 }, 89 "retry_wait_min": &schema.Schema{ 90 Type: schema.TypeInt, 91 Optional: true, 92 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1), 93 Description: "The minimum time in seconds to wait between HTTP request attempts.", 94 }, 95 "retry_wait_max": &schema.Schema{ 96 Type: schema.TypeInt, 97 Optional: true, 98 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30), 99 Description: "The maximum time in seconds to wait between HTTP request attempts.", 100 }, 101 "client_ca_certificate_pem": &schema.Schema{ 102 Type: schema.TypeString, 103 Optional: true, 104 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""), 105 Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.", 106 }, 107 "client_certificate_pem": &schema.Schema{ 108 Type: schema.TypeString, 109 Optional: true, 110 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""), 111 Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.", 112 }, 113 "client_private_key_pem": &schema.Schema{ 114 Type: schema.TypeString, 115 Optional: true, 116 DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""), 117 Description: "A PEM-encoded private key, required if client_certificate_pem is specified.", 118 }, 119 }, 120 } 121 122 b := &Backend{Backend: s} 123 b.Backend.ConfigureFunc = b.configure 124 return b 125 } 126 127 type Backend struct { 128 *schema.Backend 129 130 client *httpClient 131 } 132 133 // configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made. 134 func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error { 135 // If there are no conditions needing to configure TLS, leave the client untouched 136 skipCertVerification := data.Get("skip_cert_verification").(bool) 137 clientCACertificatePem := data.Get("client_ca_certificate_pem").(string) 138 clientCertificatePem := data.Get("client_certificate_pem").(string) 139 clientPrivateKeyPem := data.Get("client_private_key_pem").(string) 140 if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" { 141 return nil 142 } 143 if clientCertificatePem != "" && clientPrivateKeyPem == "" { 144 return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not") 145 } 146 if clientPrivateKeyPem != "" && clientCertificatePem == "" { 147 return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not") 148 } 149 150 // TLS configuration is needed; create an object and configure it 151 var tlsConfig tls.Config 152 client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig 153 154 if skipCertVerification { 155 // ignores TLS verification 156 tlsConfig.InsecureSkipVerify = true 157 } 158 if clientCACertificatePem != "" { 159 // trust servers based on a CA 160 tlsConfig.RootCAs = x509.NewCertPool() 161 if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) { 162 return errors.New("failed to append certs") 163 } 164 } 165 if clientCertificatePem != "" && clientPrivateKeyPem != "" { 166 // attach a client certificate to the TLS handshake (aka mTLS) 167 certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem)) 168 if err != nil { 169 return fmt.Errorf("cannot load client certificate: %w", err) 170 } 171 tlsConfig.Certificates = []tls.Certificate{certificate} 172 } 173 174 return nil 175 } 176 177 func (b *Backend) configure(ctx context.Context) error { 178 data := schema.FromContextBackendConfig(ctx) 179 180 address := data.Get("address").(string) 181 updateURL, err := url.Parse(address) 182 if err != nil { 183 return fmt.Errorf("failed to parse address URL: %s", err) 184 } 185 if updateURL.Scheme != "http" && updateURL.Scheme != "https" { 186 return fmt.Errorf("address must be HTTP or HTTPS") 187 } 188 189 updateMethod := data.Get("update_method").(string) 190 191 var lockURL *url.URL 192 if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" { 193 var err error 194 lockURL, err = url.Parse(v.(string)) 195 if err != nil { 196 return fmt.Errorf("failed to parse lockAddress URL: %s", err) 197 } 198 if lockURL.Scheme != "http" && lockURL.Scheme != "https" { 199 return fmt.Errorf("lockAddress must be HTTP or HTTPS") 200 } 201 } 202 203 lockMethod := data.Get("lock_method").(string) 204 205 var unlockURL *url.URL 206 if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" { 207 var err error 208 unlockURL, err = url.Parse(v.(string)) 209 if err != nil { 210 return fmt.Errorf("failed to parse unlockAddress URL: %s", err) 211 } 212 if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" { 213 return fmt.Errorf("unlockAddress must be HTTP or HTTPS") 214 } 215 } 216 217 unlockMethod := data.Get("unlock_method").(string) 218 219 rClient := retryablehttp.NewClient() 220 rClient.RetryMax = data.Get("retry_max").(int) 221 rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second 222 rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second 223 rClient.Logger = log.New(logging.LogOutput(), "", log.Flags()) 224 if err = b.configureTLS(rClient, data); err != nil { 225 return err 226 } 227 228 b.client = &httpClient{ 229 URL: updateURL, 230 UpdateMethod: updateMethod, 231 232 LockURL: lockURL, 233 LockMethod: lockMethod, 234 UnlockURL: unlockURL, 235 UnlockMethod: unlockMethod, 236 237 Username: data.Get("username").(string), 238 Password: data.Get("password").(string), 239 240 // accessible only for testing use 241 Client: rClient, 242 } 243 return nil 244 } 245 246 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 247 if name != backend.DefaultStateName { 248 return nil, backend.ErrWorkspacesNotSupported 249 } 250 251 return &remote.State{Client: b.client}, nil 252 } 253 254 func (b *Backend) Workspaces() ([]string, error) { 255 return nil, backend.ErrWorkspacesNotSupported 256 } 257 258 func (b *Backend) DeleteWorkspace(string, bool) error { 259 return backend.ErrWorkspacesNotSupported 260 }