From 97f2f4b71d6d4994e722467c9839b8bd4ac347c9 Mon Sep 17 00:00:00 2001 From: "Radek Schekalla (SAP)" Date: Thu, 6 Nov 2025 09:52:25 +0100 Subject: [PATCH 1/3] feat: check for delimiter config in templates, parse if found, apply delims and cleanup template On-behalf-of: Radek Schekalla (SAP) Signed-off-by: Radek Schekalla (SAP) --- internal/template/delimiter.go | 81 +++++++++++++++++++++++++++++ internal/template/delimiter_test.go | 60 +++++++++++++++++++++ internal/template/template.go | 8 ++- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 internal/template/delimiter.go create mode 100644 internal/template/delimiter_test.go diff --git a/internal/template/delimiter.go b/internal/template/delimiter.go new file mode 100644 index 0000000..a9171bc --- /dev/null +++ b/internal/template/delimiter.go @@ -0,0 +1,81 @@ +package template + +import ( + "encoding/json" + "errors" + "strings" +) + +const ( + prefixBootstrap = "#?bootstrap" + defaultStart = "{{" + defaultEnd = "}}" +) + +type Delimiter struct { + Start string + End string +} + +type DelimiterJson struct { + Template struct { + Delims struct { + Start string `json:"start"` + End string `json:"end"` + } `json:"delims"` + } `json:"template"` +} + +type DelimiterConfig struct { + Template string +} + +func NewDelimiterConfig(template string) *DelimiterConfig { + return &DelimiterConfig{ + Template: template, + } +} + +// ParseAndCleanup parses the delimiter configuration from the template string +// and returns the cleaned-up template string along with the Delimiter, or an error if parsing fails. +// It always returns a Delimiter, defaulting to "{{" and "}}" if no configuration is found. +// Delimiter configuration string needs to be in the first line of the template, e.g.: +// #?bootstrap { "template": { "delims": { "start": "[[", "end": "]]" } } +func (d *DelimiterConfig) ParseAndCleanup() (string, *Delimiter, error) { + if !strings.HasPrefix(d.Template, prefixBootstrap) { + return d.Template, &Delimiter{Start: defaultStart, End: defaultEnd}, nil + } + + template := strings.TrimSpace(d.Template) + firstLineEnd := strings.Index(template, "\n") + if firstLineEnd == -1 { + firstLineEnd = len(template) + } + + jsonPart := strings.TrimSpace(strings.TrimPrefix(template[:firstLineEnd], prefixBootstrap)) + if jsonPart == "" { + return "", nil, errors.New("invalid template delimiter configuration") + } + var config DelimiterJson + if err := json.Unmarshal([]byte(jsonPart), &config); err != nil { + return "", nil, errors.New("cannot parse detected template delimiter configuration") + } + + template = removeBootstrapConfig(template) + + return template, &Delimiter{Start: config.Template.Delims.Start, End: config.Template.Delims.End}, nil +} + +// removeBootstrapConfig removes the bootstrap configuration line from the template string. +func removeBootstrapConfig(template string) string { + lines := strings.Split(template, "\n") + var cleanLines []string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, prefixBootstrap) { + cleanLines = append(cleanLines, line) + } + } + return strings.Join(cleanLines, "\n") +} diff --git a/internal/template/delimiter_test.go b/internal/template/delimiter_test.go new file mode 100644 index 0000000..ccb01f2 --- /dev/null +++ b/internal/template/delimiter_test.go @@ -0,0 +1,60 @@ +package template + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDelimiterConfig_parse(t *testing.T) { + tests := []struct { + name string + template string + wantTemplate string + wantDelimiter *Delimiter + wantErr assert.ErrorAssertionFunc + }{ + { + "Valid Delimiter Configuration", + `#?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}}} +apiVersion: v1`, + "apiVersion: v1", + &Delimiter{Start: "<<", End: ">>"}, + assert.NoError, + }, + { + "Invalid JSON Configuration", + `#?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}`, + `#?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}`, + nil, + assert.Error, + }, + { + "Missing JSON Configuration", + `#?bootstrap`, + `#?bootstrap`, + nil, + assert.Error, + }, + { + "No Delimiter Configuration", + `apiVersion: v1 +kind: Pod`, + `apiVersion: v1 +kind: Pod`, + &Delimiter{"{{", "}}"}, + assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + template, delim, err := NewDelimiterConfig(tt.template).ParseAndCleanup() + if !tt.wantErr(t, err) { + return + } + assert.Equalf(t, tt.wantTemplate, template, "template") + assert.Equalf(t, tt.wantDelimiter, delim, "delimiter") + + }) + } +} diff --git a/internal/template/template.go b/internal/template/template.go index 13c9627..04e04ae 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -307,12 +307,18 @@ func (t *TemplateExecution) WithOCMComponentGetter(ctx context.Context, compGett func (t *TemplateExecution) Execute(name, template string, input map[string]interface{}) ([]byte, error) { tmpl := gotmpl.New(name) + template, delims, err := NewDelimiterConfig(template).ParseAndCleanup() + if err != nil { + return nil, err + } + tmpl = tmpl.Delims(delims.Start, delims.End) + for _, fm := range t.funcMaps { tmpl.Funcs(fm) } tmpl.Option("missingkey=" + t.missingKeyOption) - _, err := tmpl.Parse(template) + _, err = tmpl.Parse(template) if err != nil { return nil, TemplateErrorBuilder(err).WithSource(&template).WithInput(input, t.templateInputFormatter).Build() } From 5ca626490a3fc7ae7c48cbf7f9e980ffb82457a6 Mon Sep 17 00:00:00 2001 From: "Radek Schekalla (SAP)" Date: Thu, 6 Nov 2025 10:39:41 +0100 Subject: [PATCH 2/3] fix: tests, adjust comment On-behalf-of: Radek Schekalla (SAP) Signed-off-by: Radek Schekalla (SAP) --- internal/template/delimiter.go | 2 +- internal/template/delimiter_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/template/delimiter.go b/internal/template/delimiter.go index a9171bc..c7a9fc6 100644 --- a/internal/template/delimiter.go +++ b/internal/template/delimiter.go @@ -40,7 +40,7 @@ func NewDelimiterConfig(template string) *DelimiterConfig { // and returns the cleaned-up template string along with the Delimiter, or an error if parsing fails. // It always returns a Delimiter, defaulting to "{{" and "}}" if no configuration is found. // Delimiter configuration string needs to be in the first line of the template, e.g.: -// #?bootstrap { "template": { "delims": { "start": "[[", "end": "]]" } } +// #?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}}} func (d *DelimiterConfig) ParseAndCleanup() (string, *Delimiter, error) { if !strings.HasPrefix(d.Template, prefixBootstrap) { return d.Template, &Delimiter{Start: defaultStart, End: defaultEnd}, nil diff --git a/internal/template/delimiter_test.go b/internal/template/delimiter_test.go index ccb01f2..6c8d089 100644 --- a/internal/template/delimiter_test.go +++ b/internal/template/delimiter_test.go @@ -25,14 +25,14 @@ apiVersion: v1`, { "Invalid JSON Configuration", `#?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}`, - `#?bootstrap {"template": {"delims": {"start": "<<", "end": ">>"}`, + "", nil, assert.Error, }, { "Missing JSON Configuration", `#?bootstrap`, - `#?bootstrap`, + "", nil, assert.Error, }, From c1fd44bcf8c5f1391e1e907f9e82ccc1d30c7d93 Mon Sep 17 00:00:00 2001 From: "Radek Schekalla (SAP)" Date: Thu, 6 Nov 2025 10:55:36 +0100 Subject: [PATCH 3/3] chore: bump to v0.7.0 On-behalf-of: Radek Schekalla (SAP) Signed-off-by: Radek Schekalla (SAP) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3f61b55..e7f5d1a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.6.1-dev \ No newline at end of file +v0.7.0 \ No newline at end of file