Repository: argoproj/argo (go)

Issue:
WorkflowEventBinding passes raw go structs instead of json marshalled payload params
## Summary 

What happened/what you expected to happen?
Creating a simple WorkflowEventBinding for github that triggers workflows based on certain labels in PR. 

Doing payload.pull_request.labels gives me a full go struct rather than a json object. Go structs aren't very useful since the lingua franca of the web is json. I can't easily pipe this into a python container and do something with labels.

## Diagnostics

```yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  annotations:
    owner: core
  labels: {}
  name: deploy-pr
  namespace: github
spec:
  arguments:
    artifacts: []
    parameters:
    - name: action
      value: ''
    - name: pr_author
      value: ''
    - name: pr_branch
      value: ''
    - name: pr_commit
      value: ''
    - name: pr_labels
      value: ''
    - name: pr_num
      value: ''
  entrypoint: deploy-pr
  templates:
  - container:
      args:
      - '{{workflow.parameters}}'
      command:
      - echo
      image: ubuntu:latest
      imagePullPolicy: IfNotPresent
    name: print
  - dag:
      tasks:
      - arguments:
          artifacts: []
          parameters: []
        name: trigger-workflow
        template: print
    name: deploy-pr
  volumes: []
  workflowMetadata:
    annotations:
      owner: core
    labels: {}
---
apiVersion: argoproj.io/v1alpha1
kind: WorkflowEventBinding
metadata:
  annotations:
    owner: core
  labels: {}
  name: deploy-pr
  namespace: github
spec:
  event:
    selector: metadata["x-github-event"] == ["pull_request"] && any(payload.pull_request.labels,
      {.name == "stage-and-deploy"})
  submit:
    arguments:
      parameters:
      - name: action
        valueFrom:
          event: payload.action
      - name: pr_author
        valueFrom:
          event: payload.pull_request.user.login
      - name: pr_branch
        valueFrom:
          event: payload.pull_request.head.ref
      - name: pr_commit
        valueFrom:
          event: payload.pull_request.head.sha
      - name: pr_labels
        valueFrom:
          event: payload.pull_request.labels
      - name: pr_num
        valueFrom:
          event: payload.number
    workflowTemplateRef:
      name: deploy-pr

```

What version of Argo Workflows are you running?  v2.11.5

The output is 

```json
[
  {
    "name": "action",
    "value": "labeled"
  },
  {
    "name": "pr_author",
    "value": "nojvek"
  },
  {
    "name": "pr_branch",
    "value": "nojvek-test"
  },
  {
    "name": "pr_commit",
    "value": "f2116e4139e9ef8827f65b76293890e594f0fdcb"
  },
  {
    "name": "pr_labels",
    "value": "[map[color:0052cc default:false description:Only deploy configmaps id:2.442548891e+09 name:configmaps-only node_id:MDU6TGFiZWwyNDQyNTQ4ODkx url:https://api.github.com/repos/mixpanel/analytics/labels/configmaps-only] map[color:1d76db default:false description:Don't wait for user QA, if staging workflow succeeds, deploy to prod id:2.514491821e+09 name:skip-qa-confirm node_id:MDU6TGFiZWwyNTE0NDkxODIx url:https://api.github.com/repos/mixpanel/analytics/labels/skip-qa-confirm] map[color:5319e7 default:false description:Stage to staging-1, then deploy to production id:2.442682022e+09 name:stage-and-deploy node_id:MDU6TGFiZWwyNDQyNjgyMDIy url:https://api.github.com/repos/mixpanel/analytics/labels/stage-and-deploy] map[color:0052cc default:false description: id:2.292237044e+09 name:webapp/nginx node_id:MDU6TGFiZWwyMjkyMjM3MDQ0 url:https://api.github.com/repos/mixpanel/analytics/labels/webapp/nginx]]"
  },
  {
    "name": "pr_num",
    "value": "33195"
  }
]
```

The core issue is I'd expect the payload.pull_request.labels to be json marshalled, rather than go debug output e.g `[map[color:0052cc `


---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

Impacted by this bug? Give it a 👍. We prioritise the issues with the most 👍.


Pull request:
fix(server): Correct webhook event payload marshalling. Fixes #4572
Checklist:

