http provider and http request data source

This commit is contained in:
Dmitrii Korotovskii 2017-05-07 15:58:51 +02:00 committed by Martin Atkins
parent 9a1c6d990f
commit ace0456d58
9 changed files with 400 additions and 0 deletions

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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{},
}
}

View File

@ -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)
}
}

View File

@ -35,6 +35,7 @@ import (
googleprovider "github.com/hashicorp/terraform/builtin/providers/google"
grafanaprovider "github.com/hashicorp/terraform/builtin/providers/grafana"
herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku"
httpprovider "github.com/hashicorp/terraform/builtin/providers/http"
icinga2provider "github.com/hashicorp/terraform/builtin/providers/icinga2"
ignitionprovider "github.com/hashicorp/terraform/builtin/providers/ignition"
influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb"
@ -115,6 +116,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
"google": googleprovider.Provider,
"grafana": grafanaprovider.Provider,
"heroku": herokuprovider.Provider,
"http": httpprovider.Provider,
"icinga2": icinga2provider.Provider,
"ignition": ignitionprovider.Provider,
"influxdb": influxdbprovider.Provider,

View File

@ -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.

View File

@ -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.

View File

@ -288,6 +288,10 @@
<a href="/docs/providers/heroku/index.html">Heroku</a>
</li>
<li<%= sidebar_current("docs-providers-http") %>>
<a href="/docs/providers/http/index.html">HTTP</a>
</li>
<li<%= sidebar_current("docs-providers-icinga2") %>>
<a href="/docs/providers/icinga2/index.html">Icinga2</a>
</li>

View File

@ -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 %>