diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2022-02-21 22:41:32 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2022-02-27 20:16:18 -0500 |
| commit | 610a8b5a408cb3d08f4e4e2d6cf9cbe7194490d5 (patch) | |
| tree | 5a79319666ca7d71ee6744fcfd59dec07b0c3347 /internal | |
| parent | 2aa13dd3cf08ee24eb174edb78ee4d13415005b5 (diff) | |
Support piping input to plugin
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/configfile.go | 51 | ||||
| -rw-r--r-- | internal/config/configfile_test.go | 75 | ||||
| -rw-r--r-- | internal/tests/integration/config_test.go | 27 | ||||
| -rw-r--r-- | internal/tests/integration/plugins_test.go | 38 |
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) +} |
