Skip to content

Commit

Permalink
Support a config file to use instead of commandline arguments
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Henderson <[email protected]>
  • Loading branch information
hairyhenderson committed May 3, 2020
1 parent 31de929 commit bfff1d8
Show file tree
Hide file tree
Showing 30 changed files with 2,506 additions and 444 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
253 changes: 253 additions & 0 deletions cmd/gomplate/config.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit bfff1d8

Please sign in to comment.