Fix newline handling in file, softlink, and ImportLink template functions

Use state-based routing instead of sentinel strings, so whitespace-trimming
syntax (e.g. `{{- file "name" -}}`) correctly creates all named files and
symlinks.

Fixes: #2118

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
Co-authored-by: Christian Goll <cgoll@suse.com>
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2026-04-17 15:57:19 -06:00
parent eaac55a97a
commit 2bad789f4f
28 changed files with 767 additions and 256 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"path"
"strings"
"syscall"
"github.com/swaggest/usecase"
@@ -148,12 +149,46 @@ func (of *OverlayFile) renderContents(nodeName string) (string, error) {
}
tstruct.BuildSource = of.Path
buffer, _, _, renderErr := overlay.RenderTemplateFile(of.FullPath(), tstruct)
rendered, renderErr := overlay.RenderTemplate(of.FullPath(), tstruct)
if renderErr != nil {
return "", renderErr
}
return buffer.String(), nil
if !rendered.WriteFile {
// abort() was called in the template: nothing would be written to disk.
// Include any content rendered before the abort() call for debugging.
return rendered.Files[0].Buffer.String() + "Aborted\n", nil
}
// Single output file with no file() calls: return content as-is for backward
// compatibility. The default slot has Name == ""; named entries always have a
// non-empty Name regardless of how many there are.
if len(rendered.Files) == 1 && rendered.Files[0].Name == "" {
if rendered.Files[0].IsSymlink {
return "Symlink: " + rendered.Files[0].Target + "\n", nil
}
return rendered.Files[0].Buffer.String(), nil
}
// One or more file() calls: emit Filename: / Symlink: headers for each named
// entry. Skip the default slot (Name == ""); pre-file() content in it is
// for RenderTemplateFile callers, not serialized here.
var combined strings.Builder
for _, f := range rendered.Files {
if f.Name == "" {
continue
}
combined.WriteString("Filename: ")
combined.WriteString(f.Name)
combined.WriteString("\n")
if f.IsSymlink {
combined.WriteString("Symlink: ")
combined.WriteString(f.Target)
combined.WriteString("\n")
} else {
combined.WriteString(f.Buffer.String())
}
}
return combined.String(), nil
}
func NewOverlayFile(name string, path string, renderNodeName string) (*OverlayFile, error) {

View File

@@ -18,6 +18,29 @@ import (
const sampleTemplate = `{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }}
`
const multiFileTemplate = `{{- range $name := list "alpha" "beta" }}
{{- file $name -}}
content of {{ $name }}
{{ end -}}`
const abortTemplate = `{{- abort -}}`
const abortWithContentTemplate = `some content
{{- abort -}}`
const symlinkTemplate = `{{- softlink "/usr/share/zoneinfo/UTC" -}}`
const multiSymlinkTemplate = `{{- file "link1" -}}
{{- softlink "/target1" -}}
{{- file "link2" -}}
{{- softlink "/target2" -}}`
const sampleNodesConf = `
nodeprofiles: {}
nodes:
node1: {}
`
var overlayTests = []struct {
name string
initFiles map[string]string
@@ -106,6 +129,96 @@ var overlayTests = []struct {
},
response: `{"test":{"files":null, "site":true},"testoverlay":{"files":["/email.ww"], "site":true}}`,
},
{
name: "render multi-file overlay template",
initFiles: map[string]string{
"/usr/share/warewulf/overlays/multioverlay/multi.ww": multiFileTemplate,
"/etc/warewulf/nodes.conf": sampleNodesConf,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/multioverlay/file?path=multi.ww&render=node1", nil)
},
response: `{
"overlay": "multioverlay",
"path": "multi.ww",
"contents": "Filename: alpha\ncontent of alpha\nFilename: beta\ncontent of beta\n",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`,
},
{
name: "render aborted template",
initFiles: map[string]string{
"/usr/share/warewulf/overlays/multioverlay/abort.ww": abortTemplate,
"/etc/warewulf/nodes.conf": sampleNodesConf,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/multioverlay/file?path=abort.ww&render=node1", nil)
},
response: `{
"overlay": "multioverlay",
"path": "abort.ww",
"contents": "Aborted\n",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`,
},
{
name: "render aborted template with pre-abort content",
initFiles: map[string]string{
"/usr/share/warewulf/overlays/multioverlay/abort-content.ww": abortWithContentTemplate,
"/etc/warewulf/nodes.conf": sampleNodesConf,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/multioverlay/file?path=abort-content.ww&render=node1", nil)
},
response: `{
"overlay": "multioverlay",
"path": "abort-content.ww",
"contents": "some contentAborted\n",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`,
},
{
name: "render single-file symlink template",
initFiles: map[string]string{
"/usr/share/warewulf/overlays/multioverlay/symlink.ww": symlinkTemplate,
"/etc/warewulf/nodes.conf": sampleNodesConf,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/multioverlay/file?path=symlink.ww&render=node1", nil)
},
response: `{
"overlay": "multioverlay",
"path": "symlink.ww",
"contents": "Symlink: /usr/share/zoneinfo/UTC\n",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`,
},
{
name: "render multi-file symlink template",
initFiles: map[string]string{
"/usr/share/warewulf/overlays/multioverlay/multi-symlink.ww": multiSymlinkTemplate,
"/etc/warewulf/nodes.conf": sampleNodesConf,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/multioverlay/file?path=multi-symlink.ww&render=node1", nil)
},
response: `{
"overlay": "multioverlay",
"path": "multi-symlink.ww",
"contents": "Filename: link1\nSymlink: /target1\nFilename: link2\nSymlink: /target2\n",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`,
},
{
name: "delete overlay file",
initFiles: map[string]string{