Skip to content

Commit bb2f3de

Browse files
Joibelelliotgunton
andauthored
fix: add a special case for item variable during global expression replacement (cherry-pick #15033 for 3.6) (#15040)
Signed-off-by: Elliot Gunton <[email protected]> Signed-off-by: Alan Clucas <[email protected]> Co-authored-by: Elliot Gunton <[email protected]>
1 parent aa0fdfc commit bb2f3de

File tree

3 files changed

+132
-78
lines changed

3 files changed

+132
-78
lines changed

test/e2e/expr_lang.go renamed to test/e2e/expr_lang_test.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011
"github.com/stretchr/testify/suite"
1112
apiv1 "k8s.io/api/core/v1"
1213
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -19,7 +20,7 @@ type ExprSuite struct {
1920
fixtures.E2ESuite
2021
}
2122

22-
func (s *ExprSuite) TestRegression12037() {
23+
func (s *ExprSuite) TestShadowedExprFunctionsAsTaskNames() {
2324
s.Given().
2425
Workflow(`apiVersion: argoproj.io/v1alpha1
2526
kind: Workflow
@@ -39,12 +40,11 @@ spec:
3940
4041
- name: foo
4142
container:
42-
image: alpine
43+
image: argoproj/argosay:v2
4344
command:
44-
- sh
45-
- -c
46-
- |
47-
echo "foo"
45+
- echo
46+
args:
47+
- foo
4848
`).When().
4949
SubmitWorkflow().
5050
WaitForWorkflow(fixtures.ToBeSucceeded).
@@ -64,6 +64,49 @@ spec:
6464
})
6565
}
6666

67-
func TestExprLangSSuite(t *testing.T) {
67+
func (s *ExprSuite) TestItemInSprigExpr() {
68+
s.Given().
69+
Workflow(`apiVersion: argoproj.io/v1alpha1
70+
kind: Workflow
71+
metadata:
72+
generateName: item-used-in-sprig-
73+
spec:
74+
entrypoint: loop-example
75+
templates:
76+
- name: print-message
77+
container:
78+
image: argoproj/argosay:v2
79+
args:
80+
- '{{inputs.parameters.message}}'
81+
command:
82+
- echo
83+
inputs:
84+
parameters:
85+
- name: message
86+
- name: loop-example
87+
steps:
88+
- - name: print-message-loop
89+
template: print-message
90+
withItems:
91+
- example
92+
arguments:
93+
parameters:
94+
- name: message
95+
value: '{{=sprig.upper(item)}}'
96+
`).When().
97+
SubmitWorkflow().
98+
WaitForWorkflow(fixtures.ToBeSucceeded).
99+
Then().
100+
ExpectWorkflow(func(t *testing.T, metadata *v1.ObjectMeta, status *v1alpha1.WorkflowStatus) {
101+
assert.Equal(t, v1alpha1.WorkflowSucceeded, status.Phase)
102+
103+
stepNode := status.Nodes.FindByDisplayName("print-message-loop(0:example)")
104+
require.NotNil(t, stepNode)
105+
require.NotNil(t, stepNode.Inputs.Parameters[0].Value)
106+
assert.Equal(t, "EXAMPLE", string(*stepNode.Inputs.Parameters[0].Value))
107+
})
108+
}
109+
110+
func TestExprLangSuite(t *testing.T) {
68111
suite.Run(t, new(ExprSuite))
69112
}

util/template/expression_template.go

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,45 @@ func init() {
2121
}
2222
}
2323

24+
var variablesToCheck = []string{
25+
"item",
26+
"retries",
27+
"lastRetry.exitCode",
28+
"lastRetry.status",
29+
"lastRetry.duration",
30+
"lastRetry.message",
31+
"workflow.status",
32+
"workflow.failures",
33+
}
34+
35+
func anyVarNotInEnv(expression string, env map[string]interface{}) *string {
36+
for _, variable := range variablesToCheck {
37+
if hasVariableInExpression(expression, variable) && !hasVarInEnv(env, variable) {
38+
return &variable
39+
}
40+
}
41+
return nil
42+
}
43+
2444
func expressionReplace(w io.Writer, expression string, env map[string]interface{}, allowUnresolved bool) (int, error) {
2545
// The template is JSON-marshaled. This JSON-unmarshals the expression to undo any character escapes.
2646
var unmarshalledExpression string
2747
err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, expression)), &unmarshalledExpression)
2848
if err != nil && allowUnresolved {
29-
log.WithError(err).Debug("unresolved is allowed ")
49+
log.WithError(err).Debug("unresolved is allowed")
3050
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
3151
}
3252
if err != nil {
3353
return 0, fmt.Errorf("failed to unmarshall JSON expression: %w", err)
3454
}
3555

