feat: better display config templating issues by olblak · Pull Request #10132 · updatecli/updatecli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions pkg/core/config/main.go
35 changes: 32 additions & 3 deletions pkg/core/config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,64 @@ type Template struct {
Values map[string]interface{} `yaml:"-,inline"`
// Secrets contains key/value extracted from a sops file
Secrets map[string]interface{} `yaml:"-,inline"`
// Fragments describes which file each line of the content handed to
// NewStringTemplate comes from, so that templating errors can be reported
// against the file the user wrote rather than against the concatenation of
// every partial and the manifest.
Fragments []manifestFragment
// fs is a file system abstraction used to read files
fs fs.FS
}

// templateName returns the name given to the golang template, which text/template
// uses to prefix the location of parse and execution errors.
func (t *Template) templateName() string {
if t.CfgFile == "" {
return "cfg"
}

return t.CfgFile
}

// NewStringTemplate parses a golang template then return an updatecli configuration as a struct
func (t *Template) NewStringTemplate(content []byte) ([]byte, error) {
templateValues, err := t.readAllValues()
if err != nil {
return []byte{}, err
}

tmpl, err := template.New("cfg").
tmpl, err := template.New(t.templateName()).
Funcs(sprig.FuncMap()).
Funcs(helmFuncMap()). // add helm funcMap
Funcs(updatecliFuncMap()). // add custom funcMap last so that it takes precedence
Parse(string(content))
if err != nil {
return []byte{}, err
return []byte{}, t.locateError(err, content)
}

b := bytes.Buffer{}

if err := tmpl.Execute(&b, templateValues); err != nil {
return []byte{}, err
return []byte{}, t.locateError(err, content)
}

return b.Bytes(), nil
}

// locateError points a templating error at the file and line it came from, and
// appends an excerpt of the offending lines. It returns the error unchanged when
// the location cannot be resolved.
func (t *Template) locateError(err error, content []byte) error {
location, located := locateTemplateError(err, t.templateName(), t.Fragments)

excerpt := renderExcerpt(content, location, excerptContextLines)
if excerpt == "" {
return located
}

return fmt.Errorf("%w\n\n%s", located, strings.TrimRight(excerpt, "\n"))
}

// NewCueTemplate parses a Cue template then return an updatecli configuration as a struct
func (t *Template) NewCueTemplate(content []byte) (cue.Value, error) {
ctx := cuecontext.New()
Expand Down
15 changes: 14 additions & 1 deletion pkg/core/config/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type TestTemplateData struct {
ExpectedManifest string
// ExpectError is used to check if the test should return an error
ExpectError bool
// ExpectedError, when set, must be contained in the returned error
ExpectedError string
}

var (
Expand Down Expand Up @@ -61,6 +63,9 @@ var (
hello {{ .value }
`,
ExpectError: true,
// The template is named after the manifest it came from, so that the
// error points at a file the user can open.
ExpectedError: `manifest.yaml:2: unexpected "}" in operand`,
},

{
Expand All @@ -80,17 +85,25 @@ func TestTemplates(t *testing.T) {
for _, testTemplate := range testTemplates {
t.Run(fmt.Sprintf("test template %s", testTemplate.ID), func(t *testing.T) {

manifest := dedent.Dedent(testTemplate.ManifestTemplate)
fragments, _ := appendFragment(nil, "manifest.yaml", []byte(manifest))

template := Template{
CfgFile: "manifest.yaml",
Fragments: fragments,
ValuesFiles: []string{"values1.yml", "values2.yml"},
fs: fstest.MapFS{
"values1.yml": {Data: []byte(testTemplate.Values1)},
"values2.yml": {Data: []byte(testTemplate.Values2)},
},
}
expected := dedent.Dedent(testTemplate.ExpectedManifest)
rendered, err := template.NewStringTemplate([]byte(dedent.Dedent(testTemplate.ManifestTemplate)))
rendered, err := template.NewStringTemplate([]byte(manifest))
if testTemplate.ExpectError {
require.Error(t, err)
if testTemplate.ExpectedError != "" {
require.Contains(t, err.Error(), testTemplate.ExpectedError)
}
return
}
require.NoError(t, err)
Expand Down
171 changes: 171 additions & 0 deletions pkg/core/config/templateerror.go
Loading
Loading