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