mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
backend/http: implement retries for the http backend (#19702)
Fixes #19619
This commit is contained in:
parent
127cbeeda2
commit
5b6b1663ef
@ -6,8 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
"github.com/hashicorp/go-cleanhttp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
@ -66,6 +68,24 @@ func New() backend.Backend {
|
|||||||
Default: false,
|
Default: false,
|
||||||
Description: "Whether to skip TLS verification.",
|
Description: "Whether to skip TLS verification.",
|
||||||
},
|
},
|
||||||
|
"retry_max": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Default: 2,
|
||||||
|
Description: "The number of HTTP request retries.",
|
||||||
|
},
|
||||||
|
"retry_wait_min": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Default: 1,
|
||||||
|
Description: "The minimum time in seconds to wait between HTTP request attempts.",
|
||||||
|
},
|
||||||
|
"retry_wait_max": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Default: 30,
|
||||||
|
Description: "The maximum time in seconds to wait between HTTP request attempts.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +151,12 @@ func (b *Backend) configure(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rClient := retryablehttp.NewClient()
|
||||||
|
rClient.HTTPClient = client
|
||||||
|
rClient.RetryMax = data.Get("retry_max").(int)
|
||||||
|
rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
|
||||||
|
rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second
|
||||||
|
|
||||||
b.client = &httpClient{
|
b.client = &httpClient{
|
||||||
URL: updateURL,
|
URL: updateURL,
|
||||||
UpdateMethod: updateMethod,
|
UpdateMethod: updateMethod,
|
||||||
@ -144,7 +170,7 @@ func (b *Backend) configure(ctx context.Context) error {
|
|||||||
Password: data.Get("password").(string),
|
Password: data.Get("password").(string),
|
||||||
|
|
||||||
// accessible only for testing use
|
// accessible only for testing use
|
||||||
Client: client,
|
Client: rClient,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
@ -51,6 +52,9 @@ func TestHTTPClientFactory(t *testing.T) {
|
|||||||
"unlock_method": cty.StringVal("BLOOP"),
|
"unlock_method": cty.StringVal("BLOOP"),
|
||||||
"username": cty.StringVal("user"),
|
"username": cty.StringVal("user"),
|
||||||
"password": cty.StringVal("pass"),
|
"password": cty.StringVal("pass"),
|
||||||
|
"retry_max": cty.StringVal("999"),
|
||||||
|
"retry_wait_min": cty.StringVal("15"),
|
||||||
|
"retry_wait_max": cty.StringVal("150"),
|
||||||
}
|
}
|
||||||
|
|
||||||
b = backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend)
|
b = backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend)
|
||||||
@ -74,4 +78,13 @@ func TestHTTPClientFactory(t *testing.T) {
|
|||||||
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
|
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
|
||||||
client.Password, conf["password"])
|
client.Password, conf["password"])
|
||||||
}
|
}
|
||||||
|
if client.Client.RetryMax != 999 {
|
||||||
|
t.Fatalf("Expected retry_max \"%d\", got \"%d\"", 999, client.Client.RetryMax)
|
||||||
|
}
|
||||||
|
if client.Client.RetryWaitMin != 15*time.Second {
|
||||||
|
t.Fatalf("Expected retry_wait_min \"%s\", got \"%s\"", 15*time.Second, client.Client.RetryWaitMin)
|
||||||
|
}
|
||||||
|
if client.Client.RetryWaitMax != 150*time.Second {
|
||||||
|
t.Fatalf("Expected retry_wait_max \"%s\", got \"%s\"", 150*time.Second, client.Client.RetryWaitMax)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
)
|
)
|
||||||
@ -28,7 +29,7 @@ type httpClient struct {
|
|||||||
UnlockMethod string
|
UnlockMethod string
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
Client *http.Client
|
Client *retryablehttp.Client
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ func (c *httpClient) httpRequest(method string, url *url.URL, data *[]byte, what
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the request
|
// Create the request
|
||||||
req, err := http.NewRequest(method, url.String(), reader)
|
req, err := retryablehttp.NewRequest(method, url.String(), reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to make %s HTTP request: %s", what, err)
|
return nil, fmt.Errorf("Failed to make %s HTTP request: %s", what, err)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,14 +30,14 @@ func TestHTTPClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test basic get/update
|
// Test basic get/update
|
||||||
client := &httpClient{URL: url, Client: cleanhttp.DefaultClient()}
|
client := &httpClient{URL: url, Client: retryablehttp.NewClient()}
|
||||||
remote.TestClient(t, client)
|
remote.TestClient(t, client)
|
||||||
|
|
||||||
// test just a single PUT
|
// test just a single PUT
|
||||||
p := &httpClient{
|
p := &httpClient{
|
||||||
URL: url,
|
URL: url,
|
||||||
UpdateMethod: "PUT",
|
UpdateMethod: "PUT",
|
||||||
Client: cleanhttp.DefaultClient(),
|
Client: retryablehttp.NewClient(),
|
||||||
}
|
}
|
||||||
remote.TestClient(t, p)
|
remote.TestClient(t, p)
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ func TestHTTPClient(t *testing.T) {
|
|||||||
LockMethod: "LOCK",
|
LockMethod: "LOCK",
|
||||||
UnlockURL: url,
|
UnlockURL: url,
|
||||||
UnlockMethod: "UNLOCK",
|
UnlockMethod: "UNLOCK",
|
||||||
Client: cleanhttp.DefaultClient(),
|
Client: retryablehttp.NewClient(),
|
||||||
}
|
}
|
||||||
b := &httpClient{
|
b := &httpClient{
|
||||||
URL: url,
|
URL: url,
|
||||||
@ -58,7 +58,7 @@ func TestHTTPClient(t *testing.T) {
|
|||||||
LockMethod: "LOCK",
|
LockMethod: "LOCK",
|
||||||
UnlockURL: url,
|
UnlockURL: url,
|
||||||
UnlockMethod: "UNLOCK",
|
UnlockMethod: "UNLOCK",
|
||||||
Client: cleanhttp.DefaultClient(),
|
Client: retryablehttp.NewClient(),
|
||||||
}
|
}
|
||||||
remote.TestRemoteLocks(t, a, b)
|
remote.TestRemoteLocks(t, a, b)
|
||||||
|
|
||||||
@ -68,13 +68,23 @@ func TestHTTPClient(t *testing.T) {
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
url, err = url.Parse(ts.URL)
|
url, err = url.Parse(ts.URL)
|
||||||
c := &httpClient{
|
client = &httpClient{
|
||||||
URL: url,
|
URL: url,
|
||||||
UpdateMethod: "PUT",
|
UpdateMethod: "PUT",
|
||||||
Client: cleanhttp.DefaultClient(),
|
Client: retryablehttp.NewClient(),
|
||||||
}
|
}
|
||||||
remote.TestClient(t, c) // first time through: 201
|
remote.TestClient(t, client) // first time through: 201
|
||||||
remote.TestClient(t, c) // second time, with identical data: 204
|
remote.TestClient(t, client) // second time, with identical data: 204
|
||||||
|
|
||||||
|
// test a broken backend
|
||||||
|
brokenHandler := new(testBrokenHTTPHandler)
|
||||||
|
brokenHandler.handler = new(testHTTPHandler)
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(brokenHandler.Handle))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
url, err = url.Parse(ts.URL)
|
||||||
|
client = &httpClient{URL: url, Client: retryablehttp.NewClient()}
|
||||||
|
remote.TestClient(t, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertError(t *testing.T, err error, expected string) {
|
func assertError(t *testing.T, err error, expected string) {
|
||||||
@ -149,3 +159,18 @@ func (h *testHTTPHandler) HandleWebDAV(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
|
w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testBrokenHTTPHandler struct {
|
||||||
|
lastRequestWasBroken bool
|
||||||
|
handler *testHTTPHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testBrokenHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.lastRequestWasBroken {
|
||||||
|
h.lastRequestWasBroken = false
|
||||||
|
h.handler.Handle(w, r)
|
||||||
|
} else {
|
||||||
|
h.lastRequestWasBroken = true
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -677,6 +677,9 @@ func TestApply_plan_remoteState(t *testing.T) {
|
|||||||
"username": cty.NullVal(cty.String),
|
"username": cty.NullVal(cty.String),
|
||||||
"password": cty.NullVal(cty.String),
|
"password": cty.NullVal(cty.String),
|
||||||
"skip_cert_verification": cty.NullVal(cty.Bool),
|
"skip_cert_verification": cty.NullVal(cty.Bool),
|
||||||
|
"retry_max": cty.NullVal(cty.String),
|
||||||
|
"retry_wait_min": cty.NullVal(cty.String),
|
||||||
|
"retry_wait_max": cty.NullVal(cty.String),
|
||||||
})
|
})
|
||||||
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
|
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -60,3 +60,8 @@ The following configuration options are supported:
|
|||||||
* `password` - (Optional) The password for HTTP basic authentication
|
* `password` - (Optional) The password for HTTP basic authentication
|
||||||
* `skip_cert_verification` - (Optional) Whether to skip TLS verification.
|
* `skip_cert_verification` - (Optional) Whether to skip TLS verification.
|
||||||
Defaults to `false`.
|
Defaults to `false`.
|
||||||
|
* `retry_max` – (Optional) The number of HTTP request retries. Defaults to `2`.
|
||||||
|
* `retry_wait_min` – (Optional) The minimum time in seconds to wait between HTTP request attempts.
|
||||||
|
Defaults to `1`.
|
||||||
|
* `retry_wait_max` – (Optional) The maximum time in seconds to wait between HTTP request attempts.
|
||||||
|
Defaults to `30`.
|
||||||
|
Loading…
Reference in New Issue
Block a user