From 7ff174a86a935191a684f0c63f9e2a48058fabfb Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 11 Nov 2019 16:03:16 -0500 Subject: [PATCH] Support a config file to use instead of commandline arguments Signed-off-by: Dave Henderson --- .github/workflows/build.yml | 10 + cmd/gomplate/config.go | 253 ++++++++++++ cmd/gomplate/config_test.go | 258 ++++++++++++ cmd/gomplate/logger.go | 3 +- cmd/gomplate/main.go | 115 +++--- cmd/gomplate/main_test.go | 22 - cmd/gomplate/validate.go | 63 --- cmd/gomplate/validate_test.go | 95 ----- config.go | 44 +- config_test.go | 25 -- context.go | 17 +- context_test.go | 26 +- data/datasource.go | 24 ++ data/datasource_test.go | 69 ++++ docs/content/config.md | 352 ++++++++++++++++ docs/content/datasources.md | 2 +- docs/content/syntax.md | 2 +- docs/content/usage.md | 22 +- docs/static/images/gomplate-gh.png | Bin 0 -> 71710 bytes docs/static/images/gomplate-large.png | Bin 0 -> 64260 bytes gomplate.go | 46 ++- internal/config/configfile.go | 538 +++++++++++++++++++++++++ internal/config/configfile_test.go | 558 ++++++++++++++++++++++++++ plugins.go | 40 +- plugins_test.go | 50 +-- template.go | 44 +- template_test.go | 30 +- tests/integration/basic_test.go | 14 +- tests/integration/config_test.go | 227 +++++++++++ tests/integration/tmpl_test.go | 1 - 30 files changed, 2506 insertions(+), 444 deletions(-) create mode 100644 cmd/gomplate/config.go create mode 100644 cmd/gomplate/config_test.go delete mode 100644 cmd/gomplate/validate.go delete mode 100644 cmd/gomplate/validate_test.go create mode 100644 docs/content/config.md create mode 100644 docs/static/images/gomplate-gh.png create mode 100644 docs/static/images/gomplate-large.png create mode 100644 internal/config/configfile.go create mode 100644 internal/config/configfile_test.go create mode 100644 tests/integration/config_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c57accd9..b3ccd5c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,11 @@ jobs: - run: make build env: GOPATH: ${{ runner.workspace }} + - name: Save binary + uses: actions/upload-artifact@v1 + with: + name: gomplate + path: bin/gomplate - run: make test env: GOPATH: ${{ runner.workspace }} @@ -41,6 +46,11 @@ jobs: - run: make build env: GOPATH: ${{ runner.workspace }} + - name: Save binary + uses: actions/upload-artifact@v1 + with: + name: gomplate.exe + path: bin/gomplate.exe - run: make test env: GOPATH: ${{ runner.workspace }} diff --git a/cmd/gomplate/config.go b/cmd/gomplate/config.go new file mode 100644 index 00000000..02278618 --- /dev/null +++ b/cmd/gomplate/config.go @@ -0,0 +1,253 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/hairyhenderson/gomplate/v3/conv" + "github.com/hairyhenderson/gomplate/v3/env" + "github.com/hairyhenderson/gomplate/v3/internal/config" + + "github.com/rs/zerolog" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +const ( + defaultConfigFile = ".gomplate.yaml" +) + +var fs = afero.NewOsFs() + +// 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) +// - merges the two (flags take precedence) +// - validates the final config +// - converts the config to a *gomplate.Config for further use (TODO: eliminate this part) +func loadConfig(cmd *cobra.Command, args []string) (*config.Config, error) { + ctx := cmd.Context() + flagConfig, err := cobraConfig(cmd, args) + if err != nil { + return nil, err + } + + cfg, err := readConfigFile(cmd) + if err != nil { + return nil, err + } + if cfg == nil { + cfg = flagConfig + } else { + cfg = cfg.MergeFrom(flagConfig) + } + + cfg, err = applyEnvVars(ctx, cfg) + if err != nil { + return nil, err + } + + // reset defaults before validation + cfg.ApplyDefaults() + + err = cfg.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate merged config: %w\n%+v", err, cfg) + } + return cfg, nil +} + +func pickConfigFile(cmd *cobra.Command) (cfgFile string, required bool) { + cfgFile = defaultConfigFile + if c := env.Getenv("GOMPLATE_CONFIG"); c != "" { + cfgFile = c + required = true + } + if cmd.Flags().Changed("config") && cmd.Flag("config").Value.String() != "" { + // Use config file from the flag if specified + cfgFile = cmd.Flag("config").Value.String() + required = true + } + return cfgFile, required +} + +func readConfigFile(cmd *cobra.Command) (cfg *config.Config, err error) { + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + log := zerolog.Ctx(ctx) + + cfgFile, configRequired := pickConfigFile(cmd) + + f, err := fs.Open(cfgFile) + if err != nil { + if configRequired { + return cfg, fmt.Errorf("config file requested, but couldn't be opened: %w", err) + } + return nil, nil + } + + cfg, err = config.Parse(f) + if err != nil && configRequired { + return cfg, fmt.Errorf("config file requested, but couldn't be parsed: %w", err) + } + + log.Debug().Str("cfgFile", cfgFile).Msg("using config file") + + return cfg, err +} + +// cobraConfig - initialize a config from the commandline options +func cobraConfig(cmd *cobra.Command, args []string) (cfg *config.Config, err error) { + cfg = &config.Config{} + cfg.InputFiles, err = getStringSlice(cmd, "file") + if err != nil { + return nil, err + } + cfg.Input, err = getString(cmd, "in") + if err != nil { + return nil, err + } + cfg.InputDir, err = getString(cmd, "input-dir") + if err != nil { + return nil, err + } + + cfg.ExcludeGlob, err = getStringSlice(cmd, "exclude") + if err != nil { + return nil, err + } + includesFlag, err := getStringSlice(cmd, "include") + if err != nil { + return nil, err + } + // support --include + cfg.ExcludeGlob = processIncludes(includesFlag, cfg.ExcludeGlob) + + cfg.OutputFiles, err = getStringSlice(cmd, "out") + if err != nil { + return nil, err + } + cfg.Templates, err = getStringSlice(cmd, "template") + if err != nil { + return nil, err + } + cfg.OutputDir, err = getString(cmd, "output-dir") + if err != nil { + return nil, err + } + cfg.OutputMap, err = getString(cmd, "output-map") + if err != nil { + return nil, err + } + cfg.OutMode, err = getString(cmd, "chmod") + if err != nil { + return nil, err + } + + if len(args) > 0 { + cfg.PostExec = args + } + + cfg.ExecPipe, err = getBool(cmd, "exec-pipe") + if err != nil { + return nil, err + } + + cfg.LDelim, err = getString(cmd, "left-delim") + if err != nil { + return nil, err + } + cfg.RDelim, err = getString(cmd, "right-delim") + if err != nil { + return nil, err + } + + ds, err := getStringSlice(cmd, "datasource") + if err != nil { + return nil, err + } + cx, err := getStringSlice(cmd, "context") + if err != nil { + return nil, err + } + hdr, err := getStringSlice(cmd, "datasource-header") + if err != nil { + return nil, err + } + err = cfg.ParseDataSourceFlags(ds, cx, hdr) + if err != nil { + return nil, err + } + + pl, err := getStringSlice(cmd, "plugin") + if err != nil { + return nil, err + } + err = cfg.ParsePluginFlags(pl) + if err != nil { + return nil, err + } + return cfg, nil +} + +func getStringSlice(cmd *cobra.Command, flag string) (s []string, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + s, err = cmd.Flags().GetStringSlice(flag) + } + return s, err +} + +func getString(cmd *cobra.Command, flag string) (s string, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + s, err = cmd.Flags().GetString(flag) + } + return s, err +} + +func getBool(cmd *cobra.Command, flag string) (b bool, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + b, err = cmd.Flags().GetBool(flag) + } + return b, err +} + +// process --include flags - these are analogous to specifying --exclude '*', +// then the inverse of the --include options. +func processIncludes(includes, excludes []string) []string { + if len(includes) == 0 && len(excludes) == 0 { + return nil + } + + out := []string{} + // if any --includes are set, we start by excluding everything + if len(includes) > 0 { + out = make([]string, 1+len(includes)) + out[0] = "*" + } + for i, include := range includes { + // includes are just the opposite of an exclude + out[i+1] = "!" + include + } + out = append(out, excludes...) + return out +} + +func applyEnvVars(ctx context.Context, cfg *config.Config) (*config.Config, error) { + if to := env.Getenv("GOMPLATE_PLUGIN_TIMEOUT"); cfg.PluginTimeout == 0 && to != "" { + t, err := time.ParseDuration(to) + if err != nil { + return nil, fmt.Errorf("GOMPLATE_PLUGIN_TIMEOUT set to invalid value %q: %w", to, err) + } + cfg.PluginTimeout = t + } + + if !cfg.SuppressEmpty && conv.ToBool(env.Getenv("GOMPLATE_SUPPRESS_EMPTY", "false")) { + cfg.SuppressEmpty = true + } + + return cfg, nil +} diff --git a/cmd/gomplate/config_test.go b/cmd/gomplate/config_test.go new file mode 100644 index 00000000..dc05dfbb --- /dev/null +++ b/cmd/gomplate/config_test.go @@ -0,0 +1,258 @@ +package main + +import ( + "context" + "os" + "testing" + "time" + + "github.com/hairyhenderson/gomplate/v3/internal/config" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestReadConfigFile(t *testing.T) { + fs = afero.NewMemMapFs() + defer func() { fs = afero.NewOsFs() }() + cmd := &cobra.Command{} + + _, err := readConfigFile(cmd) + assert.NoError(t, err) + + cmd.Flags().String("config", defaultConfigFile, "foo") + + _, err = readConfigFile(cmd) + assert.NoError(t, err) + + cmd.ParseFlags([]string{"--config", "config.file"}) + + _, err = readConfigFile(cmd) + assert.Error(t, err) + + cmd = &cobra.Command{} + cmd.Flags().String("config", defaultConfigFile, "foo") + + f, err := fs.Create(defaultConfigFile) + assert.NoError(t, err) + f.WriteString("") + + cfg, err := readConfigFile(cmd) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{}, cfg) + + cmd.ParseFlags([]string{"--config", "config.yaml"}) + + f, err = fs.Create("config.yaml") + assert.NoError(t, err) + f.WriteString("in: hello world\n") + + cfg, err = readConfigFile(cmd) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{Input: "hello world"}, cfg) + + f.WriteString("in: ") + + _, err = readConfigFile(cmd) + assert.Error(t, err) +} + +func TestLoadConfig(t *testing.T) { + fs = afero.NewMemMapFs() + defer func() { fs = afero.NewOsFs() }() + + cmd := &cobra.Command{} + cmd.Args = optionalExecArgs + cmd.Flags().StringSlice("file", []string{"-"}, "...") + cmd.Flags().StringSlice("out", []string{"-"}, "...") + cmd.Flags().String("in", ".", "...") + cmd.Flags().String("output-dir", ".", "...") + cmd.Flags().String("left-delim", "{{", "...") + cmd.Flags().String("right-delim", "}}", "...") + cmd.Flags().Bool("exec-pipe", false, "...") + cmd.ParseFlags(nil) + + out, err := loadConfig(cmd, cmd.Flags().Args()) + expected := &config.Config{ + InputFiles: []string{"-"}, + OutputFiles: []string{"-"}, + LDelim: "{{", + RDelim: "}}", + PostExecInput: os.Stdin, + OutWriter: os.Stdout, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) + + cmd.ParseFlags([]string{"--in", "foo"}) + out, err = loadConfig(cmd, cmd.Flags().Args()) + expected = &config.Config{ + Input: "foo", + OutputFiles: []string{"-"}, + LDelim: "{{", + RDelim: "}}", + PostExecInput: os.Stdin, + OutWriter: os.Stdout, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) + + cmd.ParseFlags([]string{"--in", "foo", "--exec-pipe", "--", "tr", "[a-z]", "[A-Z]"}) + out, err = loadConfig(cmd, cmd.Flags().Args()) + expected = &config.Config{ + Input: "foo", + LDelim: "{{", + RDelim: "}}", + ExecPipe: true, + PostExec: []string{"tr", "[a-z]", "[A-Z]"}, + PostExecInput: out.PostExecInput, + OutWriter: out.PostExecInput, + OutputFiles: []string{"-"}, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) +} + +func TestCobraConfig(t *testing.T) { + t.Parallel() + cmd := &cobra.Command{} + cmd.Flags().StringSlice("file", []string{"-"}, "...") + cmd.Flags().StringSlice("out", []string{"-"}, "...") + cmd.Flags().String("output-dir", ".", "...") + cmd.Flags().String("left-delim", "{{", "...") + cmd.Flags().String("right-delim", "}}", "...") + cmd.ParseFlags(nil) + + cfg, err := cobraConfig(cmd, cmd.Flags().Args()) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{}, cfg) + + cmd.ParseFlags([]string{"--file", "in", "--", "echo", "foo"}) + + cfg, err = cobraConfig(cmd, cmd.Flags().Args()) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{ + InputFiles: []string{"in"}, + PostExec: []string{"echo", "foo"}, + }, cfg) +} + +func TestProcessIncludes(t *testing.T) { + t.Parallel() + data := []struct { + inc, exc, expected []string + }{ + {nil, nil, nil}, + {[]string{}, []string{}, nil}, + {nil, []string{"*.foo"}, []string{"*.foo"}}, + {[]string{"*.bar"}, []string{"a*.bar"}, []string{"*", "!*.bar", "a*.bar"}}, + {[]string{"*.bar"}, nil, []string{"*", "!*.bar"}}, + } + + for _, d := range data { + assert.EqualValues(t, d.expected, processIncludes(d.inc, d.exc)) + } +} + +func TestPickConfigFile(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().String("config", defaultConfigFile, "foo") + + cf, req := pickConfigFile(cmd) + assert.False(t, req) + assert.Equal(t, defaultConfigFile, cf) + + os.Setenv("GOMPLATE_CONFIG", "foo.yaml") + defer os.Unsetenv("GOMPLATE_CONFIG") + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "foo.yaml", cf) + + cmd.ParseFlags([]string{"--config", "config.file"}) + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "config.file", cf) + + os.Setenv("GOMPLATE_CONFIG", "ignored.yaml") + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "config.file", cf) +} + +func TestApplyEnvVars_PluginTimeout(t *testing.T) { + os.Setenv("GOMPLATE_PLUGIN_TIMEOUT", "bogus") + + ctx := context.TODO() + cfg := &config.Config{} + _, err := applyEnvVars(ctx, cfg) + assert.Error(t, err) + + cfg = &config.Config{ + PluginTimeout: 2 * time.Second, + } + expected := &config.Config{ + PluginTimeout: 2 * time.Second, + } + actual, err := applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_PLUGIN_TIMEOUT", "2s") + defer os.Unsetenv("GOMPLATE_PLUGIN_TIMEOUT") + + cfg = &config.Config{} + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + cfg = &config.Config{ + PluginTimeout: 100 * time.Millisecond, + } + expected = &config.Config{ + PluginTimeout: 100 * time.Millisecond, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + +} + +func TestApplyEnvVars_SuppressEmpty(t *testing.T) { + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "bogus") + defer os.Unsetenv("GOMPLATE_SUPPRESS_EMPTY") + + ctx := context.TODO() + cfg := &config.Config{} + expected := &config.Config{ + SuppressEmpty: false, + } + actual, err := applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "true") + + cfg = &config.Config{} + expected = &config.Config{ + SuppressEmpty: true, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "false") + + cfg = &config.Config{ + SuppressEmpty: true, + } + expected = &config.Config{ + SuppressEmpty: true, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) +} diff --git a/cmd/gomplate/logger.go b/cmd/gomplate/logger.go index d40281a5..c58dc31d 100644 --- a/cmd/gomplate/logger.go +++ b/cmd/gomplate/logger.go @@ -12,7 +12,8 @@ import ( ) func initLogger(ctx context.Context) context.Context { - zerolog.SetGlobalLevel(zerolog.InfoLevel) + // default to warn level + zerolog.SetGlobalLevel(zerolog.WarnLevel) zerolog.DurationFieldUnit = time.Second stdlogger := log.With().Bool("stdlog", true).Logger() diff --git a/cmd/gomplate/main.go b/cmd/gomplate/main.go index 62c85fdb..110a0503 100644 --- a/cmd/gomplate/main.go +++ b/cmd/gomplate/main.go @@ -5,33 +5,26 @@ The gomplate command package main import ( - "bytes" "context" + "fmt" "os" "os/exec" "os/signal" "github.com/hairyhenderson/gomplate/v3" "github.com/hairyhenderson/gomplate/v3/env" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/hairyhenderson/gomplate/v3/version" "github.com/rs/zerolog" - "github.com/spf13/cobra" -) -var ( - verbose bool - execPipe bool - opts gomplate.Config - includes []string - - postRunInput *bytes.Buffer + "github.com/spf13/cobra" ) // postRunExec - if templating succeeds, the command following a '--' will be executed -func postRunExec(cmd *cobra.Command, args []string) error { +func postRunExec(ctx context.Context, cfg *config.Config) error { + args := cfg.PostExec if len(args) > 0 { - ctx := cmd.Context() log := zerolog.Ctx(ctx) log.Debug().Strs("args", args).Msg("running post-exec command") @@ -39,11 +32,7 @@ func postRunExec(cmd *cobra.Command, args []string) error { args = args[1:] // nolint: gosec c := exec.CommandContext(ctx, name, args...) - if execPipe { - c.Stdin = postRunInput - } else { - c.Stdin = os.Stdin - } + c.Stdin = cfg.PostExecInput c.Stderr = os.Stderr c.Stdout = os.Stdout @@ -73,28 +62,10 @@ func optionalExecArgs(cmd *cobra.Command, args []string) error { return cobra.NoArgs(cmd, args) } -// process --include flags - these are analogous to specifying --exclude '*', -// then the inverse of the --include options. -func processIncludes(includes, excludes []string) []string { - out := []string{} - // if any --includes are set, we start by excluding everything - if len(includes) > 0 { - out = make([]string, 1+len(includes)) - out[0] = "*" - } - for i, include := range includes { - // includes are just the opposite of an exclude - out[i+1] = "!" + include - } - out = append(out, excludes...) - return out -} - func newGomplateCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "gomplate", Short: "Process text files with Go templates", - PreRunE: validateOpts, Version: version.Version, RunE: func(cmd *cobra.Command, args []string) error { if v, _ := cmd.Flags().GetBool("verbose"); v { @@ -103,27 +74,33 @@ func newGomplateCmd() *cobra.Command { ctx := cmd.Context() log := zerolog.Ctx(ctx) - log.Debug().Msgf("%s version %s, build %s\nconfig is:\n%s", - cmd.Name(), version.Version, version.GitCommit, - &opts) + cfg, err := loadConfig(cmd, args) + if err != nil { + return err + } - // support --include - opts.ExcludeGlob = processIncludes(includes, opts.ExcludeGlob) + log.Debug().Msgf("starting %s", cmd.Name()) + log.Debug(). + Str("version", version.Version). + Str("build", version.GitCommit). + Msgf("config is:\n%v", cfg) - if execPipe { - postRunInput = &bytes.Buffer{} - opts.Out = postRunInput - } - err := gomplate.RunTemplates(&opts) + err = gomplate.RunTemplatesWithContext(ctx, cfg) cmd.SilenceErrors = true cmd.SilenceUsage = true - log.Debug().Msgf("rendered %d template(s) with %d error(s) in %v", - gomplate.Metrics.TemplatesProcessed, gomplate.Metrics.Errors, gomplate.Metrics.TotalRenderDuration) - return err + fmt.Fprintf(os.Stderr, "\n") + log.Debug().Int("templatesRendered", gomplate.Metrics.TemplatesProcessed). + Int("errors", gomplate.Metrics.Errors). + Dur("duration", gomplate.Metrics.TotalRenderDuration). + Msg("completed rendering") + + if err != nil { + return err + } + return postRunExec(ctx, cfg) }, - PostRunE: postRunExec, - Args: optionalExecArgs, + Args: optionalExecArgs, } return rootCmd } @@ -131,34 +108,36 @@ func newGomplateCmd() *cobra.Command { func initFlags(command *cobra.Command) { command.Flags().SortFlags = false - command.Flags().StringArrayVarP(&opts.DataSources, "datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.") - command.Flags().StringArrayVarP(&opts.DataSourceHeaders, "datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.") + command.Flags().StringSliceP("datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.") + command.Flags().StringSliceP("datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.") - command.Flags().StringArrayVarP(&opts.Contexts, "context", "c", nil, "pre-load a `datasource` into the context, in alias=URL form. Use the special alias `.` to set the root context.") + command.Flags().StringSliceP("context", "c", nil, "pre-load a `datasource` into the context, in alias=URL form. Use the special alias `.` to set the root context.") - command.Flags().StringArrayVar(&opts.Plugins, "plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times") + command.Flags().StringSlice("plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times") - command.Flags().StringArrayVarP(&opts.InputFiles, "file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir") - command.Flags().StringVarP(&opts.Input, "in", "i", "", "Template `string` to process (alternative to --file and --input-dir)") - command.Flags().StringVar(&opts.InputDir, "input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)") + command.Flags().StringSliceP("file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir") + command.Flags().StringP("in", "i", "", "Template `string` to process (alternative to --file and --input-dir)") + command.Flags().String("input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)") - command.Flags().StringArrayVar(&opts.ExcludeGlob, "exclude", []string{}, "glob of files to not parse") - command.Flags().StringArrayVar(&includes, "include", []string{}, "glob of files to parse") + command.Flags().StringSlice("exclude", []string{}, "glob of files to not parse") + command.Flags().StringSlice("include", []string{}, "glob of files to parse") - command.Flags().StringArrayVarP(&opts.OutputFiles, "out", "o", []string{"-"}, "output `file` name. Omit to use standard output.") - command.Flags().StringArrayVarP(&opts.Templates, "template", "t", []string{}, "Additional template file(s)") - command.Flags().StringVar(&opts.OutputDir, "output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir") - command.Flags().StringVar(&opts.OutputMap, "output-map", "", "Template `string` to map the input file to an output path") - command.Flags().StringVar(&opts.OutMode, "chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") + command.Flags().StringSliceP("out", "o", []string{"-"}, "output `file` name. Omit to use standard output.") + command.Flags().StringSliceP("template", "t", []string{}, "Additional template file(s)") + command.Flags().String("output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir") + command.Flags().String("output-map", "", "Template `string` to map the input file to an output path") + command.Flags().String("chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") - command.Flags().BoolVar(&execPipe, "exec-pipe", false, "pipe the output to the post-run exec command") + command.Flags().Bool("exec-pipe", false, "pipe the output to the post-run exec command") ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{") rdDefault := env.Getenv("GOMPLATE_RIGHT_DELIM", "}}") - command.Flags().StringVar(&opts.LDelim, "left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]") - command.Flags().StringVar(&opts.RDelim, "right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]") + command.Flags().String("left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]") + command.Flags().String("right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]") + + command.Flags().BoolP("verbose", "V", false, "output extra information about what gomplate is doing") - command.Flags().BoolVarP(&verbose, "verbose", "V", false, "output extra information about what gomplate is doing") + command.Flags().String("config", defaultConfigFile, "config file (overridden by commandline flags)") } func main() { diff --git a/cmd/gomplate/main_test.go b/cmd/gomplate/main_test.go index e0d74e9d..06ab7d0f 100644 --- a/cmd/gomplate/main_test.go +++ b/cmd/gomplate/main_test.go @@ -1,23 +1 @@ package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestProcessIncludes(t *testing.T) { - data := []struct { - inc, exc, expected []string - }{ - {nil, nil, []string{}}, - {[]string{}, []string{}, []string{}}, - {nil, []string{"*.foo"}, []string{"*.foo"}}, - {[]string{"*.bar"}, []string{"a*.bar"}, []string{"*", "!*.bar", "a*.bar"}}, - {[]string{"*.bar"}, nil, []string{"*", "!*.bar"}}, - } - - for _, d := range data { - assert.EqualValues(t, d.expected, processIncludes(d.inc, d.exc)) - } -} diff --git a/cmd/gomplate/validate.go b/cmd/gomplate/validate.go deleted file mode 100644 index f4c73133..00000000 --- a/cmd/gomplate/validate.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" -) - -func notTogether(cmd *cobra.Command, flags ...string) error { - found := "" - for _, flag := range flags { - f := cmd.Flag(flag) - if f != nil && f.Changed { - if found != "" { - a := make([]string, len(flags)) - for i := range a { - a[i] = "--" + flags[i] - } - return fmt.Errorf("only one of these flags is supported at a time: %s", strings.Join(a, ", ")) - } - found = flag - } - } - return nil -} - -func mustTogether(cmd *cobra.Command, left, right string) error { - l := cmd.Flag(left) - if l != nil && l.Changed { - r := cmd.Flag(right) - if r != nil && !r.Changed { - return fmt.Errorf("--%s must be set when --%s is set", right, left) - } - } - - return nil -} - -func validateOpts(cmd *cobra.Command, args []string) (err error) { - err = notTogether(cmd, "in", "file", "input-dir") - if err == nil { - err = notTogether(cmd, "out", "output-dir", "output-map", "exec-pipe") - } - - if err == nil && len(opts.InputFiles) != len(opts.OutputFiles) { - err = fmt.Errorf("must provide same number of --out (%d) as --file (%d) options", len(opts.OutputFiles), len(opts.InputFiles)) - } - - if err == nil && cmd.Flag("exec-pipe").Changed && len(args) == 0 { - err = fmt.Errorf("--exec-pipe may only be used with a post-exec command after --") - } - - if err == nil { - err = mustTogether(cmd, "output-dir", "input-dir") - } - - if err == nil { - err = mustTogether(cmd, "output-map", "input-dir") - } - - return err -} diff --git a/cmd/gomplate/validate_test.go b/cmd/gomplate/validate_test.go deleted file mode 100644 index 54f4ba01..00000000 --- a/cmd/gomplate/validate_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" -) - -func TestValidateOpts(t *testing.T) { - err := validateOpts(parseFlags()) - assert.NoError(t, err) - - err = validateOpts(parseFlags("-i=foo", "-f", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-i=foo", "-o=bar", "-o=baz")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-i=foo", "--input-dir=baz")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--input-dir=foo", "-f=bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-dir=foo", "-o=bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-dir=foo")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-map", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-o", "foo", "--output-map", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--output-dir", "foo", - "--output-map", "bar", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags("--exec-pipe")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--exec-pipe", "--")) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--exec-pipe", - "--", "echo", "foo", - )) - assert.NoError(t, err) - - err = validateOpts(parseFlags( - "--exec-pipe", - "--out", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--exec-pipe", - "--output-dir", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--exec-pipe", - "--output-map", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--output-map", "bar", - )) - assert.NoError(t, err) -} - -func parseFlags(flags ...string) (cmd *cobra.Command, args []string) { - cmd = &cobra.Command{} - initFlags(cmd) - err := cmd.ParseFlags(flags) - if err != nil { - panic(err) - } - return cmd, cmd.Flags().Args() -} diff --git a/config.go b/config.go index 18bd5546..913370a5 100644 --- a/config.go +++ b/config.go @@ -2,9 +2,9 @@ package gomplate import ( "io" - "os" - "strconv" "strings" + + "github.com/hairyhenderson/gomplate/v3/internal/config" ) // Config - values necessary for rendering templates with gomplate. @@ -52,20 +52,6 @@ func (o *Config) defaults() *Config { return o } -// parse an os.FileMode out of the string, and let us know if it's an override or not... -func (o *Config) getMode() (os.FileMode, bool, error) { - modeOverride := o.OutMode != "" - m, err := strconv.ParseUint("0"+o.OutMode, 8, 32) - if err != nil { - return 0, false, err - } - mode := os.FileMode(m) - if mode == 0 && o.Input != "" { - mode = 0644 - } - return mode, modeOverride, nil -} - // nolint: gocyclo func (o *Config) String() string { o.defaults() @@ -124,3 +110,29 @@ func (o *Config) String() string { } return c } + +func (o *Config) toNewConfig() (*config.Config, error) { + cfg := &config.Config{ + Input: o.Input, + InputFiles: o.InputFiles, + InputDir: o.InputDir, + ExcludeGlob: o.ExcludeGlob, + OutputFiles: o.OutputFiles, + OutputDir: o.OutputDir, + OutputMap: o.OutputMap, + OutMode: o.OutMode, + LDelim: o.LDelim, + RDelim: o.RDelim, + Templates: o.Templates, + OutWriter: o.Out, + } + err := cfg.ParsePluginFlags(o.Plugins) + if err != nil { + return nil, err + } + err = cfg.ParseDataSourceFlags(o.DataSources, o.Contexts, o.DataSourceHeaders) + if err != nil { + return nil, err + } + return cfg, nil +} diff --git a/config_test.go b/config_test.go index 91c2b1a9..0ddefc3e 100644 --- a/config_test.go +++ b/config_test.go @@ -1,7 +1,6 @@ package gomplate import ( - "os" "testing" "github.com/stretchr/testify/assert" @@ -47,27 +46,3 @@ output: {{ .in }}` assert.Equal(t, expected, c.String()) } - -func TestGetMode(t *testing.T) { - c := &Config{} - m, o, err := c.getMode() - assert.NoError(t, err) - assert.Equal(t, os.FileMode(0), m) - assert.False(t, o) - - c = &Config{OutMode: "755"} - m, o, err = c.getMode() - assert.NoError(t, err) - assert.Equal(t, os.FileMode(0755), m) - assert.True(t, o) - - c = &Config{OutMode: "0755"} - m, o, err = c.getMode() - assert.NoError(t, err) - assert.Equal(t, os.FileMode(0755), m) - assert.True(t, o) - - c = &Config{OutMode: "foo"} - _, _, err = c.getMode() - assert.Error(t, err) -} diff --git a/context.go b/context.go index ab12d404..a2b8b002 100644 --- a/context.go +++ b/context.go @@ -1,10 +1,12 @@ package gomplate import ( + "context" "os" "strings" "github.com/hairyhenderson/gomplate/v3/data" + "github.com/hairyhenderson/gomplate/v3/internal/config" ) // context for templates @@ -20,11 +22,10 @@ func (c *tmplctx) Env() map[string]string { return env } -func createTmplContext(contexts []string, d *data.Data) (interface{}, error) { +func createTmplContext(ctx context.Context, contexts config.DSources, d *data.Data) (interface{}, error) { var err error tctx := &tmplctx{} - for _, c := range contexts { - a := parseAlias(c) + for a := range contexts { if a == "." { return d.Datasource(a) } @@ -35,13 +36,3 @@ func createTmplContext(contexts []string, d *data.Data) (interface{}, error) { } return tctx, nil } - -func parseAlias(arg string) string { - parts := strings.SplitN(arg, "=", 2) - switch len(parts) { - case 1: - return strings.SplitN(parts[0], ".", 2)[0] - default: - return parts[0] - } -} diff --git a/context_test.go b/context_test.go index ab273749..94f2b51b 100644 --- a/context_test.go +++ b/context_test.go @@ -1,11 +1,13 @@ package gomplate import ( + "context" "net/url" "os" "testing" "github.com/hairyhenderson/gomplate/v3/data" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/stretchr/testify/assert" ) @@ -24,7 +26,8 @@ func TestEnvGetsUpdatedEnvironment(t *testing.T) { } func TestCreateContext(t *testing.T) { - c, err := createTmplContext(nil, nil) + ctx := context.TODO() + c, err := createTmplContext(ctx, nil, nil) assert.NoError(t, err) assert.Empty(t, c) @@ -40,31 +43,18 @@ func TestCreateContext(t *testing.T) { } os.Setenv("foo", "foo: bar") defer os.Unsetenv("foo") - c, err = createTmplContext([]string{"foo=" + fooURL}, d) + c, err = createTmplContext(ctx, map[string]config.DSConfig{"foo": {URL: uf}}, d) assert.NoError(t, err) assert.IsType(t, &tmplctx{}, c) - ctx := c.(*tmplctx) - ds := ((*ctx)["foo"]).(map[string]interface{}) + tctx := c.(*tmplctx) + ds := ((*tctx)["foo"]).(map[string]interface{}) assert.Equal(t, "bar", ds["foo"]) os.Setenv("bar", "bar: baz") defer os.Unsetenv("bar") - c, err = createTmplContext([]string{".=" + barURL}, d) + c, err = createTmplContext(ctx, map[string]config.DSConfig{".": {URL: ub}}, d) assert.NoError(t, err) assert.IsType(t, map[string]interface{}{}, c) ds = c.(map[string]interface{}) assert.Equal(t, "baz", ds["bar"]) } - -func TestParseAlias(t *testing.T) { - testdata := map[string]string{ - "": "", - "foo": "foo", - "foo.bar": "foo", - "a=b": "a", - ".=foo": ".", - } - for k, v := range testdata { - assert.Equal(t, v, parseAlias(k)) - } -} diff --git a/data/datasource.go b/data/datasource.go index 1fcccf98..456b5a12 100644 --- a/data/datasource.go +++ b/data/datasource.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/hairyhenderson/gomplate/v3/libkv" "github.com/hairyhenderson/gomplate/v3/vault" ) @@ -125,6 +126,29 @@ func NewData(datasourceArgs, headerArgs []string) (*Data, error) { return data, nil } +// FromConfig - internal use only! +func FromConfig(cfg *config.Config) *Data { + sources := map[string]*Source{} + for alias, d := range cfg.DataSources { + sources[alias] = &Source{ + Alias: alias, + URL: d.URL, + header: d.Header, + } + } + for alias, d := range cfg.Context { + sources[alias] = &Source{ + Alias: alias, + URL: d.URL, + header: d.Header, + } + } + return &Data{ + Sources: sources, + extraHeaders: cfg.ExtraHeaders, + } +} + // Source - a data source type Source struct { Alias string diff --git a/data/datasource_test.go b/data/datasource_test.go index cdef3945..6752d9e5 100644 --- a/data/datasource_test.go +++ b/data/datasource_test.go @@ -2,6 +2,7 @@ package data import ( "fmt" + "net/http" "net/url" "os" "path/filepath" @@ -9,6 +10,7 @@ import ( "strings" "testing" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -455,3 +457,70 @@ func TestAbsFileURL(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, expected, u) } + +func TestFromConfig(t *testing.T) { + cfg := &config.Config{} + expected := &Data{ + Sources: map[string]*Source{}, + } + assert.EqualValues(t, expected, FromConfig(cfg)) + + cfg = &config.Config{ + DataSources: map[string]config.DSConfig{ + "foo": { + URL: mustParseURL("http://example.com"), + }, + }, + } + expected = &Data{ + Sources: map[string]*Source{ + "foo": { + Alias: "foo", + URL: mustParseURL("http://example.com"), + }, + }, + } + assert.EqualValues(t, expected, FromConfig(cfg)) + + cfg = &config.Config{ + DataSources: map[string]config.DSConfig{ + "foo": { + URL: mustParseURL("http://foo.com"), + }, + }, + Context: map[string]config.DSConfig{ + "bar": { + URL: mustParseURL("http://bar.com"), + Header: http.Header{ + "Foo": []string{"bar"}, + }, + }, + }, + ExtraHeaders: map[string]http.Header{ + "baz": { + "Foo": []string{"bar"}, + }, + }, + } + expected = &Data{ + Sources: map[string]*Source{ + "foo": { + Alias: "foo", + URL: mustParseURL("http://foo.com"), + }, + "bar": { + Alias: "bar", + URL: mustParseURL("http://bar.com"), + header: http.Header{ + "Foo": []string{"bar"}, + }, + }, + }, + extraHeaders: map[string]http.Header{ + "baz": { + "Foo": []string{"bar"}, + }, + }, + } + assert.EqualValues(t, expected, FromConfig(cfg)) +} diff --git a/docs/content/config.md b/docs/content/config.md new file mode 100644 index 00000000..7857e452 --- /dev/null +++ b/docs/content/config.md @@ -0,0 +1,352 @@ +--- +title: Configuration +weight: 12 +menu: main +--- + +In addition to [command-line arguments][], gomplate supports the use of +configuration files to control its behaviour. + +Using a file for configuration can be useful especially when rendering templates +that use multiple datasources, plugins, nested templates, etc... In situations +where teams share templates, it can be helpful to commit config files into the +team's source control system. + +By default, gomplate will look for a file `.gomplate.yaml` in the current working +diretory, but this path can be altered with the [`--config`](../usage/#--config) +command-line argument, or the `GOMPLATE_CONFIG` environment variable. + +### Configuration precedence + +[Command-line arguments][] will always take precedence over settings in a config +file. In the cases where configuration can be altered with an environment +variable, the config file will take precedence over environment variables. + +So, if the `leftDelim` setting is configured in 3 ways: + +```console +$ export GOMPLATE_LEFT_DELIM=:: +$ echo "leftDelim: ((" > .gomplate.yaml +$ gomplate --left-delim "<<" +``` + +The delimiter will be `<<`. + +## File format + +Currently, gomplate supports config files written in [YAML][] syntax, though other +structured formats may be supported in future (please [file an issue][] if this +is important to you!) + +Roughly all of the [command-line arguments][] are able to be set in a config +file, with the exception of `--help`, `--verbose`, and `--version`. Some +environment variable based settings not configurable on the command-line are +also supported in config files. + +Most of the configuration names are similar, though instead of using `kebab-case`, +multi-word names are rendered as `camelCase`. + +Here is an example of a simple config file: + +```yaml +inputDir: in/ +outputDir: out/ + +datasources: + local: + url: file:///tmp/data.json + remote: + url: https://example.com/api/v1/data + header: + Authorization: ["Basic aGF4MHI6c3dvcmRmaXNoCg=="] + +plugins: + dostuff: /usr/local/bin/stuff.sh +``` + +## `chmod` + +See [`--chmod`](../usage/#--chmod). + +Sets the output file mode. + +## `context` + +See [`--context`](../usage/#--context-c). + +Add data sources to the default context. This is a nested structure that +includes the URL for the data source and the optional HTTP header to send. + +For example: + +```yaml +context: + data: + url: https://example.com/api/v1/data + header: + Authorization: ["Basic aGF4MHI6c3dvcmRmaXNoCg=="] + stuff: + url: stuff.yaml +``` + +This adds two datasources to the context: `data` and `stuff`, and when the `data` +source is retrieved, an `Authorization` header will be sent with the given value. + +Note that the `.` name can also be used to set the entire context: + +```yaml +context: + .: + url: data.toml +``` + +## `datasources` + +See [`--datasource`](../usage/#--datasource-d). + +Define data sources. This is a nested structure that includes the URL for the data +source and the optional HTTP header to send. + +For example: + +```yaml +datasources: + data: + url: https://example.com/api/v1/data + header: + Authorization: ["Basic aGF4MHI6c3dvcmRmaXNoCg=="] + stuff: + url: stuff.yaml +``` + +This defines two datasources: `data` and `stuff`, and when the `data` +source is used, an `Authorization` header will be sent with the given value. + +## `excludes` + +See [`--exclude` and `--include`](../usage/#--exclude-and---include). + +This is an array of exclude patterns, used in conjunction with [`inputDir`](#inputdir). +Note that there is no `includes`, instead you can specify negative +exclusions by prefixing the patterns with `!`. + +```yaml +excludes: + - '*.txt' + - '!include-this.txt' +``` + +This will skip all files with the extension `.txt`, except for files named +`include-this.txt`, which will be processed. + +## `execPipe` + +See [`--exec-pipe`](../usage/#--exec-pipe). + +Use the rendered output as the [`postExec`](#postexec) command's standard input. + +Must be used in conjuction with [`postExec`](#postexec), and will override +any [`outputFiles`](#outputfiles) settings. + +## `in` + +See [`--in`/`-i`](../usage/#--file-f---in-i-and---out-o). + +Provide the input template inline. Note that unlike the `--in`/`-i` commandline +argument, there are no shell-imposed length limits. + +A simple example: +```yaml +in: hello to {{ .Env.USER }} +``` + +A multi-line example (see https://yaml-multiline.info/ for more about multi-line +string syntax in YAML): + +```yaml +in: | + A longer multi-line + document: + {{- range .foo }} + {{ .bar }} + {{ end }} +``` + +May not be used with `inputDir` or `inputFiles`. + +## `inputDir` + +See [`--input-dir`](../usage/#--input-dir-and---output-dir). + +The directory containing input template files. Must be used with +[`outputDir`](#outputdir) or [`outputMap`](#outputmap). Can also be used with [`excludes`](#excludes). + +```yaml +inputDir: templates/ +outputDir: out/ +``` + +May not be used with `in` or `inputFiles`. + +## `inputFiles` + +See [`--file`/`-f`](../usage/#--file-f---in-i-and---out-o). + +An array of input template paths. The special value `-` means `Stdin`. Multiple +values can be set, but there must be a corresponding number of `outputFiles` +entries present. + +```yaml +inputFiles: + - first.tmpl + - second.tmpl +outputFiles: + - first.out + - second.out +``` + +Flow style can be more compact: + +```yaml +inputFiles: ['-'] +outputFiles: ['-'] +``` + +May not be used with `in` or `inputDir`. + +## `leftDelim` + +See [`--left-delim`](../usage/#overriding-the-template-delimiters). + +Overrides the left template delimiter. + +```yaml +leftDelim: '%{' +``` + +## `outputDir` + +See [`--output-dir`](../usage/#--input-dir-and---output-dir). + +The directory to write rendered output files. Must be used with +[`inputDir`](#inputdir). + +```yaml +inputDir: templates/ +outputDir: out/ +``` + +May not be used with `outputFiles`. + +## `outputFiles` + +See [`--out`/`-o`](../usage/#--file-f---in-i-and---out-o). + +An array of output file paths. The special value `-` means `Stdout`. Multiple +values can be set, but there must be a corresponding number of `inputFiles` +entries present. + +```yaml +inputFiles: + - first.tmpl + - second.tmpl +outputFiles: + - first.out + - second.out +``` + +Can also be used with [`in`](#in): + +```yaml +in: >- + hello, + world! +outputFiles: [ hello.txt ] +``` + +May not be used with `inputDir`. + +## `outputMap` + +See [`--output-map`](../usage/#--output-map). + +Must be used with [`inputDir`](#inputdir). + +```yaml +inputDir: in/ +outputMap: | + out/{{ .in | strings.ReplaceAll ".yaml.tmpl" ".yaml" }} +``` + +## `plugins` + +See [`--plugin`](../usage/#--plugin). + +An mapping of key/value pairs to plug in custom functions for use in the templates. + +```yaml +in: '{{ "hello world" | figlet | lolcat }}' +plugins: + figlet: /usr/local/bin/figlet + lolcat: /home/hairyhenderson/go/bin/lolcat +``` + +## `pluginTimeout` + +See [`--plugin`](../usage/#--plugin). + +Sets the timeout for running plugins. By default, plugins will time out after 5 +seconds. This value can be set to override this default. The value must be +a valid [duration](../functions/time/#time-parseduration) such as `10s` or `3m`. + +```yaml +plugins: + figlet: /usr/local/bin/figlet +pluginTimeout: 500ms +``` + +## `postExec` + +See [post-template command execution](../usage/#post-template-command-execution). + +Configures a command to run after the template is rendered. + +See also [`execPipe`](#execpipe) for piping output directly into the `postExec` command. + +## `rightDelim` + +See [`--right-delim`](../usage/#overriding-the-template-delimiters). + +Overrides the right template delimiter. + +```yaml +rightDelim: '))' +``` + +## `suppressEmpty` + +See _[Suppressing empty output](../usage/#suppressing-empty-output)_ + +Suppresses empty output (i.e. output consisting of only whitespace). Can also be set with the `GOMPLATE_SUPPRESS_EMPTY` environment variable. + +```yaml +suppressEmpty: true +``` + +## `templates` + +See [`--template`/`-t`](../usage/#--template-t). + +An array of template references. Can be just a path or an alias and a path: + +```yaml +templates: + - t=foo/bar/helloworld.tmpl + - templatedir/ + - dir=foo/bar/ + - mytemplate.t +``` + +[command-line arguments]: ../usage +[file an issue]: https://github.com/hairyhenderson/gomplate/issues/new +[YAML]: http://yaml.org diff --git a/docs/content/datasources.md b/docs/content/datasources.md index 6f9872d5..6bf72172 100644 --- a/docs/content/datasources.md +++ b/docs/content/datasources.md @@ -1,6 +1,6 @@ --- title: Datasources -weight: 13 +weight: 14 menu: main --- diff --git a/docs/content/syntax.md b/docs/content/syntax.md index c7995217..14304eb3 100644 --- a/docs/content/syntax.md +++ b/docs/content/syntax.md @@ -1,6 +1,6 @@ --- title: Syntax -weight: 12 +weight: 13 menu: main --- diff --git a/docs/content/usage.md b/docs/content/usage.md index e5dc7d92..37501122 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -19,6 +19,23 @@ Hello, hairyhenderson ## Commandline Arguments +### `--config` + +Specify the path to a [gomplate config file](../config). The default is `.gomplate.yaml`. Can also be set with the `GOMPLATE_CONFIG` environment variable. + +For example: + +```console +$ cat myconfig.yaml +in: hello {{ .data.thing }} + +datasources: + data: + url: https://example.com/data.json +$ gomplate --config myconfig.yaml +hello world +``` + ### `--file`/`-f`, `--in`/`-i`, and `--out`/`-o` By default, `gomplate` will read from `Stdin` and write to `Stdout`. This behaviour can be changed. @@ -135,7 +152,7 @@ A few different forms are valid: - `mydata.json` - This form infers the name from the file name (without extension). Only valid for files in the current directory. -### `--context`/`c` +### `--context`/`-c` Add a data source in `name=URL` form, and make it available in the [default context][] as `.`. The special name `.` (period) can be used to override the entire default context. @@ -262,7 +279,7 @@ post-exec command. ## Suppressing empty output -Sometimes it can be desirable to suppress empty output (i.e. output consisting of only whitespace). To do so, set `GOMPLATE_SUPPRESS_EMPTY=true` in your environment: +Sometimes it can be desirable to suppress empty output (i.e. output consisting of only whitespace). To do so, set `suppressEmpty: true` in your [config][] file, or `GOMPLATE_SUPPRESS_EMPTY=true` in your environment: ```console $ export GOMPLATE_SUPPRESS_EMPTY=true @@ -273,5 +290,6 @@ cat: out: No such file or directory [default context]: ../syntax/#the-context [context]: ../syntax/#the-context +[config]: ../config/#suppressempty [external templates]: ../syntax/#external-templates [`.gitignore`]: https://git-scm.com/docs/gitignore diff --git a/docs/static/images/gomplate-gh.png b/docs/static/images/gomplate-gh.png new file mode 100644 index 0000000000000000000000000000000000000000..84d49c7755b96ba6629775de1fb480a122698bb8 GIT binary patch literal 71710 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 HPG0&S28eaC literal 0 HcmV?d00001 diff --git a/docs/static/images/gomplate-large.png b/docs/static/images/gomplate-large.png new file mode 100644 index 0000000000000000000000000000000000000000..3e8dd782bba1c16eb68d73b9fcecd265e5d35ddc GIT binary patch literal 64260 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 F{{z_(Q)>VK literal 0 HcmV?d00001 diff --git a/gomplate.go b/gomplate.go index d796c4dd..fe59c2ac 100644 --- a/gomplate.go +++ b/gomplate.go @@ -5,6 +5,7 @@ package gomplate import ( "bytes" "context" + "fmt" "io" "os" "path" @@ -14,7 +15,9 @@ import ( "time" "github.com/hairyhenderson/gomplate/v3/data" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/spf13/afero" ) @@ -109,42 +112,45 @@ func parseTemplateArg(templateArg string, ta templateAliases) error { // RunTemplates - run all gomplate templates specified by the given configuration func RunTemplates(o *Config) error { - return RunTemplatesWithContext(context.Background(), o) + cfg, err := o.toNewConfig() + if err != nil { + return err + } + return RunTemplatesWithContext(context.Background(), cfg) } // RunTemplatesWithContext - run all gomplate templates specified by the given configuration -func RunTemplatesWithContext(ctx context.Context, o *Config) error { +func RunTemplatesWithContext(ctx context.Context, cfg *config.Config) error { + log := zerolog.Ctx(ctx) + Metrics = newMetrics() defer runCleanupHooks() - // make sure config is sane - o.defaults() - ds := append(o.DataSources, o.Contexts...) - d, err := data.NewData(ds, o.DataSourceHeaders) - if err != nil { - return err - } + + d := data.FromConfig(cfg) + log.Debug().Str("data", fmt.Sprintf("%+v", d)).Msg("created data from config") + addCleanupHook(d.Cleanup) - nested, err := parseTemplateArgs(o.Templates) + nested, err := parseTemplateArgs(cfg.Templates) if err != nil { return err } - c, err := createTmplContext(o.Contexts, d) + c, err := createTmplContext(ctx, cfg.Context, d) if err != nil { return err } funcMap := Funcs(d) - err = bindPlugins(ctx, o.Plugins, funcMap) + err = bindPlugins(ctx, cfg, funcMap) if err != nil { return err } - g := newGomplate(funcMap, o.LDelim, o.RDelim, nested, c) + g := newGomplate(funcMap, cfg.LDelim, cfg.RDelim, nested, c) - return g.runTemplates(ctx, o) + return g.runTemplates(ctx, cfg) } -func (g *gomplate) runTemplates(ctx context.Context, o *Config) error { +func (g *gomplate) runTemplates(ctx context.Context, cfg *config.Config) error { start := time.Now() - tmpl, err := gatherTemplates(o, chooseNamer(o, g)) + tmpl, err := gatherTemplates(cfg, chooseNamer(cfg, g)) Metrics.GatherDuration = time.Since(start) if err != nil { Metrics.Errors++ @@ -166,11 +172,11 @@ func (g *gomplate) runTemplates(ctx context.Context, o *Config) error { return nil } -func chooseNamer(o *Config, g *gomplate) func(string) (string, error) { - if o.OutputMap == "" { - return simpleNamer(o.OutputDir) +func chooseNamer(cfg *config.Config, g *gomplate) func(string) (string, error) { + if cfg.OutputMap == "" { + return simpleNamer(cfg.OutputDir) } - return mappingNamer(o.OutputMap, g) + return mappingNamer(cfg.OutputMap, g) } func simpleNamer(outDir string) func(inPath string) (string, error) { diff --git a/internal/config/configfile.go b/internal/config/configfile.go new file mode 100644 index 00000000..eb9b713e --- /dev/null +++ b/internal/config/configfile.go @@ -0,0 +1,538 @@ +package config + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +var ( + // PluginTimeoutKey - context key for PluginTimeout - temporary! + PluginTimeoutKey = struct{}{} +) + +// 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, err + } + return out, nil +} + +// Config - +type Config struct { + Input string `yaml:"in,omitempty"` + InputFiles []string `yaml:"inputFiles,omitempty,flow"` + InputDir string `yaml:"inputDir,omitempty"` + ExcludeGlob []string `yaml:"excludes,omitempty"` + OutputFiles []string `yaml:"outputFiles,omitempty,flow"` + OutputDir string `yaml:"outputDir,omitempty"` + OutputMap string `yaml:"outputMap,omitempty"` + + SuppressEmpty bool `yaml:"suppressEmpty,omitempty"` + ExecPipe bool `yaml:"execPipe,omitempty"` + PostExec []string `yaml:"postExec,omitempty,flow"` + + OutMode string `yaml:"chmod,omitempty"` + LDelim string `yaml:"leftDelim,omitempty"` + RDelim string `yaml:"rightDelim,omitempty"` + DataSources DSources `yaml:"datasources,omitempty"` + Context DSources `yaml:"context,omitempty"` + Plugins map[string]string `yaml:"plugins,omitempty"` + PluginTimeout time.Duration `yaml:"pluginTimeout,omitempty"` + Templates []string `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.ReadWriter `yaml:"-"` + OutWriter io.Writer `yaml:"-"` +} + +// DSources - map of datasource configs +type DSources map[string]DSConfig + +func (d DSources) mergeFrom(o DSources) DSources { + for k, v := range o { + c, ok := d[k] + if ok { + d[k] = c.mergeFrom(v) + } else { + d[k] = v + } + } + return d +} + +// DSConfig - datasource config +type DSConfig 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 *DSConfig) UnmarshalYAML(value *yaml.Node) error { + type raw struct { + URL string + Header http.Header + } + r := raw{} + err := value.Decode(&r) + if err != nil { + return err + } + u, err := parseSourceURL(r.URL) + if err != nil { + return fmt.Errorf("could not parse datasource URL %q: %w", r.URL, err) + } + *d = DSConfig{ + 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 DSConfig) MarshalYAML() (interface{}, error) { + type raw struct { + URL string + Header http.Header + } + r := raw{ + URL: d.URL.String(), + Header: d.Header, + } + return r, nil +} + +func (d DSConfig) mergeFrom(o DSConfig) DSConfig { + 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 +} + +// 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.OutMode) { + c.OutMode = o.OutMode + } + if !isZero(o.LDelim) { + c.LDelim = o.LDelim + } + if !isZero(o.RDelim) { + c.RDelim = o.RDelim + } + if !isZero(o.Templates) { + c.Templates = o.Templates + } + c.DataSources.mergeFrom(o.DataSources) + c.Context.mergeFrom(o.Context) + if len(o.Plugins) > 0 { + for k, v := range o.Plugins { + c.Plugins[k] = v + } + } + + return c +} + +// ParseDataSourceFlags - sets the DataSources and Context fields from the +// key=value format flags as provided at the command-line +func (c *Config) ParseDataSourceFlags(datasources, contexts, headers []string) error { + for _, d := range datasources { + k, ds, err := parseDatasourceArg(d) + if err != nil { + return err + } + if c.DataSources == nil { + c.DataSources = DSources{} + } + c.DataSources[k] = ds + } + for _, d := range contexts { + k, ds, err := parseDatasourceArg(d) + if err != nil { + return err + } + if c.Context == nil { + c.Context = DSources{} + } + c.Context[k] = ds + } + + 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 len(hdrs) > 0 { + c.ExtraHeaders = hdrs + } + 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]string{} + } + c.Plugins[parts[0]] = parts[1] + } + return nil +} + +func parseDatasourceArg(value string) (key string, ds DSConfig, err error) { + parts := strings.SplitN(value, "=", 2) + if len(parts) == 1 { + f := parts[0] + key = strings.SplitN(value, ".", 2)[0] + if path.Base(f) != f { + err = fmt.Errorf("invalid datasource (%s): must provide an alias with files not in working directory", value) + return key, ds, err + } + ds.URL, err = absFileURL(f) + } else if len(parts) == 2 { + key = parts[0] + ds.URL, err = parseSourceURL(parts[1]) + } + return key, 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'") + } + } + + 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 - +func (c *Config) ApplyDefaults() { + 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.ExecPipe { + c.PostExecInput = &bytes.Buffer{} + c.OutWriter = c.PostExecInput + c.OutputFiles = []string{"-"} + } else { + c.PostExecInput = os.Stdin + c.OutWriter = os.Stdout + } + + if c.PluginTimeout == 0 { + c.PluginTimeout = 5 * time.Second + } +} + +// 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() +} + +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 !srcURL.IsAbs() { + srcURL, err = absFileURL(value) + if err != nil { + return nil, err + } + } + return srcURL, nil +} + +func absFileURL(value string) (*url.URL, error) { + wd, err := os.Getwd() + if err != nil { + return nil, errors.Wrapf(err, "can't get working directory") + } + wd = filepath.ToSlash(wd) + baseURL := &url.URL{ + Scheme: "file", + Path: wd + "/", + } + relURL, err := url.Parse(value) + if err != nil { + return nil, fmt.Errorf("can't parse value %s as URL: %w", value, err) + } + resolved := baseURL.ResolveReference(relURL) + // deal with Windows drive letters + if !strings.HasPrefix(wd, "/") && resolved.Path[2] == ':' { + resolved.Path = resolved.Path[1:] + } + return resolved, nil +} + +// 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 := os.FileMode(m) + if mode == 0 && c.Input != "" { + mode = 0644 + } + return mode, modeOverride, nil +} diff --git a/internal/config/configfile_test.go b/internal/config/configfile_test.go new file mode 100644 index 00000000..b5f3104a --- /dev/null +++ b/internal/config/configfile_test.go @@ -0,0 +1,558 @@ +package config + +import ( + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseConfigFile(t *testing.T) { + t.Parallel() + in := "in: hello world\n" + expected := &Config{ + Input: "hello world", + } + cf, err := Parse(strings.NewReader(in)) + assert.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 + +pluginTimeout: 2s +` + expected = &Config{ + Input: "hello world", + OutputFiles: []string{"out.txt"}, + DataSources: map[string]DSConfig{ + "data": { + URL: mustURL("file:///data.json"), + }, + "moredata": { + URL: mustURL("https://example.com/more.json"), + Header: map[string][]string{ + "Authorization": {"Bearer abcd1234"}, + }, + }, + }, + Context: map[string]DSConfig{ + ".": { + URL: mustURL("file:///data.json"), + }, + }, + OutMode: "644", + PluginTimeout: 2 * time.Second, + } + + cf, err = Parse(strings.NewReader(in)) + assert.NoError(t, err) + assert.EqualValues(t, expected, cf) +} + +func mustURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + // handle the case where it's a relative URL - just like in parseSourceURL. + if !u.IsAbs() { + u, err = absFileURL(s) + if err != nil { + panic(err) + } + } + return u +} + +func TestValidate(t *testing.T) { + t.Parallel() + assert.NoError(t, validateConfig("")) + + assert.Error(t, validateConfig(`in: foo +inputFiles: [bar] +`)) + assert.Error(t, validateConfig(`inputDir: foo +inputFiles: [bar] +`)) + assert.Error(t, validateConfig(`inputDir: foo +in: bar +`)) + + assert.Error(t, validateConfig(`outputDir: foo +outputFiles: [bar] +`)) + + assert.Error(t, validateConfig(`in: foo +outputFiles: [bar, baz] +`)) + + assert.Error(t, validateConfig(`inputFiles: [foo] +outputFiles: [bar, baz] +`)) + + assert.Error(t, validateConfig(`outputDir: foo +outputFiles: [bar] +`)) + + assert.Error(t, validateConfig(`outputDir: foo +`)) + + assert.Error(t, validateConfig(`outputMap: foo +`)) + + assert.Error(t, validateConfig(`outputMap: foo +outputFiles: [bar] +`)) + + assert.Error(t, validateConfig(`inputDir: foo +outputDir: bar +outputMap: bar +`)) + + assert.Error(t, validateConfig(`execPipe: true +`)) + assert.Error(t, validateConfig(`execPipe: true +postExec: "" +`)) + + assert.NoError(t, validateConfig(`execPipe: true +postExec: [echo, foo] +`)) + + assert.Error(t, validateConfig(`execPipe: true +outputFiles: [foo] +postExec: [echo] +`)) + + assert.NoError(t, validateConfig(`execPipe: true +inputFiles: ['-'] +postExec: [echo] +`)) + + assert.Error(t, validateConfig(`inputDir: foo +execPipe: true +outputDir: foo +postExec: [echo] +`)) + + assert.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]DSConfig{ + "data": { + URL: mustURL("file:///data.json"), + }, + "moredata": { + URL: mustURL("https://example.com/more.json"), + Header: http.Header{ + "Authorization": {"Bearer abcd1234"}, + }, + }, + }, + Context: map[string]DSConfig{ + "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]DSConfig{ + "data": { + Header: http.Header{ + "Accept": {"foo/bar"}, + }, + }, + }, + Context: map[string]DSConfig{ + "foo": { + Header: http.Header{ + "Accept": {"application/json"}, + }, + }, + "bar": {URL: mustURL("stdin:///")}, + }, + } + expected := &Config{ + Input: "hello world", + OutputFiles: []string{"out.txt"}, + DataSources: map[string]DSConfig{ + "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]DSConfig{ + "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]string{ + "sleep": "echo", + }, + PluginTimeout: 500 * time.Microsecond, + } + other = &Config{ + InputFiles: []string{"-"}, + OutputFiles: []string{"-"}, + Plugins: map[string]string{ + "sleep": "sleep.sh", + }, + } + expected = &Config{ + Input: "hello world", + OutputFiles: []string{"-"}, + Plugins: map[string]string{ + "sleep": "sleep.sh", + }, + PluginTimeout: 500 * time.Microsecond, + } + + assert.EqualValues(t, expected, cfg.MergeFrom(other)) +} + +func TestParseDataSourceFlags(t *testing.T) { + t.Parallel() + cfg := &Config{} + err := cfg.ParseDataSourceFlags(nil, nil, nil) + assert.NoError(t, err) + assert.EqualValues(t, &Config{}, cfg) + + cfg = &Config{} + err = cfg.ParseDataSourceFlags([]string{"foo/bar/baz.json"}, nil, nil) + assert.Error(t, err) + + cfg = &Config{} + err = cfg.ParseDataSourceFlags([]string{"baz=foo/bar/baz.json"}, nil, nil) + assert.NoError(t, err) + expected := &Config{ + DataSources: DSources{ + "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, + []string{"baz=Accept: application/json"}) + assert.NoError(t, err) + assert.EqualValues(t, &Config{ + DataSources: DSources{ + "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"}, + []string{"foo=Accept: application/json", + "bar=Authorization: Basic xxxxx"}) + assert.NoError(t, err) + assert.EqualValues(t, &Config{ + DataSources: DSources{ + "baz": {URL: mustURL("foo/bar/baz.json")}, + }, + Context: DSources{ + "foo": { + URL: mustURL("http://example.com"), + Header: http.Header{ + "Accept": {"application/json"}, + }, + }, + }, + ExtraHeaders: map[string]http.Header{ + "bar": {"Authorization": {"Basic xxxxx"}}, + }, + }, cfg) +} + +func TestParsePluginFlags(t *testing.T) { + t.Parallel() + cfg := &Config{} + err := cfg.ParsePluginFlags(nil) + assert.NoError(t, err) + + cfg = &Config{} + err = cfg.ParsePluginFlags([]string{"foo=bar"}) + assert.NoError(t, err) + assert.EqualValues(t, &Config{Plugins: map[string]string{"foo": "bar"}}, cfg) +} + +func TestConfigString(t *testing.T) { + c := &Config{} + c.ApplyDefaults() + + expected := `--- +inputFiles: ['-'] +outputFiles: ['-'] +leftDelim: '{{' +rightDelim: '}}' +pluginTimeout: 5s +` + assert.Equal(t, expected, c.String()) + + c = &Config{ + LDelim: "L", + RDelim: "R", + Input: "foo", + OutputFiles: []string{"-"}, + Templates: []string{"foo=foo.t", "bar=bar.t"}, + } + expected = `--- +in: foo +outputFiles: ['-'] +leftDelim: L +rightDelim: R +templates: +- foo=foo.t +- bar=bar.t +` + assert.Equal(t, expected, c.String()) + + c = &Config{ + LDelim: "L", + RDelim: "R", + Input: "long input that should be truncated", + OutputFiles: []string{"-"}, + Templates: []string{"foo=foo.t", "bar=bar.t"}, + } + expected = `--- +in: long inp... +outputFiles: ['-'] +leftDelim: L +rightDelim: R +templates: +- foo=foo.t +- bar=bar.t +` + assert.Equal(t, expected, c.String()) + + c = &Config{ + InputDir: "in/", + OutputDir: "out/", + } + expected = `--- +inputDir: in/ +outputDir: out/ +` + + assert.Equal(t, expected, c.String()) + + c = &Config{ + InputDir: "in/", + OutputMap: "{{ .in }}", + } + expected = `--- +inputDir: in/ +outputMap: '{{ .in }}' +` + + assert.Equal(t, expected, c.String()) + + c = &Config{ + PluginTimeout: 500 * time.Millisecond, + } + expected = `--- +pluginTimeout: 500ms +` + + assert.Equal(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() + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0), m) + assert.False(t, o) + + c = &Config{OutMode: "755"} + m, o, err = c.GetMode() + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0755), m) + assert.True(t, o) + + c = &Config{OutMode: "0755"} + m, o, err = c.GetMode() + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0755), m) + assert.True(t, o) + + c = &Config{OutMode: "foo"} + _, _, err = c.GetMode() + assert.Error(t, err) +} diff --git a/plugins.go b/plugins.go index 0699fb61..b4d56188 100644 --- a/plugins.go +++ b/plugins.go @@ -3,26 +3,26 @@ package gomplate import ( "bytes" "context" - "errors" "fmt" "os" "os/exec" "os/signal" "path/filepath" "runtime" - "strings" "text/template" "time" "github.com/hairyhenderson/gomplate/v3/conv" - "github.com/hairyhenderson/gomplate/v3/env" + "github.com/hairyhenderson/gomplate/v3/internal/config" ) -func bindPlugins(ctx context.Context, plugins []string, funcMap template.FuncMap) error { - for _, p := range plugins { - plugin, err := newPlugin(ctx, p) - if err != nil { - return err +func bindPlugins(ctx context.Context, cfg *config.Config, funcMap template.FuncMap) error { + for k, v := range cfg.Plugins { + plugin := &plugin{ + ctx: ctx, + name: k, + path: v, + timeout: cfg.PluginTimeout, } if _, ok := funcMap[plugin.name]; ok { return fmt.Errorf("function %q is already bound, and can not be overridden", plugin.name) @@ -35,23 +35,10 @@ func bindPlugins(ctx context.Context, plugins []string, funcMap template.FuncMap // plugin represents a custom function that binds to an external process to be executed type plugin struct { name, path string + timeout time.Duration ctx context.Context } -func newPlugin(ctx context.Context, value string) (*plugin, error) { - parts := strings.SplitN(value, "=", 2) - if len(parts) < 2 { - return nil, errors.New("plugin requires both name and path") - } - - p := &plugin{ - ctx: ctx, - name: parts[0], - path: parts[1], - } - return p, nil -} - // builds a command that's appropriate for running scripts // nolint: gosec func (p *plugin) buildCommand(a []string) (name string, args []string) { @@ -87,12 +74,7 @@ func (p *plugin) run(args ...interface{}) (interface{}, error) { name, a := p.buildCommand(a) - t, err := time.ParseDuration(env.Getenv("GOMPLATE_PLUGIN_TIMEOUT", "5s")) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(p.ctx, t) + ctx, cancel := context.WithTimeout(p.ctx, p.timeout) defer cancel() c := exec.CommandContext(ctx, name, a...) c.Stdin = nil @@ -112,7 +94,7 @@ func (p *plugin) run(args ...interface{}) (interface{}, error) { } }() start := time.Now() - err = c.Run() + err := c.Run() elapsed := time.Since(start) if ctx.Err() != nil { diff --git a/plugins_test.go b/plugins_test.go index 07e23259..d1292eaa 100644 --- a/plugins_test.go +++ b/plugins_test.go @@ -7,54 +7,48 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" -) - -func TestNewPlugin(t *testing.T) { - ctx := context.TODO() - in := "foo" - _, err := newPlugin(ctx, in) - assert.ErrorContains(t, err, "") - in = "foo=/bin/bar" - out, err := newPlugin(ctx, in) - assert.NilError(t, err) - assert.Equal(t, "foo", out.name) - assert.Equal(t, "/bin/bar", out.path) -} + "github.com/hairyhenderson/gomplate/v3/internal/config" +) func TestBindPlugins(t *testing.T) { ctx := context.TODO() fm := template.FuncMap{} - in := []string{} - err := bindPlugins(ctx, in, fm) + cfg := &config.Config{ + Plugins: map[string]string{}, + } + err := bindPlugins(ctx, cfg, fm) assert.NilError(t, err) assert.DeepEqual(t, template.FuncMap{}, fm) - in = []string{"foo=bar"} - err = bindPlugins(ctx, in, fm) + cfg.Plugins = map[string]string{"foo": "bar"} + err = bindPlugins(ctx, cfg, fm) assert.NilError(t, err) assert.Check(t, cmp.Contains(fm, "foo")) - err = bindPlugins(ctx, in, fm) + err = bindPlugins(ctx, cfg, fm) assert.ErrorContains(t, err, "already bound") } func TestBuildCommand(t *testing.T) { ctx := context.TODO() data := []struct { - plugin string - args []string - expected []string + name, path string + args []string + expected []string }{ - {"foo=foo", nil, []string{"foo"}}, - {"foo=foo", []string{"bar"}, []string{"foo", "bar"}}, - {"foo=foo.bat", nil, []string{"cmd.exe", "/c", "foo.bat"}}, - {"foo=foo.cmd", []string{"bar"}, []string{"cmd.exe", "/c", "foo.cmd", "bar"}}, - {"foo=foo.ps1", []string{"bar", "baz"}, []string{"pwsh", "-File", "foo.ps1", "bar", "baz"}}, + {"foo", "foo", nil, []string{"foo"}}, + {"foo", "foo", []string{"bar"}, []string{"foo", "bar"}}, + {"foo", "foo.bat", nil, []string{"cmd.exe", "/c", "foo.bat"}}, + {"foo", "foo.cmd", []string{"bar"}, []string{"cmd.exe", "/c", "foo.cmd", "bar"}}, + {"foo", "foo.ps1", []string{"bar", "baz"}, []string{"pwsh", "-File", "foo.ps1", "bar", "baz"}}, } for _, d := range data { - p, err := newPlugin(ctx, d.plugin) - assert.NilError(t, err) + p := &plugin{ + ctx: ctx, + name: d.name, + path: d.path, + } name, args := p.buildCommand(d.args) actual := append([]string{name}, args...) assert.DeepEqual(t, d.expected, actual) diff --git a/template.go b/template.go index 775b75cd..138625fd 100644 --- a/template.go +++ b/template.go @@ -9,10 +9,9 @@ import ( "path/filepath" "text/template" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/hairyhenderson/gomplate/v3/tmpl" - "github.com/hairyhenderson/gomplate/v3/conv" - "github.com/hairyhenderson/gomplate/v3/env" "github.com/pkg/errors" "github.com/spf13/afero" @@ -84,66 +83,65 @@ func (t *tplate) loadContents() (err error) { return err } -func (t *tplate) addTarget() (err error) { +func (t *tplate) addTarget(cfg *config.Config) (err error) { if t.name == "" && t.targetPath == "" { t.targetPath = "-" } if t.target == nil { - t.target, err = openOutFile(t.targetPath, t.mode, t.modeOverride) + t.target, err = openOutFile(cfg, t.targetPath, t.mode, t.modeOverride) } return err } // gatherTemplates - gather and prepare input template(s) and output file(s) for rendering // nolint: gocyclo -func gatherTemplates(o *Config, outFileNamer func(string) (string, error)) (templates []*tplate, err error) { - o.defaults() - mode, modeOverride, err := o.getMode() +func gatherTemplates(cfg *config.Config, outFileNamer func(string) (string, error)) (templates []*tplate, err error) { + mode, modeOverride, err := cfg.GetMode() if err != nil { return nil, err } // --exec-pipe redirects standard out to the out pipe - if o.Out != nil { - Stdout = &nopWCloser{o.Out} + if cfg.OutWriter != nil { + Stdout = &nopWCloser{cfg.OutWriter} } switch { // the arg-provided input string gets a special name - case o.Input != "": + case cfg.Input != "": templates = []*tplate{{ name: "", - contents: o.Input, + contents: cfg.Input, mode: mode, modeOverride: modeOverride, - targetPath: o.OutputFiles[0], + targetPath: cfg.OutputFiles[0], }} - case o.InputDir != "": + case cfg.InputDir != "": // input dirs presume output dirs are set too - templates, err = walkDir(o.InputDir, outFileNamer, o.ExcludeGlob, mode, modeOverride) + templates, err = walkDir(cfg.InputDir, outFileNamer, cfg.ExcludeGlob, mode, modeOverride) if err != nil { return nil, err } - case o.Input == "": - templates = make([]*tplate, len(o.InputFiles)) - for i := range o.InputFiles { - templates[i], err = fileToTemplates(o.InputFiles[i], o.OutputFiles[i], mode, modeOverride) + case cfg.Input == "": + templates = make([]*tplate, len(cfg.InputFiles)) + for i := range cfg.InputFiles { + templates[i], err = fileToTemplates(cfg.InputFiles[i], cfg.OutputFiles[i], mode, modeOverride) if err != nil { return nil, err } } } - return processTemplates(templates) + return processTemplates(cfg, templates) } -func processTemplates(templates []*tplate) ([]*tplate, error) { +func processTemplates(cfg *config.Config, templates []*tplate) ([]*tplate, error) { for _, t := range templates { if err := t.loadContents(); err != nil { return nil, err } - if err := t.addTarget(); err != nil { + if err := t.addTarget(cfg); err != nil { return nil, err } } @@ -229,8 +227,8 @@ func fileToTemplates(inFile, outFile string, mode os.FileMode, modeOverride bool return tmpl, nil } -func openOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { - if conv.ToBool(env.Getenv("GOMPLATE_SUPPRESS_EMPTY", "false")) { +func openOutFile(cfg *config.Config, filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { + if cfg.SuppressEmpty { out = newEmptySkipper(func() (io.WriteCloser, error) { if filename == "-" { return Stdout, nil diff --git a/template_test.go b/template_test.go index 49091776..5f10530d 100644 --- a/template_test.go +++ b/template_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -44,7 +45,8 @@ func TestOpenOutFile(t *testing.T) { fs = afero.NewMemMapFs() _ = fs.Mkdir("/tmp", 0777) - _, err := openOutFile("/tmp/foo", 0644, false) + cfg := &config.Config{} + _, err := openOutFile(cfg, "/tmp/foo", 0644, false) assert.NoError(t, err) i, err := fs.Stat("/tmp/foo") assert.NoError(t, err) @@ -53,7 +55,7 @@ func TestOpenOutFile(t *testing.T) { defer func() { Stdout = os.Stdout }() Stdout = &nopWCloser{&bytes.Buffer{}} - f, err := openOutFile("-", 0644, false) + f, err := openOutFile(cfg, "-", 0644, false) assert.NoError(t, err) assert.Equal(t, Stdout, f) } @@ -76,8 +78,9 @@ func TestAddTarget(t *testing.T) { defer func() { fs = origfs }() fs = afero.NewMemMapFs() + cfg := &config.Config{} tmpl := &tplate{name: "foo", targetPath: "/out/outfile"} - err := tmpl.addTarget() + err := tmpl.addTarget(cfg) assert.NoError(t, err) assert.NotNil(t, tmpl.target) } @@ -92,19 +95,23 @@ func TestGatherTemplates(t *testing.T) { afero.WriteFile(fs, "in/2", []byte("bar"), 0644) afero.WriteFile(fs, "in/3", []byte("baz"), 0644) - templates, err := gatherTemplates(&Config{}, nil) + cfg := &config.Config{} + cfg.ApplyDefaults() + templates, err := gatherTemplates(cfg, nil) assert.NoError(t, err) assert.Len(t, templates, 1) - templates, err = gatherTemplates(&Config{ + cfg = &config.Config{ Input: "foo", - }, nil) + } + cfg.ApplyDefaults() + templates, err = gatherTemplates(cfg, nil) assert.NoError(t, err) assert.Len(t, templates, 1) assert.Equal(t, "foo", templates[0].contents) assert.Equal(t, Stdout, templates[0].target) - templates, err = gatherTemplates(&Config{ + templates, err = gatherTemplates(&config.Config{ Input: "foo", OutputFiles: []string{"out"}, }, nil) @@ -117,7 +124,7 @@ func TestGatherTemplates(t *testing.T) { assert.Equal(t, os.FileMode(0644), info.Mode()) fs.Remove("out") - templates, err = gatherTemplates(&Config{ + templates, err = gatherTemplates(&config.Config{ InputFiles: []string{"foo"}, OutputFiles: []string{"out"}, }, nil) @@ -131,7 +138,7 @@ func TestGatherTemplates(t *testing.T) { assert.Equal(t, os.FileMode(0600), info.Mode()) fs.Remove("out") - templates, err = gatherTemplates(&Config{ + templates, err = gatherTemplates(&config.Config{ InputFiles: []string{"foo"}, OutputFiles: []string{"out"}, OutMode: "755", @@ -146,7 +153,7 @@ func TestGatherTemplates(t *testing.T) { assert.Equal(t, os.FileMode(0755), info.Mode()) fs.Remove("out") - templates, err = gatherTemplates(&Config{ + templates, err = gatherTemplates(&config.Config{ InputDir: "in", OutputDir: "out", }, simpleNamer("out")) @@ -168,6 +175,7 @@ func TestProcessTemplates(t *testing.T) { afero.WriteFile(fs, "existing", []byte(""), 0644) + cfg := &config.Config{} testdata := []struct { templates []*tplate contents []string @@ -221,7 +229,7 @@ func TestProcessTemplates(t *testing.T) { }, } for _, in := range testdata { - actual, err := processTemplates(in.templates) + actual, err := processTemplates(cfg, in.templates) assert.NoError(t, err) assert.Len(t, actual, len(in.templates)) for i, a := range actual { diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index 39ce824d..2c4005e1 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -74,7 +74,7 @@ func (s *BasicSuite) TestErrorsWithInputOutputImbalance(c *C) { }) result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "must provide same number of --out (1) as --file (2) options", + Err: "must provide same number of 'outputFiles' (1) as 'in' or 'inputFiles' (2) options", }) } @@ -114,37 +114,37 @@ func (s *BasicSuite) TestFlagRules(c *C) { result := icmd.RunCommand(GomplateBin, "-f", "-", "-i", "HELLO WORLD") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "only one of these flags is supported at a time: --in, --file, --input-dir", + Err: "only one of these options is supported at a time: 'in', 'inputFiles'", }) result = icmd.RunCommand(GomplateBin, "--output-dir", ".") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "--input-dir must be set when --output-dir is set", + Err: "these options must be set together: 'outputDir', 'inputDir'", }) result = icmd.RunCommand(GomplateBin, "--input-dir", ".", "--in", "param") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "only one of these flags is supported at a time: --in, --file, --input-dir", + Err: "only one of these options is supported at a time: 'in', 'inputDir'", }) result = icmd.RunCommand(GomplateBin, "--input-dir", ".", "--file", "input.txt") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "only one of these flags is supported at a time: --in, --file, --input-dir", + Err: "only one of these options is supported at a time: 'inputFiles', 'inputDir'", }) result = icmd.RunCommand(GomplateBin, "--output-dir", ".", "--out", "param") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "only one of these flags is supported at a time: --out, --output-dir, --output-map", + Err: "only one of these options is supported at a time: 'outputFiles', 'outputDir'", }) result = icmd.RunCommand(GomplateBin, "--output-map", ".", "--out", "param") result.Assert(c, icmd.Expected{ ExitCode: 1, - Err: "only one of these flags is supported at a time: --out, --output-dir, --output-map", + Err: "only one of these options is supported at a time: 'outputFiles', 'outputMap'", }) } diff --git a/tests/integration/config_test.go b/tests/integration/config_test.go new file mode 100644 index 00000000..e454dff6 --- /dev/null +++ b/tests/integration/config_test.go @@ -0,0 +1,227 @@ +//+build integration + +package integration + +import ( + "bytes" + "io/ioutil" + "os" + "runtime" + + "gopkg.in/check.v1" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/icmd" +) + +type ConfigSuite struct { + tmpDir *fs.Dir +} + +var _ = check.Suite(&ConfigSuite{}) + +func (s *ConfigSuite) SetUpTest(c *check.C) { + s.tmpDir = fs.NewDir(c, "gomplate-inttests", + fs.WithDir("indir"), + fs.WithDir("outdir"), + fs.WithFile(".gomplate.yaml", "in: hello world\n"), + fs.WithFile("sleep.sh", "#!/bin/sh\n\nexec sleep $1\n", fs.WithMode(0755)), + ) +} + +func (s *ConfigSuite) writeFile(f, content string) { + f = s.tmpDir.Join(f) + err := ioutil.WriteFile(f, []byte(content), 0600) + if err != nil { + panic(err) + } +} + +func (s *ConfigSuite) writeConfig(content string) { + s.writeFile(".gomplate.yaml", content) +} + +func (s *ConfigSuite) TearDownTest(c *check.C) { + s.tmpDir.Remove() +} + +func (s *ConfigSuite) TestReadsFromSimpleConfigFile(c *check.C) { + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello world"}) +} + +func (s *ConfigSuite) TestReadsStdin(c *check.C) { + s.writeConfig("inputFiles: [-]") + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + cmd.Stdin = bytes.NewBufferString("foo bar") + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "foo bar"}) +} + +func (s *ConfigSuite) TestFlagOverridesConfig(c *check.C) { + s.writeConfig("inputFiles: [in]") + result := icmd.RunCmd(icmd.Command(GomplateBin, "-i", "hello from the cli"), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello from the cli"}) +} + +func (s *ConfigSuite) TestReadsFromInputFile(c *check.C) { + s.writeConfig("inputFiles: [in]") + s.writeFile("in", "blah blah") + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "blah blah"}) +} + +func (s *ConfigSuite) TestDatasource(c *check.C) { + s.writeConfig(`inputFiles: [in] +datasources: + data: + url: in.yaml +`) + s.writeFile("in", `{{ (ds "data").value }}`) + s.writeFile("in.yaml", `value: hello world`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello world"}) +} + +func (s *ConfigSuite) TestOutputDir(c *check.C) { + s.writeConfig(`inputDir: indir/ +outputDir: outdir/ +datasources: + data: + url: in.yaml +`) + s.writeFile("indir/file", `{{ (ds "data").value }}`) + s.writeFile("in.yaml", `value: hello world`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + b, err := ioutil.ReadFile(s.tmpDir.Join("outdir", "file")) + assert.NilError(c, err) + assert.Equal(c, "hello world", string(b)) +} + +func (s *ConfigSuite) TestExecPipeOverridesConfigFile(c *check.C) { + // make sure exec-pipe works, and outFiles is replaced + s.writeConfig(`in: hello world +outputFiles: ['-'] +`) + result := icmd.RunCmd(icmd.Command(GomplateBin, "-i", "hi", "--exec-pipe", "--", "tr", "[a-z]", "[A-Z]"), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "HI"}) +} + +func (s *ConfigSuite) TestOutFile(c *check.C) { + s.writeConfig(`in: hello world +outputFiles: [out] +`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + b, err := ioutil.ReadFile(s.tmpDir.Join("out")) + assert.NilError(c, err) + assert.Equal(c, "hello world", string(b)) +} + +func (s *ConfigSuite) TestAlternateConfigFile(c *check.C) { + s.writeFile("config.yaml", `in: this is from an alternate config +`) + result := icmd.RunCmd(icmd.Command(GomplateBin, "--config=config.yaml"), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "this is from an alternate config"}) +} + +func (s *ConfigSuite) TestEnvConfigFile(c *check.C) { + s.writeFile("envconfig.yaml", `in: yet another alternate config +`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + cmd.Env = []string{"GOMPLATE_CONFIG=./envconfig.yaml"} + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "yet another alternate config"}) +} + +func (s *ConfigSuite) TestConfigOverridesEnvDelim(c *check.C) { + if runtime.GOOS != "windows" { + s.writeConfig(`inputFiles: [in] +leftDelim: (╯°□°)╯︵ ┻━┻ +datasources: + data: + url: in.yaml +`) + s.writeFile("in", `(╯°□°)╯︵ ┻━┻ (ds "data").value }}`) + s.writeFile("in.yaml", `value: hello world`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + cmd.Env = []string{"GOMPLATE_LEFT_DELIM", "<<"} + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello world"}) + } +} + +func (s *ConfigSuite) TestFlagOverridesAllDelim(c *check.C) { + if runtime.GOOS != "windows" { + s.writeConfig(`inputFiles: [in] +leftDelim: (╯°□°)╯︵ ┻━┻ +datasources: + data: + url: in.yaml +`) + s.writeFile("in", `{{ (ds "data").value }}`) + s.writeFile("in.yaml", `value: hello world`) + result := icmd.RunCmd(icmd.Command(GomplateBin, "--left-delim={{"), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + cmd.Env = []string{"GOMPLATE_LEFT_DELIM", "<<"} + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello world"}) + } +} + +func (s *ConfigSuite) TestConfigOverridesEnvPluginTimeout(c *check.C) { + if runtime.GOOS != "windows" { + s.writeConfig(`in: hi there {{ sleep 2 }} +plugins: + sleep: echo + +pluginTimeout: 500ms +`) + result := icmd.RunCmd(icmd.Command(GomplateBin, + "--plugin", "sleep="+s.tmpDir.Join("sleep.sh"), + ), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + cmd.Env = []string{"GOMPLATE_PLUGIN_TIMEOUT=5s"} + }) + result.Assert(c, icmd.Expected{ExitCode: 1, Err: "plugin timed out"}) + } +} + +func (s *ConfigSuite) TestConfigOverridesEnvSuppressEmpty(c *check.C) { + s.writeConfig(`in: | + {{- print "\t \n\n\r\n\t\t \v\n" -}} + + {{ print " " -}} +out: ./missing +suppressEmpty: true +`) + result := icmd.RunCmd(icmd.Command(GomplateBin), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + // should have no effect, as config overrides + cmd.Env = []string{"GOMPLATE_SUPPRESS_EMPTY=false"} + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + _, err := os.Stat(s.tmpDir.Join("missing")) + assert.Equal(c, true, os.IsNotExist(err)) +} diff --git a/tests/integration/tmpl_test.go b/tests/integration/tmpl_test.go index 4669df29..93e27148 100644 --- a/tests/integration/tmpl_test.go +++ b/tests/integration/tmpl_test.go @@ -96,7 +96,6 @@ func (s *TmplSuite) TestExec(c *C) { }) result.Assert(c, icmd.Expected{ExitCode: 0}) assert.Equal(c, "", result.Stdout()) - assert.Equal(c, "", result.Stderr()) out, err := ioutil.ReadFile(s.tmpDir.Join("out", "users", "config.json")) assert.NilError(c, err)