Skip to content

Commit 96276a4

Browse files
fix: exit handler variables don't get resolved correctly. Fixes argoproj#10393 (argoproj#10449) (#245)
Signed-off-by: Jiacheng Xu <[email protected]> Co-authored-by: Jiacheng Xu <[email protected]>
1 parent 9bad67c commit 96276a4

File tree

5 files changed

+165
-1
lines changed

5 files changed

+165
-1
lines changed

test/e2e/functional_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,23 @@ spec:
688688
})
689689
}
690690

691+
func (s *FunctionalSuite) TestWorkflowHookParameterTemplates() {
692+
s.Given().
693+
Workflow("@testdata/workflow-hook-parameter.yaml").
694+
When().
695+
SubmitWorkflow().
696+
WaitForWorkflow(fixtures.ToBeSucceeded).
697+
Then().
698+
ExpectWorkflow(func(t *testing.T, md *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
699+
assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase)
700+
}).
701+
ExpectWorkflowNode(wfv1.NodeWithDisplayName("workflow-hook-parameter.onExit"), func(t *testing.T, n *wfv1.NodeStatus, p *apiv1.Pod) {
702+
assert.Equal(t, wfv1.NodeSucceeded, n.Phase)
703+
assert.Equal(t, "Succeeded", n.Inputs.Parameters[0].Value.String())
704+
assert.Equal(t, "Succeeded", n.Inputs.Parameters[1].Value.String())
705+
})
706+
}
707+
691708
func (s *FunctionalSuite) TestParametrizableAds() {
692709
s.Given().
693710
Workflow(`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
apiVersion: argoproj.io/v1alpha1
2+
kind: Workflow
3+
metadata:
4+
name: workflow-hook-parameter
5+
spec:
6+
entrypoint: run-test
7+
templates:
8+
- name: run-test
9+
container:
10+
name: runner
11+
image: 'argoproj/argosay:v2'
12+
command: ['sh','-c']
13+
args:
14+
- exit 0
15+
- name: cowsay
16+
inputs:
17+
parameters:
18+
- name: ternary
19+
- name: status
20+
container:
21+
image: 'argoproj/argosay:v2'
22+
command: ['bash','-c']
23+
args:
24+
- |
25+
echo "{{inputs.parameters.ternary}}"
26+
echo "{{inputs.parameters.status}}"
27+
[[ "{{inputs.parameters.ternary}}" = "{{inputs.parameters.status}}" ]]
28+
hooks:
29+
exit:
30+
template: cowsay
31+
arguments:
32+
parameters:
33+
- name: ternary
34+
value: '{{= workflow.status == "Succeeded" ? "Succeeded" : "Failed" }}'
35+
- name: status
36+
value: '{{= workflow.status }}'

util/template/expression_template.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"strings"
89

910
"github.com/antonmedv/expr"
1011
"github.com/antonmedv/expr/file"
1112
"github.com/antonmedv/expr/parser/lexer"
13+
"github.com/doublerebel/bellows"
1214
)
1315

1416
func init() {
@@ -33,6 +35,18 @@ func expressionReplace(w io.Writer, expression string, env map[string]interface{
3335
// See https://github.com/argoproj/argo-workflows/issues/5388
3436
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
3537
}
38+
39+
// This is to make sure expressions which contains `workflow.status` and `work.failures` don't get resolved to nil
40+
// when `workflow.status` and `workflow.failures` don't exist in the env.
41+
// See https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/antonmedv/expr/issues/330
42+
// This issue doesn't happen to other template parameters since `workflow.status` and `workflow.failures` only exist in the env
43+
// when the exit handlers complete.
44+
if ((hasWorkflowStatus(unmarshalledExpression) && !hasVarInEnv(env, "workflow.status")) ||
45+
(hasWorkflowFailures(unmarshalledExpression) && !hasVarInEnv(env, "workflow.failures"))) &&
46+
allowUnresolved {
47+
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
48+
}
49+
3650
result, err := expr.Eval(unmarshalledExpression, env)
3751
if (err != nil || result == nil) && allowUnresolved { // <nil> result is also un-resolved, and any error can be unresolved
3852
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
@@ -79,3 +93,48 @@ func hasRetries(expression string) bool {
7993
}
8094
return false
8195
}
96+
97+
// hasWorkflowStatus checks if expression contains `workflow.status`
98+
func hasWorkflowStatus(expression string) bool {
99+
if !strings.Contains(expression, "workflow.status") {
100+
return false
101+
}
102+
// Even if the expression contains `workflow.status`, it could be the case that it represents a string (`"workflow.status"`),
103+
// not a variable, so we need to parse it and handle filter the string case.
104+
tokens, err := lexer.Lex(file.NewSource(expression))
105+
if err != nil {
106+
return false
107+
}
108+
for i := 0; i < len(tokens)-2; i++ {
109+
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.status" {
110+
return true
111+
}
112+
}
113+
return false
114+
}
115+
116+
// hasWorkflowFailures checks if expression contains `workflow.failures`
117+
func hasWorkflowFailures(expression string) bool {
118+
if !strings.Contains(expression, "workflow.failures") {
119+
return false
120+
}
121+
// Even if the expression contains `workflow.failures`, it could be the case that it represents a string (`"workflow.failures"`),
122+
// not a variable, so we need to parse it and handle filter the string case.
123+
tokens, err := lexer.Lex(file.NewSource(expression))
124+
if err != nil {
125+
return false
126+
}
127+
for i := 0; i < len(tokens)-2; i++ {
128+
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.failures" {
129+
return true
130+
}
131+
}
132+
return false
133+
}
134+
135+
// hasVarInEnv checks if a parameter is in env or not
136+
func hasVarInEnv(env map[string]interface{}, parameter string) bool {
137+
flattenEnv := bellows.Flatten(env)
138+
_, ok := flattenEnv[parameter]
139+
return ok
140+
}

util/template/expression_template_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,21 @@ func Test_hasRetries(t *testing.T) {
1616
assert.False(t, hasRetries("retriesCustom + 1"))
1717
})
1818
}
19+
20+
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+
})
28+
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+
})
36+
}

util/template/replace_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ func Test_Replace(t *testing.T) {
4040
assert.NoError(t, err)
4141
assert.Equal(t, toJsonString("bar"), r)
4242
})
43+
t.Run("Valid WorkflowStatus", func(t *testing.T) {
44+
replaced, err := Replace(toJsonString(`{{=workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), map[string]string{"workflow.status": "Succeeded"}, false)
45+
assert.NoError(t, err)
46+
assert.Equal(t, toJsonString(`SUCCESSFUL`), replaced)
47+
replaced, err = Replace(toJsonString(`{{=workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), map[string]string{"workflow.status": "Failed"}, false)
48+
assert.NoError(t, err)
49+
assert.Equal(t, toJsonString(`FAILED`), replaced)
50+
})
51+
t.Run("Valid WorkflowFailures", func(t *testing.T) {
52+
replaced, err := Replace(toJsonString(`{{=workflow.failures == "{\"foo\":\"bar\"}" ? "SUCCESSFUL" : "FAILED"}}`), map[string]string{"workflow.failures": `{"foo":"bar"}`}, false)
53+
assert.NoError(t, err)
54+
assert.Equal(t, toJsonString(`SUCCESSFUL`), replaced)
55+
replaced, err = Replace(toJsonString(`{{=workflow.failures == "{\"foo\":\"bar\"}" ? "SUCCESSFUL" : "FAILED"}}`), map[string]string{"workflow.failures": `{"foo":"barr"}`}, false)
56+
assert.NoError(t, err)
57+
assert.Equal(t, toJsonString(`FAILED`), replaced)
58+
})
4359
t.Run("Unresolved", func(t *testing.T) {
4460
t.Run("Allowed", func(t *testing.T) {
4561
_, err := Replace(toJsonString("{{=foo}}"), nil, true)
@@ -48,12 +64,30 @@ func Test_Replace(t *testing.T) {
4864
t.Run("AllowedRetries", func(t *testing.T) {
4965
replaced, err := Replace(toJsonString("{{=sprig.int(retries)}}"), nil, true)
5066
assert.NoError(t, err)
51-
assert.Equal(t, replaced, toJsonString("{{=sprig.int(retries)}}"))
67+
assert.Equal(t, toJsonString("{{=sprig.int(retries)}}"), replaced)
68+
})
69+
t.Run("AllowedWorkflowStatus", func(t *testing.T) {
70+
replaced, err := Replace(toJsonString(`{{=workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), nil, true)
71+
assert.NoError(t, err)
72+
assert.Equal(t, toJsonString(`{{=workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), replaced)
73+
})
74+
t.Run("AllowedWorkflowFailures", func(t *testing.T) {
75+
replaced, err := Replace(toJsonString(`{{=workflow.failures == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), nil, true)
76+
assert.NoError(t, err)
77+
assert.Equal(t, toJsonString(`{{=workflow.failures == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), replaced)
5278
})
5379
t.Run("Disallowed", func(t *testing.T) {
5480
_, err := Replace(toJsonString("{{=foo}}"), nil, false)
5581
assert.EqualError(t, err, "failed to evaluate expression \"foo\"")
5682
})
83+
t.Run("DisallowedWorkflowStatus", func(t *testing.T) {
84+
_, err := Replace(toJsonString(`{{=workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), nil, false)
85+
assert.ErrorContains(t, err, "failed to evaluate expression")
86+
})
87+
t.Run("DisallowedWorkflowFailures", func(t *testing.T) {
88+
_, err := Replace(toJsonString(`{{=workflow.failures == "Succeeded" ? "SUCCESSFUL" : "FAILED"}}`), nil, false)
89+
assert.ErrorContains(t, err, "failed to evaluate expression")
90+
})
5791
})
5892
t.Run("Error", func(t *testing.T) {
5993
_, err := Replace(toJsonString("{{=!}}"), nil, false)

0 commit comments

Comments
 (0)