summaryrefslogtreecommitdiff
path: root/data
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2018-12-03 21:32:43 -0500
committerGitHub <noreply@github.com>2018-12-03 21:32:43 -0500
commitd97981ecc9929df01da0cce6cbbcea940202f643 (patch)
tree80eebe0c78694a9feb00db3fdb0769eee8d2f008 /data
parent2f357bd5a55169452deeebfaccab6515b06364de (diff)
Support subpaths for http datasources (#442)
* bug: subpaths were ignored in http datasources Signed-off-by: Dave Henderson <dhenderson@gmail.com> * fixup! bug: subpaths were ignored in http datasources
Diffstat (limited to 'data')
-rw-r--r--data/datasource.go74
-rw-r--r--data/datasource_http.go100
-rw-r--r--data/datasource_http_test.go220
-rw-r--r--data/datasource_test.go166
4 files changed, 332 insertions, 228 deletions
diff --git a/data/datasource.go b/data/datasource.go
index 2e4d845d..5ceadd11 100644
--- a/data/datasource.go
+++ b/data/datasource.go
@@ -11,7 +11,6 @@ import (
"path"
"path/filepath"
"strings"
- "time"
"github.com/pkg/errors"
@@ -373,42 +372,6 @@ func readStdin(source *Source, args ...string) ([]byte, error) {
return b, nil
}
-func readHTTP(source *Source, args ...string) ([]byte, error) {
- if source.hc == nil {
- source.hc = &http.Client{Timeout: time.Second * 5}
- }
- req, err := http.NewRequest("GET", source.URL.String(), nil)
- if err != nil {
- return nil, err
- }
- req.Header = source.header
- res, err := source.hc.Do(req)
- if err != nil {
- return nil, err
- }
- body, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return nil, err
- }
- err = res.Body.Close()
- if err != nil {
- return nil, err
- }
- if res.StatusCode != 200 {
- err := errors.Errorf("Unexpected HTTP status %d on GET from %s: %s", res.StatusCode, source.URL, string(body))
- return nil, err
- }
- ctypeHdr := res.Header.Get("Content-Type")
- if ctypeHdr != "" {
- mediatype, _, e := mime.ParseMediaType(ctypeHdr)
- if e != nil {
- return nil, e
- }
- source.mediaType = mediatype
- }
- return body, nil
-}
-
func readConsul(source *Source, args ...string) (data []byte, err error) {
if source.kv == nil {
source.kv, err = libkv.NewConsul(source.URL)
@@ -454,40 +417,3 @@ func readBoltDB(source *Source, args ...string) (data []byte, err error) {
return data, nil
}
-
-func parseHeaderArgs(headerArgs []string) (map[string]http.Header, error) {
- headers := make(map[string]http.Header)
- for _, v := range headerArgs {
- ds, name, value, err := splitHeaderArg(v)
- if err != nil {
- return nil, err
- }
- if _, ok := headers[ds]; !ok {
- headers[ds] = make(http.Header)
- }
- headers[ds][name] = append(headers[ds][name], strings.TrimSpace(value))
- }
- return headers, nil
-}
-
-func splitHeaderArg(arg string) (datasourceAlias, name, value string, err error) {
- parts := strings.SplitN(arg, "=", 2)
- if len(parts) != 2 {
- err = errors.Errorf("Invalid datasource-header option '%s'", arg)
- return "", "", "", err
- }
- datasourceAlias = parts[0]
- name, value, err = splitHeader(parts[1])
- return datasourceAlias, name, value, err
-}
-
-func splitHeader(header string) (name, value string, err error) {
- parts := strings.SplitN(header, ":", 2)
- if len(parts) != 2 {
- err = errors.Errorf("Invalid HTTP Header format '%s'", header)
- return "", "", err
- }
- name = http.CanonicalHeaderKey(parts[0])
- value = parts[1]
- return name, value, nil
-}
diff --git a/data/datasource_http.go b/data/datasource_http.go
new file mode 100644
index 00000000..5424fbd0
--- /dev/null
+++ b/data/datasource_http.go
@@ -0,0 +1,100 @@
+package data
+
+import (
+ "io/ioutil"
+ "mime"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+func buildURL(base *url.URL, args ...string) (*url.URL, error) {
+ if len(args) == 0 {
+ return base, nil
+ }
+ p, err := url.Parse(args[0])
+ if err != nil {
+ return nil, errors.Wrapf(err, "bad sub-path %s", args[0])
+ }
+ return base.ResolveReference(p), nil
+}
+
+func readHTTP(source *Source, args ...string) ([]byte, error) {
+ if source.hc == nil {
+ source.hc = &http.Client{Timeout: time.Second * 5}
+ }
+ u, err := buildURL(source.URL, args...)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest("GET", u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header = source.header
+ res, err := source.hc.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+ err = res.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+ if res.StatusCode != 200 {
+ err := errors.Errorf("Unexpected HTTP status %d on GET from %s: %s", res.StatusCode, source.URL, string(body))
+ return nil, err
+ }
+ ctypeHdr := res.Header.Get("Content-Type")
+ if ctypeHdr != "" {
+ mediatype, _, e := mime.ParseMediaType(ctypeHdr)
+ if e != nil {
+ return nil, e
+ }
+ source.mediaType = mediatype
+ }
+ return body, nil
+}
+
+func parseHeaderArgs(headerArgs []string) (map[string]http.Header, error) {
+ headers := make(map[string]http.Header)
+ for _, v := range headerArgs {
+ ds, name, value, err := splitHeaderArg(v)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := headers[ds]; !ok {
+ headers[ds] = make(http.Header)
+ }
+ headers[ds][name] = append(headers[ds][name], strings.TrimSpace(value))
+ }
+ return headers, nil
+}
+
+func splitHeaderArg(arg string) (datasourceAlias, name, value string, err error) {
+ parts := strings.SplitN(arg, "=", 2)
+ if len(parts) != 2 {
+ err = errors.Errorf("Invalid datasource-header option '%s'", arg)
+ return "", "", "", err
+ }
+ datasourceAlias = parts[0]
+ name, value, err = splitHeader(parts[1])
+ return datasourceAlias, name, value, err
+}
+
+func splitHeader(header string) (name, value string, err error) {
+ parts := strings.SplitN(header, ":", 2)
+ if len(parts) != 2 {
+ err = errors.Errorf("Invalid HTTP Header format '%s'", header)
+ return "", "", err
+ }
+ name = http.CanonicalHeaderKey(parts[0])
+ value = parts[1]
+ return name, value, nil
+}
diff --git a/data/datasource_http_test.go b/data/datasource_http_test.go
new file mode 100644
index 00000000..62531241
--- /dev/null
+++ b/data/datasource_http_test.go
@@ -0,0 +1,220 @@
+package data
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func must(r interface{}, err error) interface{} {
+ if err != nil {
+ panic(err)
+ }
+ return r
+}
+
+func setupHTTP(code int, mimetype string, body string) (*httptest.Server, *http.Client) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ w.Header().Set("Content-Type", mimetype)
+ w.WriteHeader(code)
+ if body == "" {
+ // mirror back the headers
+ fmt.Fprintln(w, must(marshalObj(r.Header, json.Marshal)))
+ } else {
+ fmt.Fprintln(w, body)
+ }
+ }))
+
+ client := &http.Client{
+ Transport: &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ },
+ }
+
+ return server, client
+}
+
+func TestHTTPFile(t *testing.T) {
+ server, client := setupHTTP(200, "application/json; charset=utf-8", `{"hello": "world"}`)
+ defer server.Close()
+
+ sources := make(map[string]*Source)
+ sources["foo"] = &Source{
+ Alias: "foo",
+ URL: &url.URL{
+ Scheme: "http",
+ Host: "example.com",
+ Path: "/foo",
+ },
+ hc: client,
+ }
+ data := &Data{
+ Sources: sources,
+ }
+
+ expected := map[string]interface{}{
+ "hello": "world",
+ }
+
+ actual, err := data.Datasource("foo")
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+
+ actual, err = data.Datasource(server.URL)
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+}
+
+func TestHTTPFileWithHeaders(t *testing.T) {
+ server, client := setupHTTP(200, jsonMimetype, "")
+ defer server.Close()
+
+ sources := make(map[string]*Source)
+ sources["foo"] = &Source{
+ Alias: "foo",
+ URL: &url.URL{
+ Scheme: "http",
+ Host: "example.com",
+ Path: "/foo",
+ },
+ hc: client,
+ header: http.Header{
+ "Foo": {"bar"},
+ "foo": {"baz"},
+ "User-Agent": {},
+ "Accept-Encoding": {"test"},
+ },
+ }
+ data := &Data{
+ Sources: sources,
+ }
+ expected := http.Header{
+ "Accept-Encoding": {"test"},
+ "Foo": {"bar", "baz"},
+ }
+ actual, err := data.Datasource("foo")
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+
+ expected = http.Header{
+ "Accept-Encoding": {"test"},
+ "Foo": {"bar", "baz"},
+ "User-Agent": {"Go-http-client/1.1"},
+ }
+ data = &Data{
+ Sources: sources,
+ extraHeaders: map[string]http.Header{server.URL: expected},
+ }
+ actual, err = data.Datasource(server.URL)
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+}
+
+func TestParseHeaderArgs(t *testing.T) {
+ args := []string{
+ "foo=Accept: application/json",
+ "bar=Authorization: Bearer supersecret",
+ }
+ expected := map[string]http.Header{
+ "foo": {
+ "Accept": {jsonMimetype},
+ },
+ "bar": {
+ "Authorization": {"Bearer supersecret"},
+ },
+ }
+ parsed, err := parseHeaderArgs(args)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ _, err = parseHeaderArgs([]string{"foo"})
+ assert.Error(t, err)
+
+ _, err = parseHeaderArgs([]string{"foo=bar"})
+ assert.Error(t, err)
+
+ args = []string{
+ "foo=Accept: application/json",
+ "foo=Foo: bar",
+ "foo=foo: baz",
+ "foo=fOO: qux",
+ "bar=Authorization: Bearer supersecret",
+ }
+ expected = map[string]http.Header{
+ "foo": {
+ "Accept": {jsonMimetype},
+ "Foo": {"bar", "baz", "qux"},
+ },
+ "bar": {
+ "Authorization": {"Bearer supersecret"},
+ },
+ }
+ parsed, err = parseHeaderArgs(args)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+}
+
+func TestHTTPFileWithSubPath(t *testing.T) {
+ server, client := setupHTTP(200, "application/json; charset=utf-8", `{"hello": "world"}`)
+ defer server.Close()
+
+ sources := make(map[string]*Source)
+ sources["foo"] = &Source{
+ Alias: "foo",
+ URL: &url.URL{
+ Scheme: "http",
+ Host: "example.com",
+ Path: "/foo",
+ },
+ hc: client,
+ }
+ data := &Data{
+ Sources: sources,
+ }
+
+ expected := map[string]interface{}{
+ "hello": "world",
+ }
+
+ actual, err := data.Datasource("foo")
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+
+ actual, err = data.Datasource(server.URL)
+ assert.NoError(t, err)
+ assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
+}
+
+func TestBuildURL(t *testing.T) {
+ expected := "https://example.com/index.html"
+ base := mustParseURL(expected)
+ u, err := buildURL(base)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, u.String())
+
+ expected = "https://example.com/index.html"
+ base = mustParseURL("https://example.com")
+ u, err = buildURL(base, "index.html")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, u.String())
+
+ expected = "https://example.com/a/b/c/index.html"
+ base = mustParseURL("https://example.com/a/")
+ u, err = buildURL(base, "b/c/index.html")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, u.String())
+
+ expected = "https://example.com/bar/baz/index.html"
+ base = mustParseURL("https://example.com/foo")
+ u, err = buildURL(base, "bar/baz/index.html")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, u.String())
+}
diff --git a/data/datasource_test.go b/data/datasource_test.go
index 2d966f52..a3075945 100644
--- a/data/datasource_test.go
+++ b/data/datasource_test.go
@@ -3,10 +3,7 @@
package data
import (
- "encoding/json"
"fmt"
- "net/http"
- "net/http/httptest"
"net/url"
"strings"
"testing"
@@ -148,157 +145,6 @@ func TestDatasourceExists(t *testing.T) {
assert.False(t, data.DatasourceExists("bar"))
}
-func must(r interface{}, err error) interface{} {
- if err != nil {
- panic(err)
- }
- return r
-}
-
-func setupHTTP(code int, mimetype string, body string) (*httptest.Server, *http.Client) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
- w.Header().Set("Content-Type", mimetype)
- w.WriteHeader(code)
- if body == "" {
- // mirror back the headers
- fmt.Fprintln(w, must(marshalObj(r.Header, json.Marshal)))
- } else {
- fmt.Fprintln(w, body)
- }
- }))
-
- client := &http.Client{
- Transport: &http.Transport{
- Proxy: func(req *http.Request) (*url.URL, error) {
- return url.Parse(server.URL)
- },
- },
- }
-
- return server, client
-}
-
-func TestHTTPFile(t *testing.T) {
- server, client := setupHTTP(200, "application/json; charset=utf-8", `{"hello": "world"}`)
- defer server.Close()
-
- sources := make(map[string]*Source)
- sources["foo"] = &Source{
- Alias: "foo",
- URL: &url.URL{
- Scheme: "http",
- Host: "example.com",
- Path: "/foo",
- },
- hc: client,
- }
- data := &Data{
- Sources: sources,
- }
-
- expected := map[string]interface{}{
- "hello": "world",
- }
-
- actual, err := data.Datasource("foo")
- assert.NoError(t, err)
- assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
-
- actual, err = data.Datasource(server.URL)
- assert.NoError(t, err)
- assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
-}
-
-func TestHTTPFileWithHeaders(t *testing.T) {
- server, client := setupHTTP(200, jsonMimetype, "")
- defer server.Close()
-
- sources := make(map[string]*Source)
- sources["foo"] = &Source{
- Alias: "foo",
- URL: &url.URL{
- Scheme: "http",
- Host: "example.com",
- Path: "/foo",
- },
- hc: client,
- header: http.Header{
- "Foo": {"bar"},
- "foo": {"baz"},
- "User-Agent": {},
- "Accept-Encoding": {"test"},
- },
- }
- data := &Data{
- Sources: sources,
- }
- expected := http.Header{
- "Accept-Encoding": {"test"},
- "Foo": {"bar", "baz"},
- }
- actual, err := data.Datasource("foo")
- assert.NoError(t, err)
- assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
-
- expected = http.Header{
- "Accept-Encoding": {"test"},
- "Foo": {"bar", "baz"},
- "User-Agent": {"Go-http-client/1.1"},
- }
- data = &Data{
- Sources: sources,
- extraHeaders: map[string]http.Header{server.URL: expected},
- }
- actual, err = data.Datasource(server.URL)
- assert.NoError(t, err)
- assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
-}
-
-func TestParseHeaderArgs(t *testing.T) {
- args := []string{
- "foo=Accept: application/json",
- "bar=Authorization: Bearer supersecret",
- }
- expected := map[string]http.Header{
- "foo": {
- "Accept": {jsonMimetype},
- },
- "bar": {
- "Authorization": {"Bearer supersecret"},
- },
- }
- parsed, err := parseHeaderArgs(args)
- assert.NoError(t, err)
- assert.Equal(t, expected, parsed)
-
- _, err = parseHeaderArgs([]string{"foo"})
- assert.Error(t, err)
-
- _, err = parseHeaderArgs([]string{"foo=bar"})
- assert.Error(t, err)
-
- args = []string{
- "foo=Accept: application/json",
- "foo=Foo: bar",
- "foo=foo: baz",
- "foo=fOO: qux",
- "bar=Authorization: Bearer supersecret",
- }
- expected = map[string]http.Header{
- "foo": {
- "Accept": {jsonMimetype},
- "Foo": {"bar", "baz", "qux"},
- },
- "bar": {
- "Authorization": {"Bearer supersecret"},
- },
- }
- parsed, err = parseHeaderArgs(args)
- assert.NoError(t, err)
- assert.Equal(t, expected, parsed)
-}
-
func TestInclude(t *testing.T) {
ext := "txt"
contents := "hello world"
@@ -417,3 +263,15 @@ func TestMimeType(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "application/yaml", mt)
}
+
+func TestQueryParse(t *testing.T) {
+ expected := &url.URL{
+ Scheme: "http",
+ Host: "example.com",
+ Path: "/foo.json",
+ RawQuery: "bar",
+ }
+ u, err := parseSourceURL("http://example.com/foo.json?bar")
+ assert.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+}