-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
expression_template.go
146 lines (134 loc) · 5.28 KB
/
expression_template.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package template
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/file"
"github.com/antonmedv/expr/parser/lexer"
"github.com/doublerebel/bellows"
log "github.com/sirupsen/logrus"
)
func init() {
if os.Getenv("EXPRESSION_TEMPLATES") != "false" {
registerKind(kindExpression)
}
}
func expressionReplace(w io.Writer, expression string, env map[string]interface{}, allowUnresolved bool) (int, error) {
// The template is JSON-marshaled. This JSON-unmarshals the expression to undo any character escapes.
var unmarshalledExpression string
err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, expression)), &unmarshalledExpression)
if err != nil && allowUnresolved {
log.WithError(err).Debug("unresolved is allowed ")
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
}
if err != nil {
return 0, fmt.Errorf("failed to unmarshall JSON expression: %w", err)
}
if _, ok := env["retries"]; !ok && hasRetries(unmarshalledExpression) && allowUnresolved {
// this is to make sure expressions like `sprig.int(retries)` don't get resolved to 0 when `retries` don't exist in the env
// See https://github.com/argoproj/argo-workflows/issues/5388
log.WithError(err).Debug("Retries are present and unresolved is allowed")
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
}
// This is to make sure expressions which contains `workflow.status` and `work.failures` don't get resolved to nil
// when `workflow.status` and `workflow.failures` don't exist in the env.
// See https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/antonmedv/expr/issues/330
// This issue doesn't happen to other template parameters since `workflow.status` and `workflow.failures` only exist in the env
// when the exit handlers complete.
if ((hasWorkflowStatus(unmarshalledExpression) && !hasVarInEnv(env, "workflow.status")) ||
(hasWorkflowFailures(unmarshalledExpression) && !hasVarInEnv(env, "workflow.failures"))) &&
allowUnresolved {
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
}
result, err := expr.Eval(unmarshalledExpression, env)
if (err != nil || result == nil) && allowUnresolved {
// <nil> result is also un-resolved, and any error can be unresolved
log.WithError(err).Debug("Result and error are unresolved")
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
}
if err != nil {
return 0, fmt.Errorf("failed to evaluate expression: %w", err)
}
if result == nil {
return 0, fmt.Errorf("failed to evaluate expression %q", expression)
}
resultMarshaled, err := json.Marshal(fmt.Sprintf("%v", result))
if (err != nil || resultMarshaled == nil) && allowUnresolved {
log.WithError(err).Debug("resultMarshaled is nil and unresolved is allowed ")
return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression)))
}
if err != nil {
return 0, fmt.Errorf("failed to marshal evaluated expression: %w", err)
}
if resultMarshaled == nil {
return 0, fmt.Errorf("failed to marshal evaluated marshaled expression %q", expression)
}
// Trim leading and trailing quotes. The value is being inserted into something that's already a string.
marshaledLength := len(resultMarshaled)
return w.Write(resultMarshaled[1 : marshaledLength-1])
}
func EnvMap(replaceMap map[string]string) map[string]interface{} {
envMap := make(map[string]interface{})
for k, v := range replaceMap {
envMap[k] = v
}
return envMap
}
// hasRetries checks if the variable `retries` exists in the expression template
func hasRetries(expression string) bool {
tokens, err := lexer.Lex(file.NewSource(expression))
if err != nil {
return false
}
for _, token := range tokens {
if token.Kind == lexer.Identifier && token.Value == "retries" {
return true
}
}
return false
}
// hasWorkflowStatus checks if expression contains `workflow.status`
func hasWorkflowStatus(expression string) bool {
if !strings.Contains(expression, "workflow.status") {
return false
}
// Even if the expression contains `workflow.status`, it could be the case that it represents a string (`"workflow.status"`),
// not a variable, so we need to parse it and handle filter the string case.
tokens, err := lexer.Lex(file.NewSource(expression))
if err != nil {
return false
}
for i := 0; i < len(tokens)-2; i++ {
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.status" {
return true
}
}
return false
}
// hasWorkflowFailures checks if expression contains `workflow.failures`
func hasWorkflowFailures(expression string) bool {
if !strings.Contains(expression, "workflow.failures") {
return false
}
// Even if the expression contains `workflow.failures`, it could be the case that it represents a string (`"workflow.failures"`),
// not a variable, so we need to parse it and handle filter the string case.
tokens, err := lexer.Lex(file.NewSource(expression))
if err != nil {
return false
}
for i := 0; i < len(tokens)-2; i++ {
if tokens[i].Value+tokens[i+1].Value+tokens[i+2].Value == "workflow.failures" {
return true
}
}
return false
}
// hasVarInEnv checks if a parameter is in env or not
func hasVarInEnv(env map[string]interface{}, parameter string) bool {
flattenEnv := bellows.Flatten(env)
_, ok := flattenEnv[parameter]
return ok
}