summaryrefslogtreecommitdiff
path: root/data
diff options
context:
space:
mode:
Diffstat (limited to 'data')
-rw-r--r--data/data.go507
-rw-r--r--data/data_test.go777
-rw-r--r--data/datafuncs.go98
-rw-r--r--data/datasource.go401
-rw-r--r--data/datasource_aws_sm.go87
-rw-r--r--data/datasource_aws_sm_test.go177
-rw-r--r--data/datasource_awssmp.go77
-rw-r--r--data/datasource_awssmp_test.go144
-rw-r--r--data/datasource_blob.go173
-rw-r--r--data/datasource_blob_test.go133
-rw-r--r--data/datasource_consul.go39
-rw-r--r--data/datasource_env.go20
-rw-r--r--data/datasource_env_test.go53
-rw-r--r--data/datasource_file.go87
-rw-r--r--data/datasource_file_test.go69
-rw-r--r--data/datasource_git.go328
-rw-r--r--data/datasource_git_test.go551
-rw-r--r--data/datasource_http.go62
-rw-r--r--data/datasource_http_test.go149
-rw-r--r--data/datasource_merge.go100
-rw-r--r--data/datasource_merge_test.go162
-rw-r--r--data/datasource_stdin.go32
-rw-r--r--data/datasource_stdin_test.go23
-rw-r--r--data/datasource_test.go253
-rw-r--r--data/datasource_vault.go47
-rw-r--r--data/datasource_vault_test.go51
-rw-r--r--data/mimetypes.go7
-rw-r--r--data/mimetypes_test.go2
28 files changed, 408 insertions, 4201 deletions
diff --git a/data/data.go b/data/data.go
deleted file mode 100644
index d2fc32fb..00000000
--- a/data/data.go
+++ /dev/null
@@ -1,507 +0,0 @@
-// Package data contains functions that parse and produce data structures in
-// different formats.
-//
-// Supported formats are: JSON, YAML, TOML, and CSV.
-package data
-
-import (
- "bytes"
- "encoding/csv"
- "encoding/json"
- "fmt"
- "io"
- "strings"
-
- "cuelang.org/go/cue"
- "cuelang.org/go/cue/cuecontext"
- "cuelang.org/go/cue/format"
- "github.com/Shopify/ejson"
- ejsonJson "github.com/Shopify/ejson/json"
- "github.com/hairyhenderson/gomplate/v4/conv"
- "github.com/hairyhenderson/gomplate/v4/env"
- "github.com/joho/godotenv"
-
- // XXX: replace once https://github.com/BurntSushi/toml/pull/179 is merged
- "github.com/hairyhenderson/toml"
- "github.com/ugorji/go/codec"
-
- "github.com/hairyhenderson/yaml"
-)
-
-func unmarshalObj(obj map[string]interface{}, in string, f func([]byte, interface{}) error) (map[string]interface{}, error) {
- err := f([]byte(in), &obj)
- if err != nil {
- return nil, fmt.Errorf("unable to unmarshal object %s: %w", in, err)
- }
- return obj, nil
-}
-
-func unmarshalArray(obj []interface{}, in string, f func([]byte, interface{}) error) ([]interface{}, error) {
- err := f([]byte(in), &obj)
- if err != nil {
- return nil, fmt.Errorf("unable to unmarshal array %s: %w", in, err)
- }
- return obj, nil
-}
-
-// JSON - Unmarshal a JSON Object. Can be ejson-encrypted.
-func JSON(in string) (map[string]interface{}, error) {
- obj := make(map[string]interface{})
- out, err := unmarshalObj(obj, in, yaml.Unmarshal)
- if err != nil {
- return out, err
- }
-
- _, ok := out[ejsonJson.PublicKeyField]
- if ok {
- out, err = decryptEJSON(in)
- }
- return out, err
-}
-
-// decryptEJSON - decrypts an ejson input, and unmarshals it, stripping the _public_key field.
-func decryptEJSON(in string) (map[string]interface{}, error) {
- keyDir := env.Getenv("EJSON_KEYDIR", "/opt/ejson/keys")
- key := env.Getenv("EJSON_KEY")
-
- rIn := bytes.NewBufferString(in)
- rOut := &bytes.Buffer{}
- err := ejson.Decrypt(rIn, rOut, keyDir, key)
- if err != nil {
- return nil, err
- }
- obj := make(map[string]interface{})
- out, err := unmarshalObj(obj, rOut.String(), yaml.Unmarshal)
- if err != nil {
- return nil, err
- }
- delete(out, ejsonJson.PublicKeyField)
- return out, nil
-}
-
-// JSONArray - Unmarshal a JSON Array
-func JSONArray(in string) ([]interface{}, error) {
- obj := make([]interface{}, 1)
- return unmarshalArray(obj, in, yaml.Unmarshal)
-}
-
-// YAML - Unmarshal a YAML Object
-func YAML(in string) (map[string]interface{}, error) {
- obj := make(map[string]interface{})
- s := strings.NewReader(in)
- d := yaml.NewDecoder(s)
- for {
- err := d.Decode(&obj)
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- if obj != nil {
- break
- }
- }
-
- err := stringifyYAMLMapMapKeys(obj)
- return obj, err
-}
-
-// YAMLArray - Unmarshal a YAML Array
-func YAMLArray(in string) ([]interface{}, error) {
- obj := make([]interface{}, 1)
- s := strings.NewReader(in)
- d := yaml.NewDecoder(s)
- for {
- err := d.Decode(&obj)
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- if obj != nil {
- break
- }
- }
- err := stringifyYAMLArrayMapKeys(obj)
- return obj, err
-}
-
-// stringifyYAMLArrayMapKeys recurses into the input array and changes all
-// non-string map keys to string map keys. Modifies the input array.
-func stringifyYAMLArrayMapKeys(in []interface{}) error {
- if _, changed := stringifyMapKeys(in); changed {
- return fmt.Errorf("stringifyYAMLArrayMapKeys: output type did not match input type, this should be impossible")
- }
- return nil
-}
-
-// stringifyYAMLMapMapKeys recurses into the input map and changes all
-// non-string map keys to string map keys. Modifies the input map.
-func stringifyYAMLMapMapKeys(in map[string]interface{}) error {
- if _, changed := stringifyMapKeys(in); changed {
- return fmt.Errorf("stringifyYAMLMapMapKeys: output type did not match input type, this should be impossible")
- }
- return nil
-}
-
-// stringifyMapKeys recurses into in and changes all instances of
-// map[interface{}]interface{} to map[string]interface{}. This is useful to
-// work around the impedance mismatch between JSON and YAML unmarshaling that's
-// described here: https://github.com/go-yaml/yaml/issues/139
-//
-// Taken and modified from https://github.com/gohugoio/hugo/blob/cdfd1c99baa22d69e865294dfcd783811f96c880/parser/metadecoders/decoder.go#L257, Apache License 2.0
-// Originally inspired by https://github.com/stripe/stripe-mock/blob/24a2bb46a49b2a416cfea4150ab95781f69ee145/mapstr.go#L13, MIT License
-func stringifyMapKeys(in interface{}) (interface{}, bool) {
- switch in := in.(type) {
- case []interface{}:
- for i, v := range in {
- if vv, replaced := stringifyMapKeys(v); replaced {
- in[i] = vv
- }
- }
- case map[string]interface{}:
- for k, v := range in {
- if vv, changed := stringifyMapKeys(v); changed {
- in[k] = vv
- }
- }
- case map[interface{}]interface{}:
- res := make(map[string]interface{})
-
- for k, v := range in {
- ks := conv.ToString(k)
- if vv, replaced := stringifyMapKeys(v); replaced {
- res[ks] = vv
- } else {
- res[ks] = v
- }
- }
- return res, true
- }
-
- return nil, false
-}
-
-// TOML - Unmarshal a TOML Object
-func TOML(in string) (interface{}, error) {
- obj := make(map[string]interface{})
- return unmarshalObj(obj, in, toml.Unmarshal)
-}
-
-// dotEnv - Unmarshal a dotenv file
-func dotEnv(in string) (interface{}, error) {
- env, err := godotenv.Unmarshal(in)
- if err != nil {
- return nil, err
- }
- out := make(map[string]interface{})
- for k, v := range env {
- out[k] = v
- }
- return out, nil
-}
-
-func parseCSV(args ...string) ([][]string, []string, error) {
- in, delim, hdr := csvParseArgs(args...)
- c := csv.NewReader(strings.NewReader(in))
- c.Comma = rune(delim[0])
- records, err := c.ReadAll()
- if err != nil {
- return nil, nil, err
- }
- if len(records) > 0 {
- if hdr == nil {
- hdr = records[0]
- records = records[1:]
- } else if len(hdr) == 0 {
- hdr = make([]string, len(records[0]))
- for i := range hdr {
- hdr[i] = autoIndex(i)
- }
- }
- }
- return records, hdr, nil
-}
-
-func csvParseArgs(args ...string) (in, delim string, hdr []string) {
- delim = ","
- switch len(args) {
- case 1:
- in = args[0]
- case 2:
- in = args[1]
- switch len(args[0]) {
- case 1:
- delim = args[0]
- case 0:
- hdr = []string{}
- default:
- hdr = strings.Split(args[0], delim)
- }
- case 3:
- delim = args[0]
- hdr = strings.Split(args[1], delim)
- in = args[2]
- }
- return in, delim, hdr
-}
-
-// autoIndex - calculates a default string column name given a numeric value
-func autoIndex(i int) string {
- s := &strings.Builder{}
- for n := 0; n <= i/26; n++ {
- s.WriteRune('A' + rune(i%26))
- }
- return s.String()
-}
-
-// CSV - Unmarshal CSV
-// parameters:
-//
-// delim - (optional) the (single-character!) field delimiter, defaults to ","
-// in - the CSV-format string to parse
-//
-// returns:
-//
-// an array of rows, which are arrays of cells (strings)
-func CSV(args ...string) ([][]string, error) {
- records, hdr, err := parseCSV(args...)
- if err != nil {
- return nil, err
- }
- records = append(records, nil)
- copy(records[1:], records)
- records[0] = hdr
- return records, nil
-}
-
-// CSVByRow - Unmarshal CSV in a row-oriented form
-// parameters:
-//
-// delim - (optional) the (single-character!) field delimiter, defaults to ","
-// hdr - (optional) list of column names separated by `delim`,
-// set to "" to get auto-named columns (A-Z), omit
-// to use the first line
-// in - the CSV-format string to parse
-//
-// returns:
-//
-// an array of rows, indexed by the header name
-func CSVByRow(args ...string) (rows []map[string]string, err error) {
- records, hdr, err := parseCSV(args...)
- if err != nil {
- return nil, err
- }
- for _, record := range records {
- m := make(map[string]string)
- for i, v := range record {
- m[hdr[i]] = v
- }
- rows = append(rows, m)
- }
- return rows, nil
-}
-
-// CSVByColumn - Unmarshal CSV in a Columnar form
-// parameters:
-//
-// delim - (optional) the (single-character!) field delimiter, defaults to ","
-// hdr - (optional) list of column names separated by `delim`,
-// set to "" to get auto-named columns (A-Z), omit
-// to use the first line
-// in - the CSV-format string to parse
-//
-// returns:
-//
-// a map of columns, indexed by the header name. values are arrays of strings
-func CSVByColumn(args ...string) (cols map[string][]string, err error) {
- records, hdr, err := parseCSV(args...)
- if err != nil {
- return nil, err
- }
- cols = make(map[string][]string)
- for _, record := range records {
- for i, v := range record {
- cols[hdr[i]] = append(cols[hdr[i]], v)
- }
- }
- return cols, nil
-}
-
-// ToCSV -
-func ToCSV(args ...interface{}) (string, error) {
- delim := ","
- var in [][]string
- if len(args) == 2 {
- var ok bool
- delim, ok = args[0].(string)
- if !ok {
- return "", fmt.Errorf("can't parse ToCSV delimiter (%v) - must be string (is a %T)", args[0], args[0])
- }
- args = args[1:]
- }
- if len(args) == 1 {
- switch a := args[0].(type) {
- case [][]string:
- in = a
- case [][]interface{}:
- in = make([][]string, len(a))
- for i, v := range a {
- in[i] = conv.ToStrings(v...)
- }
- case []interface{}:
- in = make([][]string, len(a))
- for i, v := range a {
- ar, ok := v.([]interface{})
- if !ok {
- return "", fmt.Errorf("can't parse ToCSV input - must be a two-dimensional array (like [][]string or [][]interface{}) (was %T)", args[0])
- }
- in[i] = conv.ToStrings(ar...)
- }
- default:
- return "", fmt.Errorf("can't parse ToCSV input - must be a two-dimensional array (like [][]string or [][]interface{}) (was %T)", args[0])
- }
- }
- b := &bytes.Buffer{}
- c := csv.NewWriter(b)
- c.Comma = rune(delim[0])
- // We output RFC4180 CSV, so force this to CRLF
- c.UseCRLF = true
- err := c.WriteAll(in)
- if err != nil {
- return "", err
- }
- return b.String(), nil
-}
-
-func marshalObj(obj interface{}, f func(interface{}) ([]byte, error)) (string, error) {
- b, err := f(obj)
- if err != nil {
- return "", fmt.Errorf("unable to marshal object %s: %w", obj, err)
- }
-
- return string(b), nil
-}
-
-func toJSONBytes(in interface{}) ([]byte, error) {
- h := &codec.JsonHandle{}
- h.Canonical = true
- buf := new(bytes.Buffer)
- err := codec.NewEncoder(buf, h).Encode(in)
- if err != nil {
- return nil, fmt.Errorf("unable to marshal %s: %w", in, err)
- }
- return buf.Bytes(), nil
-}
-
-// ToJSON - Stringify a struct as JSON
-func ToJSON(in interface{}) (string, error) {
- s, err := toJSONBytes(in)
- if err != nil {
- return "", err
- }
- return string(s), nil
-}
-
-// ToJSONPretty - Stringify a struct as JSON (indented)
-func ToJSONPretty(indent string, in interface{}) (string, error) {
- out := new(bytes.Buffer)
- b, err := toJSONBytes(in)
- if err != nil {
- return "", err
- }
- err = json.Indent(out, b, "", indent)
- if err != nil {
- return "", fmt.Errorf("unable to indent JSON %s: %w", b, err)
- }
-
- return out.String(), nil
-}
-
-// ToYAML - Stringify a struct as YAML
-func ToYAML(in interface{}) (string, error) {
- // I'd use yaml.Marshal, but between v2 and v3 the indent has changed from
- // 2 to 4. This explicitly sets it back to 2.
- marshal := func(in interface{}) (out []byte, err error) {
- buf := &bytes.Buffer{}
- e := yaml.NewEncoder(buf)
- e.SetIndent(2)
- defer e.Close()
- err = e.Encode(in)
- return buf.Bytes(), err
- }
-
- return marshalObj(in, marshal)
-}
-
-// ToTOML - Stringify a struct as TOML
-func ToTOML(in interface{}) (string, error) {
- buf := new(bytes.Buffer)
- err := toml.NewEncoder(buf).Encode(in)
- if err != nil {
- return "", fmt.Errorf("unable to marshal %s: %w", in, err)
- }
- return buf.String(), nil
-}
-
-// CUE - Unmarshal a CUE expression into the appropriate type
-func CUE(in string) (interface{}, error) {
- cuectx := cuecontext.New()
- val := cuectx.CompileString(in)
-
- if val.Err() != nil {
- return nil, fmt.Errorf("unable to process CUE: %w", val.Err())
- }
-
- switch val.Kind() {
- case cue.StructKind:
- out := map[string]interface{}{}
- err := val.Decode(&out)
- return out, err
- case cue.ListKind:
- out := []interface{}{}
- err := val.Decode(&out)
- return out, err
- case cue.BytesKind:
- out := []byte{}
- err := val.Decode(&out)
- return out, err
- case cue.StringKind:
- out := ""
- err := val.Decode(&out)
- return out, err
- case cue.IntKind:
- out := 0
- err := val.Decode(&out)
- return out, err
- case cue.NumberKind, cue.FloatKind:
- out := 0.0
- err := val.Decode(&out)
- return out, err
- case cue.BoolKind:
- out := false
- err := val.Decode(&out)
- return out, err
- case cue.NullKind:
- return nil, nil
- default:
- return nil, fmt.Errorf("unsupported CUE type %q", val.Kind())
- }
-}
-
-func ToCUE(in interface{}) (string, error) {
- cuectx := cuecontext.New()
- v := cuectx.Encode(in)
- if v.Err() != nil {
- return "", v.Err()
- }
-
- bs, err := format.Node(v.Syntax())
- if err != nil {
- return "", err
- }
-
- return string(bs), nil
-}
diff --git a/data/data_test.go b/data/data_test.go
deleted file mode 100644
index e4f0f555..00000000
--- a/data/data_test.go
+++ /dev/null
@@ -1,777 +0,0 @@
-package data
-
-import (
- "fmt"
- "os"
- "testing"
- "time"
-
- "github.com/ugorji/go/codec"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "gotest.tools/v3/fs"
-)
-
-func TestUnmarshalObj(t *testing.T) {
- expected := map[string]interface{}{
- "foo": map[string]interface{}{"bar": "baz"},
- "one": 1.0,
- "true": true,
- "escaped": "\"/\\\b\f\n\r\t∞",
- }
-
- test := func(actual map[string]interface{}, err error) {
- t.Helper()
- require.NoError(t, err)
- assert.Equal(t, expected["foo"], actual["foo"], "foo")
- assert.Equal(t, expected["one"], actual["one"], "one")
- assert.Equal(t, expected["true"], actual["true"], "true")
- assert.Equal(t, expected["escaped"], actual["escaped"], "escaped")
- }
- test(JSON(`{"foo":{"bar":"baz"},"one":1.0,"true":true,"escaped":"\"\/\\\b\f\n\r\t\u221e"}`))
- test(YAML(`foo:
- bar: baz
-one: 1.0
-'true': true
-escaped: "\"\/\\\b\f\n\r\t\u221e"
-`))
- test(YAML(`anchor: &anchor
- bar: baz
-foo:
- <<: *anchor
-one: 1.0
-'true': true
-escaped: "\"\/\\\b\f\n\r\t\u221e"
-`))
- test(YAML(`# this comment marks an empty (nil!) document
----
-# this one too, for good measure
----
-foo:
- bar: baz
-one: 1.0
-'true': true
-escaped: "\"\/\\\b\f\n\r\t\u221e"
-`))
-
- obj := make(map[string]interface{})
- _, err := unmarshalObj(obj, "SOMETHING", func(in []byte, out interface{}) error {
- return fmt.Errorf("fail")
- })
- assert.EqualError(t, err, "unable to unmarshal object SOMETHING: fail")
-}
-
-func TestUnmarshalArray(t *testing.T) {
- expected := []interface{}{
- "foo", "bar",
- map[string]interface{}{
- "baz": map[string]interface{}{"qux": true},
- "quux": map[string]interface{}{"42": 18},
- "corge": map[string]interface{}{"false": "blah"},
- },
- }
-
- test := func(actual []interface{}, err error) {
- require.NoError(t, err)
- assert.EqualValues(t, expected, actual)
- }
- test(JSONArray(`["foo","bar",{"baz":{"qux": true},"quux":{"42":18},"corge":{"false":"blah"}}]`))
- test(YAMLArray(`
-- foo
-- bar
-- baz:
- qux: true
- quux:
- "42": 18
- corge:
- "false": blah
-`))
- test(YAMLArray(`---
-# blah blah blah ignore this!
----
-- foo
-- bar
-- baz:
- qux: true
- quux:
- "42": 18
- corge:
- "false": blah
----
-this shouldn't be reached
-`))
-
- actual, err := YAMLArray(`---
-- foo: &foo
- bar: baz
-- qux:
- <<: *foo
- quux: corge
-- baz:
- qux: true
- 42: 18
- false: blah
-`)
- require.NoError(t, err)
- assert.EqualValues(t,
- []interface{}{
- map[string]interface{}{
- "foo": map[string]interface{}{
- "bar": "baz",
- },
- },
- map[string]interface{}{
- "qux": map[string]interface{}{
- "bar": "baz",
- "quux": "corge",
- },
- },
- map[string]interface{}{
- "baz": map[string]interface{}{
- "qux": true,
- "42": 18,
- "false": "blah",
- },
- },
- },
- actual)
-
- obj := make([]interface{}, 1)
- _, err = unmarshalArray(obj, "SOMETHING", func(in []byte, out interface{}) error {
- return fmt.Errorf("fail")
- })
- assert.EqualError(t, err, "unable to unmarshal array SOMETHING: fail")
-}
-
-func TestMarshalObj(t *testing.T) {
- expected := "foo"
- actual, err := marshalObj(nil, func(in interface{}) ([]byte, error) {
- return []byte("foo"), nil
- })
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- _, err = marshalObj(nil, func(in interface{}) ([]byte, error) {
- return nil, fmt.Errorf("fail")
- })
- assert.Error(t, err)
-}
-
-func TestToJSONBytes(t *testing.T) {
- expected := []byte("null")
- actual, err := toJSONBytes(nil)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
-
- _, err = toJSONBytes(&badObject{})
- assert.Error(t, err)
-}
-
-type badObject struct{}
-
-func (b *badObject) CodecEncodeSelf(_ *codec.Encoder) {
- panic("boom")
-}
-
-func (b *badObject) CodecDecodeSelf(_ *codec.Decoder) {
-}
-
-func TestToJSON(t *testing.T) {
- expected := `{"down":{"the":{"rabbit":{"hole":true}}},"foo":"bar","one":1,"true":true}`
- in := map[string]interface{}{
- "foo": "bar",
- "one": 1,
- "true": true,
- "down": map[interface{}]interface{}{
- "the": map[interface{}]interface{}{
- "rabbit": map[interface{}]interface{}{
- "hole": true,
- },
- },
- },
- }
- out, err := ToJSON(in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- _, err = ToJSON(&badObject{})
- assert.Error(t, err)
-}
-
-func TestToJSONPretty(t *testing.T) {
- expected := `{
- "down": {
- "the": {
- "rabbit": {
- "hole": true
- }
- }
- },
- "foo": "bar",
- "one": 1,
- "true": true
-}`
- in := map[string]interface{}{
- "foo": "bar",
- "one": 1,
- "true": true,
- "down": map[string]interface{}{
- "the": map[string]interface{}{
- "rabbit": map[string]interface{}{
- "hole": true,
- },
- },
- },
- }
- out, err := ToJSONPretty(" ", in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- _, err = ToJSONPretty(" ", &badObject{})
- assert.Error(t, err)
-}
-
-func TestToYAML(t *testing.T) {
- expected := `d: 2006-01-02T15:04:05.999999999-07:00
-foo: bar
-? |-
- multi
- line
- key
-: hello: world
-one: 1
-"true": true
-`
- mst, _ := time.LoadLocation("MST")
- in := map[string]interface{}{
- "foo": "bar",
- "one": 1,
- "true": true,
- `multi
-line
-key`: map[string]interface{}{
- "hello": "world",
- },
- "d": time.Date(2006, time.January, 2, 15, 4, 5, 999999999, mst),
- }
- out, err := ToYAML(in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-}
-
-func TestCSV(t *testing.T) {
- expected := [][]string{
- {"first", "second", "third"},
- {"1", "2", "3"},
- {"4", "5", "6"},
- }
- testdata := []struct {
- args []string
- out [][]string
- }{
- {[]string{"first,second,third\n1,2,3\n4,5,6"}, expected},
- {[]string{";", "first;second;third\r\n1;2;3\r\n4;5;6\r\n"}, expected},
-
- {[]string{""}, [][]string{nil}},
- {[]string{"\n"}, [][]string{nil}},
- {[]string{"foo"}, [][]string{{"foo"}}},
- }
- for _, d := range testdata {
- out, err := CSV(d.args...)
- require.NoError(t, err)
- assert.Equal(t, d.out, out)
- }
-}
-
-func TestCSVByRow(t *testing.T) {
- in := "first,second,third\n1,2,3\n4,5,6"
- expected := []map[string]string{
- {
- "first": "1",
- "second": "2",
- "third": "3",
- },
- {
- "first": "4",
- "second": "5",
- "third": "6",
- },
- }
- testdata := []struct {
- args []string
- out []map[string]string
- }{
- {[]string{in}, expected},
- {[]string{"first,second,third", "1,2,3\n4,5,6"}, expected},
- {[]string{";", "first;second;third", "1;2;3\n4;5;6"}, expected},
- {[]string{";", "first;second;third\r\n1;2;3\r\n4;5;6"}, expected},
- {[]string{"", "1,2,3\n4,5,6"}, []map[string]string{
- {"A": "1", "B": "2", "C": "3"},
- {"A": "4", "B": "5", "C": "6"},
- }},
- {[]string{"", "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1"}, []map[string]string{
- {"A": "1", "B": "1", "C": "1", "D": "1", "E": "1", "F": "1", "G": "1", "H": "1", "I": "1", "J": "1", "K": "1", "L": "1", "M": "1", "N": "1", "O": "1", "P": "1", "Q": "1", "R": "1", "S": "1", "T": "1", "U": "1", "V": "1", "W": "1", "X": "1", "Y": "1", "Z": "1", "AA": "1", "BB": "1", "CC": "1", "DD": "1"},
- }},
- }
- for _, d := range testdata {
- out, err := CSVByRow(d.args...)
- require.NoError(t, err)
- assert.Equal(t, d.out, out)
- }
-}
-
-func TestCSVByColumn(t *testing.T) {
- expected := map[string][]string{
- "first": {"1", "4"},
- "second": {"2", "5"},
- "third": {"3", "6"},
- }
-
- testdata := []struct {
- out map[string][]string
- args []string
- }{
- {expected, []string{"first,second,third\n1,2,3\n4,5,6"}},
- {expected, []string{"first,second,third", "1,2,3\n4,5,6"}},
- {expected, []string{";", "first;second;third", "1;2;3\n4;5;6"}},
- {expected, []string{";", "first;second;third\r\n1;2;3\r\n4;5;6"}},
- {map[string][]string{
- "A": {"1", "4"},
- "B": {"2", "5"},
- "C": {"3", "6"},
- }, []string{"", "1,2,3\n4,5,6"}},
- }
- for _, d := range testdata {
- out, err := CSVByColumn(d.args...)
- require.NoError(t, err)
- assert.Equal(t, d.out, out)
- }
-}
-
-func TestAutoIndex(t *testing.T) {
- assert.Equal(t, "A", autoIndex(0))
- assert.Equal(t, "B", autoIndex(1))
- assert.Equal(t, "Z", autoIndex(25))
- assert.Equal(t, "AA", autoIndex(26))
- assert.Equal(t, "ZZ", autoIndex(51))
- assert.Equal(t, "AAA", autoIndex(52))
- assert.Equal(t, "YYYYY", autoIndex(128))
-}
-
-func TestToCSV(t *testing.T) {
- in := [][]string{
- {"first", "second", "third"},
- {"1", "2", "3"},
- {"4", "5", "6"},
- }
- expected := "first,second,third\r\n1,2,3\r\n4,5,6\r\n"
-
- out, err := ToCSV(in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- expected = "first;second;third\r\n1;2;3\r\n4;5;6\r\n"
-
- out, err = ToCSV(";", in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- _, err = ToCSV(42, [][]int{{1, 2}})
- assert.Error(t, err)
-
- _, err = ToCSV([][]int{{1, 2}})
- assert.Error(t, err)
-
- expected = "first,second,third\r\n1,2,3\r\n4,5,6\r\n"
- out, err = ToCSV([][]interface{}{
- {"first", "second", "third"},
- {"1", "2", "3"},
- {"4", "5", "6"},
- })
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- expected = "first|second|third\r\n1|2|3\r\n4|5|6\r\n"
- out, err = ToCSV("|", []interface{}{
- []interface{}{"first", "second", "third"},
- []interface{}{1, "2", 3},
- []interface{}{"4", 5, "6"},
- })
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-}
-
-func TestTOML(t *testing.T) {
- in := `# This is a TOML document. Boom.
-
-title = "TOML Example"
-
-[owner]
-name = "Tom Preston-Werner"
-organization = "GitHub"
-bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
-dob = 1979-05-27T07:32:00Z # First class dates? Why not?
-
-[database]
-server = "192.168.1.1"
-ports = [ 8001, 8001, 8002 ]
-connection_max = 5000
-enabled = true
-
-[servers]
-
- # You can indent as you please. Tabs or spaces. TOML don't care.
- [servers.alpha]
- ip = "10.0.0.1"
- dc = "eqdc10"
-
- [servers.beta]
- ip = "10.0.0.2"
- dc = "eqdc10"
-
-[clients]
-data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
-
-# Line breaks are OK when inside arrays
-hosts = [
- "alpha",
- "omega"
-]
-`
- expected := map[string]interface{}{
- "title": "TOML Example",
- "owner": map[string]interface{}{
- "name": "Tom Preston-Werner",
- "organization": "GitHub",
- "bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
- "dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
- },
- "database": map[string]interface{}{
- "server": "192.168.1.1",
- "ports": []interface{}{int64(8001), int64(8001), int64(8002)},
- "connection_max": int64(5000),
- "enabled": true,
- },
- "servers": map[string]interface{}{
- "alpha": map[string]interface{}{
- "ip": "10.0.0.1",
- "dc": "eqdc10",
- },
- "beta": map[string]interface{}{
- "ip": "10.0.0.2",
- "dc": "eqdc10",
- },
- },
- "clients": map[string]interface{}{
- "data": []interface{}{
- []interface{}{"gamma", "delta"},
- []interface{}{int64(1), int64(2)},
- },
- "hosts": []interface{}{"alpha", "omega"},
- },
- }
-
- out, err := TOML(in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-}
-
-func TestToTOML(t *testing.T) {
- expected := `foo = "bar"
-one = 1
-true = true
-
-[down]
- [down.the]
- [down.the.rabbit]
- hole = true
-`
- in := map[string]interface{}{
- "foo": "bar",
- "one": 1,
- "true": true,
- "down": map[interface{}]interface{}{
- "the": map[interface{}]interface{}{
- "rabbit": map[interface{}]interface{}{
- "hole": true,
- },
- },
- },
- }
- out, err := ToTOML(in)
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-}
-
-func TestDecryptEJSON(t *testing.T) {
- privateKey := "e282d979654f88267f7e6c2d8268f1f4314b8673579205ed0029b76de9c8223f"
- publicKey := "6e05ec625bcdca34864181cc43e6fcc20a57732a453bc2f4a2e117ffdf1a6762"
- expected := map[string]interface{}{
- "password": "supersecret",
- "_unencrypted": "notsosecret",
- }
- in := `{
- "_public_key": "` + publicKey + `",
- "password": "EJ[1:yJ7n4UorqxkJZMoKevIA1dJeDvaQhkbgENIVZW18jig=:0591iW+paVSh4APOytKBVW/ZcxHO/5wO:TssnpVtkiXmpDIxPlXSiYdgnWyd44stGcwG1]",
- "_unencrypted": "notsosecret"
- }`
-
- t.Setenv("EJSON_KEY", privateKey)
- actual, err := decryptEJSON(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, actual)
-
- actual, err = JSON(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, actual)
-
- tmpDir := fs.NewDir(t, "gomplate-ejsontest",
- fs.WithFile(publicKey, privateKey),
- )
- t.Cleanup(tmpDir.Remove)
-
- os.Unsetenv("EJSON_KEY")
- t.Setenv("EJSON_KEY_FILE", tmpDir.Join(publicKey))
- actual, err = decryptEJSON(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, actual)
-
- os.Unsetenv("EJSON_KEY")
- os.Unsetenv("EJSON_KEY_FILE")
- t.Setenv("EJSON_KEYDIR", tmpDir.Path())
- actual, err = decryptEJSON(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, actual)
-}
-
-func TestDotEnv(t *testing.T) {
- in := `FOO=a regular unquoted value
-export BAR=another value, exports are ignored
-
-# comments are totally ignored, as are blank lines
-FOO.BAR = "values can be double-quoted, and shell\nescapes are supported"
-
-BAZ = "variable expansion: ${FOO}"
-QUX='single quotes ignore $variables'
-`
- expected := map[string]interface{}{
- "FOO": "a regular unquoted value",
- "BAR": "another value, exports are ignored",
- "FOO.BAR": "values can be double-quoted, and shell\nescapes are supported",
- "BAZ": "variable expansion: a regular unquoted value",
- "QUX": "single quotes ignore $variables",
- }
- out, err := dotEnv(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, out)
-}
-
-func TestStringifyYAMLArrayMapKeys(t *testing.T) {
- cases := []struct {
- input []interface{}
- want []interface{}
- replaced bool
- }{
- {
- []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}},
- []interface{}{map[string]interface{}{"a": 1, "b": 2}},
- false,
- },
- {
- []interface{}{map[interface{}]interface{}{"a": []interface{}{1, map[interface{}]interface{}{"b": 2}}}},
- []interface{}{map[string]interface{}{"a": []interface{}{1, map[string]interface{}{"b": 2}}}},
- false,
- },
- {
- []interface{}{map[interface{}]interface{}{true: 1, "b": false}},
- []interface{}{map[string]interface{}{"true": 1, "b": false}},
- false,
- },
- {
- []interface{}{map[interface{}]interface{}{1: "a", 2: "b"}},
- []interface{}{map[string]interface{}{"1": "a", "2": "b"}},
- false,
- },
- {
- []interface{}{map[interface{}]interface{}{"a": map[interface{}]interface{}{"b": 1}}},
- []interface{}{map[string]interface{}{"a": map[string]interface{}{"b": 1}}},
- false,
- },
- {
- []interface{}{map[string]interface{}{"a": map[string]interface{}{"b": 1}}},
- []interface{}{map[string]interface{}{"a": map[string]interface{}{"b": 1}}},
- false,
- },
- {
- []interface{}{map[interface{}]interface{}{1: "a", 2: "b"}},
- []interface{}{map[string]interface{}{"1": "a", "2": "b"}},
- false,
- },
- }
-
- for _, c := range cases {
- err := stringifyYAMLArrayMapKeys(c.input)
- require.NoError(t, err)
- assert.EqualValues(t, c.want, c.input)
- }
-}
-
-func TestStringifyYAMLMapMapKeys(t *testing.T) {
- cases := []struct {
- input map[string]interface{}
- want map[string]interface{}
- }{
- {
- map[string]interface{}{"root": map[interface{}]interface{}{"a": 1, "b": 2}},
- map[string]interface{}{"root": map[string]interface{}{"a": 1, "b": 2}},
- },
- {
- map[string]interface{}{"root": map[interface{}]interface{}{"a": []interface{}{1, map[interface{}]interface{}{"b": 2}}}},
- map[string]interface{}{"root": map[string]interface{}{"a": []interface{}{1, map[string]interface{}{"b": 2}}}},
- },
- {
- map[string]interface{}{"root": map[interface{}]interface{}{true: 1, "b": false}},
- map[string]interface{}{"root": map[string]interface{}{"true": 1, "b": false}},
- },
- {
- map[string]interface{}{"root": map[interface{}]interface{}{1: "a", 2: "b"}},
- map[string]interface{}{"root": map[string]interface{}{"1": "a", "2": "b"}},
- },
- {
- map[string]interface{}{"root": map[interface{}]interface{}{"a": map[interface{}]interface{}{"b": 1}}},
- map[string]interface{}{"root": map[string]interface{}{"a": map[string]interface{}{"b": 1}}},
- },
- {
- map[string]interface{}{"a": map[string]interface{}{"b": 1}},
- map[string]interface{}{"a": map[string]interface{}{"b": 1}},
- },
- {
- map[string]interface{}{"root": []interface{}{map[interface{}]interface{}{1: "a", 2: "b"}}},
- map[string]interface{}{"root": []interface{}{map[string]interface{}{"1": "a", "2": "b"}}},
- },
- }
-
- for _, c := range cases {
- err := stringifyYAMLMapMapKeys(c.input)
- require.NoError(t, err)
- assert.EqualValues(t, c.want, c.input)
- }
-}
-
-func TestCUE(t *testing.T) {
- in := `package foo
-import "regexp"
-matches: regexp.FindSubmatch(#"^([^:]*):(\d+)$"#, "localhost:443")
-one: 1
-two: 2
-// A field using quotes.
-"two-and-a-half": 2.5
-list: [ 1, 2, 3 ]
-`
-
- expected := map[string]interface{}{
- "matches": []interface{}{
- "localhost:443",
- "localhost",
- "443",
- },
- "one": 1,
- "two": 2,
- "two-and-a-half": 2.5,
- "list": []interface{}{1, 2, 3},
- }
-
- out, err := CUE(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, out)
-
- out, err = CUE(`[1,2,3]`)
- require.NoError(t, err)
- assert.EqualValues(t, []interface{}{1, 2, 3}, out)
-
- out, err = CUE(`"hello world"`)
- require.NoError(t, err)
- assert.EqualValues(t, "hello world", out)
-
- out, err = CUE(`true`)
- require.NoError(t, err)
- assert.EqualValues(t, true, out)
-
- out, err = CUE(`'\x00\x01\x02\x03\x04'`)
- require.NoError(t, err)
- assert.EqualValues(t, []byte{0, 1, 2, 3, 4}, out)
-
- out, err = CUE(`42`)
- require.NoError(t, err)
- assert.EqualValues(t, 42, out)
-
- out, err = CUE(`42.0`)
- require.NoError(t, err)
- assert.EqualValues(t, 42.0, out)
-
- out, err = CUE(`null`)
- require.NoError(t, err)
- assert.EqualValues(t, nil, out)
-
- _, err = CUE(`>=0 & <=7 & >=3 & <=10`)
- require.Error(t, err)
-}
-
-func TestToCUE(t *testing.T) {
- in := map[string]interface{}{
- "matches": []interface{}{
- "localhost:443",
- "localhost",
- "443",
- },
- "one": 1,
- "two": 2,
- "two-and-a-half": 2.5,
- "list": []interface{}{1, 2, 3},
- }
-
- expected := `{
- "two-and-a-half": 2.5
- list: [1, 2, 3]
- two: 2
- one: 1
- matches: ["localhost:443", "localhost", "443"]
-}`
-
- out, err := ToCUE(in)
- require.NoError(t, err)
- assert.EqualValues(t, expected, out)
-
- out, err = ToCUE([]interface{}{1, 2, 3})
- require.NoError(t, err)
- assert.EqualValues(t, `[1, 2, 3]`, out)
-
- out, err = ToCUE("hello world")
- require.NoError(t, err)
- assert.EqualValues(t, `"hello world"`, out)
-
- out, err = ToCUE(true)
- require.NoError(t, err)
- assert.EqualValues(t, `true`, out)
-
- out, err = ToCUE([]byte{0, 1, 2, 3, 4})
- require.NoError(t, err)
- assert.EqualValues(t, `'\x00\x01\x02\x03\x04'`, out)
-
- out, err = ToCUE(42)
- require.NoError(t, err)
- assert.EqualValues(t, `42`, out)
-
- out, err = ToCUE(42.0)
- require.NoError(t, err)
- assert.EqualValues(t, `42.0`, out)
-
- out, err = ToCUE(nil)
- require.NoError(t, err)
- assert.EqualValues(t, `null`, out)
-
- out, err = ToCUE(struct{}{})
- require.NoError(t, err)
- assert.EqualValues(t, `{}`, out)
-}
diff --git a/data/datafuncs.go b/data/datafuncs.go
new file mode 100644
index 00000000..ccd767e1
--- /dev/null
+++ b/data/datafuncs.go
@@ -0,0 +1,98 @@
+package data
+
+import (
+ "github.com/hairyhenderson/gomplate/v4/internal/parsers"
+)
+
+// temporary aliases for parser functions while I figure out if they need to be
+// exported from the internal parsers package
+
+// JSON - Unmarshal a JSON Object. Can be ejson-encrypted.
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var JSON = parsers.JSON
+
+// JSONArray - Unmarshal a JSON Array
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var JSONArray = parsers.JSONArray
+
+// YAML - Unmarshal a YAML Object
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var YAML = parsers.YAML
+
+// YAMLArray - Unmarshal a YAML Array
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var YAMLArray = parsers.YAMLArray
+
+// TOML - Unmarshal a TOML Object
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var TOML = parsers.TOML
+
+// CSV - Unmarshal CSV
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var CSV = parsers.CSV
+
+// CSVByRow - Unmarshal CSV in a row-oriented form
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var CSVByRow = parsers.CSVByRow
+
+// CSVByColumn - Unmarshal CSV in a Columnar form
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var CSVByColumn = parsers.CSVByColumn
+
+// ToCSV -
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToCSV = parsers.ToCSV
+
+// ToJSON - Stringify a struct as JSON
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToJSON = parsers.ToJSON
+
+// ToJSONPretty - Stringify a struct as JSON (indented)
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToJSONPretty = parsers.ToJSONPretty
+
+// ToYAML - Stringify a struct as YAML
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToYAML = parsers.ToYAML
+
+// ToTOML - Stringify a struct as TOML
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToTOML = parsers.ToTOML
+
+// CUE - Unmarshal a CUE expression into the appropriate type
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var CUE = parsers.CUE
+
+// ToCUE - Stringify a struct as CUE
+//
+// Deprecated: will be removed in a future version of gomplate. If you have a
+// need for this, please open an issue!
+var ToCUE = parsers.ToCUE
diff --git a/data/datasource.go b/data/datasource.go
index fe3f0877..27260ac1 100644
--- a/data/datasource.go
+++ b/data/datasource.go
@@ -2,98 +2,43 @@ package data
import (
"context"
+ "encoding/json"
"fmt"
+ "io"
"io/fs"
- "mime"
"net/http"
"net/url"
- "path/filepath"
+ "runtime"
"sort"
"strings"
+ "github.com/hairyhenderson/go-fsimpl"
"github.com/hairyhenderson/gomplate/v4/internal/config"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
- "github.com/hairyhenderson/gomplate/v4/libkv"
- "github.com/hairyhenderson/gomplate/v4/vault"
+ "github.com/hairyhenderson/gomplate/v4/internal/parsers"
+ "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
)
-func regExtension(ext, typ string) {
- err := mime.AddExtensionType(ext, typ)
- if err != nil {
- panic(err)
- }
-}
-
-func init() {
- // Add some types we want to be able to handle which can be missing by default
- regExtension(".json", jsonMimetype)
- regExtension(".yml", yamlMimetype)
- regExtension(".yaml", yamlMimetype)
- regExtension(".csv", csvMimetype)
- regExtension(".toml", tomlMimetype)
- regExtension(".env", envMimetype)
- regExtension(".cue", cueMimetype)
-}
-
-// registerReaders registers the source-reader functions
-func (d *Data) registerReaders() {
- d.sourceReaders = make(map[string]func(context.Context, *Source, ...string) ([]byte, error))
-
- d.sourceReaders["aws+smp"] = readAWSSMP
- d.sourceReaders["aws+sm"] = readAWSSecretsManager
- d.sourceReaders["consul"] = readConsul
- d.sourceReaders["consul+http"] = readConsul
- d.sourceReaders["consul+https"] = readConsul
- d.sourceReaders["env"] = readEnv
- d.sourceReaders["file"] = readFile
- d.sourceReaders["http"] = readHTTP
- d.sourceReaders["https"] = readHTTP
- d.sourceReaders["merge"] = d.readMerge
- d.sourceReaders["stdin"] = readStdin
- d.sourceReaders["vault"] = readVault
- d.sourceReaders["vault+http"] = readVault
- d.sourceReaders["vault+https"] = readVault
- d.sourceReaders["s3"] = readBlob
- d.sourceReaders["gs"] = readBlob
- d.sourceReaders["git"] = readGit
- d.sourceReaders["git+file"] = readGit
- d.sourceReaders["git+http"] = readGit
- d.sourceReaders["git+https"] = readGit
- d.sourceReaders["git+ssh"] = readGit
-}
-
-// lookupReader - return the reader function for the given scheme. Empty scheme
-// will return the file reader.
-func (d *Data) lookupReader(scheme string) (func(context.Context, *Source, ...string) ([]byte, error), error) {
- if d.sourceReaders == nil {
- d.registerReaders()
- }
- if scheme == "" {
- scheme = "file"
- }
-
- r, ok := d.sourceReaders[scheme]
- if !ok {
- return nil, fmt.Errorf("scheme %s not registered", scheme)
- }
- return r, nil
-}
-
// Data -
//
// Deprecated: will be replaced in future
type Data struct {
Ctx context.Context
+ // TODO: remove this before 4.0
Sources map[string]*Source
- sourceReaders map[string]func(context.Context, *Source, ...string) ([]byte, error)
- cache map[string][]byte
+ cache map[string]*fileContent
// headers from the --datasource-header/-H option that don't reference datasources from the commandline
ExtraHeaders map[string]http.Header
}
+type fileContent struct {
+ contentType string
+ b []byte
+}
+
// Cleanup - clean up datasources before shutting the process down - things
// like Logging out happen here
func (d *Data) Cleanup() {
@@ -119,7 +64,7 @@ func NewData(datasourceArgs, headerArgs []string) (*Data, error) {
func FromConfig(ctx context.Context, cfg *config.Config) *Data {
// XXX: This is temporary, and will be replaced with something a bit cleaner
// when datasources are refactored
- ctx = ContextWithStdin(ctx, cfg.Stdin)
+ ctx = datafs.ContextWithStdin(ctx, cfg.Stdin)
sources := map[string]*Source{}
for alias, d := range cfg.DataSources {
@@ -147,89 +92,17 @@ func FromConfig(ctx context.Context, cfg *config.Config) *Data {
//
// Deprecated: will be replaced in future
type Source struct {
- Alias string
- URL *url.URL
- Header http.Header // used for http[s]: URLs, nil otherwise
- fs fs.FS // used for file: URLs, nil otherwise
- hc *http.Client // used for http[s]: URLs, nil otherwise
- vc *vault.Vault // used for vault: URLs, nil otherwise
- kv *libkv.LibKV // used for consul:, etcd:, zookeeper: URLs, nil otherwise
- asmpg awssmpGetter // used for aws+smp:, nil otherwise
- awsSecretsManager awsSecretsManagerGetter // used for aws+sm, nil otherwise
- mediaType string
-}
-
-func (s *Source) inherit(parent *Source) {
- s.fs = parent.fs
- s.hc = parent.hc
- s.vc = parent.vc
- s.kv = parent.kv
- s.asmpg = parent.asmpg
+ Alias string
+ URL *url.URL
+ Header http.Header // used for http[s]: URLs, nil otherwise
+ mediaType string
}
+// Deprecated: no-op
func (s *Source) cleanup() {
- if s.vc != nil {
- s.vc.Logout()
- }
- if s.kv != nil {
- s.kv.Logout()
- }
-}
-
-// mimeType returns the MIME type to use as a hint for parsing the datasource.
-// It's expected that the datasource will have already been read before
-// this function is called, and so the Source's Type property may be already set.
-//
-// The MIME type is determined by these rules:
-// 1. the 'type' URL query parameter is used if present
-// 2. otherwise, the Type property on the Source is used, if present
-// 3. otherwise, a MIME type is calculated from the file extension, if the extension is registered
-// 4. otherwise, the default type of 'text/plain' is used
-func (s *Source) mimeType(arg string) (mimeType string, err error) {
- if len(arg) > 0 {
- if strings.HasPrefix(arg, "//") {
- arg = arg[1:]
- }
- if !strings.HasPrefix(arg, "/") {
- arg = "/" + arg
- }
- }
- argURL, err := url.Parse(arg)
- if err != nil {
- return "", fmt.Errorf("mimeType: couldn't parse arg %q: %w", arg, err)
- }
- mediatype := argURL.Query().Get("type")
- if mediatype == "" {
- mediatype = s.URL.Query().Get("type")
- }
-
- if mediatype == "" {
- mediatype = s.mediaType
- }
-
- // make it so + doesn't need to be escaped
- mediatype = strings.ReplaceAll(mediatype, " ", "+")
-
- if mediatype == "" {
- ext := filepath.Ext(argURL.Path)
- mediatype = mime.TypeByExtension(ext)
- }
-
- if mediatype == "" {
- ext := filepath.Ext(s.URL.Path)
- mediatype = mime.TypeByExtension(ext)
- }
-
- if mediatype != "" {
- t, _, err := mime.ParseMediaType(mediatype)
- if err != nil {
- return "", fmt.Errorf("MIME type was %q: %w", mediatype, err)
- }
- mediatype = t
- return mediatype, nil
- }
-
- return textMimetype, nil
+ // if s.kv != nil {
+ // s.kv.Logout()
+ // }
}
// String is the method to format the flag's value, part of the flag.Value interface.
@@ -246,7 +119,7 @@ func (d *Data) DefineDatasource(alias, value string) (string, error) {
if d.DatasourceExists(alias) {
return "", nil
}
- srcURL, err := datafs.ParseSourceURL(value)
+ srcURL, err := urlhelpers.ParseSourceURL(value)
if err != nil {
return "", err
}
@@ -288,73 +161,37 @@ func (d *Data) lookupSource(alias string) (*Source, error) {
return source, nil
}
-func (d *Data) readDataSource(ctx context.Context, alias string, args ...string) (data, mimeType string, err error) {
+func (d *Data) readDataSource(ctx context.Context, alias string, args ...string) (*fileContent, error) {
source, err := d.lookupSource(alias)
if err != nil {
- return "", "", err
+ return nil, err
}
- b, err := d.readSource(ctx, source, args...)
+ fc, err := d.readSource(ctx, source, args...)
if err != nil {
- return "", "", fmt.Errorf("couldn't read datasource '%s': %w", alias, err)
+ return nil, fmt.Errorf("couldn't read datasource '%s': %w", alias, err)
}
- subpath := ""
- if len(args) > 0 {
- subpath = args[0]
- }
- mimeType, err = source.mimeType(subpath)
- if err != nil {
- return "", "", err
- }
- return string(b), mimeType, nil
+ return fc, nil
}
// Include -
func (d *Data) Include(alias string, args ...string) (string, error) {
- data, _, err := d.readDataSource(d.Ctx, alias, args...)
- return data, err
+ fc, err := d.readDataSource(d.Ctx, alias, args...)
+ if err != nil {
+ return "", err
+ }
+
+ return string(fc.b), err
}
// Datasource -
func (d *Data) Datasource(alias string, args ...string) (interface{}, error) {
- data, mimeType, err := d.readDataSource(d.Ctx, alias, args...)
+ fc, err := d.readDataSource(d.Ctx, alias, args...)
if err != nil {
return nil, err
}
- return parseData(mimeType, data)
-}
-
-func parseData(mimeType, s string) (out interface{}, err error) {
- switch mimeAlias(mimeType) {
- case jsonMimetype:
- out, err = JSON(s)
- if err != nil {
- // maybe it's a JSON array
- out, err = JSONArray(s)
- }
- case jsonArrayMimetype:
- out, err = JSONArray(s)
- case yamlMimetype:
- out, err = YAML(s)
- if err != nil {
- // maybe it's a YAML array
- out, err = YAMLArray(s)
- }
- case csvMimetype:
- out, err = CSV(s)
- case tomlMimetype:
- out, err = TOML(s)
- case envMimetype:
- out, err = dotEnv(s)
- case textMimetype:
- out = s
- case cueMimetype:
- out, err = CUE(s)
- default:
- return nil, fmt.Errorf("datasources of type %s not yet supported", mimeType)
- }
- return out, err
+ return parsers.ParseData(fc.contentType, string(fc.b))
}
// DatasourceReachable - Determines if the named datasource is reachable with
@@ -370,9 +207,9 @@ func (d *Data) DatasourceReachable(alias string, args ...string) bool {
// readSource returns the (possibly cached) data from the given source,
// as referenced by the given args
-func (d *Data) readSource(ctx context.Context, source *Source, args ...string) ([]byte, error) {
+func (d *Data) readSource(ctx context.Context, source *Source, args ...string) (*fileContent, error) {
if d.cache == nil {
- d.cache = make(map[string][]byte)
+ d.cache = make(map[string]*fileContent)
}
cacheKey := source.Alias
for _, v := range args {
@@ -382,16 +219,107 @@ func (d *Data) readSource(ctx context.Context, source *Source, args ...string) (
if ok {
return cached, nil
}
- r, err := d.lookupReader(source.URL.Scheme)
- if err != nil {
- return nil, fmt.Errorf("Datasource not yet supported")
+
+ arg := ""
+ if len(args) > 0 {
+ arg = args[0]
}
- data, err := r(ctx, source, args...)
+ u, err := resolveURL(source.URL, arg)
if err != nil {
return nil, err
}
- d.cache[cacheKey] = data
- return data, nil
+
+ fc, err := d.readFileContent(ctx, u, source.Header)
+ if err != nil {
+ return nil, fmt.Errorf("reading %s: %w", u, err)
+ }
+ d.cache[cacheKey] = fc
+ return fc, nil
+}
+
+// readFileContent returns content from the given URL
+func (d Data) readFileContent(ctx context.Context, u *url.URL, hdr http.Header) (*fileContent, error) {
+ fsys, err := datafs.FSysForPath(ctx, u.String())
+ if err != nil {
+ return nil, fmt.Errorf("fsys for path %v: %w", u, err)
+ }
+
+ u, fname := datafs.SplitFSMuxURL(u)
+
+ // need to support absolute paths on local filesystem too
+ // TODO: this is a hack, probably fix this?
+ if u.Scheme == "file" && runtime.GOOS != "windows" {
+ fname = u.Path + fname
+ }
+
+ fsys = fsimpl.WithContextFS(ctx, fsys)
+ fsys = fsimpl.WithHeaderFS(hdr, fsys)
+
+ // convert d.Sources to a map[string]config.DataSources
+ // TODO: remove this when d.Sources is removed
+ ds := make(map[string]config.DataSource)
+ for k, v := range d.Sources {
+ ds[k] = config.DataSource{
+ URL: v.URL,
+ Header: v.Header,
+ }
+ }
+
+ fsys = datafs.WithDataSourcesFS(ds, fsys)
+
+ f, err := fsys.Open(fname)
+ if err != nil {
+ return nil, fmt.Errorf("open (url: %q, name: %q): %w", u, fname, err)
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, fmt.Errorf("stat (url: %q, name: %q): %w", u, fname, err)
+ }
+
+ // possible type hint in the type query param. Contrary to spec, we allow
+ // unescaped '+' characters to make it simpler to provide types like
+ // "application/array+json"
+ mimeType := u.Query().Get("type")
+ mimeType = strings.ReplaceAll(mimeType, " ", "+")
+
+ if mimeType == "" {
+ mimeType = fsimpl.ContentType(fi)
+ }
+
+ var data []byte
+
+ if fi.IsDir() {
+ var dirents []fs.DirEntry
+ dirents, err = fs.ReadDir(fsys, fname)
+ if err != nil {
+ return nil, fmt.Errorf("readDir (url: %q, name: %s): %w", u, fname, err)
+ }
+
+ entries := make([]string, len(dirents))
+ for i, e := range dirents {
+ entries[i] = e.Name()
+ }
+ data, err = json.Marshal(entries)
+ if err != nil {
+ return nil, fmt.Errorf("json.Marshal: %w", err)
+ }
+
+ mimeType = jsonArrayMimetype
+ } else {
+ data, err = io.ReadAll(f)
+ if err != nil {
+ return nil, fmt.Errorf("read (url: %q, name: %s): %w", u, fname, err)
+ }
+ }
+
+ if mimeType == "" {
+ // default to text/plain
+ mimeType = textMimetype
+ }
+
+ return &fileContent{contentType: mimeType, b: data}, nil
}
// Show all datasources -
@@ -403,3 +331,62 @@ func (d *Data) ListDatasources() []string {
sort.Strings(datasources)
return datasources
}
+
+// resolveURL parses the relative URL rel against base, and returns the
+// resolved URL. Differs from url.ResolveReference in that query parameters are
+// added. In case of duplicates, params from rel are used.
+func resolveURL(base *url.URL, rel string) (*url.URL, error) {
+ // if there's an opaque part, there's no resolving to do - just return the
+ // base URL
+ if base.Opaque != "" {
+ return base, nil
+ }
+
+ // git URLs are special - they have double-slashes that separate a repo
+ // from a path in the repo. A missing double-slash means the path is the
+ // root.
+ switch base.Scheme {
+ case "git", "git+file", "git+http", "git+https", "git+ssh":
+ if strings.Contains(base.Path, "//") && strings.Contains(rel, "//") {
+ return nil, fmt.Errorf("both base URL and subpath contain '//', which is not allowed in git URLs")
+ }
+
+ // If there's a subpath, the base path must end with '/'. This behaviour
+ // is unique to git URLs - other schemes would instead drop the last
+ // path element and replace with the subpath.
+ if rel != "" && !strings.HasSuffix(base.Path, "/") {
+ base.Path += "/"
+ }
+
+ // If subpath starts with '//', make it relative by prefixing a '.',
+ // otherwise it'll be treated as a schemeless URI and the first part
+ // will be interpreted as a hostname.
+ if strings.HasPrefix(rel, "//") {
+ rel = "." + rel
+ }
+ }
+
+ relURL, err := url.Parse(rel)
+ if err != nil {
+ return nil, err
+ }
+
+ // URL.ResolveReference requires (or assumes, at least) that the base is
+ // absolute. We want to support relative URLs too though, so we need to
+ // correct for that.
+ out := base.ResolveReference(relURL)
+ if out.Scheme == "" && out.Path[0] == '/' {
+ out.Path = out.Path[1:]
+ }
+
+ if base.RawQuery != "" {
+ bq := base.Query()
+ rq := relURL.Query()
+ for k := range rq {
+ bq.Set(k, rq.Get(k))
+ }
+ out.RawQuery = bq.Encode()
+ }
+
+ return out, nil
+}
diff --git a/data/datasource_aws_sm.go b/data/datasource_aws_sm.go
deleted file mode 100644
index aa42fdf9..00000000
--- a/data/datasource_aws_sm.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "net/url"
- "path"
- "strings"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/service/secretsmanager"
-
- gaws "github.com/hairyhenderson/gomplate/v4/aws"
-)
-
-// awsSecretsManagerGetter - A subset of Secrets Manager API for use in unit testing
-type awsSecretsManagerGetter interface {
- GetSecretValueWithContext(ctx context.Context, input *secretsmanager.GetSecretValueInput, opts ...request.Option) (*secretsmanager.GetSecretValueOutput, error)
-}
-
-func parseDatasourceURLArgs(sourceURL *url.URL, args ...string) (params map[string]interface{}, p string, err error) {
- if len(args) >= 2 {
- err = fmt.Errorf("maximum two arguments to %s datasource: alias, extraPath (found %d)",
- sourceURL.Scheme, len(args))
- return nil, "", err
- }
-
- p = sourceURL.Path
- params = make(map[string]interface{})
- for key, val := range sourceURL.Query() {
- params[key] = strings.Join(val, " ")
- }
-
- if p == "" && sourceURL.Opaque != "" {
- p = sourceURL.Opaque
- }
-
- if len(args) == 1 {
- parsed, err := url.Parse(args[0])
- if err != nil {
- return nil, "", err
- }
-
- if parsed.Path != "" {
- p = path.Join(p, parsed.Path)
- if strings.HasSuffix(parsed.Path, "/") {
- p += "/"
- }
- }
-
- for key, val := range parsed.Query() {
- params[key] = strings.Join(val, " ")
- }
- }
- return params, p, nil
-}
-
-func readAWSSecretsManager(ctx context.Context, source *Source, args ...string) ([]byte, error) {
- if source.awsSecretsManager == nil {
- source.awsSecretsManager = secretsmanager.New(gaws.SDKSession())
- }
-
- _, paramPath, err := parseDatasourceURLArgs(source.URL, args...)
- if err != nil {
- return nil, err
- }
-
- return readAWSSecretsManagerParam(ctx, source, paramPath)
-}
-
-func readAWSSecretsManagerParam(ctx context.Context, source *Source, paramPath string) ([]byte, error) {
- input := &secretsmanager.GetSecretValueInput{
- SecretId: aws.String(paramPath),
- }
-
- response, err := source.awsSecretsManager.GetSecretValueWithContext(ctx, input)
- if err != nil {
- return nil, fmt.Errorf("reading aws+sm source %q: %w", source.Alias, err)
- }
-
- if response.SecretString != nil {
- return []byte(*response.SecretString), nil
- }
-
- return response.SecretBinary, nil
-}
diff --git a/data/datasource_aws_sm_test.go b/data/datasource_aws_sm_test.go
deleted file mode 100644
index e6e2cef3..00000000
--- a/data/datasource_aws_sm_test.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package data
-
-import (
- "context"
- "net/url"
- "testing"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/service/secretsmanager"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// DummyAWSSecretsManagerSecretGetter - test double
-type DummyAWSSecretsManagerSecretGetter struct {
- t *testing.T
- secretValut *secretsmanager.GetSecretValueOutput
- err awserr.Error
- mockGetSecretValue func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error)
-}
-
-func (d DummyAWSSecretsManagerSecretGetter) GetSecretValueWithContext(_ context.Context, input *secretsmanager.GetSecretValueInput, _ ...request.Option) (*secretsmanager.GetSecretValueOutput, error) {
- if d.mockGetSecretValue != nil {
- output, err := d.mockGetSecretValue(input)
- return output, err
- }
- if d.err != nil {
- return nil, d.err
- }
- assert.NotNil(d.t, d.secretValut, "Must provide a param if no error!")
- return d.secretValut, nil
-}
-
-func simpleAWSSecretsManagerSourceHelper(dummyGetter awsSecretsManagerGetter) *Source {
- return &Source{
- Alias: "foo",
- URL: &url.URL{
- Scheme: "aws+sm",
- Path: "/foo",
- },
- awsSecretsManager: dummyGetter,
- }
-}
-
-func TestAWSSecretsManager_ParseAWSSecretsManagerArgs(t *testing.T) {
- _, _, err := parseDatasourceURLArgs(mustParseURL("base"), "extra", "too many!")
- assert.Error(t, err)
-
- tplain := map[string]interface{}{"type": "text/plain"}
-
- data := []struct {
- eParams map[string]interface{}
- u string
- ePath string
- args string
- }{
- {u: "noddy", ePath: "noddy"},
- {u: "base", ePath: "base/extra", args: "extra"},
- {u: "/foo/", ePath: "/foo/extra", args: "/extra"},
- {u: "aws+sm:///foo", ePath: "/foo/bar", args: "bar"},
- {u: "aws+sm:foo", ePath: "foo"},
- {u: "aws+sm:foo/bar", ePath: "foo/bar"},
- {u: "aws+sm:/foo/bar", ePath: "/foo/bar"},
- {u: "aws+sm:foo", ePath: "foo/baz", args: "baz"},
- {u: "aws+sm:foo/bar", ePath: "foo/bar/baz", args: "baz"},
- {u: "aws+sm:/foo/bar", ePath: "/foo/bar/baz", args: "baz"},
- {u: "aws+sm:///foo", ePath: "/foo/dir/", args: "dir/"},
- {u: "aws+sm:///foo/", ePath: "/foo/"},
- {u: "aws+sm:///foo/", ePath: "/foo/baz", args: "baz"},
- {eParams: tplain, u: "aws+sm:foo?type=text/plain", ePath: "foo/baz", args: "baz"},
- {eParams: tplain, u: "aws+sm:foo/bar?type=text/plain", ePath: "foo/bar/baz", args: "baz"},
- {eParams: tplain, u: "aws+sm:/foo/bar?type=text/plain", ePath: "/foo/bar/baz", args: "baz"},
- {
- eParams: map[string]interface{}{
- "type": "application/json",
- "param": "quux",
- },
- u: "aws+sm:/foo/bar?type=text/plain",
- ePath: "/foo/bar/baz/qux",
- args: "baz/qux?type=application/json&param=quux",
- },
- }
-
- for _, d := range data {
- args := []string{d.args}
- if d.args == "" {
- args = nil
- }
- params, p, err := parseDatasourceURLArgs(mustParseURL(d.u), args...)
- require.NoError(t, err)
- if d.eParams == nil {
- assert.Empty(t, params)
- } else {
- assert.EqualValues(t, d.eParams, params)
- }
- assert.Equal(t, d.ePath, p)
- }
-}
-
-func TestAWSSecretsManager_GetParameterSetup(t *testing.T) {
- calledOk := false
- s := simpleAWSSecretsManagerSourceHelper(DummyAWSSecretsManagerSecretGetter{
- t: t,
- mockGetSecretValue: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
- assert.Equal(t, "/foo/bar", *input.SecretId)
- calledOk = true
- return &secretsmanager.GetSecretValueOutput{SecretString: aws.String("blub")}, nil
- },
- })
-
- _, err := readAWSSecretsManager(context.Background(), s, "/bar")
- assert.True(t, calledOk)
- assert.Nil(t, err)
-}
-
-func TestAWSSecretsManager_GetParameterSetupWrongArgs(t *testing.T) {
- calledOk := false
- s := simpleAWSSecretsManagerSourceHelper(DummyAWSSecretsManagerSecretGetter{
- t: t,
- mockGetSecretValue: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
- assert.Equal(t, "/foo/bar", *input.SecretId)
- calledOk = true
- return &secretsmanager.GetSecretValueOutput{SecretString: aws.String("blub")}, nil
- },
- })
-
- _, err := readAWSSecretsManager(context.Background(), s, "/bar", "/foo", "/bla")
- assert.False(t, calledOk)
- assert.Error(t, err)
-}
-
-func TestAWSSecretsManager_GetParameterMissing(t *testing.T) {
- expectedErr := awserr.New("ParameterNotFound", "Test of error message", nil)
- s := simpleAWSSecretsManagerSourceHelper(DummyAWSSecretsManagerSecretGetter{
- t: t,
- err: expectedErr,
- })
-
- _, err := readAWSSecretsManager(context.Background(), s, "")
- assert.Error(t, err, "Test of error message")
-}
-
-func TestAWSSecretsManager_ReadSecret(t *testing.T) {
- calledOk := false
- s := simpleAWSSecretsManagerSourceHelper(DummyAWSSecretsManagerSecretGetter{
- t: t,
- mockGetSecretValue: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
- assert.Equal(t, "/foo/bar", *input.SecretId)
- calledOk = true
- return &secretsmanager.GetSecretValueOutput{SecretString: aws.String("blub")}, nil
- },
- })
-
- output, err := readAWSSecretsManager(context.Background(), s, "/bar")
- assert.True(t, calledOk)
- require.NoError(t, err)
- assert.Equal(t, []byte("blub"), output)
-}
-
-func TestAWSSecretsManager_ReadSecretBinary(t *testing.T) {
- calledOk := false
- s := simpleAWSSecretsManagerSourceHelper(DummyAWSSecretsManagerSecretGetter{
- t: t,
- mockGetSecretValue: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
- assert.Equal(t, "/foo/bar", *input.SecretId)
- calledOk = true
- return &secretsmanager.GetSecretValueOutput{SecretBinary: []byte("supersecret")}, nil
- },
- })
-
- output, err := readAWSSecretsManager(context.Background(), s, "/bar")
- assert.True(t, calledOk)
- require.NoError(t, err)
- assert.Equal(t, []byte("supersecret"), output)
-}
diff --git a/data/datasource_awssmp.go b/data/datasource_awssmp.go
deleted file mode 100644
index 74979129..00000000
--- a/data/datasource_awssmp.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "strings"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/service/ssm"
-
- gaws "github.com/hairyhenderson/gomplate/v4/aws"
-)
-
-// awssmpGetter - A subset of SSM API for use in unit testing
-type awssmpGetter interface {
- GetParameterWithContext(ctx context.Context, input *ssm.GetParameterInput, opts ...request.Option) (*ssm.GetParameterOutput, error)
- GetParametersByPathWithContext(ctx context.Context, input *ssm.GetParametersByPathInput, opts ...request.Option) (*ssm.GetParametersByPathOutput, error)
-}
-
-func readAWSSMP(ctx context.Context, source *Source, args ...string) (data []byte, err error) {
- if source.asmpg == nil {
- source.asmpg = ssm.New(gaws.SDKSession())
- }
-
- _, paramPath, err := parseDatasourceURLArgs(source.URL, args...)
- if err != nil {
- return nil, err
- }
-
- source.mediaType = jsonMimetype
- switch {
- case strings.HasSuffix(paramPath, "/"):
- source.mediaType = jsonArrayMimetype
- data, err = listAWSSMPParams(ctx, source, paramPath)
- default:
- data, err = readAWSSMPParam(ctx, source, paramPath)
- }
- return data, err
-}
-
-func readAWSSMPParam(ctx context.Context, source *Source, paramPath string) ([]byte, error) {
- input := &ssm.GetParameterInput{
- Name: aws.String(paramPath),
- WithDecryption: aws.Bool(true),
- }
-
- response, err := source.asmpg.GetParameterWithContext(ctx, input)
- if err != nil {
- return nil, fmt.Errorf("error reading aws+smp from AWS using GetParameter with input %v: %w", input, err)
- }
-
- result := *response.Parameter
-
- output, err := ToJSON(result)
- return []byte(output), err
-}
-
-// listAWSSMPParams - supports directory semantics, returns array
-func listAWSSMPParams(ctx context.Context, source *Source, paramPath string) ([]byte, error) {
- input := &ssm.GetParametersByPathInput{
- Path: aws.String(paramPath),
- }
-
- response, err := source.asmpg.GetParametersByPathWithContext(ctx, input)
- if err != nil {
- return nil, fmt.Errorf("error reading aws+smp from AWS using GetParameter with input %v: %w", input, err)
- }
-
- listing := make([]string, len(response.Parameters))
- for i, p := range response.Parameters {
- listing[i] = (*p.Name)[len(paramPath):]
- }
-
- output, err := ToJSON(listing)
- return []byte(output), err
-}
diff --git a/data/datasource_awssmp_test.go b/data/datasource_awssmp_test.go
deleted file mode 100644
index 5c5e02db..00000000
--- a/data/datasource_awssmp_test.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package data
-
-import (
- "context"
- "encoding/json"
- "net/url"
- "testing"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/service/ssm"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// DummyParamGetter - test double
-type DummyParamGetter struct {
- err awserr.Error
- t *testing.T
- param *ssm.Parameter
- mockGetParameter func(*ssm.GetParameterInput) (*ssm.GetParameterOutput, error)
- params []*ssm.Parameter
-}
-
-func (d DummyParamGetter) GetParameterWithContext(_ context.Context, input *ssm.GetParameterInput, _ ...request.Option) (*ssm.GetParameterOutput, error) {
- if d.mockGetParameter != nil {
- output, err := d.mockGetParameter(input)
- return output, err
- }
- if d.err != nil {
- return nil, d.err
- }
- assert.NotNil(d.t, d.param, "Must provide a param if no error!")
- return &ssm.GetParameterOutput{
- Parameter: d.param,
- }, nil
-}
-
-func (d DummyParamGetter) GetParametersByPathWithContext(_ context.Context, _ *ssm.GetParametersByPathInput, _ ...request.Option) (*ssm.GetParametersByPathOutput, error) {
- if d.err != nil {
- return nil, d.err
- }
- assert.NotNil(d.t, d.params, "Must provide a param if no error!")
- return &ssm.GetParametersByPathOutput{
- Parameters: d.params,
- }, nil
-}
-
-func simpleAWSSourceHelper(dummy awssmpGetter) *Source {
- return &Source{
- Alias: "foo",
- URL: &url.URL{
- Scheme: "aws+smp",
- Path: "/foo",
- },
- asmpg: dummy,
- }
-}
-
-func TestAWSSMP_GetParameterSetup(t *testing.T) {
- calledOk := false
- s := simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- mockGetParameter: func(input *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
- assert.Equal(t, "/foo/bar", *input.Name)
- assert.True(t, *input.WithDecryption)
- calledOk = true
- return &ssm.GetParameterOutput{
- Parameter: &ssm.Parameter{},
- }, nil
- },
- })
-
- _, err := readAWSSMP(context.Background(), s, "/bar")
- assert.True(t, calledOk)
- assert.Nil(t, err)
-}
-
-func TestAWSSMP_GetParameterValidOutput(t *testing.T) {
- expected := &ssm.Parameter{
- Name: aws.String("/foo"),
- Type: aws.String("String"),
- Value: aws.String("val"),
- Version: aws.Int64(1),
- }
- s := simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- param: expected,
- })
-
- output, err := readAWSSMP(context.Background(), s, "")
- assert.Nil(t, err)
- actual := &ssm.Parameter{}
- err = json.Unmarshal(output, &actual)
- assert.Nil(t, err)
- assert.Equal(t, expected, actual)
- assert.Equal(t, jsonMimetype, s.mediaType)
-}
-
-func TestAWSSMP_GetParameterMissing(t *testing.T) {
- expectedErr := awserr.New("ParameterNotFound", "Test of error message", nil)
- s := simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- err: expectedErr,
- })
-
- _, err := readAWSSMP(context.Background(), s, "")
- assert.Error(t, err, "Test of error message")
-}
-
-func TestAWSSMP_listAWSSMPParams(t *testing.T) {
- ctx := context.Background()
- s := simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- err: awserr.New("ParameterNotFound", "foo", nil),
- })
- _, err := listAWSSMPParams(ctx, s, "")
- assert.Error(t, err)
-
- s = simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- params: []*ssm.Parameter{
- {Name: aws.String("/a")},
- {Name: aws.String("/b")},
- {Name: aws.String("/c")},
- },
- })
- data, err := listAWSSMPParams(ctx, s, "/")
- require.NoError(t, err)
- assert.Equal(t, []byte(`["a","b","c"]`), data)
-
- s = simpleAWSSourceHelper(DummyParamGetter{
- t: t,
- params: []*ssm.Parameter{
- {Name: aws.String("/a/a")},
- {Name: aws.String("/a/b")},
- {Name: aws.String("/a/c")},
- },
- })
- data, err = listAWSSMPParams(ctx, s, "/a/")
- require.NoError(t, err)
- assert.Equal(t, []byte(`["a","b","c"]`), data)
-}
diff --git a/data/datasource_blob.go b/data/datasource_blob.go
deleted file mode 100644
index 1dac584a..00000000
--- a/data/datasource_blob.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package data
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "mime"
- "net/url"
- "path"
- "strings"
-
- gaws "github.com/hairyhenderson/gomplate/v4/aws"
- "github.com/hairyhenderson/gomplate/v4/env"
-
- "gocloud.dev/blob"
- "gocloud.dev/blob/gcsblob"
- "gocloud.dev/blob/s3blob"
- "gocloud.dev/gcp"
-)
-
-func readBlob(ctx context.Context, source *Source, args ...string) (output []byte, err error) {
- if len(args) >= 2 {
- return nil, fmt.Errorf("maximum two arguments to blob datasource: alias, extraPath")
- }
-
- key := source.URL.Path
- if len(args) == 1 {
- key = path.Join(key, args[0])
- }
-
- opener, err := newOpener(ctx, source.URL)
- if err != nil {
- return nil, err
- }
-
- mux := blob.URLMux{}
- mux.RegisterBucket(source.URL.Scheme, opener)
-
- u := blobURL(source.URL)
- bucket, err := mux.OpenBucket(ctx, u)
- if err != nil {
- return nil, err
- }
- defer bucket.Close()
-
- var r func(context.Context, *blob.Bucket, string) (string, []byte, error)
- if strings.HasSuffix(key, "/") {
- r = listBucket
- } else {
- r = getBlob
- }
-
- mediaType, data, err := r(ctx, bucket, key)
- if mediaType != "" {
- source.mediaType = mediaType
- }
- return data, err
-}
-
-// create the correct kind of blob.BucketURLOpener for the given URL
-func newOpener(ctx context.Context, u *url.URL) (opener blob.BucketURLOpener, err error) {
- switch u.Scheme {
- case "s3":
- // set up a "regular" gomplate AWS SDK session
- sess := gaws.SDKSession()
- // see https://gocloud.dev/concepts/urls/#muxes
- opener = &s3blob.URLOpener{ConfigProvider: sess}
- case "gs":
- creds, err := gcp.DefaultCredentials(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to retrieve GCP credentials: %w", err)
- }
-
- client, err := gcp.NewHTTPClient(
- gcp.DefaultTransport(),
- gcp.CredentialsTokenSource(creds))
- if err != nil {
- return nil, fmt.Errorf("failed to create GCP HTTP client: %w", err)
- }
- opener = &gcsblob.URLOpener{
- Client: client,
- }
- }
- return opener, nil
-}
-
-func getBlob(ctx context.Context, bucket *blob.Bucket, key string) (mediaType string, data []byte, err error) {
- key = strings.TrimPrefix(key, "/")
- attr, err := bucket.Attributes(ctx, key)
- if err != nil {
- return "", nil, fmt.Errorf("failed to retrieve attributes for %s: %w", key, err)
- }
- if attr.ContentType != "" {
- mt, _, e := mime.ParseMediaType(attr.ContentType)
- if e != nil {
- return "", nil, e
- }
- mediaType = mt
- }
- data, err = bucket.ReadAll(ctx, key)
- if err != nil {
- return "", nil, fmt.Errorf("failed to read %s: %w", key, err)
- }
- return mediaType, data, nil
-}
-
-// calls the bucket listing API, returning a JSON Array
-func listBucket(ctx context.Context, bucket *blob.Bucket, path string) (mediaType string, data []byte, err error) {
- path = strings.TrimPrefix(path, "/")
- opts := &blob.ListOptions{
- Prefix: path,
- Delimiter: "/",
- }
- li := bucket.List(opts)
- keys := []string{}
- for {
- obj, err := li.Next(ctx)
- if err == io.EOF {
- break
- }
- if err != nil {
- return "", nil, err
- }
- keys = append(keys, strings.TrimPrefix(obj.Key, path))
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(keys); err != nil {
- return "", nil, err
- }
- b := buf.Bytes()
- // chop off the newline added by the json encoder
- data = b[:len(b)-1]
- return jsonArrayMimetype, data, nil
-}
-
-// copy/sanitize the URL for the Go CDK - it doesn't like params it can't parse
-func blobURL(u *url.URL) string {
- out := cloneURL(u)
- q := out.Query()
-
- for param := range q {
- switch u.Scheme {
- case "s3":
- switch param {
- case "region", "endpoint", "disableSSL", "s3ForcePathStyle":
- default:
- q.Del(param)
- }
- case "gs":
- switch param {
- case "access_id", "private_key_path":
- default:
- q.Del(param)
- }
- }
- }
-
- if u.Scheme == "s3" {
- // handle AWS_S3_ENDPOINT env var
- endpoint := env.Getenv("AWS_S3_ENDPOINT")
- if endpoint != "" {
- q.Set("endpoint", endpoint)
- }
- }
-
- out.RawQuery = q.Encode()
-
- return out.String()
-}
diff --git a/data/datasource_blob_test.go b/data/datasource_blob_test.go
deleted file mode 100644
index 6be8ea00..00000000
--- a/data/datasource_blob_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package data
-
-import (
- "bytes"
- "context"
- "net/http/httptest"
- "net/url"
- "testing"
-
- "github.com/johannesboyne/gofakes3"
- "github.com/johannesboyne/gofakes3/backend/s3mem"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func setupTestBucket(t *testing.T) (*httptest.Server, *url.URL) {
- backend := s3mem.New()
- faker := gofakes3.New(backend)
- ts := httptest.NewServer(faker.Server())
-
- err := backend.CreateBucket("mybucket")
- require.NoError(t, err)
- c := "hello"
- err = putFile(backend, "file1", "text/plain", c)
- require.NoError(t, err)
-
- c = `{"value": "goodbye world"}`
- err = putFile(backend, "file2", "application/json", c)
- require.NoError(t, err)
-
- c = `value: what a world`
- err = putFile(backend, "file3", "application/yaml", c)
- require.NoError(t, err)
-
- c = `value: out of this world`
- err = putFile(backend, "dir1/file1", "application/yaml", c)
- require.NoError(t, err)
-
- c = `value: foo`
- err = putFile(backend, "dir1/file2", "application/yaml", c)
- require.NoError(t, err)
-
- u, _ := url.Parse(ts.URL)
- return ts, u
-}
-
-func putFile(backend gofakes3.Backend, file, mime, content string) error {
- _, err := backend.PutObject(
- "mybucket",
- file,
- map[string]string{"Content-Type": mime},
- bytes.NewBufferString(content),
- int64(len(content)),
- )
- return err
-}
-
-func TestReadBlob(t *testing.T) {
- _, err := readBlob(context.Background(), nil, "foo", "bar")
- assert.Error(t, err)
-
- ts, u := setupTestBucket(t)
- defer ts.Close()
-
- t.Run("no authentication", func(t *testing.T) {
- t.Setenv("AWS_ANON", "true")
-
- d, err := NewData([]string{"-d", "data=s3://mybucket/file1?region=us-east-1&disableSSL=true&s3ForcePathStyle=true&type=text/plain&endpoint=" + u.Host}, nil)
- require.NoError(t, err)
-
- expected := "hello"
- out, err := d.Datasource("data")
- require.NoError(t, err)
- assert.Equal(t, expected, out)
- })
-
- t.Run("with authentication", func(t *testing.T) {
- t.Setenv("AWS_ACCESS_KEY_ID", "fake")
- t.Setenv("AWS_SECRET_ACCESS_KEY", "fake")
- t.Setenv("AWS_S3_ENDPOINT", u.Host)
-
- d, err := NewData([]string{"-d", "data=s3://mybucket/file2?region=us-east-1&disableSSL=true&s3ForcePathStyle=true"}, nil)
- require.NoError(t, err)
-
- var expected interface{}
- expected = map[string]interface{}{"value": "goodbye world"}
- out, err := d.Datasource("data")
- require.NoError(t, err)
- assert.Equal(t, expected, out)
-
- d, err = NewData([]string{"-d", "data=s3://mybucket/?region=us-east-1&disableSSL=true&s3ForcePathStyle=true"}, nil)
- require.NoError(t, err)
-
- expected = []interface{}{"dir1/", "file1", "file2", "file3"}
- out, err = d.Datasource("data")
- require.NoError(t, err)
- assert.EqualValues(t, expected, out)
-
- d, err = NewData([]string{"-d", "data=s3://mybucket/dir1/?region=us-east-1&disableSSL=true&s3ForcePathStyle=true"}, nil)
- require.NoError(t, err)
-
- expected = []interface{}{"file1", "file2"}
- out, err = d.Datasource("data")
- require.NoError(t, err)
- assert.EqualValues(t, expected, out)
- })
-}
-
-func TestBlobURL(t *testing.T) {
- data := []struct {
- in string
- expected string
- }{
- {"s3://foo/bar/baz", "s3://foo/bar/baz"},
- {"s3://foo/bar/baz?type=hello/world", "s3://foo/bar/baz"},
- {"s3://foo/bar/baz?region=us-east-1", "s3://foo/bar/baz?region=us-east-1"},
- {"s3://foo/bar/baz?disableSSL=true&type=text/csv", "s3://foo/bar/baz?disableSSL=true"},
- {"s3://foo/bar/baz?type=text/csv&s3ForcePathStyle=true&endpoint=1.2.3.4", "s3://foo/bar/baz?endpoint=1.2.3.4&s3ForcePathStyle=true"},
- {"gs://foo/bar/baz", "gs://foo/bar/baz"},
- {"gs://foo/bar/baz?type=foo/bar", "gs://foo/bar/baz"},
- {"gs://foo/bar/baz?access_id=123", "gs://foo/bar/baz?access_id=123"},
- {"gs://foo/bar/baz?private_key_path=/foo/bar", "gs://foo/bar/baz?private_key_path=%2Ffoo%2Fbar"},
- {"gs://foo/bar/baz?private_key_path=key.json&foo=bar", "gs://foo/bar/baz?private_key_path=key.json"},
- {"gs://foo/bar/baz?private_key_path=key.json&foo=bar&access_id=abcd", "gs://foo/bar/baz?access_id=abcd&private_key_path=key.json"},
- }
-
- for _, d := range data {
- u, _ := url.Parse(d.in)
- out := blobURL(u)
- assert.Equal(t, d.expected, out)
- }
-}
diff --git a/data/datasource_consul.go b/data/datasource_consul.go
deleted file mode 100644
index ecc7e516..00000000
--- a/data/datasource_consul.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package data
-
-import (
- "context"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/libkv"
-)
-
-func readConsul(_ context.Context, source *Source, args ...string) (data []byte, err error) {
- if source.kv == nil {
- source.kv, err = libkv.NewConsul(source.URL)
- if err != nil {
- return nil, err
- }
- err = source.kv.Login()
- if err != nil {
- return nil, err
- }
- }
-
- p := source.URL.Path
- if len(args) == 1 {
- p = strings.TrimRight(p, "/") + "/" + args[0]
- }
-
- if strings.HasSuffix(p, "/") {
- source.mediaType = jsonArrayMimetype
- data, err = source.kv.List(p)
- } else {
- data, err = source.kv.Read(p)
- }
-
- if err != nil {
- return nil, err
- }
-
- return data, nil
-}
diff --git a/data/datasource_env.go b/data/datasource_env.go
deleted file mode 100644
index e5bf180f..00000000
--- a/data/datasource_env.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package data
-
-import (
- "context"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/env"
-)
-
-//nolint:unparam
-func readEnv(_ context.Context, source *Source, _ ...string) (b []byte, err error) {
- n := source.URL.Path
- n = strings.TrimPrefix(n, "/")
- if n == "" {
- n = source.URL.Opaque
- }
-
- b = []byte(env.Getenv(n))
- return b, nil
-}
diff --git a/data/datasource_env_test.go b/data/datasource_env_test.go
deleted file mode 100644
index 6512578c..00000000
--- a/data/datasource_env_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package data
-
-import (
- "context"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func mustParseURL(in string) *url.URL {
- u, _ := url.Parse(in)
- return u
-}
-
-func TestReadEnv(t *testing.T) {
- ctx := context.Background()
-
- content := []byte(`hello world`)
- t.Setenv("HELLO_WORLD", "hello world")
- t.Setenv("HELLO_UNIVERSE", "hello universe")
-
- source := &Source{Alias: "foo", URL: mustParseURL("env:HELLO_WORLD")}
-
- actual, err := readEnv(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "foo", URL: mustParseURL("env:/HELLO_WORLD")}
-
- actual, err = readEnv(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "foo", URL: mustParseURL("env:///HELLO_WORLD")}
-
- actual, err = readEnv(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "foo", URL: mustParseURL("env:HELLO_WORLD?foo=bar")}
-
- actual, err = readEnv(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "foo", URL: mustParseURL("env:///HELLO_WORLD?foo=bar")}
-
- actual, err = readEnv(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-}
diff --git a/data/datasource_file.go b/data/datasource_file.go
deleted file mode 100644
index f5c764fe..00000000
--- a/data/datasource_file.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package data
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io/fs"
- "net/url"
- "path/filepath"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
-)
-
-func readFile(ctx context.Context, source *Source, args ...string) ([]byte, error) {
- if source.fs == nil {
- fsp := datafs.FSProviderFromContext(ctx)
- fsys, err := fsp.New(source.URL)
- if err != nil {
- return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", source.URL, err)
- }
- source.fs = fsys
- }
-
- p := filepath.FromSlash(source.URL.Path)
-
- if len(args) == 1 {
- parsed, err := url.Parse(args[0])
- if err != nil {
- return nil, err
- }
-
- if parsed.Path != "" {
- p = filepath.Join(p, parsed.Path)
- }
-
- // reset the media type - it may have been set by a parent dir read
- source.mediaType = ""
- }
-
- isDir := strings.HasSuffix(p, string(filepath.Separator))
- if strings.HasSuffix(p, string(filepath.Separator)) {
- p = p[:len(p)-1]
- }
-
- // make sure we can access the file
- i, err := fs.Stat(source.fs, p)
- if err != nil {
- return nil, fmt.Errorf("stat %s: %w", p, err)
- }
-
- if isDir {
- source.mediaType = jsonArrayMimetype
- if i.IsDir() {
- return readFileDir(source, p)
- }
- return nil, fmt.Errorf("%s is not a directory", p)
- }
-
- b, err := fs.ReadFile(source.fs, p)
- if err != nil {
- return nil, fmt.Errorf("readFile %s: %w", p, err)
- }
- return b, nil
-}
-
-func readFileDir(source *Source, p string) ([]byte, error) {
- names, err := fs.ReadDir(source.fs, p)
- if err != nil {
- return nil, err
- }
-
- files := make([]string, len(names))
- for i, v := range names {
- files[i] = v.Name()
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(files); err != nil {
- return nil, err
- }
- b := buf.Bytes()
- // chop off the newline added by the json encoder
- return b[:len(b)-1], nil
-}
diff --git a/data/datasource_file_test.go b/data/datasource_file_test.go
deleted file mode 100644
index 7ad1c2a0..00000000
--- a/data/datasource_file_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package data
-
-import (
- "context"
- "io/fs"
- "testing"
- "testing/fstest"
-
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestReadFile(t *testing.T) {
- ctx := context.Background()
-
- content := []byte(`hello world`)
-
- fsys := datafs.WrapWdFS(fstest.MapFS{
- "tmp": {Mode: fs.ModeDir | 0o777},
- "tmp/foo": {Data: content},
- "tmp/partial": {Mode: fs.ModeDir | 0o777},
- "tmp/partial/foo.txt": {Data: content},
- "tmp/partial/bar.txt": {},
- "tmp/partial/baz.txt": {},
- })
-
- source := &Source{Alias: "foo", URL: mustParseURL("file:///tmp/foo")}
- source.fs = fsys
-
- actual, err := readFile(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "bogus", URL: mustParseURL("file:///bogus")}
- source.fs = fsys
- _, err = readFile(ctx, source)
- assert.Error(t, err)
-
- source = &Source{Alias: "partial", URL: mustParseURL("file:///tmp/partial")}
- source.fs = fsys
- actual, err = readFile(ctx, source, "foo.txt")
- require.NoError(t, err)
- assert.Equal(t, content, actual)
-
- source = &Source{Alias: "dir", URL: mustParseURL("file:///tmp/partial/")}
- source.fs = fsys
- actual, err = readFile(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, []byte(`["bar.txt","baz.txt","foo.txt"]`), actual)
-
- source = &Source{Alias: "dir", URL: mustParseURL("file:///tmp/partial/?type=application/json")}
- source.fs = fsys
- actual, err = readFile(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, []byte(`["bar.txt","baz.txt","foo.txt"]`), actual)
- mime, err := source.mimeType("")
- require.NoError(t, err)
- assert.Equal(t, "application/json", mime)
-
- source = &Source{Alias: "dir", URL: mustParseURL("file:///tmp/partial/?type=application/json")}
- source.fs = fsys
- actual, err = readFile(ctx, source, "foo.txt")
- require.NoError(t, err)
- assert.Equal(t, content, actual)
- mime, err = source.mimeType("")
- require.NoError(t, err)
- assert.Equal(t, "application/json", mime)
-}
diff --git a/data/datasource_git.go b/data/datasource_git.go
deleted file mode 100644
index 3859d344..00000000
--- a/data/datasource_git.go
+++ /dev/null
@@ -1,328 +0,0 @@
-package data
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/base64"
- "github.com/hairyhenderson/gomplate/v4/env"
- "github.com/rs/zerolog"
-
- "github.com/go-git/go-billy/v5"
- "github.com/go-git/go-billy/v5/memfs"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/transport"
- "github.com/go-git/go-git/v5/plumbing/transport/client"
- "github.com/go-git/go-git/v5/plumbing/transport/http"
- "github.com/go-git/go-git/v5/plumbing/transport/ssh"
- "github.com/go-git/go-git/v5/storage/memory"
-)
-
-func readGit(ctx context.Context, source *Source, args ...string) ([]byte, error) {
- g := gitsource{}
-
- u := source.URL
- repoURL, path, err := g.parseGitPath(u, args...)
- if err != nil {
- return nil, err
- }
-
- depth := 1
- if u.Scheme == "git+file" {
- // we can't do shallow clones for filesystem repos apparently
- depth = 0
- }
-
- fs, _, err := g.clone(ctx, repoURL, depth)
- if err != nil {
- return nil, err
- }
-
- mimeType, out, err := g.read(fs, path)
- if mimeType != "" {
- source.mediaType = mimeType
- }
- return out, err
-}
-
-type gitsource struct{}
-
-func (g gitsource) parseArgURL(arg string) (u *url.URL, err error) {
- if strings.HasPrefix(arg, "//") {
- u, err = url.Parse(arg[1:])
- u.Path = "/" + u.Path
- } else {
- u, err = url.Parse(arg)
- }
-
- if err != nil {
- return nil, fmt.Errorf("failed to parse arg %s: %w", arg, err)
- }
- return u, err
-}
-
-func (g gitsource) parseQuery(orig, arg *url.URL) string {
- q := orig.Query()
- pq := arg.Query()
- for k, vs := range pq {
- for _, v := range vs {
- q.Add(k, v)
- }
- }
- return q.Encode()
-}
-
-func (g gitsource) parseArgPath(u *url.URL, arg string) (repo, p string) {
- // if the source URL already specified a repo and subpath, the whole
- // arg is interpreted as subpath
- if strings.Contains(u.Path, "//") || strings.HasPrefix(arg, "//") {
- return "", arg
- }
-
- parts := strings.SplitN(arg, "//", 2)
- repo = parts[0]
- if len(parts) == 2 {
- p = "/" + parts[1]
- }
- return repo, p
-}
-
-// Massage the URL and args together to produce the repo to clone,
-// and the path to read.
-// The path is delimited from the repo by '//'
-func (g gitsource) parseGitPath(u *url.URL, args ...string) (out *url.URL, p string, err error) {
- if u == nil {
- return nil, "", fmt.Errorf("parseGitPath: no url provided (%v)", u)
- }
- // copy the input url so we can modify it
- out = cloneURL(u)
-
- parts := strings.SplitN(out.Path, "//", 2)
- switch len(parts) {
- case 1:
- p = "/"
- case 2:
- p = "/" + parts[1]
-
- i := strings.LastIndex(out.Path, p)
- out.Path = out.Path[:i-1]
- }
-
- if len(args) > 0 {
- argURL, uerr := g.parseArgURL(args[0])
- if uerr != nil {
- return nil, "", uerr
- }
- repo, argpath := g.parseArgPath(u, argURL.Path)
- out.Path = path.Join(out.Path, repo)
- p = path.Join(p, argpath)
-
- out.RawQuery = g.parseQuery(u, argURL)
-
- if argURL.Fragment != "" {
- out.Fragment = argURL.Fragment
- }
- }
- return out, p, err
-}
-
-//nolint:interfacer
-func cloneURL(u *url.URL) *url.URL {
- out, _ := url.Parse(u.String())
- return out
-}
-
-// refFromURL - extract the ref from the URL fragment if present
-func (g gitsource) refFromURL(u *url.URL) plumbing.ReferenceName {
- switch {
- case strings.HasPrefix(u.Fragment, "refs/"):
- return plumbing.ReferenceName(u.Fragment)
- case u.Fragment != "":
- return plumbing.NewBranchReferenceName(u.Fragment)
- default:
- return plumbing.ReferenceName("")
- }
-}
-
-// refFromRemoteHead - extract the ref from the remote HEAD, to work around
-// hard-coded 'master' default branch in go-git.
-// Should be unnecessary once https://github.com/go-git/go-git/issues/249 is
-// fixed.
-func (g gitsource) refFromRemoteHead(ctx context.Context, u *url.URL, auth transport.AuthMethod) (plumbing.ReferenceName, error) {
- e, err := transport.NewEndpoint(u.String())
- if err != nil {
- return "", err
- }
-
- cli, err := client.NewClient(e)
- if err != nil {
- return "", err
- }
-
- s, err := cli.NewUploadPackSession(e, auth)
- if err != nil {
- return "", err
- }
-
- info, err := s.AdvertisedReferencesContext(ctx)
- if err != nil {
- return "", err
- }
-
- refs, err := info.AllReferences()
- if err != nil {
- return "", err
- }
-
- headRef, ok := refs["HEAD"]
- if !ok {
- return "", fmt.Errorf("no HEAD ref found")
- }
-
- return headRef.Target(), nil
-}
-
-// clone a repo for later reading through http(s), git, or ssh. u must be the URL to the repo
-// itself, and must have any file path stripped
-func (g gitsource) clone(ctx context.Context, repoURL *url.URL, depth int) (billy.Filesystem, *git.Repository, error) {
- fs := memfs.New()
- storer := memory.NewStorage()
-
- // preserve repoURL by cloning it
- u := cloneURL(repoURL)
-
- auth, err := g.auth(u)
- if err != nil {
- return nil, nil, err
- }
-
- if strings.HasPrefix(u.Scheme, "git+") {
- scheme := u.Scheme[len("git+"):]
- u.Scheme = scheme
- }
-
- ref := g.refFromURL(u)
- u.Fragment = ""
- u.RawQuery = ""
-
- // attempt to get the ref from the remote so we don't default to master
- if ref == "" {
- ref, err = g.refFromRemoteHead(ctx, u, auth)
- if err != nil {
- zerolog.Ctx(ctx).Warn().
- Stringer("repoURL", u).
- Err(err).
- Msg("failed to get ref from remote, using default")
- }
- }
-
- opts := &git.CloneOptions{
- URL: u.String(),
- Auth: auth,
- Depth: depth,
- ReferenceName: ref,
- SingleBranch: true,
- Tags: git.NoTags,
- }
- repo, err := git.CloneContext(ctx, storer, fs, opts)
- if u.Scheme == "file" && err == transport.ErrRepositoryNotFound && !strings.HasSuffix(u.Path, ".git") {
- // maybe this has a `.git` subdirectory...
- u = cloneURL(repoURL)
- u.Path = path.Join(u.Path, ".git")
- return g.clone(ctx, u, depth)
- }
- if err != nil {
- return nil, nil, fmt.Errorf("git clone %s: %w", u, err)
- }
- return fs, repo, nil
-}
-
-// read - reads the provided path out of a git repo
-func (g gitsource) read(fsys billy.Filesystem, path string) (string, []byte, error) {
- fi, err := fsys.Stat(path)
- if err != nil {
- return "", nil, fmt.Errorf("can't stat %s: %w", path, err)
- }
- if fi.IsDir() || strings.HasSuffix(path, string(filepath.Separator)) {
- out, rerr := g.readDir(fsys, path)
- return jsonArrayMimetype, out, rerr
- }
-
- f, err := fsys.OpenFile(path, os.O_RDONLY, 0)
- if err != nil {
- return "", nil, fmt.Errorf("can't open %s: %w", path, err)
- }
-
- b, err := io.ReadAll(f)
- if err != nil {
- return "", nil, fmt.Errorf("can't read %s: %w", path, err)
- }
-
- return "", b, nil
-}
-
-func (g gitsource) readDir(fs billy.Filesystem, path string) ([]byte, error) {
- names, err := fs.ReadDir(path)
- if err != nil {
- return nil, fmt.Errorf("couldn't read dir %s: %w", path, err)
- }
- files := make([]string, len(names))
- for i, v := range names {
- files[i] = v.Name()
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(files); err != nil {
- return nil, err
- }
- b := buf.Bytes()
- // chop off the newline added by the json encoder
- return b[:len(b)-1], nil
-}
-
-/*
-auth methods:
-- ssh named key (no password support)
- - GIT_SSH_KEY (base64-encoded) or GIT_SSH_KEY_FILE (base64-encoded, or not)
-
-- ssh agent auth (preferred)
-- http basic auth (for github, gitlab, bitbucket tokens)
-- http token auth (bearer token, somewhat unusual)
-*/
-func (g gitsource) auth(u *url.URL) (auth transport.AuthMethod, err error) {
- user := u.User.Username()
- switch u.Scheme {
- case "git+http", "git+https":
- if pass, ok := u.User.Password(); ok {
- auth = &http.BasicAuth{Username: user, Password: pass}
- } else if pass := env.Getenv("GIT_HTTP_PASSWORD"); pass != "" {
- auth = &http.BasicAuth{Username: user, Password: pass}
- } else if tok := env.Getenv("GIT_HTTP_TOKEN"); tok != "" {
- // note docs on TokenAuth - this is rarely to be used
- auth = &http.TokenAuth{Token: tok}
- }
- case "git+ssh":
- k := env.Getenv("GIT_SSH_KEY")
- if k != "" {
- var key []byte
- key, err = base64.Decode(k)
- if err != nil {
- key = []byte(k)
- }
- auth, err = ssh.NewPublicKeys(user, key, "")
- } else {
- auth, err = ssh.NewSSHAgentAuth(user)
- }
- }
- return auth, err
-}
diff --git a/data/datasource_git_test.go b/data/datasource_git_test.go
deleted file mode 100644
index 3b187ecc..00000000
--- a/data/datasource_git_test.go
+++ /dev/null
@@ -1,551 +0,0 @@
-package data
-
-import (
- "context"
- "encoding/base64"
- "fmt"
- "io"
- "net/url"
- "os"
- "strings"
- "testing"
- "time"
-
- "github.com/go-git/go-billy/v5"
- "github.com/go-git/go-billy/v5/memfs"
- "github.com/go-git/go-billy/v5/osfs"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/cache"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/go-git/go-git/v5/plumbing/transport/client"
- "github.com/go-git/go-git/v5/plumbing/transport/http"
- "github.com/go-git/go-git/v5/plumbing/transport/server"
- "github.com/go-git/go-git/v5/plumbing/transport/ssh"
- "github.com/go-git/go-git/v5/storage/filesystem"
-
- "golang.org/x/crypto/ssh/testdata"
-
- "gotest.tools/v3/assert"
- is "gotest.tools/v3/assert/cmp"
-)
-
-func TestParseArgPath(t *testing.T) {
- t.Parallel()
- g := gitsource{}
-
- data := []struct {
- url string
- arg string
- repo, path string
- }{
- {
- "git+file:///foo//foo",
- "/bar",
- "", "/bar",
- },
- {
- "git+file:///foo//bar",
- "/baz//qux",
- "", "/baz//qux",
- },
- {
- "git+https://example.com/foo",
- "/bar",
- "/bar", "",
- },
- {
- "git+https://example.com/foo",
- "//bar",
- "", "//bar",
- },
- {
- "git+https://example.com/foo//bar",
- "//baz",
- "", "//baz",
- },
- {
- "git+https://example.com/foo",
- "/bar//baz",
- "/bar", "/baz",
- },
- {
- "git+https://example.com/foo?type=t",
- "/bar//baz",
- "/bar", "/baz",
- },
- {
- "git+https://example.com/foo#master",
- "/bar//baz",
- "/bar", "/baz",
- },
- {
- "git+https://example.com/foo",
- "//bar",
- "", "//bar",
- },
- {
- "git+https://example.com/foo?type=t",
- "//baz",
- "", "//baz",
- },
- {
- "git+https://example.com/foo?type=t#v1",
- "//bar",
- "", "//bar",
- },
- }
-
- for i, d := range data {
- d := d
- t.Run(fmt.Sprintf("%d:(%q,%q)==(%q,%q)", i, d.url, d.arg, d.repo, d.path), func(t *testing.T) {
- t.Parallel()
- u, _ := url.Parse(d.url)
- repo, path := g.parseArgPath(u, d.arg)
- assert.Equal(t, d.repo, repo)
- assert.Equal(t, d.path, path)
- })
- }
-}
-
-func TestParseGitPath(t *testing.T) {
- t.Parallel()
- g := gitsource{}
- _, _, err := g.parseGitPath(nil)
- assert.ErrorContains(t, err, "")
-
- u := mustParseURL("http://example.com//foo")
- assert.Equal(t, "//foo", u.Path)
- parts := strings.SplitN(u.Path, "//", 2)
- assert.Equal(t, 2, len(parts))
- assert.DeepEqual(t, []string{"", "foo"}, parts)
-
- data := []struct {
- url string
- args string
- repo, path string
- }{
- {
- "git+https://github.com/hairyhenderson/gomplate//docs-src/content/functions/aws.yml",
- "",
- "git+https://github.com/hairyhenderson/gomplate",
- "/docs-src/content/functions/aws.yml",
- },
- {
- "git+ssh://github.com/hairyhenderson/gomplate.git",
- "",
- "git+ssh://github.com/hairyhenderson/gomplate.git",
- "/",
- },
- {
- "https://github.com",
- "",
- "https://github.com",
- "/",
- },
- {
- "git://example.com/foo//file.txt#someref",
- "",
- "git://example.com/foo#someref", "/file.txt",
- },
- {
- "git+file:///home/foo/repo//file.txt#someref",
- "",
- "git+file:///home/foo/repo#someref", "/file.txt",
- },
- {
- "git+file:///repo",
- "",
- "git+file:///repo", "/",
- },
- {
- "git+file:///foo//foo",
- "",
- "git+file:///foo", "/foo",
- },
- {
- "git+file:///foo//foo",
- "/bar",
- "git+file:///foo", "/foo/bar",
- },
- {
- "git+file:///foo//bar",
- // in this case the // is meaningless
- "/baz//qux",
- "git+file:///foo", "/bar/baz/qux",
- },
- {
- "git+https://example.com/foo",
- "/bar",
- "git+https://example.com/foo/bar", "/",
- },
- {
- "git+https://example.com/foo",
- "//bar",
- "git+https://example.com/foo", "/bar",
- },
- {
- "git+https://example.com//foo",
- "/bar",
- "git+https://example.com", "/foo/bar",
- },
- {
- "git+https://example.com/foo//bar",
- "//baz",
- "git+https://example.com/foo", "/bar/baz",
- },
- {
- "git+https://example.com/foo",
- "/bar//baz",
- "git+https://example.com/foo/bar", "/baz",
- },
- {
- "git+https://example.com/foo?type=t",
- "/bar//baz",
- "git+https://example.com/foo/bar?type=t", "/baz",
- },
- {
- "git+https://example.com/foo#master",
- "/bar//baz",
- "git+https://example.com/foo/bar#master", "/baz",
- },
- {
- "git+https://example.com/foo",
- "/bar//baz?type=t",
- "git+https://example.com/foo/bar?type=t", "/baz",
- },
- {
- "git+https://example.com/foo",
- "/bar//baz#master",
- "git+https://example.com/foo/bar#master", "/baz",
- },
- {
- "git+https://example.com/foo",
- "//bar?type=t",
- "git+https://example.com/foo?type=t", "/bar",
- },
- {
- "git+https://example.com/foo",
- "//bar#master",
- "git+https://example.com/foo#master", "/bar",
- },
- {
- "git+https://example.com/foo?type=t",
- "//bar#master",
- "git+https://example.com/foo?type=t#master", "/bar",
- },
- {
- "git+https://example.com/foo?type=t#v1",
- "//bar?type=j#v2",
- "git+https://example.com/foo?type=t&type=j#v2", "/bar",
- },
- }
-
- for i, d := range data {
- d := d
- t.Run(fmt.Sprintf("%d:(%q,%q)==(%q,%q)", i, d.url, d.args, d.repo, d.path), func(t *testing.T) {
- t.Parallel()
- u, _ := url.Parse(d.url)
- args := []string{d.args}
- if d.args == "" {
- args = nil
- }
- repo, path, err := g.parseGitPath(u, args...)
- assert.NilError(t, err)
- assert.Equal(t, d.repo, repo.String())
- assert.Equal(t, d.path, path)
- })
- }
-}
-
-func TestReadGitRepo(t *testing.T) {
- g := gitsource{}
- fs := setupGitRepo(t)
- fs, err := fs.Chroot("/repo")
- assert.NilError(t, err)
-
- _, _, err = g.read(fs, "/bogus")
- assert.ErrorContains(t, err, "can't stat /bogus")
-
- mtype, out, err := g.read(fs, "/foo")
- assert.NilError(t, err)
- assert.Equal(t, `["bar"]`, string(out))
- assert.Equal(t, jsonArrayMimetype, mtype)
-
- mtype, out, err = g.read(fs, "/foo/bar")
- assert.NilError(t, err)
- assert.Equal(t, `["hi.txt"]`, string(out))
- assert.Equal(t, jsonArrayMimetype, mtype)
-
- mtype, out, err = g.read(fs, "/foo/bar/hi.txt")
- assert.NilError(t, err)
- assert.Equal(t, `hello world`, string(out))
- assert.Equal(t, "", mtype)
-}
-
-var testHashes = map[string]string{}
-
-func setupGitRepo(t *testing.T) billy.Filesystem {
- fs := memfs.New()
- fs.MkdirAll("/repo/.git", os.ModeDir)
- repo, _ := fs.Chroot("/repo")
- dot, _ := repo.Chroot("/.git")
- s := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
-
- r, err := git.Init(s, repo)
- assert.NilError(t, err)
-
- // default to main
- h := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName("refs/heads/main"))
- err = s.SetReference(h)
- assert.NilError(t, err)
-
- // config needs to be created after setting up a "normal" fs repo
- // this is possibly a bug in git-go?
- c, err := r.Config()
- assert.NilError(t, err)
-
- c.Init.DefaultBranch = "main"
-
- s.SetConfig(c)
- assert.NilError(t, err)
-
- w, err := r.Worktree()
- assert.NilError(t, err)
-
- repo.MkdirAll("/foo/bar", os.ModeDir)
- f, err := repo.Create("/foo/bar/hi.txt")
- assert.NilError(t, err)
- _, err = f.Write([]byte("hello world"))
- assert.NilError(t, err)
- _, err = w.Add(f.Name())
- assert.NilError(t, err)
- hash, err := w.Commit("initial commit", &git.CommitOptions{Author: &object.Signature{}})
- assert.NilError(t, err)
-
- ref, err := r.CreateTag("v1", hash, nil)
- assert.NilError(t, err)
- testHashes["v1"] = hash.String()
-
- branchName := plumbing.NewBranchReferenceName("mybranch")
- err = w.Checkout(&git.CheckoutOptions{
- Branch: branchName,
- Hash: ref.Hash(),
- Create: true,
- })
- assert.NilError(t, err)
-
- f, err = repo.Create("/secondfile.txt")
- assert.NilError(t, err)
- _, err = f.Write([]byte("another file\n"))
- assert.NilError(t, err)
- n := f.Name()
- _, err = w.Add(n)
- assert.NilError(t, err)
- hash, err = w.Commit("second commit", &git.CommitOptions{
- Author: &object.Signature{
- Name: "John Doe",
- },
- })
- ref = plumbing.NewHashReference(branchName, hash)
- assert.NilError(t, err)
- testHashes["mybranch"] = ref.Hash().String()
-
- // make the repo dirty
- _, err = f.Write([]byte("dirty file"))
- assert.NilError(t, err)
-
- // set up a bare repo
- fs.MkdirAll("/bare.git", os.ModeDir)
- fs.MkdirAll("/barewt", os.ModeDir)
- repo, _ = fs.Chroot("/barewt")
- dot, _ = fs.Chroot("/bare.git")
- s = filesystem.NewStorage(dot, nil)
-
- r, err = git.Init(s, repo)
- assert.NilError(t, err)
-
- w, err = r.Worktree()
- assert.NilError(t, err)
-
- f, err = repo.Create("/hello.txt")
- assert.NilError(t, err)
- f.Write([]byte("hello world"))
- w.Add(f.Name())
- _, err = w.Commit("initial commit", &git.CommitOptions{
- Author: &object.Signature{
- Name: "John Doe",
- Email: "john@doe.org",
- When: time.Now(),
- },
- })
- assert.NilError(t, err)
-
- return fs
-}
-
-func overrideFSLoader(fs billy.Filesystem) {
- l := server.NewFilesystemLoader(fs)
- client.InstallProtocol("file", server.NewClient(l))
-}
-
-func TestOpenFileRepo(t *testing.T) {
- ctx := context.Background()
- repoFS := setupGitRepo(t)
- g := gitsource{}
-
- overrideFSLoader(repoFS)
- defer overrideFSLoader(osfs.New(""))
-
- fsys, _, err := g.clone(ctx, mustParseURL("git+file:///repo"), 0)
- assert.NilError(t, err)
-
- f, err := fsys.Open("/foo/bar/hi.txt")
- assert.NilError(t, err)
- b, _ := io.ReadAll(f)
- assert.Equal(t, "hello world", string(b))
-
- _, repo, err := g.clone(ctx, mustParseURL("git+file:///repo#main"), 0)
- assert.NilError(t, err)
-
- ref, err := repo.Reference(plumbing.NewBranchReferenceName("main"), true)
- assert.NilError(t, err)
- assert.Equal(t, "refs/heads/main", ref.Name().String())
-
- _, repo, err = g.clone(ctx, mustParseURL("git+file:///repo#refs/tags/v1"), 0)
- assert.NilError(t, err)
-
- ref, err = repo.Head()
- assert.NilError(t, err)
- assert.Equal(t, testHashes["v1"], ref.Hash().String())
-
- _, repo, err = g.clone(ctx, mustParseURL("git+file:///repo/#mybranch"), 0)
- assert.NilError(t, err)
-
- ref, err = repo.Head()
- assert.NilError(t, err)
- assert.Equal(t, "refs/heads/mybranch", ref.Name().String())
- assert.Equal(t, testHashes["mybranch"], ref.Hash().String())
-}
-
-func TestOpenBareFileRepo(t *testing.T) {
- ctx := context.Background()
- repoFS := setupGitRepo(t)
- g := gitsource{}
-
- overrideFSLoader(repoFS)
- defer overrideFSLoader(osfs.New(""))
-
- fsys, _, err := g.clone(ctx, mustParseURL("git+file:///bare.git"), 0)
- assert.NilError(t, err)
-
- f, err := fsys.Open("/hello.txt")
- assert.NilError(t, err)
- b, _ := io.ReadAll(f)
- assert.Equal(t, "hello world", string(b))
-}
-
-func TestReadGit(t *testing.T) {
- ctx := context.Background()
- repoFS := setupGitRepo(t)
-
- overrideFSLoader(repoFS)
- defer overrideFSLoader(osfs.New(""))
-
- s := &Source{
- Alias: "hi",
- URL: mustParseURL("git+file:///bare.git//hello.txt"),
- }
- b, err := readGit(ctx, s)
- assert.NilError(t, err)
- assert.Equal(t, "hello world", string(b))
-
- s = &Source{
- Alias: "hi",
- URL: mustParseURL("git+file:///bare.git"),
- }
- b, err = readGit(ctx, s)
- assert.NilError(t, err)
- assert.Equal(t, "application/array+json", s.mediaType)
- assert.Equal(t, `["hello.txt"]`, string(b))
-}
-
-func TestGitAuth(t *testing.T) {
- g := gitsource{}
- a, err := g.auth(mustParseURL("git+file:///bare.git"))
- assert.NilError(t, err)
- assert.Equal(t, nil, a)
-
- a, err = g.auth(mustParseURL("git+https://example.com/foo"))
- assert.NilError(t, err)
- assert.Assert(t, is.Nil(a))
-
- a, err = g.auth(mustParseURL("git+https://user:swordfish@example.com/foo"))
- assert.NilError(t, err)
- assert.DeepEqual(t, &http.BasicAuth{Username: "user", Password: "swordfish"}, a)
-
- t.Setenv("GIT_HTTP_PASSWORD", "swordfish")
- a, err = g.auth(mustParseURL("git+https://user@example.com/foo"))
- assert.NilError(t, err)
- assert.DeepEqual(t, &http.BasicAuth{Username: "user", Password: "swordfish"}, a)
- os.Unsetenv("GIT_HTTP_PASSWORD")
-
- t.Setenv("GIT_HTTP_TOKEN", "mytoken")
- a, err = g.auth(mustParseURL("git+https://user@example.com/foo"))
- assert.NilError(t, err)
- assert.DeepEqual(t, &http.TokenAuth{Token: "mytoken"}, a)
- os.Unsetenv("GIT_HTTP_TOKEN")
-
- if os.Getenv("SSH_AUTH_SOCK") == "" {
- t.Log("no SSH_AUTH_SOCK - skipping ssh agent test")
- } else {
- a, err = g.auth(mustParseURL("git+ssh://git@example.com/foo"))
- assert.NilError(t, err)
- sa, ok := a.(*ssh.PublicKeysCallback)
- assert.Equal(t, true, ok)
- assert.Equal(t, "git", sa.User)
- }
-
- t.Run("plain string ed25519", func(t *testing.T) {
- key := string(testdata.PEMBytes["ed25519"])
- t.Setenv("GIT_SSH_KEY", key)
- a, err = g.auth(mustParseURL("git+ssh://git@example.com/foo"))
- assert.NilError(t, err)
- ka, ok := a.(*ssh.PublicKeys)
- assert.Equal(t, true, ok)
- assert.Equal(t, "git", ka.User)
- })
-
- t.Run("base64 ed25519", func(t *testing.T) {
- key := base64.StdEncoding.EncodeToString(testdata.PEMBytes["ed25519"])
- t.Setenv("GIT_SSH_KEY", key)
- a, err = g.auth(mustParseURL("git+ssh://git@example.com/foo"))
- assert.NilError(t, err)
- ka, ok := a.(*ssh.PublicKeys)
- assert.Equal(t, true, ok)
- assert.Equal(t, "git", ka.User)
- os.Unsetenv("GIT_SSH_KEY")
- })
-}
-
-func TestRefFromURL(t *testing.T) {
- t.Parallel()
- g := gitsource{}
- data := []struct {
- url, expected string
- }{
- {"git://localhost:1234/foo/bar.git//baz", ""},
- {"git+http://localhost:1234/foo/bar.git//baz", ""},
- {"git+ssh://localhost:1234/foo/bar.git//baz", ""},
- {"git+file:///foo/bar.git//baz", ""},
- {"git://localhost:1234/foo/bar.git//baz#master", "refs/heads/master"},
- {"git+http://localhost:1234/foo/bar.git//baz#mybranch", "refs/heads/mybranch"},
- {"git+ssh://localhost:1234/foo/bar.git//baz#refs/tags/foo", "refs/tags/foo"},
- {"git+file:///foo/bar.git//baz#mybranch", "refs/heads/mybranch"},
- }
-
- for _, d := range data {
- out := g.refFromURL(mustParseURL(d.url))
- assert.Equal(t, plumbing.ReferenceName(d.expected), out)
- }
-}
diff --git a/data/datasource_http.go b/data/datasource_http.go
deleted file mode 100644
index 23c7dc36..00000000
--- a/data/datasource_http.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "io"
- "mime"
- "net/http"
- "net/url"
- "time"
-)
-
-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, fmt.Errorf("bad sub-path %s: %w", args[0], err)
- }
- return base.ResolveReference(p), nil
-}
-
-func readHTTP(ctx context.Context, 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.NewRequestWithContext(ctx, http.MethodGet, 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 := io.ReadAll(res.Body)
- if err != nil {
- return nil, err
- }
- err = res.Body.Close()
- if err != nil {
- return nil, err
- }
- if res.StatusCode != 200 {
- err := fmt.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
-}
diff --git a/data/datasource_http_test.go b/data/datasource_http_test.go
deleted file mode 100644
index 90c4a7f9..00000000
--- a/data/datasource_http_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package data
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-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{
- Ctx: context.Background(),
- Sources: sources,
- }
-
- expected := map[string]interface{}{
- "hello": "world",
- }
-
- actual, err := data.Datasource("foo")
- require.NoError(t, err)
- assert.Equal(t, must(marshalObj(expected, json.Marshal)), must(marshalObj(actual, json.Marshal)))
-
- actual, err = data.Datasource(server.URL)
- require.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{
- Ctx: context.Background(),
- Sources: sources,
- }
- expected := http.Header{
- "Accept-Encoding": {"test"},
- "Foo": {"bar", "baz"},
- }
- actual, err := data.Datasource("foo")
- require.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{
- Ctx: context.Background(),
- Sources: sources,
- ExtraHeaders: map[string]http.Header{server.URL: expected},
- }
- actual, err = data.Datasource(server.URL)
- require.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)
- require.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")
- require.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")
- require.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")
- require.NoError(t, err)
- assert.Equal(t, expected, u.String())
-}
diff --git a/data/datasource_merge.go b/data/datasource_merge.go
deleted file mode 100644
index b0d8b6bd..00000000
--- a/data/datasource_merge.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/coll"
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
-)
-
-// readMerge demultiplexes a `merge:` datasource. The 'args' parameter currently
-// has no meaning for this source.
-//
-// URI format is 'merge:<source 1>|<source 2>[|<source n>...]' where `<source #>`
-// is a supported URI or a pre-defined alias name.
-//
-// Query strings and fragments are interpreted relative to the merged data, not
-// the source data. To merge datasources with query strings or fragments, define
-// separate sources first and specify the alias names. HTTP headers are also not
-// supported directly.
-func (d *Data) readMerge(ctx context.Context, source *Source, _ ...string) ([]byte, error) {
- opaque := source.URL.Opaque
- parts := strings.Split(opaque, "|")
- if len(parts) < 2 {
- return nil, fmt.Errorf("need at least 2 datasources to merge")
- }
- data := make([]map[string]interface{}, len(parts))
- for i, part := range parts {
- // supports either URIs or aliases
- subSource, err := d.lookupSource(part)
- if err != nil {
- // maybe it's a relative filename?
- u, uerr := datafs.ParseSourceURL(part)
- if uerr != nil {
- return nil, uerr
- }
- subSource = &Source{
- Alias: part,
- URL: u,
- }
- }
- subSource.inherit(source)
-
- b, err := d.readSource(ctx, subSource)
- if err != nil {
- return nil, fmt.Errorf("couldn't read datasource '%s': %w", part, err)
- }
-
- mimeType, err := subSource.mimeType("")
- if err != nil {
- return nil, fmt.Errorf("failed to read datasource %s: %w", subSource.URL, err)
- }
-
- data[i], err = parseMap(mimeType, string(b))
- if err != nil {
- return nil, err
- }
- }
-
- // Merge the data together
- b, err := mergeData(data)
- if err != nil {
- return nil, err
- }
-
- source.mediaType = yamlMimetype
- return b, nil
-}
-
-func mergeData(data []map[string]interface{}) (out []byte, err error) {
- dst := data[0]
- data = data[1:]
-
- dst, err = coll.Merge(dst, data...)
- if err != nil {
- return nil, err
- }
-
- s, err := ToYAML(dst)
- if err != nil {
- return nil, err
- }
- return []byte(s), nil
-}
-
-func parseMap(mimeType, data string) (map[string]interface{}, error) {
- datum, err := parseData(mimeType, data)
- if err != nil {
- return nil, err
- }
- var m map[string]interface{}
- switch datum := datum.(type) {
- case map[string]interface{}:
- m = datum
- default:
- return nil, fmt.Errorf("unexpected data type '%T' for datasource (type %s); merge: can only merge maps", datum, mimeType)
- }
- return m, nil
-}
diff --git a/data/datasource_merge_test.go b/data/datasource_merge_test.go
deleted file mode 100644
index 48d1f85e..00000000
--- a/data/datasource_merge_test.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package data
-
-import (
- "context"
- "io/fs"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "testing"
- "testing/fstest"
-
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestReadMerge(t *testing.T) {
- ctx := context.Background()
-
- jsonContent := `{"hello": "world"}`
- yamlContent := "hello: earth\ngoodnight: moon\n"
- arrayContent := `["hello", "world"]`
-
- mergedContent := "goodnight: moon\nhello: world\n"
-
- wd, _ := os.Getwd()
-
- // MapFS doesn't support windows path separators, so we use / exclusively
- // in this test
- vol := filepath.VolumeName(wd)
- if vol != "" && wd != vol {
- wd = wd[len(vol)+1:]
- } else if wd[0] == '/' {
- wd = wd[1:]
- }
- wd = filepath.ToSlash(wd)
-
- fsys := datafs.WrapWdFS(fstest.MapFS{
- "tmp": {Mode: fs.ModeDir | 0o777},
- "tmp/jsonfile.json": {Data: []byte(jsonContent)},
- "tmp/array.json": {Data: []byte(arrayContent)},
- "tmp/yamlfile.yaml": {Data: []byte(yamlContent)},
- "tmp/textfile.txt": {Data: []byte(`plain text...`)},
- path.Join(wd, "jsonfile.json"): {Data: []byte(jsonContent)},
- path.Join(wd, "array.json"): {Data: []byte(arrayContent)},
- path.Join(wd, "yamlfile.yaml"): {Data: []byte(yamlContent)},
- path.Join(wd, "textfile.txt"): {Data: []byte(`plain text...`)},
- })
-
- source := &Source{Alias: "foo", URL: mustParseURL("merge:file:///tmp/jsonfile.json|file:///tmp/yamlfile.yaml")}
- source.fs = fsys
- d := &Data{
- Sources: map[string]*Source{
- "foo": source,
- "bar": {Alias: "bar", URL: mustParseURL("file:///tmp/jsonfile.json")},
- "baz": {Alias: "baz", URL: mustParseURL("file:///tmp/yamlfile.yaml")},
- "text": {Alias: "text", URL: mustParseURL("file:///tmp/textfile.txt")},
- "badscheme": {Alias: "badscheme", URL: mustParseURL("bad:///scheme.json")},
- "badtype": {Alias: "badtype", URL: mustParseURL("file:///tmp/textfile.txt?type=foo/bar")},
- "array": {Alias: "array", URL: mustParseURL("file:///tmp/array.json?type=" + url.QueryEscape(jsonArrayMimetype))},
- },
- }
-
- actual, err := d.readMerge(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, mergedContent, string(actual))
-
- source.URL = mustParseURL("merge:bar|baz")
- actual, err = d.readMerge(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, mergedContent, string(actual))
-
- source.URL = mustParseURL("merge:jsonfile.json|baz")
- actual, err = d.readMerge(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, mergedContent, string(actual))
-
- source.URL = mustParseURL("merge:./jsonfile.json|baz")
- actual, err = d.readMerge(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, mergedContent, string(actual))
-
- source.URL = mustParseURL("merge:file:///tmp/jsonfile.json")
- _, err = d.readMerge(ctx, source)
- assert.Error(t, err)
-
- source.URL = mustParseURL("merge:bogusalias|file:///tmp/jsonfile.json")
- _, err = d.readMerge(ctx, source)
- assert.Error(t, err)
-
- source.URL = mustParseURL("merge:file:///tmp/jsonfile.json|badscheme")
- _, err = d.readMerge(ctx, source)
- assert.Error(t, err)
-
- source.URL = mustParseURL("merge:file:///tmp/jsonfile.json|badtype")
- _, err = d.readMerge(ctx, source)
- assert.Error(t, err)
-
- source.URL = mustParseURL("merge:file:///tmp/jsonfile.json|array")
- _, err = d.readMerge(ctx, source)
- assert.Error(t, err)
-}
-
-func TestMergeData(t *testing.T) {
- def := map[string]interface{}{
- "f": true,
- "t": false,
- "z": "def",
- }
- out, err := mergeData([]map[string]interface{}{def})
- require.NoError(t, err)
- assert.Equal(t, "f: true\nt: false\nz: def\n", string(out))
-
- over := map[string]interface{}{
- "f": false,
- "t": true,
- "z": "over",
- }
- out, err = mergeData([]map[string]interface{}{over, def})
- require.NoError(t, err)
- assert.Equal(t, "f: false\nt: true\nz: over\n", string(out))
-
- over = map[string]interface{}{
- "f": false,
- "t": true,
- "z": "over",
- "m": map[string]interface{}{
- "a": "aaa",
- },
- }
- out, err = mergeData([]map[string]interface{}{over, def})
- require.NoError(t, err)
- assert.Equal(t, "f: false\nm:\n a: aaa\nt: true\nz: over\n", string(out))
-
- uber := map[string]interface{}{
- "z": "über",
- }
- out, err = mergeData([]map[string]interface{}{uber, over, def})
- require.NoError(t, err)
- assert.Equal(t, "f: false\nm:\n a: aaa\nt: true\nz: über\n", string(out))
-
- uber = map[string]interface{}{
- "m": "notamap",
- "z": map[string]interface{}{
- "b": "bbb",
- },
- }
- out, err = mergeData([]map[string]interface{}{uber, over, def})
- require.NoError(t, err)
- assert.Equal(t, "f: false\nm: notamap\nt: true\nz:\n b: bbb\n", string(out))
-
- uber = map[string]interface{}{
- "m": map[string]interface{}{
- "b": "bbb",
- },
- }
- out, err = mergeData([]map[string]interface{}{uber, over, def})
- require.NoError(t, err)
- assert.Equal(t, "f: false\nm:\n a: aaa\n b: bbb\nt: true\nz: over\n", string(out))
-}
diff --git a/data/datasource_stdin.go b/data/datasource_stdin.go
deleted file mode 100644
index 13bb5fa4..00000000
--- a/data/datasource_stdin.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "io"
- "os"
-)
-
-func readStdin(ctx context.Context, _ *Source, _ ...string) ([]byte, error) {
- stdin := stdinFromContext(ctx)
-
- b, err := io.ReadAll(stdin)
- if err != nil {
- return nil, fmt.Errorf("can't read %s: %w", stdin, err)
- }
- return b, nil
-}
-
-type stdinCtxKey struct{}
-
-func ContextWithStdin(ctx context.Context, r io.Reader) context.Context {
- return context.WithValue(ctx, stdinCtxKey{}, r)
-}
-
-func stdinFromContext(ctx context.Context) io.Reader {
- if r, ok := ctx.Value(stdinCtxKey{}).(io.Reader); ok {
- return r
- }
-
- return os.Stdin
-}
diff --git a/data/datasource_stdin_test.go b/data/datasource_stdin_test.go
deleted file mode 100644
index 8cef6827..00000000
--- a/data/datasource_stdin_test.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package data
-
-import (
- "context"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestReadStdin(t *testing.T) {
- ctx := context.Background()
-
- ctx = ContextWithStdin(ctx, strings.NewReader("foo"))
- out, err := readStdin(ctx, nil)
- require.NoError(t, err)
- assert.Equal(t, []byte("foo"), out)
-
- ctx = ContextWithStdin(ctx, errorReader{})
- _, err = readStdin(ctx, nil)
- assert.Error(t, err)
-}
diff --git a/data/datasource_test.go b/data/datasource_test.go
index a77f2645..da3d3094 100644
--- a/data/datasource_test.go
+++ b/data/datasource_test.go
@@ -2,13 +2,16 @@ package data
import (
"context"
- "fmt"
"net/http"
+ "net/http/httptest"
"net/url"
+ "os"
"runtime"
"testing"
"testing/fstest"
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/go-fsimpl/httpfs"
"github.com/hairyhenderson/gomplate/v4/internal/config"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
@@ -18,6 +21,11 @@ import (
const osWindows = "windows"
+func mustParseURL(in string) *url.URL {
+ u, _ := url.Parse(in)
+ return u
+}
+
func TestNewData(t *testing.T) {
d, err := NewData(nil, nil)
require.NoError(t, err)
@@ -56,21 +64,22 @@ func TestDatasource(t *testing.T) {
fsys := datafs.WrapWdFS(fstest.MapFS{
"tmp/" + fname: &fstest.MapFile{Data: contents},
})
+ ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
sources := map[string]*Source{
"foo": {
Alias: "foo",
URL: &url.URL{Scheme: "file", Path: uPath},
mediaType: mime,
- fs: fsys,
},
}
- return &Data{Sources: sources}
+ return &Data{Sources: sources, Ctx: ctx}
}
+
test := func(ext, mime string, contents []byte, expected interface{}) {
data := setup(ext, mime, contents)
- actual, err := data.Datasource("foo")
+ actual, err := data.Datasource("foo", "?type="+mime)
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
@@ -110,21 +119,20 @@ func TestDatasourceReachable(t *testing.T) {
fsys := datafs.WrapWdFS(fstest.MapFS{
"tmp/" + fname: &fstest.MapFile{Data: []byte("{}")},
})
+ ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
sources := map[string]*Source{
"foo": {
Alias: "foo",
URL: &url.URL{Scheme: "file", Path: uPath},
mediaType: jsonMimetype,
- fs: fsys,
},
"bar": {
Alias: "bar",
URL: &url.URL{Scheme: "file", Path: "/bogus"},
- fs: fsys,
},
}
- data := &Data{Sources: sources}
+ data := &Data{Sources: sources, Ctx: ctx}
assert.True(t, data.DatasourceReachable("foo"))
assert.False(t, data.DatasourceReachable("bar"))
@@ -154,29 +162,21 @@ func TestInclude(t *testing.T) {
fsys := datafs.WrapWdFS(fstest.MapFS{
"tmp/" + fname: &fstest.MapFile{Data: []byte(contents)},
})
+ ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
sources := map[string]*Source{
"foo": {
Alias: "foo",
URL: &url.URL{Scheme: "file", Path: uPath},
mediaType: textMimetype,
- fs: fsys,
},
}
- data := &Data{
- Sources: sources,
- }
+ data := &Data{Sources: sources, Ctx: ctx}
actual, err := data.Include("foo")
require.NoError(t, err)
assert.Equal(t, contents, actual)
}
-type errorReader struct{}
-
-func (e errorReader) Read(_ []byte) (n int, err error) {
- return 0, fmt.Errorf("error")
-}
-
func TestDefineDatasource(t *testing.T) {
d := &Data{}
_, err := d.DefineDatasource("", "foo.json")
@@ -231,134 +231,6 @@ func TestDefineDatasource(t *testing.T) {
s = d.Sources["data"]
require.NoError(t, err)
assert.Equal(t, "data", s.Alias)
- m, err := s.mimeType("")
- require.NoError(t, err)
- assert.Equal(t, "application/x-env", m)
-}
-
-func TestMimeType(t *testing.T) {
- s := &Source{URL: mustParseURL("http://example.com/list?type=a/b/c")}
- _, err := s.mimeType("")
- assert.Error(t, err)
-
- data := []struct {
- url string
- mediaType string
- expected string
- }{
- {
- "http://example.com/foo.json",
- "",
- jsonMimetype,
- },
- {
- "http://example.com/foo.json",
- "text/foo",
- "text/foo",
- },
- {
- "http://example.com/foo.json?type=application/yaml",
- "text/foo",
- "application/yaml",
- },
- {
- "http://example.com/list?type=application/array%2Bjson",
- "text/foo",
- "application/array+json",
- },
- {
- "http://example.com/list?type=application/array+json",
- "",
- "application/array+json",
- },
- {
- "http://example.com/unknown",
- "",
- "text/plain",
- },
- }
-
- for i, d := range data {
- d := d
- t.Run(fmt.Sprintf("%d:%q,%q==%q", i, d.url, d.mediaType, d.expected), func(t *testing.T) {
- s := &Source{URL: mustParseURL(d.url), mediaType: d.mediaType}
- mt, err := s.mimeType("")
- require.NoError(t, err)
- assert.Equal(t, d.expected, mt)
- })
- }
-}
-
-func TestMimeTypeWithArg(t *testing.T) {
- s := &Source{URL: mustParseURL("http://example.com")}
- _, err := s.mimeType("h\nttp://foo")
- assert.Error(t, err)
-
- data := []struct {
- url string
- mediaType string
- arg string
- expected string
- }{
- {
- "http://example.com/unknown",
- "",
- "/foo.json",
- "application/json",
- },
- {
- "http://example.com/unknown",
- "",
- "foo.json",
- "application/json",
- },
- {
- "http://example.com/",
- "text/foo",
- "/foo.json",
- "text/foo",
- },
- {
- "git+https://example.com/myrepo",
- "",
- "//foo.yaml",
- "application/yaml",
- },
- {
- "http://example.com/foo.json",
- "",
- "/foo.yaml",
- "application/yaml",
- },
- {
- "http://example.com/foo.json?type=application/array+yaml",
- "",
- "/foo.yaml",
- "application/array+yaml",
- },
- {
- "http://example.com/foo.json?type=application/array+yaml",
- "",
- "/foo.yaml?type=application/yaml",
- "application/yaml",
- },
- {
- "http://example.com/foo.json?type=application/array+yaml",
- "text/plain",
- "/foo.yaml?type=application/yaml",
- "application/yaml",
- },
- }
-
- for i, d := range data {
- d := d
- t.Run(fmt.Sprintf("%d:%q,%q,%q==%q", i, d.url, d.mediaType, d.arg, d.expected), func(t *testing.T) {
- s := &Source{URL: mustParseURL(d.url), mediaType: d.mediaType}
- mt, err := s.mimeType(d.arg)
- require.NoError(t, err)
- assert.Equal(t, d.expected, mt)
- })
- }
}
func TestFromConfig(t *testing.T) {
@@ -445,3 +317,94 @@ func TestListDatasources(t *testing.T) {
assert.Equal(t, []string{"bar", "foo"}, data.ListDatasources())
}
+
+func TestResolveURL(t *testing.T) {
+ out, err := resolveURL(mustParseURL("http://example.com/foo.json"), "bar.json")
+ assert.NoError(t, err)
+ assert.Equal(t, "http://example.com/bar.json", out.String())
+
+ out, err = resolveURL(mustParseURL("http://example.com/a/b/?n=2"), "bar.json?q=1")
+ assert.NoError(t, err)
+ assert.Equal(t, "http://example.com/a/b/bar.json?n=2&q=1", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo"), "//myfile?type=application/json")
+ assert.NoError(t, err)
+ assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/foo/bar/"), "//myfile?type=application/json")
+ assert.NoError(t, err)
+ assert.Equal(t, "git+file:///tmp/foo/bar//myfile?type=application/json", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo/"), ".//myfile?type=application/json")
+ assert.NoError(t, err)
+ assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/repo//foo.txt"), "")
+ assert.NoError(t, err)
+ assert.Equal(t, "git+file:///tmp/repo//foo.txt", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo"), ".//myfile?type=application/json")
+ assert.NoError(t, err)
+ assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
+
+ out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo//foo/?type=application/json"), "bar/myfile")
+ assert.NoError(t, err)
+ // note that the '/' in the query string is encoded to %2F - that's OK
+ assert.Equal(t, "git+file:///tmp/myrepo//foo/bar/myfile?type=application%2Fjson", out.String())
+
+ // both base and relative may not contain "//"
+ _, err = resolveURL(mustParseURL("git+ssh://git@example.com/foo//bar"), ".//myfile")
+ assert.Error(t, err)
+
+ _, err = resolveURL(mustParseURL("git+ssh://git@example.com/foo//bar"), "baz//myfile")
+ assert.Error(t, err)
+
+ // relative urls must remain relative
+ out, err = resolveURL(mustParseURL("tmp/foo.json"), "")
+ require.NoError(t, err)
+ assert.Equal(t, "tmp/foo.json", out.String())
+}
+
+func TestReadFileContent(t *testing.T) {
+ wd, _ := os.Getwd()
+ t.Cleanup(func() {
+ _ = os.Chdir(wd)
+ })
+ _ = os.Chdir("/")
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/foo.json", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", jsonMimetype)
+ w.Write([]byte(`{"foo": "bar"}`))
+ })
+
+ srv := httptest.NewServer(mux)
+ t.Cleanup(srv.Close)
+
+ fsys := datafs.WrapWdFS(fstest.MapFS{
+ "foo.json": &fstest.MapFile{Data: []byte(`{"foo": "bar"}`)},
+ "dir/1.yaml": &fstest.MapFile{Data: []byte(`foo: bar`)},
+ "dir/2.yaml": &fstest.MapFile{Data: []byte(`baz: qux`)},
+ "dir/sub/sub1.yaml": &fstest.MapFile{Data: []byte(`quux: corge`)},
+ })
+
+ fsp := fsimpl.NewMux()
+ fsp.Add(httpfs.FS)
+ fsp.Add(datafs.WrappedFSProvider(fsys, "file", ""))
+
+ ctx := datafs.ContextWithFSProvider(context.Background(), fsp)
+
+ d := Data{}
+
+ fc, err := d.readFileContent(ctx, mustParseURL("file:///foo.json"), nil)
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`{"foo": "bar"}`), fc.b)
+
+ fc, err = d.readFileContent(ctx, mustParseURL("dir/"), nil)
+ require.NoError(t, err)
+ assert.JSONEq(t, `["1.yaml", "2.yaml", "sub"]`, string(fc.b))
+
+ fc, err = d.readFileContent(ctx, mustParseURL(srv.URL+"/foo.json"), nil)
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`{"foo": "bar"}`), fc.b)
+}
diff --git a/data/datasource_vault.go b/data/datasource_vault.go
deleted file mode 100644
index 5f736dcb..00000000
--- a/data/datasource_vault.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package data
-
-import (
- "context"
- "fmt"
- "strings"
-
- "github.com/hairyhenderson/gomplate/v4/vault"
-)
-
-func readVault(_ context.Context, source *Source, args ...string) (data []byte, err error) {
- if source.vc == nil {
- source.vc, err = vault.New(source.URL)
- if err != nil {
- return nil, err
- }
- err = source.vc.Login()
- if err != nil {
- return nil, err
- }
- }
-
- params, p, err := parseDatasourceURLArgs(source.URL, args...)
- if err != nil {
- return nil, err
- }
-
- source.mediaType = jsonMimetype
- switch {
- case len(params) > 0:
- data, err = source.vc.Write(p, params)
- case strings.HasSuffix(p, "/"):
- source.mediaType = jsonArrayMimetype
- data, err = source.vc.List(p)
- default:
- data, err = source.vc.Read(p)
- }
- if err != nil {
- return nil, err
- }
-
- if len(data) == 0 {
- return nil, fmt.Errorf("no value found for path %s", p)
- }
-
- return data, nil
-}
diff --git a/data/datasource_vault_test.go b/data/datasource_vault_test.go
deleted file mode 100644
index 1c63d696..00000000
--- a/data/datasource_vault_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package data
-
-import (
- "context"
- "net/url"
- "testing"
-
- "github.com/hairyhenderson/gomplate/v4/vault"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestReadVault(t *testing.T) {
- ctx := context.Background()
-
- expected := "{\"value\":\"foo\"}\n"
- server, v := vault.MockServer(200, `{"data":`+expected+`}`)
- defer server.Close()
-
- source := &Source{
- Alias: "foo",
- URL: &url.URL{Scheme: "vault", Path: "/secret/foo"},
- mediaType: textMimetype,
- vc: v,
- }
-
- r, err := readVault(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, []byte(expected), r)
-
- r, err = readVault(ctx, source, "bar")
- require.NoError(t, err)
- assert.Equal(t, []byte(expected), r)
-
- r, err = readVault(ctx, source, "?param=value")
- require.NoError(t, err)
- assert.Equal(t, []byte(expected), r)
-
- source.URL, _ = url.Parse("vault:///secret/foo?param1=value1&param2=value2")
- r, err = readVault(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, []byte(expected), r)
-
- expected = "[\"one\",\"two\"]\n"
- server, source.vc = vault.MockServer(200, `{"data":{"keys":`+expected+`}}`)
- defer server.Close()
- source.URL, _ = url.Parse("vault:///secret/foo/")
- r, err = readVault(ctx, source)
- require.NoError(t, err)
- assert.Equal(t, []byte(expected), r)
-}
diff --git a/data/mimetypes.go b/data/mimetypes.go
index 1f243219..24ea87de 100644
--- a/data/mimetypes.go
+++ b/data/mimetypes.go
@@ -1,5 +1,9 @@
package data
+import (
+ "mime"
+)
+
const (
textMimetype = "text/plain"
csvMimetype = "text/csv"
@@ -19,6 +23,9 @@ var mimeTypeAliases = map[string]string{
}
func mimeAlias(m string) string {
+ // normalize the type by removing any extra parameters
+ m, _, _ = mime.ParseMediaType(m)
+
if a, ok := mimeTypeAliases[m]; ok {
return a
}
diff --git a/data/mimetypes_test.go b/data/mimetypes_test.go
index 0dd1ab05..04c54439 100644
--- a/data/mimetypes_test.go
+++ b/data/mimetypes_test.go
@@ -3,7 +3,7 @@ package data
import (
"testing"
- "gotest.tools/v3/assert"
+ "github.com/stretchr/testify/assert"
)
func TestMimeAlias(t *testing.T) {