* [x] My organization is added to [USERS.md](https://github.com/argoproj/argo/blob/master/USERS.md).

<!--

Please leave your PR in draft if you don't need a review yet. 

To fix failing `CI / Codegen`, run `make codegen`. 

-->
Fixes #4572

Patch:
diff --git a/server/event/dispatch/operation.go b/server/event/dispatch/operation.go
index 545fd83ae..5970bd022 100644
--- a/server/event/dispatch/operation.go
+++ b/server/event/dispatch/operation.go
@@ -113,7 +113,11 @@ func (o *Operation) dispatch(wfeb wfv1.WorkflowEventBinding, nameSuffix string)
 				if err != nil {
 					return nil, fmt.Errorf("failed to evaluate workflow template parameter \"%s\" expression: %w", p.Name, err)
 				}
-				wf.Spec.Arguments.Parameters = append(wf.Spec.Arguments.Parameters, wfv1.Parameter{Name: p.Name, Value: wfv1.AnyStringPtr(result)})
+				data, err := json.Marshal(result)
+				if err != nil {
+					return nil, fmt.Errorf("failed to convert result to JSON \"%s\" expression: %w", p.Name, err)
+				}
+				wf.Spec.Arguments.Parameters = append(wf.Spec.Arguments.Parameters, wfv1.Parameter{Name: p.Name, Value: wfv1.AnyStringPtr(wfv1.Item{Value: data})})
 			}
 		}
 		wf, err = client.ArgoprojV1alpha1().Workflows(wfeb.Namespace).Create(wf)


Test patch:
diff --git a/server/event/dispatch/operation_test.go b/server/event/dispatch/operation_test.go
index a49827c0a..6d265caeb 100644
--- a/server/event/dispatch/operation_test.go
+++ b/server/event/dispatch/operation_test.go
@@ -2,6 +2,7 @@ package dispatch
 
 import (
 	"context"
+	"encoding/json"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -44,58 +45,136 @@ func TestNewOperation(t *testing.T) {
 		&wfv1.WorkflowTemplate{
 			ObjectMeta: metav1.ObjectMeta{Name: "my-wft", Namespace: "my-ns", Labels: map[string]string{common.LabelKeyControllerInstanceID: "my-instanceid"}},
 		},
+		&wfv1.WorkflowTemplate{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wft-2", Namespace: "my-ns", Labels: map[string]string{common.LabelKeyControllerInstanceID: "my-instanceid"}},
+		},
+		&wfv1.WorkflowTemplate{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wft-3", Namespace: "my-ns"},
+		},
 	)
 	ctx := context.WithValue(context.WithValue(context.Background(), auth.WfKey, client), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: "my-sub"}})
-	recorder := record.NewFakeRecorder(1)
+	recorder := record.NewFakeRecorder(6)
 
 	// act
 	operation, err := NewOperation(ctx, instanceid.NewService("my-instanceid"), recorder, []wfv1.WorkflowEventBinding{
+		// test a malformed binding
 		{
 			ObjectMeta: metav1.ObjectMeta{Name: "malformed", Namespace: "my-ns"},
 		},
+		// test a binding that cannot match
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-0", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "false"},
+			},
+		},
+		// test a binding with a cluster template
 		{
 			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-1", Namespace: "my-ns"},
 			Spec: wfv1.WorkflowEventBindingSpec{
 				Event: wfv1.Event{Selector: "true"},
 				Submit: &wfv1.Submit{
 					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "my-cwft", ClusterScope: true},
-					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: `"foo"`}}}},
+					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: `"bar"`}}}},
 				},
 			},
 		},
