Skip to content

Commit 75caaf2

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

File tree

3 files changed

+93
-60
lines changed

3 files changed

+93
-60
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: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,45 @@ func init() {
2121
}
2222
}
2323

24-
func anyVarNotInEnv(expression string, variables []string, env map[string]interface{}) bool {
25-
for _, variable := range variables {
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 {
2637
if hasVariableInExpression(expression, variable) && !hasVarInEnv(env, variable) {
27-
return true
38+
return &variable
2839
}
2940
}
30-
return false
41+
return nil
3142
}
3243

3344
func expressionReplace(w io.Writer, expression string, env map[string]interface{}, allowUnresolved bool) (int, error) {
3445
// The template is JSON-marshaled. This JSON-unmarshals the expression to undo any character escapes.
3546
var unmarshalledExpression string
3647
err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, expression)), &unmarshalledExpression)
3748
if err != nil && allowUnresolved {
38-
log.WithError(err).Debug("unresolved is allowed ")
49+
log.WithError(err).Debug("unresolved is allowed")
3950
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
4051
}
4152
if err != nil {
4253
return 0, fmt.Errorf("failed to unmarshall JSON expression: %w", err)
4354
}
4455

45-
if anyVarNotInEnv(unmarshalledExpression, []string{"retries"}, env) && allowUnresolved {
46-
// this is to make sure expressions like `sprig.int(retries)` don't get resolved to 0 when `retries` don't exist in the env
47-
// See https://github.com/argoproj/argo-workflows/issues/5388
48-
log.WithError(err).Debug("Retries are present and unresolved is allowed")
49-
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
50-
}
51-
52-
lastRetryVariables := []string{"lastRetry.exitCode", "lastRetry.status", "lastRetry.duration", "lastRetry.message"}
53-
if anyVarNotInEnv(unmarshalledExpression, lastRetryVariables, env) && allowUnresolved {
54-
// This is to make sure expressions which contains `lastRetry.*` don't get resolved to nil
55-
// when they don't exist in the env.
56-
log.WithError(err).Debug("LastRetry variables are present and unresolved is allowed")
57-
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
58-
}
59-
60-
// This is to make sure expressions which contains `workflow.status` and `work.failures` don't get resolved to nil
61-
// when `workflow.status` and `workflow.failures` don't exist in the env.
62-
// See https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/expr-lang/expr/issues/330
63-
// This issue doesn't happen to other template parameters since `workflow.status` and `workflow.failures` only exist in the env
64-
// when the exit handlers complete.
65-
if anyVarNotInEnv(unmarshalledExpression, []string{"workflow.status", "workflow.failures"}, env) && allowUnresolved {
66-
log.WithError(err).Debug("workflow.status or workflow.failures are present and unresolved is allowed")
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")
6763
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
6864
}
6965

@@ -116,11 +112,6 @@ func EnvMap(replaceMap map[string]string) map[string]interface{} {
116112
return envMap
117113
}
118114

119-
// hasRetries checks if the variable `retries` exists in the expression template
120-
func hasRetries(expression string) bool {
121-
return hasVariableInExpression(expression, "retries")
122-
}
123-
124115
func searchTokens(haystack []lexer.Token, needle []lexer.Token) bool {
125116
if len(needle) > len(haystack) {
126117
return false

util/template/expression_template_test.go

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,29 @@ import (
88
"github.com/stretchr/testify/assert"
99
)
1010

11-
func Test_hasRetries(t *testing.T) {
12-
t.Run("hasRetiresInExpression", func(t *testing.T) {
13-
assert.True(t, hasRetries("retries"))
14-
assert.True(t, hasRetries("retries + 1"))
15-
assert.True(t, hasRetries("sprig(retries)"))
16-
assert.True(t, hasRetries("sprig(retries + 1) * 64"))
17-
assert.False(t, hasRetries("foo"))
18-
assert.False(t, hasRetries("retriesCustom + 1"))
19-
})
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"))
2021
}
2122

2223
func Test_hasWorkflowParameters(t *testing.T) {
23-
t.Run("hasVariableInExpression", func(t *testing.T) {
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"))
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"))
3030

31-
assert.False(t, hasVariableInExpression(expression, "workflow status"))
32-
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
31+
assert.False(t, hasVariableInExpression(expression, "workflow status"))
32+
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
3333

34-
expression = `"workflow.status" == "Succeeded" ? "SUCCESSFUL" : "FAILED"`
35-
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
36-
})
34+
expression = `"workflow.status" == "Succeeded" ? "SUCCESSFUL" : "FAILED"`
35+
assert.False(t, hasVariableInExpression(expression, "workflow .status"))
3736
}

0 commit comments

Comments
 (0)