Skip to content

Commit 0175420

Browse files
committed
feat: support provider 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 <[email protected]>
1 parent ccd7a81 commit 0175420

File tree

6 files changed

+323
-7
lines changed

6 files changed

+323
-7
lines changed

docs/index.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,22 @@ 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 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 host name for which the host configuration should take affect.
28+
29+
Optional:
30+
31+
- `request_headers` (Map of String, Sensitive) A map of request header field names and values.

internal/provider/data_source_http.go

+25-1
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

+83
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

+142-4
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,142 @@ 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 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 host name for which the host configuration should take affect.",
63+
Required: true,
64+
},
65+
"request_headers": schema.MapAttribute{
66+
Description: "A map of request header field names and values.",
67+
ElementType: types.StringType,
68+
Optional: true,
69+
Sensitive: true,
70+
},
71+
},
72+
},
73+
},
74+
},
75+
}
2776
}
2877

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

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

0 commit comments

Comments
 (0)