Skip to content

Commit

Permalink
http provider and http request data source
Browse files Browse the repository at this point in the history
  • Loading branch information
korotovsky authored and apparentlymart committed May 9, 2017
1 parent 9a1c6d9 commit c3f1784
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 0 deletions.
104 changes: 104 additions & 0 deletions builtin/providers/http/data_source.go
Original file line number Diff line number Diff line change
@@ -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
}
166 changes: 166 additions & 0 deletions builtin/providers/http/data_source_test.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
18 changes: 18 additions & 0 deletions builtin/providers/http/provider.go
Original file line number Diff line number Diff line change
@@ -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{},
}
}
18 changes: 18 additions & 0 deletions builtin/providers/http/provider_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions command/internal_plugin_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions website/source/docs/providers/http/data_source.html.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions website/source/docs/providers/http/index.html.markdown
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit c3f1784

Please sign in to comment.