http provider and http request data source
This commit is contained in:
parent
9a1c6d990f
commit
ace0456d58
|
@ -0,0 +1,104 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dataSource() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Read: dataSourceRead,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"url": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"request_headers": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"body": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataSourceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
|
||||||
|
url := d.Get("url").(string)
|
||||||
|
headers := d.Get("request_headers").(map[string]interface{})
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Set(name, value.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error during making a request: %s", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("HTTP request error. Response code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if contentType == "" || isContentTypeAllowed(contentType) == false {
|
||||||
|
return fmt.Errorf("Content-Type is not a text type. Got: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error while reading response body. %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("body", string(bytes))
|
||||||
|
d.SetId(time.Now().UTC().String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is to prevent potential issues w/ binary files
|
||||||
|
// and generally unprintable characters
|
||||||
|
// See https://github.com/hashicorp/terraform/pull/3858#issuecomment-156856738
|
||||||
|
func isContentTypeAllowed(contentType string) bool {
|
||||||
|
allowedContentTypes := []*regexp.Regexp{
|
||||||
|
regexp.MustCompile("^text/.+"),
|
||||||
|
regexp.MustCompile("^application/json$"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range allowedContentTypes {
|
||||||
|
if r.MatchString(contentType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestHttpMock struct {
|
||||||
|
server *httptest.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDataSourceConfig_basic = `
|
||||||
|
data "http" "http_test" {
|
||||||
|
url = "%s/meta_%d.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "body" {
|
||||||
|
value = "${data.http.http_test.body}"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestDataSource_http200(t *testing.T) {
|
||||||
|
testHttpMock := setUpMockHttpServer()
|
||||||
|
|
||||||
|
defer testHttpMock.server.Close()
|
||||||
|
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: fmt.Sprintf(testDataSourceConfig_basic, testHttpMock.server.URL, 200),
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
_, ok := s.RootModule().Resources["data.http.http_test"]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("missing data resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := s.RootModule().Outputs
|
||||||
|
|
||||||
|
if outputs["body"].Value != "1.0.0" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
`'body' output is %s; want '1.0.0'`,
|
||||||
|
outputs["body"].Value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataSource_http404(t *testing.T) {
|
||||||
|
testHttpMock := setUpMockHttpServer()
|
||||||
|
|
||||||
|
defer testHttpMock.server.Close()
|
||||||
|
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: fmt.Sprintf(testDataSourceConfig_basic, testHttpMock.server.URL, 404),
|
||||||
|
ExpectError: regexp.MustCompile("HTTP request error. Response code: 404"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDataSourceConfig_withHeaders = `
|
||||||
|
data "http" "http_test" {
|
||||||
|
url = "%s/restricted/meta_%d.txt"
|
||||||
|
|
||||||
|
request_headers = {
|
||||||
|
"Authorization" = "Zm9vOmJhcg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "body" {
|
||||||
|
value = "${data.http.http_test.body}"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestDataSource_withHeaders200(t *testing.T) {
|
||||||
|
testHttpMock := setUpMockHttpServer()
|
||||||
|
|
||||||
|
defer testHttpMock.server.Close()
|
||||||
|
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: fmt.Sprintf(testDataSourceConfig_withHeaders, testHttpMock.server.URL, 200),
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
_, ok := s.RootModule().Resources["data.http.http_test"]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("missing data resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := s.RootModule().Outputs
|
||||||
|
|
||||||
|
if outputs["body"].Value != "1.0.0" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
`'body' output is %s; want '1.0.0'`,
|
||||||
|
outputs["body"].Value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDataSourceConfig_error = `
|
||||||
|
data "http" "http_test" {
|
||||||
|
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestDataSource_compileError(t *testing.T) {
|
||||||
|
resource.UnitTest(t, resource.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testDataSourceConfig_error,
|
||||||
|
ExpectError: regexp.MustCompile("required field is not set"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpMockHttpServer() *TestHttpMock {
|
||||||
|
Server := httptest.NewServer(
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/meta_200.txt" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("1.0.0"))
|
||||||
|
} else if r.URL.Path == "/restricted/meta_200.txt" {
|
||||||
|
if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("1.0.0"))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
} else if r.URL.Path == "/meta_404.txt" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &TestHttpMock{
|
||||||
|
server: Server,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Provider() terraform.ResourceProvider {
|
||||||
|
return &schema.Provider{
|
||||||
|
Schema: map[string]*schema.Schema{},
|
||||||
|
|
||||||
|
DataSourcesMap: map[string]*schema.Resource{
|
||||||
|
"http": dataSource(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ResourcesMap: map[string]*schema.Resource{},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testProviders = map[string]terraform.ResourceProvider{
|
||||||
|
"http": Provider(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider(t *testing.T) {
|
||||||
|
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import (
|
||||||
googleprovider "github.com/hashicorp/terraform/builtin/providers/google"
|
googleprovider "github.com/hashicorp/terraform/builtin/providers/google"
|
||||||
grafanaprovider "github.com/hashicorp/terraform/builtin/providers/grafana"
|
grafanaprovider "github.com/hashicorp/terraform/builtin/providers/grafana"
|
||||||
herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku"
|
herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku"
|
||||||
|
httpprovider "github.com/hashicorp/terraform/builtin/providers/http"
|
||||||
icinga2provider "github.com/hashicorp/terraform/builtin/providers/icinga2"
|
icinga2provider "github.com/hashicorp/terraform/builtin/providers/icinga2"
|
||||||
ignitionprovider "github.com/hashicorp/terraform/builtin/providers/ignition"
|
ignitionprovider "github.com/hashicorp/terraform/builtin/providers/ignition"
|
||||||
influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb"
|
influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb"
|
||||||
|
@ -115,6 +116,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
|
||||||
"google": googleprovider.Provider,
|
"google": googleprovider.Provider,
|
||||||
"grafana": grafanaprovider.Provider,
|
"grafana": grafanaprovider.Provider,
|
||||||
"heroku": herokuprovider.Provider,
|
"heroku": herokuprovider.Provider,
|
||||||
|
"http": httpprovider.Provider,
|
||||||
"icinga2": icinga2provider.Provider,
|
"icinga2": icinga2provider.Provider,
|
||||||
"ignition": ignitionprovider.Provider,
|
"ignition": ignitionprovider.Provider,
|
||||||
"influxdb": influxdbprovider.Provider,
|
"influxdb": influxdbprovider.Provider,
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
layout: "http"
|
||||||
|
page_title: "HTTP Data Source"
|
||||||
|
sidebar_current: "docs-http-data-source"
|
||||||
|
description: |-
|
||||||
|
Retrieves the content at an HTTP or HTTPS URL.
|
||||||
|
---
|
||||||
|
|
||||||
|
# `http` Data Source
|
||||||
|
|
||||||
|
The `http` data source makes an HTTP GET request to the given URL and exports
|
||||||
|
information about the response.
|
||||||
|
|
||||||
|
The given URL may be either an `http` or `https` URL. At present this resource
|
||||||
|
can only retrieve data from URLs that respond with `text/*` or
|
||||||
|
`application/json` content types, and expects the result to be UTF-8 encoded
|
||||||
|
regardless of the returned content type header.
|
||||||
|
|
||||||
|
~> **Important** Although `https` URLs can be used, there is currently no
|
||||||
|
mechanism to authenticate the remote server except for general verification of
|
||||||
|
the server certificate's chain of trust. Data retrieved from servers not under
|
||||||
|
your control should be treated as untrustworthy.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "http" "example" {
|
||||||
|
url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
|
||||||
|
|
||||||
|
# Optional request headers
|
||||||
|
request_headers {
|
||||||
|
"Accept" = "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `url` - (Required) The URL to request data from. This URL must respond with
|
||||||
|
a `200 OK` response and a `text/*` or `application/json` Content-Type.
|
||||||
|
|
||||||
|
* `request_headers` - (Optional) A map of strings representing additional HTTP
|
||||||
|
headers to include in the request.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `body` - The raw body of the HTTP response.
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
layout: "http"
|
||||||
|
page_title: "Provider: HTTP"
|
||||||
|
sidebar_current: "docs-http-index"
|
||||||
|
description: |-
|
||||||
|
The HTTP provider interacts with HTTP servers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HTTP Provider
|
||||||
|
|
||||||
|
The HTTP provider is a utility provider for interacting with generic HTTP
|
||||||
|
servers as part of a Terraform configuration.
|
||||||
|
|
||||||
|
This provider requires no configuration. For information on the resources
|
||||||
|
it provides, see the navigation bar.
|
|
@ -288,6 +288,10 @@
|
||||||
<a href="/docs/providers/heroku/index.html">Heroku</a>
|
<a href="/docs/providers/heroku/index.html">Heroku</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-providers-http") %>>
|
||||||
|
<a href="/docs/providers/http/index.html">HTTP</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-providers-icinga2") %>>
|
<li<%= sidebar_current("docs-providers-icinga2") %>>
|
||||||
<a href="/docs/providers/icinga2/index.html">Icinga2</a>
|
<a href="/docs/providers/icinga2/index.html">Icinga2</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<% wrap_layout :inner do %>
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||||
|
<ul class="nav docs-sidenav">
|
||||||
|
<li<%#= sidebar_current("docs-home") %>>
|
||||||
|
<a href="/docs/providers/index.html">All Providers</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-http-index") %>>
|
||||||
|
<a href="/docs/providers/http/index.html">HTTP Provider</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-http-data-source") %>>
|
||||||
|
<a href="/docs/providers/http/data_source.html">Data Source</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
Loading…
Reference in New Issue