summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2024-06-09 19:25:17 -0400
committerGitHub <noreply@github.com>2024-06-09 19:25:17 -0400
commit47b74a5505d4c9979d24a8bcffde711a60c5f23a (patch)
tree479b4331848c549d69d0e2ba1f228dddb2069402 /internal
parentdc41e375484759c09e0480b10a30f6f80318bb56 (diff)
chore(api)!: Overhauling config and rendering types (#2094)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/cmd/config.go46
-rw-r--r--internal/cmd/config_test.go97
-rw-r--r--internal/cmd/main.go12
-rw-r--r--internal/config/configfile.go602
-rw-r--r--internal/config/configfile_test.go873
-rw-r--r--internal/config/types.go60
-rw-r--r--internal/config/types_test.go91
-rw-r--r--internal/datafs/context.go5
8 files changed, 156 insertions, 1630 deletions
diff --git a/internal/cmd/config.go b/internal/cmd/config.go
index fc64f8e4..431139a1 100644
--- a/internal/cmd/config.go
+++ b/internal/cmd/config.go
@@ -1,14 +1,17 @@
package cmd
import (
+ "bytes"
"context"
"fmt"
+ "io"
"log/slog"
+ "os"
"time"
+ "github.com/hairyhenderson/gomplate/v4"
"github.com/hairyhenderson/gomplate/v4/conv"
"github.com/hairyhenderson/gomplate/v4/env"
- "github.com/hairyhenderson/gomplate/v4/internal/config"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/spf13/cobra"
@@ -19,10 +22,10 @@ const (
)
// loadConfig is intended to be called before command execution. It:
-// - creates a config.Config from the cobra flags
-// - creates a config.Config from the config file (if present)
+// - creates a gomplate.Config from the cobra flags
+// - creates a gomplate.Config from the config file (if present)
// - merges the two (flags take precedence)
-func loadConfig(ctx context.Context, cmd *cobra.Command, args []string) (*config.Config, error) {
+func loadConfig(ctx context.Context, cmd *cobra.Command, args []string) (*gomplate.Config, error) {
flagConfig, err := cobraConfig(cmd, args)
if err != nil {
return nil, err
@@ -64,7 +67,7 @@ func pickConfigFile(cmd *cobra.Command) (cfgFile string, required bool) {
return cfgFile, required
}
-func readConfigFile(ctx context.Context, cmd *cobra.Command) (cfg *config.Config, err error) {
+func readConfigFile(ctx context.Context, cmd *cobra.Command) (*gomplate.Config, error) {
cfgFile, configRequired := pickConfigFile(cmd)
// we only support loading configs from the local filesystem for now
@@ -76,14 +79,14 @@ func readConfigFile(ctx context.Context, cmd *cobra.Command) (cfg *config.Config
f, err := fsys.Open(cfgFile)
if err != nil {
if configRequired {
- return cfg, fmt.Errorf("config file requested, but couldn't be opened: %w", err)
+ return nil, fmt.Errorf("config file requested, but couldn't be opened: %w", err)
}
return nil, nil
}
- cfg, err = config.Parse(f)
+ cfg, err := gomplate.Parse(f)
if err != nil {
- return cfg, fmt.Errorf("parsing config file %q: %w", cfgFile, err)
+ return nil, fmt.Errorf("parsing config file %q: %w", cfgFile, err)
}
slog.DebugContext(ctx, "using config file", "cfgFile", cfgFile)
@@ -92,8 +95,8 @@ func readConfigFile(ctx context.Context, cmd *cobra.Command) (cfg *config.Config
}
// cobraConfig - initialize a config from the commandline options
-func cobraConfig(cmd *cobra.Command, args []string) (cfg *config.Config, err error) {
- cfg = &config.Config{}
+func cobraConfig(cmd *cobra.Command, args []string) (cfg *gomplate.Config, err error) {
+ cfg = &gomplate.Config{}
cfg.InputFiles, err = getStringSlice(cmd, "file")
if err != nil {
return nil, err
@@ -241,7 +244,7 @@ func processIncludes(includes, excludes []string) []string {
return out
}
-func applyEnvVars(_ context.Context, cfg *config.Config) (*config.Config, error) {
+func applyEnvVars(_ context.Context, cfg *gomplate.Config) (*gomplate.Config, error) {
if to := env.Getenv("GOMPLATE_PLUGIN_TIMEOUT"); cfg.PluginTimeout == 0 && to != "" {
t, err := time.ParseDuration(to)
if err != nil {
@@ -263,3 +266,24 @@ func applyEnvVars(_ context.Context, cfg *config.Config) (*config.Config, error)
return cfg, nil
}
+
+// postExecInput - return the input to be used after the post-exec command. The
+// input config may be modified if ExecPipe is set (OutputFiles is set to "-"),
+// and Stdout is redirected to a pipe.
+func postExecInput(cfg *gomplate.Config) io.Reader {
+ if cfg.ExecPipe {
+ pipe := &bytes.Buffer{}
+ cfg.OutputFiles = []string{"-"}
+
+ // --exec-pipe redirects standard out to the out pipe
+ cfg.Stdout = pipe
+
+ return pipe
+ }
+
+ if cfg.Stdin != nil {
+ return cfg.Stdin
+ }
+
+ return os.Stdin
+}
diff --git a/internal/cmd/config_test.go b/internal/cmd/config_test.go
index 1c9adfe5..56d7b1d5 100644
--- a/internal/cmd/config_test.go
+++ b/internal/cmd/config_test.go
@@ -6,12 +6,13 @@ import (
"fmt"
"io/fs"
"net/url"
+ "os"
"testing"
"testing/fstest"
"time"
"github.com/hairyhenderson/go-fsimpl"
- "github.com/hairyhenderson/gomplate/v4/internal/config"
+ "github.com/hairyhenderson/gomplate/v4"
"github.com/hairyhenderson/gomplate/v4/internal/datafs"
"github.com/spf13/cobra"
@@ -47,7 +48,7 @@ func TestReadConfigFile(t *testing.T) {
cfg, err := readConfigFile(ctx, cmd)
require.NoError(t, err)
- assert.EqualValues(t, &config.Config{}, cfg)
+ assert.EqualValues(t, &gomplate.Config{}, cfg)
cmd.ParseFlags([]string{"--config", "config.yaml"})
@@ -55,7 +56,7 @@ func TestReadConfigFile(t *testing.T) {
cfg, err = readConfigFile(ctx, cmd)
require.NoError(t, err)
- assert.EqualValues(t, &config.Config{Input: "hello world"}, cfg)
+ assert.EqualValues(t, &gomplate.Config{Input: "hello world"}, cfg)
fsys["config.yaml"] = &fstest.MapFile{Data: []byte("in: hello world\nin: \n")}
@@ -87,7 +88,7 @@ func TestLoadConfig(t *testing.T) {
cmd.ParseFlags(nil)
out, err := loadConfig(ctx, cmd, cmd.Flags().Args())
- expected := &config.Config{
+ expected := &gomplate.Config{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
@@ -97,7 +98,7 @@ func TestLoadConfig(t *testing.T) {
cmd.ParseFlags([]string{"--in", "foo"})
out, err = loadConfig(ctx, cmd, cmd.Flags().Args())
- expected = &config.Config{
+ expected = &gomplate.Config{
Input: "foo",
Stdin: stdin,
Stdout: out.Stdout,
@@ -108,19 +109,37 @@ func TestLoadConfig(t *testing.T) {
cmd.ParseFlags([]string{"--in", "foo", "--exec-pipe", "--", "tr", "[a-z]", "[A-Z]"})
out, err = loadConfig(ctx, cmd, cmd.Flags().Args())
- expected = &config.Config{
- Input: "foo",
- ExecPipe: true,
- PostExec: []string{"tr", "[a-z]", "[A-Z]"},
- PostExecInput: out.PostExecInput,
- Stdin: stdin,
- Stdout: out.Stdout,
- Stderr: stderr,
+ expected = &gomplate.Config{
+ Input: "foo",
+ ExecPipe: true,
+ PostExec: []string{"tr", "[a-z]", "[A-Z]"},
+ Stdin: stdin,
+ Stdout: out.Stdout,
+ Stderr: stderr,
}
require.NoError(t, err)
assert.EqualValues(t, expected, out)
}
+func TestPostExecInput(t *testing.T) {
+ t.Parallel()
+
+ cfg := &gomplate.Config{ExecPipe: false}
+ assert.Equal(t, os.Stdin, postExecInput(cfg))
+
+ cfg = &gomplate.Config{ExecPipe: true}
+
+ pipe := postExecInput(cfg)
+ assert.IsType(t, &bytes.Buffer{}, pipe)
+ assert.Equal(t, []string{"-"}, cfg.OutputFiles)
+ assert.Equal(t, pipe, cfg.Stdout)
+
+ stdin := &bytes.Buffer{}
+ cfg = &gomplate.Config{ExecPipe: false, Stdin: stdin}
+ pipe = postExecInput(cfg)
+ assert.Equal(t, stdin, pipe)
+}
+
func TestCobraConfig(t *testing.T) {
t.Parallel()
cmd := &cobra.Command{}
@@ -133,13 +152,13 @@ func TestCobraConfig(t *testing.T) {
cfg, err := cobraConfig(cmd, cmd.Flags().Args())
require.NoError(t, err)
- assert.EqualValues(t, &config.Config{}, cfg)
+ assert.EqualValues(t, &gomplate.Config{}, cfg)
cmd.ParseFlags([]string{"--file", "in", "--", "echo", "foo"})
cfg, err = cobraConfig(cmd, cmd.Flags().Args())
require.NoError(t, err)
- assert.EqualValues(t, &config.Config{
+ assert.EqualValues(t, &gomplate.Config{
InputFiles: []string{"in"},
PostExec: []string{"echo", "foo"},
}, cfg)
@@ -195,68 +214,68 @@ func TestPickConfigFile(t *testing.T) {
func TestApplyEnvVars(t *testing.T) {
t.Run("invalid GOMPLATE_PLUGIN_TIMEOUT", func(t *testing.T) {
t.Setenv("GOMPLATE_PLUGIN_TIMEOUT", "bogus")
- _, err := applyEnvVars(context.Background(), &config.Config{})
+ _, err := applyEnvVars(context.Background(), &gomplate.Config{})
require.Error(t, err)
})
data := []struct {
- input, expected *config.Config
+ input, expected *gomplate.Config
env string
value string
}{
{
- &config.Config{PluginTimeout: 2 * time.Second},
- &config.Config{PluginTimeout: 2 * time.Second},
+ &gomplate.Config{PluginTimeout: 2 * time.Second},
+ &gomplate.Config{PluginTimeout: 2 * time.Second},
"GOMPLATE_PLUGIN_TIMEOUT", "bogus",
},
{
- &config.Config{},
- &config.Config{PluginTimeout: 2 * time.Second},
+ &gomplate.Config{},
+ &gomplate.Config{PluginTimeout: 2 * time.Second},
"GOMPLATE_PLUGIN_TIMEOUT", "2s",
},
{
- &config.Config{PluginTimeout: 100 * time.Millisecond},
- &config.Config{PluginTimeout: 100 * time.Millisecond},
+ &gomplate.Config{PluginTimeout: 100 * time.Millisecond},
+ &gomplate.Config{PluginTimeout: 100 * time.Millisecond},
"GOMPLATE_PLUGIN_TIMEOUT", "2s",
},
{
- &config.Config{},
- &config.Config{Experimental: false},
+ &gomplate.Config{},
+ &gomplate.Config{Experimental: false},
"GOMPLATE_EXPERIMENTAL", "bogus",
},
{
- &config.Config{},
- &config.Config{Experimental: true},
+ &gomplate.Config{},
+ &gomplate.Config{Experimental: true},
"GOMPLATE_EXPERIMENTAL", "true",
},
{
- &config.Config{Experimental: true},
- &config.Config{Experimental: true},
+ &gomplate.Config{Experimental: true},
+ &gomplate.Config{Experimental: true},
"GOMPLATE_EXPERIMENTAL", "false",
},
{
- &config.Config{},
- &config.Config{LDelim: "--"},
+ &gomplate.Config{},
+ &gomplate.Config{LDelim: "--"},
"GOMPLATE_LEFT_DELIM", "--",
},
{
- &config.Config{LDelim: "{{"},
- &config.Config{LDelim: "{{"},
+ &gomplate.Config{LDelim: "{{"},
+ &gomplate.Config{LDelim: "{{"},
"GOMPLATE_LEFT_DELIM", "--",
},
{
- &config.Config{},
- &config.Config{RDelim: ")>"},
+ &gomplate.Config{},
+ &gomplate.Config{RDelim: ")>"},
"GOMPLATE_RIGHT_DELIM", ")>",
},
{
- &config.Config{RDelim: "}}"},
- &config.Config{RDelim: "}}"},
+ &gomplate.Config{RDelim: "}}"},
+ &gomplate.Config{RDelim: "}}"},
"GOMPLATE_RIGHT_DELIM", ")>",
},
{
- &config.Config{RDelim: "}}"},
- &config.Config{RDelim: "}}"},
+ &gomplate.Config{RDelim: "}}"},
+ &gomplate.Config{RDelim: "}}"},
"GOMPLATE_RIGHT_DELIM", "",
},
}
diff --git a/internal/cmd/main.go b/internal/cmd/main.go
index 22388521..fa7c97c3 100644
--- a/internal/cmd/main.go
+++ b/internal/cmd/main.go
@@ -88,12 +88,8 @@ func NewGomplateCmd(stderr io.Writer) *cobra.Command {
return err
}
- if cfg.Experimental {
- slog.SetDefault(slog.With("experimental", true))
- slog.InfoContext(ctx, "experimental functions and features enabled!")
-
- ctx = gomplate.SetExperimental(ctx)
- }
+ // get the post-exec reader now as this may modify cfg
+ postExecReader := postExecInput(cfg)
slog.DebugContext(ctx, fmt.Sprintf("starting %s", cmd.Name()))
slog.DebugContext(ctx, fmt.Sprintf("config is:\n%v", cfg),
@@ -101,6 +97,7 @@ func NewGomplateCmd(stderr io.Writer) *cobra.Command {
slog.String("build", version.GitCommit),
)
+ // run the main command
err = gomplate.Run(ctx, cfg)
cmd.SilenceErrors = true
cmd.SilenceUsage = true
@@ -113,7 +110,8 @@ func NewGomplateCmd(stderr io.Writer) *cobra.Command {
if err != nil {
return err
}
- return postRunExec(ctx, cfg.PostExec, cfg.PostExecInput, cmd.OutOrStdout(), cmd.ErrOrStderr())
+
+ return postRunExec(ctx, cfg.PostExec, postExecReader, cmd.OutOrStdout(), cmd.ErrOrStderr())
},
Args: optionalExecArgs,
}
diff --git a/internal/config/configfile.go b/internal/config/configfile.go
deleted file mode 100644
index d255a36c..00000000
--- a/internal/config/configfile.go
+++ /dev/null
@@ -1,602 +0,0 @@
-package config
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "path"
- "strconv"
- "strings"
- "time"
-
- "golang.org/x/exp/slices"
-
- "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
- "github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
- "github.com/hairyhenderson/yaml"
-)
-
-// Parse a config file
-func Parse(in io.Reader) (*Config, error) {
- out := &Config{}
- dec := yaml.NewDecoder(in)
- err := dec.Decode(out)
- if err != nil && err != io.EOF {
- return out, fmt.Errorf("YAML decoding failed, syntax may be invalid: %w", err)
- }
- return out, nil
-}
-
-// Config - configures the gomplate execution
-type Config struct {
- Stdin io.Reader `yaml:"-"`
- Stdout io.Writer `yaml:"-"`
- Stderr io.Writer `yaml:"-"`
-
- DataSources map[string]DataSource `yaml:"datasources,omitempty"`
- Context map[string]DataSource `yaml:"context,omitempty"`
- Plugins map[string]PluginConfig `yaml:"plugins,omitempty"`
- Templates Templates `yaml:"templates,omitempty"`
-
- // Extra HTTP headers not attached to pre-defined datsources. Potentially
- // used by datasources defined in the template.
- ExtraHeaders map[string]http.Header `yaml:"-"`
-
- // internal use only, can't be injected in YAML
- PostExecInput io.Reader `yaml:"-"`
-
- Input string `yaml:"in,omitempty"`
- InputDir string `yaml:"inputDir,omitempty"`
- InputFiles []string `yaml:"inputFiles,omitempty,flow"`
- ExcludeGlob []string `yaml:"excludes,omitempty"`
- ExcludeProcessingGlob []string `yaml:"excludeProcessing,omitempty"`
-
- OutputDir string `yaml:"outputDir,omitempty"`
- OutputMap string `yaml:"outputMap,omitempty"`
- OutputFiles []string `yaml:"outputFiles,omitempty,flow"`
- OutMode string `yaml:"chmod,omitempty"`
-
- LDelim string `yaml:"leftDelim,omitempty"`
- RDelim string `yaml:"rightDelim,omitempty"`
-
- MissingKey string `yaml:"missingKey,omitempty"`
-
- PostExec []string `yaml:"postExec,omitempty,flow"`
-
- PluginTimeout time.Duration `yaml:"pluginTimeout,omitempty"`
-
- ExecPipe bool `yaml:"execPipe,omitempty"`
- Experimental bool `yaml:"experimental,omitempty"`
-}
-
-type experimentalCtxKey struct{}
-
-func SetExperimental(ctx context.Context) context.Context {
- return context.WithValue(ctx, experimentalCtxKey{}, true)
-}
-
-func ExperimentalEnabled(ctx context.Context) bool {
- v, ok := ctx.Value(experimentalCtxKey{}).(bool)
- return ok && v
-}
-
-// mergeDataSources - use d as defaults, and override with values from o
-func mergeDataSources(d, o map[string]DataSource) map[string]DataSource {
- for k, v := range o {
- c, ok := d[k]
- if ok {
- d[k] = c.mergeFrom(v)
- } else {
- d[k] = v
- }
- }
- return d
-}
-
-// DataSource - datasource configuration
-type DataSource struct {
- URL *url.URL `yaml:"-"`
- Header http.Header `yaml:"header,omitempty,flow"`
-}
-
-// UnmarshalYAML - satisfy the yaml.Umarshaler interface - URLs aren't
-// well supported, and anyway we need to do some extra parsing
-func (d *DataSource) UnmarshalYAML(value *yaml.Node) error {
- type raw struct {
- Header http.Header
- URL string
- }
- r := raw{}
- err := value.Decode(&r)
- if err != nil {
- return err
- }
- u, err := urlhelpers.ParseSourceURL(r.URL)
- if err != nil {
- return fmt.Errorf("could not parse datasource URL %q: %w", r.URL, err)
- }
- *d = DataSource{
- URL: u,
- Header: r.Header,
- }
- return nil
-}
-
-// MarshalYAML - satisfy the yaml.Marshaler interface - URLs aren't
-// well supported, and anyway we need to do some extra parsing
-func (d DataSource) MarshalYAML() (interface{}, error) {
- type raw struct {
- Header http.Header
- URL string
- }
- r := raw{
- URL: d.URL.String(),
- Header: d.Header,
- }
- return r, nil
-}
-
-// mergeFrom - use this as default, and override with values from o
-func (d DataSource) mergeFrom(o DataSource) DataSource {
- if o.URL != nil {
- d.URL = o.URL
- }
- if d.Header == nil {
- d.Header = o.Header
- } else {
- for k, v := range o.Header {
- d.Header[k] = v
- }
- }
- return d
-}
-
-type PluginConfig struct {
- Cmd string
- Args []string `yaml:"args,omitempty"`
- Timeout time.Duration `yaml:"timeout,omitempty"`
- Pipe bool `yaml:"pipe,omitempty"`
-}
-
-// UnmarshalYAML - satisfy the yaml.Umarshaler interface - plugin configs can
-// either be a plain string (to specify only the name), or a map with a name,
-// timeout, and pipe flag.
-func (p *PluginConfig) UnmarshalYAML(value *yaml.Node) error {
- if value.Kind == yaml.ScalarNode {
- s := ""
- err := value.Decode(&s)
- if err != nil {
- return err
- }
-
- *p = PluginConfig{Cmd: s}
- return nil
- }
-
- if value.Kind != yaml.MappingNode {
- return fmt.Errorf("plugin config must be a string or map")
- }
-
- type raw struct {
- Cmd string
- Args []string
- Timeout time.Duration
- Pipe bool
- }
- r := raw{}
- err := value.Decode(&r)
- if err != nil {
- return err
- }
-
- *p = PluginConfig(r)
-
- return nil
-}
-
-// MergeFrom - use this Config as the defaults, and override it with any
-// non-zero values from the other Config
-//
-// Note that Input/InputDir/InputFiles will override each other, as well as
-// OutputDir/OutputFiles.
-func (c *Config) MergeFrom(o *Config) *Config {
- switch {
- case !isZero(o.Input):
- c.Input = o.Input
- c.InputDir = ""
- c.InputFiles = nil
- c.OutputDir = ""
- case !isZero(o.InputDir):
- c.Input = ""
- c.InputDir = o.InputDir
- c.InputFiles = nil
- case !isZero(o.InputFiles):
- if !(len(o.InputFiles) == 1 && o.InputFiles[0] == "-") {
- c.Input = ""
- c.InputFiles = o.InputFiles
- c.InputDir = ""
- c.OutputDir = ""
- }
- }
-
- if !isZero(o.OutputMap) {
- c.OutputDir = ""
- c.OutputFiles = nil
- c.OutputMap = o.OutputMap
- }
- if !isZero(o.OutputDir) {
- c.OutputDir = o.OutputDir
- c.OutputFiles = nil
- c.OutputMap = ""
- }
- if !isZero(o.OutputFiles) {
- c.OutputDir = ""
- c.OutputFiles = o.OutputFiles
- c.OutputMap = ""
- }
- if !isZero(o.ExecPipe) {
- c.ExecPipe = o.ExecPipe
- c.PostExec = o.PostExec
- c.OutputFiles = o.OutputFiles
- }
- if !isZero(o.ExcludeGlob) {
- c.ExcludeGlob = o.ExcludeGlob
- }
- if !isZero(o.ExcludeProcessingGlob) {
- c.ExcludeProcessingGlob = o.ExcludeProcessingGlob
- }
- if !isZero(o.OutMode) {
- c.OutMode = o.OutMode
- }
- if !isZero(o.LDelim) {
- c.LDelim = o.LDelim
- }
- if !isZero(o.RDelim) {
- c.RDelim = o.RDelim
- }
- if c.Templates == nil {
- c.Templates = o.Templates
- } else {
- c.Templates = mergeDataSources(c.Templates, o.Templates)
- }
- if c.DataSources == nil {
- c.DataSources = o.DataSources
- } else {
- c.DataSources = mergeDataSources(c.DataSources, o.DataSources)
- }
- if c.Context == nil {
- c.Context = o.Context
- } else {
- c.Context = mergeDataSources(c.Context, o.Context)
- }
- if len(o.Plugins) > 0 {
- for k, v := range o.Plugins {
- c.Plugins[k] = v
- }
- }
-
- return c
-}
-
-// ParseDataSourceFlags - sets DataSources, Context, and Templates fields from
-// the key=value format flags as provided at the command-line
-// Unreferenced headers will be set in c.ExtraHeaders
-func (c *Config) ParseDataSourceFlags(datasources, contexts, templates, headers []string) error {
- err := c.parseResources(datasources, contexts, templates)
- if err != nil {
- return err
- }
-
- hdrs, err := parseHeaderArgs(headers)
- if err != nil {
- return err
- }
-
- for k, v := range hdrs {
- if d, ok := c.Context[k]; ok {
- d.Header = v
- c.Context[k] = d
- delete(hdrs, k)
- }
- if d, ok := c.DataSources[k]; ok {
- d.Header = v
- c.DataSources[k] = d
- delete(hdrs, k)
- }
- if t, ok := c.Templates[k]; ok {
- t.Header = v
- c.Templates[k] = t
- delete(hdrs, k)
- }
- }
- if len(hdrs) > 0 {
- c.ExtraHeaders = hdrs
- }
- return nil
-}
-
-func (c *Config) parseResources(datasources, contexts, templates []string) error {
- for _, d := range datasources {
- k, ds, err := parseDatasourceArg(d)
- if err != nil {
- return err
- }
- if c.DataSources == nil {
- c.DataSources = map[string]DataSource{}
- }
- c.DataSources[k] = ds
- }
- for _, d := range contexts {
- k, ds, err := parseDatasourceArg(d)
- if err != nil {
- return err
- }
- if c.Context == nil {
- c.Context = map[string]DataSource{}
- }
- c.Context[k] = ds
- }
- for _, t := range templates {
- k, ds, err := parseTemplateArg(t)
- if err != nil {
- return err
- }
- if c.Templates == nil {
- c.Templates = map[string]DataSource{}
- }
- c.Templates[k] = ds
- }
-
- return nil
-}
-
-// ParsePluginFlags - sets the Plugins field from the
-// key=value format flags as provided at the command-line
-func (c *Config) ParsePluginFlags(plugins []string) error {
- for _, plugin := range plugins {
- parts := strings.SplitN(plugin, "=", 2)
- if len(parts) < 2 {
- return fmt.Errorf("plugin requires both name and path")
- }
- if c.Plugins == nil {
- c.Plugins = map[string]PluginConfig{}
- }
- c.Plugins[parts[0]] = PluginConfig{Cmd: parts[1]}
- }
- return nil
-}
-
-func parseDatasourceArg(value string) (alias string, ds DataSource, err error) {
- alias, u, _ := strings.Cut(value, "=")
- if u == "" {
- u = alias
- alias, _, _ = strings.Cut(value, ".")
- if path.Base(u) != u {
- err = fmt.Errorf("invalid argument (%s): must provide an alias with files not in working directory", value)
- return alias, ds, err
- }
- }
-
- ds.URL, err = urlhelpers.ParseSourceURL(u)
-
- return alias, ds, err
-}
-
-func parseHeaderArgs(headerArgs []string) (map[string]http.Header, error) {
- headers := make(map[string]http.Header)
- for _, v := range headerArgs {
- ds, name, value, err := splitHeaderArg(v)
- if err != nil {
- return nil, err
- }
- if _, ok := headers[ds]; !ok {
- headers[ds] = make(http.Header)
- }
- headers[ds][name] = append(headers[ds][name], strings.TrimSpace(value))
- }
- return headers, nil
-}
-
-func splitHeaderArg(arg string) (datasourceAlias, name, value string, err error) {
- parts := strings.SplitN(arg, "=", 2)
- if len(parts) != 2 {
- err = fmt.Errorf("invalid datasource-header option '%s'", arg)
- return "", "", "", err
- }
- datasourceAlias = parts[0]
- name, value, err = splitHeader(parts[1])
- return datasourceAlias, name, value, err
-}
-
-func splitHeader(header string) (name, value string, err error) {
- parts := strings.SplitN(header, ":", 2)
- if len(parts) != 2 {
- err = fmt.Errorf("invalid HTTP Header format '%s'", header)
- return "", "", err
- }
- name = http.CanonicalHeaderKey(parts[0])
- value = parts[1]
- return name, value, nil
-}
-
-// Validate the Config
-func (c Config) Validate() (err error) {
- err = notTogether(
- []string{"in", "inputFiles", "inputDir"},
- c.Input, c.InputFiles, c.InputDir)
- if err == nil {
- err = notTogether(
- []string{"outputFiles", "outputDir", "outputMap"},
- c.OutputFiles, c.OutputDir, c.OutputMap)
- }
- if err == nil {
- err = notTogether(
- []string{"outputDir", "outputMap", "execPipe"},
- c.OutputDir, c.OutputMap, c.ExecPipe)
- }
-
- if err == nil {
- err = mustTogether("outputDir", "inputDir",
- c.OutputDir, c.InputDir)
- }
-
- if err == nil {
- err = mustTogether("outputMap", "inputDir",
- c.OutputMap, c.InputDir)
- }
-
- if err == nil {
- f := len(c.InputFiles)
- if f == 0 && c.Input != "" {
- f = 1
- }
- o := len(c.OutputFiles)
- if f != o && !c.ExecPipe {
- err = fmt.Errorf("must provide same number of 'outputFiles' (%d) as 'in' or 'inputFiles' (%d) options", o, f)
- }
- }
-
- if err == nil {
- if c.ExecPipe && len(c.PostExec) == 0 {
- err = fmt.Errorf("execPipe may only be used with a postExec command")
- }
- }
-
- if err == nil {
- if c.ExecPipe && (len(c.OutputFiles) > 0 && c.OutputFiles[0] != "-") {
- err = fmt.Errorf("must not set 'outputFiles' when using 'execPipe'")
- }
- }
-
- if err == nil {
- missingKeyValues := []string{"", "error", "zero", "default", "invalid"}
- if !slices.Contains(missingKeyValues, c.MissingKey) {
- err = fmt.Errorf("not allowed value for the 'missing-key' flag: %s. Allowed values: %s", c.MissingKey, strings.Join(missingKeyValues, ","))
- }
- }
-
- return err
-}
-
-func notTogether(names []string, values ...interface{}) error {
- found := ""
- for i, value := range values {
- if isZero(value) {
- continue
- }
- if found != "" {
- return fmt.Errorf("only one of these options is supported at a time: '%s', '%s'",
- found, names[i])
- }
- found = names[i]
- }
- return nil
-}
-
-func mustTogether(left, right string, lValue, rValue interface{}) error {
- if !isZero(lValue) && isZero(rValue) {
- return fmt.Errorf("these options must be set together: '%s', '%s'",
- left, right)
- }
-
- return nil
-}
-
-func isZero(value interface{}) bool {
- switch v := value.(type) {
- case string:
- return v == ""
- case []string:
- return len(v) == 0
- case bool:
- return !v
- default:
- return false
- }
-}
-
-// ApplyDefaults - any defaults changed here should be added to cmd.InitFlags as
-// well for proper help/usage display.
-func (c *Config) ApplyDefaults() {
- if c.Stdout == nil {
- c.Stdout = os.Stdout
- }
- if c.Stderr == nil {
- c.Stderr = os.Stderr
- }
- if c.Stdin == nil {
- c.Stdin = os.Stdin
- }
-
- if c.InputDir != "" && c.OutputDir == "" && c.OutputMap == "" {
- c.OutputDir = "."
- }
- if c.Input == "" && c.InputDir == "" && len(c.InputFiles) == 0 {
- c.InputFiles = []string{"-"}
- }
- if c.OutputDir == "" && c.OutputMap == "" && len(c.OutputFiles) == 0 && !c.ExecPipe {
- c.OutputFiles = []string{"-"}
- }
- if c.LDelim == "" {
- c.LDelim = "{{"
- }
- if c.RDelim == "" {
- c.RDelim = "}}"
- }
- if c.MissingKey == "" {
- c.MissingKey = "error"
- }
-
- if c.ExecPipe {
- pipe := &bytes.Buffer{}
- c.PostExecInput = pipe
- c.OutputFiles = []string{"-"}
-
- // --exec-pipe redirects standard out to the out pipe
- c.Stdout = pipe
- } else {
- c.PostExecInput = c.Stdin
- }
-
- if c.PluginTimeout == 0 {
- c.PluginTimeout = 5 * time.Second
- }
-}
-
-// GetMode - parse an os.FileMode out of the string, and let us know if it's an override or not...
-func (c *Config) GetMode() (os.FileMode, bool, error) {
- modeOverride := c.OutMode != ""
- m, err := strconv.ParseUint("0"+c.OutMode, 8, 32)
- if err != nil {
- return 0, false, err
- }
- mode := iohelpers.NormalizeFileMode(os.FileMode(m))
- if mode == 0 && c.Input != "" {
- mode = iohelpers.NormalizeFileMode(0o644)
- }
- return mode, modeOverride, nil
-}
-
-// String -
-func (c *Config) String() string {
- out := &strings.Builder{}
- out.WriteString("---\n")
- enc := yaml.NewEncoder(out)
- enc.SetIndent(2)
-
- // dereferenced copy so we can truncate input for display
- c2 := *c
- if len(c2.Input) >= 11 {
- c2.Input = c2.Input[0:8] + "..."
- }
-
- err := enc.Encode(c2)
- if err != nil {
- return err.Error()
- }
- return out.String()
-}
diff --git a/internal/config/configfile_test.go b/internal/config/configfile_test.go
deleted file mode 100644
index 3cd1b109..00000000
--- a/internal/config/configfile_test.go
+++ /dev/null
@@ -1,873 +0,0 @@
-package config
-
-import (
- "net/http"
- "net/url"
- "runtime"
- "strings"
- "testing"
- "time"
-
- "github.com/hairyhenderson/gomplate/v4/internal/iohelpers"
- "github.com/hairyhenderson/yaml"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestParseConfigFile(t *testing.T) {
- t.Parallel()
- in := "in: hello world\n"
- expected := &Config{
- Input: "hello world",
- }
- cf, err := Parse(strings.NewReader(in))
- require.NoError(t, err)
- assert.Equal(t, expected, cf)
-
- in = `in: hello world
-outputFiles: [out.txt]
-chmod: 644
-
-datasources:
- data:
- url: file:///data.json
- moredata:
- url: https://example.com/more.json
- header:
- Authorization: ["Bearer abcd1234"]
-
-context:
- .:
- url: file:///data.json
-
-plugins:
- foo:
- cmd: echo
- pipe: true
-
-templates:
- foo:
- url: file:///tmp/foo.t
-
-pluginTimeout: 2s
-`
- expected = &Config{
- Input: "hello world",
- OutputFiles: []string{"out.txt"},
- DataSources: map[string]DataSource{
- "data": {
- URL: mustURL("file:///data.json"),
- },
- "moredata": {
- URL: mustURL("https://example.com/more.json"),
- Header: map[string][]string{
- "Authorization": {"Bearer abcd1234"},
- },
- },
- },
- Context: map[string]DataSource{
- ".": {
- URL: mustURL("file:///data.json"),
- },
- },
- OutMode: "644",
- Plugins: map[string]PluginConfig{
- "foo": {Cmd: "echo", Pipe: true},
- },
- Templates: Templates{"foo": DataSource{URL: mustURL("file:///tmp/foo.t")}},
- PluginTimeout: 2 * time.Second,
- }
-
- cf, err = Parse(strings.NewReader(in))
- require.NoError(t, err)
- assert.EqualValues(t, expected, cf)
-}
-
-func mustURL(s string) *url.URL {
- u, err := url.Parse(s)
- if err != nil {
- panic(err)
- }
-
- return u
-}
-
-func TestValidate(t *testing.T) {
- t.Parallel()
- require.NoError(t, validateConfig(""))
-
- require.Error(t, validateConfig(`in: foo
-inputFiles: [bar]
-`))
- require.Error(t, validateConfig(`inputDir: foo
-inputFiles: [bar]
-`))
- require.Error(t, validateConfig(`inputDir: foo
-in: bar
-`))
-
- require.Error(t, validateConfig(`outputDir: foo
-outputFiles: [bar]
-`))
-
- require.Error(t, validateConfig(`in: foo
-outputFiles: [bar, baz]
-`))
-
- require.Error(t, validateConfig(`inputFiles: [foo]
-outputFiles: [bar, baz]
-`))
-
- require.Error(t, validateConfig(`outputDir: foo
-outputFiles: [bar]
-`))
-
- require.Error(t, validateConfig(`outputDir: foo
-`))
-
- require.Error(t, validateConfig(`outputMap: foo
-`))
-
- require.Error(t, validateConfig(`outputMap: foo
-outputFiles: [bar]
-`))
-
- require.Error(t, validateConfig(`inputDir: foo
-outputDir: bar
-outputMap: bar
-`))
-
- require.Error(t, validateConfig(`execPipe: true
-`))
- require.Error(t, validateConfig(`execPipe: true
-postExec: ""
-`))
-
- require.NoError(t, validateConfig(`execPipe: true
-postExec: [echo, foo]
-`))
-
- require.Error(t, validateConfig(`execPipe: true
-outputFiles: [foo]
-postExec: [echo]
-`))
-
- require.NoError(t, validateConfig(`execPipe: true
-inputFiles: ['-']
-postExec: [echo]
-`))
-
- require.Error(t, validateConfig(`inputDir: foo
-execPipe: true
-outputDir: foo
-postExec: [echo]
-`))
-
- require.Error(t, validateConfig(`inputDir: foo
-execPipe: true
-outputMap: foo
-postExec: [echo]
-`))
-}
-
-func validateConfig(c string) error {
- in := strings.NewReader(c)
- cfg, err := Parse(in)
- if err != nil {
- return err
- }
- err = cfg.Validate()
- return err
-}
-
-func TestMergeFrom(t *testing.T) {
- t.Parallel()
- cfg := &Config{
- Input: "hello world",
- DataSources: map[string]DataSource{
- "data": {
- URL: mustURL("file:///data.json"),
- },
- "moredata": {
- URL: mustURL("https://example.com/more.json"),
- Header: http.Header{
- "Authorization": {"Bearer abcd1234"},
- },
- },
- },
- Context: map[string]DataSource{
- "foo": {
- URL: mustURL("https://example.com/foo.yaml"),
- Header: http.Header{
- "Accept": {"application/yaml"},
- },
- },
- },
- OutMode: "644",
- }
- other := &Config{
- OutputFiles: []string{"out.txt"},
- DataSources: map[string]DataSource{
- "data": {
- Header: http.Header{
- "Accept": {"foo/bar"},
- },
- },
- },
- Context: map[string]DataSource{
- "foo": {
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- "bar": {URL: mustURL("stdin:///")},
- },
- }
-
- expected := &Config{
- Input: "hello world",
- OutputFiles: []string{"out.txt"},
- DataSources: map[string]DataSource{
- "data": {
- URL: mustURL("file:///data.json"),
- Header: http.Header{
- "Accept": {"foo/bar"},
- },
- },
- "moredata": {
- URL: mustURL("https://example.com/more.json"),
- Header: http.Header{
- "Authorization": {"Bearer abcd1234"},
- },
- },
- },
- Context: map[string]DataSource{
- "foo": {
- URL: mustURL("https://example.com/foo.yaml"),
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- "bar": {URL: mustURL("stdin:///")},
- },
- OutMode: "644",
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- cfg = &Config{
- Input: "hello world",
- }
- other = &Config{
- InputFiles: []string{"in.tmpl", "in2.tmpl"},
- OutputFiles: []string{"out", "out2"},
- }
- expected = &Config{
- InputFiles: []string{"in.tmpl", "in2.tmpl"},
- OutputFiles: []string{"out", "out2"},
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- cfg = &Config{
- Input: "hello world",
- OutputFiles: []string{"out", "out2"},
- }
- other = &Config{
- InputDir: "in/",
- OutputDir: "out/",
- }
- expected = &Config{
- InputDir: "in/",
- OutputDir: "out/",
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- cfg = &Config{
- Input: "hello world",
- OutputFiles: []string{"out"},
- }
- other = &Config{
- Input: "hi",
- ExecPipe: true,
- PostExec: []string{"cat"},
- }
- expected = &Config{
- Input: "hi",
- ExecPipe: true,
- PostExec: []string{"cat"},
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- cfg = &Config{
- Input: "hello world",
- OutputFiles: []string{"-"},
- Plugins: map[string]PluginConfig{
- "sleep": {Cmd: "echo"},
- },
- PluginTimeout: 500 * time.Microsecond,
- }
- other = &Config{
- InputFiles: []string{"-"},
- OutputFiles: []string{"-"},
- Plugins: map[string]PluginConfig{
- "sleep": {Cmd: "sleep.sh"},
- },
- }
- expected = &Config{
- Input: "hello world",
- OutputFiles: []string{"-"},
- Plugins: map[string]PluginConfig{
- "sleep": {Cmd: "sleep.sh"},
- },
- PluginTimeout: 500 * time.Microsecond,
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- cfg = &Config{
- Input: "hello world",
- OutMode: "644",
- }
- other = &Config{
- OutputFiles: []string{"out.txt"},
- Context: map[string]DataSource{
- "foo": {
- URL: mustURL("https://example.com/foo.yaml"),
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- "bar": {URL: mustURL("stdin:///")},
- },
- DataSources: map[string]DataSource{
- "data": {
- URL: mustURL("file:///data.json"),
- },
- "moredata": {
- URL: mustURL("https://example.com/more.json"),
- Header: http.Header{
- "Authorization": {"Bearer abcd1234"},
- },
- },
- },
- }
- expected = &Config{
- Input: "hello world",
- OutputFiles: []string{"out.txt"},
- Context: map[string]DataSource{
- "foo": {
- URL: mustURL("https://example.com/foo.yaml"),
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- "bar": {URL: mustURL("stdin:///")},
- },
- DataSources: map[string]DataSource{
- "data": {
- URL: mustURL("file:///data.json"),
- },
- "moredata": {
- URL: mustURL("https://example.com/more.json"),
- Header: http.Header{
- "Authorization": {"Bearer abcd1234"},
- },
- },
- },
- OutMode: "644",
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-
- // test template merging & a few other things
- cfg = &Config{
- InputDir: "indir/",
- ExcludeGlob: []string{"*.txt"},
- Templates: Templates{
- "foo": {
- URL: mustURL("file:///foo.yaml"),
- },
- "bar": {
- URL: mustURL("stdin:///"),
- Header: http.Header{"Accept": {"application/json"}},
- },
- },
- }
- other = &Config{
- ExcludeGlob: []string{"*.yaml"},
- OutputMap: "${ .in }.out",
- OutMode: "600",
- LDelim: "${",
- RDelim: "}",
- Templates: Templates{
- "foo": {URL: mustURL("https://example.com/foo.yaml")},
- "baz": {URL: mustURL("vault:///baz")},
- },
- }
- expected = &Config{
- InputDir: "indir/",
- ExcludeGlob: []string{"*.yaml"},
- OutputMap: "${ .in }.out",
- OutMode: "600",
- LDelim: "${",
- RDelim: "}",
- Templates: Templates{
- "foo": {URL: mustURL("https://example.com/foo.yaml")},
- "bar": {
- URL: mustURL("stdin:///"),
- Header: http.Header{"Accept": {"application/json"}},
- },
- "baz": {URL: mustURL("vault:///baz")},
- },
- }
-
- assert.EqualValues(t, expected, cfg.MergeFrom(other))
-}
-
-func TestParseDataSourceFlags(t *testing.T) {
- t.Parallel()
- cfg := &Config{}
- err := cfg.ParseDataSourceFlags(nil, nil, nil, nil)
- require.NoError(t, err)
- assert.EqualValues(t, &Config{}, cfg)
-
- cfg = &Config{}
- err = cfg.ParseDataSourceFlags([]string{"foo/bar/baz.json"}, nil, nil, nil)
- require.Error(t, err)
-
- cfg = &Config{}
- err = cfg.ParseDataSourceFlags([]string{"baz=foo/bar/baz.json"}, nil, nil, nil)
- require.NoError(t, err)
- expected := &Config{
- DataSources: map[string]DataSource{
- "baz": {URL: mustURL("foo/bar/baz.json")},
- },
- }
- assert.EqualValues(t, expected, cfg, "expected: %+v\nactual: %+v\n", expected, cfg)
-
- cfg = &Config{}
- err = cfg.ParseDataSourceFlags(
- []string{"baz=foo/bar/baz.json"},
- nil,
- nil,
- []string{"baz=Accept: application/json"})
- require.NoError(t, err)
- assert.EqualValues(t, &Config{
- DataSources: map[string]DataSource{
- "baz": {
- URL: mustURL("foo/bar/baz.json"),
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- },
- }, cfg)
-
- cfg = &Config{}
- err = cfg.ParseDataSourceFlags(
- []string{"baz=foo/bar/baz.json"},
- []string{"foo=http://example.com"},
- nil,
- []string{
- "foo=Accept: application/json",
- "bar=Authorization: Basic xxxxx",
- },
- )
- require.NoError(t, err)
- assert.EqualValues(t, &Config{
- DataSources: map[string]DataSource{
- "baz": {URL: mustURL("foo/bar/baz.json")},
- },
- Context: map[string]DataSource{
- "foo": {
- URL: mustURL("http://example.com"),
- Header: http.Header{
- "Accept": {"application/json"},
- },
- },
- },
- ExtraHeaders: map[string]http.Header{
- "bar": {"Authorization": {"Basic xxxxx"}},
- },
- }, cfg)
-
- cfg = &Config{}
- err = cfg.ParseDataSourceFlags(
- nil,
- nil,
- []string{"foo=http://example.com", "file.tmpl", "tmpldir/"},
- []string{"foo=Accept: application/json", "bar=Authorization: Basic xxxxx"},
- )
- require.NoError(t, err)
- assert.EqualValues(t, &Config{
- Templates: Templates{
- "foo": {
- URL: mustURL("http://example.com"),
- Header: http.Header{"Accept": {"application/json"}},
- },
- "file.tmpl": {URL: mustURL("file.tmpl")},
- "tmpldir/": {URL: mustURL("tmpldir/")},
- },
- ExtraHeaders: map[string]http.Header{
- "bar": {"Authorization": {"Basic xxxxx"}},
- },
- }, cfg)
-}
-
-func TestParsePluginFlags(t *testing.T) {
- t.Parallel()
- cfg := &Config{}
- err := cfg.ParsePluginFlags(nil)
- require.NoError(t, err)
-
- cfg = &Config{}
- err = cfg.ParsePluginFlags([]string{"foo=bar"})
- require.NoError(t, err)
- assert.EqualValues(t, &Config{Plugins: map[string]PluginConfig{"foo": {Cmd: "bar"}}}, cfg)
-}
-
-func TestConfig_String(t *testing.T) {
- t.Run("defaults", func(t *testing.T) {
- c := &Config{}
- c.ApplyDefaults()
-
- expected := `---
-inputFiles: ['-']
-outputFiles: ['-']
-leftDelim: '{{'
-rightDelim: '}}'
-missingKey: error
-pluginTimeout: 5s
-`
- assert.Equal(t, expected, c.String())
- })
-
- t.Run("overridden values", func(t *testing.T) {
- c := &Config{
- LDelim: "L",
- RDelim: "R",
- Input: "foo",
- OutputFiles: []string{"-"},
- Templates: Templates{
- "foo": {URL: mustURL("https://www.example.com/foo.tmpl")},
- "bar": {URL: mustURL("file:///tmp/bar.t")},
- },
- }
- expected := `---
-in: foo
-outputFiles: ['-']
-leftDelim: L
-rightDelim: R
-templates:
- foo:
- url: https://www.example.com/foo.tmpl
- bar:
- url: file:///tmp/bar.t
-`
- assert.YAMLEq(t, expected, c.String())
- })
-
- t.Run("long input", func(t *testing.T) {
- c := &Config{
- LDelim: "L",
- RDelim: "R",
- Input: "long input that should be truncated",
- OutputFiles: []string{"-"},
- Templates: Templates{
- "foo": {URL: mustURL("https://www.example.com/foo.tmpl")},
- "bar": {URL: mustURL("file:///tmp/bar.t")},
- },
- }
- expected := `---
-in: long inp...
-outputFiles: ['-']
-leftDelim: L
-rightDelim: R
-templates:
- foo:
- url: https://www.example.com/foo.tmpl
- bar:
- url: file:///tmp/bar.t
-`
- assert.YAMLEq(t, expected, c.String())
- })
-
- t.Run("relative dirs", func(t *testing.T) {
- c := &Config{
- InputDir: "in/",
- OutputDir: "out/",
- }
- expected := `---
-inputDir: in/
-outputDir: out/
-`
- assert.YAMLEq(t, expected, c.String())
- })
-
- t.Run("outputmap", func(t *testing.T) {
- c := &Config{
- InputDir: "in/",
- OutputMap: "{{ .in }}",
- }
- expected := `---
-inputDir: in/
-outputMap: '{{ .in }}'
-`
-
- assert.YAMLEq(t, expected, c.String())
- })
-
- t.Run("pluginTimeout", func(t *testing.T) {
- c := &Config{
- PluginTimeout: 500 * time.Millisecond,
- }
- expected := `---
-pluginTimeout: 500ms
-`
-
- assert.YAMLEq(t, expected, c.String())
- })
-
- t.Run("plugins", func(t *testing.T) {
- c := &Config{
- Plugins: map[string]PluginConfig{
- "foo": {
- Cmd: "bar",
- Timeout: 1 * time.Second,
- Pipe: true,
- },
- },
- }
- expected := `---
-plugins:
- foo:
- cmd: bar
- timeout: 1s
- pipe: true
-`
-
- assert.YAMLEq(t, expected, c.String())
- })
-}
-
-func TestApplyDefaults(t *testing.T) {
- t.Parallel()
- cfg := &Config{}
-
- cfg.ApplyDefaults()
- assert.EqualValues(t, []string{"-"}, cfg.InputFiles)
- assert.EqualValues(t, []string{"-"}, cfg.OutputFiles)
- assert.Empty(t, cfg.OutputDir)
- assert.Equal(t, "{{", cfg.LDelim)
- assert.Equal(t, "}}", cfg.RDelim)
-
- cfg = &Config{
- InputDir: "in",
- }
-
- cfg.ApplyDefaults()
- assert.Empty(t, cfg.InputFiles)
- assert.Empty(t, cfg.OutputFiles)
- assert.Equal(t, ".", cfg.OutputDir)
- assert.Equal(t, "{{", cfg.LDelim)
- assert.Equal(t, "}}", cfg.RDelim)
-
- cfg = &Config{
- Input: "foo",
- LDelim: "<",
- RDelim: ">",
- }
-
- cfg.ApplyDefaults()
- assert.Empty(t, cfg.InputFiles)
- assert.EqualValues(t, []string{"-"}, cfg.OutputFiles)
- assert.Empty(t, cfg.OutputDir)
- assert.Equal(t, "<", cfg.LDelim)
- assert.Equal(t, ">", cfg.RDelim)
-
- cfg = &Config{
- Input: "foo",
- ExecPipe: true,
- }
-
- cfg.ApplyDefaults()
- assert.Empty(t, cfg.InputFiles)
- assert.EqualValues(t, []string{"-"}, cfg.OutputFiles)
- assert.Empty(t, cfg.OutputDir)
- assert.True(t, cfg.ExecPipe)
-
- cfg = &Config{
- InputDir: "foo",
- OutputMap: "bar",
- }
-
- cfg.ApplyDefaults()
- assert.Empty(t, cfg.InputFiles)
- assert.Empty(t, cfg.Input)
- assert.Empty(t, cfg.OutputFiles)
- assert.Empty(t, cfg.OutputDir)
- assert.False(t, cfg.ExecPipe)
- assert.Equal(t, "bar", cfg.OutputMap)
-}
-
-func TestGetMode(t *testing.T) {
- c := &Config{}
- m, o, err := c.GetMode()
- require.NoError(t, err)
- assert.Equal(t, iohelpers.NormalizeFileMode(0), m)
- assert.False(t, o)
-
- c = &Config{OutMode: "755"}
- m, o, err = c.GetMode()
- require.NoError(t, err)
- assert.Equal(t, iohelpers.NormalizeFileMode(0o755), m)
- assert.True(t, o)
-
- c = &Config{OutMode: "0755"}
- m, o, err = c.GetMode()
- require.NoError(t, err)
- assert.Equal(t, iohelpers.NormalizeFileMode(0o755), m)
- assert.True(t, o)
-
- c = &Config{OutMode: "foo"}
- _, _, err = c.GetMode()
- require.Error(t, err)
-}
-
-func TestParseHeaderArgs(t *testing.T) {
- args := []string{
- "foo=Accept: application/json",
- "bar=Authorization: Bearer supersecret",
- }
- expected := map[string]http.Header{
- "foo": {
- "Accept": {"application/json"},
- },
- "bar": {
- "Authorization": {"Bearer supersecret"},
- },
- }
- parsed, err := parseHeaderArgs(args)
- require.NoError(t, err)
- assert.Equal(t, expected, parsed)
-
- _, err = parseHeaderArgs([]string{"foo"})
- require.Error(t, err)
-
- _, err = parseHeaderArgs([]string{"foo=bar"})
- require.Error(t, err)
-
- args = []string{
- "foo=Accept: application/json",
- "foo=Foo: bar",
- "foo=foo: baz",
- "foo=fOO: qux",
- "bar=Authorization: Bearer supersecret",
- }
- expected = map[string]http.Header{
- "foo": {
- "Accept": {"application/json"},
- "Foo": {"bar", "baz", "qux"},
- },
- "bar": {
- "Authorization": {"Bearer supersecret"},
- },
- }
- parsed, err = parseHeaderArgs(args)
- require.NoError(t, err)
- assert.Equal(t, expected, parsed)
-}
-
-func TestParseDatasourceArgNoAlias(t *testing.T) {
- alias, ds, err := parseDatasourceArg("foo.json")
- require.NoError(t, err)
- assert.Equal(t, "foo", alias)
- assert.Empty(t, ds.URL.Scheme)
-
- _, _, err = parseDatasourceArg("../foo.json")
- require.Error(t, err)
-
- _, _, err = parseDatasourceArg("ftp://example.com/foo.yml")
- require.Error(t, err)
-}
-
-func TestParseDatasourceArgWithAlias(t *testing.T) {
- alias, ds, err := parseDatasourceArg("data=foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Path: "foo.json"}, ds.URL)
-
- alias, ds, err = parseDatasourceArg("data=/otherdir/foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.Equal(t, "file", ds.URL.Scheme)
- assert.True(t, ds.URL.IsAbs())
- assert.Equal(t, "/otherdir/foo.json", ds.URL.Path)
-
- if runtime.GOOS == "windows" {
- alias, ds, err = parseDatasourceArg("data=foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Path: "foo.json"}, ds.URL)
-
- alias, ds, err = parseDatasourceArg(`data=\otherdir\foo.json`)
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Scheme: "file", Path: "/otherdir/foo.json"}, ds.URL)
-
- alias, ds, err = parseDatasourceArg("data=C:\\windowsdir\\foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Scheme: "file", Path: "C:/windowsdir/foo.json"}, ds.URL)
-
- alias, ds, err = parseDatasourceArg("data=\\\\somehost\\share\\foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Scheme: "file", Host: "somehost", Path: "/share/foo.json"}, ds.URL)
- }
-
- alias, ds, err = parseDatasourceArg("data=sftp://example.com/blahblah/foo.json")
- require.NoError(t, err)
- assert.Equal(t, "data", alias)
- assert.EqualValues(t, &url.URL{Scheme: "sftp", Host: "example.com", Path: "/blahblah/foo.json"}, ds.URL)
-
- alias, ds, err = parseDatasourceArg("merged=merge:./foo.yaml|http://example.com/bar.json%3Ffoo=bar")
- require.NoError(t, err)
- assert.Equal(t, "merged", alias)
- assert.EqualValues(t, &url.URL{Scheme: "merge", Opaque: "./foo.yaml|http://example.com/bar.json%3Ffoo=bar"}, ds.URL)
-}
-
-func TestPluginConfig_UnmarshalYAML(t *testing.T) {
- in := `foo`
- out := PluginConfig{}
- err := yaml.Unmarshal([]byte(in), &out)
- require.NoError(t, err)
- assert.EqualValues(t, PluginConfig{Cmd: "foo"}, out)
-
- in = `[foo, bar]`
- out = PluginConfig{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.Error(t, err)
-
- in = `cmd: foo`
- out = PluginConfig{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.NoError(t, err)
- assert.EqualValues(t, PluginConfig{Cmd: "foo"}, out)
-
- in = `cmd: foo
-timeout: 10ms
-pipe: true
-`
- out = PluginConfig{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.NoError(t, err)
- assert.EqualValues(t, PluginConfig{
- Cmd: "foo",
- Timeout: time.Duration(10) * time.Millisecond,
- Pipe: true,
- }, out)
-}
diff --git a/internal/config/types.go b/internal/config/types.go
index 6c38dc6a..022363f7 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "net/url"
"strings"
"github.com/hairyhenderson/gomplate/v4/internal/deprecated"
@@ -88,13 +89,58 @@ func (t Templates) MarshalYAML() (interface{}, error) {
return m, nil
}
-func parseTemplateArg(value string) (alias string, ds DataSource, err error) {
- alias, u, _ := strings.Cut(value, "=")
- if u == "" {
- u = alias
- }
+type experimentalCtxKey struct{}
+
+func SetExperimental(ctx context.Context) context.Context {
+ return context.WithValue(ctx, experimentalCtxKey{}, true)
+}
+
+func ExperimentalEnabled(ctx context.Context) bool {
+ v, ok := ctx.Value(experimentalCtxKey{}).(bool)
+ return ok && v
+}
- ds.URL, err = urlhelpers.ParseSourceURL(u)
+// DataSource - datasource configuration
+//
+// defined in this package to avoid cyclic dependencies
+type DataSource struct {
+ URL *url.URL `yaml:"-"`
+ Header http.Header `yaml:"header,omitempty,flow"`
+}
+
+// UnmarshalYAML - satisfy the yaml.Umarshaler interface - URLs aren't
+// well supported, and anyway we need to do some extra parsing
+func (d *DataSource) UnmarshalYAML(value *yaml.Node) error {
+ type raw struct {
+ Header http.Header
+ URL string
+ }
+ r := raw{}
+ err := value.Decode(&r)
+ if err != nil {
+ return err
+ }
+ u, err := urlhelpers.ParseSourceURL(r.URL)
+ if err != nil {
+ return fmt.Errorf("could not parse datasource URL %q: %w", r.URL, err)
+ }
+ *d = DataSource{
+ URL: u,
+ Header: r.Header,
+ }
+ return nil
+}
- return alias, ds, err
+// MarshalYAML - satisfy the yaml.Marshaler interface - URLs aren't
+// well supported, and anyway we need to do some extra parsing
+func (d DataSource) MarshalYAML() (interface{}, error) {
+ type raw struct {
+ Header http.Header
+ URL string
+ }
+ r := raw{
+ URL: d.URL.String(),
+ Header: d.Header,
+ }
+ return r, nil
}
diff --git a/internal/config/types_test.go b/internal/config/types_test.go
deleted file mode 100644
index 1ee6c4fe..00000000
--- a/internal/config/types_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package config
-
-import (
- "net/http"
- "testing"
-
- "github.com/hairyhenderson/yaml"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestTemplates_UnmarshalYAML(t *testing.T) {
- in := `t:
- url: foo/bar/helloworld.tmpl
-templatedir:
- url: templatedir/
-dir:
- url: foo/bar/
-mytemplate.t:
- url: mytemplate.t
-remote:
- url: https://example.com/foo/bar/helloworld.tmpl
- header:
- Accept: [text/plain, text/template]`
- out := Templates{}
- err := yaml.Unmarshal([]byte(in), &out)
- require.NoError(t, err)
- assert.EqualValues(t, Templates{
- "t": {URL: mustURL("foo/bar/helloworld.tmpl")},
- "templatedir": {URL: mustURL("templatedir/")},
- "dir": {URL: mustURL("foo/bar/")},
- "mytemplate.t": {URL: mustURL("mytemplate.t")},
- "remote": {
- URL: mustURL("https://example.com/foo/bar/helloworld.tmpl"),
- Header: http.Header{"Accept": {"text/plain", "text/template"}},
- },
- }, out)
-
- // legacy array format
- in = `- t=foo/bar/helloworld.tmpl
-- templatedir/
-- dir=foo/bar/
-- mytemplate.t
-- remote=https://example.com/foo/bar/helloworld.tmpl`
- out = Templates{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.NoError(t, err)
- assert.EqualValues(t, Templates{
- "t": {URL: mustURL("foo/bar/helloworld.tmpl")},
- "templatedir/": {URL: mustURL("templatedir/")},
- "dir": {URL: mustURL("foo/bar/")},
- "mytemplate.t": {URL: mustURL("mytemplate.t")},
- "remote": {URL: mustURL("https://example.com/foo/bar/helloworld.tmpl")},
- }, out)
-
- // invalid format
- in = `"neither an array nor a map"`
- out = Templates{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.Error(t, err)
-
- // invalid URL
- in = `- t="not a:valid url"`
- out = Templates{}
- err = yaml.Unmarshal([]byte(in), &out)
- require.Error(t, err)
-}
-
-func TestParseTemplateArg(t *testing.T) {
- data := []struct {
- ds DataSource
- in string
- alias string
- }{
- {in: "t=foo/bar/helloworld.tmpl", alias: "t", ds: DataSource{URL: mustURL("foo/bar/helloworld.tmpl")}},
- {in: "templatedir/", alias: "templatedir/", ds: DataSource{URL: mustURL("templatedir/")}},
- {in: "dir=foo/bar/", alias: "dir", ds: DataSource{URL: mustURL("foo/bar/")}},
- {in: "mytemplate.t", alias: "mytemplate.t", ds: DataSource{URL: mustURL("mytemplate.t")}},
- {
- in: "remote=https://example.com/foo/bar/helloworld.tmpl",
- alias: "remote", ds: DataSource{URL: mustURL("https://example.com/foo/bar/helloworld.tmpl")},
- },
- }
-
- for _, d := range data {
- alias, ds, err := parseTemplateArg(d.in)
- require.NoError(t, err)
- assert.Equal(t, d.alias, alias)
- assert.EqualValues(t, d.ds, ds)
- }
-}
diff --git a/internal/datafs/context.go b/internal/datafs/context.go
index 30722f2b..310d4445 100644
--- a/internal/datafs/context.go
+++ b/internal/datafs/context.go
@@ -30,10 +30,15 @@ func WithDataSourceRegistryFS(registry Registry, fsys fs.FS) fs.FS {
type stdinCtxKey struct{}
+// ContextWithStdin injects an [io.Reader] into the context, which can be used
+// to override the default stdin.
func ContextWithStdin(ctx context.Context, r io.Reader) context.Context {
return context.WithValue(ctx, stdinCtxKey{}, r)
}
+// StdinFromContext returns the io.Reader that should be used for stdin as
+// injected by [ContextWithStdin]. If no reader has been injected, [os.Stdin] is
+// returned.
func StdinFromContext(ctx context.Context) io.Reader {
if r, ok := ctx.Value(stdinCtxKey{}).(io.Reader); ok {
return r