83 changes: 83 additions & 0 deletions util/template/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package template

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

type SimpleValue struct {
Value string `json:"value,omitempty"`
}

func processTemplate(t *testing.T, tmpl SimpleValue) SimpleValue {
tmplBytes, err := json.Marshal(tmpl)
assert.NoError(t, err)
r, err := Replace(string(tmplBytes), map[string]string{}, true)
assert.NoError(t, err)
var newTmpl SimpleValue
err = json.Unmarshal([]byte(r), &newTmpl)
assert.NoError(t, err)
return newTmpl
}

func Test_Template_Replace(t *testing.T) {
t.Run("ExpressionWithEscapedCharacters", func(t *testing.T) {
t.Run("SingleQuotes", func(t *testing.T) {
tmpl := SimpleValue{Value: "{{='test'}}"}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, "test", newTmpl.Value)
})
t.Run("DoubleQuotes", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{="test"}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, "test", newTmpl.Value)
})
t.Run("EscapedBackslashInString", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{='some\\path\\with\\backslashes'}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `some\path\with\backslashes`, newTmpl.Value)
})
t.Run("EscapedNewlineInString", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{='some\nstring\nwith\nescaped\nnewlines'}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, "some\nstring\nwith\nescaped\nnewlines", newTmpl.Value)
})
t.Run("Newline", func(t *testing.T) {
tmpl := SimpleValue{Value: "{{=1 + \n1}}"}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, "2", newTmpl.Value)
})
t.Run("StringAsJson", func(t *testing.T) {
tmpl := SimpleValue{Value: "{{=toJson('test')}}"}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `"test"`, newTmpl.Value)
})
t.Run("ObjectAsJson", func(t *testing.T) {
tmpl := SimpleValue{Value: "{{=toJson({test: 1})}}"}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `{"test":1}`, newTmpl.Value)
})
t.Run("ArrayAsJson", func(t *testing.T) {
tmpl := SimpleValue{Value: "{{=toJson([1, '2', {an: 'object'}])}}"}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `[1,"2",{"an":"object"}]`, newTmpl.Value)
})
t.Run("SingleQuoteAsString", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{="'"}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `'`, newTmpl.Value)
})
t.Run("DoubleQuoteAsString", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{='"'}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, `"`, newTmpl.Value)
})
t.Run("Boolean", func(t *testing.T) {
tmpl := SimpleValue{Value: `{{=true == false}}`}
newTmpl := processTemplate(t, tmpl)
assert.Equal(t, "false", newTmpl.Value)
})
})
}
20 changes: 18 additions & 2 deletions workflow/controller/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3154,12 +3154,28 @@ func (woc *wfOperationCtx) computeMetrics(metricList []*wfv1.Prometheus, localSc
metricSpec := metricTmpl.DeepCopy()

// Finally substitute value parameters
replacedValue, err := template.Replace(metricSpec.GetValueString(), localScope, false)
metricValueString := metricSpec.GetValueString()

metricValueStringJson, err := json.Marshal(metricValueString)
if err != nil {
woc.reportMetricEmissionError(fmt.Sprintf("unable to marshal metric to JSON for templating '%s': %s", metricSpec.Name, err))
continue
}

replacedValueJson, err := template.Replace(string(metricValueStringJson), localScope, false)
if err != nil {
woc.reportMetricEmissionError(fmt.Sprintf("unable to substitute parameters for metric '%s': %s", metricSpec.Name, err))
continue
}
metricSpec.SetValueString(replacedValue)

var replacedStringJson string
err = json.Unmarshal([]byte(replacedValueJson), &replacedStringJson)
if err != nil {
woc.reportMetricEmissionError(fmt.Sprintf("unable to unmarshal templated metric JSON '%s': %s", metricSpec.Name, err))
continue
}

metricSpec.SetValueString(replacedStringJson)

metric := woc.controller.metrics.GetCustomMetric(metricSpec.GetDesc())
// It is valid to pass a nil metric to ConstructOrUpdateMetric, in that case the metric will be created for us
Expand Down
19 changes: 16 additions & 3 deletions workflow/controller/scope.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"encoding/json"
"fmt"

"github.com/antonmedv/expr"
Expand Down Expand Up @@ -106,13 +107,25 @@ func (s *wfScope) resolveArtifact(art *wfv1.Artifact) (*wfv1.Artifact, error) {
}

if art.SubPath != "" {
resolvedSubPath, err := template.Replace(art.SubPath, s.getParameters(), true)
// Copy resolved artifact pointer before adding subpath
copyArt := valArt.DeepCopy()

subPathAsJson, err := json.Marshal(art.SubPath)
if err != nil {
return copyArt, errors.New(errors.CodeBadRequest, "failed to marshal artifact subpath for templating")
}

resolvedSubPathAsJson, err := template.Replace(string(subPathAsJson), s.getParameters(), true)
if err != nil {
return nil, err
}

// Copy resolved artifact pointer before adding subpath
copyArt := valArt.DeepCopy()
var resolvedSubPath string
err = json.Unmarshal([]byte(resolvedSubPathAsJson), &resolvedSubPath)
if err != nil {
return copyArt, errors.New(errors.CodeBadRequest, "failed to unmarshal artifact subpath for templating")
}

return copyArt, copyArt.AppendToKey(resolvedSubPath)
}

Expand Down