+		// test a binding with a namespace template
 		{
 			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-2", Namespace: "my-ns"},
 			Spec: wfv1.WorkflowEventBindingSpec{
 				Event: wfv1.Event{Selector: "true"},
 				Submit: &wfv1.Submit{
 					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "my-wft"},
-					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: `"foo"`}}}},
+					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: `"bar"`}}}},
+				},
+			},
+		},
+		// test a bind with a payload and expression with a map in it
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-3", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "payload.foo.bar == 'baz'"},
+				Submit: &wfv1.Submit{
+					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "my-wft-2"},
+					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: "payload.foo"}}}},
+				},
+			},
+		},
+		// test a binding that errors due to missing workflow template
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-4", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "true"},
+				Submit: &wfv1.Submit{
+					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "not-found"},
+				},
+			},
+		},
+		// test a binding that errors match due to wrong instance ID on template
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-5", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "true"},
+				Submit: &wfv1.Submit{
+					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "my-wft-3"},
+				},
+			},
+		},
+		// test a binding with an invalid event selector
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-6", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "garbage!!!!!!"},
+			},
+		},
+		// test a binding with a non-bool event selector
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-6", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: `"garbage"`},
+			},
+		},
+		// test a binding with an invalid param expression
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "my-wfeb-7", Namespace: "my-ns"},
+			Spec: wfv1.WorkflowEventBindingSpec{
+				Event: wfv1.Event{Selector: "true"},
+				Submit: &wfv1.Submit{
+					WorkflowTemplateRef: wfv1.WorkflowTemplateRef{Name: "my-wft"},
+					Arguments:           &wfv1.Arguments{Parameters: []wfv1.Parameter{{Name: "my-param", ValueFrom: &wfv1.ValueFrom{Event: "rubbish!!!"}}}},
 				},
 			},
 		},
-	}, "my-ns", "my-discriminator", &wfv1.Item{})
+	}, "my-ns", "my-discriminator", &wfv1.Item{Value: json.RawMessage(`{"foo": {"bar": "baz"}}`)})
 	assert.NoError(t, err)
 	operation.Dispatch()
 
+	expectedParamValues := []string{"bar", "bar", `{"bar":"baz"}`}
 	// assert
 	list, err := client.ArgoprojV1alpha1().Workflows("my-ns").List(metav1.ListOptions{})
-	if assert.NoError(t, err) && assert.Len(t, list.Items, 2) {
-		for _, wf := range list.Items {
+	if assert.NoError(t, err) && assert.Len(t, list.Items, 3) {
+		for i, wf := range list.Items {
 			assert.Equal(t, "my-instanceid", wf.Labels[common.LabelKeyControllerInstanceID])
 			assert.Equal(t, "my-sub", wf.Labels[common.LabelKeyCreator])
 			assert.Contains(t, wf.Labels, common.LabelKeyWorkflowEventBinding)
-			assert.Equal(t, []wfv1.Parameter{{Name: "my-param", Value: wfv1.AnyStringPtr(`foo`)}}, wf.Spec.Arguments.Parameters)
+			assert.Equal(t, []wfv1.Parameter{{Name: "my-param", Value: wfv1.AnyStringPtr(expectedParamValues[i])}}, wf.Spec.Arguments.Parameters)
 		}
 	}
 	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to evaluate workflow template expression: unexpected token EOF (1:1)", <-recorder.Events)
+	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to get workflow template: workflowtemplates.argoproj.io \"not-found\" not found", <-recorder.Events)
+	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to validate workflow template instanceid: 'my-wft-3' is not managed by the current Argo Server", <-recorder.Events)
+	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to evaluate workflow template expression: unexpected token Operator(\"!\") (1:8)\n | garbage!!!!!!\n | .......^", <-recorder.Events)
+	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: malformed workflow template expression: did not evaluate to boolean", <-recorder.Events)
+	assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to evaluate workflow template parameter \"my-param\" expression: unexpected token Operator(\"!\") (1:8)\n | rubbish!!!\n | .......^", <-recorder.Events)
 }
 
 func Test_expressionEnvironment(t *testing.T) {
-	env, err := expressionEnvironment(context.TODO(), "my-ns", "my-d", &wfv1.Item{Value: []byte(`"foo"`)})
+	env, err := expressionEnvironment(context.TODO(), "my-ns", "my-d", &wfv1.Item{Value: []byte(`{"foo":"bar"}`)})
 	if assert.NoError(t, err) {
 		assert.Equal(t, "my-ns", env["namespace"])
 		assert.Equal(t, "my-d", env["discriminator"])
 		assert.Contains(t, env, "metadata")
-		assert.Equal(t, "foo", env["payload"])
+		assert.Equal(t, map[string]interface{}{"foo": "bar"}, env["payload"], "make sure we parse an object as a map")
 	}
 }