summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2022-02-21 22:41:32 -0500
committerDave Henderson <dhenderson@gmail.com>2022-02-27 20:16:18 -0500
commit610a8b5a408cb3d08f4e4e2d6cf9cbe7194490d5 (patch)
tree5a79319666ca7d71ee6744fcfd59dec07b0c3347 /internal
parent2aa13dd3cf08ee24eb174edb78ee4d13415005b5 (diff)
Support piping input to plugin
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/config/configfile.go51
-rw-r--r--internal/config/configfile_test.go75
-rw-r--r--internal/tests/integration/config_test.go27
-rw-r--r--internal/tests/integration/plugins_test.go38
4 files changed, 166 insertions, 25 deletions
diff --git a/internal/config/configfile.go b/internal/config/configfile.go
index 3858f85e..459f9f9e 100644
--- a/internal/config/configfile.go
+++ b/internal/config/configfile.go
@@ -54,9 +54,9 @@ type Config struct {
PostExec []string `yaml:"postExec,omitempty,flow"`
- DataSources map[string]DataSource `yaml:"datasources,omitempty"`
- Context map[string]DataSource `yaml:"context,omitempty"`
- Plugins map[string]string `yaml:"plugins,omitempty"`
+ DataSources map[string]DataSource `yaml:"datasources,omitempty"`
+ Context map[string]DataSource `yaml:"context,omitempty"`
+ Plugins map[string]PluginConfig `yaml:"plugins,omitempty"`
// Extra HTTP headers not attached to pre-defined datsources. Potentially
// used by datasources defined in the template.
@@ -163,6 +163,47 @@ func (d DataSource) mergeFrom(o DataSource) DataSource {
return d
}
+type PluginConfig struct {
+ Cmd string
+ Timeout time.Duration
+ Pipe bool
+}
+
+// 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
+ 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
//
@@ -290,9 +331,9 @@ func (c *Config) ParsePluginFlags(plugins []string) error {
return fmt.Errorf("plugin requires both name and path")
}
if c.Plugins == nil {
- c.Plugins = map[string]string{}
+ c.Plugins = map[string]PluginConfig{}
}
- c.Plugins[parts[0]] = parts[1]
+ c.Plugins[parts[0]] = PluginConfig{Cmd: parts[1]}
}
return nil
}
diff --git a/internal/config/configfile_test.go b/internal/config/configfile_test.go
index 2c65467a..e22ddf94 100644
--- a/internal/config/configfile_test.go
+++ b/internal/config/configfile_test.go
@@ -14,6 +14,7 @@ import (
"github.com/hairyhenderson/gomplate/v3/internal/iohelpers"
"github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v3"
)
func TestParseConfigFile(t *testing.T) {
@@ -42,6 +43,11 @@ context:
.:
url: file:///data.json
+plugins:
+ foo:
+ cmd: echo
+ pipe: true
+
pluginTimeout: 2s
`
expected = &Config{
@@ -63,7 +69,10 @@ pluginTimeout: 2s
URL: mustURL("file:///data.json"),
},
},
- OutMode: "644",
+ OutMode: "644",
+ Plugins: map[string]PluginConfig{
+ "foo": {Cmd: "echo", Pipe: true},
+ },
PluginTimeout: 2 * time.Second,
}
@@ -298,23 +307,23 @@ func TestMergeFrom(t *testing.T) {
cfg = &Config{
Input: "hello world",
OutputFiles: []string{"-"},
- Plugins: map[string]string{
- "sleep": "echo",
+ Plugins: map[string]PluginConfig{
+ "sleep": {Cmd: "echo"},
},
PluginTimeout: 500 * time.Microsecond,
}
other = &Config{
InputFiles: []string{"-"},
OutputFiles: []string{"-"},
- Plugins: map[string]string{
- "sleep": "sleep.sh",
+ Plugins: map[string]PluginConfig{
+ "sleep": {Cmd: "sleep.sh"},
},
}
expected = &Config{
Input: "hello world",
OutputFiles: []string{"-"},
- Plugins: map[string]string{
- "sleep": "sleep.sh",
+ Plugins: map[string]PluginConfig{
+ "sleep": {Cmd: "sleep.sh"},
},
PluginTimeout: 500 * time.Microsecond,
}
@@ -394,7 +403,7 @@ func TestParsePluginFlags(t *testing.T) {
cfg = &Config{}
err = cfg.ParsePluginFlags([]string{"foo=bar"})
assert.NoError(t, err)
- assert.EqualValues(t, &Config{Plugins: map[string]string{"foo": "bar"}}, cfg)
+ assert.EqualValues(t, &Config{Plugins: map[string]PluginConfig{"foo": {Cmd: "bar"}}}, cfg)
}
func TestConfigString(t *testing.T) {
@@ -476,6 +485,24 @@ pluginTimeout: 500ms
`
assert.Equal(t, expected, c.String())
+
+ c = &Config{
+ Plugins: map[string]PluginConfig{
+ "foo": {
+ Cmd: "bar",
+ Pipe: true,
+ },
+ },
+ }
+ expected = `---
+plugins:
+ foo:
+ cmd: bar
+ timeout: 0s
+ pipe: true
+`
+
+ assert.Equal(t, expected, c.String())
}
func TestApplyDefaults(t *testing.T) {
@@ -756,3 +783,35 @@ func TestFromContext(t *testing.T) {
// assert that the returned config looks like a default one
assert.Equal(t, "{{", cfg.LDelim)
}
+
+func TestPluginConfig_UnmarshalYAML(t *testing.T) {
+ in := `foo`
+ out := PluginConfig{}
+ err := yaml.Unmarshal([]byte(in), &out)
+ assert.NoError(t, err)
+ assert.EqualValues(t, PluginConfig{Cmd: "foo"}, out)
+
+ in = `[foo, bar]`
+ out = PluginConfig{}
+ err = yaml.Unmarshal([]byte(in), &out)
+ assert.Error(t, err)
+
+ in = `cmd: foo`
+ out = PluginConfig{}
+ err = yaml.Unmarshal([]byte(in), &out)
+ assert.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)
+ assert.NoError(t, err)
+ assert.EqualValues(t, PluginConfig{
+ Cmd: "foo",
+ Timeout: time.Duration(10) * time.Millisecond,
+ Pipe: true,
+ }, out)
+}
diff --git a/internal/tests/integration/config_test.go b/internal/tests/integration/config_test.go
index 4ae47a10..7f31bfb6 100644
--- a/internal/tests/integration/config_test.go
+++ b/internal/tests/integration/config_test.go
@@ -28,8 +28,11 @@ func writeFile(dir *fs.Dir, f, content string) {
}
}
-func writeConfig(dir *fs.Dir, content string) {
+func writeConfig(t *testing.T, dir *fs.Dir, content string) {
+ t.Helper()
+
writeFile(dir, ".gomplate.yaml", content)
+ t.Logf("writing config: %s", content)
}
func TestConfig_ReadsFromSimpleConfigFile(t *testing.T) {
@@ -41,7 +44,7 @@ func TestConfig_ReadsFromSimpleConfigFile(t *testing.T) {
func TestConfig_ReadsStdin(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, "inputFiles: [-]")
+ writeConfig(t, tmpDir, "inputFiles: [-]")
o, e, err := cmd(t).withDir(tmpDir.Path()).withStdin("foo bar").run()
assertSuccess(t, o, e, err, "foo bar")
@@ -49,7 +52,7 @@ func TestConfig_ReadsStdin(t *testing.T) {
func TestConfig_FlagOverridesConfig(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, "inputFiles: [in]")
+ writeConfig(t, tmpDir, "inputFiles: [in]")
o, e, err := cmd(t, "-i", "hello from the cli").
withDir(tmpDir.Path()).run()
@@ -58,7 +61,7 @@ func TestConfig_FlagOverridesConfig(t *testing.T) {
func TestConfig_ReadsFromInputFile(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, "inputFiles: [in]")
+ writeConfig(t, tmpDir, "inputFiles: [in]")
writeFile(tmpDir, "in", "blah blah")
o, e, err := cmd(t).withDir(tmpDir.Path()).run()
@@ -67,7 +70,7 @@ func TestConfig_ReadsFromInputFile(t *testing.T) {
func TestConfig_Datasource(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `inputFiles: [in]
+ writeConfig(t, tmpDir, `inputFiles: [in]
datasources:
data:
url: in.yaml
@@ -82,7 +85,7 @@ datasources:
func TestConfig_OutputDir(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `inputDir: indir/
+ writeConfig(t, tmpDir, `inputDir: indir/
outputDir: outdir/
datasources:
data:
@@ -103,7 +106,7 @@ func TestConfig_ExecPipeOverridesConfigFile(t *testing.T) {
tmpDir := setupConfigTest(t)
// make sure exec-pipe works, and outFiles is replaced
- writeConfig(tmpDir, `in: hello world
+ writeConfig(t, tmpDir, `in: hello world
outputFiles: ['-']
`)
o, e, err := cmd(t, "-i", "hi", "--exec-pipe", "--", "tr", "[a-z]", "[A-Z]").
@@ -114,7 +117,7 @@ outputFiles: ['-']
func TestConfig_OutFile(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `in: hello world
+ writeConfig(t, tmpDir, `in: hello world
outputFiles: [out]
`)
o, e, err := cmd(t).withDir(tmpDir.Path()).run()
@@ -152,7 +155,7 @@ func TestConfig_ConfigOverridesEnvDelim(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `inputFiles: [in]
+ writeConfig(t, tmpDir, `inputFiles: [in]
leftDelim: (╯°□°)╯︵ ┻━┻
datasources:
data:
@@ -175,7 +178,7 @@ func TestConfig_FlagOverridesAllDelim(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `inputFiles: [in]
+ writeConfig(t, tmpDir, `inputFiles: [in]
leftDelim: (╯°□°)╯︵ ┻━┻
datasources:
data:
@@ -199,7 +202,7 @@ func TestConfig_ConfigOverridesEnvPluginTimeout(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `in: hi there {{ sleep 2 }}
+ writeConfig(t, tmpDir, `in: hi there {{ sleep 2 }}
plugins:
sleep: echo
@@ -215,7 +218,7 @@ pluginTimeout: 500ms
func TestConfig_ConfigOverridesEnvSuppressEmpty(t *testing.T) {
tmpDir := setupConfigTest(t)
- writeConfig(tmpDir, `in: |
+ writeConfig(t, tmpDir, `in: |
{{- print "\t \n\n\r\n\t\t \v\n" -}}
{{ print " " -}}
diff --git a/internal/tests/integration/plugins_test.go b/internal/tests/integration/plugins_test.go
index c95b024d..451274d8 100644
--- a/internal/tests/integration/plugins_test.go
+++ b/internal/tests/integration/plugins_test.go
@@ -27,6 +27,13 @@ write-error $msg
exit $code
`, fs.WithMode(0755)),
fs.WithFile("sleep.sh", "#!/bin/sh\n\nexec sleep $1\n", fs.WithMode(0755)),
+ fs.WithFile("replace.sh", `#!/bin/sh
+if [ "$#" -eq 2 ]; then
+ exec tr $1 $2
+elif [ "$#" -eq 3 ]; then
+ printf "=%s" $3 | tr $1 $2
+fi
+`, fs.WithMode(0755)),
)
t.Cleanup(tmpDir.Remove)
@@ -58,6 +65,10 @@ func TestPlugins_Errors(t *testing.T) {
}
func TestPlugins_Timeout(t *testing.T) {
+ if testing.Short() {
+ t.Skip()
+ }
+
tmpDir := setupPluginsTest(t)
_, _, err := cmd(t, "--plugin", "sleep="+tmpDir.Join("sleep.sh"),
"-i", `{{ sleep 10 }}`).run()
@@ -68,3 +79,30 @@ func TestPlugins_Timeout(t *testing.T) {
withEnv("GOMPLATE_PLUGIN_TIMEOUT", "500ms").run()
assert.ErrorContains(t, err, "plugin timed out")
}
+
+func TestPlugins_PipeMode(t *testing.T) {
+ tmpDir := setupPluginsTest(t)
+
+ writeConfig(t, tmpDir, `in: '{{ "hi there" | replace "h" "H" }}'
+plugins:
+ replace:
+ cmd: `+tmpDir.Join("replace.sh")+`
+ pipe: true
+`)
+
+ o, e, err := cmd(t).withDir(tmpDir.Path()).run()
+ assert.NilError(t, err)
+ assert.Equal(t, "", e)
+ assert.Equal(t, "Hi tHere", o)
+
+ writeConfig(t, tmpDir, `in: '{{ "hi there" | replace "e" "Z" }}'
+plugins:
+ replace:
+ cmd: `+tmpDir.Join("replace.sh")+`
+`)
+
+ o, e, err = cmd(t).withDir(tmpDir.Path()).run()
+ assert.NilError(t, err)
+ assert.Equal(t, "", e)
+ assert.Equal(t, "=hi=thZrZ", o)
+}