summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.golangci.yml2
-rw-r--r--aws/ec2meta.go3
-rw-r--r--context.go15
-rw-r--r--context_test.go7
-rw-r--r--crypto/pbkdf2.go23
-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
-rw-r--r--docs-src/content/functions/aws.yml3
-rw-r--r--docs/content/functions/aws.md3
-rw-r--r--env/env.go52
-rw-r--r--env/env_test.go91
-rw-r--r--funcs/data.go31
-rw-r--r--go.mod88
-rw-r--r--go.sum211
-rw-r--r--gomplate.go5
-rw-r--r--gomplate_test.go11
-rw-r--r--internal/cmd/config.go2
-rw-r--r--internal/cmd/main.go10
-rw-r--r--internal/config/configfile.go6
-rw-r--r--internal/config/types.go6
-rw-r--r--internal/datafs/context.go45
-rw-r--r--internal/datafs/envfs.go221
-rw-r--r--internal/datafs/envfs_test.go140
-rw-r--r--internal/datafs/fsurl.go42
-rw-r--r--internal/datafs/fsurl_test.go106
-rw-r--r--internal/datafs/fsys.go98
-rw-r--r--internal/datafs/fsys_test.go98
-rw-r--r--internal/datafs/getenv.go51
-rw-r--r--internal/datafs/getenv_test.go97
-rw-r--r--internal/datafs/mergefs.go280
-rw-r--r--internal/datafs/mergefs_test.go358
-rw-r--r--internal/datafs/stdinfs.go110
-rw-r--r--internal/datafs/stdinfs_test.go109
-rw-r--r--internal/datafs/vaultauth.go89
-rw-r--r--internal/datafs/wdfs.go100
-rw-r--r--internal/datafs/wdfs_test.go35
-rw-r--r--internal/deprecated/deprecated.go7
-rw-r--r--internal/iohelpers/mimetypes.go33
-rw-r--r--internal/iohelpers/write_test.go71
-rw-r--r--internal/iohelpers/writers_test.go86
-rw-r--r--internal/parsers/parsefuncs.go (renamed from data/data.go)41
-rw-r--r--internal/parsers/parsefuncs_test.go (renamed from data/data_test.go)4
-rw-r--r--internal/parsers/parser.go39
-rw-r--r--internal/tests/integration/basic_test.go27
-rw-r--r--internal/tests/integration/datasources_consul_test.go19
-rw-r--r--internal/tests/integration/datasources_file_test.go3
-rw-r--r--internal/tests/integration/datasources_git_test.go4
-rw-r--r--internal/tests/integration/datasources_vault_ec2_test.go9
-rw-r--r--internal/tests/integration/datasources_vault_test.go37
-rw-r--r--internal/tests/integration/integration_test.go4
-rw-r--r--internal/urlhelpers/urlhelpers.go46
-rw-r--r--internal/urlhelpers/urlhelpers_test.go42
-rw-r--r--libkv/consul.go193
-rw-r--r--libkv/consul_test.go176
-rw-r--r--libkv/libkv.go64
-rw-r--r--libkv/libkv_test.go86
-rw-r--r--render.go40
-rw-r--r--render_test.go10
-rw-r--r--template.go13
-rw-r--r--vault/auth.go205
-rw-r--r--vault/auth_test.go26
-rw-r--r--vault/testutils.go32
-rw-r--r--vault/vault.go118
-rw-r--r--vault/vault_test.go68
88 files changed, 2967 insertions, 4409 deletions
diff --git a/.golangci.yml b/.golangci.yml
index 2ddcc5c7..afcd93bc 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -33,7 +33,7 @@ linters:
# - funlen
# - gci
# - gochecknoglobals
- # - gochecknoinits
+ - gochecknoinits
- gocognit
- goconst
- gocritic
diff --git a/aws/ec2meta.go b/aws/ec2meta.go
index 6c1dd76f..c6cab50d 100644
--- a/aws/ec2meta.go
+++ b/aws/ec2meta.go
@@ -1,6 +1,7 @@
package aws
import (
+ "context"
"net/http"
"strings"
@@ -8,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/hairyhenderson/gomplate/v4/env"
+ "github.com/hairyhenderson/gomplate/v4/internal/deprecated"
)
const (
@@ -38,6 +40,7 @@ func NewEc2Meta(options ClientOptions) *Ec2Meta {
config := aws.NewConfig()
config = config.WithHTTPClient(&http.Client{Timeout: options.Timeout})
if endpoint := env.Getenv("AWS_META_ENDPOINT"); endpoint != "" {
+ deprecated.WarnDeprecated(context.Background(), "Use AWS_EC2_METADATA_SERVICE_ENDPOINT instead of AWS_META_ENDPOINT")
config = config.WithEndpoint(endpoint)
}
diff --git a/context.go b/context.go
index dd8557de..f98ee3b3 100644
--- a/context.go
+++ b/context.go
@@ -22,9 +22,18 @@ func (c *tmplctx) Env() map[string]string {
}
// createTmplContext reads the datasources for the given aliases
-//
-//nolint:staticcheck
-func createTmplContext(_ context.Context, aliases []string, d *data.Data) (interface{}, error) {
+func createTmplContext(
+ ctx context.Context, aliases []string,
+ //nolint:staticcheck
+ d *data.Data,
+) (interface{}, error) {
+ // we need to inject the current context into the Data value, because
+ // the Datasource method may need it
+ // TODO: remove this before v4
+ if d != nil {
+ d.Ctx = ctx
+ }
+
var err error
tctx := &tmplctx{}
for _, a := range aliases {
diff --git a/context_test.go b/context_test.go
index aa3a4468..c8885a47 100644
--- a/context_test.go
+++ b/context_test.go
@@ -6,7 +6,9 @@ import (
"os"
"testing"
+ "github.com/hairyhenderson/go-fsimpl"
"github.com/hairyhenderson/gomplate/v4/data"
+ "github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -31,6 +33,11 @@ func TestCreateContext(t *testing.T) {
require.NoError(t, err)
assert.Empty(t, c)
+ fsmux := fsimpl.NewMux()
+ fsmux.Add(datafs.EnvFS)
+
+ ctx = datafs.ContextWithFSProvider(ctx, fsmux)
+
fooURL := "env:///foo?type=application/yaml"
barURL := "env:///bar?type=application/yaml"
uf, _ := url.Parse(fooURL)
diff --git a/crypto/pbkdf2.go b/crypto/pbkdf2.go
index d5217429..0cc44d7e 100644
--- a/crypto/pbkdf2.go
+++ b/crypto/pbkdf2.go
@@ -7,22 +7,23 @@ import (
"crypto/sha512"
"fmt"
"hash"
+ "sync"
"golang.org/x/crypto/pbkdf2"
)
-var hashFuncs map[crypto.Hash]func() hash.Hash
+var hashFuncs = sync.OnceValue[map[crypto.Hash]func() hash.Hash](func() map[crypto.Hash]func() hash.Hash {
+ h := make(map[crypto.Hash]func() hash.Hash)
+ h[crypto.SHA1] = sha1.New
+ h[crypto.SHA224] = sha256.New224
+ h[crypto.SHA256] = sha256.New
+ h[crypto.SHA384] = sha512.New384
+ h[crypto.SHA512] = sha512.New
+ h[crypto.SHA512_224] = sha512.New512_224
+ h[crypto.SHA512_256] = sha512.New512_256
-func init() {
- hashFuncs = make(map[crypto.Hash]func() hash.Hash)
- hashFuncs[crypto.SHA1] = sha1.New
- hashFuncs[crypto.SHA224] = sha256.New224
- hashFuncs[crypto.SHA256] = sha256.New
- hashFuncs[crypto.SHA384] = sha512.New384
- hashFuncs[crypto.SHA512] = sha512.New
- hashFuncs[crypto.SHA512_224] = sha512.New512_224
- hashFuncs[crypto.SHA512_256] = sha512.New512_256
-}
+ return h
+})()
// StrToHash - find a hash given a certain string
func StrToHash(hash string) (crypto.Hash, error) {
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) {
diff --git a/docs-src/content/functions/aws.yml b/docs-src/content/functions/aws.yml
index 749397c3..9b02d7c5 100644
--- a/docs-src/content/functions/aws.yml
+++ b/docs-src/content/functions/aws.yml
@@ -16,7 +16,8 @@ preamble: |
| `AWS_TIMEOUT` | _(Default `500`)_ Adjusts timeout for API requests, in milliseconds. Not part of the AWS SDK. |
| `AWS_PROFILE` | Profile name the SDK should use when loading shared config from the configuration files. If not provided `default` will be used as the profile name. |
| `AWS_REGION` | Specifies where to send requests. See [this list](https://docs.aws.amazon.com/general/latest/gr/rande.html). Note that the region must be set for AWS functions to work correctly, either through this variable, through a configuration profile, or by running on an EC2 instance. |
- | `AWS_META_ENDPOINT` | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. |
+ | `AWS_EC2_METADATA_SERVICE_ENDPOINT` | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. |
+ | `AWS_META_ENDPOINT` _(Deprecated)_ | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. Use `AWS_EC2_METADATA_SERVICE_ENDPOINT` instead. |
funcs:
- name: aws.EC2Meta
alias: ec2meta
diff --git a/docs/content/functions/aws.md b/docs/content/functions/aws.md
index c93cc764..5ad2ee18 100644
--- a/docs/content/functions/aws.md
+++ b/docs/content/functions/aws.md
@@ -21,7 +21,8 @@ for details.
| `AWS_TIMEOUT` | _(Default `500`)_ Adjusts timeout for API requests, in milliseconds. Not part of the AWS SDK. |
| `AWS_PROFILE` | Profile name the SDK should use when loading shared config from the configuration files. If not provided `default` will be used as the profile name. |
| `AWS_REGION` | Specifies where to send requests. See [this list](https://docs.aws.amazon.com/general/latest/gr/rande.html). Note that the region must be set for AWS functions to work correctly, either through this variable, through a configuration profile, or by running on an EC2 instance. |
-| `AWS_META_ENDPOINT` | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. |
+| `AWS_EC2_METADATA_SERVICE_ENDPOINT` | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. |
+| `AWS_META_ENDPOINT` _(Deprecated)_ | _(Default `http://169.254.169.254`)_ Sets the base address of the instance metadata service. Use `AWS_EC2_METADATA_SERVICE_ENDPOINT` instead. |
## `aws.EC2Meta`
diff --git a/env/env.go b/env/env.go
index e9ab3632..598361bf 100644
--- a/env/env.go
+++ b/env/env.go
@@ -2,10 +2,6 @@
package env
import (
- "io/fs"
- "os"
- "strings"
-
osfs "github.com/hack-pad/hackpadfs/os"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
)
@@ -16,55 +12,11 @@ import (
// Otherwise the provided default (or an emptry string) is returned.
func Getenv(key string, def ...string) string {
fsys := datafs.WrapWdFS(osfs.NewFS())
- return getenvVFS(fsys, key, def...)
+ return datafs.GetenvFsys(fsys, key, def...)
}
// ExpandEnv - like os.ExpandEnv, except supports `_FILE` vars as well
func ExpandEnv(s string) string {
fsys := datafs.WrapWdFS(osfs.NewFS())
- return expandEnvVFS(fsys, s)
-}
-
-// expandEnvVFS -
-func expandEnvVFS(fsys fs.FS, s string) string {
- return os.Expand(s, func(s string) string {
- return getenvVFS(fsys, s)
- })
-}
-
-// getenvVFS - a convenience function intended for internal use only!
-func getenvVFS(fsys fs.FS, key string, def ...string) string {
- val := getenvFile(fsys, key)
- if val == "" && len(def) > 0 {
- return def[0]
- }
-
- return val
-}
-
-func getenvFile(fsys fs.FS, key string) string {
- val := os.Getenv(key)
- if val != "" {
- return val
- }
-
- p := os.Getenv(key + "_FILE")
- if p != "" {
- val, err := readFile(fsys, p)
- if err != nil {
- return ""
- }
- return strings.TrimSpace(val)
- }
-
- return ""
-}
-
-func readFile(fsys fs.FS, p string) (string, error) {
- b, err := fs.ReadFile(fsys, p)
- if err != nil {
- return "", err
- }
-
- return string(b), nil
+ return datafs.ExpandEnvFsys(fsys, s)
}
diff --git a/env/env_test.go b/env/env_test.go
index faaaa603..9f637944 100644
--- a/env/env_test.go
+++ b/env/env_test.go
@@ -1,14 +1,8 @@
package env
import (
- "errors"
- "io/fs"
"os"
"testing"
- "testing/fstest"
-
- "github.com/hack-pad/hackpadfs"
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/stretchr/testify/assert"
)
@@ -19,25 +13,6 @@ func TestGetenv(t *testing.T) {
assert.Equal(t, "default value", Getenv("BLAHBLAHBLAH", "default value"))
}
-func TestGetenvFile(t *testing.T) {
- fsys := fs.FS(fstest.MapFS{
- "tmp": &fstest.MapFile{Mode: fs.ModeDir | 0o777},
- "tmp/foo": &fstest.MapFile{Data: []byte("foo")},
- "tmp/unreadable": &fstest.MapFile{Data: []byte("foo"), Mode: 0o000},
- })
- fsys = datafs.WrapWdFS(fsys)
-
- t.Setenv("FOO_FILE", "/tmp/foo")
- assert.Equal(t, "foo", getenvVFS(fsys, "FOO", "bar"))
-
- t.Setenv("FOO_FILE", "/tmp/missing")
- assert.Equal(t, "bar", getenvVFS(fsys, "FOO", "bar"))
-
- fsys = writeOnly(fsys)
- t.Setenv("FOO_FILE", "/tmp/unreadable")
- assert.Equal(t, "bar", getenvVFS(fsys, "FOO", "bar"))
-}
-
func TestExpandEnv(t *testing.T) {
assert.Empty(t, ExpandEnv("${FOOBARBAZ}"))
assert.Equal(t, os.Getenv("USER"), ExpandEnv("$USER"))
@@ -45,69 +20,3 @@ func TestExpandEnv(t *testing.T) {
assert.Equal(t, os.Getenv("USER")+": "+os.Getenv("HOME"),
ExpandEnv("$USER: ${HOME}"))
}
-
-func TestExpandEnvFile(t *testing.T) {
- fsys := fs.FS(fstest.MapFS{
- "tmp": &fstest.MapFile{Mode: fs.ModeDir | 0o777},
- "tmp/foo": &fstest.MapFile{Data: []byte("foo")},
- "tmp/unreadable": &fstest.MapFile{Data: []byte("foo"), Mode: 0o000},
- })
- fsys = datafs.WrapWdFS(fsys)
-
- t.Setenv("FOO_FILE", "/tmp/foo")
- assert.Equal(t, "foo is foo", expandEnvVFS(fsys, "foo is $FOO"))
-
- t.Setenv("FOO_FILE", "/tmp/missing")
- assert.Equal(t, "empty", expandEnvVFS(fsys, "${FOO}empty"))
-
- fsys = writeOnly(fsys)
- t.Setenv("FOO_FILE", "/tmp/unreadable")
- assert.Equal(t, "", expandEnvVFS(fsys, "${FOO}"))
-}
-
-// Maybe extract this into a separate package sometime...
-// writeOnly - represents a filesystem that's writeable, but read operations fail
-func writeOnly(fsys fs.FS) fs.FS {
- return &woFS{fsys}
-}
-
-type woFS struct {
- fsys fs.FS
-}
-
-func (fsys woFS) Open(name string) (fs.File, error) {
- f, err := fsys.fsys.Open(name)
- return writeOnlyFile(f), err
-}
-
-func (fsys woFS) ReadDir(_ string) ([]fs.DirEntry, error) {
- return nil, ErrWriteOnly
-}
-
-func (fsys woFS) Stat(_ string) (fs.FileInfo, error) {
- return nil, ErrWriteOnly
-}
-
-func writeOnlyFile(f fs.File) fs.File {
- if f == nil {
- return nil
- }
-
- return &woFile{f}
-}
-
-type woFile struct {
- fs.File
-}
-
-// Write -
-func (f woFile) Write(p []byte) (n int, err error) {
- return hackpadfs.WriteFile(f.File, p)
-}
-
-// Read is disabled and returns ErrWriteOnly
-func (f woFile) Read([]byte) (n int, err error) {
- return 0, ErrWriteOnly
-}
-
-var ErrWriteOnly = errors.New("filesystem is write-only")
diff --git a/funcs/data.go b/funcs/data.go
index e9ce7fa5..32f5b2da 100644
--- a/funcs/data.go
+++ b/funcs/data.go
@@ -5,6 +5,7 @@ import (
"github.com/hairyhenderson/gomplate/v4/conv"
"github.com/hairyhenderson/gomplate/v4/data"
+ "github.com/hairyhenderson/gomplate/v4/internal/parsers"
)
// DataNS -
@@ -65,75 +66,75 @@ type DataFuncs struct {
// JSON -
func (f *DataFuncs) JSON(in interface{}) (map[string]interface{}, error) {
- return data.JSON(conv.ToString(in))
+ return parsers.JSON(conv.ToString(in))
}
// JSONArray -
func (f *DataFuncs) JSONArray(in interface{}) ([]interface{}, error) {
- return data.JSONArray(conv.ToString(in))
+ return parsers.JSONArray(conv.ToString(in))
}
// YAML -
func (f *DataFuncs) YAML(in interface{}) (map[string]interface{}, error) {
- return data.YAML(conv.ToString(in))
+ return parsers.YAML(conv.ToString(in))
}
// YAMLArray -
func (f *DataFuncs) YAMLArray(in interface{}) ([]interface{}, error) {
- return data.YAMLArray(conv.ToString(in))
+ return parsers.YAMLArray(conv.ToString(in))
}
// TOML -
func (f *DataFuncs) TOML(in interface{}) (interface{}, error) {
- return data.TOML(conv.ToString(in))
+ return parsers.TOML(conv.ToString(in))
}
// CSV -
func (f *DataFuncs) CSV(args ...string) ([][]string, error) {
- return data.CSV(args...)
+ return parsers.CSV(args...)
}
// CSVByRow -
func (f *DataFuncs) CSVByRow(args ...string) (rows []map[string]string, err error) {
- return data.CSVByRow(args...)
+ return parsers.CSVByRow(args...)
}
// CSVByColumn -
func (f *DataFuncs) CSVByColumn(args ...string) (cols map[string][]string, err error) {
- return data.CSVByColumn(args...)
+ return parsers.CSVByColumn(args...)
}
// CUE -
func (f *DataFuncs) CUE(in interface{}) (interface{}, error) {
- return data.CUE(conv.ToString(in))
+ return parsers.CUE(conv.ToString(in))
}
// ToCSV -
func (f *DataFuncs) ToCSV(args ...interface{}) (string, error) {
- return data.ToCSV(args...)
+ return parsers.ToCSV(args...)
}
// ToCUE -
func (f *DataFuncs) ToCUE(in interface{}) (string, error) {
- return data.ToCUE(in)
+ return parsers.ToCUE(in)
}
// ToJSON -
func (f *DataFuncs) ToJSON(in interface{}) (string, error) {
- return data.ToJSON(in)
+ return parsers.ToJSON(in)
}
// ToJSONPretty -
func (f *DataFuncs) ToJSONPretty(indent string, in interface{}) (string, error) {
- return data.ToJSONPretty(indent, in)
+ return parsers.ToJSONPretty(indent, in)
}
// ToYAML -
func (f *DataFuncs) ToYAML(in interface{}) (string, error) {
- return data.ToYAML(in)
+ return parsers.ToYAML(in)
}
// ToTOML -
func (f *DataFuncs) ToTOML(in interface{}) (string, error) {
- return data.ToTOML(in)
+ return parsers.ToTOML(in)
}
diff --git a/go.mod b/go.mod
index 58334e88..548e945b 100644
--- a/go.mod
+++ b/go.mod
@@ -8,32 +8,31 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Shopify/ejson v1.4.1
github.com/aws/aws-sdk-go v1.49.20
- github.com/docker/libkv v0.2.2-0.20180912205406-458977154600
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
github.com/google/uuid v1.5.0
github.com/gosimple/slug v1.13.1
github.com/hack-pad/hackpadfs v0.2.1
- github.com/hairyhenderson/go-fsimpl v0.0.0-20230121155226-8aa24800449d
+ github.com/hairyhenderson/go-fsimpl v0.0.0-20240117024222-dd2398b673d3
github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf
github.com/hairyhenderson/xignore v0.3.3-0.20230403012150-95fe86932830 // iofs-port branch
- github.com/hashicorp/consul/api v1.26.1
github.com/hashicorp/go-sockaddr v1.0.6
github.com/hashicorp/vault/api v1.10.0
+ github.com/hashicorp/vault/api/auth/aws v0.5.0
github.com/itchyny/gojq v0.12.14
- github.com/johannesboyne/gofakes3 v0.0.0-20220627085814-c3ac35da23b2
+ github.com/johannesboyne/gofakes3 v0.0.0-20230914150226-f005f5cc03aa
github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.31.0
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/ugorji/go/codec v1.2.12
- go4.org/netipx v0.0.0-20230125063823-8449b0a6169f
- gocloud.dev v0.36.0
+ go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
golang.org/x/crypto v0.18.0
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
golang.org/x/text v0.14.0
gotest.tools/v3 v3.5.1
- inet.af/netaddr v0.0.0-20220811202034-502d2d690317
+ inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a
k8s.io/client-go v0.29.0
)
@@ -42,43 +41,47 @@ require (
// cherry-pick from the PR on top of v5.11.0
replace github.com/go-git/go-git/v5 => github.com/hairyhenderson/go-git/v5 v5.0.0-20240112193603-9068a607f23a
-require (
- github.com/go-git/go-billy/v5 v5.5.0
- github.com/go-git/go-git/v5 v5.11.0
-)
-
// TODO: replace with gopkg.in/yaml.v3 after https://github.com/go-yaml/yaml/pull/862
// is merged
require github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce
require (
- cloud.google.com/go v0.110.10 // indirect
+ cloud.google.com/go v0.111.0 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
- cloud.google.com/go/storage v1.35.1 // indirect
+ cloud.google.com/go/storage v1.36.0 // indirect
dario.cat/mergo v1.0.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1 // indirect
+ github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+ github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
- github.com/aws/aws-sdk-go-v2 v1.24.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
- github.com/aws/aws-sdk-go-v2/config v1.26.1 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.26.3 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
@@ -87,9 +90,15 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
github.com/emirpasic/gods v1.18.1 // indirect
- github.com/fatih/color v1.14.1 // indirect
+ github.com/fatih/color v1.15.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.5.0 // indirect
+ github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+ github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
@@ -98,6 +107,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
+ github.com/hashicorp/consul/api v1.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
@@ -105,47 +115,59 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
- github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
+ github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 // indirect
+ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
+ github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
+ github.com/hashicorp/vault/api/auth/approle v0.5.0 // indirect
+ github.com/hashicorp/vault/api/auth/userpass v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
- github.com/sergi/go-diff v1.2.0 // indirect
+ github.com/sergi/go-diff v1.3.1 // indirect
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
+ go.opentelemetry.io/otel v1.21.0 // indirect
+ go.opentelemetry.io/otel/metric v1.21.0 // indirect
+ go.opentelemetry.io/otel/trace v1.21.0 // indirect
go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
- golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+ gocloud.dev v0.36.0 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.20.0 // indirect
- golang.org/x/oauth2 v0.14.0 // indirect
+ golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sync v0.5.0 // indirect
- golang.org/x/time v0.4.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
- google.golang.org/api v0.151.0 // indirect
+ google.golang.org/api v0.154.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
- google.golang.org/grpc v1.59.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
+ google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 86df0e25..0e33b4ab 100644
--- a/go.sum
+++ b/go.sum
@@ -1,20 +1,38 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
-cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
+cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
+cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
-cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
-cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g=
+cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc=
+cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
+cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13 h1:zkiIe8AxZ/kDjqQN+mDKc5BxoVJOqioSdqApjc+eB1I=
cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13/go.mod h1:XGKYSMtsJWfqQYPwq51ZygxAPqpEUj/9bdg16iDPTAA=
cuelang.org/go v0.7.0 h1:gMztinxuKfJwMIxtboFsNc6s8AxwJGgsJV+3CuLffHI=
cuelang.org/go v0.7.0/go.mod h1:ix+3dM/bSpdG9xg6qpCgnJnpeLtciZu+O/rDbywoMII=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1 h1:AMf7YbZOZIW5b66cXNHMWWT/zkjhz5+a+k/3x40EO7E=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1/go.mod h1:uwfk06ZBcvL/g4VHNjurPfVln9NMbsk2XIZxJ+hu81k=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
+github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -42,25 +60,26 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
-github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.49.20 h1:VgEUq2/ZbUkLbqPyDcxrirfXB+PgiZUUF5XbsgWe2S0=
github.com/aws/aws-sdk-go v1.49.20/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
-github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk=
-github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
+github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
+github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
-github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o=
-github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg=
-github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU=
-github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw=
+github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA=
+github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 h1:FnLf60PtjXp8ZOzQfhJVsqF0OtYKQZWQfqOLshh8YXg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7/go.mod h1:tDVvl8hyU6E9B8TrnNrZQEVkQlB8hjJwcgpPhgtlnNg=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw=
@@ -69,18 +88,22 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38=
-github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg=
-github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1vkSIWAN8tPwPCoYZBp7aruR540=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac=
+github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -88,6 +111,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -99,18 +123,21 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/docker/libkv v0.2.2-0.20180912205406-458977154600 h1:x0AMRhackzbivKKiEeSMzH6gZmbALPXCBG0ecBmRlco=
-github.com/docker/libkv v0.2.2-0.20180912205406-458977154600/go.mod h1:r5hEwHwW8dr0TFBYGCarMNbrQOiwL1xoqDYZ/JqoTK0=
+github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
@@ -124,11 +151,17 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
+github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
-github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsouza/fake-gcs-server v1.47.7 h1:56/U4rKY081TaNbq0gHWi7/71UxC2KROqcnrD9BRJhs=
+github.com/fsouza/fake-gcs-server v1.47.7/go.mod h1:4vPUynN8/zZlxk5Jpy6LvvTTxItdTAObK4DYnp89Jys=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
@@ -139,19 +172,28 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
+github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -181,6 +223,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE=
@@ -190,6 +233,8 @@ github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
+github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
@@ -204,14 +249,18 @@ github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw=
github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU=
-github.com/hairyhenderson/go-fsimpl v0.0.0-20230121155226-8aa24800449d h1:RqyRRWUi3ftiPrEuO9ZbaILf6C8TDRw90fLM8pqbzGs=
-github.com/hairyhenderson/go-fsimpl v0.0.0-20230121155226-8aa24800449d/go.mod h1:2k9HLXToBp8dOXrbgZUTaZfm03JZlkCGkrgvp4ip/JQ=
+github.com/hairyhenderson/go-fsimpl v0.0.0-20240117024222-dd2398b673d3 h1:hEAU1+bbqbGTUIQfoKcMiRgza+47nByeZ8jEHlOLDac=
+github.com/hairyhenderson/go-fsimpl v0.0.0-20240117024222-dd2398b673d3/go.mod h1:/8Yhp1ir+A+t3n4iYIL/O47X7c35QrkTi39ZjJpfoYw=
github.com/hairyhenderson/go-git/v5 v5.0.0-20240112193603-9068a607f23a h1:tY0NASRMFiqOxJgT5CngoUft1ZZGTDK9jCWNljFrGMs=
github.com/hairyhenderson/go-git/v5 v5.0.0-20240112193603-9068a607f23a/go.mod h1:rGg8aLOxsDU5LgK8kiSJUzXaTPmwYZQYCj4+ouohMuM=
github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf h1:I1sbT4ZbIt9i+hB1zfKw2mE8C12TuGxPiW7YmtLbPa4=
@@ -228,9 +277,11 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -244,12 +295,16 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
+github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
+github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
@@ -260,10 +315,11 @@ github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3ly
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
-github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
@@ -277,6 +333,12 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
+github.com/hashicorp/vault/api/auth/approle v0.5.0 h1:a1TK6VGwYqSAfkmX4y4dJ4WBxMU5dStIZqScW4EPXR8=
+github.com/hashicorp/vault/api/auth/approle v0.5.0/go.mod h1:CHOQIA1AZACfjTzHggmyfiOZ+xCSKNRFqe48FTCzH0k=
+github.com/hashicorp/vault/api/auth/aws v0.5.0 h1:IKf0W3A2tXEtw9KrooslBWw72Ld63V+fUHkkSmm+2T0=
+github.com/hashicorp/vault/api/auth/aws v0.5.0/go.mod h1:U2Y6Ci/kDsUkDTzUXq0OKG2/GQkEtqzQjTY1YYSQFnk=
+github.com/hashicorp/vault/api/auth/userpass v0.5.0 h1:u//BC15YJviWSpeTlxsmt96FPULsCF7dYhPHg5oOAzo=
+github.com/hashicorp/vault/api/auth/userpass v0.5.0/go.mod h1:TNxl3X6ZaeILi1rfxP/mhGnWuiCiP7SNv2qeZ5aSAMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
@@ -285,13 +347,13 @@ github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/johannesboyne/gofakes3 v0.0.0-20220627085814-c3ac35da23b2 h1:V5q1Mx2WTE5coXLG2QpkRZ7LsJvgkedm6Ib4AwC1Lfg=
-github.com/johannesboyne/gofakes3 v0.0.0-20220627085814-c3ac35da23b2/go.mod h1:LIAXxPvcUXwOcTIj9LSNSUpE9/eMHalTWxsP/kmWxQI=
+github.com/johannesboyne/gofakes3 v0.0.0-20230914150226-f005f5cc03aa h1:a6Hc6Hlq6MxPNBW53/S/HnVwVXKc0nbdD/vgnQYuxG0=
+github.com/johannesboyne/gofakes3 v0.0.0-20230914150226-f005f5cc03aa/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -304,12 +366,15 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -321,6 +386,7 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -361,10 +427,14 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
+github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -384,6 +454,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk=
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -398,9 +469,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -425,6 +495,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -443,11 +514,23 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/intern v0.0.0-20220617035311-6925f38cc365 h1:t9hFvR102YlOqU0fQn1wgwhNvSbHGBbbJxX9JKfU3l0=
go4.org/intern v0.0.0-20220617035311-6925f38cc365/go.mod h1:WXRv3p7T6gzt0CcJm43AAKdKVZmcQbwwC7EwquU5BZU=
-go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s=
-go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
+go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo=
+go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
@@ -463,6 +546,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
@@ -475,32 +559,36 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
-golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -522,11 +610,13 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -535,6 +625,7 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -543,18 +634,22 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -566,14 +661,15 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
-golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -583,6 +679,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -591,8 +688,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-google.golang.org/api v0.151.0 h1:FhfXLO/NFdJIzQtCqjpysWwqKk8AzGWBUhMIx67cVDU=
-google.golang.org/api v0.151.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg=
+google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
+google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@@ -604,15 +701,15 @@ google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
-google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
+google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -628,9 +725,11 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
@@ -648,7 +747,7 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
-inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
+inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
+inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
diff --git a/gomplate.go b/gomplate.go
index f95dc2a3..b1312a34 100644
--- a/gomplate.go
+++ b/gomplate.go
@@ -11,8 +11,8 @@ import (
"text/template"
"time"
- "github.com/hairyhenderson/gomplate/v4/data"
"github.com/hairyhenderson/gomplate/v4/internal/config"
+ "github.com/hairyhenderson/gomplate/v4/internal/datafs"
)
// RunTemplates - run all gomplate templates specified by the given configuration
@@ -46,7 +46,7 @@ func Run(ctx context.Context, cfg *config.Config) error {
}
// if a custom Stdin is set in the config, inject it into the context now
- ctx = data.ContextWithStdin(ctx, cfg.Stdin)
+ ctx = datafs.ContextWithStdin(ctx, cfg.Stdin)
opts := optionsFromConfig(cfg)
opts.Funcs = funcMap
@@ -87,7 +87,6 @@ func simpleNamer(outDir string) func(ctx context.Context, inPath string) (string
func mappingNamer(outMap string, tr *Renderer) func(context.Context, string) (string, error) {
return func(ctx context.Context, inPath string) (string, error) {
- tr.data.Ctx = ctx
tcontext, err := createTmplContext(ctx, tr.tctxAliases, tr.data)
if err != nil {
return "", err
diff --git a/gomplate_test.go b/gomplate_test.go
index 2485e7af..62ae4525 100644
--- a/gomplate_test.go
+++ b/gomplate_test.go
@@ -12,6 +12,7 @@ import (
"github.com/hairyhenderson/gomplate/v4/conv"
"github.com/hairyhenderson/gomplate/v4/data"
"github.com/hairyhenderson/gomplate/v4/env"
+ "github.com/hairyhenderson/gomplate/v4/internal/parsers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -73,7 +74,7 @@ func TestEc2MetaTemplates_WithJSON(t *testing.T) {
Funcs: template.FuncMap{
"ec2meta": ec2meta.Meta,
"ec2dynamic": ec2meta.Dynamic,
- "json": data.JSON,
+ "json": parsers.JSON,
},
})
@@ -84,7 +85,7 @@ func TestEc2MetaTemplates_WithJSON(t *testing.T) {
func TestJSONArrayTemplates(t *testing.T) {
g := NewRenderer(Options{
Funcs: template.FuncMap{
- "jsonArray": data.JSONArray,
+ "jsonArray": parsers.JSONArray,
},
})
@@ -95,8 +96,8 @@ func TestJSONArrayTemplates(t *testing.T) {
func TestYAMLTemplates(t *testing.T) {
g := NewRenderer(Options{
Funcs: template.FuncMap{
- "yaml": data.YAML,
- "yamlArray": data.YAMLArray,
+ "yaml": parsers.YAML,
+ "yamlArray": parsers.YAMLArray,
},
})
@@ -108,7 +109,7 @@ func TestYAMLTemplates(t *testing.T) {
func TestHasTemplate(t *testing.T) {
g := NewRenderer(Options{
Funcs: template.FuncMap{
- "yaml": data.YAML,
+ "yaml": parsers.YAML,
"has": conv.Has,
},
})
diff --git a/internal/cmd/config.go b/internal/cmd/config.go
index 7f071417..7dc53911 100644
--- a/internal/cmd/config.go
+++ b/internal/cmd/config.go
@@ -73,7 +73,7 @@ func readConfigFile(ctx context.Context, cmd *cobra.Command) (cfg *config.Config
// we only support loading configs from the local filesystem for now
fsys, err := datafs.FSysForPath(ctx, cfgFile)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("fsys for path %v: %w", cfgFile, err)
}
f, err := fsys.Open(cfgFile)
diff --git a/internal/cmd/main.go b/internal/cmd/main.go
index 3cce83e2..7a3dea98 100644
--- a/internal/cmd/main.go
+++ b/internal/cmd/main.go
@@ -7,7 +7,6 @@ import (
"os/exec"
"os/signal"
- "github.com/hairyhenderson/go-fsimpl"
"github.com/hairyhenderson/gomplate/v4"
"github.com/hairyhenderson/gomplate/v4/env"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
@@ -169,13 +168,10 @@ func InitFlags(command *cobra.Command) {
func Main(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
ctx = initLogger(ctx, stderr)
- // inject a default filesystem provider for file:// URLs
+ // inject default filesystem provider if it hasn't already been provided in
+ // the context
if datafs.FSProviderFromContext(ctx) == nil {
- // TODO: expand this to support other schemes!
- mux := fsimpl.NewMux()
- mux.Add(datafs.WdFS)
-
- ctx = datafs.ContextWithFSProvider(ctx, mux)
+ ctx = datafs.ContextWithFSProvider(ctx, gomplate.DefaultFSProvider)
}
command := NewGomplateCmd()
diff --git a/internal/config/configfile.go b/internal/config/configfile.go
index 1ed266b3..1fcccc20 100644
--- a/internal/config/configfile.go
+++ b/internal/config/configfile.go
@@ -15,8 +15,8 @@ import (
"golang.org/x/exp/slices"
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
"github.com/hairyhenderson/yaml"
)
@@ -115,7 +115,7 @@ func (d *DataSource) UnmarshalYAML(value *yaml.Node) error {
if err != nil {
return err
}
- u, err := datafs.ParseSourceURL(r.URL)
+ u, err := urlhelpers.ParseSourceURL(r.URL)
if err != nil {
return fmt.Errorf("could not parse datasource URL %q: %w", r.URL, err)
}
@@ -378,7 +378,7 @@ func parseDatasourceArg(value string) (alias string, ds DataSource, err error) {
}
}
- ds.URL, err = datafs.ParseSourceURL(u)
+ ds.URL, err = urlhelpers.ParseSourceURL(u)
return alias, ds, err
}
diff --git a/internal/config/types.go b/internal/config/types.go
index 57901f0d..648ad2b9 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -5,7 +5,7 @@ import (
"net/http"
"strings"
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
+ "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
"github.com/hairyhenderson/yaml"
)
@@ -53,7 +53,7 @@ func (t *Templates) unmarshalYAMLArray(value *yaml.Node) error {
pth = alias
}
- u, err := datafs.ParseSourceURL(pth)
+ u, err := urlhelpers.ParseSourceURL(pth)
if err != nil {
return fmt.Errorf("could not parse template URL %q: %w", pth, err)
}
@@ -90,7 +90,7 @@ func parseTemplateArg(value string) (alias string, ds DataSource, err error) {
u = alias
}
- ds.URL, err = datafs.ParseSourceURL(u)
+ ds.URL, err = urlhelpers.ParseSourceURL(u)
return alias, ds, err
}
diff --git a/internal/datafs/context.go b/internal/datafs/context.go
new file mode 100644
index 00000000..7f1235bf
--- /dev/null
+++ b/internal/datafs/context.go
@@ -0,0 +1,45 @@
+package datafs
+
+import (
+ "context"
+ "io"
+ "io/fs"
+ "os"
+
+ "github.com/hairyhenderson/gomplate/v4/internal/config"
+)
+
+// withContexter is an fs.FS that can be configured with a custom context
+// copied from go-fsimpl - see internal/types.go
+type withContexter interface {
+ WithContext(ctx context.Context) fs.FS
+}
+
+type withDataSourceser interface {
+ WithDataSources(sources map[string]config.DataSource) fs.FS
+}
+
+// WithDataSourcesFS injects a datasource map into the filesystem fs, if the
+// filesystem supports it (i.e. has a WithDataSources method). This is used for
+// the mergefs filesystem.
+func WithDataSourcesFS(sources map[string]config.DataSource, fsys fs.FS) fs.FS {
+ if fsys, ok := fsys.(withDataSourceser); ok {
+ return fsys.WithDataSources(sources)
+ }
+
+ return fsys
+}
+
+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/internal/datafs/envfs.go b/internal/datafs/envfs.go
new file mode 100644
index 00000000..41b3928f
--- /dev/null
+++ b/internal/datafs/envfs.go
@@ -0,0 +1,221 @@
+package datafs
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/hairyhenderson/go-fsimpl"
+)
+
+// NewEnvFS returns a filesystem (an fs.FS) that can be used to read data from
+// environment variables.
+func NewEnvFS(_ *url.URL) (fs.FS, error) {
+ return &envFS{locfs: os.DirFS("/")}, nil
+}
+
+type envFS struct {
+ locfs fs.FS
+}
+
+//nolint:gochecknoglobals
+var EnvFS = fsimpl.FSProviderFunc(NewEnvFS, "env")
+
+var _ fs.FS = (*envFS)(nil)
+
+func (f *envFS) Open(name string) (fs.File, error) {
+ if !fs.ValidPath(name) {
+ return nil, &fs.PathError{
+ Op: "open",
+ Path: name,
+ Err: fs.ErrInvalid,
+ }
+ }
+
+ return &envFile{locfs: f.locfs, name: name}, nil
+}
+
+type envFile struct {
+ locfs fs.FS
+ body io.Reader
+ name string
+
+ dirents []fs.DirEntry
+ diroff int
+}
+
+var (
+ _ fs.File = (*envFile)(nil)
+ _ fs.ReadDirFile = (*envFile)(nil)
+)
+
+// overridable env functions
+var (
+ lookupEnv = os.LookupEnv
+ environ = os.Environ
+)
+
+func (e *envFile) Close() error {
+ e.body = nil
+ return nil
+}
+
+func (e *envFile) envReader() (int, io.Reader, error) {
+ v, found := lookupEnv(e.name)
+ if found {
+ return len(v), bytes.NewBufferString(v), nil
+ }
+
+ fname, found := lookupEnv(e.name + "_FILE")
+ if found && fname != "" {
+ fname = strings.TrimPrefix(fname, "/")
+
+ b, err := fs.ReadFile(e.locfs, fname)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ b = bytes.TrimSpace(b)
+
+ return len(b), bytes.NewBuffer(b), nil
+ }
+
+ return 0, nil, fs.ErrNotExist
+}
+
+func (e *envFile) Stat() (fs.FileInfo, error) {
+ n, _, err := e.envReader()
+ if err != nil {
+ return nil, err
+ }
+
+ return FileInfo(e.name, int64(n), 0o444, time.Time{}, ""), nil
+}
+
+func (e *envFile) Read(p []byte) (int, error) {
+ if e.body == nil {
+ _, r, err := e.envReader()
+ if err != nil {
+ return 0, err
+ }
+ e.body = r
+ }
+
+ return e.body.Read(p)
+}
+
+func (e *envFile) ReadDir(n int) ([]fs.DirEntry, error) {
+ // envFS has no concept of subdirectories, but we can support a root
+ // directory by listing all environment variables.
+ if e.name != "." {
+ return nil, fmt.Errorf("%s: not a directory", e.name)
+ }
+
+ if e.dirents == nil {
+ envs := environ()
+ e.dirents = make([]fs.DirEntry, 0, len(envs))
+ for _, env := range envs {
+ parts := strings.SplitN(env, "=", 2)
+ name, value := parts[0], parts[1]
+
+ if name == "" {
+ // this might be a Windows =C: style env var, so skip it
+ continue
+ }
+
+ e.dirents = append(e.dirents, FileInfoDirEntry(
+ FileInfo(name, int64(len(value)), 0o444, time.Time{}, ""),
+ ))
+ }
+ }
+
+ if n > 0 && e.diroff >= len(e.dirents) {
+ return nil, io.EOF
+ }
+
+ low := e.diroff
+ high := e.diroff + n
+
+ // clamp high at the max, and ensure it's higher than low
+ if high >= len(e.dirents) || high <= low {
+ high = len(e.dirents)
+ }
+
+ entries := make([]fs.DirEntry, high-low)
+ copy(entries, e.dirents[e.diroff:])
+
+ e.diroff = high
+
+ return entries, nil
+}
+
+// FileInfo/DirInfo/FileInfoDirEntry/etc are taken from go-fsimpl's internal
+// package, and may be exported in the future...
+
+// FileInfo creates a static fs.FileInfo with the given properties.
+// The result is also a fs.DirEntry and can be safely cast.
+func FileInfo(name string, size int64, mode fs.FileMode, modTime time.Time, contentType string) fs.FileInfo {
+ return &staticFileInfo{
+ name: name,
+ size: size,
+ mode: mode,
+ modTime: modTime,
+ contentType: contentType,
+ }
+}
+
+// DirInfo creates a fs.FileInfo for a directory with the given name. Use
+// FileInfo to set other values.
+func DirInfo(name string, modTime time.Time) fs.FileInfo {
+ return FileInfo(name, 0, fs.ModeDir, modTime, "")
+}
+
+type staticFileInfo struct {
+ modTime time.Time
+ name string
+ contentType string
+ size int64
+ mode fs.FileMode
+}
+
+var (
+ _ fs.FileInfo = (*staticFileInfo)(nil)
+ _ fs.DirEntry = (*staticFileInfo)(nil)
+)
+
+func (fi staticFileInfo) ContentType() string { return fi.contentType }
+func (fi staticFileInfo) IsDir() bool { return fi.Mode().IsDir() }
+func (fi staticFileInfo) Mode() fs.FileMode { return fi.mode }
+func (fi *staticFileInfo) ModTime() time.Time { return fi.modTime }
+func (fi staticFileInfo) Name() string { return fi.name }
+func (fi staticFileInfo) Size() int64 { return fi.size }
+func (fi staticFileInfo) Sys() interface{} { return nil }
+func (fi *staticFileInfo) Info() (fs.FileInfo, error) { return fi, nil }
+func (fi staticFileInfo) Type() fs.FileMode { return fi.Mode().Type() }
+
+// FileInfoDirEntry adapts a fs.FileInfo into a fs.DirEntry. If it doesn't
+// already implement fs.DirEntry, it will be wrapped to always return the
+// same fs.FileInfo.
+func FileInfoDirEntry(fi fs.FileInfo) fs.DirEntry {
+ de, ok := fi.(fs.DirEntry)
+ if ok {
+ return de
+ }
+
+ return &fileinfoDirEntry{fi}
+}
+
+// a wrapper to make a fs.FileInfo into an fs.DirEntry
+type fileinfoDirEntry struct {
+ fs.FileInfo
+}
+
+var _ fs.DirEntry = (*fileinfoDirEntry)(nil)
+
+func (fi *fileinfoDirEntry) Info() (fs.FileInfo, error) { return fi, nil }
+func (fi *fileinfoDirEntry) Type() fs.FileMode { return fi.Mode().Type() }
diff --git a/internal/datafs/envfs_test.go b/internal/datafs/envfs_test.go
new file mode 100644
index 00000000..d8bfd092
--- /dev/null
+++ b/internal/datafs/envfs_test.go
@@ -0,0 +1,140 @@
+package datafs
+
+import (
+ "io/fs"
+ "net/url"
+ "os"
+ "testing"
+ "testing/fstest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEnvFS_Open(t *testing.T) {
+ fsys, err := NewEnvFS(nil)
+ assert.NoError(t, err)
+ assert.IsType(t, &envFS{}, fsys)
+
+ f, err := fsys.Open("foo")
+ assert.NoError(t, err)
+ assert.IsType(t, &envFile{}, f)
+}
+
+func TestEnvFile_Read(t *testing.T) {
+ content := `hello world`
+ t.Setenv("HELLO_WORLD", "hello world")
+
+ f := &envFile{name: "HELLO_WORLD"}
+ b := make([]byte, len(content))
+ n, err := f.Read(b)
+ assert.NoError(t, err)
+ assert.Equal(t, len(content), n)
+ assert.Equal(t, content, string(b))
+
+ fsys := fstest.MapFS{}
+ fsys["foo/bar/baz.txt"] = &fstest.MapFile{Data: []byte("\nhello world\n")}
+
+ t.Setenv("FOO_FILE", "/foo/bar/baz.txt")
+
+ f = &envFile{name: "FOO", locfs: fsys}
+
+ b = make([]byte, len(content))
+ t.Logf("b len is %d", len(b))
+ n, err = f.Read(b)
+ t.Logf("b len is %d", len(b))
+ assert.NoError(t, err)
+ assert.Equal(t, len(content), n)
+ assert.Equal(t, content, string(b))
+}
+
+func TestEnvFile_Stat(t *testing.T) {
+ content := []byte(`hello world`)
+ t.Setenv("HELLO_WORLD", "hello world")
+
+ f := &envFile{name: "HELLO_WORLD"}
+
+ fi, err := f.Stat()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), fi.Size())
+
+ fsys := fstest.MapFS{}
+ fsys["foo/bar/baz.txt"] = &fstest.MapFile{Data: []byte("\nhello world\n")}
+
+ t.Setenv("FOO_FILE", "/foo/bar/baz.txt")
+
+ f = &envFile{name: "FOO", locfs: fsys}
+
+ fi, err = f.Stat()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), fi.Size())
+}
+
+func TestEnvFS(t *testing.T) {
+ t.Cleanup(func() { environ = os.Environ })
+
+ u, _ := url.Parse("env:")
+
+ lfsys := fstest.MapFS{}
+ lfsys["foo/bar/baz.txt"] = &fstest.MapFile{Data: []byte("\nhello file\n")}
+
+ fsys, err := NewEnvFS(u)
+ assert.NoError(t, err)
+ assert.IsType(t, &envFS{}, fsys)
+
+ envfs, ok := fsys.(*envFS)
+ assert.True(t, ok)
+ envfs.locfs = lfsys
+
+ t.Setenv("FOO_FILE", "/foo/bar/baz.txt")
+
+ b, err := fs.ReadFile(fsys, "FOO")
+ assert.NoError(t, err)
+ assert.Equal(t, "hello file", string(b))
+
+ t.Setenv("FOO", "hello world")
+
+ b, err = fs.ReadFile(fsys, "FOO")
+ assert.NoError(t, err)
+ assert.Equal(t, "hello world", string(b))
+
+ assert.NoError(t, fstest.TestFS(fsys, "FOO", "FOO_FILE"))
+}
+
+func TestEnvFile_ReadDir(t *testing.T) {
+ t.Cleanup(func() { environ = os.Environ })
+
+ t.Run("name must be .", func(t *testing.T) {
+ f := &envFile{name: "foo"}
+ _, err := f.ReadDir(-1)
+ require.Error(t, err)
+ })
+
+ t.Run("empty env should return empty dir", func(t *testing.T) {
+ f := &envFile{name: "."}
+ environ = func() []string { return []string{} }
+ des, err := f.ReadDir(-1)
+ require.NoError(t, err)
+ assert.Empty(t, des)
+ })
+
+ t.Run("non-empty env should return dir with entries", func(t *testing.T) {
+ f := &envFile{name: "."}
+ environ = func() []string { return []string{"FOO=bar", "BAR=quux"} }
+ des, err := f.ReadDir(-1)
+ require.NoError(t, err)
+ require.Len(t, des, 2)
+ assert.Equal(t, "FOO", des[0].Name())
+ assert.Equal(t, "BAR", des[1].Name())
+ })
+
+ t.Run("deal with odd Windows env vars like '=C:=C:\tmp'", func(t *testing.T) {
+ f := &envFile{name: "."}
+ environ = func() []string { return []string{"FOO=bar", "=C:=C:\\tmp", "BAR=quux"} }
+ des, err := f.ReadDir(-1)
+ require.NoError(t, err)
+ require.Len(t, des, 2)
+ assert.Equal(t, "FOO", des[0].Name())
+ assert.Equal(t, "BAR", des[1].Name())
+ })
+}
diff --git a/internal/datafs/fsurl.go b/internal/datafs/fsurl.go
new file mode 100644
index 00000000..a1cf5cbc
--- /dev/null
+++ b/internal/datafs/fsurl.go
@@ -0,0 +1,42 @@
+package datafs
+
+import (
+ "net/url"
+ "strings"
+)
+
+// SplitFSMuxURL splits a URL into a filesystem URL and a relative file path
+func SplitFSMuxURL(in *url.URL) (*url.URL, string) {
+ u := *in
+
+ // 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 u.Scheme {
+ case "git", "git+file", "git+http", "git+https", "git+ssh":
+ repo, base, _ := strings.Cut(u.Path, "//")
+ u.Path = repo
+ if base == "" {
+ base = "."
+ }
+
+ return &u, base
+ }
+
+ // trim leading and trailing slashes - they are not part of a valid path
+ // according to [io/fs.ValidPath]
+ base := strings.Trim(u.Path, "/")
+
+ if base == "" && u.Opaque != "" {
+ base = u.Opaque
+ u.Opaque = ""
+ }
+
+ if base == "" {
+ base = "."
+ }
+
+ u.Path = "/"
+
+ return &u, base
+}
diff --git a/internal/datafs/fsurl_test.go b/internal/datafs/fsurl_test.go
new file mode 100644
index 00000000..a44edaff
--- /dev/null
+++ b/internal/datafs/fsurl_test.go
@@ -0,0 +1,106 @@
+package datafs
+
+import (
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSplitFSMuxURL(t *testing.T) {
+ testdata := []struct {
+ in string
+ url string
+ file string
+ }{
+ {
+ "http://example.com/foo.json",
+ "http://example.com/",
+ "foo.json",
+ },
+ {
+ "http://example.com/foo.json?type=application/array+yaml",
+ "http://example.com/?type=application/array+yaml",
+ "foo.json",
+ },
+ {
+ "vault:///secret/a/b/c",
+ "vault:///",
+ "secret/a/b/c",
+ },
+ {
+ "vault:///secret/a/b/",
+ "vault:///",
+ "secret/a/b",
+ },
+ {
+ "s3://bucket/a/b/",
+ "s3://bucket/",
+ "a/b",
+ },
+ {
+ "vault:///foo/bar",
+ "vault:///",
+ "foo/bar",
+ },
+ {
+ "consul://myhost/foo/bar/baz?q=1",
+ "consul://myhost/?q=1",
+ "foo/bar/baz",
+ },
+ {
+ "git+https://example.com/myrepo//foo.yaml",
+ "git+https://example.com/myrepo",
+ "foo.yaml",
+ },
+ {
+ "git+https://example.com/myrepo//",
+ "git+https://example.com/myrepo",
+ ".",
+ },
+ {
+ // git repos are special - no double-slash means the root
+ "git+https://example.com/myrepo",
+ "git+https://example.com/myrepo",
+ ".",
+ },
+ {
+ "git+ssh://git@github.com/hairyhenderson/go-which.git//a/b/c/d?q=1",
+ "git+ssh://git@github.com/hairyhenderson/go-which.git?q=1",
+ "a/b/c/d",
+ },
+ {
+ "merge:file:///tmp/jsonfile.json",
+ "merge:///",
+ "file:///tmp/jsonfile.json",
+ },
+ {
+ "merge:a|b",
+ "merge:///",
+ "a|b",
+ },
+ {
+ "merge:a|b|c|d|e",
+ "merge:///",
+ "a|b|c|d|e",
+ },
+ {
+ "merge:foo/bar/baz.json|qux",
+ "merge:///",
+ "foo/bar/baz.json|qux",
+ },
+ {
+ "merge:vault:///foo/bar|foo|git+ssh://git@github.com/hairyhenderson/go-which.git//a/b/c/d",
+ "merge:///",
+ "vault:///foo/bar|foo|git+ssh://git@github.com/hairyhenderson/go-which.git//a/b/c/d",
+ },
+ }
+
+ for _, d := range testdata {
+ u, err := url.Parse(d.in)
+ assert.NoError(t, err)
+ url, file := SplitFSMuxURL(u)
+ assert.Equal(t, d.url, url.String())
+ assert.Equal(t, d.file, file)
+ }
+}
diff --git a/internal/datafs/fsys.go b/internal/datafs/fsys.go
index fb88b0da..08f52b88 100644
--- a/internal/datafs/fsys.go
+++ b/internal/datafs/fsys.go
@@ -5,11 +5,11 @@ import (
"fmt"
"io/fs"
"net/url"
- "path"
- "path/filepath"
"strings"
"github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/go-fsimpl/vaultfs/vaultauth"
+ "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
)
type fsProviderCtxKey struct{}
@@ -29,50 +29,11 @@ func FSProviderFromContext(ctx context.Context) fsimpl.FSProvider {
return nil
}
-// ParseSourceURL parses a datasource URL value, which may be '-' (for stdin://),
-// or it may be a Windows path (with driver letter and back-slash separators) or
-// UNC, or it may be relative. It also might just be a regular absolute URL...
-// In all cases it returns a correct URL for the value. It may be a relative URL
-// in which case the scheme should be assumed to be 'file'
-func ParseSourceURL(value string) (*url.URL, error) {
- if value == "-" {
- value = "stdin://"
- }
- value = filepath.ToSlash(value)
- // handle absolute Windows paths
- volName := ""
- if volName = filepath.VolumeName(value); volName != "" {
- // handle UNCs
- if len(volName) > 2 {
- value = "file:" + value
- } else {
- value = "file:///" + value
- }
- }
- srcURL, err := url.Parse(value)
- if err != nil {
- return nil, err
- }
-
- if volName != "" && len(srcURL.Path) >= 3 {
- if srcURL.Path[0] == '/' && srcURL.Path[2] == ':' {
- srcURL.Path = srcURL.Path[1:]
- }
- }
-
- // if it's an absolute path with no scheme, assume it's a file
- if srcURL.Scheme == "" && path.IsAbs(srcURL.Path) {
- srcURL.Scheme = "file"
- }
-
- return srcURL, nil
-}
-
// FSysForPath returns an [io/fs.FS] for the given path (which may be an URL),
// rooted at /. A [fsimpl.FSProvider] is required to be present in ctx,
// otherwise an error is returned.
func FSysForPath(ctx context.Context, path string) (fs.FS, error) {
- u, err := ParseSourceURL(path)
+ u, err := urlhelpers.ParseSourceURL(path)
if err != nil {
return nil, err
}
@@ -82,18 +43,47 @@ func FSysForPath(ctx context.Context, path string) (fs.FS, error) {
return nil, fmt.Errorf("no filesystem provider in context")
}
- // default to "/" so we have a rooted filesystem for all schemes, but also
- // support volumes on Windows
origPath := u.Path
- if u.Scheme == "file" || strings.HasSuffix(u.Scheme, "+file") || u.Scheme == "" {
- u.Path, _, err = ResolveLocalPath(origPath)
- if err != nil {
- return nil, fmt.Errorf("resolve local path %q: %w", origPath, err)
+
+ switch u.Scheme {
+ case "git+file", "git+http", "git+https", "git+ssh", "git":
+ // 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.
+ u.Path, _, _ = strings.Cut(u.Path, "//")
+ }
+
+ switch u.Scheme {
+ case "git+http", "git+https", "git+ssh", "git":
+ // no-op, these are handled
+ case "", "file", "git+file":
+ // default to "/" so we have a rooted filesystem for all schemes, but also
+ // support volumes on Windows
+ root, name, rerr := ResolveLocalPath(nil, u.Path)
+ if rerr != nil {
+ return nil, fmt.Errorf("resolve local path %q: %w", origPath, rerr)
+ }
+
+ // windows absolute paths need a slash between the volume and path
+ if root != "" && root[0] != '/' {
+ u.Path = root + "/" + name
+ } else {
+ u.Path = root + name
}
+
// if this is a drive letter, add a trailing slash
- if u.Path[0] != '/' {
+ if len(u.Path) == 2 && u.Path[0] != '/' && u.Path[1] == ':' {
+ u.Path += "/"
+ } else if u.Path[0] != '/' {
u.Path += "/"
}
+
+ // if this starts with a drive letter, add a leading slash
+ // NOPE - this breaks lots of things
+ // if len(u.Path) > 2 && u.Path[0] != '/' && u.Path[1] == ':' {
+ // u.Path = "/" + u.Path
+ // }
+ default:
+ u.Path = "/"
}
fsys, err := fsp.New(u)
@@ -101,6 +91,16 @@ func FSysForPath(ctx context.Context, path string) (fs.FS, error) {
return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", path, err)
}
+ // inject vault auth methods if needed
+ switch u.Scheme {
+ case "vault", "vault+http", "vault+https":
+ fileFsys, err := fsp.New(&url.URL{Scheme: "file", Path: "/"})
+ if err != nil {
+ return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", path, err)
+ }
+ fsys = vaultauth.WithAuthMethod(compositeVaultAuthMethod(fileFsys), fsys)
+ }
+
return fsys, nil
}
diff --git a/internal/datafs/fsys_test.go b/internal/datafs/fsys_test.go
index 80ff5ca1..14e2a0bd 100644
--- a/internal/datafs/fsys_test.go
+++ b/internal/datafs/fsys_test.go
@@ -1,42 +1,78 @@
package datafs
import (
+ "context"
+ "io/fs"
"net/url"
+ "os"
+ "runtime"
"testing"
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/go-fsimpl/gitfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestParseSourceURL(t *testing.T) {
- expected := &url.URL{
- Scheme: "http",
- Host: "example.com",
- Path: "/foo.json",
- RawQuery: "bar",
- }
- u, err := ParseSourceURL("http://example.com/foo.json?bar")
- require.NoError(t, err)
- assert.EqualValues(t, expected, u)
-
- expected = &url.URL{Scheme: "", Path: ""}
- u, err = ParseSourceURL("")
- require.NoError(t, err)
- assert.EqualValues(t, expected, u)
-
- expected = &url.URL{Scheme: "stdin"}
- u, err = ParseSourceURL("-")
- require.NoError(t, err)
- assert.EqualValues(t, expected, u)
-
- // behviour change in v4 - return relative if it's relative
- expected = &url.URL{Path: "./foo/bar.json"}
- u, err = ParseSourceURL("./foo/bar.json")
- require.NoError(t, err)
- assert.EqualValues(t, expected, u)
-
- expected = &url.URL{Scheme: "file", Path: "/absolute/bar.json"}
- u, err = ParseSourceURL("/absolute/bar.json")
- require.NoError(t, err)
- assert.EqualValues(t, expected, u)
+func TestFSysForPath(t *testing.T) {
+ vol, _ := workingVolume()
+
+ t.Run("no provider", func(t *testing.T) {
+ ctx := ContextWithFSProvider(context.Background(), nil)
+ _, err := FSysForPath(ctx, "foo")
+ require.Error(t, err)
+
+ _, err = FSysForPath(ctx, "foo://bar")
+ require.Error(t, err)
+ })
+
+ t.Run("file url", func(t *testing.T) {
+ fsp := fsimpl.FSProviderFunc(func(u *url.URL) (fs.FS, error) {
+ assert.Equal(t, "file", u.Scheme)
+
+ if runtime.GOOS == "windows" {
+ assert.Equal(t, vol+"/tmp/foo/", u.Path)
+ return os.DirFS(vol + "/"), nil
+ }
+
+ assert.Equal(t, "/tmp/foo", u.Path)
+ return os.DirFS("/"), nil
+ }, "file")
+
+ ctx := ContextWithFSProvider(context.Background(), fsp)
+ fsys, err := FSysForPath(ctx, "file:///tmp/foo")
+ require.NoError(t, err)
+ require.NotNil(t, fsys)
+ })
+
+ t.Run("git url", func(t *testing.T) {
+ fsp := fsimpl.FSProviderFunc(func(u *url.URL) (fs.FS, error) {
+ assert.Equal(t, "git://github.com/hairyhenderson/gomplate", u.String())
+ return gitfs.New(u)
+ }, "git")
+
+ ctx := ContextWithFSProvider(context.Background(), fsp)
+
+ fsys, err := FSysForPath(ctx, "git://github.com/hairyhenderson/gomplate//README.md")
+ require.NoError(t, err)
+ require.NotNil(t, fsys)
+ })
+
+ t.Run("git+file url", func(t *testing.T) {
+ fsp := fsimpl.FSProviderFunc(func(u *url.URL) (fs.FS, error) {
+ assert.Equal(t, "git+file", u.Scheme)
+ if runtime.GOOS == "windows" {
+ assert.Equal(t, vol+"/tmp/repo/", u.Path)
+ } else {
+ assert.Equal(t, "/tmp/repo", u.Path)
+ }
+
+ return gitfs.New(u)
+ }, "git+file")
+
+ ctx := ContextWithFSProvider(context.Background(), fsp)
+ fsys, err := FSysForPath(ctx, "git+file:///tmp/repo//README.md")
+ require.NoError(t, err)
+ require.NotNil(t, fsys)
+ })
}
diff --git a/internal/datafs/getenv.go b/internal/datafs/getenv.go
new file mode 100644
index 00000000..5f9761d4
--- /dev/null
+++ b/internal/datafs/getenv.go
@@ -0,0 +1,51 @@
+package datafs
+
+import (
+ "io/fs"
+ "os"
+ "strings"
+)
+
+// ExpandEnvFsys - a convenience function intended for internal use only!
+func ExpandEnvFsys(fsys fs.FS, s string) string {
+ return os.Expand(s, func(s string) string {
+ return GetenvFsys(fsys, s)
+ })
+}
+
+// GetenvFsys - a convenience function intended for internal use only!
+func GetenvFsys(fsys fs.FS, key string, def ...string) string {
+ val := getenvFile(fsys, key)
+ if val == "" && len(def) > 0 {
+ return def[0]
+ }
+
+ return val
+}
+
+func getenvFile(fsys fs.FS, key string) string {
+ val := os.Getenv(key)
+ if val != "" {
+ return val
+ }
+
+ p := os.Getenv(key + "_FILE")
+ if p != "" {
+ val, err := readFile(fsys, p)
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(val)
+ }
+
+ return ""
+}
+
+func readFile(fsys fs.FS, p string) (string, error) {
+ b, err := fs.ReadFile(fsys, p)
+ if err != nil {
+ return "", err
+ }
+
+ return string(b), nil
+}
diff --git a/internal/datafs/getenv_test.go b/internal/datafs/getenv_test.go
new file mode 100644
index 00000000..c05a4c4b
--- /dev/null
+++ b/internal/datafs/getenv_test.go
@@ -0,0 +1,97 @@
+package datafs
+
+import (
+ "errors"
+ "io/fs"
+ "testing"
+ "testing/fstest"
+
+ "github.com/hack-pad/hackpadfs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetenvFsys(t *testing.T) {
+ fsys := fs.FS(fstest.MapFS{
+ "tmp": &fstest.MapFile{Mode: fs.ModeDir | 0o777},
+ "tmp/foo": &fstest.MapFile{Data: []byte("foo")},
+ "tmp/unreadable": &fstest.MapFile{Data: []byte("foo"), Mode: 0o000},
+ })
+ fsys = WrapWdFS(fsys)
+
+ t.Setenv("FOO_FILE", "/tmp/foo")
+ assert.Equal(t, "foo", GetenvFsys(fsys, "FOO", "bar"))
+
+ t.Setenv("FOO_FILE", "/tmp/missing")
+ assert.Equal(t, "bar", GetenvFsys(fsys, "FOO", "bar"))
+
+ fsys = writeOnly(fsys)
+ t.Setenv("FOO_FILE", "/tmp/unreadable")
+ assert.Equal(t, "bar", GetenvFsys(fsys, "FOO", "bar"))
+}
+
+func TestExpandEnvFsys(t *testing.T) {
+ fsys := fs.FS(fstest.MapFS{
+ "tmp": &fstest.MapFile{Mode: fs.ModeDir | 0o777},
+ "tmp/foo": &fstest.MapFile{Data: []byte("foo")},
+ "tmp/unreadable": &fstest.MapFile{Data: []byte("foo"), Mode: 0o000},
+ })
+ fsys = WrapWdFS(fsys)
+
+ t.Setenv("FOO_FILE", "/tmp/foo")
+ assert.Equal(t, "foo is foo", ExpandEnvFsys(fsys, "foo is $FOO"))
+
+ t.Setenv("FOO_FILE", "/tmp/missing")
+ assert.Equal(t, "empty", ExpandEnvFsys(fsys, "${FOO}empty"))
+
+ fsys = writeOnly(fsys)
+ t.Setenv("FOO_FILE", "/tmp/unreadable")
+ assert.Equal(t, "", ExpandEnvFsys(fsys, "${FOO}"))
+}
+
+// Maybe extract this into a separate package sometime...
+// writeOnly - represents a filesystem that's writeable, but read operations fail
+func writeOnly(fsys fs.FS) fs.FS {
+ return &woFS{fsys}
+}
+
+type woFS struct {
+ fsys fs.FS
+}
+
+func (fsys woFS) Open(name string) (fs.File, error) {
+ f, err := fsys.fsys.Open(name)
+ return writeOnlyFile(f), err
+}
+
+func (fsys woFS) ReadDir(_ string) ([]fs.DirEntry, error) {
+ return nil, ErrWriteOnly
+}
+
+func (fsys woFS) Stat(_ string) (fs.FileInfo, error) {
+ return nil, ErrWriteOnly
+}
+
+func writeOnlyFile(f fs.File) fs.File {
+ if f == nil {
+ return nil
+ }
+
+ return &woFile{f}
+}
+
+type woFile struct {
+ fs.File
+}
+
+// Write -
+func (f woFile) Write(p []byte) (n int, err error) {
+ return hackpadfs.WriteFile(f.File, p)
+}
+
+// Read is disabled and returns ErrWriteOnly
+func (f woFile) Read([]byte) (n int, err error) {
+ return 0, ErrWriteOnly
+}
+
+var ErrWriteOnly = errors.New("filesystem is write-only")
diff --git a/internal/datafs/mergefs.go b/internal/datafs/mergefs.go
new file mode 100644
index 00000000..7c612382
--- /dev/null
+++ b/internal/datafs/mergefs.go
@@ -0,0 +1,280 @@
+package datafs
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/http"
+ "net/url"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/gomplate/v4/coll"
+ "github.com/hairyhenderson/gomplate/v4/internal/config"
+ "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/hairyhenderson/gomplate/v4/internal/parsers"
+ "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
+)
+
+// NewMergeFS returns a new filesystem that merges the contents of multiple
+// paths. Only a URL like "merge:" or "merge:///" makes sense here - the
+// piped-separated lists of sub-sources to merge must be given to Open.
+//
+// Usually you'll want to use WithDataSourcesFS to provide the map of
+// datasources that can be referenced. Otherwise, only URLs will be supported.
+//
+// An FSProvider will also be needed, which can be provided with a context
+// using ContextWithFSProvider. Provide that context with fsimpl.WithContextFS.
+func NewMergeFS(u *url.URL) (fs.FS, error) {
+ if u.Scheme != "merge" {
+ return nil, fmt.Errorf("unsupported scheme %q", u.Scheme)
+ }
+
+ return &mergeFS{
+ ctx: context.Background(),
+ sources: map[string]config.DataSource{},
+ }, nil
+}
+
+type mergeFS struct {
+ ctx context.Context
+ httpClient *http.Client
+ sources map[string]config.DataSource
+}
+
+//nolint:gochecknoglobals
+var MergeFS = fsimpl.FSProviderFunc(NewMergeFS, "merge")
+
+var (
+ _ fs.FS = (*mergeFS)(nil)
+ _ withContexter = (*mergeFS)(nil)
+ _ withDataSourceser = (*mergeFS)(nil)
+)
+
+func (f *mergeFS) WithContext(ctx context.Context) fs.FS {
+ if ctx == nil {
+ return f
+ }
+
+ fsys := *f
+ fsys.ctx = ctx
+
+ return &fsys
+}
+
+func (f *mergeFS) WithHTTPClient(client *http.Client) fs.FS {
+ if client == nil {
+ return f
+ }
+
+ fsys := *f
+ fsys.httpClient = client
+
+ return &fsys
+}
+
+func (f *mergeFS) WithDataSources(sources map[string]config.DataSource) fs.FS {
+ if sources == nil {
+ return f
+ }
+
+ fsys := *f
+ fsys.sources = sources
+
+ return &fsys
+}
+
+func (f *mergeFS) Open(name string) (fs.File, error) {
+ parts := strings.Split(name, "|")
+ if len(parts) < 2 {
+ return nil, &fs.PathError{
+ Op: "open", Path: name,
+ Err: fmt.Errorf("need at least 2 datasources to merge"),
+ }
+ }
+
+ // now open each of the sub-files
+ subFiles := make([]subFile, len(parts))
+
+ modTime := time.Time{}
+
+ for i, part := range parts {
+ // if this is a datasource, look it up
+ subSource, ok := f.sources[part]
+ if !ok {
+ // maybe it's a relative filename?
+ u, uerr := urlhelpers.ParseSourceURL(part)
+ if uerr != nil {
+ return nil, fmt.Errorf("unknown datasource %q, and couldn't parse URL: %w", part, uerr)
+ }
+ subSource = config.DataSource{URL: u}
+ }
+
+ u := subSource.URL
+
+ fsURL, base := SplitFSMuxURL(u)
+
+ // need to support absolute paths on local filesystem too
+ // TODO: this is a hack, probably fix this?
+ if fsURL.Scheme == "file" && runtime.GOOS != "windows" {
+ base = fsURL.Path + base
+ }
+
+ fsys, err := FSysForPath(f.ctx, fsURL.String())
+ if err != nil {
+ return nil, &fs.PathError{
+ Op: "open", Path: name,
+ Err: fmt.Errorf("lookup for %s: %w", u.String(), err),
+ }
+ }
+
+ // pass in the context and other bits
+ fsys = fsimpl.WithContextFS(f.ctx, fsys)
+ fsys = fsimpl.WithHeaderFS(subSource.Header, fsys)
+
+ fsys = fsimpl.WithHTTPClientFS(f.httpClient, fsys)
+
+ // find the content type
+ fi, err := fs.Stat(fsys, base)
+ if err != nil {
+ return nil, &fs.PathError{
+ Op: "open", Path: name,
+ Err: fmt.Errorf("stat merge part %q: %w", part, err),
+ }
+ }
+
+ if fi.ModTime().After(modTime) {
+ modTime = fi.ModTime()
+ }
+
+ // 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)
+ }
+
+ f, err := fsys.Open(base)
+ if err != nil {
+ return nil, &fs.PathError{
+ Op: "open", Path: name,
+ Err: fmt.Errorf("opening merge part %q: %w", part, err),
+ }
+ }
+
+ subFiles[i] = subFile{f, mimeType}
+ }
+
+ return &mergeFile{
+ name: name,
+ subFiles: subFiles,
+ modTime: modTime,
+ }, nil
+}
+
+type subFile struct {
+ fs.File
+ contentType string
+}
+
+type mergeFile struct {
+ name string
+ merged io.Reader // the file's contents, post-merge - buffered here to enable partial reads
+ fi fs.FileInfo
+ modTime time.Time // the modTime of the most recently modified sub-file
+ subFiles []subFile
+ readMux sync.Mutex
+}
+
+var _ fs.File = (*mergeFile)(nil)
+
+func (f *mergeFile) Close() error {
+ for _, f := range f.subFiles {
+ f.Close()
+ }
+ return nil
+}
+
+func (f *mergeFile) Stat() (fs.FileInfo, error) {
+ if f.merged == nil {
+ p := make([]byte, 0)
+ _, err := f.Read(p)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return nil, fmt.Errorf("read: %w", err)
+ }
+ }
+
+ return f.fi, nil
+}
+
+func (f *mergeFile) Read(p []byte) (int, error) {
+ // read from all and merge, then return the requested amount
+ if f.merged == nil {
+ f.readMux.Lock()
+ defer f.readMux.Unlock()
+ // read from all and merge
+ data := make([]map[string]interface{}, len(f.subFiles))
+ for i, sf := range f.subFiles {
+ b, err := io.ReadAll(sf)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return 0, fmt.Errorf("readAll: %w", err)
+ }
+
+ data[i], err = parseMap(sf.contentType, string(b))
+ if err != nil {
+ return 0, fmt.Errorf("parsing map with content type %s: %w", sf.contentType, err)
+ }
+ }
+
+ md, err := mergeData(data)
+ if err != nil {
+ return 0, fmt.Errorf("mergeData: %w", err)
+ }
+
+ f.merged = bytes.NewReader(md)
+
+ f.fi = FileInfo(f.name, int64(len(md)), 0o400, f.modTime, iohelpers.YAMLMimetype)
+ }
+
+ return f.merged.Read(p)
+}
+
+func mergeData(data []map[string]interface{}) ([]byte, error) {
+ dst := data[0]
+ data = data[1:]
+
+ dst, err := coll.Merge(dst, data...)
+ if err != nil {
+ return nil, err
+ }
+
+ s, err := parsers.ToYAML(dst)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(s), nil
+}
+
+func parseMap(mimeType, data string) (map[string]interface{}, error) {
+ datum, err := parsers.ParseData(mimeType, data)
+ if err != nil {
+ return nil, fmt.Errorf("parseData: %w", 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/internal/datafs/mergefs_test.go b/internal/datafs/mergefs_test.go
new file mode 100644
index 00000000..1e2c777b
--- /dev/null
+++ b/internal/datafs/mergefs_test.go
@@ -0,0 +1,358 @@
+package datafs
+
+import (
+ "context"
+ "io"
+ "io/fs"
+ "mime"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+ "testing/fstest"
+
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/gomplate/v4/internal/config"
+ "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func mustParseURL(in string) *url.URL {
+ u, _ := url.Parse(in)
+ return u
+}
+
+func setupMergeFsys(ctx context.Context, t *testing.T) fs.FS {
+ t.Helper()
+
+ jsonContent := `{"hello": "world"}`
+ yamlContent := "hello: earth\ngoodnight: moon\n"
+ arrayContent := `["hello", "world"]`
+
+ 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)
+
+ t.Logf("wd: %s", wd)
+
+ fsys := 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...`)},
+ path.Join(wd, "tmp/jsonfile.json"): {Data: []byte(jsonContent)},
+ path.Join(wd, "tmp/array.json"): {Data: []byte(arrayContent)},
+ path.Join(wd, "tmp/yamlfile.yaml"): {Data: []byte(yamlContent)},
+ path.Join(wd, "tmp/textfile.txt"): {Data: []byte(`plain text...`)},
+ })
+
+ source := config.DataSource{
+ URL: mustParseURL("merge:file:///tmp/jsonfile.json|file:///tmp/yamlfile.yaml"),
+ }
+ sources := map[string]config.DataSource{
+ "foo": source,
+ "bar": {URL: mustParseURL("file:///tmp/jsonfile.json")},
+ "baz": {URL: mustParseURL("file:///tmp/yamlfile.yaml")},
+ "text": {URL: mustParseURL("file:///tmp/textfile.txt")},
+ "badscheme": {URL: mustParseURL("bad:///scheme.json")},
+ // mime type overridden by URL query, should fail to parse
+ "badtype": {URL: mustParseURL("file:///tmp/jsonfile.json?type=foo/bar")},
+ "array": {
+ URL: mustParseURL("file:///tmp/array.json?type=" + url.QueryEscape(iohelpers.JSONArrayMimetype)),
+ },
+ }
+
+ mux := fsimpl.NewMux()
+ mux.Add(MergeFS)
+ mux.Add(WrappedFSProvider(fsys, "file", ""))
+
+ ctx = ContextWithFSProvider(ctx, mux)
+
+ fsys, err := NewMergeFS(mustParseURL("merge:///"))
+ require.NoError(t, err)
+
+ fsys = WithDataSourcesFS(sources, fsys)
+ fsys = fsimpl.WithContextFS(ctx, fsys)
+
+ return fsys
+}
+
+// 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"
+
+// fsys := fstest.MapFS{}
+// fsys["tmp"] = &fstest.MapFile{Mode: fs.ModeDir | 0777}
+// fsys["tmp/jsonfile.json"] = &fstest.MapFile{Data: []byte(jsonContent)}
+// fsys["tmp/array.json"] = &fstest.MapFile{Data: []byte(arrayContent)}
+// fsys["tmp/yamlfile.yaml"] = &fstest.MapFile{Data: []byte(yamlContent)}
+// fsys["tmp/textfile.txt"] = &fstest.MapFile{Data: []byte(`plain text...`)}
+
+// // workding dir with volume name trimmed
+// wd, _ := os.Getwd()
+// vol := filepath.VolumeName(wd)
+// wd = wd[len(vol)+1:]
+
+// fsys[path.Join(wd, "jsonfile.json")] = &fstest.MapFile{Data: []byte(jsonContent)}
+// fsys[path.Join(wd, "array.json")] = &fstest.MapFile{Data: []byte(arrayContent)}
+// fsys[path.Join(wd, "yamlfile.yaml")] = &fstest.MapFile{Data: []byte(yamlContent)}
+// fsys[path.Join(wd, "textfile.txt")] = &fstest.MapFile{Data: []byte(`plain text...`)}
+
+// fsmux := fsimpl.NewMux()
+// fsmux.Add(fsimpl.WrappedFSProvider(&fsys, "file"))
+// ctx = datafs.ContextWithFSProvider(ctx, fsmux)
+
+// source := &Source{Alias: "foo", URL: mustParseURL("merge:file:///tmp/jsonfile.json|file:///tmp/yamlfile.yaml")}
+// 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))},
+// },
+// Ctx: ctx,
+// }
+
+// actual, err := d.readMerge(ctx, source)
+// assert.NoError(t, err)
+// assert.Equal(t, mergedContent, string(actual))
+
+// source.URL = mustParseURL("merge:bar|baz")
+// actual, err = d.readMerge(ctx, source)
+// assert.NoError(t, err)
+// assert.Equal(t, mergedContent, string(actual))
+
+// source.URL = mustParseURL("merge:./jsonfile.json|baz")
+// actual, err = d.readMerge(ctx, source)
+// assert.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})
+ assert.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})
+ assert.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})
+ assert.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})
+ assert.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})
+ assert.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})
+ assert.NoError(t, err)
+ assert.Equal(t, "f: false\nm:\n a: aaa\n b: bbb\nt: true\nz: over\n", string(out))
+}
+
+func TestMergeFS_Open(t *testing.T) {
+ // u, _ := url.Parse("merge:")
+ fsys := setupMergeFsys(context.Background(), t)
+ assert.IsType(t, &mergeFS{}, fsys)
+
+ _, err := fsys.Open("/")
+ assert.Error(t, err)
+
+ _, err = fsys.Open("just/one/part")
+ assert.Error(t, err)
+ assert.ErrorContains(t, err, "need at least 2 datasources to merge")
+
+ // missing aliases, fallback to relative files, but there's no FS registered
+ // for the empty scheme
+ _, err = fsys.Open("a|b")
+ assert.ErrorIs(t, err, fs.ErrNotExist)
+
+ // missing alias
+ _, err = fsys.Open("bogusalias|file:///tmp/jsonfile.json")
+ assert.ErrorIs(t, err, fs.ErrNotExist)
+
+ // unregistered scheme
+ _, err = fsys.Open("file:///tmp/jsonfile.json|badscheme")
+ assert.ErrorContains(t, err, "no filesystem registered for scheme \"bad\"")
+}
+
+func TestMergeFile_Read(t *testing.T) {
+ fsys := fstest.MapFS{
+ "one.yml": {Data: []byte("one: 1\n")},
+ "two.json": {Data: []byte(`{"one": false, "two": 2}`)},
+ "three.toml": {Data: []byte("one = 999\nthree = 3\n")},
+ }
+
+ files := make([]subFile, 3)
+ for i, fn := range []string{"one.yml", "two.json", "three.toml"} {
+ f, _ := fsys.Open(fn)
+ defer f.Close()
+
+ ct := mime.TypeByExtension(filepath.Ext(fn))
+
+ files[i] = subFile{f, ct}
+ }
+
+ mf := &mergeFile{name: "one.yml|two.json|three.toml", subFiles: files}
+
+ b, err := io.ReadAll(mf)
+ require.NoError(t, err)
+ assert.Equal(t, "one: 1\nthree: 3\ntwo: 2\n", string(b))
+
+ // now try with partial reads
+ for i, fn := range []string{"one.yml", "two.json", "three.toml"} {
+ f, _ := fsys.Open(fn)
+ defer f.Close()
+
+ ct := mime.TypeByExtension(filepath.Ext(fn))
+
+ files[i] = subFile{f, ct}
+ }
+
+ mf = &mergeFile{name: "one.yml|two.json|three.toml", subFiles: files}
+
+ p := make([]byte, 10)
+ n, err := mf.Read(p)
+ require.NoError(t, err)
+ assert.Equal(t, 10, n)
+ assert.Equal(t, "one: 1\nthr", string(p))
+
+ n, err = mf.Read(p)
+ require.NoError(t, err)
+ assert.Equal(t, 10, n)
+ assert.Equal(t, "ee: 3\ntwo:", string(p))
+
+ n, err = mf.Read(p)
+ require.NoError(t, err)
+ assert.Equal(t, 3, n)
+ assert.Equal(t, " 2\n 3\ntwo:", string(p))
+}
+
+func TestMergeFS_ReadFile(t *testing.T) {
+ mergedContent := "goodnight: moon\nhello: world\n"
+
+ fsys := setupMergeFsys(context.Background(), t)
+
+ testdata := []string{
+ // absolute URLs
+ "file:///tmp/jsonfile.json|file:///tmp/yamlfile.yaml",
+ // aliases
+ "bar|baz",
+ // mixed relative file and alias
+ "jsonfile.json|baz",
+ // relative file with ./ and alias
+ "./jsonfile.json|baz",
+ }
+
+ for _, td := range testdata {
+ t.Run(td, func(t *testing.T) {
+ f, err := fsys.Open(td)
+ require.NoError(t, err)
+ defer f.Close()
+
+ b, err := io.ReadAll(f)
+ require.NoError(t, err)
+ assert.Equal(t, mergedContent, string(b))
+ })
+ }
+
+ // read errors
+ errortests := []struct {
+ in string
+ expectedError string
+ }{
+ {"file:///tmp/jsonfile.json|badtype", "data of type \"foo/bar\" not yet supported"},
+ {"file:///tmp/jsonfile.json|array", "can only merge maps"},
+ }
+
+ for _, td := range errortests {
+ t.Run(td.in, func(t *testing.T) {
+ f, err := fsys.Open(td.in)
+ require.NoError(t, err)
+ defer f.Close()
+
+ _, err = io.ReadAll(f)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), td.expectedError)
+ })
+ }
+}
diff --git a/internal/datafs/stdinfs.go b/internal/datafs/stdinfs.go
new file mode 100644
index 00000000..46cb030a
--- /dev/null
+++ b/internal/datafs/stdinfs.go
@@ -0,0 +1,110 @@
+package datafs
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "io/fs"
+ "net/url"
+ "time"
+
+ "github.com/hairyhenderson/go-fsimpl"
+)
+
+// NewStdinFS returns a filesystem (an fs.FS) that can be used to read data from
+// standard input (os.Stdin).
+func NewStdinFS(_ *url.URL) (fs.FS, error) {
+ return &stdinFS{ctx: context.Background()}, nil
+}
+
+type stdinFS struct {
+ ctx context.Context
+}
+
+//nolint:gochecknoglobals
+var StdinFS = fsimpl.FSProviderFunc(NewStdinFS, "stdin")
+
+var (
+ _ fs.FS = (*stdinFS)(nil)
+ _ fs.ReadFileFS = (*stdinFS)(nil)
+ _ withContexter = (*stdinFS)(nil)
+)
+
+func (f stdinFS) WithContext(ctx context.Context) fs.FS {
+ fsys := f
+ fsys.ctx = ctx
+
+ return &fsys
+}
+
+func (f *stdinFS) Open(name string) (fs.File, error) {
+ if !fs.ValidPath(name) {
+ return nil, &fs.PathError{
+ Op: "open",
+ Path: name,
+ Err: fs.ErrInvalid,
+ }
+ }
+
+ stdin := StdinFromContext(f.ctx)
+
+ return &stdinFile{name: name, body: stdin}, nil
+}
+
+func (f *stdinFS) ReadFile(name string) ([]byte, error) {
+ if !fs.ValidPath(name) {
+ return nil, &fs.PathError{
+ Op: "readFile",
+ Path: name,
+ Err: fs.ErrInvalid,
+ }
+ }
+
+ stdin := StdinFromContext(f.ctx)
+
+ return io.ReadAll(stdin)
+}
+
+type stdinFile struct {
+ body io.Reader
+ name string
+}
+
+var _ fs.File = (*stdinFile)(nil)
+
+func (f *stdinFile) Close() error {
+ if f.body == nil {
+ return &fs.PathError{Op: "close", Path: f.name, Err: fs.ErrClosed}
+ }
+
+ f.body = nil
+ return nil
+}
+
+func (f *stdinFile) stdinReader() (int, error) {
+ b, err := io.ReadAll(f.body)
+ if err != nil {
+ return 0, err
+ }
+
+ f.body = bytes.NewReader(b)
+
+ return len(b), err
+}
+
+func (f *stdinFile) Stat() (fs.FileInfo, error) {
+ n, err := f.stdinReader()
+ if err != nil {
+ return nil, err
+ }
+
+ return FileInfo(f.name, int64(n), 0o444, time.Time{}, ""), nil
+}
+
+func (f *stdinFile) Read(p []byte) (int, error) {
+ if f.body == nil {
+ return 0, io.EOF
+ }
+
+ return f.body.Read(p)
+}
diff --git a/internal/datafs/stdinfs_test.go b/internal/datafs/stdinfs_test.go
new file mode 100644
index 00000000..2de477cc
--- /dev/null
+++ b/internal/datafs/stdinfs_test.go
@@ -0,0 +1,109 @@
+package datafs
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "io/fs"
+ "net/url"
+ "testing"
+
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestStdinFS_Open(t *testing.T) {
+ fsys, err := NewStdinFS(nil)
+ assert.NoError(t, err)
+ assert.IsType(t, &stdinFS{}, fsys)
+
+ f, err := fsys.Open("foo")
+ assert.NoError(t, err)
+ assert.IsType(t, &stdinFile{}, f)
+}
+
+func TestStdinFile_Read(t *testing.T) {
+ content := `hello world`
+
+ f := &stdinFile{name: "foo", body: bytes.NewBufferString(content)}
+ b := make([]byte, len(content))
+ n, err := f.Read(b)
+ assert.NoError(t, err)
+ assert.Equal(t, len(content), n)
+ assert.Equal(t, content, string(b))
+}
+
+func TestStdinFile_Stat(t *testing.T) {
+ content := []byte(`hello world`)
+
+ f := &stdinFile{name: "hello", body: bytes.NewReader(content)}
+
+ fi, err := f.Stat()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), fi.Size())
+
+ f = &stdinFile{name: "hello", body: &errorReader{err: fs.ErrPermission}}
+
+ _, err = f.Stat()
+ assert.ErrorIs(t, err, fs.ErrPermission)
+}
+
+func TestStdinFS(t *testing.T) {
+ u, _ := url.Parse("stdin:")
+
+ content := []byte("\nhello file\n")
+
+ ctx := ContextWithStdin(context.Background(), bytes.NewReader(content))
+
+ fsys, err := NewStdinFS(u)
+ assert.NoError(t, err)
+ assert.IsType(t, &stdinFS{}, fsys)
+
+ _, ok := fsys.(*stdinFS)
+ assert.True(t, ok)
+
+ fsys = fsimpl.WithContextFS(ctx, fsys)
+
+ b, err := fs.ReadFile(fsys, "foo")
+ assert.NoError(t, err)
+ assert.Equal(t, "\nhello file\n", string(b))
+
+ ctx = ContextWithStdin(context.Background(), bytes.NewReader(content))
+ fsys = fsimpl.WithContextFS(ctx, fsys)
+
+ _, err = fsys.Open("..")
+ assert.ErrorIs(t, err, fs.ErrInvalid)
+
+ _, err = fs.ReadFile(fsys, "/foo")
+ assert.ErrorIs(t, err, fs.ErrInvalid)
+
+ f, err := fsys.Open("doesn't matter what it's named.txt")
+ assert.NoError(t, err)
+
+ fi, err := f.Stat()
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), fi.Size())
+
+ b, err = io.ReadAll(f)
+ assert.NoError(t, err)
+ assert.Equal(t, content, b)
+
+ err = f.Close()
+ assert.NoError(t, err)
+
+ err = f.Close()
+ assert.ErrorIs(t, err, fs.ErrClosed)
+
+ p := make([]byte, 5)
+ _, err = f.Read(p)
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, io.EOF)
+}
+
+type errorReader struct {
+ err error
+}
+
+func (r *errorReader) Read(_ []byte) (int, error) {
+ return 0, r.err
+}
diff --git a/internal/datafs/vaultauth.go b/internal/datafs/vaultauth.go
new file mode 100644
index 00000000..3ad733a0
--- /dev/null
+++ b/internal/datafs/vaultauth.go
@@ -0,0 +1,89 @@
+package datafs
+
+import (
+ "context"
+ "fmt"
+ "io/fs"
+ "os"
+
+ "github.com/hairyhenderson/go-fsimpl/vaultfs/vaultauth"
+ "github.com/hairyhenderson/gomplate/v4/internal/deprecated"
+ "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/hashicorp/vault/api"
+ "github.com/hashicorp/vault/api/auth/aws"
+)
+
+// compositeVaultAuthMethod configures the auth method based on environment
+// variables. It extends [vaultfs.EnvAuthMethod] by falling back to AWS EC2
+// authentication if the other methods fail.
+func compositeVaultAuthMethod(envFsys fs.FS) api.AuthMethod {
+ return vaultauth.CompositeAuthMethod(
+ vaultauth.EnvAuthMethod(),
+ envEC2AuthAdapter(envFsys),
+ )
+}
+
+// func CompositeVaultAuthMethod() api.AuthMethod {
+// return compositeVaultAuthMethod(WrapWdFS(osfs.NewFS()))
+// }
+
+// envEC2AuthAdapter builds an AWS EC2 authentication method from environment
+// variables, for use only with [CompositeVaultAuthMethod]
+func envEC2AuthAdapter(envFS fs.FS) api.AuthMethod {
+ mountPath := GetenvFsys(envFS, "VAULT_AUTH_AWS_MOUNT", "aws")
+
+ nonce := GetenvFsys(envFS, "VAULT_AUTH_AWS_NONCE")
+ role := GetenvFsys(envFS, "VAULT_AUTH_AWS_ROLE")
+
+ // temporary workaround while we wait to deprecate AWS_META_ENDPOINT
+ if endpoint := os.Getenv("AWS_META_ENDPOINT"); endpoint != "" {
+ deprecated.WarnDeprecated(context.Background(), "Use AWS_EC2_METADATA_SERVICE_ENDPOINT instead of AWS_META_ENDPOINT")
+ if os.Getenv("AWS_EC2_METADATA_SERVICE_ENDPOINT") == "" {
+ os.Setenv("AWS_EC2_METADATA_SERVICE_ENDPOINT", endpoint)
+ }
+ }
+
+ awsauth, err := aws.NewAWSAuth(
+ aws.WithEC2Auth(),
+ aws.WithMountPath(mountPath),
+ aws.WithNonce(nonce),
+ aws.WithRole(role),
+ )
+ if err != nil {
+ return nil
+ }
+
+ output := GetenvFsys(envFS, "VAULT_AUTH_AWS_NONCE_OUTPUT")
+ if output == "" {
+ return awsauth
+ }
+
+ return &ec2AuthNonceWriter{AWSAuth: awsauth, nonce: nonce, output: output}
+}
+
+// ec2AuthNonceWriter - wraps an AWSAuth, and writes the nonce to the nonce
+// output file
+type ec2AuthNonceWriter struct {
+ *aws.AWSAuth
+ nonce string
+ output string
+}
+
+func (a *ec2AuthNonceWriter) Login(ctx context.Context, client *api.Client) (*api.Secret, error) {
+ secret, err := a.AWSAuth.Login(ctx, client)
+ if err != nil {
+ return nil, err
+ }
+
+ nonce := a.nonce
+ if val, ok := secret.Auth.Metadata["nonce"]; ok {
+ nonce = val
+ }
+
+ err = os.WriteFile(a.output, []byte(nonce+"\n"), iohelpers.NormalizeFileMode(0o600))
+ if err != nil {
+ return nil, fmt.Errorf("error writing nonce output file: %w", err)
+ }
+
+ return secret, nil
+}
diff --git a/internal/datafs/wdfs.go b/internal/datafs/wdfs.go
index e696e7d1..e10fb0f5 100644
--- a/internal/datafs/wdfs.go
+++ b/internal/datafs/wdfs.go
@@ -13,23 +13,38 @@ import (
"github.com/hairyhenderson/go-fsimpl"
)
-// ResolveLocalPath resolves a path on the local filesystem, relative to the
+// ResolveLocalPath resolves a path on the given filesystem, relative to the
// current working directory, and returns both the root (/ or a volume name on
// Windows) and the resolved path. If the path is absolute (e.g. starts with a `/` or
// volume name on Windows), it is split and returned as-is.
+// If fsys is nil, the current working directory is used.
// The output is suitable for use with [io/fs] functions.
-//
-// TODO: maybe take fsys as an argument, and if it's a wdFS, use its vol instead
-// of calling os.Getwd?
-func ResolveLocalPath(name string) (root, resolved string, err error) {
+func ResolveLocalPath(fsys fs.FS, name string) (root, resolved string, err error) {
// ignore empty names
if len(name) == 0 {
return "", "", nil
}
+ switch fsys := fsys.(type) {
+ case *wdFS:
+ return resolveLocalPath(fsys.vol, name)
+ default:
+ }
+
+ vol, err := workingVolume()
+ if err != nil {
+ return "", "", err
+ }
+
+ return resolveLocalPath(vol, name)
+}
+
+// workingVolume - returns the current working directory's volume name, or "/" if
+// the current working directory has no volume name (e.g. on Unix).
+func workingVolume() (string, error) {
wd, err := os.Getwd()
if err != nil {
- return "", "", fmt.Errorf("getwd: %w", err)
+ return "", fmt.Errorf("getwd: %w", err)
}
vol := filepath.VolumeName(wd)
@@ -37,11 +52,10 @@ func ResolveLocalPath(name string) (root, resolved string, err error) {
vol = "/"
}
- f := &wdFS{vol: vol}
- return f.resolveLocalPath(name)
+ return vol, nil
}
-func (w *wdFS) resolveLocalPath(name string) (root, resolved string, err error) {
+func resolveLocalPath(wvol, name string) (root, resolved string, err error) {
// ignore empty names
if len(name) == 0 {
return "", "", nil
@@ -53,15 +67,11 @@ func (w *wdFS) resolveLocalPath(name string) (root, resolved string, err error)
// special-case for (Windows) paths that start with '/' but have no volume
// name (e.g. "/foo/bar"). UNC paths (beginning with "//") are ignored.
if name[0] == '/' && (len(name) == 1 || (name[1] != '/' && name[1] != '?')) {
- name = filepath.Join(w.vol, name)
+ name = filepath.Join(wvol, name)
// TODO: maybe this can be reduced to just '!filepath.IsAbs(name)'?
} else if name[0] != '/' && !filepath.IsAbs(name) {
- abs := ""
- abs, err = filepath.Abs(name)
- if err != nil {
- return "", "", fmt.Errorf("abs %q: %w", name, err)
- }
- name = abs
+ wd, _ := os.Getwd()
+ name = filepath.Join(wd, name)
}
name, err = normalizeWindowsPath(name)
@@ -200,7 +210,7 @@ var WdFS = fsimpl.FSProviderFunc(
return nil, fmt.Errorf("unsupported path %q: %w", u.Path, fs.ErrInvalid)
}
- vol, _, err := ResolveLocalPath(u.Path)
+ vol, _, err := ResolveLocalPath(nil, u.Path)
if err != nil {
return nil, fmt.Errorf("resolve %q: %w", u.Path, err)
}
@@ -232,31 +242,37 @@ func WrapWdFS(fsys fs.FS) fs.FS {
return fsys
}
- return &wdFS{fsys: fsys}
+ vol, _ := workingVolume()
+
+ return &wdFS{vol: vol, fsys: fsys}
}
// wdFS is a filesystem wrapper that assumes non-absolute paths are relative to
// the current working directory (as reported by [os.Getwd]).
-// It only works in a meaningful way when used with a local filesystem (e.g.
+// It only works in a meaningful way when used with a local filesystem (e.g.
// [os.DirFS] or [hackpadfs/os.FS]).
type wdFS struct {
fsys fs.FS
- vol string
+
+ // volume name used for drive-relative paths on Windows for cases when they
+ // shouldn't be relative to the current working directory's volume
+ // TODO: validate that this is actually needed
+ vol string
}
var (
- _ fs.FS = &wdFS{}
- _ fs.StatFS = &wdFS{}
- _ fs.ReadFileFS = &wdFS{}
- _ fs.ReadDirFS = &wdFS{}
- _ fs.SubFS = &wdFS{}
- _ fs.GlobFS = &wdFS{}
- _ hackpadfs.CreateFS = &wdFS{}
- _ hackpadfs.OpenFileFS = &wdFS{}
- _ hackpadfs.MkdirFS = &wdFS{}
- _ hackpadfs.MkdirAllFS = &wdFS{}
- _ hackpadfs.RemoveFS = &wdFS{}
- _ hackpadfs.ChmodFS = &wdFS{}
+ _ fs.FS = (*wdFS)(nil)
+ _ fs.StatFS = (*wdFS)(nil)
+ _ fs.ReadFileFS = (*wdFS)(nil)
+ _ fs.ReadDirFS = (*wdFS)(nil)
+ _ fs.SubFS = (*wdFS)(nil)
+ _ fs.GlobFS = (*wdFS)(nil)
+ _ hackpadfs.CreateFS = (*wdFS)(nil)
+ _ hackpadfs.OpenFileFS = (*wdFS)(nil)
+ _ hackpadfs.MkdirFS = (*wdFS)(nil)
+ _ hackpadfs.MkdirAllFS = (*wdFS)(nil)
+ _ hackpadfs.RemoveFS = (*wdFS)(nil)
+ _ hackpadfs.ChmodFS = (*wdFS)(nil)
)
func (w *wdFS) fsysFor(vol string) (fs.FS, error) {
@@ -280,7 +296,7 @@ func (w *wdFS) fsysFor(vol string) (fs.FS, error) {
}
func (w *wdFS) Open(name string) (fs.File, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -292,7 +308,7 @@ func (w *wdFS) Open(name string) (fs.File, error) {
}
func (w *wdFS) Stat(name string) (fs.FileInfo, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -304,7 +320,7 @@ func (w *wdFS) Stat(name string) (fs.FileInfo, error) {
}
func (w *wdFS) ReadFile(name string) ([]byte, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -316,7 +332,7 @@ func (w *wdFS) ReadFile(name string) ([]byte, error) {
}
func (w *wdFS) ReadDir(name string) ([]fs.DirEntry, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -344,7 +360,7 @@ func (w *wdFS) Glob(_ string) ([]string, error) {
}
func (w *wdFS) Create(name string) (fs.File, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -356,7 +372,7 @@ func (w *wdFS) Create(name string) (fs.File, error) {
}
func (w *wdFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return nil, fmt.Errorf("resolve: %w", err)
}
@@ -368,7 +384,7 @@ func (w *wdFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error
}
func (w *wdFS) Mkdir(name string, perm fs.FileMode) error {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return fmt.Errorf("resolve: %w", err)
}
@@ -384,7 +400,7 @@ func (w *wdFS) Mkdir(name string, perm fs.FileMode) error {
}
func (w *wdFS) MkdirAll(name string, perm fs.FileMode) error {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return fmt.Errorf("resolve: %w", err)
}
@@ -396,7 +412,7 @@ func (w *wdFS) MkdirAll(name string, perm fs.FileMode) error {
}
func (w *wdFS) Remove(name string) error {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return fmt.Errorf("resolve: %w", err)
}
@@ -408,7 +424,7 @@ func (w *wdFS) Remove(name string) error {
}
func (w *wdFS) Chmod(name string, mode fs.FileMode) error {
- root, resolved, err := w.resolveLocalPath(name)
+ root, resolved, err := resolveLocalPath(w.vol, name)
if err != nil {
return fmt.Errorf("resolve: %w", err)
}
diff --git a/internal/datafs/wdfs_test.go b/internal/datafs/wdfs_test.go
index 768ec386..f14ebb19 100644
--- a/internal/datafs/wdfs_test.go
+++ b/internal/datafs/wdfs_test.go
@@ -140,6 +140,20 @@ func TestWDFS_WriteOps(t *testing.T) {
// and check that it's gone
_, err = fsys.Stat("/tmp/foo")
assert.ErrorIs(t, err, fs.ErrNotExist)
+
+ // make sure we can write to a subfs
+ subfs, err := fs.Sub(fsys, "tmp")
+ require.NoError(t, err)
+ require.NotNil(t, subfs)
+
+ // this is no longer a wdFS so we need to make sure not to use absolute
+ // paths - the path is relative to the root of the subfs
+ err = hackpadfs.WriteFullFile(subfs, "foo", []byte("hello world"), 0o600)
+ require.NoError(t, err)
+
+ b, err = fs.ReadFile(subfs, "foo")
+ require.NoError(t, err)
+ assert.Equal(t, "hello world", string(b))
}
func skipWindows(t *testing.T) {
@@ -160,6 +174,8 @@ func TestResolveLocalPath_NonWindows(t *testing.T) {
skipWindows(t)
wd, _ := os.Getwd()
+ fsys := &wdFS{vol: "/", fsys: osfs.NewFS()}
+
wd = wd[1:]
testdata := []struct {
@@ -176,7 +192,7 @@ func TestResolveLocalPath_NonWindows(t *testing.T) {
for _, td := range testdata {
td := td
t.Run(td.path, func(t *testing.T) {
- root, path, err := ResolveLocalPath(td.path)
+ root, path, err := ResolveLocalPath(fsys, td.path)
require.NoError(t, err)
assert.Equal(t, "/", root)
assert.Equal(t, td.expected, path)
@@ -189,9 +205,12 @@ func TestResolveLocalPath_Windows(t *testing.T) {
wd, _ := os.Getwd()
volname := filepath.VolumeName(wd)
- wd = wd[len(volname)+1:]
wd = filepath.ToSlash(wd)
+ fsys := &wdFS{vol: volname, fsys: osfs.NewFS()}
+
+ wd = wd[len(volname)+1:]
+
testdata := []struct {
path string
expRoot string
@@ -208,7 +227,7 @@ func TestResolveLocalPath_Windows(t *testing.T) {
for _, td := range testdata {
td := td
t.Run(td.path, func(t *testing.T) {
- root, path, err := ResolveLocalPath(td.path)
+ root, path, err := ResolveLocalPath(fsys, td.path)
require.NoError(t, err)
assert.Equal(t, td.expRoot, root)
assert.Equal(t, td.expected, path)
@@ -233,10 +252,8 @@ func TestWdFS_ResolveLocalPath_NonWindows(t *testing.T) {
{"/", "."},
}
- fsys := &wdFS{}
-
for _, td := range testdata {
- root, path, err := fsys.resolveLocalPath(td.path)
+ root, path, err := resolveLocalPath("/", td.path)
require.NoError(t, err)
assert.Equal(t, "/", root)
assert.Equal(t, td.expected, path)
@@ -248,8 +265,8 @@ func TestWdFS_ResolveLocalPath_Windows(t *testing.T) {
wd, _ := os.Getwd()
volname := filepath.VolumeName(wd)
- wd = wd[len(volname)+1:]
wd = filepath.ToSlash(wd)
+ wd = wd[len(volname)+1:]
testdata := []struct {
path string
@@ -268,12 +285,10 @@ func TestWdFS_ResolveLocalPath_Windows(t *testing.T) {
{`//somehost/share/foo/bar`, "//somehost/share", "foo/bar"},
}
- fsys := &wdFS{vol: volname}
-
for _, td := range testdata {
td := td
t.Run(td.path, func(t *testing.T) {
- root, path, err := fsys.resolveLocalPath(td.path)
+ root, path, err := resolveLocalPath(volname, td.path)
require.NoError(t, err)
assert.Equal(t, td.expRoot, root)
assert.Equal(t, td.expected, path)
diff --git a/internal/deprecated/deprecated.go b/internal/deprecated/deprecated.go
index c453ebbe..6f9ef317 100644
--- a/internal/deprecated/deprecated.go
+++ b/internal/deprecated/deprecated.go
@@ -2,6 +2,8 @@ package deprecated
import (
"context"
+ "fmt"
+ "log/slog"
"github.com/rs/zerolog"
)
@@ -10,5 +12,10 @@ import (
// datasources
func WarnDeprecated(ctx context.Context, msg string) {
logger := zerolog.Ctx(ctx)
+ if !logger.Warn().Enabled() {
+ // we'll flip to slog soon, but in the meantime if we don't have a
+ // logger in the context, just log it
+ slog.WarnContext(ctx, fmt.Sprintf("Deprecated: %s", msg))
+ }
logger.Warn().Msgf("Deprecated: %s", msg)
}
diff --git a/internal/iohelpers/mimetypes.go b/internal/iohelpers/mimetypes.go
new file mode 100644
index 00000000..f0678fe3
--- /dev/null
+++ b/internal/iohelpers/mimetypes.go
@@ -0,0 +1,33 @@
+package iohelpers
+
+import (
+ "mime"
+)
+
+const (
+ TextMimetype = "text/plain"
+ CSVMimetype = "text/csv"
+ JSONMimetype = "application/json"
+ JSONArrayMimetype = "application/array+json"
+ TOMLMimetype = "application/toml"
+ YAMLMimetype = "application/yaml"
+ EnvMimetype = "application/x-env"
+ CUEMimetype = "application/cue"
+)
+
+// mimeTypeAliases defines a mapping for non-canonical mime types that are
+// sometimes seen in the wild
+var mimeTypeAliases = map[string]string{
+ "application/x-yaml": YAMLMimetype,
+ "application/text": TextMimetype,
+}
+
+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
+ }
+ return m
+}
diff --git a/internal/iohelpers/write_test.go b/internal/iohelpers/write_test.go
new file mode 100644
index 00000000..e11b583f
--- /dev/null
+++ b/internal/iohelpers/write_test.go
@@ -0,0 +1,71 @@
+// this is in a separate package so WriteFile can be more thoroughly tested
+// without involving an import cycle with datafs
+package iohelpers_test
+
+import (
+ "io/fs"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/hack-pad/hackpadfs"
+ osfs "github.com/hack-pad/hackpadfs/os"
+ "github.com/hairyhenderson/gomplate/v4/internal/datafs"
+ "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ tfs "gotest.tools/v3/fs"
+)
+
+func TestWrite(t *testing.T) {
+ oldwd, _ := os.Getwd()
+ defer os.Chdir(oldwd)
+
+ rootDir := tfs.NewDir(t, "gomplate-test")
+ t.Cleanup(rootDir.Remove)
+
+ // we want to use a real filesystem here, so we can test interactions with
+ // the current working directory
+ fsys := datafs.WrapWdFS(osfs.NewFS())
+
+ newwd := rootDir.Join("the", "path", "we", "want")
+ badwd := rootDir.Join("some", "other", "dir")
+ hackpadfs.MkdirAll(fsys, newwd, 0o755)
+ hackpadfs.MkdirAll(fsys, badwd, 0o755)
+ newwd, _ = filepath.EvalSymlinks(newwd)
+ badwd, _ = filepath.EvalSymlinks(badwd)
+
+ err := os.Chdir(newwd)
+ require.NoError(t, err)
+
+ err = iohelpers.WriteFile(fsys, "/foo", []byte("Hello world"))
+ assert.Error(t, err)
+
+ rel, err := filepath.Rel(newwd, badwd)
+ require.NoError(t, err)
+ err = iohelpers.WriteFile(fsys, rel, []byte("Hello world"))
+ assert.Error(t, err)
+
+ foopath := filepath.Join(newwd, "foo")
+ err = iohelpers.WriteFile(fsys, foopath, []byte("Hello world"))
+ require.NoError(t, err)
+
+ out, err := fs.ReadFile(fsys, foopath)
+ require.NoError(t, err)
+ assert.Equal(t, "Hello world", string(out))
+
+ err = iohelpers.WriteFile(fsys, foopath, []byte("truncate"))
+ require.NoError(t, err)
+
+ out, err = fs.ReadFile(fsys, foopath)
+ require.NoError(t, err)
+ assert.Equal(t, "truncate", string(out))
+
+ foopath = filepath.Join(newwd, "nonexistant", "subdir", "foo")
+ err = iohelpers.WriteFile(fsys, foopath, []byte("Hello subdirranean world!"))
+ require.NoError(t, err)
+
+ out, err = fs.ReadFile(fsys, foopath)
+ require.NoError(t, err)
+ assert.Equal(t, "Hello subdirranean world!", string(out))
+}
diff --git a/internal/iohelpers/writers_test.go b/internal/iohelpers/writers_test.go
index 8a9c68e0..3dbce93b 100644
--- a/internal/iohelpers/writers_test.go
+++ b/internal/iohelpers/writers_test.go
@@ -4,17 +4,12 @@ import (
"bytes"
"fmt"
"io"
- "io/fs"
"os"
"path/filepath"
"testing"
- "github.com/hack-pad/hackpadfs"
- osfs "github.com/hack-pad/hackpadfs/os"
- "github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- tfs "gotest.tools/v3/fs"
)
func TestAllWhitespace(t *testing.T) {
@@ -166,58 +161,59 @@ func TestLazyWriteCloser(t *testing.T) {
assert.Error(t, err)
}
-func TestWrite(t *testing.T) {
- oldwd, _ := os.Getwd()
- defer os.Chdir(oldwd)
+// TODO: uncomment this and fix the import cycle!
+// func TestWrite(t *testing.T) {
+// oldwd, _ := os.Getwd()
+// defer os.Chdir(oldwd)
- rootDir := tfs.NewDir(t, "gomplate-test")
- t.Cleanup(rootDir.Remove)
+// rootDir := tfs.NewDir(t, "gomplate-test")
+// t.Cleanup(rootDir.Remove)
- // we want to use a real filesystem here, so we can test interactions with
- // the current working directory
- fsys := datafs.WrapWdFS(osfs.NewFS())
+// // we want to use a real filesystem here, so we can test interactions with
+// // the current working directory
+// fsys := datafs.WrapWdFS(osfs.NewFS())
- newwd := rootDir.Join("the", "path", "we", "want")
- badwd := rootDir.Join("some", "other", "dir")
- hackpadfs.MkdirAll(fsys, newwd, 0o755)
- hackpadfs.MkdirAll(fsys, badwd, 0o755)
- newwd, _ = filepath.EvalSymlinks(newwd)
- badwd, _ = filepath.EvalSymlinks(badwd)
+// newwd := rootDir.Join("the", "path", "we", "want")
+// badwd := rootDir.Join("some", "other", "dir")
+// hackpadfs.MkdirAll(fsys, newwd, 0o755)
+// hackpadfs.MkdirAll(fsys, badwd, 0o755)
+// newwd, _ = filepath.EvalSymlinks(newwd)
+// badwd, _ = filepath.EvalSymlinks(badwd)
- err := os.Chdir(newwd)
- require.NoError(t, err)
+// err := os.Chdir(newwd)
+// require.NoError(t, err)
- err = WriteFile(fsys, "/foo", []byte("Hello world"))
- assert.Error(t, err)
+// err = WriteFile(fsys, "/foo", []byte("Hello world"))
+// assert.Error(t, err)
- rel, err := filepath.Rel(newwd, badwd)
- require.NoError(t, err)
- err = WriteFile(fsys, rel, []byte("Hello world"))
- assert.Error(t, err)
+// rel, err := filepath.Rel(newwd, badwd)
+// require.NoError(t, err)
+// err = WriteFile(fsys, rel, []byte("Hello world"))
+// assert.Error(t, err)
- foopath := filepath.Join(newwd, "foo")
- err = WriteFile(fsys, foopath, []byte("Hello world"))
- require.NoError(t, err)
+// foopath := filepath.Join(newwd, "foo")
+// err = WriteFile(fsys, foopath, []byte("Hello world"))
+// require.NoError(t, err)
- out, err := fs.ReadFile(fsys, foopath)
- require.NoError(t, err)
- assert.Equal(t, "Hello world", string(out))
+// out, err := fs.ReadFile(fsys, foopath)
+// require.NoError(t, err)
+// assert.Equal(t, "Hello world", string(out))
- err = WriteFile(fsys, foopath, []byte("truncate"))
- require.NoError(t, err)
+// err = WriteFile(fsys, foopath, []byte("truncate"))
+// require.NoError(t, err)
- out, err = fs.ReadFile(fsys, foopath)
- require.NoError(t, err)
- assert.Equal(t, "truncate", string(out))
+// out, err = fs.ReadFile(fsys, foopath)
+// require.NoError(t, err)
+// assert.Equal(t, "truncate", string(out))
- foopath = filepath.Join(newwd, "nonexistant", "subdir", "foo")
- err = WriteFile(fsys, foopath, []byte("Hello subdirranean world!"))
- require.NoError(t, err)
+// foopath = filepath.Join(newwd, "nonexistant", "subdir", "foo")
+// err = WriteFile(fsys, foopath, []byte("Hello subdirranean world!"))
+// require.NoError(t, err)
- out, err = fs.ReadFile(fsys, foopath)
- require.NoError(t, err)
- assert.Equal(t, "Hello subdirranean world!", string(out))
-}
+// out, err = fs.ReadFile(fsys, foopath)
+// require.NoError(t, err)
+// assert.Equal(t, "Hello subdirranean world!", string(out))
+// }
func TestAssertPathInWD(t *testing.T) {
oldwd, _ := os.Getwd()
diff --git a/data/data.go b/internal/parsers/parsefuncs.go
index d2fc32fb..c16f86e7 100644
--- a/data/data.go
+++ b/internal/parsers/parsefuncs.go
@@ -1,8 +1,5 @@
-// Package data contains functions that parse and produce data structures in
-// different formats.
-//
-// Supported formats are: JSON, YAML, TOML, and CSV.
-package data
+// Package parsers has internal parsers for various formats.
+package parsers
import (
"bytes"
@@ -10,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "os"
"strings"
"cuelang.org/go/cue"
@@ -18,7 +16,6 @@ import (
"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
@@ -61,8 +58,8 @@ func JSON(in string) (map[string]interface{}, error) {
// 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")
+ keyDir := getenv("EJSON_KEYDIR", "/opt/ejson/keys")
+ key := getenv("EJSON_KEY")
rIn := bytes.NewBufferString(in)
rOut := &bytes.Buffer{}
@@ -79,6 +76,30 @@ func decryptEJSON(in string) (map[string]interface{}, error) {
return out, nil
}
+// a reimplementation of env.Getenv to avoid import cycles
+func getenv(key string, def ...string) string {
+ val := os.Getenv(key)
+ if val != "" {
+ return val
+ }
+
+ p := os.Getenv(key + "_FILE")
+ if p != "" {
+ b, err := os.ReadFile(p)
+ if err != nil {
+ return ""
+ }
+
+ val = strings.TrimSpace(string(b))
+ }
+
+ if val == "" && len(def) > 0 {
+ return def[0]
+ }
+
+ return val
+}
+
// JSONArray - Unmarshal a JSON Array
func JSONArray(in string) ([]interface{}, error) {
obj := make([]interface{}, 1)
@@ -190,8 +211,8 @@ func TOML(in string) (interface{}, error) {
return unmarshalObj(obj, in, toml.Unmarshal)
}
-// dotEnv - Unmarshal a dotenv file
-func dotEnv(in string) (interface{}, error) {
+// DotEnv - Unmarshal a dotenv file
+func DotEnv(in string) (interface{}, error) {
env, err := godotenv.Unmarshal(in)
if err != nil {
return nil, err
diff --git a/data/data_test.go b/internal/parsers/parsefuncs_test.go
index e4f0f555..caf83fba 100644
--- a/data/data_test.go
+++ b/internal/parsers/parsefuncs_test.go
@@ -1,4 +1,4 @@
-package data
+package parsers
import (
"fmt"
@@ -562,7 +562,7 @@ QUX='single quotes ignore $variables'
"BAZ": "variable expansion: a regular unquoted value",
"QUX": "single quotes ignore $variables",
}
- out, err := dotEnv(in)
+ out, err := DotEnv(in)
require.NoError(t, err)
assert.EqualValues(t, expected, out)
}
diff --git a/internal/parsers/parser.go b/internal/parsers/parser.go
new file mode 100644
index 00000000..b01972d8
--- /dev/null
+++ b/internal/parsers/parser.go
@@ -0,0 +1,39 @@
+package parsers
+
+import (
+ "fmt"
+
+ "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+)
+
+func ParseData(mimeType, s string) (out any, err error) {
+ switch iohelpers.MimeAlias(mimeType) {
+ case iohelpers.JSONMimetype:
+ out, err = JSON(s)
+ if err != nil {
+ // maybe it's a JSON array
+ out, err = JSONArray(s)
+ }
+ case iohelpers.JSONArrayMimetype:
+ out, err = JSONArray(s)
+ case iohelpers.YAMLMimetype:
+ out, err = YAML(s)
+ if err != nil {
+ // maybe it's a YAML array
+ out, err = YAMLArray(s)
+ }
+ case iohelpers.CSVMimetype:
+ out, err = CSV(s)
+ case iohelpers.TOMLMimetype:
+ out, err = TOML(s)
+ case iohelpers.EnvMimetype:
+ out, err = DotEnv(s)
+ case iohelpers.TextMimetype:
+ out = s
+ case iohelpers.CUEMimetype:
+ out, err = CUE(s)
+ default:
+ return nil, fmt.Errorf("data of type %q not yet supported", mimeType)
+ }
+ return out, err
+}
diff --git a/internal/tests/integration/basic_test.go b/internal/tests/integration/basic_test.go
index 49a3b168..f8aec7cf 100644
--- a/internal/tests/integration/basic_test.go
+++ b/internal/tests/integration/basic_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
+ "github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
tfs "gotest.tools/v3/fs"
@@ -29,7 +30,7 @@ func setupBasicTest(t *testing.T) *tfs.Dir {
func TestBasic_ReportsVersion(t *testing.T) {
o, e, err := cmd(t, "-v").run()
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, "", e)
assert.Assert(t, cmp.Contains(o, "gomplate version "))
}
@@ -88,11 +89,11 @@ func TestBasic_RoutesInputsToProperOutputs(t *testing.T) {
}
for _, v := range testdata {
info, err := os.Stat(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
m := iohelpers.NormalizeFileMode(v.mode)
assert.Equal(t, m, info.Mode(), v.path)
content, err := os.ReadFile(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, v.content, string(content))
}
}
@@ -209,10 +210,10 @@ func TestBasic_RoutesInputsToProperOutputsWithChmod(t *testing.T) {
}
for _, v := range testdata {
info, err := os.Stat(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, iohelpers.NormalizeFileMode(v.mode), info.Mode())
content, err := os.ReadFile(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, v.content, string(content))
}
}
@@ -237,10 +238,10 @@ func TestBasic_OverridesOutputModeWithChmod(t *testing.T) {
}
for _, v := range testdata {
info, err := os.Stat(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, iohelpers.NormalizeFileMode(v.mode), info.Mode())
content, err := os.ReadFile(v.path)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, v.content, string(content))
}
}
@@ -254,13 +255,13 @@ func TestBasic_AppliesChmodBeforeWrite(t *testing.T) {
"-f", tmpDir.Join("one"),
"-o", out,
"--chmod", "0644").run()
- assert.NilError(t, err)
+ require.NoError(t, err)
info, err := os.Stat(out)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, iohelpers.NormalizeFileMode(0o644), info.Mode())
content, err := os.ReadFile(out)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, "hi\n", string(content))
}
@@ -271,10 +272,10 @@ func TestBasic_CreatesMissingDirectory(t *testing.T) {
assertSuccess(t, o, e, err, "")
info, err := os.Stat(out)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, iohelpers.NormalizeFileMode(0o640), info.Mode())
content, err := os.ReadFile(out)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, "hi\n", string(content))
out = tmpDir.Join("outdir")
@@ -285,7 +286,7 @@ func TestBasic_CreatesMissingDirectory(t *testing.T) {
assertSuccess(t, o, e, err, "")
info, err = os.Stat(out)
- assert.NilError(t, err)
+ require.NoError(t, err)
assert.Equal(t, iohelpers.NormalizeFileMode(0o755|fs.ModeDir), info.Mode())
assert.Equal(t, true, info.IsDir())
diff --git a/internal/tests/integration/datasources_consul_test.go b/internal/tests/integration/datasources_consul_test.go
index cbf154d6..f04d5020 100644
--- a/internal/tests/integration/datasources_consul_test.go
+++ b/internal/tests/integration/datasources_consul_test.go
@@ -139,23 +139,28 @@ func TestDatasources_Consul_ListKeys(t *testing.T) {
consulPut(t, consulAddr, "list-of-keys/foo2", "bar2")
// Get a list of keys using the ds args
- expectedResult := `[{"key":"foo1","value":"{\"bar1\": \"bar1\"}"},{"key":"foo2","value":"bar2"}]`
+ // expectedResult := `[{"key":"foo1","value":"{\"bar1\": \"bar1\"}"},{"key":"foo2","value":"bar2"}]`
+ expectedResult := `["foo1","foo2"]`
o, e, err := cmd(t, "-d", "consul=consul://",
"-i", `{{(ds "consul" "list-of-keys/") | data.ToJSON }}`).
withEnv("CONSUL_HTTP_ADDR", "http://"+consulAddr).run()
assertSuccess(t, o, e, err, expectedResult)
// Get a list of keys using the ds uri
- expectedResult = `[{"key":"foo1","value":"{\"bar1\": \"bar1\"}"},{"key":"foo2","value":"bar2"}]`
+ // expectedResult = `[{"key":"foo1","value":"{\"bar1\": \"bar1\"}"},{"key":"foo2","value":"bar2"}]`
+ expectedResult = `["foo1","foo2"]`
o, e, err = cmd(t, "-d", "consul=consul+http://"+consulAddr+"/list-of-keys/",
"-i", `{{(ds "consul" ) | data.ToJSON }}`).run()
assertSuccess(t, o, e, err, expectedResult)
- // Get a specific value from the list of Consul keys
- expectedResult = `{"bar1": "bar1"}`
- o, e, err = cmd(t, "-d", "consul=consul+http://"+consulAddr+"/list-of-keys/",
- "-i", `{{ $data := ds "consul" }}{{ (index $data 0).value }}`).run()
- assertSuccess(t, o, e, err, expectedResult)
+ // TODO: this doesn't work anymore because consulfs returns a directory
+ // listing now.
+ //
+ // // Get a specific value from the list of Consul keys
+ // expectedResult = `{"bar1": "bar1"}`
+ // o, e, err = cmd(t, "-d", "consul=consul+http://"+consulAddr+"/list-of-keys/",
+ // "-i", `{{ $data := ds "consul" }}{{ (index $data 0).value }}`).run()
+ // assertSuccess(t, o, e, err, expectedResult)
}
func TestDatasources_Consul_WithVaultAuth(t *testing.T) {
diff --git a/internal/tests/integration/datasources_file_test.go b/internal/tests/integration/datasources_file_test.go
index 9af8503b..37e65a0f 100644
--- a/internal/tests/integration/datasources_file_test.go
+++ b/internal/tests/integration/datasources_file_test.go
@@ -1,6 +1,7 @@
package integration
import (
+ "path/filepath"
"testing"
"gotest.tools/v3/fs"
@@ -83,7 +84,7 @@ func TestDatasources_File(t *testing.T) {
"-i", `{{(datasource "config").foo.bar}}`).run()
assertSuccess(t, o, e, err, "baz")
- o, e, err = cmd(t, "-d", "dir="+tmpDir.Path(),
+ o, e, err = cmd(t, "-d", "dir="+tmpDir.Path()+string(filepath.Separator),
"-i", `{{ (datasource "dir" "config.json").foo.bar }}`).run()
assertSuccess(t, o, e, err, "baz")
diff --git a/internal/tests/integration/datasources_git_test.go b/internal/tests/integration/datasources_git_test.go
index fbc8e85d..cb8c78a9 100644
--- a/internal/tests/integration/datasources_git_test.go
+++ b/internal/tests/integration/datasources_git_test.go
@@ -15,6 +15,8 @@ import (
)
func setupDatasourcesGitTest(t *testing.T) *fs.Dir {
+ t.Helper()
+
tmpDir := fs.NewDir(t, "gomplate-inttests",
fs.WithDir("repo",
fs.WithFiles(map[string]string{
@@ -44,6 +46,8 @@ func setupDatasourcesGitTest(t *testing.T) *fs.Dir {
}
func startGitDaemon(t *testing.T) string {
+ t.Helper()
+
tmpDir := setupDatasourcesGitTest(t)
pidDir := fs.NewDir(t, "gomplate-inttests-pid")
diff --git a/internal/tests/integration/datasources_vault_ec2_test.go b/internal/tests/integration/datasources_vault_ec2_test.go
index fb34f0eb..9e4a5192 100644
--- a/internal/tests/integration/datasources_vault_ec2_test.go
+++ b/internal/tests/integration/datasources_vault_ec2_test.go
@@ -35,6 +35,10 @@ func setupDatasourcesVaultEc2Test(t *testing.T) (*fs.Dir, *vaultClient, *httptes
w.Write([]byte("testtoken"))
}))
+ mux.HandleFunc("/latest/meta-data/instance-id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Logf("IMDS request: %s %s", r.Method, r.URL)
+ w.Write([]byte("i-00000000"))
+ }))
mux.HandleFunc("/sts/", stsHandler)
mux.HandleFunc("/ec2/", ec2Handler)
mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -74,6 +78,7 @@ func TestDatasources_VaultEc2(t *testing.T) {
"endpoint": srv.URL + "/ec2",
"iam_endpoint": srv.URL + "/iam",
"sts_endpoint": srv.URL + "/sts",
+ "sts_region": "us-east-1",
})
require.NoError(t, err)
@@ -88,11 +93,11 @@ func TestDatasources_VaultEc2(t *testing.T) {
})
require.NoError(t, err)
- o, e, err := cmd(t, "-d", "vault=vault:///secret",
+ o, e, err := cmd(t, "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("HOME", tmpDir.Join("home")).
withEnv("VAULT_ADDR", "http://"+v.addr).
- withEnv("AWS_META_ENDPOINT", srv.URL).
+ withEnv("AWS_EC2_METADATA_SERVICE_ENDPOINT", srv.URL).
run()
assertSuccess(t, o, e, err, "bar")
}
diff --git a/internal/tests/integration/datasources_vault_test.go b/internal/tests/integration/datasources_vault_test.go
index 39ccb549..448fa759 100644
--- a/internal/tests/integration/datasources_vault_test.go
+++ b/internal/tests/integration/datasources_vault_test.go
@@ -20,6 +20,8 @@ import (
const vaultRootToken = "00000000-1111-2222-3333-444455556666"
func setupDatasourcesVaultTest(t *testing.T) *vaultClient {
+ t.Helper()
+
_, vaultClient := startVault(t)
err := vaultClient.vc.Sys().PutPolicy("writepol", `path "*" {
@@ -30,7 +32,7 @@ func setupDatasourcesVaultTest(t *testing.T) *vaultClient {
capabilities = ["read","delete"]
}`)
require.NoError(t, err)
- err = vaultClient.vc.Sys().PutPolicy("listPol", `path "*" {
+ err = vaultClient.vc.Sys().PutPolicy("listpol", `path "*" {
capabilities = ["read","list","delete"]
}`)
require.NoError(t, err)
@@ -39,6 +41,8 @@ func setupDatasourcesVaultTest(t *testing.T) *vaultClient {
}
func startVault(t *testing.T) (*fs.Dir, *vaultClient) {
+ t.Helper()
+
pidDir := fs.NewDir(t, "gomplate-inttests-vaultpid")
t.Cleanup(pidDir.Remove)
@@ -85,6 +89,8 @@ func startVault(t *testing.T) (*fs.Dir, *vaultClient) {
result.Assert(t, icmd.Expected{ExitCode: 0})
+ t.Log(result.Combined())
+
// restore old token if it was backed up
u, _ := user.Current()
homeDir := u.HomeDir
@@ -106,30 +112,32 @@ func TestDatasources_Vault_TokenAuth(t *testing.T) {
tok, err := v.tokenCreate("readpol", 5)
require.NoError(t, err)
- o, e, err := cmd(t, "-d", "vault=vault:///secret",
+ o, e, err := cmd(t, "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_TOKEN", tok).
run()
assertSuccess(t, o, e, err, "bar")
- o, e, err = cmd(t, "-d", "vault=vault+http://"+v.addr+"/secret",
+ o, e, err = cmd(t, "-d", "vault=vault+http://"+v.addr+"/secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_TOKEN", tok).
run()
assertSuccess(t, o, e, err, "bar")
- _, _, err = cmd(t, "-d", "vault=vault:///secret",
+ _, _, err = cmd(t, "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "bar").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_TOKEN", tok).
run()
- assert.ErrorContains(t, err, "error calling ds: couldn't read datasource 'vault': no value found for path /secret/bar")
+ assert.ErrorContains(t, err, "error calling ds: couldn't read datasource 'vault':")
+ assert.ErrorContains(t, err, "stat secret/bar")
+ assert.ErrorContains(t, err, "file does not exist")
tokFile := fs.NewFile(t, "test-vault-token", fs.WithContent(tok))
defer tokFile.Remove()
- o, e, err = cmd(t, "-d", "vault=vault:///secret",
+ o, e, err = cmd(t, "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_TOKEN_FILE", tokFile.Path()).
@@ -157,7 +165,7 @@ func TestDatasources_Vault_UserPassAuth(t *testing.T) {
})
require.NoError(t, err)
- o, e, err := cmd(t, "-d", "vault=vault:///secret",
+ o, e, err := cmd(t, "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_AUTH_USERNAME", "dave").
@@ -170,7 +178,7 @@ func TestDatasources_Vault_UserPassAuth(t *testing.T) {
defer userFile.Remove()
defer passFile.Remove()
o, e, err = cmd(t,
- "-d", "vault=vault:///secret",
+ "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_AUTH_USERNAME_FILE", userFile.Path()).
@@ -179,7 +187,7 @@ func TestDatasources_Vault_UserPassAuth(t *testing.T) {
assertSuccess(t, o, e, err, "bar")
o, e, err = cmd(t,
- "-d", "vault=vault:///secret",
+ "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_AUTH_USERNAME", "dave").
@@ -216,7 +224,7 @@ func TestDatasources_Vault_AppRoleAuth(t *testing.T) {
sid, _ := v.vc.Logical().Write("auth/approle/role/testrole/secret-id", nil)
secretID := sid.Data["secret_id"].(string)
o, e, err := cmd(t,
- "-d", "vault=vault:///secret",
+ "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_ROLE_ID", roleID).
@@ -229,7 +237,7 @@ func TestDatasources_Vault_AppRoleAuth(t *testing.T) {
sid, _ = v.vc.Logical().Write("auth/approle2/role/testrole/secret-id", nil)
secretID = sid.Data["secret_id"].(string)
o, e, err = cmd(t,
- "-d", "vault=vault:///secret",
+ "-d", "vault=vault:///secret/",
"-i", `{{(ds "vault" "foo").value}}`).
withEnv("VAULT_ADDR", "http://"+v.addr).
withEnv("VAULT_ROLE_ID", roleID).
@@ -258,7 +266,8 @@ func TestDatasources_Vault_DynamicAuth(t *testing.T) {
{"vault=vault:///ssh/creds/test?ip=10.1.2.3&username=user", `{{(ds "vault").ip}}`},
{"vault=vault:///?ip=10.1.2.3&username=user", `{{(ds "vault" "ssh/creds/test").ip}}`},
}
- tok, err := v.tokenCreate("writepol", len(testCommands)*2)
+
+ tok, err := v.tokenCreate("writepol", len(testCommands)*4)
require.NoError(t, err)
for _, tc := range testCommands {
@@ -277,7 +286,7 @@ func TestDatasources_Vault_List(t *testing.T) {
v.vc.Logical().Write("secret/dir/bar", map[string]interface{}{"value": "two"})
defer v.vc.Logical().Delete("secret/dir/foo")
defer v.vc.Logical().Delete("secret/dir/bar")
- tok, err := v.tokenCreate("listpol", 5)
+ tok, err := v.tokenCreate("listpol", 15)
require.NoError(t, err)
o, e, err := cmd(t,
@@ -289,7 +298,7 @@ func TestDatasources_Vault_List(t *testing.T) {
assertSuccess(t, o, e, err, "bar: two foo: one ")
o, e, err = cmd(t,
- "-d", "vault=vault+http://"+v.addr+"/secret",
+ "-d", "vault=vault+http://"+v.addr+"/secret/",
"-i", `{{ range (ds "vault" "dir/" ) }}{{ . }} {{end}}`).
withEnv("VAULT_TOKEN", tok).
run()
diff --git a/internal/tests/integration/integration_test.go b/internal/tests/integration/integration_test.go
index a57d8d0b..e80ed350 100644
--- a/internal/tests/integration/integration_test.go
+++ b/internal/tests/integration/integration_test.go
@@ -256,7 +256,9 @@ func (c *command) runInProcess() (o, e string, err error) {
stdin := strings.NewReader(c.stdin)
- ctx := context.Background()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
err = gcmd.Main(ctx, c.args, stdin, stdout, stderr)
return stdout.String(), stderr.String(), err
diff --git a/internal/urlhelpers/urlhelpers.go b/internal/urlhelpers/urlhelpers.go
new file mode 100644
index 00000000..d3de4ce8
--- /dev/null
+++ b/internal/urlhelpers/urlhelpers.go
@@ -0,0 +1,46 @@
+package urlhelpers
+
+import (
+ "net/url"
+ "path"
+ "path/filepath"
+)
+
+// ParseSourceURL parses a datasource URL value, which may be '-' (for stdin://),
+// or it may be a Windows path (with driver letter and back-slash separators) or
+// UNC, or it may be relative. It also might just be a regular absolute URL...
+// In all cases it returns a correct URL for the value. It may be a relative URL
+// in which case the scheme should be assumed to be 'file'
+func ParseSourceURL(value string) (*url.URL, error) {
+ if value == "-" {
+ value = "stdin://"
+ }
+ value = filepath.ToSlash(value)
+ // handle absolute Windows paths
+ volName := ""
+ if volName = filepath.VolumeName(value); volName != "" {
+ // handle UNCs
+ if len(volName) > 2 {
+ value = "file:" + value
+ } else {
+ value = "file:///" + value
+ }
+ }
+ srcURL, err := url.Parse(value)
+ if err != nil {
+ return nil, err
+ }
+
+ if volName != "" && len(srcURL.Path) >= 3 {
+ if srcURL.Path[0] == '/' && srcURL.Path[2] == ':' {
+ srcURL.Path = srcURL.Path[1:]
+ }
+ }
+
+ // if it's an absolute path with no scheme, assume it's a file
+ if srcURL.Scheme == "" && path.IsAbs(srcURL.Path) {
+ srcURL.Scheme = "file"
+ }
+
+ return srcURL, nil
+}
diff --git a/internal/urlhelpers/urlhelpers_test.go b/internal/urlhelpers/urlhelpers_test.go
new file mode 100644
index 00000000..9a0df386
--- /dev/null
+++ b/internal/urlhelpers/urlhelpers_test.go
@@ -0,0 +1,42 @@
+package urlhelpers
+
+import (
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseSourceURL(t *testing.T) {
+ expected := &url.URL{
+ Scheme: "http",
+ Host: "example.com",
+ Path: "/foo.json",
+ RawQuery: "bar",
+ }
+ u, err := ParseSourceURL("http://example.com/foo.json?bar")
+ require.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+
+ expected = &url.URL{Scheme: "", Path: ""}
+ u, err = ParseSourceURL("")
+ require.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+
+ expected = &url.URL{Scheme: "stdin"}
+ u, err = ParseSourceURL("-")
+ require.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+
+ // behviour change in v4 - return relative if it's relative
+ expected = &url.URL{Path: "./foo/bar.json"}
+ u, err = ParseSourceURL("./foo/bar.json")
+ require.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+
+ expected = &url.URL{Scheme: "file", Path: "/absolute/bar.json"}
+ u, err = ParseSourceURL("/absolute/bar.json")
+ require.NoError(t, err)
+ assert.EqualValues(t, expected, u)
+}
diff --git a/libkv/consul.go b/libkv/consul.go
deleted file mode 100644
index c47fb798..00000000
--- a/libkv/consul.go
+++ /dev/null
@@ -1,193 +0,0 @@
-package libkv
-
-import (
- "fmt"
- "net/url"
- "os"
- "strings"
- "time"
-
- "github.com/hairyhenderson/yaml"
-
- "github.com/docker/libkv"
- "github.com/docker/libkv/store"
- "github.com/docker/libkv/store/consul"
- "github.com/hairyhenderson/gomplate/v4/conv"
- "github.com/hairyhenderson/gomplate/v4/env"
- "github.com/hairyhenderson/gomplate/v4/vault"
- consulapi "github.com/hashicorp/consul/api"
-)
-
-const (
- http = "http"
- https = "https"
-
- // environment variables which aren't used by the consul client
- consulVaultRoleEnv = "CONSUL_VAULT_ROLE"
- consulVaultMountEnv = "CONSUL_VAULT_MOUNT"
- consulTimeoutEnv = "CONSUL_TIMEOUT"
-)
-
-// NewConsul - instantiate a new Consul datasource handler
-func NewConsul(u *url.URL) (*LibKV, error) {
- consul.Register()
-
- c, err := consulURL(u)
- if err != nil {
- return nil, err
- }
- config, err := consulConfig(c.Scheme == https)
- if err != nil {
- return nil, err
- }
-
- token, err := consulTokenFromVault()
- if err != nil {
- return nil, fmt.Errorf("failed to set Consul Vault token: %w", err)
- }
-
- if token != "" {
- // set CONSUL_HTTP_TOKEN before creating the client
- _ = os.Setenv(consulapi.HTTPTokenEnvName, token)
- }
-
- kv, err := libkv.NewStore(store.CONSUL, []string{c.String()}, config)
- if err != nil {
- return nil, fmt.Errorf("consul setup failed: %w", err)
- }
-
- return &LibKV{kv}, nil
-}
-
-func consulTokenFromVault() (string, error) {
- role := env.Getenv(consulVaultRoleEnv)
- if role == "" {
- return "", nil
- }
-
- client, err := vault.New(nil)
- if err != nil {
- return "", err
- }
-
- err = client.Login()
- defer client.Logout()
- if err != nil {
- return "", err
- }
-
- mount := env.Getenv(consulVaultMountEnv, "consul")
- path := fmt.Sprintf("%s/creds/%s", mount, role)
-
- data, err := client.Read(path)
- if err != nil {
- return "", fmt.Errorf("vault auth failed: %w", err)
- }
-
- decoded := make(map[string]interface{})
- err = yaml.Unmarshal(data, &decoded)
- if err != nil {
- return "", fmt.Errorf("YAML unmarshal: %w", err)
- }
-
- token := decoded["token"].(string)
-
- return token, nil
-}
-
-// consulAddrFromEnv parses the given address as either a URL or a host:port
-// pair. Given no schema, the URL will need to have a schema set separately.
-func consulAddrFromEnv(addr string) (*url.URL, error) {
- parts := strings.SplitN(addr, "://", 2)
- if len(parts) < 2 {
- // temporary schema so it parses correctly
- addr = "temp://" + addr
- }
-
- u, err := url.Parse(addr)
- if err != nil {
- return nil, err
- }
-
- if u.Scheme == "temp" {
- u.Scheme = ""
- }
-
- return u, nil
-}
-
-// consulURL gets the Consul URL from either the given URL or the
-// CONSUL_HTTP_ADDR environment variable. The given URL takes precedence.
-func consulURL(u *url.URL) (c *url.URL, err error) {
- if u.Host == "" {
- addrEnv := env.Getenv(consulapi.HTTPAddrEnvName)
- c, err = consulAddrFromEnv(addrEnv)
- if err != nil {
- return nil, fmt.Errorf("invalid URL %q: %w", addrEnv, err)
- }
-
- if c.Scheme == "" {
- c.Scheme = u.Scheme
- }
- } else {
- // We don't want the full URL here, just the scheme and host
- c = &url.URL{
- Scheme: u.Scheme,
- Host: u.Host,
- }
- }
-
- switch c.Scheme {
- case "consul+http", http:
- c.Scheme = http
- case "consul+https", https:
- c.Scheme = https
- case "consul":
- if conv.ToBool(env.Getenv(consulapi.HTTPSSLEnvName)) {
- c.Scheme = https
- } else {
- c.Scheme = http
- }
- }
-
- if c.Host == "" && u.Host == "" {
- c.Host = "localhost:8500"
- }
-
- return c, nil
-}
-
-func consulConfig(useTLS bool) (*store.Config, error) {
- t := conv.MustAtoi(env.Getenv(consulTimeoutEnv))
- config := &store.Config{
- ConnectionTimeout: time.Duration(t) * time.Second,
- }
-
- if useTLS {
- tconf := setupTLS()
-
- var err error
- config.TLS, err = consulapi.SetupTLSConfig(tconf)
- if err != nil {
- return nil, fmt.Errorf("TLS config setup failed: %w", err)
- }
- }
-
- return config, nil
-}
-
-func setupTLS() *consulapi.TLSConfig {
- tlsConfig := consulapi.TLSConfig{
- Address: env.Getenv(consulapi.HTTPTLSServerName),
- CAFile: env.Getenv(consulapi.HTTPCAFile),
- CAPath: env.Getenv(consulapi.HTTPCAPath),
- CertFile: env.Getenv(consulapi.HTTPClientCert),
- KeyFile: env.Getenv(consulapi.HTTPClientKey),
- }
-
- if v := env.Getenv(consulapi.HTTPSSLVerifyEnvName); v != "" {
- verify := conv.ToBool(v)
- tlsConfig.InsecureSkipVerify = !verify
- }
- return &tlsConfig
-}
diff --git a/libkv/consul_test.go b/libkv/consul_test.go
deleted file mode 100644
index 5f128aa7..00000000
--- a/libkv/consul_test.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package libkv
-
-import (
- "crypto/tls"
- "net/url"
- "os"
- "testing"
- "time"
-
- "github.com/docker/libkv/store"
- consulapi "github.com/hashicorp/consul/api"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestConsulURL(t *testing.T) {
- os.Unsetenv("CONSUL_HTTP_SSL")
-
- t.Run("consul scheme, CONSUL_HTTP_SSL set to true", func(t *testing.T) {
- t.Setenv("CONSUL_HTTP_SSL", "true")
-
- u, _ := url.Parse("consul://")
- expected := &url.URL{Host: "localhost:8500", Scheme: "https"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("consul+http scheme", func(t *testing.T) {
- u, _ := url.Parse("consul+http://myconsul.server")
- expected := &url.URL{Host: "myconsul.server", Scheme: "http"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("consul+https scheme, CONSUL_HTTP_SSL set to false", func(t *testing.T) {
- t.Setenv("CONSUL_HTTP_SSL", "false")
-
- u, _ := url.Parse("consul+https://myconsul.server:1234")
- expected := &url.URL{Host: "myconsul.server:1234", Scheme: "https"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("consul scheme, CONSUL_HTTP_SSL unset", func(t *testing.T) {
- u, _ := url.Parse("consul://myconsul.server:2345")
- expected := &url.URL{Host: "myconsul.server:2345", Scheme: "http"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("consul scheme, ignore path", func(t *testing.T) {
- u, _ := url.Parse("consul://myconsul.server:3456/foo/bar/baz")
- expected := &url.URL{Host: "myconsul.server:3456", Scheme: "http"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("given URL takes precedence over env var", func(t *testing.T) {
- t.Setenv("CONSUL_HTTP_ADDR", "https://foo:8500")
-
- u, _ := url.Parse("consul://myconsul.server:3456/foo/bar/baz")
- expected := &url.URL{Host: "myconsul.server:3456", Scheme: "http"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("TLS enabled, HTTP_ADDR is set, URL has no host and ambiguous scheme", func(t *testing.T) {
- t.Setenv("CONSUL_HTTP_ADDR", "https://foo:8500")
- t.Setenv("CONSUL_HTTP_SSL", "true")
-
- u, _ := url.Parse("consul://")
- expected := &url.URL{Host: "foo:8500", Scheme: "https"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-
- t.Run("TLS enabled, HTTP_ADDR is set without scheme, URL has no host and ambiguous scheme", func(t *testing.T) {
- t.Setenv("CONSUL_HTTP_ADDR", "localhost:8501")
- t.Setenv("CONSUL_HTTP_SSL", "true")
-
- u, _ := url.Parse("consul://")
- expected := &url.URL{Host: "localhost:8501", Scheme: "https"}
- actual, err := consulURL(u)
- require.NoError(t, err)
- assert.Equal(t, expected, actual)
- })
-}
-
-func TestConsulAddrFromEnv(t *testing.T) {
- in := ""
-
- _, err := consulAddrFromEnv("bogus:url:xxx")
- assert.Error(t, err)
-
- addr, err := consulAddrFromEnv(in)
- require.NoError(t, err)
- assert.Empty(t, addr)
-
- addr, err = consulAddrFromEnv("https://foo:8500")
- require.NoError(t, err)
- assert.Equal(t, &url.URL{Scheme: "https", Host: "foo:8500"}, addr)
-
- addr, err = consulAddrFromEnv("foo:8500")
- require.NoError(t, err)
- assert.Equal(t, &url.URL{Host: "foo:8500"}, addr)
-}
-
-func TestSetupTLS(t *testing.T) {
- expected := &consulapi.TLSConfig{
- Address: "address",
- CAFile: "cafile",
- CAPath: "ca/path",
- CertFile: "certfile",
- KeyFile: "keyfile",
- }
-
- t.Setenv("CONSUL_TLS_SERVER_NAME", expected.Address)
- t.Setenv("CONSUL_CACERT", expected.CAFile)
- t.Setenv("CONSUL_CAPATH", expected.CAPath)
- t.Setenv("CONSUL_CLIENT_CERT", expected.CertFile)
- t.Setenv("CONSUL_CLIENT_KEY", expected.KeyFile)
-
- assert.Equal(t, expected, setupTLS())
-
- t.Run("CONSUL_HTTP_SSL_VERIFY is true", func(t *testing.T) {
- expected.InsecureSkipVerify = false
- t.Setenv("CONSUL_HTTP_SSL_VERIFY", "true")
- assert.Equal(t, expected, setupTLS())
- })
-
- t.Run("CONSUL_HTTP_SSL_VERIFY is false", func(t *testing.T) {
- expected.InsecureSkipVerify = true
- t.Setenv("CONSUL_HTTP_SSL_VERIFY", "false")
- assert.Equal(t, expected, setupTLS())
- })
-}
-
-func TestConsulConfig(t *testing.T) {
- t.Run("default ", func(t *testing.T) {
- expectedConfig := &store.Config{}
-
- actualConfig, err := consulConfig(false)
- require.NoError(t, err)
-
- assert.Equal(t, expectedConfig, actualConfig)
- })
-
- t.Run("with CONSUL_TIMEOUT", func(t *testing.T) {
- t.Setenv("CONSUL_TIMEOUT", "10")
- expectedConfig := &store.Config{
- ConnectionTimeout: 10 * time.Second,
- }
-
- actualConfig, err := consulConfig(false)
- require.NoError(t, err)
- assert.Equal(t, expectedConfig, actualConfig)
- })
-
- t.Run("with TLS", func(t *testing.T) {
- expectedConfig := &store.Config{
- TLS: &tls.Config{MinVersion: tls.VersionTLS13},
- }
- actualConfig, err := consulConfig(true)
- require.NoError(t, err)
- assert.NotNil(t, actualConfig.TLS)
- actualConfig.TLS = &tls.Config{MinVersion: tls.VersionTLS13}
- assert.Equal(t, expectedConfig, actualConfig)
- })
-}
diff --git a/libkv/libkv.go b/libkv/libkv.go
deleted file mode 100644
index 881bfbf0..00000000
--- a/libkv/libkv.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package libkv
-
-import (
- "bytes"
- "encoding/json"
- "strings"
-
- "github.com/docker/libkv/store"
-)
-
-// LibKV -
-type LibKV struct {
- store store.Store
-}
-
-// Login -
-func (kv *LibKV) Login() error {
- return nil
-}
-
-// Logout -
-func (kv *LibKV) Logout() {
-}
-
-// Read -
-func (kv *LibKV) Read(path string) ([]byte, error) {
- data, err := kv.store.Get(path)
- if err != nil {
- return nil, err
- }
-
- return data.Value, nil
-}
-
-// List -
-func (kv *LibKV) List(path string) ([]byte, error) {
- data, err := kv.store.List(path)
- if err != nil {
- return nil, err
- }
-
- result := []map[string]string{}
- for _, pair := range data {
- // Remove the path from the key
- key := strings.TrimPrefix(
- pair.Key,
- strings.TrimLeft(path, "/"),
- )
- result = append(
- result,
- map[string]string{
- "key": key,
- "value": string(pair.Value),
- },
- )
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(result); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
diff --git a/libkv/libkv_test.go b/libkv/libkv_test.go
deleted file mode 100644
index a0f9da68..00000000
--- a/libkv/libkv_test.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package libkv
-
-import (
- "errors"
- "testing"
-
- "github.com/docker/libkv/store"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestRead(t *testing.T) {
- s := &FakeStore{data: []*store.KVPair{
- {Key: "foo", Value: []byte("bar")},
- }}
- kv := &LibKV{s}
- _, err := kv.Read("foo")
-
- require.NoError(t, err)
-
- s = &FakeStore{err: errors.New("fail")}
- kv = &LibKV{s}
- _, err = kv.Read("foo")
-
- assert.Error(t, err)
-}
-
-type FakeStore struct {
- err error
- data []*store.KVPair
-}
-
-func (s *FakeStore) Put(_ string, _ []byte, _ *store.WriteOptions) error {
- return nil
-}
-
-func (s *FakeStore) Get(key string) (*store.KVPair, error) {
- if s.err != nil {
- return nil, s.err
- }
-
- for _, v := range s.data {
- if v.Key == key {
- return v, nil
- }
- }
- return nil, nil
-}
-
-func (s *FakeStore) Delete(_ string) error {
- return nil
-}
-
-func (s *FakeStore) Exists(_ string) (bool, error) {
- return false, nil
-}
-
-func (s *FakeStore) Watch(_ string, _ <-chan struct{}) (<-chan *store.KVPair, error) {
- return nil, nil
-}
-
-func (s *FakeStore) WatchTree(_ string, _ <-chan struct{}) (<-chan []*store.KVPair, error) {
- return nil, nil
-}
-
-func (s *FakeStore) NewLock(_ string, _ *store.LockOptions) (store.Locker, error) {
- return nil, nil
-}
-
-func (s *FakeStore) List(_ string) ([]*store.KVPair, error) {
- return nil, nil
-}
-
-func (s *FakeStore) DeleteTree(_ string) error {
- return nil
-}
-
-func (s *FakeStore) AtomicPut(_ string, _ []byte, _ *store.KVPair, _ *store.WriteOptions) (bool, *store.KVPair, error) {
- return false, nil, nil
-}
-
-func (s *FakeStore) AtomicDelete(_ string, _ *store.KVPair) (bool, error) {
- return false, nil
-}
-
-func (s *FakeStore) Close() {}
diff --git a/render.go b/render.go
index f3c99e54..8d79d80a 100644
--- a/render.go
+++ b/render.go
@@ -7,17 +7,25 @@ import (
"net/http"
"net/url"
"os"
+ "sync"
"text/template"
"time"
+ "github.com/hairyhenderson/go-fsimpl"
+ "github.com/hairyhenderson/go-fsimpl/autofs"
"github.com/hairyhenderson/gomplate/v4/data"
"github.com/hairyhenderson/gomplate/v4/internal/config"
+ "github.com/hairyhenderson/gomplate/v4/internal/datafs"
)
// Options for template rendering.
//
// Experimental: subject to breaking changes before the next major release
type Options struct {
+ // FSProvider - allows lookups of data source filesystems. Defaults to
+ // [DefaultFSProvider].
+ FSProvider fsimpl.FSProvider
+
// Datasources - map of datasources to be read on demand when the
// 'datasource'/'ds'/'include' functions are used.
Datasources map[string]Datasource
@@ -103,6 +111,7 @@ type Datasource struct {
type Renderer struct {
//nolint:staticcheck
data *data.Data
+ fsp fsimpl.FSProvider
nested config.Templates
funcs template.FuncMap
lDelim string
@@ -171,6 +180,10 @@ func NewRenderer(opts Options) *Renderer {
missingKey = "error"
}
+ if opts.FSProvider == nil {
+ opts.FSProvider = DefaultFSProvider
+ }
+
return &Renderer{
nested: nested,
data: d,
@@ -179,6 +192,7 @@ func NewRenderer(opts Options) *Renderer {
lDelim: opts.LDelim,
rDelim: opts.RDelim,
missingKey: missingKey,
+ fsp: opts.FSProvider,
}
}
@@ -202,10 +216,9 @@ type Template struct {
//
// Experimental: subject to breaking changes before the next major release
func (t *Renderer) RenderTemplates(ctx context.Context, templates []Template) error {
- // we need to inject the current context into the Data value, because
- // the Datasource method may need it
- // TODO: remove this in v4
- t.data.Ctx = ctx
+ if datafs.FSProviderFromContext(ctx) == nil {
+ ctx = datafs.ContextWithFSProvider(ctx, t.fsp)
+ }
// configure the template context with the refreshed Data value
// only done here because the data context may have changed
@@ -273,3 +286,22 @@ func (t *Renderer) Render(ctx context.Context, name, text string, wr io.Writer)
{Name: name, Text: text, Writer: wr},
})
}
+
+// DefaultFSProvider is the default filesystem provider used by gomplate
+var DefaultFSProvider = sync.OnceValue[fsimpl.FSProvider](
+ func() fsimpl.FSProvider {
+ fsp := fsimpl.NewMux()
+
+ // start with all go-fsimpl filesystems
+ fsp.Add(autofs.FS)
+
+ // override go-fsimpl's filefs with wdfs to handle working directories
+ fsp.Add(datafs.WdFS)
+
+ // gomplate-only filesystem
+ fsp.Add(datafs.EnvFS)
+ fsp.Add(datafs.StdinFS)
+ fsp.Add(datafs.MergeFS)
+
+ return fsp
+ })()
diff --git a/render_test.go b/render_test.go
index 45b22067..ab6f58f2 100644
--- a/render_test.go
+++ b/render_test.go
@@ -10,7 +10,7 @@ import (
"testing"
"testing/fstest"
- "github.com/hairyhenderson/gomplate/v4/data"
+ "github.com/hairyhenderson/go-fsimpl"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,7 +24,11 @@ func TestRenderTemplate(t *testing.T) {
_ = os.Chdir("/")
fsys := fstest.MapFS{}
- ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "mem"))
+ fsp := fsimpl.NewMux()
+ fsp.Add(datafs.EnvFS)
+ fsp.Add(datafs.StdinFS)
+ fsp.Add(datafs.WrappedFSProvider(fsys, "mem", ""))
+ ctx := datafs.ContextWithFSProvider(context.Background(), fsp)
// no options - built-in function
tr := NewRenderer(Options{})
@@ -47,7 +51,7 @@ func TestRenderTemplate(t *testing.T) {
"world": {URL: wu},
},
})
- ctx = data.ContextWithStdin(ctx, strings.NewReader("hello"))
+ ctx = datafs.ContextWithStdin(ctx, strings.NewReader("hello"))
out = &bytes.Buffer{}
err = tr.Render(ctx, "test", `{{ .hi | toUpper }} {{ (ds "world") | toUpper }}`, out)
require.NoError(t, err)
diff --git a/template.go b/template.go
index 71796ab4..8c0aa64a 100644
--- a/template.go
+++ b/template.go
@@ -100,7 +100,7 @@ func parseNestedTemplates(ctx context.Context, nested config.Templates, tmpl *te
}
// TODO: maybe need to do something with root here?
- _, reldir, err := datafs.ResolveLocalPath(u.Path)
+ _, reldir, err := datafs.ResolveLocalPath(fsys, u.Path)
if err != nil {
return fmt.Errorf("resolveLocalPath: %w", err)
}
@@ -227,20 +227,21 @@ func gatherTemplates(ctx context.Context, cfg *config.Config, outFileNamer func(
func walkDir(ctx context.Context, cfg *config.Config, dir string, outFileNamer func(context.Context, string) (string, error), excludeGlob []string, mode os.FileMode, modeOverride bool) ([]Template, error) {
dir = filepath.ToSlash(filepath.Clean(dir))
- // we want a filesystem rooted at dir, for relative matching
+ // get a filesystem rooted in the same volume as dir (or / on non-Windows)
fsys, err := datafs.FSysForPath(ctx, dir)
if err != nil {
- return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", dir, err)
+ return nil, err
}
// we need dir to be relative to the root of fsys
// TODO: maybe need to do something with root here?
- _, reldir, err := datafs.ResolveLocalPath(dir)
+ _, resolvedDir, err := datafs.ResolveLocalPath(fsys, dir)
if err != nil {
return nil, fmt.Errorf("resolveLocalPath: %w", err)
}
- subfsys, err := fs.Sub(fsys, reldir)
+ // we need to sub the filesystem to the dir
+ subfsys, err := fs.Sub(fsys, resolvedDir)
if err != nil {
return nil, fmt.Errorf("sub: %w", err)
}
@@ -248,7 +249,7 @@ func walkDir(ctx context.Context, cfg *config.Config, dir string, outFileNamer f
// just check . because fsys is subbed to dir already
dirStat, err := fs.Stat(subfsys, ".")
if err != nil {
- return nil, fmt.Errorf("stat %q (%q): %w", dir, reldir, err)
+ return nil, fmt.Errorf("stat %q (%q): %w", dir, resolvedDir, err)
}
dirMode := dirStat.Mode()
diff --git a/vault/auth.go b/vault/auth.go
deleted file mode 100644
index 188978ed..00000000
--- a/vault/auth.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package vault
-
-import (
- "fmt"
- "io"
- "os"
- "path"
- "strings"
- "time"
-
- "github.com/hairyhenderson/gomplate/v4/aws"
- "github.com/hairyhenderson/gomplate/v4/conv"
- "github.com/hairyhenderson/gomplate/v4/env"
- "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
-)
-
-// GetToken -
-func (v *Vault) GetToken() (string, error) {
- // sorted in order of precedence
- authFuncs := []func() (string, error){
- v.AppRoleLogin,
- v.GitHubLogin,
- v.UserPassLogin,
- v.TokenLogin,
- v.EC2Login,
- }
- for _, f := range authFuncs {
- if token, err := f(); token != "" || err != nil {
- return token, err
- }
- }
- return "", fmt.Errorf("no vault auth methods succeeded")
-}
-
-// AppRoleLogin - approle auth backend
-func (v *Vault) AppRoleLogin() (string, error) {
- roleID := env.Getenv("VAULT_ROLE_ID")
- secretID := env.Getenv("VAULT_SECRET_ID")
-
- if roleID == "" || secretID == "" {
- return "", nil
- }
-
- mount := env.Getenv("VAULT_AUTH_APPROLE_MOUNT", "approle")
-
- vars := map[string]interface{}{
- "role_id": roleID,
- "secret_id": secretID,
- }
-
- path := fmt.Sprintf("auth/%s/login", mount)
- secret, err := v.client.Logical().Write(path, vars)
- if err != nil {
- return "", fmt.Errorf("appRole logon failed: %w", err)
- }
- if secret == nil {
- return "", fmt.Errorf("empty response from AppRole logon")
- }
-
- return secret.Auth.ClientToken, nil
-}
-
-// GitHubLogin - github auth backend
-func (v *Vault) GitHubLogin() (string, error) {
- githubToken := env.Getenv("VAULT_AUTH_GITHUB_TOKEN")
-
- if githubToken == "" {
- return "", nil
- }
-
- mount := env.Getenv("VAULT_AUTH_GITHUB_MOUNT", "github")
-
- vars := map[string]interface{}{
- "token": githubToken,
- }
-
- path := fmt.Sprintf("auth/%s/login", mount)
- secret, err := v.client.Logical().Write(path, vars)
- if err != nil {
- return "", fmt.Errorf("appRole logon failed: %w", err)
- }
- if secret == nil {
- return "", fmt.Errorf("empty response from AppRole logon")
- }
-
- return secret.Auth.ClientToken, nil
-}
-
-// UserPassLogin - userpass auth backend
-func (v *Vault) UserPassLogin() (string, error) {
- username := env.Getenv("VAULT_AUTH_USERNAME")
- password := env.Getenv("VAULT_AUTH_PASSWORD")
-
- if username == "" || password == "" {
- return "", nil
- }
-
- mount := env.Getenv("VAULT_AUTH_USERPASS_MOUNT", "userpass")
-
- vars := map[string]interface{}{
- "password": password,
- }
-
- path := fmt.Sprintf("auth/%s/login/%s", mount, username)
- secret, err := v.client.Logical().Write(path, vars)
- if err != nil {
- return "", fmt.Errorf("userPass logon failed: %w", err)
- }
- if secret == nil {
- return "", fmt.Errorf("empty response from UserPass logon")
- }
-
- return secret.Auth.ClientToken, nil
-}
-
-// EC2Login - AWS EC2 auth backend
-func (v *Vault) EC2Login() (string, error) {
- mount := env.Getenv("VAULT_AUTH_AWS_MOUNT", "aws")
- output := env.Getenv("VAULT_AUTH_AWS_NONCE_OUTPUT")
-
- nonce := env.Getenv("VAULT_AUTH_AWS_NONCE")
-
- vars, err := createEc2LoginVars(nonce)
- if err != nil {
- return "", err
- }
- if vars["pkcs7"] == "" {
- return "", nil
- }
-
- path := fmt.Sprintf("auth/%s/login", mount)
- secret, err := v.client.Logical().Write(path, vars)
- if err != nil {
- return "", fmt.Errorf("AWS EC2 logon failed: %w", err)
- }
- if secret == nil {
- return "", fmt.Errorf("empty response from AWS EC2 logon")
- }
-
- if output != "" {
- if val, ok := secret.Auth.Metadata["nonce"]; ok {
- nonce = val
- }
- f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, iohelpers.NormalizeFileMode(0o600))
- if err != nil {
- return "", fmt.Errorf("error opening nonce output file: %w", err)
- }
- n, err := f.Write([]byte(nonce + "\n"))
- if err != nil {
- return "", fmt.Errorf("error writing nonce output file: %w", err)
- }
- if n == 0 {
- return "", fmt.Errorf("no bytes written to nonce output file: %w", err)
- }
- }
-
- return secret.Auth.ClientToken, nil
-}
-
-func createEc2LoginVars(nonce string) (map[string]interface{}, error) {
- role := env.Getenv("VAULT_AUTH_AWS_ROLE")
-
- vars := map[string]interface{}{}
-
- if role != "" {
- vars["role"] = role
- }
-
- if nonce != "" {
- vars["nonce"] = nonce
- }
-
- opts := aws.ClientOptions{
- Timeout: time.Duration(conv.MustAtoi(env.Getenv("AWS_TIMEOUT"))) * time.Millisecond,
- }
-
- meta := aws.NewEc2Meta(opts)
-
- doc, err := meta.Dynamic("instance-identity/pkcs7")
- if err != nil {
- return nil, err
- }
- vars["pkcs7"] = strings.ReplaceAll(strings.TrimSpace(doc), "\n", "")
- return vars, nil
-}
-
-// TokenLogin -
-func (v *Vault) TokenLogin() (string, error) {
- if token := env.Getenv("VAULT_TOKEN"); token != "" {
- return token, nil
- }
- homeDir, err := os.UserHomeDir()
- if err != nil {
- return "", err
- }
- f, err := os.OpenFile(path.Join(homeDir, ".vault-token"), os.O_RDONLY, 0)
- if err != nil {
- return "", nil
- }
- b, err := io.ReadAll(f)
- if err != nil {
- return "", err
- }
- return string(b), nil
-}
diff --git a/vault/auth_test.go b/vault/auth_test.go
deleted file mode 100644
index fa8f29cc..00000000
--- a/vault/auth_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package vault
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestLogin(t *testing.T) {
- server, v := MockServer(404, "Not Found")
- defer server.Close()
- t.Setenv("VAULT_TOKEN", "foo")
- v.Login()
- assert.Equal(t, "foo", v.client.Token())
-}
-
-func TestTokenLogin(t *testing.T) {
- server, v := MockServer(404, "Not Found")
- defer server.Close()
- t.Setenv("VAULT_TOKEN", "foo")
-
- token, err := v.TokenLogin()
- require.NoError(t, err)
- assert.Equal(t, "foo", token)
-}
diff --git a/vault/testutils.go b/vault/testutils.go
deleted file mode 100644
index 479a669c..00000000
--- a/vault/testutils.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package vault
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "net/url"
-
- "github.com/hashicorp/vault/api"
-)
-
-// MockServer -
-func MockServer(code int, body string) (*httptest.Server, *Vault) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(code)
- fmt.Fprintln(w, body)
- }))
-
- tr := &http.Transport{
- Proxy: func(req *http.Request) (*url.URL, error) {
- return url.Parse(server.URL)
- },
- }
- httpClient := &http.Client{Transport: tr}
- config := &api.Config{
- Address: server.URL,
- HttpClient: httpClient,
- }
-
- c, _ := api.NewClient(config)
- return server, &Vault{c}
-}
diff --git a/vault/vault.go b/vault/vault.go
deleted file mode 100644
index 456e8820..00000000
--- a/vault/vault.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package vault
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/url"
-
- vaultapi "github.com/hashicorp/vault/api"
-)
-
-// Vault -
-type Vault struct {
- client *vaultapi.Client
-}
-
-// New -
-func New(u *url.URL) (*Vault, error) {
- vaultConfig := vaultapi.DefaultConfig()
-
- err := vaultConfig.ReadEnvironment()
- if err != nil {
- return nil, fmt.Errorf("vault setup failed: %w", err)
- }
-
- setVaultURL(vaultConfig, u)
-
- client, err := vaultapi.NewClient(vaultConfig)
- if err != nil {
- return nil, fmt.Errorf("vault setup failed: %w", err)
- }
-
- return &Vault{client}, nil
-}
-
-func setVaultURL(c *vaultapi.Config, u *url.URL) {
- if u != nil && u.Host != "" {
- scheme := "https"
- if u.Scheme == "vault+http" {
- scheme = "http"
- }
- c.Address = scheme + "://" + u.Host
- }
-}
-
-// Login -
-func (v *Vault) Login() error {
- token, err := v.GetToken()
- if err != nil {
- return err
- }
- v.client.SetToken(token)
- return nil
-}
-
-// Logout -
-func (v *Vault) Logout() {
- v.client.ClearToken()
-}
-
-// Read - returns the value of a given path. If no value is found at the given
-// path, returns empty slice.
-func (v *Vault) Read(path string) ([]byte, error) {
- secret, err := v.client.Logical().Read(path)
- if err != nil {
- return nil, err
- }
- if secret == nil {
- return []byte{}, nil
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(secret.Data); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-func (v *Vault) Write(path string, data map[string]interface{}) ([]byte, error) {
- secret, err := v.client.Logical().Write(path, data)
- if secret == nil {
- return []byte{}, err
- }
- if err != nil {
- return nil, err
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(secret.Data); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-// List -
-func (v *Vault) List(path string) ([]byte, error) {
- secret, err := v.client.Logical().List(path)
- if err != nil {
- return nil, err
- }
- if secret == nil {
- return nil, nil
- }
-
- keys, ok := secret.Data["keys"]
- if !ok {
- return nil, fmt.Errorf("keys param missing from vault list")
- }
-
- var buf bytes.Buffer
- enc := json.NewEncoder(&buf)
- if err := enc.Encode(keys); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
diff --git a/vault/vault_test.go b/vault/vault_test.go
deleted file mode 100644
index 1cc5e31d..00000000
--- a/vault/vault_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package vault
-
-import (
- "net/url"
- "os"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestNew(t *testing.T) {
- os.Unsetenv("VAULT_ADDR")
- v, err := New(nil)
- require.NoError(t, err)
- assert.Equal(t, "https://127.0.0.1:8200", v.client.Address())
-
- t.Setenv("VAULT_ADDR", "http://example.com:1234")
- v, err = New(nil)
- require.NoError(t, err)
- assert.Equal(t, "http://example.com:1234", v.client.Address())
- os.Unsetenv("VAULT_ADDR")
-
- u, _ := url.Parse("vault://vault.rocks:8200/secret/foo/bar")
- v, err = New(u)
- require.NoError(t, err)
- assert.Equal(t, "https://vault.rocks:8200", v.client.Address())
-
- u, _ = url.Parse("vault+https://vault.rocks:8200/secret/foo/bar")
- v, err = New(u)
- require.NoError(t, err)
- assert.Equal(t, "https://vault.rocks:8200", v.client.Address())
-
- u, _ = url.Parse("vault+http://vault.rocks:8200/secret/foo/bar")
- v, err = New(u)
- require.NoError(t, err)
- assert.Equal(t, "http://vault.rocks:8200", v.client.Address())
-}
-
-func TestRead(t *testing.T) {
- server, v := MockServer(404, "")
- defer server.Close()
- val, err := v.Read("secret/bogus")
- assert.Empty(t, val)
- require.NoError(t, err)
-
- expected := "{\"value\":\"foo\"}\n"
- server, v = MockServer(200, `{"data":`+expected+`}`)
- defer server.Close()
- val, err = v.Read("s")
- assert.Equal(t, expected, string(val))
- require.NoError(t, err)
-}
-
-func TestWrite(t *testing.T) {
- server, v := MockServer(404, "Not Found")
- defer server.Close()
- val, err := v.Write("secret/bogus", nil)
- assert.Empty(t, val)
- assert.Error(t, err)
-
- expected := "{\"value\":\"foo\"}\n"
- server, v = MockServer(200, `{"data":`+expected+`}`)
- defer server.Close()
- val, err = v.Write("s", nil)
- assert.Equal(t, expected, string(val))
- require.NoError(t, err)
-}