mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
HTTP backend user-defined headers (#1487)
Signed-off-by: David Sims <simsdj82@gmail.com>
This commit is contained in:
parent
d6f2783752
commit
1f3db74281
@ -56,6 +56,7 @@ BUG FIXES:
|
|||||||
* Fix large number will be truncated in plan ([#1382](https://github.com/opentofu/opentofu/pull/1382))
|
* Fix large number will be truncated in plan ([#1382](https://github.com/opentofu/opentofu/pull/1382))
|
||||||
* S3 backend no longer requires to have permissions to use the default 'env:' workspace prefix ([#1445](https://github.com/opentofu/opentofu/pull/1445))
|
* S3 backend no longer requires to have permissions to use the default 'env:' workspace prefix ([#1445](https://github.com/opentofu/opentofu/pull/1445))
|
||||||
* Fixed a crash when using a conditional with Twingate resource ([1446](https://github.com/opentofu/opentofu/pull/1446))
|
* Fixed a crash when using a conditional with Twingate resource ([1446](https://github.com/opentofu/opentofu/pull/1446))
|
||||||
|
* Added support for user-defined headers when configuring the HTTP backend ([1427](https://github.com/opentofu/opentofu/pull/1487))
|
||||||
|
|
||||||
## Previous Releases
|
## Previous Releases
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
@ -119,6 +121,32 @@ func New(enc encryption.StateEncryption) backend.Backend {
|
|||||||
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
|
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
|
||||||
Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
|
Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
|
||||||
},
|
},
|
||||||
|
"headers": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
Optional: true,
|
||||||
|
ValidateFunc: func(cv interface{}, ck string) ([]string, []error) {
|
||||||
|
nameRegex := regexp.MustCompile("[^a-zA-Z0-9-_]")
|
||||||
|
valueRegex := regexp.MustCompile("[^[:ascii:]]")
|
||||||
|
|
||||||
|
headers := cv.(map[string]interface{})
|
||||||
|
err := make([]error, 0, len(headers))
|
||||||
|
for name, value := range headers {
|
||||||
|
if len(name) == 0 || nameRegex.MatchString(name) {
|
||||||
|
err = append(err, fmt.Errorf(
|
||||||
|
"%s \"%s\" name must not be empty and only contain A-Za-z0-9-_ characters", ck, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
v := value.(string)
|
||||||
|
if len(strings.TrimSpace(v)) == 0 || valueRegex.MatchString(v) {
|
||||||
|
err = append(err, fmt.Errorf(
|
||||||
|
"%s \"%s\" value must not be empty and only contain ascii characters", ck, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
},
|
||||||
|
Description: "A map of headers, when set will be included with HTTP requests sent to the HTTP backend",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +248,28 @@ func (b *Backend) configure(ctx context.Context) error {
|
|||||||
|
|
||||||
unlockMethod := data.Get("unlock_method").(string)
|
unlockMethod := data.Get("unlock_method").(string)
|
||||||
|
|
||||||
|
username := data.Get("username").(string)
|
||||||
|
password := data.Get("password").(string)
|
||||||
|
|
||||||
|
var headers map[string]string
|
||||||
|
if dv, ok := data.GetOk("headers"); ok {
|
||||||
|
dh := dv.(map[string]interface{})
|
||||||
|
headers = make(map[string]string, len(dh))
|
||||||
|
|
||||||
|
for k, v := range dh {
|
||||||
|
switch strings.ToLower(k) {
|
||||||
|
case "authorization":
|
||||||
|
if username != "" {
|
||||||
|
return fmt.Errorf("headers \"%s\" cannot be set when providing username", k)
|
||||||
|
}
|
||||||
|
case "content-type", "content-md5":
|
||||||
|
return fmt.Errorf("headers \"%s\" is reserved", k)
|
||||||
|
default:
|
||||||
|
headers[k] = v.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rClient := retryablehttp.NewClient()
|
rClient := retryablehttp.NewClient()
|
||||||
rClient.RetryMax = data.Get("retry_max").(int)
|
rClient.RetryMax = data.Get("retry_max").(int)
|
||||||
rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
|
rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
|
||||||
@ -238,8 +288,9 @@ func (b *Backend) configure(ctx context.Context) error {
|
|||||||
UnlockURL: unlockURL,
|
UnlockURL: unlockURL,
|
||||||
UnlockMethod: unlockMethod,
|
UnlockMethod: unlockMethod,
|
||||||
|
|
||||||
Username: data.Get("username").(string),
|
Headers: headers,
|
||||||
Password: data.Get("password").(string),
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
|
||||||
// accessible only for testing use
|
// accessible only for testing use
|
||||||
Client: rClient,
|
Client: rClient,
|
||||||
|
@ -48,6 +48,10 @@ func TestHTTPClientFactory(t *testing.T) {
|
|||||||
t.Fatal("Unexpected username or password")
|
t.Fatal("Unexpected username or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.Headers != nil {
|
||||||
|
t.Fatal("Unexpected headers")
|
||||||
|
}
|
||||||
|
|
||||||
// custom
|
// custom
|
||||||
conf = map[string]cty.Value{
|
conf = map[string]cty.Value{
|
||||||
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
|
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
|
||||||
@ -61,6 +65,9 @@ func TestHTTPClientFactory(t *testing.T) {
|
|||||||
"retry_max": cty.StringVal("999"),
|
"retry_max": cty.StringVal("999"),
|
||||||
"retry_wait_min": cty.StringVal("15"),
|
"retry_wait_min": cty.StringVal("15"),
|
||||||
"retry_wait_max": cty.StringVal("150"),
|
"retry_wait_max": cty.StringVal("150"),
|
||||||
|
"headers": cty.MapVal(map[string]cty.Value{
|
||||||
|
"user-defined": cty.StringVal("test"),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
b = backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), configs.SynthBody("synth", conf)).(*Backend)
|
b = backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), configs.SynthBody("synth", conf)).(*Backend)
|
||||||
@ -93,6 +100,10 @@ func TestHTTPClientFactory(t *testing.T) {
|
|||||||
if client.Client.RetryWaitMax != 150*time.Second {
|
if client.Client.RetryWaitMax != 150*time.Second {
|
||||||
t.Fatalf("Expected retry_wait_max \"%s\", got \"%s\"", 150*time.Second, client.Client.RetryWaitMax)
|
t.Fatalf("Expected retry_wait_max \"%s\", got \"%s\"", 150*time.Second, client.Client.RetryWaitMax)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(client.Headers) != 1 || client.Headers["user-defined"] != "test" {
|
||||||
|
t.Fatalf("Expected headers \"user-defined\" to be \"test\", got \"%s\"", client.Headers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHTTPClientFactoryWithEnv(t *testing.T) {
|
func TestHTTPClientFactoryWithEnv(t *testing.T) {
|
||||||
|
@ -34,6 +34,7 @@ type httpClient struct {
|
|||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
Client *retryablehttp.Client
|
Client *retryablehttp.Client
|
||||||
|
Headers map[string]string
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
||||||
@ -53,7 +54,12 @@ func (c *httpClient) httpRequest(method string, url *url.URL, data *[]byte, what
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to make %s HTTP request: %w", what, err)
|
return nil, fmt.Errorf("Failed to make %s HTTP request: %w", what, err)
|
||||||
}
|
}
|
||||||
// Set up basic auth
|
|
||||||
|
// Add user-defined headers
|
||||||
|
for k, v := range c.Headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
if c.Username != "" {
|
if c.Username != "" {
|
||||||
req.SetBasicAuth(c.Username, c.Password)
|
req.SetBasicAuth(c.Username, c.Password)
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,34 @@ func TestHTTPClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
remote.TestClient(t, p)
|
remote.TestClient(t, p)
|
||||||
|
|
||||||
|
// Test headers
|
||||||
|
c := retryablehttp.NewClient()
|
||||||
|
c.RequestLogHook = func(_ retryablehttp.Logger, req *http.Request, _ int) {
|
||||||
|
// Test user defined header is part of the request
|
||||||
|
v := req.Header.Get("user-defined")
|
||||||
|
if v != "test" {
|
||||||
|
t.Fatalf("Expected header \"user-defined\" with value \"test\", got \"%s\"", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the content-type header was not overridden
|
||||||
|
v = req.Header.Get("content-type")
|
||||||
|
if req.Method == "PUT" && v != "application/json" {
|
||||||
|
t.Fatalf("Expected header \"content-type\" with value \"application/json\", got \"%s\"", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p = &httpClient{
|
||||||
|
URL: url,
|
||||||
|
UpdateMethod: "PUT",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"user-defined": "test",
|
||||||
|
"content-type": "application/xml",
|
||||||
|
},
|
||||||
|
Client: c,
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.TestClient(t, p)
|
||||||
|
|
||||||
// Test locking and alternative UpdateMethod
|
// Test locking and alternative UpdateMethod
|
||||||
a := &httpClient{
|
a := &httpClient{
|
||||||
URL: url,
|
URL: url,
|
||||||
|
@ -837,6 +837,7 @@ func TestApply_plan_remoteState(t *testing.T) {
|
|||||||
"client_ca_certificate_pem": cty.NullVal(cty.String),
|
"client_ca_certificate_pem": cty.NullVal(cty.String),
|
||||||
"client_certificate_pem": cty.NullVal(cty.String),
|
"client_certificate_pem": cty.NullVal(cty.String),
|
||||||
"client_private_key_pem": cty.NullVal(cty.String),
|
"client_private_key_pem": cty.NullVal(cty.String),
|
||||||
|
"headers": cty.NullVal(cty.String),
|
||||||
})
|
})
|
||||||
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
|
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user