36-
if _, ok := env["retries"]; !ok && hasRetries(unmarshalledExpression) && allowUnresolved {
37-
// this is to make sure expressions like `sprig.int(retries)` don't get resolved to 0 when `retries` don't exist in the env
38-
// See https://github.com/argoproj/argo-workflows/issues/5388
39-
log.WithError(err).Debug("Retries are present and unresolved is allowed")
40-
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
41-
}
42-
43-
// This is to make sure expressions which contains `workflow.status` and `work.failures` don't get resolved to nil
44-
// when `workflow.status` and `workflow.failures` don't exist in the env.
45-
// See https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/expr-lang/expr/issues/330
46-
// This issue doesn't happen to other template parameters since `workflow.status` and `workflow.failures` only exist in the env
47-
// when the exit handlers complete.
48-
if ((hasWorkflowStatus(unmarshalledExpression) && !hasVarInEnv(env, "workflow.status")) ||
49-
(hasWorkflowFailures(unmarshalledExpression) && !hasVarInEnv(env, "workflow.failures"))) &&
50-
allowUnresolved {
56+
varNameNotInEnv := anyVarNotInEnv(unmarshalledExpression, env)
57+
if varNameNotInEnv != nil && allowUnresolved {
58+
// this is to make sure expressions don't get resolved to nil or an empty string when certain variables
59+
// don't exist in the env during the "global" replacement.
60+
// See https://github.com/argoproj/argo-workflows/issues/5388, https://github.com/argoproj/argo-workflows/issues/15008,
61+
// https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/expr-lang/expr/issues/330
62+
log.WithField("variable", *varNameNotInEnv).Debug("variable not in env but unresolved is allowed")
5163
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
5264
}
5365

@@ -100,56 +112,55 @@ func EnvMap(replaceMap map[string]string) map[string]interface{} {
100112
return envMap
101113
}
102114

103-
// hasRetries checks if the variable `retries` exists in the expression template
104-
func hasRetries(expression string) bool {
105-
tokens, err := lexer.Lex(file.NewSource(expression))
106-
if err != nil {
115+
func searchTokens(haystack []lexer.Token, needle []lexer.Token) bool {
116+
if len(needle) > len(haystack) {
107117
return false
108118
}
109-
for _, token := range tokens {
110-
if token.Kind == lexer.Identifier && token.Value == "retries" {
111-
return true
119+
if len(needle) == 0 {
120+
return true
121+
}
122+
outer:
123+
for i := 0; i <= len(haystack)-len(needle); i++ {
124+
for j := 0; j < len(needle); j++ {
125+
if haystack[i+j].String() != needle[j].String() {
126+
continue outer
127+
}
112128
}
129+
return true
113130
}
114131
return false
115132
}
116133

117-
// hasWorkflowStatus checks if expression contains `workflow.status`
118-
func hasWorkflowStatus(expression string) bool {
119-
if !strings.Contains(expression, "workflow.status") {
120-
return false
121-
}
122-
// Even if the expression contains `workflow.status`, it could be the case that it represents a string (`"workflow.status"`),
123-
// not a variable, so we need to parse it and handle filter the string case.
124-
tokens, err := lexer.Lex(file.NewSource(expression))
125-
if err != nil {
126-
return false
127-
}
128-
for i := 0; i < len(tokens)-2; i++ {
129-
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.status" {
130-
return true
134+
func filterEOF(toks []lexer.Token) []lexer.Token {
135+
newToks := []lexer.Token{}
136+
for _, tok := range toks {
137+
if tok.Kind != lexer.EOF {
138+
newToks = append(newToks, tok)
131139
}
132140
}
133-
return false
141+
return newToks
134142
}
135143

136-
// hasWorkflowFailures checks if expression contains `workflow.failures`
137-
func hasWorkflowFailures(expression string) bool {
138-
if !strings.Contains(expression, "workflow.failures") {
144+
// hasVariableInExpression checks if an expression contains a variable.
145+
// This function is somewhat cursed and I have attempted my best to
146+
// remove this curse, but it still exists.
147+
// The strings.Contains is needed because the lexer doesn't do
148+
// any whitespace processing (workflow .status will be seen as workflow.status)
149+
func hasVariableInExpression(expression, variable string) bool {
150+
if !strings.Contains(expression, variable) {
139151
return false
140152
}
141-
// Even if the expression contains `workflow.failures`, it could be the case that it represents a string (`"workflow.failures"`),
142-
// not a variable, so we need to parse it and handle filter the string case.
143153
tokens, err := lexer.Lex(file.NewSource(expression))
144154
if err != nil {
145155
return false
146156
}
147-
for i := 0; i < len(tokens)-2; i++ {
148-
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.failures" {
149-
return true
150-
}
157+
variableTokens, err := lexer.Lex(file.NewSource(variable))
158+
if err != nil {
159+
return false
151160
}
152-
return false
161+
variableTokens = filterEOF(variableTokens)
162+
163+
return searchTokens(tokens, variableTokens)
153164
}
154165

155166
// hasVarInEnv checks if a parameter is in env or not

util/template/expression_template_test.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,34 @@ package template
33
import (
44
"testing"
55

6+
"github.com/expr-lang/expr/file"
7+
"github.com/expr-lang/expr/parser/lexer"
68
"github.com/stretchr/testify/assert"
79
)
810

9-
func Test_hasRetries(t *testing.T) {
10-
t.Run("hasRetiresInExpression", func(t *testing.T) {
11-
assert.True(t, hasRetries("retries"))
12-
assert.True(t, hasRetries("retries + 1"))
13-
assert.True(t, hasRetries("sprig(retries)"))
14-
assert.True(t, hasRetries("sprig(retries + 1) * 64"))
15-
assert.False(t, hasRetries("foo"))
16-
assert.False(t, hasRetries("retriesCustom + 1"))
17-
})
11+
func Test_hasVariableInExpression(t *testing.T) {
12+
assert.True(t, hasVariableInExpression("retries", "retries"))
13+
assert.True(t, hasVariableInExpression("retries + 1", "retries"))
14+
assert.True(t, hasVariableInExpression("sprig(retries)", "retries"))
15+
assert.True(t, hasVariableInExpression("sprig(retries + 1) * 64", "retries"))
16+
assert.False(t, hasVariableInExpression("foo", "retries"))
17+
assert.False(t, hasVariableInExpression("retriesCustom + 1", "retries"))
18+
assert.True(t, hasVariableInExpression("item", "item"))
19+
assert.False(t, hasVariableInExpression("foo", "item"))
20+
assert.True(t, hasVariableInExpression("sprig.upper(item)", "item"))
1821
}
1922

2023
func Test_hasWorkflowParameters(t *testing.T) {
21-
t.Run("hasWorkflowStatusInExpression", func(t *testing.T) {
22-
assert.True(t, hasWorkflowStatus("workflow.status"))
23-
assert.True(t, hasWorkflowStatus(`workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"`))
24-
assert.False(t, hasWorkflowStatus(`"workflow.status" == "Succeeded" ? "SUCCESSFUL" : "FAILED"`))
25-
assert.False(t, hasWorkflowStatus("workflow status"))
26-
assert.False(t, hasWorkflowStatus("workflow .status"))
27-
})
24+
expression := `workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"`
25+
exprToks, _ := lexer.Lex(file.NewSource(expression))
26+
variableToks, _ := lexer.Lex(file.NewSource("workflow.status"))
27+
variableToks = filterEOF(variableToks)
28+
assert.True(t, searchTokens(exprToks, variableToks))
29+
assert.True(t, hasVariableInExpression(expression, "workflow.status"))
2830

29-
t.Run("hasWorkflowFailuresInExpression", func(t *testing.T) {
30-
assert.True(t, hasWorkflowFailures("workflow.failures"))
31-
assert.True(t, hasWorkflowFailures(`workflow.failures == "Succeeded" ? "SUCCESSFUL" : "FAILED"`))
32-
assert.False(t, hasWorkflowFailures(`"workflow.failures" == "Succeeded" ? "SUCCESSFUL" : "FAILED"`))
33-
assert.False(t, hasWorkflowFailures("workflow failures"))
34-
assert.False(t, hasWorkflowFailures("workflow .failures"))
35-
})
31+
assert.False(t, hasVariableInExpression(expression, "workflow status"))
32+
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
33+
34+
expression = `"workflow.status" == "Succeeded" ? "SUCCESSFUL" : "FAILED"`
35+
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
3636
}

0 commit comments

Comments
 (0)