1// Package gomplate is a template renderer which supports a number of datasources, 2// and includes hundreds of built-in functions. 3package gomplate 4 5import ( 6 "bytes" 7 "context" 8 "fmt" 9 "io" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14 "text/template" 15 "time" 16 17 "github.com/hairyhenderson/gomplate/v3/data" 18 "github.com/hairyhenderson/gomplate/v3/internal/config" 19 "github.com/pkg/errors" 20 "github.com/rs/zerolog" 21 "github.com/spf13/afero" 22) 23 24// gomplate - 25type gomplate struct { 26 funcMap template.FuncMap 27 leftDelim string 28 rightDelim string 29 nestedTemplates templateAliases 30 rootTemplate *template.Template 31 tmplctx interface{} 32} 33 34// runTemplate - 35func (g *gomplate) runTemplate(_ context.Context, t *tplate) error { 36 tmpl, err := t.toGoTemplate(g) 37 if err != nil { 38 return err 39 } 40 41 // nolint: gocritic 42 switch t.target.(type) { 43 case io.Closer: 44 if t.target != os.Stdout { 45 // nolint: errcheck 46 defer t.target.(io.Closer).Close() 47 } 48 } 49 err = tmpl.Execute(t.target, g.tmplctx) 50 return err 51} 52 53type templateAliases map[string]string 54 55// newGomplate - 56func newGomplate(funcMap template.FuncMap, leftDelim, rightDelim string, nested templateAliases, tctx interface{}) *gomplate { 57 return &gomplate{ 58 leftDelim: leftDelim, 59 rightDelim: rightDelim, 60 funcMap: funcMap, 61 nestedTemplates: nested, 62 tmplctx: tctx, 63 } 64} 65 66func parseTemplateArgs(templateArgs []string) (templateAliases, error) { 67 nested := templateAliases{} 68 for _, templateArg := range templateArgs { 69 err := parseTemplateArg(templateArg, nested) 70 if err != nil { 71 return nil, err 72 } 73 } 74 return nested, nil 75} 76 77func parseTemplateArg(templateArg string, ta templateAliases) error { 78 parts := strings.SplitN(templateArg, "=", 2) 79 pth := parts[0] 80 alias := "" 81 if len(parts) > 1 { 82 alias = parts[0] 83 pth = parts[1] 84 } 85 86 switch fi, err := fs.Stat(pth); { 87 case err != nil: 88 return err 89 case fi.IsDir(): 90 files, err := afero.ReadDir(fs, pth) 91 if err != nil { 92 return err 93 } 94 prefix := pth 95 if alias != "" { 96 prefix = alias 97 } 98 for _, f := range files { 99 if !f.IsDir() { // one-level only 100 ta[path.Join(prefix, f.Name())] = path.Join(pth, f.Name()) 101 } 102 } 103 default: 104 if alias != "" { 105 ta[alias] = pth 106 } else { 107 ta[pth] = pth 108 } 109 } 110 return nil 111} 112 113// RunTemplates - run all gomplate templates specified by the given configuration 114// 115// Deprecated: use Run instead 116func RunTemplates(o *Config) error { 117 cfg, err := o.toNewConfig() 118 if err != nil { 119 return err 120 } 121 return Run(context.Background(), cfg) 122} 123 124// Run all gomplate templates specified by the given configuration 125func Run(ctx context.Context, cfg *config.Config) error { 126 log := zerolog.Ctx(ctx) 127 128 Metrics = newMetrics() 129 defer runCleanupHooks() 130 131 d := data.FromConfig(ctx, cfg) 132 log.Debug().Str("data", fmt.Sprintf("%+v", d)).Msg("created data from config") 133 134 addCleanupHook(d.Cleanup) 135 nested, err := parseTemplateArgs(cfg.Templates) 136 if err != nil { 137 return err 138 } 139 c, err := createTmplContext(ctx, cfg.Context, d) 140 if err != nil { 141 return err 142 } 143 funcMap := CreateFuncs(ctx, d) 144 err = bindPlugins(ctx, cfg, funcMap) 145 if err != nil { 146 return err 147 } 148 g := newGomplate(funcMap, cfg.LDelim, cfg.RDelim, nested, c) 149 150 return g.runTemplates(ctx, cfg) 151} 152 153func (g *gomplate) runTemplates(ctx context.Context, cfg *config.Config) error { 154 start := time.Now() 155 tmpl, err := gatherTemplates(cfg, chooseNamer(cfg, g)) 156 Metrics.GatherDuration = time.Since(start) 157 if err != nil { 158 Metrics.Errors++ 159 return err 160 } 161 Metrics.TemplatesGathered = len(tmpl) 162 start = time.Now() 163 defer func() { Metrics.TotalRenderDuration = time.Since(start) }() 164 for _, t := range tmpl { 165 tstart := time.Now() 166 err := g.runTemplate(ctx, t) 167 Metrics.RenderDuration[t.name] = time.Since(tstart) 168 if err != nil { 169 Metrics.Errors++ 170 return err 171 } 172 Metrics.TemplatesProcessed++ 173 } 174 return nil 175} 176 177func chooseNamer(cfg *config.Config, g *gomplate) func(string) (string, error) { 178 if cfg.OutputMap == "" { 179 return simpleNamer(cfg.OutputDir) 180 } 181 return mappingNamer(cfg.OutputMap, g) 182} 183 184func simpleNamer(outDir string) func(inPath string) (string, error) { 185 return func(inPath string) (string, error) { 186 outPath := filepath.Join(outDir, inPath) 187 return filepath.Clean(outPath), nil 188 } 189} 190 191func mappingNamer(outMap string, g *gomplate) func(string) (string, error) { 192 return func(inPath string) (string, error) { 193 out := &bytes.Buffer{} 194 t := &tplate{ 195 name: "<OutputMap>", 196 contents: outMap, 197 target: out, 198 } 199 tpl, err := t.toGoTemplate(g) 200 if err != nil { 201 return "", err 202 } 203 tctx := &tmplctx{} 204 // nolint: gocritic 205 switch c := g.tmplctx.(type) { 206 case *tmplctx: 207 for k, v := range *c { 208 if k != "in" && k != "ctx" { 209 (*tctx)[k] = v 210 } 211 } 212 } 213 (*tctx)["ctx"] = g.tmplctx 214 (*tctx)["in"] = inPath 215 216 err = tpl.Execute(t.target, tctx) 217 if err != nil { 218 return "", errors.Wrapf(err, "failed to render outputMap with ctx %+v and inPath %s", tctx, inPath) 219 } 220 221 return filepath.Clean(strings.TrimSpace(out.String())), nil 222 } 223} 224