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