Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions test/e2e/expr_lang.go → test/e2e/expr_lang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -39,12 +40,11 @@ spec:

- name: foo
container:
image: alpine
image: argoproj/argosay:v2
command:
- sh
- -c
- |
echo "foo"
- echo
args:
- foo
`).When().
SubmitWorkflow().
WaitForWorkflow(fixtures.ToBeSucceeded).
Expand All @@ -64,6 +64,49 @@ spec:
})
}

func TestExprLangSSuite(t *testing.T) {
func (s *ExprSuite) TestRegression15008() {
Comment thread
elliotgunton marked this conversation as resolved.
Outdated
s.Given().
Workflow(`apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: item-used-in-sprig-
spec:
entrypoint: loop-example
templates:
- name: print-message
container:
image: argoproj/argosay:v2
args:
- '{{inputs.parameters.message}}'
command:
- echo
inputs:
parameters:
- name: message
- name: loop-example
steps:
- - name: print-message-loop
template: print-message
withItems:
- example
arguments:
parameters:
- name: message
value: '{{=sprig.upper(item)}}'
`).When().
SubmitWorkflow().
WaitForWorkflow(fixtures.ToBeSucceeded).
Then().
ExpectWorkflow(func(t *testing.T, metadata *v1.ObjectMeta, status *v1alpha1.WorkflowStatus) {
assert.Equal(t, v1alpha1.WorkflowSucceeded, status.Phase)

stepNode := status.Nodes.FindByDisplayName("print-message-loop(0:example)")
require.NotNil(t, stepNode)
require.NotNil(t, stepNode.Inputs.Parameters[0].Value)
assert.Equal(t, "EXAMPLE", string(*stepNode.Inputs.Parameters[0].Value))
})
}

func TestExprLangSuite(t *testing.T) {
suite.Run(t, new(ExprSuite))
}
55 changes: 23 additions & 32 deletions util/template/expression_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,24 @@ func init() {
}
}

func anyVarNotInEnv(expression string, variables []string, env map[string]interface{}) bool {
for _, variable := range variables {
var variablesToCheck = []string{
Comment thread
Joibel marked this conversation as resolved.
"item",
"retries",
"lastRetry.exitCode",
"lastRetry.status",
"lastRetry.duration",
"lastRetry.message",
"workflow.status",
"workflow.failures",
}

func anyVarNotInEnv(expression string, env map[string]interface{}) *string {
for _, variable := range variablesToCheck {
if hasVariableInExpression(expression, variable) && !hasVarInEnv(env, variable) {
return true
return &variable
}
}
return false
return nil
}

func expressionReplace(ctx context.Context, w io.Writer, expression string, env map[string]interface{}, allowUnresolved bool) (int, error) {
Expand All @@ -38,35 +49,20 @@ func expressionReplace(ctx context.Context, w io.Writer, expression string, env
var unmarshalledExpression string
err := json.Unmarshal(fmt.Appendf(nil, `"%s"`, expression), &unmarshalledExpression)
if err != nil && allowUnresolved {
log.WithError(err).Debug(ctx, "unresolved is allowed ")
log.WithError(err).Debug(ctx, "unresolved is allowed")
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
}
if err != nil {
return 0, fmt.Errorf("failed to unmarshall JSON expression: %w", err)
}

if anyVarNotInEnv(unmarshalledExpression, []string{"retries"}, env) && 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(ctx, "Retries are present and unresolved is allowed")
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
}

lastRetryVariables := []string{"lastRetry.exitCode", "lastRetry.status", "lastRetry.duration", "lastRetry.message"}
if anyVarNotInEnv(unmarshalledExpression, lastRetryVariables, env) && allowUnresolved {
// This is to make sure expressions which contains `lastRetry.*` don't get resolved to nil
// when they don't exist in the env.
log.WithError(err).Debug(ctx, "LastRetry variables are present and unresolved is allowed")
return fmt.Fprintf(w, "{{%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/expr-lang/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 anyVarNotInEnv(unmarshalledExpression, []string{"workflow.status", "workflow.failures"}, env) && allowUnresolved {
log.WithError(err).Debug(ctx, "workflow.status or workflow.failures are present and unresolved is allowed")
varNameNotInEnv := anyVarNotInEnv(unmarshalledExpression, env)
if varNameNotInEnv != nil && allowUnresolved {
// this is to make sure expressions don't get resolved to nil or an empty string when certain variables
// don't exist in the env during the "global" replacement.
// See https://github.com/argoproj/argo-workflows/issues/5388, https://github.com/argoproj/argo-workflows/issues/15008,
// https://github.com/argoproj/argo-workflows/issues/10393, https://github.com/expr-lang/expr/issues/330
log.WithError(err).WithField("variable", varNameNotInEnv).Debug(ctx, "variable not in env but unresolved is allowed")
Comment thread
elliotgunton marked this conversation as resolved.
Outdated
Comment thread
elliotgunton marked this conversation as resolved.
Outdated
return fmt.Fprintf(w, "{{%s%s}}", kindExpression, expression)
}

Expand Down Expand Up @@ -119,11 +115,6 @@ func EnvMap(replaceMap map[string]string) map[string]interface{} {
return envMap
}

// hasRetries checks if the variable `retries` exists in the expression template
func hasRetries(expression string) bool {
return hasVariableInExpression(expression, "retries")
}

func searchTokens(haystack []lexer.Token, needle []lexer.Token) bool {
if len(needle) > len(haystack) {
return false
Expand Down
21 changes: 12 additions & 9 deletions util/template/expression_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import (
"github.com/stretchr/testify/assert"
)

func Test_hasRetries(t *testing.T) {
t.Run("hasRetiresInExpression", func(t *testing.T) {
assert.True(t, hasRetries("retries"))
assert.True(t, hasRetries("retries + 1"))
assert.True(t, hasRetries("sprig(retries)"))
assert.True(t, hasRetries("sprig(retries + 1) * 64"))
assert.False(t, hasRetries("foo"))
assert.False(t, hasRetries("retriesCustom + 1"))
func Test_hasVariableInExpression(t *testing.T) {
t.Run("hasVariableInExpression", func(t *testing.T) {
Comment thread
elliotgunton marked this conversation as resolved.
Outdated
assert.True(t, hasVariableInExpression("retries", "retries"))
assert.True(t, hasVariableInExpression("retries + 1", "retries"))
assert.True(t, hasVariableInExpression("sprig(retries)", "retries"))
assert.True(t, hasVariableInExpression("sprig(retries + 1) * 64", "retries"))
assert.False(t, hasVariableInExpression("foo", "retries"))
assert.False(t, hasVariableInExpression("retriesCustom + 1", "retries"))
assert.True(t, hasVariableInExpression("item", "item"))
assert.False(t, hasVariableInExpression("foo", "item"))
assert.True(t, hasVariableInExpression("sprig.upper(item)", "item"))
})
}

func Test_hasWorkflowParameters(t *testing.T) {
t.Run("hasVariableInExpression", func(t *testing.T) {
t.Run("hasWorkflowParameters", func(t *testing.T) {
expression := `workflow.status == "Succeeded" ? "SUCCESSFUL" : "FAILED"`
exprToks, _ := lexer.Lex(file.NewSource(expression))
variableToks, _ := lexer.Lex(file.NewSource("workflow.status"))
Expand Down
Loading