Skip to content

Commit db7d1a5

Browse files
committed
feat: support provider-level host configuration
This seeks to address issue #34 by offering the ability to specify provider-level host configuration & host-specific request headers. Signed-off-by: Mike Ball <mikedball@gmail.com>
1 parent 3bcb679 commit db7d1a5

File tree

6 files changed

+349
-7
lines changed

6 files changed

+349
-7
lines changed

docs/index.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,27 @@ The HTTP provider is a utility provider for interacting with generic HTTP
1010
servers as part of a Terraform configuration.
1111

1212
This provider requires no configuration. For information on the resources
13-
it provides, see the navigation bar.
13+
it provides, see the navigation bar.
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Optional
19+
20+
- `host` (Block List) A host-specific provider configuration. (see [below for nested schema](#nestedblock--host))
21+
22+
<a id="nestedblock--host"></a>
23+
### Nested Schema for `host`
24+
25+
Required:
26+
27+
- `name` (String) The hostname for which the host configuration should
28+
take affect. If the name matches an HTTP request URL's hostname, the provider's
29+
host configuration takes affect (in addition to any data- or resource-specific
30+
request configuration.
31+
32+
Optional:
33+
34+
- `request_headers` (Map of String, Sensitive) A map of request header field names and values to
35+
include in HTTP requests if/when the request URL's hostname matches the provider
36+
host configuration name.

internal/provider/data_source_http.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ func NewHttpDataSource() datasource.DataSource {
3737
return &httpDataSource{}
3838
}
3939

40-
type httpDataSource struct{}
40+
type httpDataSource struct {
41+
provider *httpProvider
42+
}
4143

4244
func (d *httpDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
4345
// This data source name unconventionally is equal to the provider name,
@@ -47,6 +49,10 @@ func (d *httpDataSource) Metadata(_ context.Context, _ datasource.MetadataReques
4749
resp.TypeName = "http"
4850
}
4951

52+
func (d *httpDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
53+
d.provider, resp.Diagnostics = toProvider(req.ProviderData)
54+
}
55+
5056
func (d *httpDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
5157
resp.Schema = schema.Schema{
5258
Description: `
@@ -350,6 +356,24 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
350356
}
351357
}
352358

359+
parsedRequestURL, err := url.Parse(requestURL)
360+
if err != nil {
361+
resp.Diagnostics.AddError(
362+
"Error parsing URL",
363+
"An unexpected error occurred while parsing the request URL: "+err.Error(),
364+
)
365+
return
366+
}
367+
368+
if d.provider.Hostname == parsedRequestURL.Hostname() {
369+
for name, header := range d.provider.RequestHeaders {
370+
request.Header.Set(name, header)
371+
if strings.ToLower(name) == "host" {
372+
request.Host = header
373+
}
374+
}
375+
}
376+
353377
response, err := retryClient.Do(request)
354378
if err != nil {
355379
target := &url.Error{}

internal/provider/data_source_http_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,89 @@ func TestDataSource_withAuthorizationRequestHeader_200(t *testing.T) {
149149
})
150150
}
151151

152+
func TestDataSource_withAuthorizationRequestHeaderFromProvider_200(t *testing.T) {
153+
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154+
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
155+
w.Header().Set("Content-Type", "text/plain")
156+
_, err := w.Write([]byte("1.0.0"))
157+
if err != nil {
158+
t.Errorf("error writing body: %s", err)
159+
}
160+
} else {
161+
w.WriteHeader(http.StatusForbidden)
162+
}
163+
}))
164+
defer testServer.Close()
165+
166+
testServerURL, _ := url.Parse(testServer.URL)
167+
testServerHostname := testServerURL.Hostname()
168+
169+
resource.ParallelTest(t, resource.TestCase{
170+
ProtoV5ProviderFactories: protoV5ProviderFactories(),
171+
Steps: []resource.TestStep{
172+
{
173+
Config: fmt.Sprintf(`
174+
provider "http" {
175+
host {
176+
name = "%s"
177+
request_headers = {
178+
"Authorization" = "Zm9vOmJhcg=="
179+
}
180+
}
181+
}
182+
data "http" "http_test" {
183+
url = "%s"
184+
}`, testServerHostname, testServer.URL),
185+
Check: resource.ComposeTestCheckFunc(
186+
resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"),
187+
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"),
188+
resource.TestCheckNoResourceAttr("data.http.http_test", "request_headers"),
189+
),
190+
},
191+
},
192+
})
193+
}
194+
195+
func TestDataSource_withDifferentHostnameFromProvider_403(t *testing.T) {
196+
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
197+
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
198+
w.Header().Set("Content-Type", "text/plain")
199+
_, err := w.Write([]byte("1.0.0"))
200+
if err != nil {
201+
t.Errorf("error writing body: %s", err)
202+
}
203+
} else {
204+
w.WriteHeader(http.StatusForbidden)
205+
}
206+
}))
207+
defer testServer.Close()
208+
209+
resource.ParallelTest(t, resource.TestCase{
210+
ProtoV5ProviderFactories: protoV5ProviderFactories(),
211+
Steps: []resource.TestStep{
212+
{
213+
Config: fmt.Sprintf(`
214+
provider "http" {
215+
host {
216+
name = "host.com"
217+
request_headers = {
218+
"Authorization" = "Zm9vOmJhcg=="
219+
}
220+
}
221+
}
222+
data "http" "http_test" {
223+
url = "%s"
224+
}`, testServer.URL),
225+
Check: resource.ComposeTestCheckFunc(
226+
resource.TestCheckResourceAttr("data.http.http_test", "response_body", ""),
227+
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "403"),
228+
resource.TestCheckNoResourceAttr("data.http.http_test", "request_headers"),
229+
),
230+
},
231+
},
232+
})
233+
}
234+
152235
func TestDataSource_withAuthorizationRequestHeader_403(t *testing.T) {
153236
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154237
if r.Header.Get("Authorization") != "Zm9vOmJhcg==" {

internal/provider/provider.go

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,145 @@ package provider
55

66
import (
77
"context"
8+
"fmt"
89

10+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
911
"github.com/hashicorp/terraform-plugin-framework/datasource"
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
1014
"github.com/hashicorp/terraform-plugin-framework/provider"
15+
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
1116
"github.com/hashicorp/terraform-plugin-framework/resource"
17+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
18+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
19+
"github.com/hashicorp/terraform-plugin-framework/types"
20+
"github.com/hashicorp/terraform-plugin-log/tflog"
1221
)
1322

23+
type httpProviderConfig struct {
24+
}
25+
26+
type httpProvider struct {
27+
Hostname string
28+
RequestHeaders map[string]string
29+
}
30+
31+
var _ provider.Provider = (*httpProvider)(nil)
32+
1433
func New() provider.Provider {
1534
return &httpProvider{}
1635
}
1736

18-
var _ provider.Provider = (*httpProvider)(nil)
37+
type httpProviderConfigModel struct {
38+
Host types.List `tfsdk:"host"`
39+
}
1940

20-
type httpProvider struct{}
41+
type httpProviderHostConfigModel struct {
42+
Name types.String `tfsdk:"name"`
43+
RequestHeaders types.Map `tfsdk:"request_headers"`
44+
}
2145

2246
func (p *httpProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
2347
resp.TypeName = "http"
2448
}
2549

26-
func (p *httpProvider) Schema(context.Context, provider.SchemaRequest, *provider.SchemaResponse) {
50+
func (p *httpProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
51+
resp.Schema = schema.Schema{
52+
Description: "Configures the HTTP provider",
53+
Blocks: map[string]schema.Block{
54+
"host": schema.ListNestedBlock{
55+
Description: "A host-specific provider configuration.",
56+
Validators: []validator.List{
57+
listvalidator.SizeBetween(0, 1),
58+
},
59+
NestedObject: schema.NestedBlockObject{
60+
Attributes: map[string]schema.Attribute{
61+
"name": schema.StringAttribute{
62+
Description: `The hostname for which the host configuration should
63+
take affect. If the name matches an HTTP request URL's hostname, the provider's
64+
host configuration takes affect (in addition to any data- or resource-specific
65+
request configuration.`,
66+
Required: true,
67+
},
68+
"request_headers": schema.MapAttribute{
69+
Description: `A map of request header field names and values to
70+
include in HTTP requests if/when the request URL's hostname matches the provider
71+
host configuration name.`,
72+
ElementType: types.StringType,
73+
Optional: true,
74+
Sensitive: true,
75+
},
76+
},
77+
},
78+
},
79+
},
80+
}
2781
}
2882

29-
func (p *httpProvider) Configure(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) {
83+
func (p *httpProvider) Configure(ctx context.Context, req provider.ConfigureRequest, res *provider.ConfigureResponse) {
84+
tflog.Debug(ctx, "Configuring provider")
85+
//p.resetConfig()
86+
87+
// Ensure these response values are set before early returns, etc.
88+
res.DataSourceData = p
89+
res.ResourceData = p
90+
91+
// Load configuration into the model
92+
var conf httpProviderConfigModel
93+
res.Diagnostics.Append(req.Config.Get(ctx, &conf)...)
94+
if res.Diagnostics.HasError() {
95+
return
96+
}
97+
if conf.Host.IsNull() || conf.Host.IsUnknown() || len(conf.Host.Elements()) == 0 {
98+
tflog.Debug(ctx, "No host configuration detected; using provider defaults")
99+
return
100+
}
101+
102+
// Load proxy configuration into model
103+
hostConfSlice := make([]httpProviderHostConfigModel, 1)
104+
res.Diagnostics.Append(conf.Host.ElementsAs(ctx, &hostConfSlice, true)...)
105+
if res.Diagnostics.HasError() {
106+
return
107+
}
108+
if len(hostConfSlice) != 1 {
109+
res.Diagnostics.AddAttributeError(
110+
path.Root("host"),
111+
"Provider Proxy Configuration Handling Error",
112+
"The provider failed to fully load the expected host configuration. "+
113+
"This is likely a bug in the Terraform Provider and should be reported to the provider developers.",
114+
)
115+
return
116+
}
117+
hostConf := hostConfSlice[0]
118+
tflog.Debug(ctx, "Loaded provider configuration")
119+
120+
// Parse the host name
121+
if !hostConf.Name.IsNull() && !hostConf.Name.IsUnknown() {
122+
tflog.Debug(ctx, "Configuring host via name", map[string]interface{}{
123+
"name": hostConf.Name.ValueString(),
124+
})
125+
126+
p.Hostname = hostConf.Name.ValueString()
127+
}
128+
129+
if !hostConf.RequestHeaders.IsNull() && !hostConf.RequestHeaders.IsUnknown() {
130+
tflog.Debug(ctx, "Configuring request headers")
131+
requestHeaders := map[string]string{}
132+
for name, value := range hostConf.RequestHeaders.Elements() {
133+
var header string
134+
diags := tfsdk.ValueAs(ctx, value, &header)
135+
res.Diagnostics.Append(diags...)
136+
if res.Diagnostics.HasError() {
137+
return
138+
}
139+
140+
requestHeaders[name] = header
141+
}
142+
143+
p.RequestHeaders = requestHeaders
144+
}
145+
146+
tflog.Debug(ctx, "Provider configured")
30147
}
31148

32149
func (p *httpProvider) Resources(context.Context) []func() resource.Resource {
@@ -38,3 +155,25 @@ func (p *httpProvider) DataSources(context.Context) []func() datasource.DataSour
38155
NewHttpDataSource,
39156
}
40157
}
158+
159+
// toProvider casts a generic provider.Provider reference to this specific provider.
160+
// This can be used in DataSourceType.NewDataSource and ResourceType.NewResource calls.
161+
func toProvider(in any) (*httpProvider, diag.Diagnostics) {
162+
if in == nil {
163+
return nil, nil
164+
}
165+
166+
var diags diag.Diagnostics
167+
p, ok := in.(*httpProvider)
168+
if !ok {
169+
diags.AddError(
170+
"Unexpected Provider Instance Type",
171+
fmt.Sprintf("While creating the data source or resource, an unexpected provider type (%T) was received. "+
172+
"This is likely a bug in the provider code and should be reported to the provider developers.", in,
173+
),
174+
)
175+
return nil, diags
176+
}
177+
178+
return p, diags
179+
}

0 commit comments

Comments
 (0)