Repository: argoproj/argo
Pull request #3219 (2020-06-11 18:54:24)

feat: Tick CLI Workflow watch even if there are no new events
Currently if we are watching a Workflow on the CLI and there are no new events from it, we don't refresh the screen. Since the screen contains a duration in seconds, we may perhaps be giving the impression that Workflow execution has stalled:

```
Name:                steps-vhd7c
Namespace:           argo
ServiceAccount:      default
Status:              Running
Created:             Thu Jun 11 11:51:33 -0700 (9 seconds ago)
Started:             Thu Jun 11 11:51:33 -0700 (9 seconds ago)
Duration:            9 seconds

STEP            TEMPLATE           PODNAME                 DURATION  MESSAGE
 ● steps-vhd7c  hello-hello-hello
 └---● hello1   whalesay           steps-vhd7c-3181874478  9s
```

If this screen does not refresh, it could give the impression that execution has stalled since `Duration: 9 seconds` is not refreshing.

With this change, if there are no new events at every second, we simply refresh the screen with updated durations.

alexec: could we tick client side?

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index da5fecf34..9398d0249 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -88,7 +88,7 @@ commands:
             echo '127.0.0.1 postgres' | sudo tee -a /etc/hosts
             echo '127.0.0.1 mysql'    | sudo tee -a /etc/hosts
             mkdir dist
-            KUBECONFIG=~/.kube/config make start DB=$DB E2E_EXECUTOR=docker DEV_IMAGE=true
+            KUBECONFIG=~/.kube/config make start PROFILE=$PROFILE E2E_EXECUTOR=docker DEV_IMAGE=true
             # CircleCI will kill spawned processes, so lets block for 20m
             sleep 20m
           background: true
@@ -185,16 +185,16 @@ jobs:
     machine:
       image: ubuntu-1604:201903-01
     environment:
-      DB: mysql
+      PROFILE: mysql
       SUITE: test-e2e-cron
     steps:
       - e2e
-  e2e-no-db:
+  e2e-minimal:
     working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo
     machine:
       image: ubuntu-1604:201903-01
     environment:
-      DB: no-db
+      PROFILE: minimal
       SUITE: test-e2e
     steps:
       - e2e
@@ -203,7 +203,7 @@ jobs:
     machine:
       image: ubuntu-1604:201903-01
     environment:
-      DB: postgres
+      PROFILE: postgres
       SUITE: test-e2e
     steps:
       - e2e
@@ -212,7 +212,7 @@ jobs:
     machine:
       image: ubuntu-1604:201903-01
     environment:
-      DB: mysql
+      PROFILE: mysql
       SUITE: test-e2e
     steps:
       - e2e
@@ -255,6 +255,6 @@ workflows:
       - codegen
       - e2e-cron
       - e2e-mysql
-      - e2e-no-db
+      - e2e-minimal
       - e2e-postgres
       - docker-build
diff --git a/Makefile b/Makefile
index 4aad08c28..2019e9d8b 100644
--- a/Makefile
+++ b/Makefile
@@ -70,18 +70,22 @@ CONTROLLER_IMAGE_FILE  := dist/controller-image.$(VERSION)
 # perform static compilation
 STATIC_BUILD          ?= true
 CI                    ?= false
-DB                    ?= no-db
-ifeq ($(CI),false)
+PROFILE               ?= minimal
 AUTH_MODE             := hybrid
-else
+ifeq ($(PROFILE),sso)
+AUTH_MODE             := sso
+endif
+ifeq ($(CI),true)
 AUTH_MODE             := client
 endif
 K3D                   := $(shell if [ "`which kubectl`" != '' ] && [ "`kubectl config current-context`" = "k3s-default" ]; then echo true; else echo false; fi)
 LOG_LEVEL             := debug
 
-ifeq ($(DB),no-db)
 ALWAYS_OFFLOAD_NODE_STATUS := false
-else
+ifeq ($(PROFILE),mysql)
+ALWAYS_OFFLOAD_NODE_STATUS := true
+endif
+ifeq ($(PROFILE),postgres)
 ALWAYS_OFFLOAD_NODE_STATUS := true
 endif
 
@@ -281,7 +285,7 @@ manifests:
 	./hack/update-image-tags.sh manifests/base $(MANIFESTS_VERSION)
 	kustomize build --load_restrictor=none manifests/cluster-install | ./hack/auto-gen-msg.sh > manifests/install.yaml
 	kustomize build --load_restrictor=none manifests/namespace-install | ./hack/auto-gen-msg.sh > manifests/namespace-install.yaml
-	kustomize build --load_restrictor=none manifests/quick-start/no-db | ./hack/auto-gen-msg.sh > manifests/quick-start-no-db.yaml
+	kustomize build --load_restrictor=none manifests/quick-start/minimal | ./hack/auto-gen-msg.sh > manifests/quick-start-minimal.yaml
 	kustomize build --load_restrictor=none manifests/quick-start/mysql | ./hack/auto-gen-msg.sh > manifests/quick-start-mysql.yaml
 	kustomize build --load_restrictor=none manifests/quick-start/postgres | ./hack/auto-gen-msg.sh > manifests/quick-start-postgres.yaml
 
@@ -323,22 +327,16 @@ $(VERSION_FILE):
 	@mkdir -p dist
 	touch $(VERSION_FILE)
 
-dist/$(DB).yaml: $(MANIFESTS) $(E2E_MANIFESTS) $(VERSION_FILE)
-	kustomize build --load_restrictor=none test/e2e/manifests/$(DB) | sed 's/:$(MANIFESTS_VERSION)/:$(VERSION)/' | sed 's/pns/$(E2E_EXECUTOR)/'  > dist/$(DB).yaml
+dist/$(PROFILE).yaml: $(MANIFESTS) $(E2E_MANIFESTS) $(VERSION_FILE)
+	kustomize build --load_restrictor=none test/e2e/manifests/$(PROFILE) | sed 's/:$(MANIFESTS_VERSION)/:$(VERSION)/' | sed 's/pns/$(E2E_EXECUTOR)/'  > dist/$(PROFILE).yaml
 
 .PHONY: install
-install: dist/$(DB).yaml
+install: dist/$(PROFILE).yaml
 ifeq ($(K3D),true)
 	k3d start
 endif
 	kubectl apply -f test/e2e/manifests/argo-ns.yaml
-	# If you want SSO, then we want Dex, otherwise we delete any previouly installed Dex.
-ifeq ($(AUTH_MODE),sso)
-	kubectl -n argo apply -l app.kubernetes.io/part-of=dex --prune --force -k manifests/quick-start/base/dex
-else
-	kubectl -n argo delete all -l app.kubernetes.io/part-of=dex
-endif
-	kubectl -n argo apply -l app.kubernetes.io/part-of=argo --prune --force -f dist/$(DB).yaml
+	kubectl -n argo apply -l app.kubernetes.io/part-of=argo --prune --force -f dist/$(PROFILE).yaml
 
 .PHONY: pull-build-images
 pull-build-images:
diff --git a/USERS.md b/USERS.md
index 1de2e6836..0c75333a9 100644
--- a/USERS.md
+++ b/USERS.md
@@ -63,6 +63,7 @@ Currently, the following organizations are **officially** using Argo Workflows:
 1. [Ravelin](https://www.ravelin.com/)
 1. [Red Hat](https://www.redhat.com/en)
 1. [Riskified](https://www.riskified.com)
+1. [Sage (Sage AI Labs)](https://sage.com/)
 1. [SAP Fieldglass](https://www.fieldglass.com/)
 1. [SAP Hybris](https://cx.sap.com/)
 1. [SegmentStream](https://segmentstream.com)
diff --git a/cmd/argo/commands/wait.go b/cmd/argo/commands/wait.go
index 4513b18bf..b6b82a98e 100644
--- a/cmd/argo/commands/wait.go
+++ b/cmd/argo/commands/wait.go
@@ -13,11 +13,11 @@ import (
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/fields"
 
 	"github.com/argoproj/argo/cmd/argo/commands/client"
 	workflowpkg "github.com/argoproj/argo/pkg/apiclient/workflow"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
+	"github.com/argoproj/argo/util"
 )
 
 func NewWaitCommand() *cobra.Command {
@@ -67,7 +67,7 @@ func waitOnOne(serviceClient workflowpkg.WorkflowServiceClient, ctx context.Cont
 	req := &workflowpkg.WatchWorkflowsRequest{
 		Namespace: namespace,
 		ListOptions: &metav1.ListOptions{
-			FieldSelector: fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", wfName)).String(),
+			FieldSelector: util.GenerateFieldSelectorFromWorkflowName(wfName),
 		},
 	}
 	stream, err := serviceClient.WatchWorkflows(ctx, req)
diff --git a/cmd/argo/commands/watch.go b/cmd/argo/commands/watch.go
index dda0d1352..3a12f7120 100644
--- a/cmd/argo/commands/watch.go
+++ b/cmd/argo/commands/watch.go
@@ -5,17 +5,18 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"time"
 
 	"github.com/argoproj/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/fields"
 
 	"github.com/argoproj/argo/cmd/argo/commands/client"
 	workflowpkg "github.com/argoproj/argo/pkg/apiclient/workflow"
 
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
+	"github.com/argoproj/argo/util"
 	"github.com/argoproj/argo/workflow/packer"
 )
 
@@ -33,7 +34,6 @@ func NewWatchCommand() *cobra.Command {
 				os.Exit(1)
 			}
 			watchWorkflow(args[0], getArgs)
-
 		},
 	}
 	command.Flags().StringVar(&getArgs.status, "status", "", "Filter by status (Pending, Running, Succeeded, Skipped, Failed, Error)")
@@ -51,24 +51,40 @@ func watchWorkflow(wfName string, getArgs getFlags) {
 	req := &workflowpkg.WatchWorkflowsRequest{
 		Namespace: namespace,
 		ListOptions: &metav1.ListOptions{
-			FieldSelector: fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", wfName)).String(),
+			FieldSelector: util.GenerateFieldSelectorFromWorkflowName(wfName),
 		},
 	}
 	stream, err := serviceClient.WatchWorkflows(ctx, req)
 	errors.CheckError(err)
-	for {
-		event, err := stream.Recv()
-		if err == io.EOF {
-			log.Debug("Re-establishing workflow watch")
-			stream, err = serviceClient.WatchWorkflows(ctx, req)
+
+	wfChan := make(chan *wfv1.Workflow)
+	go func() {
+		for {
+			event, err := stream.Recv()
+			if err == io.EOF {
+				log.Debug("Re-establishing workflow watch")
+				stream, err = serviceClient.WatchWorkflows(ctx, req)
+				errors.CheckError(err)
+			}
 			errors.CheckError(err)
-			continue
+			wfChan <- event.Object
 		}
-		errors.CheckError(err)
-		wf := event.Object
-		if wf == nil {
-			break
+	}()
+
+	var wf *wfv1.Workflow
+	ticker := time.NewTicker(time.Second)
+	for {
+		select {
+		case newWf := <-wfChan:
+			// If we get a new event, update our workflow
+			if newWf == nil {
+				return
+			}
+			wf = newWf
+		case <-ticker.C:
+			// If we don't, refresh the workflow screen every second
 		}
+
 		printWorkflowStatus(wf, getArgs)
 		if !wf.Status.FinishedAt.IsZero() {
 			return
@@ -77,6 +93,9 @@ func watchWorkflow(wfName string, getArgs getFlags) {
 }
 
 func printWorkflowStatus(wf *wfv1.Workflow, getArgs getFlags) {
+	if wf == nil {
+		return
+	}
 	err := packer.DecompressWorkflow(wf)
 	errors.CheckError(err)
 	print("\033[H\033[2J")
diff --git a/config/config.go b/config/config.go
index 94deeefce..f55ad6ce6 100644
--- a/config/config.go
+++ b/config/config.go
@@ -14,6 +14,9 @@ type Config struct {
 	// SSO in settings for single-sign on
 	SSO sso.Config `json:"sso,omitempty"`
 
+	// NodeEvents configures how node events are omitted
+	NodeEvents NodeEvents `json:"nodeEvents,omitempty"`
+
 	// ExecutorImage is the image name of the executor to use when running pods
 	// DEPRECATED: use --executor-image flag to workflow-controller instead
 	ExecutorImage string `json:"executorImage,omitempty"`
diff --git a/config/node_events.go b/config/node_events.go
new file mode 100644
index 000000000..51c76e263
--- /dev/null
+++ b/config/node_events.go
@@ -0,0 +1,9 @@
+package config
+
+type NodeEvents struct {
+	Enabled *bool `json:"enabled,omitempty"`
+}
+
+func (e NodeEvents) IsEnabled() bool {
+	return e.Enabled == nil || *e.Enabled
+}
diff --git a/docs/workflow-controller-configmap.yaml b/docs/workflow-controller-configmap.yaml
index 8e7078f5b..118224c21 100644
--- a/docs/workflow-controller-configmap.yaml
+++ b/docs/workflow-controller-configmap.yaml
@@ -18,6 +18,14 @@ data:
     # (available since Argo v2.3)
     parallelism: 10
 
+    # Whether or not to emit events on node completion. These can take a up a lot of space in
+    # k8s (typically etcd) resulting in errors when trying to create new events:
+    # "Unable to create audit event: etcdserver: mvcc: database space exceeded"
+    # This config item allows you to disable this.
+    # (since v2.9)
+    nodeEvents:
+      enabled: true
+
     # uncomment flowing lines if workflow controller runs in a different k8s cluster with the 
     # workflow workloads, or needs to communicate with the k8s apiserver using an out-of-cluster
     # kubeconfig secret
@@ -193,3 +201,22 @@ data:
         ttlStrategy:
           secondsAfterSuccess: 5
         parallelism: 3
+
+    # SSO Configuration for the Argo server.
+    sso:
+      # This is the root URL of the OIDC provider (required).
+      issuer: https://issuer.root.url/
+      # This is name of the secret and the key in it that contain OIDC client
+      # ID issued to the application by the provider (required).
+      clientId:
+        name: client-id-secret
+        key: client-id-key
+      # This is name of the secret and the key in it that contain OIDC client
+      # secret issued to the application by the provider (required).
+      clientSecret:
+        name: client-secret-secret
+        key: client-secret-key
+      # This is the redirect URL supplied to the provider (required). It must
+      # be in the form <argo-server-root-url>/oauth2/callback. It must be
+      # browser-accessible.
+      redirectUrl: https://argo-server/oauth2/callback
diff --git a/go.mod b/go.mod
index 6b27be490..c692e0772 100644
--- a/go.mod
+++ b/go.mod
@@ -49,6 +49,7 @@ require (
 	github.com/lib/pq v1.3.0 // indirect
 	github.com/mailru/easyjson v0.7.1 // indirect
 	github.com/mattn/go-colorable v0.1.4 // indirect
+	github.com/mattn/go-isatty v0.0.10 // indirect
 	github.com/minio/minio-go v6.0.14+incompatible // indirect
 	github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index c6003812a..7e788531f 100644
--- a/go.sum
+++ b/go.sum
@@ -337,6 +337,8 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -601,6 +603,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/manifests/README.md b/manifests/README.md
index dc2a8f668..974f73b75 100644
--- a/manifests/README.md
+++ b/manifests/README.md
@@ -6,8 +6,8 @@ Several sets of manifests are provided:
 |------|-------------|
 | [install.yaml](install.yaml) | Standard argo cluster-wide installation. Controller operates on all namespaces |
 | [namespace-install.yaml](namespace-install.yaml) | Installation of argo which operates on a single namespace. Controller does not require to be run with clusterrole. Installs to `argo` namespace as an example. |
+| [quick-start-minimal.yaml](quick-start-minimal.yaml) | Quick start including MinIO. Suitable for testing. |
 | [quick-start-mysql.yaml](quick-start-mysql.yaml) | Quick start including MinIO and MySQL. Suitable for testing. |
-| [quick-start-no-db.yaml](quick-start-no-db.yaml) | Quick start including MinIO. Suitable for testing. |
 | [quick-start-postgres.yaml](quick-start-postgres.yaml) | Quick start including MinIO and Postgres. Suitable for testing. |
 
 If installing with `kubectl install -f https://...`, remember to use the link to the file's raw version.
diff --git a/manifests/cluster-install/kustomization.yaml b/manifests/cluster-install/kustomization.yaml
index eb60d905e..a1698bd2a 100644
--- a/manifests/cluster-install/kustomization.yaml
+++ b/manifests/cluster-install/kustomization.yaml
@@ -1,7 +1,7 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
 - ../base
 - ./workflow-controller-rbac
 - ./argo-server-rbac
diff --git a/manifests/cluster-install/workflow-controller-rbac/workflow-controller-clusterrole.yaml b/manifests/cluster-install/workflow-controller-rbac/workflow-controller-clusterrole.yaml
index e5e78d239..169168378 100644
--- a/manifests/cluster-install/workflow-controller-rbac/workflow-controller-clusterrole.yaml
+++ b/manifests/cluster-install/workflow-controller-rbac/workflow-controller-clusterrole.yaml
@@ -80,6 +80,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
     - "policy"
   resources:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index 1dcf2702b..36811d84c 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -250,6 +250,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - policy
   resources:
diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml
index 47f2b70be..30a7eb1b3 100644
--- a/manifests/namespace-install.yaml
+++ b/manifests/namespace-install.yaml
@@ -163,6 +163,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - policy
   resources:
diff --git a/manifests/namespace-install/kustomization.yaml b/manifests/namespace-install/kustomization.yaml
index c6006075c..8e55646bc 100644
--- a/manifests/namespace-install/kustomization.yaml
+++ b/manifests/namespace-install/kustomization.yaml
@@ -1,12 +1,11 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
   - ../base
   - ./argo-server-rbac
   - ./workflow-controller-rbac
 
-
 patchesJson6902:
   - target:
       version: v1
diff --git a/manifests/namespace-install/workflow-controller-rbac/workflow-controller-role.yaml b/manifests/namespace-install/workflow-controller-rbac/workflow-controller-role.yaml
index 9677d76a2..5556ab739 100644
--- a/manifests/namespace-install/workflow-controller-rbac/workflow-controller-role.yaml
+++ b/manifests/namespace-install/workflow-controller-rbac/workflow-controller-role.yaml
@@ -84,6 +84,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
     - "policy"
   resources:
diff --git a/manifests/quick-start-no-db.yaml b/manifests/quick-start-minimal.yaml
similarity index 85%
rename from manifests/quick-start-no-db.yaml
rename to manifests/quick-start-minimal.yaml
index d8f0283ae..8f41294d5 100644
--- a/manifests/quick-start-no-db.yaml
+++ b/manifests/quick-start-minimal.yaml
@@ -2,8 +2,6 @@
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: clusterworkflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -19,8 +17,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: cronworkflows.argoproj.io
 spec:
   group: argoproj.io
@@ -36,8 +32,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflows.argoproj.io
 spec:
   additionalPrinterColumns:
@@ -62,8 +56,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -78,22 +70,16 @@ spec:
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo
 ---
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-role
 rules:
 - apiGroups:
@@ -177,6 +163,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - policy
   resources:
@@ -189,8 +176,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-role
 rules:
 - apiGroups:
@@ -242,8 +227,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-role
 rules:
 - apiGroups:
@@ -265,8 +248,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -282,8 +263,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -302,8 +281,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -316,8 +293,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -330,8 +305,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-default-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -344,8 +317,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -359,8 +330,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -386,8 +355,6 @@ data:
         key: secretkey
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: artifact-repositories
 ---
 apiVersion: v1
@@ -416,26 +383,16 @@ data:
     enabled: true
     path: /metrics
     port: 9090
-  sso: |
-    issuer: http://dex:5556/dex
-    clientId: argo-server
-    clientSecret:
-      name: argo-server-sso
-      key: clientSecret
-    redirectUrl: https://localhost:2746/oauth2/callback
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-configmap
 ---
 apiVersion: v1
 kind: Secret
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-sso
 stringData:
+  clientID: argo-server
   clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
 ---
 apiVersion: v1
@@ -443,7 +400,6 @@ kind: Secret
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: my-minio-cred
 stringData:
   accesskey: admin
@@ -453,8 +409,6 @@ type: Opaque
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   ports:
@@ -463,14 +417,12 @@ spec:
     targetPort: 2746
   selector:
     app: argo-server
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   ports:
@@ -479,13 +431,10 @@ spec:
     targetPort: 9000
   selector:
     app: minio
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-metrics
 spec:
   ports:
@@ -495,24 +444,19 @@ spec:
     targetPort: 9090
   selector:
     app: workflow-controller
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   selector:
     matchLabels:
       app: argo-server
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: argo-server
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -541,19 +485,15 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller
 spec:
   selector:
     matchLabels:
       app: workflow-controller
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: workflow-controller
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -575,7 +515,6 @@ kind: Pod
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   containers:
diff --git a/manifests/quick-start-mysql.yaml b/manifests/quick-start-mysql.yaml
index 587aa8c55..5e53202d4 100644
--- a/manifests/quick-start-mysql.yaml
+++ b/manifests/quick-start-mysql.yaml
@@ -2,8 +2,6 @@
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: clusterworkflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -19,8 +17,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: cronworkflows.argoproj.io
 spec:
   group: argoproj.io
@@ -36,8 +32,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflows.argoproj.io
 spec:
   additionalPrinterColumns:
@@ -62,8 +56,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -78,22 +70,16 @@ spec:
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo
 ---
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-role
 rules:
 - apiGroups:
@@ -177,6 +163,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - policy
   resources:
@@ -189,8 +176,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-role
 rules:
 - apiGroups:
@@ -242,8 +227,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-role
 rules:
 - apiGroups:
@@ -265,8 +248,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -282,8 +263,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -302,8 +281,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -316,8 +293,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -330,8 +305,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-default-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -344,8 +317,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -359,8 +330,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -386,8 +355,6 @@ data:
         key: secretkey
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: artifact-repositories
 ---
 apiVersion: v1
@@ -435,17 +402,8 @@ data:
       passwordSecret:
         name: argo-mysql-config
         key: password
-  sso: |
-    issuer: http://dex:5556/dex
-    clientId: argo-server
-    clientSecret:
-      name: argo-server-sso
-      key: clientSecret
-    redirectUrl: https://localhost:2746/oauth2/callback
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-configmap
 ---
 apiVersion: v1
@@ -453,7 +411,6 @@ kind: Secret
 metadata:
   labels:
     app: mysql
-    app.kubernetes.io/part-of: argo
   name: argo-mysql-config
 stringData:
   password: password
@@ -463,10 +420,9 @@ type: Opaque
 apiVersion: v1
 kind: Secret
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-sso
 stringData:
+  clientID: argo-server
   clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
 ---
 apiVersion: v1
@@ -474,7 +430,6 @@ kind: Secret
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: my-minio-cred
 stringData:
   accesskey: admin
@@ -484,8 +439,6 @@ type: Opaque
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   ports:
@@ -494,14 +447,12 @@ spec:
     targetPort: 2746
   selector:
     app: argo-server
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   ports:
@@ -510,14 +461,12 @@ spec:
     targetPort: 9000
   selector:
     app: minio
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: mysql
-    app.kubernetes.io/part-of: argo
   name: mysql
 spec:
   ports:
@@ -526,13 +475,10 @@ spec:
     targetPort: 3306
   selector:
     app: mysql
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-metrics
 spec:
   ports:
@@ -542,24 +488,19 @@ spec:
     targetPort: 9090
   selector:
     app: workflow-controller
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   selector:
     matchLabels:
       app: argo-server
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: argo-server
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -590,18 +531,15 @@ kind: Deployment
 metadata:
   labels:
     app: mysql
-    app.kubernetes.io/part-of: argo
   name: mysql
 spec:
   selector:
     matchLabels:
       app: mysql
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: mysql
-        app.kubernetes.io/part-of: argo
       name: mysql
     spec:
       containers:
@@ -636,19 +574,15 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller
 spec:
   selector:
     matchLabels:
       app: workflow-controller
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: workflow-controller
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -670,7 +604,6 @@ kind: Pod
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   containers:
diff --git a/manifests/quick-start-postgres.yaml b/manifests/quick-start-postgres.yaml
index 3d500cef7..7390dd522 100644
--- a/manifests/quick-start-postgres.yaml
+++ b/manifests/quick-start-postgres.yaml
@@ -2,8 +2,6 @@
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: clusterworkflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -19,8 +17,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: cronworkflows.argoproj.io
 spec:
   group: argoproj.io
@@ -36,8 +32,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflows.argoproj.io
 spec:
   additionalPrinterColumns:
@@ -62,8 +56,6 @@ spec:
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflowtemplates.argoproj.io
 spec:
   group: argoproj.io
@@ -78,22 +70,16 @@ spec:
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo
 ---
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-role
 rules:
 - apiGroups:
@@ -177,6 +163,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - policy
   resources:
@@ -189,8 +176,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-role
 rules:
 - apiGroups:
@@ -242,8 +227,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-role
 rules:
 - apiGroups:
@@ -265,8 +248,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -282,8 +263,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role
 rules:
 - apiGroups:
@@ -302,8 +281,6 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -316,8 +293,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -330,8 +305,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-default-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -344,8 +317,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -359,8 +330,6 @@ subjects:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-clusterworkflowtemplate-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
@@ -386,8 +355,6 @@ data:
         key: secretkey
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: artifact-repositories
 ---
 apiVersion: v1
@@ -435,17 +402,8 @@ data:
       passwordSecret:
         name: argo-postgres-config
         key: password
-  sso: |
-    issuer: http://dex:5556/dex
-    clientId: argo-server
-    clientSecret:
-      name: argo-server-sso
-      key: clientSecret
-    redirectUrl: https://localhost:2746/oauth2/callback
 kind: ConfigMap
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-configmap
 ---
 apiVersion: v1
@@ -453,7 +411,6 @@ kind: Secret
 metadata:
   labels:
     app: postgres
-    app.kubernetes.io/part-of: argo
   name: argo-postgres-config
 stringData:
   password: password
@@ -463,10 +420,9 @@ type: Opaque
 apiVersion: v1
 kind: Secret
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server-sso
 stringData:
+  clientID: argo-server
   clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
 ---
 apiVersion: v1
@@ -474,7 +430,6 @@ kind: Secret
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: my-minio-cred
 stringData:
   accesskey: admin
@@ -484,8 +439,6 @@ type: Opaque
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   ports:
@@ -494,14 +447,12 @@ spec:
     targetPort: 2746
   selector:
     app: argo-server
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   ports:
@@ -510,14 +461,12 @@ spec:
     targetPort: 9000
   selector:
     app: minio
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: postgres
-    app.kubernetes.io/part-of: argo
   name: postgres
 spec:
   ports:
@@ -526,13 +475,10 @@ spec:
     targetPort: 5432
   selector:
     app: postgres
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller-metrics
 spec:
   ports:
@@ -542,24 +488,19 @@ spec:
     targetPort: 9090
   selector:
     app: workflow-controller
-    app.kubernetes.io/part-of: argo
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: argo-server
 spec:
   selector:
     matchLabels:
       app: argo-server
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: argo-server
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -590,18 +531,15 @@ kind: Deployment
 metadata:
   labels:
     app: postgres
-    app.kubernetes.io/part-of: argo
   name: postgres
 spec:
   selector:
     matchLabels:
       app: postgres
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: postgres
-        app.kubernetes.io/part-of: argo
       name: postgres
     spec:
       containers:
@@ -628,19 +566,15 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  labels:
-    app.kubernetes.io/part-of: argo
   name: workflow-controller
 spec:
   selector:
     matchLabels:
       app: workflow-controller
-      app.kubernetes.io/part-of: argo
   template:
     metadata:
       labels:
         app: workflow-controller
-        app.kubernetes.io/part-of: argo
     spec:
       containers:
       - args:
@@ -662,7 +596,6 @@ kind: Pod
 metadata:
   labels:
     app: minio
-    app.kubernetes.io/part-of: argo
   name: minio
 spec:
   containers:
diff --git a/manifests/quick-start/base/argo-server-sso-secret.yaml b/manifests/quick-start/base/argo-server-sso-secret.yaml
index 352d16c04..173d6b314 100644
--- a/manifests/quick-start/base/argo-server-sso-secret.yaml
+++ b/manifests/quick-start/base/argo-server-sso-secret.yaml
@@ -3,4 +3,5 @@ apiVersion: v1
 metadata:
   name: argo-server-sso
 stringData:
-  clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
\ No newline at end of file
+  clientID: argo-server
+  clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
diff --git a/manifests/quick-start/base/kustomization.yaml b/manifests/quick-start/base/kustomization.yaml
index 622efdc27..a059066d1 100644
--- a/manifests/quick-start/base/kustomization.yaml
+++ b/manifests/quick-start/base/kustomization.yaml
@@ -1,11 +1,9 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
   - ../../namespace-install
   - minio
-
-resources:
   - argo-server-sso-secret.yaml
   - workflow-role.yaml
   - workflow-default-rolebinding.yaml
diff --git a/manifests/quick-start/base/overlays/workflow-controller-configmap.yaml b/manifests/quick-start/base/overlays/workflow-controller-configmap.yaml
index fc15683b4..61b856f70 100644
--- a/manifests/quick-start/base/overlays/workflow-controller-configmap.yaml
+++ b/manifests/quick-start/base/overlays/workflow-controller-configmap.yaml
@@ -1,12 +1,5 @@
 apiVersion: v1
 data:
-  sso : |
-    issuer: http://dex:5556/dex
-    clientId: argo-server
-    clientSecret:
-      name: argo-server-sso
-      key: clientSecret
-    redirectUrl: https://localhost:2746/oauth2/callback
   artifactRepository: |
     archiveLogs: true
     s3:
diff --git a/manifests/quick-start/no-db/kustomization.yaml b/manifests/quick-start/minimal/kustomization.yaml
similarity index 56%
rename from manifests/quick-start/no-db/kustomization.yaml
rename to manifests/quick-start/minimal/kustomization.yaml
index 2e69cbc4f..aa0b761e8 100644
--- a/manifests/quick-start/no-db/kustomization.yaml
+++ b/manifests/quick-start/minimal/kustomization.yaml
@@ -1,8 +1,5 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
   - ../base
-
-commonLabels:
-  "app.kubernetes.io/part-of": "argo"
\ No newline at end of file
diff --git a/manifests/quick-start/mysql/kustomization.yaml b/manifests/quick-start/mysql/kustomization.yaml
index 4aa85554e..97b3ca0a1 100644
--- a/manifests/quick-start/mysql/kustomization.yaml
+++ b/manifests/quick-start/mysql/kustomization.yaml
@@ -1,13 +1,8 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
-  - ../base
-
-commonLabels:
-  "app.kubernetes.io/part-of": "argo"
-
 resources:
+  - ../base
   - argo-mysql-config-secret.yaml
   - mysql-deployment.yaml
   - mysql-service.yaml
diff --git a/manifests/quick-start/postgres/kustomization.yaml b/manifests/quick-start/postgres/kustomization.yaml
index c5ff1363d..b039183f4 100644
--- a/manifests/quick-start/postgres/kustomization.yaml
+++ b/manifests/quick-start/postgres/kustomization.yaml
@@ -1,13 +1,8 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
-  - ../base
-
-commonLabels:
-  "app.kubernetes.io/part-of": "argo"
-
 resources:
+  - ../base
   - argo-postgres-config-secret.yaml
   - postgres-deployment.yaml
   - postgres-service.yaml
diff --git a/manifests/quick-start/base/dex/dev-svc.yaml b/manifests/quick-start/sso/dex/dev-svc.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dev-svc.yaml
rename to manifests/quick-start/sso/dex/dev-svc.yaml
diff --git a/manifests/quick-start/base/dex/dex-cm.yaml b/manifests/quick-start/sso/dex/dex-cm.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dex-cm.yaml
rename to manifests/quick-start/sso/dex/dex-cm.yaml
diff --git a/manifests/quick-start/base/dex/dex-deploy.yaml b/manifests/quick-start/sso/dex/dex-deploy.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dex-deploy.yaml
rename to manifests/quick-start/sso/dex/dex-deploy.yaml
diff --git a/manifests/quick-start/base/dex/dex-rb.yaml b/manifests/quick-start/sso/dex/dex-rb.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dex-rb.yaml
rename to manifests/quick-start/sso/dex/dex-rb.yaml
diff --git a/manifests/quick-start/base/dex/dex-role.yaml b/manifests/quick-start/sso/dex/dex-role.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dex-role.yaml
rename to manifests/quick-start/sso/dex/dex-role.yaml
diff --git a/manifests/quick-start/base/dex/dex-sa.yaml b/manifests/quick-start/sso/dex/dex-sa.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/dex-sa.yaml
rename to manifests/quick-start/sso/dex/dex-sa.yaml
diff --git a/manifests/quick-start/base/dex/kustomization.yaml b/manifests/quick-start/sso/dex/kustomization.yaml
similarity index 100%
rename from manifests/quick-start/base/dex/kustomization.yaml
rename to manifests/quick-start/sso/dex/kustomization.yaml
diff --git a/manifests/quick-start/sso/kustomization.yaml b/manifests/quick-start/sso/kustomization.yaml
new file mode 100644
index 000000000..b8cb89158
--- /dev/null
+++ b/manifests/quick-start/sso/kustomization.yaml
@@ -0,0 +1,9 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+  - ../base
+  - dex
+
+patchesStrategicMerge:
+  - overlays/workflow-controller-configmap.yaml
\ No newline at end of file
diff --git a/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml
new file mode 100644
index 000000000..88e6f69ff
--- /dev/null
+++ b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+data:
+  sso : |
+    issuer: http://dex:5556/dex
+    clientId:
+      name: argo-server-sso
+      key: clientID
+    clientSecret:
+      name: argo-server-sso
+      key: clientSecret
+    redirectUrl: https://localhost:2746/oauth2/callback
+kind: ConfigMap
+metadata:
+  name: workflow-controller-configmap
diff --git a/pkg/apis/workflow/v1alpha1/workflow_types.go b/pkg/apis/workflow/v1alpha1/workflow_types.go
index 7ed053254..fc63dc354 100644
--- a/pkg/apis/workflow/v1alpha1/workflow_types.go
+++ b/pkg/apis/workflow/v1alpha1/workflow_types.go
@@ -1139,6 +1139,8 @@ const (
 	ConditionTypeCompleted ConditionType = "Completed"
 	// ConditionTypeSpecWarning is a warning on the current application spec
 	ConditionTypeSpecWarning ConditionType = "SpecWarning"
+	// ConditionTypeSpecWarning is an error on the current application spec
+	ConditionTypeSpecError ConditionType = "SpecError"
 	// ConditionTypeMetricsError is an error during metric emission
 	ConditionTypeMetricsError ConditionType = "MetricsError"
 )
diff --git a/server/auth/sso/sso.go b/server/auth/sso/sso.go
index 0140c2664..39e83cb35 100644
--- a/server/auth/sso/sso.go
+++ b/server/auth/sso/sso.go
@@ -40,17 +40,42 @@ type sso struct {
 
 type Config struct {
 	Issuer       string                  `json:"issuer"`
-	ClientID     string                  `json:"clientId"`
+	ClientID     apiv1.SecretKeySelector `json:"clientId"`
 	ClientSecret apiv1.SecretKeySelector `json:"clientSecret"`
 	RedirectURL  string                  `json:"redirectUrl"`
 }
 
+// Abtsract methods of oidc.Provider that our code uses into an interface. That
+// will allow us to implement a stub for unit testing.  If you start using more
+// oidc.Provider methods in this file, add them here and provide a stub
+// implementation in test.
+type providerInterface interface {
+	Endpoint() oauth2.Endpoint
+	Verifier(config *oidc.Config) *oidc.IDTokenVerifier
+}
+
+type providerFactory func(ctx context.Context, issuer string) (providerInterface, error)
+
+func providerFactoryOIDC(ctx context.Context, issuer string) (providerInterface, error) {
+	return oidc.NewProvider(ctx, issuer)
+}
+
 func New(c Config, secretsIf corev1.SecretInterface, baseHRef string, secure bool) (Interface, error) {
+	return newSso(providerFactoryOIDC, c, secretsIf, baseHRef, secure)
+}
+
+func newSso(
+	factory providerFactory,
+	c Config,
+	secretsIf corev1.SecretInterface,
+	baseHRef string,
+	secure bool,
+) (Interface, error) {
 	if c.Issuer == "" {
 		return nil, fmt.Errorf("issuer empty")
 	}
-	if c.ClientID == "" {
-		return nil, fmt.Errorf("clientId empty")
+	if c.ClientID.Name == "" || c.ClientID.Key == "" {
+		return nil, fmt.Errorf("clientID empty")
 	}
 	if c.ClientSecret.Name == "" || c.ClientSecret.Key == "" {
 		return nil, fmt.Errorf("clientSecret empty")
@@ -58,17 +83,36 @@ func New(c Config, secretsIf corev1.SecretInterface, baseHRef string, secure boo
 	if c.RedirectURL == "" {
 		return nil, fmt.Errorf("redirectUrl empty")
 	}
-	secrets, err := secretsIf.Get(c.ClientSecret.Name, metav1.GetOptions{})
+	clientSecretObj, err := secretsIf.Get(c.ClientSecret.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
-	provider, err := oidc.NewProvider(context.Background(), c.Issuer)
+	provider, err := factory(context.Background(), c.Issuer)
 	if err != nil {
 		return nil, err
 	}
+
+	var clientIDObj *apiv1.Secret
+	if c.ClientID.Name == c.ClientSecret.Name {
+		clientIDObj = clientSecretObj
+	} else {
+		clientIDObj, err = secretsIf.Get(c.ClientID.Name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+	}
+	clientID := clientIDObj.Data[c.ClientID.Key]
+	if clientID == nil {
+		return nil, fmt.Errorf("key %s missing in secret %s", c.ClientID.Key, c.ClientID.Name)
+	}
+	clientSecret := clientSecretObj.Data[c.ClientSecret.Key]
+	if clientSecret == nil {
+		return nil, fmt.Errorf("key %s missing in secret %s", c.ClientSecret.Key, c.ClientSecret.Name)
+	}
+
 	config := &oauth2.Config{
-		ClientID:     c.ClientID,
-		ClientSecret: string(secrets.Data[c.ClientSecret.Key]),
+		ClientID:     string(clientID),
+		ClientSecret: string(clientSecret),
 		RedirectURL:  c.RedirectURL,
 		Endpoint:     provider.Endpoint(),
 		Scopes:       []string{oidc.ScopeOpenID, "groups"},
diff --git a/server/workflow/workflow_server.go b/server/workflow/workflow_server.go
index 2692136ec..e8121ee9c 100644
--- a/server/workflow/workflow_server.go
+++ b/server/workflow/workflow_server.go
@@ -5,6 +5,8 @@ import (
 	"reflect"
 	"sort"
 
+	"github.com/argoproj/argo/pkg/client/clientset/versioned"
+
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/net/context"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -15,6 +17,7 @@ import (
 	"github.com/argoproj/argo/pkg/apis/workflow"
 	"github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/server/auth"
+	argoutil "github.com/argoproj/argo/util"
 	"github.com/argoproj/argo/util/instanceid"
 	"github.com/argoproj/argo/util/logs"
 	"github.com/argoproj/argo/workflow/common"
@@ -30,6 +33,8 @@ type workflowServer struct {
 	hydrator              hydrator.Interface
 }
 
+const latestAlias = "@latest"
+
 // NewWorkflowServer returns a new workflowServer
 func NewWorkflowServer(instanceIDService instanceid.Service, offloadNodeStatusRepo sqldb.OffloadNodeStatusRepo) workflowpkg.WorkflowServiceServer {
 	return &workflowServer{instanceIDService, offloadNodeStatusRepo, hydrator.New(offloadNodeStatusRepo)}
@@ -80,7 +85,12 @@ func (s *workflowServer) GetWorkflow(ctx context.Context, req *workflowpkg.Workf
 	if req.GetOptions != nil {
 		wfGetOption = *req.GetOptions
 	}
-	wf, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, wfGetOption)
+	wfClient := auth.GetWfClient(ctx)
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, wfGetOption)
+	if err != nil {
+		return nil, err
+	}
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
@@ -122,7 +132,7 @@ func (s *workflowServer) ListWorkflows(ctx context.Context, req *workflowpkg.Wor
 	// we make no promises about the overall list sorting, we just sort each page
 	sort.Sort(wfList.Items)
 
-	return &v1alpha1.WorkflowList{ListMeta: metav1.ListMeta{Continue: wfList.Continue}, Items: wfList.Items}, nil
+	return &v1alpha1.WorkflowList{ListMeta: metav1.ListMeta{Continue: wfList.Continue, ResourceVersion: wfList.ResourceVersion}, Items: wfList.Items}, nil
 }
 
 func (s *workflowServer) WatchWorkflows(req *workflowpkg.WatchWorkflowsRequest, ws workflowpkg.WorkflowService_WatchWorkflowsServer) error {
@@ -131,6 +141,14 @@ func (s *workflowServer) WatchWorkflows(req *workflowpkg.WatchWorkflowsRequest,
 	opts := &metav1.ListOptions{}
 	if req.ListOptions != nil {
 		opts = req.ListOptions
+		wfName := argoutil.RecoverWorkflowNameFromSelectorString(opts.FieldSelector)
+		if len(wfName) > 0 {
+			wf, err := s.getWorkflow(wfClient, req.Namespace, wfName, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+			opts.FieldSelector = argoutil.GenerateFieldSelectorFromWorkflowName(wf.Name)
+		}
 	}
 	s.instanceIDService.With(opts)
 	wfIf := wfClient.ArgoprojV1alpha1().Workflows(req.Namespace)
@@ -176,11 +194,16 @@ func (s *workflowServer) WatchWorkflows(req *workflowpkg.WatchWorkflowsRequest,
 }
 
 func (s *workflowServer) DeleteWorkflow(ctx context.Context, req *workflowpkg.WorkflowDeleteRequest) (*workflowpkg.WorkflowDeleteResponse, error) {
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wfClient := auth.GetWfClient(ctx)
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
-	err = auth.GetWfClient(ctx).ArgoprojV1alpha1().Workflows(req.Namespace).Delete(req.Name, &metav1.DeleteOptions{})
+	err = auth.GetWfClient(ctx).ArgoprojV1alpha1().Workflows(wf.Namespace).Delete(wf.Name, &metav1.DeleteOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -191,7 +214,12 @@ func (s *workflowServer) RetryWorkflow(ctx context.Context, req *workflowpkg.Wor
 	wfClient := auth.GetWfClient(ctx)
 	kubeClient := auth.GetKubeClient(ctx)
 
-	wf, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
@@ -205,7 +233,12 @@ func (s *workflowServer) RetryWorkflow(ctx context.Context, req *workflowpkg.Wor
 
 func (s *workflowServer) ResubmitWorkflow(ctx context.Context, req *workflowpkg.WorkflowResubmitRequest) (*v1alpha1.Workflow, error) {
 	wfClient := auth.GetWfClient(ctx)
-	wf, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
@@ -224,18 +257,23 @@ func (s *workflowServer) ResubmitWorkflow(ctx context.Context, req *workflowpkg.
 
 func (s *workflowServer) ResumeWorkflow(ctx context.Context, req *workflowpkg.WorkflowResumeRequest) (*v1alpha1.Workflow, error) {
 	wfClient := auth.GetWfClient(ctx)
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
 
-	err = util.ResumeWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), s.hydrator, req.Name, req.NodeFieldSelector)
+	err = util.ResumeWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), s.hydrator, wf.Name, req.NodeFieldSelector)
 	if err != nil {
-		log.Warnf("Failed to resume %s: %+v", req.Name, err)
+		log.Warnf("Failed to resume %s: %+v", wf.Name, err)
 		return nil, err
 	}
 
-	wf, err := wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(req.Name, metav1.GetOptions{})
+	wf, err = wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(wf.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -246,17 +284,22 @@ func (s *workflowServer) ResumeWorkflow(ctx context.Context, req *workflowpkg.Wo
 func (s *workflowServer) SuspendWorkflow(ctx context.Context, req *workflowpkg.WorkflowSuspendRequest) (*v1alpha1.Workflow, error) {
 	wfClient := auth.GetWfClient(ctx)
 
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
 
-	err = util.SuspendWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), req.Name)
+	err = util.SuspendWorkflow(wfClient.ArgoprojV1alpha1().Workflows(wf.Namespace), wf.Name)
 	if err != nil {
 		return nil, err
 	}
 
-	wf, err := wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(req.Name, metav1.GetOptions{})
+	wf, err = wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(wf.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -267,17 +310,22 @@ func (s *workflowServer) SuspendWorkflow(ctx context.Context, req *workflowpkg.W
 func (s *workflowServer) TerminateWorkflow(ctx context.Context, req *workflowpkg.WorkflowTerminateRequest) (*v1alpha1.Workflow, error) {
 	wfClient := auth.GetWfClient(ctx)
 
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.validateWorkflow(wf)
 	if err != nil {
 		return nil, err
 	}
 
-	err = util.TerminateWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), req.Name)
+	err = util.TerminateWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), wf.Name)
 	if err != nil {
 		return nil, err
 	}
 
-	wf, err := wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(req.Name, metav1.GetOptions{})
+	wf, err = wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(wf.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -286,16 +334,20 @@ func (s *workflowServer) TerminateWorkflow(ctx context.Context, req *workflowpkg
 
 func (s *workflowServer) StopWorkflow(ctx context.Context, req *workflowpkg.WorkflowStopRequest) (*v1alpha1.Workflow, error) {
 	wfClient := auth.GetWfClient(ctx)
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
-	err = util.StopWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), s.hydrator, req.Name, req.NodeFieldSelector, req.Message)
+	err = s.validateWorkflow(wf)
+	if err != nil {
+		return nil, err
+	}
+	err = util.StopWorkflow(wfClient.ArgoprojV1alpha1().Workflows(req.Namespace), s.hydrator, wf.Name, req.NodeFieldSelector, req.Message)
 	if err != nil {
 		return nil, err
 	}
 
-	wf, err := wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(req.Name, metav1.GetOptions{})
+	wf, err = wfClient.ArgoprojV1alpha1().Workflows(req.Namespace).Get(wf.Name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -321,24 +373,53 @@ func (s *workflowServer) PodLogs(req *workflowpkg.WorkflowLogRequest, ws workflo
 	ctx := ws.Context()
 	wfClient := auth.GetWfClient(ctx)
 	kubeClient := auth.GetKubeClient(ctx)
-	_, err := s.getWorkflowAndValidate(ctx, req.Namespace, req.Name, metav1.GetOptions{})
+	wf, err := s.getWorkflow(wfClient, req.Namespace, req.Name, metav1.GetOptions{})
 	if err != nil {
 		return err
 	}
+	err = s.validateWorkflow(wf)
+	if err != nil {
+		return err
+	}
+	req.Name = wf.Name
 	return logs.WorkflowLogs(ctx, wfClient, kubeClient, req, ws)
 }
 
-func (s *workflowServer) getWorkflowAndValidate(ctx context.Context, namespace string, name string, options metav1.GetOptions) (*v1alpha1.Workflow, error) {
-	wfClient := auth.GetWfClient(ctx)
+func (s *workflowServer) getWorkflow(wfClient versioned.Interface, namespace string, name string, options metav1.GetOptions) (*v1alpha1.Workflow, error) {
+	if name == latestAlias {
+		latest, err := getLatestWorkflow(wfClient, namespace)
+		if err != nil {
+			return nil, err
+		}
+		log.Infof("Resolved alias %s to workflow %s.\n", latestAlias, latest.Name)
+		return latest, nil
+	}
 	wf, err := wfClient.ArgoprojV1alpha1().Workflows(namespace).Get(name, options)
 	if err != nil {
 		return nil, err
 	}
-	err = s.instanceIDService.Validate(wf)
+	return wf, nil
+}
+
+func (s *workflowServer) validateWorkflow(wf *v1alpha1.Workflow) error {
+	return s.instanceIDService.Validate(wf)
+}
+
+func getLatestWorkflow(wfClient versioned.Interface, namespace string) (*v1alpha1.Workflow, error) {
+	wfList, err := wfClient.ArgoprojV1alpha1().Workflows(namespace).List(metav1.ListOptions{})
 	if err != nil {
 		return nil, err
 	}
-	return wf, nil
+	if len(wfList.Items) < 1 {
+		return nil, fmt.Errorf("No workflows found.")
+	}
+	latest := wfList.Items[0]
+	for _, wf := range wfList.Items {
+		if latest.ObjectMeta.CreationTimestamp.Before(&wf.ObjectMeta.CreationTimestamp) {
+			latest = wf
+		}
+	}
+	return &latest, nil
 }
 
 func (s *workflowServer) SubmitWorkflow(ctx context.Context, req *workflowpkg.WorkflowSubmitRequest) (*v1alpha1.Workflow, error) {
diff --git a/ui/src/app/shared/conditions-panel.tsx b/ui/src/app/shared/conditions-panel.tsx
index dbbe45cd0..ce129e32d 100644
--- a/ui/src/app/shared/conditions-panel.tsx
+++ b/ui/src/app/shared/conditions-panel.tsx
@@ -6,7 +6,7 @@ interface Props {
 }
 
 const WarningConditions: ConditionType[] = ['SpecWarning'];
-const ErrorConditions: ConditionType[] = ['MetricsError', 'SubmissionError'];
+const ErrorConditions: ConditionType[] = ['MetricsError', 'SubmissionError', 'SpecError'];
 
 export function hasWarningConditionBadge(conditions: Condition[]): boolean {
     if (conditions.length === 0) {
diff --git a/ui/src/app/shared/services/workflows-service.ts b/ui/src/app/shared/services/workflows-service.ts
index c95f054c2..9bf7e5486 100644
--- a/ui/src/app/shared/services/workflows-service.ts
+++ b/ui/src/app/shared/services/workflows-service.ts
@@ -23,7 +23,17 @@ export class WorkflowsService {
         if (pagination.limit) {
             params.push(`listOptions.limit=${pagination.limit}`);
         }
-        params.push(`fields=metadata,items.metadata.name,items.metadata.namespace,items.status.phase,items.status.finishedAt,items.status.startedAt`);
+        const fields = [
+            'metadata',
+            'items.metadata.uid',
+            'items.metadata.name',
+            'items.metadata.namespace',
+            'items.metadata.labels',
+            'items.status.phase',
+            'items.status.finishedAt',
+            'items.status.startedAt'
+        ];
+        params.push(`fields=${fields.join(',')}`);
         return requests.get(`api/v1/workflows/${namespace}?${params.join('&')}`).then(res => res.body as WorkflowList);
     }
 
@@ -31,17 +41,37 @@ export class WorkflowsService {
         return requests.get(`api/v1/workflows/${namespace}/${name}`).then(res => res.body as Workflow);
     }
 
-    public watch(filter: {namespace?: string; name?: string; phases?: Array<string>; labels?: Array<string>}): Observable<models.kubernetes.WatchEvent<Workflow>> {
+    public watch(filter: {
+        namespace?: string;
+        name?: string;
+        phases?: Array<string>;
+        labels?: Array<string>;
+        resourceVersion?: string;
+    }): Observable<models.kubernetes.WatchEvent<Workflow>> {
         const url = `api/v1/workflow-events/${filter.namespace || ''}?${this.queryParams(filter).join('&')}`;
-
         return requests.loadEventSource(url, true).map(data => JSON.parse(data).result as models.kubernetes.WatchEvent<Workflow>);
     }
 
-    public watchFields(filter: {namespace?: string; name?: string; phases?: Array<string>; labels?: Array<string>}): Observable<models.kubernetes.WatchEvent<Workflow>> {
+    public watchFields(filter: {
+        namespace?: string;
+        name?: string;
+        phases?: Array<string>;
+        labels?: Array<string>;
+        resourceVersion?: string;
+    }): Observable<models.kubernetes.WatchEvent<Workflow>> {
         const params = this.queryParams(filter);
-        params.push(
-            `fields=result.object.metadata.name,result.object.metadata.namespace,result.object.status.finishedAt,result.object.status.startedAt,result.object.status.phase`
-        );
+        const fields = [
+            'result.object.metadata.name',
+            'result.object.metadata.namespace',
+            'result.object.metadata.resourceVersion',
+            'result.object.metadata.uid',
+            'result.object.status.finishedAt',
+            'result.object.status.phase',
+            'result.object.status.startedAt',
+            'result.type',
+            'result.object.metadata.labels'
+        ];
+        params.push(`fields=${fields.join(',')}`);
         const url = `api/v1/workflow-events/${filter.namespace || ''}?${params.join('&')}`;
 
         return requests.loadEventSource(url, true).map(data => JSON.parse(data).result as models.kubernetes.WatchEvent<Workflow>);
@@ -115,7 +145,7 @@ export class WorkflowsService {
             : `artifacts/${workflow.metadata.namespace}/${workflow.metadata.name}/${nodeId}/${encodeURIComponent(artifactName)}`;
     }
 
-    private queryParams(filter: {namespace?: string; name?: string; phases?: Array<string>; labels?: Array<string>}) {
+    private queryParams(filter: {namespace?: string; name?: string; phases?: Array<string>; labels?: Array<string>; resourceVersion?: string}) {
         const queryParams: string[] = [];
         if (filter.name) {
             queryParams.push(`listOptions.fieldSelector=metadata.name=${filter.name}`);
@@ -124,6 +154,9 @@ export class WorkflowsService {
         if (labelSelector.length > 0) {
             queryParams.push(`listOptions.labelSelector=${labelSelector}`);
         }
+        if (filter.resourceVersion) {
+            queryParams.push(`listOptions.resourceVersion=${filter.resourceVersion}`);
+        }
         return queryParams;
     }
 
diff --git a/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx b/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx
index 6f6cadaf4..95dfbcc22 100644
--- a/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx
+++ b/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx
@@ -1,19 +1,40 @@
 import * as React from 'react';
-import * as models from '../../../../models';
+import {Workflow} from '../../../../models';
 
+import {Loading} from '../../../shared/components/loading';
 import {ConditionsPanel} from '../../../shared/conditions-panel';
+import {services} from '../../../shared/services';
 import {WorkflowLabels} from '../workflow-labels/workflow-labels';
 
 require('./workflow-drawer.scss');
 
 interface WorkflowDrawerProps {
-    workflow: models.Workflow;
+    name: string;
+    namespace: string;
     onChange: (key: string) => void;
 }
 
-export class WorkflowDrawer extends React.Component<WorkflowDrawerProps, {}> {
+interface WorkflowDrawerState {
+    workflow?: Workflow;
+}
+
+export class WorkflowDrawer extends React.Component<WorkflowDrawerProps, WorkflowDrawerState> {
+    constructor(props: Readonly<WorkflowDrawerProps>) {
+        super(props);
+        this.state = {};
+    }
+
+    public componentDidMount() {
+        services.workflows.get(this.props.namespace, this.props.name).then(workflow => {
+            this.setState({workflow});
+        });
+    }
+
     public render() {
-        const wf = this.props.workflow;
+        if (!this.state.workflow) {
+            return <Loading />;
+        }
+        const wf = this.state.workflow;
         return (
             <div className='workflow-drawer'>
                 {!wf.status.message ? null : (
diff --git a/ui/src/app/workflows/components/workflow-labels/workflow-labels.tsx b/ui/src/app/workflows/components/workflow-labels/workflow-labels.tsx
index 9750cea5c..289a5374b 100644
--- a/ui/src/app/workflows/components/workflow-labels/workflow-labels.tsx
+++ b/ui/src/app/workflows/components/workflow-labels/workflow-labels.tsx
@@ -21,7 +21,7 @@ export class WorkflowLabels extends React.Component<WorkflowLabelsProps, {}> {
                 Object.keys(w.metadata.labels).map(key => (
                     <div
                         className='tag'
-                        key={`${w.metadata.namespace}-${w.metadata.name}-${key}`}
+                        key={`${w.metadata.uid}-${key}`}
                         onClick={async e => {
                             e.preventDefault();
                             this.props.onChange(key);
@@ -32,7 +32,7 @@ export class WorkflowLabels extends React.Component<WorkflowLabelsProps, {}> {
                 ))
             );
         } else {
-            labels.push(<div key={`${w.metadata.namespace}-${w.metadata.name}-none`}> No labels </div>);
+            labels.push(<div key={`${w.metadata.uid}-none`}> No labels </div>);
         }
 
         return <div className='wf-row-labels'>{labels}</div>;
diff --git a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
index adf6cbe23..4ff97311f 100644
--- a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
+++ b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
@@ -167,16 +167,17 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
                     selectedLabels
                 });
                 Utils.setCurrentNamespace(newNamespace);
+                return wfList.metadata.resourceVersion;
             })
-            .then(() => {
+            .then(resourceVersion => {
                 this.subscription = services.workflows
-                    .watchFields({namespace: newNamespace, phases: selectedPhases, labels: selectedLabels})
+                    .watchFields({namespace: newNamespace, phases: selectedPhases, labels: selectedLabels, resourceVersion})
                     .map(workflowChange => {
                         const workflows = this.state.workflows;
                         if (!workflowChange) {
                             return {workflows, updated: false};
                         }
-                        const index = workflows.findIndex(item => item.metadata.name === workflowChange.object.metadata.name);
+                        const index = workflows.findIndex(item => item.metadata.uid === workflowChange.object.metadata.uid);
                         if (index > -1 && workflowChange.object.metadata.resourceVersion === workflows[index].metadata.resourceVersion) {
                             return {workflows, updated: false};
                         }
@@ -195,9 +196,7 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
                     })
                     .filter(item => item.updated)
                     .map(item => item.workflows)
-                    .catch((error, caught) => {
-                        return caught;
-                    })
+                    .catch((error, caught) => caught)
                     .subscribe(workflows => this.setState({workflows}));
             })
             .then(_ => this.setState({loading: false}))
@@ -271,7 +270,7 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
                         return (
                             <WorkflowsRow
                                 workflow={wf}
-                                key={`${wf.metadata.namespace}-${wf.metadata.name}`}
+                                key={wf.metadata.uid}
                                 onChange={key => {
                                     const value = `${key}=${wf.metadata.labels[key]}`;
                                     let newTags: string[] = [];
diff --git a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx
index 464a2cef0..49d274e20 100644
--- a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx
+++ b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx
@@ -6,7 +6,6 @@ import {uiUrl} from '../../../shared/base';
 import {PhaseIcon} from '../../../shared/components/phase-icon';
 import {Timestamp} from '../../../shared/components/timestamp';
 import {formatDuration, wfDuration} from '../../../shared/duration';
-import {services} from '../../../shared/services';
 import {WorkflowDrawer} from '../workflow-drawer/workflow-drawer';
 
 interface WorkflowsRowProps {
@@ -16,20 +15,18 @@ interface WorkflowsRowProps {
 
 interface WorkflowRowState {
     hideDrawer: boolean;
-    workflow: models.Workflow;
 }
 
 export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRowState> {
     constructor(props: WorkflowsRowProps) {
         super(props);
         this.state = {
-            workflow: this.props.workflow,
             hideDrawer: true
         };
     }
 
     public render() {
-        const wf = this.state.workflow;
+        const wf = this.props.workflow;
         return (
             <div className='workflows-list__row-container'>
                 <Link className='row argo-table-list__row' to={uiUrl(`workflows/${wf.metadata.namespace}/${wf.metadata.name}`)}>
@@ -52,7 +49,6 @@ export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRow
                             <div
                                 onClick={e => {
                                     e.preventDefault();
-                                    this.fetchFullWorkflow();
                                     this.setState({hideDrawer: !this.state.hideDrawer});
                                 }}
                                 className={`workflows-row__action workflows-row__action--${this.state.hideDrawer ? 'show' : 'hide'}`}>
@@ -73,7 +69,8 @@ export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRow
                     <span />
                 ) : (
                     <WorkflowDrawer
-                        workflow={wf}
+                        name={wf.metadata.name}
+                        namespace={wf.metadata.namespace}
                         onChange={key => {
                             this.props.onChange(key);
                         }}
@@ -82,10 +79,4 @@ export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRow
             </div>
         );
     }
-
-    private fetchFullWorkflow(): void {
-        services.workflows.get(this.props.workflow.metadata.namespace, this.props.workflow.metadata.name).then(wf => {
-            this.setState({workflow: wf});
-        });
-    }
 }
diff --git a/ui/src/models/workflows.ts b/ui/src/models/workflows.ts
index d3ff8d28b..0290ce52a 100644
--- a/ui/src/models/workflows.ts
+++ b/ui/src/models/workflows.ts
@@ -839,7 +839,7 @@ export interface Condition {
     message: string;
 }
 
-export type ConditionType = 'Completed' | 'SpecWarning' | 'MetricsError' | 'SubmissionError';
+export type ConditionType = 'Completed' | 'SpecWarning' | 'MetricsError' | 'SubmissionError' | 'SpecError';
 export type ConditionStatus = 'True' | 'False' | 'Unknown;';
 
 /**
diff --git a/util/argo/audit_logger.go b/util/argo/audit_logger.go
deleted file mode 100644
index 141349fe0..000000000
--- a/util/argo/audit_logger.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package argo
-
-import (
-	"fmt"
-	"time"
-
-	log "github.com/sirupsen/logrus"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
-
-	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
-	"github.com/argoproj/argo/workflow/common"
-)
-
-type AuditLogger struct {
-	kIf       kubernetes.Interface
-	component string
-	ns        string
-}
-
-type EventInfo struct {
-	Type   string
-	Reason string
-}
-
-const (
-	EventReasonWorkflowRunning       = "WorkflowRunning"
-	EventReasonWorkflowSucceeded     = "WorkflowSucceeded"
-	EventReasonWorkflowFailed        = "WorkflowFailed"
-	EventReasonWorkflowTimedOut      = "WorkflowTimedOut"
-	EventReasonWorkflowNodeSucceeded = "WorkflowNodeSucceeded"
-	EventReasonWorkflowNodeFailed    = "WorkflowNodeFailed"
-	EventReasonWorkflowNodeError     = "WorkflowNodeError"
-)
-
-func (l *AuditLogger) logEvent(workflow *wfv1.Workflow, info EventInfo, message string, annotations map[string]string) {
-	logCtx := log.WithFields(log.Fields{
-		"type":     info.Type,
-		"reason":   info.Reason,
-		"workflow": workflow.Name,
-	})
-	t := metav1.Time{Time: time.Now()}
-	event := corev1.Event{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        fmt.Sprintf("%v.%x", workflow.Name, t.UnixNano()),
-			Annotations: annotations,
-		},
-		Source: corev1.EventSource{
-			Component: l.component,
-		},
-		InvolvedObject: corev1.ObjectReference{
-			Kind:            wfv1.WorkflowSchemaGroupVersionKind.Kind,
-			APIVersion:      wfv1.SchemeGroupVersion.String(),
-			Name:            workflow.Name,
-			Namespace:       workflow.Namespace,
-			ResourceVersion: workflow.ResourceVersion,
-			UID:             workflow.UID,
-		},
-		FirstTimestamp: t,
-		LastTimestamp:  t,
-		Count:          1,
-		Message:        message,
-		Type:           info.Type,
-		Reason:         info.Reason,
-	}
-	logCtx.WithField("event", event).Debug()
-	_, err := l.kIf.CoreV1().Events(workflow.Namespace).Create(&event)
-	if err != nil {
-		logCtx.Errorf("Unable to create audit event: %v", err)
-		return
-	}
-}
-
-func (l *AuditLogger) logWorkflowEvent(workflow *wfv1.Workflow, info EventInfo, message string, annotations map[string]string) {
-	l.logEvent(workflow, info, message, annotations)
-}
-
-func (l *AuditLogger) LogWorkflowEvent(workflow *wfv1.Workflow, info EventInfo, message string) {
-	l.logWorkflowEvent(workflow, info, message, nil)
-}
-
-func (l *AuditLogger) LogWorkflowNodeEvent(workflow *wfv1.Workflow, node *wfv1.NodeStatus) {
-	l.logWorkflowEvent(
-		workflow,
-		EventInfo{Type: eventType(node.Phase), Reason: nodePhaseReason(node.Phase)},
-		nodeMessage(node),
-		map[string]string{
-			common.AnnotationKeyNodeType: string(node.Type),
-			common.AnnotationKeyNodeName: node.Name,
-		})
-}
-
-func eventType(phase wfv1.NodePhase) string {
-	return map[wfv1.NodePhase]string{
-		wfv1.NodeError:     corev1.EventTypeWarning,
-		wfv1.NodeFailed:    corev1.EventTypeWarning,
-		wfv1.NodeSucceeded: corev1.EventTypeNormal,
-	}[phase]
-}
-func nodePhaseReason(phase wfv1.NodePhase) string {
-	return map[wfv1.NodePhase]string{
-		wfv1.NodeError:     EventReasonWorkflowNodeError,
-		wfv1.NodeFailed:    EventReasonWorkflowNodeFailed,
-		wfv1.NodeSucceeded: EventReasonWorkflowNodeSucceeded,
-	}[phase]
-}
-
-func nodeMessage(node *wfv1.NodeStatus) string {
-	message := fmt.Sprintf("%v node %s", node.Phase, node.Name)
-	if node.Message != "" {
-		message = message + ": " + node.Message
-	}
-	return message
-}
-
-func NewAuditLogger(ns string, kIf kubernetes.Interface, component string) *AuditLogger {
-	return &AuditLogger{
-		ns:        ns,
-		kIf:       kIf,
-		component: component,
-	}
-}
diff --git a/util/util.go b/util/util.go
index 963823288..49169db5e 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,10 +1,13 @@
 package util
 
 import (
+	"fmt"
 	"io/ioutil"
 	"strconv"
 	"strings"
 
+	"k8s.io/apimachinery/pkg/fields"
+
 	log "github.com/sirupsen/logrus"
 	apiv1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -90,3 +93,17 @@ func RecoverIndexFromNodeName(name string) int {
 	}
 	return out
 }
+
+func GenerateFieldSelectorFromWorkflowName(wfName string) string {
+	result := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", wfName)).String()
+	if compare := RecoverWorkflowNameFromSelectorString(result); wfName != compare {
+		panic(fmt.Sprintf("Could not recover field selector from workflow name. Expected '%s' but got '%s'\n", wfName, compare))
+	}
+	return result
+}
+
+func RecoverWorkflowNameFromSelectorString(selector string) string {
+	nameIndex := strings.Index(selector, "=")
+	name := selector[nameIndex+1:]
+	return name
+}
diff --git a/workflow/controller/controller.go b/workflow/controller/controller.go
index aecd24532..e6d49b67d 100644
--- a/workflow/controller/controller.go
+++ b/workflow/controller/controller.go
@@ -22,8 +22,11 @@ import (
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/scheme"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 	"upper.io/db.v3/lib/sqlbuilder"
 
@@ -81,6 +84,7 @@ type WorkflowController struct {
 	hydrator              hydrator.Interface
 	wfArchive             sqldb.WorkflowArchive
 	metrics               metrics.Metrics
+	eventRecorder         record.EventRecorder
 }
 
 const (
@@ -102,6 +106,10 @@ func NewWorkflowController(
 	containerRuntimeExecutor,
 	configMap string,
 ) *WorkflowController {
+	eventBroadcaster := record.NewBroadcaster()
+	eventBroadcaster.StartLogging(log.Debugf)
+	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events(namespace)})
+	eventRecorder := eventBroadcaster.NewRecorder(scheme.Scheme, apiv1.EventSource{Component: "workflow-controller"})
 	wfc := WorkflowController{
 		restConfig:                 restConfig,
 		kubeclientset:              kubeclientset,
@@ -116,6 +124,7 @@ func NewWorkflowController(
 		configController:           config.NewController(namespace, configMap, kubeclientset),
 		completedPods:              make(chan string, 512),
 		gcPods:                     make(chan string, 512),
+		eventRecorder:              eventRecorder,
 	}
 	wfc.throttler = NewThrottler(0, wfc.wfQueue)
 	wfc.UpdateConfig()
@@ -509,10 +518,9 @@ func (wfc *WorkflowController) processNextPodItem() bool {
 		log.Warnf("watch returned pod unrelated to any workflow: %s", pod.ObjectMeta.Name)
 		return true
 	}
-	// TODO: currently we reawaken the workflow on *any* pod updates.
-	// But this could be be much improved to become smarter by only
-	// requeue the workflow when there are changes that we care about.
-	wfc.wfQueue.Add(pod.ObjectMeta.Namespace + "/" + workflowName)
+	// add this change after 1s - this reduces the number of workflow reconciliations -
+	//with each reconciliation doing more work
+	wfc.wfQueue.AddAfter(pod.ObjectMeta.Namespace+"/"+workflowName, 1*time.Second)
 	return true
 }
 
@@ -570,6 +578,7 @@ func (wfc *WorkflowController) addWorkflowInformerHandlers() {
 				},
 				UpdateFunc: func(old, new interface{}) {
 					oldWf, newWf := old.(*unstructured.Unstructured), new.(*unstructured.Unstructured)
+					// this check is very important to prevent doing many reconciliations we do not need to do
 					if oldWf.GetResourceVersion() == newWf.GetResourceVersion() {
 						return
 					}
@@ -592,7 +601,6 @@ func (wfc *WorkflowController) addWorkflowInformerHandlers() {
 			},
 		},
 	)
-
 	wfc.wfInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
 		AddFunc: func(obj interface{}) {
 			wfc.metrics.WorkflowAdded(getWfPhase(obj))
diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go
index 24355b83f..2cf2611ee 100644
--- a/workflow/controller/operator.go
+++ b/workflow/controller/operator.go
@@ -25,6 +25,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/yaml"
 
@@ -34,7 +35,6 @@ import (
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1"
 	"github.com/argoproj/argo/util"
-	"github.com/argoproj/argo/util/argo"
 	"github.com/argoproj/argo/util/resource"
 	"github.com/argoproj/argo/util/retry"
 	"github.com/argoproj/argo/workflow/common"
@@ -81,8 +81,7 @@ type wfOperationCtx struct {
 	// workflowDeadline is the deadline which the workflow is expected to complete before we
 	// terminate the workflow.
 	workflowDeadline *time.Time
-	// auditLogger is the argo audit logger
-	auditLogger *argo.AuditLogger
+	eventRecorder    record.EventRecorder
 	// preExecutionNodePhases contains the phases of all the nodes before the current operation. Necessary to infer
 	// changes in phase for metric emission
 	preExecutionNodePhases map[string]wfv1.NodePhase
@@ -132,7 +131,7 @@ func newWorkflowOperationCtx(wf *wfv1.Workflow, wfc *WorkflowController) *wfOper
 		completedPods:          make(map[string]bool),
 		succeededPods:          make(map[string]bool),
 		deadline:               time.Now().UTC().Add(maxOperationTime),
-		auditLogger:            argo.NewAuditLogger(wf.ObjectMeta.Namespace, wfc.kubeclientset, wf.ObjectMeta.Name),
+		eventRecorder:          wfc.eventRecorder,
 		preExecutionNodePhases: make(map[string]wfv1.NodePhase),
 	}
 
@@ -194,10 +193,10 @@ func (woc *wfOperationCtx) operate() {
 		if err != nil {
 			msg := fmt.Sprintf("Unable to create PDB resource for workflow, %s error: %s", woc.wf.Name, err)
 			woc.markWorkflowFailed(msg)
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 			return
 		}
-		woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeNormal, Reason: argo.EventReasonWorkflowRunning}, "Workflow Running")
+		woc.eventRecorder.Event(woc.wf, apiv1.EventTypeNormal, "WorkflowRunning", "Workflow Running")
 		validateOpts := validate.ValidateOpts{ContainerRuntimeExecutor: woc.controller.GetContainerRuntimeExecutor()}
 		wftmplGetter := templateresolution.WrapWorkflowTemplateInterface(woc.controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates(woc.wf.Namespace))
 		cwftmplGetter := templateresolution.WrapClusterWorkflowTemplateInterface(woc.controller.wfclientset.ArgoprojV1alpha1().ClusterWorkflowTemplates())
@@ -207,7 +206,7 @@ func (woc *wfOperationCtx) operate() {
 		if err != nil {
 			msg := fmt.Sprintf("invalid spec: %s", err.Error())
 			woc.markWorkflowFailed(msg)
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 			return
 		}
 		// If we received conditions during validation (such as SpecWarnings), add them to the Workflow object
@@ -232,7 +231,7 @@ func (woc *wfOperationCtx) operate() {
 		}
 		if err != nil {
 			woc.log.Errorf("%s error: %+v", woc.wf.ObjectMeta.Name, err)
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowTimedOut}, "Workflow timed out")
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowTimedOut", "Workflow timed out")
 			// TODO: we need to re-add to the workqueue, but should happen in caller
 			return
 		}
@@ -265,7 +264,7 @@ func (woc *wfOperationCtx) operate() {
 			msg := fmt.Sprintf("Failed to load artifact repository configMap: %+v", err)
 			woc.log.Errorf(msg)
 			woc.markWorkflowError(err, true)
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 			return
 		}
 	}
@@ -283,7 +282,7 @@ func (woc *wfOperationCtx) operate() {
 		msg := fmt.Sprintf("%s pvc create error: %+v", woc.wf.ObjectMeta.Name, err)
 		woc.log.Errorf(msg)
 		woc.markWorkflowError(err, true)
-		woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+		woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 		return
 	}
 
@@ -294,9 +293,9 @@ func (woc *wfOperationCtx) operate() {
 		woc.log.Errorf(msg)
 		switch err {
 		case ErrDeadlineExceeded:
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowTimedOut}, msg)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowTimedOut", msg)
 		default:
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 		}
 		return
 	}
@@ -359,7 +358,7 @@ func (woc *wfOperationCtx) operate() {
 		// markCompletion so that we can retry PVC deletion (TODO: use workqueue.ReAdd())
 		// This error phase may be cleared if a subsequent delete attempt is successful.
 		woc.markWorkflowError(err, false)
-		woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, msg)
+		woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", msg)
 		return
 	}
 
@@ -379,17 +378,17 @@ func (woc *wfOperationCtx) operate() {
 			// if main workflow succeeded, but the exit node was unsuccessful
 			// the workflow is now considered unsuccessful.
 			woc.markWorkflowPhase(onExitNode.Phase, true, onExitNode.Message)
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, onExitNode.Message)
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", onExitNode.Message)
 		} else {
 			woc.markWorkflowSuccess()
-			woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeNormal, Reason: argo.EventReasonWorkflowSucceeded}, "Workflow completed")
+			woc.eventRecorder.Event(woc.wf, apiv1.EventTypeNormal, "WorkflowSucceeded", "Workflow completed")
 		}
 	case wfv1.NodeFailed:
 		woc.markWorkflowFailed(workflowMessage)
-		woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, workflowMessage)
+		woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", workflowMessage)
 	case wfv1.NodeError:
 		woc.markWorkflowPhase(wfv1.NodeError, true, workflowMessage)
-		woc.auditLogger.LogWorkflowEvent(woc.wf, argo.EventInfo{Type: apiv1.EventTypeWarning, Reason: argo.EventReasonWorkflowFailed}, workflowMessage)
+		woc.eventRecorder.Event(woc.wf, apiv1.EventTypeWarning, "WorkflowFailed", workflowMessage)
 	default:
 		// NOTE: we should never make it here because if the the node was 'Running'
 		// we should have returned earlier.
@@ -504,14 +503,6 @@ func (woc *wfOperationCtx) persistUpdates() {
 
 	woc.log.WithFields(log.Fields{"resourceVersion": woc.wf.ResourceVersion, "phase": woc.wf.Status.Phase}).Info("Workflow update successful")
 
-	// HACK(jessesuen) after we successfully persist an update to the workflow, the informer's
-	// cache is now invalid. It's very common that we will need to immediately re-operate on a
-	// workflow due to queuing by the pod workers. The following sleep gives a *chance* for the
-	// informer's cache to catch up to the version of the workflow we just persisted. Without
-	// this sleep, the next worker to work on this workflow will very likely operate on a stale
-	// object and redo work.
-	time.Sleep(1 * time.Second)
-
 	// It is important that we *never* label pods as completed until we successfully updated the workflow
 	// Failing to do so means we can have inconsistent state.
 	// TODO: The completedPods will be labeled multiple times. I think it would be improved in the future.
@@ -1746,7 +1737,27 @@ func (woc *wfOperationCtx) markNodePhase(nodeName string, phase wfv1.NodePhase,
 }
 
 func (woc *wfOperationCtx) onNodeComplete(node *wfv1.NodeStatus) {
-	woc.auditLogger.LogWorkflowNodeEvent(woc.wf, node)
+	if !woc.controller.Config.NodeEvents.IsEnabled() {
+		return
+	}
+	message := fmt.Sprintf("%v node %s", node.Phase, node.Name)
+	if node.Message != "" {
+		message = message + ": " + node.Message
+	}
+	eventType := apiv1.EventTypeWarning
+	if node.Phase == wfv1.NodeSucceeded {
+		eventType = apiv1.EventTypeNormal
+	}
+	woc.eventRecorder.AnnotatedEventf(
+		woc.wf,
+		map[string]string{
+			common.AnnotationKeyNodeType: string(node.Type),
+			common.AnnotationKeyNodeName: node.Name,
+		},
+		eventType,
+		fmt.Sprintf("WorkflowNode%s", node.Phase),
+		message,
+	)
 }
 
 // markNodeError is a convenience method to mark a node with an error and set the message from the error
diff --git a/workflow/cron/controller.go b/workflow/cron/controller.go
index f7439c160..19999203d 100644
--- a/workflow/cron/controller.go
+++ b/workflow/cron/controller.go
@@ -2,6 +2,7 @@ package cron
 
 import (
 	"context"
+	"fmt"
 	"sync"
 	"time"
 
@@ -126,7 +127,7 @@ func (cc *Controller) processNextCronItem() bool {
 
 	obj, exists, err := cc.cronWfInformer.Informer().GetIndexer().GetByKey(key.(string))
 	if err != nil {
-		log.Errorf("Failed to get CronWorkflow '%s' from informer index: %+v", key, err)
+		log.WithError(err).Error(fmt.Sprintf("Failed to get CronWorkflow '%s' from informer index", key))
 		return true
 	}
 	cc.nameEntryIDMapLock.Lock()
@@ -146,15 +147,17 @@ func (cc *Controller) processNextCronItem() bool {
 		return true
 	}
 
-	cronWorkflowOperationCtx, err := newCronWfOperationCtx(cronWf, cc.wfClientset, cc.wfLister, cc.metrics)
+	cronWorkflowOperationCtx := newCronWfOperationCtx(cronWf, cc.wfClientset, cc.wfLister, cc.metrics)
+
+	err = cronWorkflowOperationCtx.validateCronWorkflow()
 	if err != nil {
-		log.Error(err)
+		log.WithError(err).Error("invalid cron workflow")
 		return true
 	}
 
 	err = cronWorkflowOperationCtx.runOutstandingWorkflows()
 	if err != nil {
-		log.Errorf("could not run outstanding Workflow: %s", err)
+		log.WithError(err).Error("could not run outstanding Workflow")
 		return true
 	}
 
@@ -171,7 +174,7 @@ func (cc *Controller) processNextCronItem() bool {
 
 	entryId, err := cc.cron.AddJob(cronSchedule, cronWorkflowOperationCtx)
 	if err != nil {
-		log.Errorf("could not schedule CronWorkflow: %s", err)
+		log.WithError(err).Error("could not schedule CronWorkflow")
 		return true
 	}
 	cc.nameEntryIDMap[key.(string)] = entryId
@@ -195,7 +198,7 @@ func (cc *Controller) processNextWorkflowItem() bool {
 
 	obj, wfExists, err := cc.wfInformer.GetIndexer().GetByKey(key.(string))
 	if err != nil {
-		log.Errorf("Failed to get Workflow '%s' from informer index: %+v", key, err)
+		log.WithError(err).Error(fmt.Sprintf("Failed to get Workflow '%s' from informer index", key))
 		return true
 	}
 
diff --git a/workflow/cron/operator.go b/workflow/cron/operator.go
index 07ba5e7ea..1c53562b0 100644
--- a/workflow/cron/operator.go
+++ b/workflow/cron/operator.go
@@ -17,7 +17,9 @@ import (
 	typed "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1"
 	"github.com/argoproj/argo/workflow/common"
 	"github.com/argoproj/argo/workflow/metrics"
+	"github.com/argoproj/argo/workflow/templateresolution"
 	"github.com/argoproj/argo/workflow/util"
+	"github.com/argoproj/argo/workflow/validate"
 )
 
 type cronWfOperationCtx struct {
@@ -32,7 +34,7 @@ type cronWfOperationCtx struct {
 	metrics     *metrics.Metrics
 }
 
-func newCronWfOperationCtx(cronWorkflow *v1alpha1.CronWorkflow, wfClientset versioned.Interface, wfLister util.WorkflowLister, metrics *metrics.Metrics) (*cronWfOperationCtx, error) {
+func newCronWfOperationCtx(cronWorkflow *v1alpha1.CronWorkflow, wfClientset versioned.Interface, wfLister util.WorkflowLister, metrics *metrics.Metrics) *cronWfOperationCtx {
 	return &cronWfOperationCtx{
 		name:        cronWorkflow.ObjectMeta.Name,
 		cronWf:      cronWorkflow,
@@ -45,21 +47,26 @@ func newCronWfOperationCtx(cronWorkflow *v1alpha1.CronWorkflow, wfClientset vers
 			"namespace": cronWorkflow.ObjectMeta.Namespace,
 		}),
 		metrics: metrics,
-	}, nil
+	}
 }
 
 func (woc *cronWfOperationCtx) Run() {
 	woc.log.Infof("Running %s", woc.name)
 
-	err := woc.reconcileDeletedWfs()
+	err := woc.validateCronWorkflow()
+	if err != nil {
+		return
+	}
+
+	err = woc.reconcileDeletedWfs()
 	if err != nil {
-		woc.reportCronWorkflowError(fmt.Sprintf("Could not remove deleted Workflow: %s", err))
+		woc.reportCronWorkflowError(v1alpha1.ConditionTypeSubmissionError, fmt.Sprintf("Could not remove deleted Workflow: %s", err))
 		return
 	}
 
 	proceed, err := woc.enforceRuntimePolicy()
 	if err != nil {
-		woc.reportCronWorkflowError(fmt.Sprintf("Concurrency policy error: %s", err))
+		woc.reportCronWorkflowError(v1alpha1.ConditionTypeSubmissionError, fmt.Sprintf("Concurrency policy error: %s", err))
 		return
 	} else if !proceed {
 		return
@@ -69,7 +76,7 @@ func (woc *cronWfOperationCtx) Run() {
 
 	runWf, err := util.SubmitWorkflow(woc.wfClient, woc.wfClientset, woc.cronWf.Namespace, wf, &v1alpha1.SubmitOpts{})
 	if err != nil {
-		woc.reportCronWorkflowError(fmt.Sprintf("Failed to submit Workflow: %s", err))
+		woc.reportCronWorkflowError(v1alpha1.ConditionTypeSubmissionError, fmt.Sprintf("Failed to submit Workflow: %s", err))
 		return
 	}
 
@@ -79,6 +86,19 @@ func (woc *cronWfOperationCtx) Run() {
 	woc.persistUpdate()
 }
 
+func (woc *cronWfOperationCtx) validateCronWorkflow() error {
+	wftmplGetter := templateresolution.WrapWorkflowTemplateInterface(woc.wfClientset.ArgoprojV1alpha1().WorkflowTemplates(woc.cronWf.Namespace))
+	cwftmplGetter := templateresolution.WrapClusterWorkflowTemplateInterface(woc.wfClientset.ArgoprojV1alpha1().ClusterWorkflowTemplates())
+	err := validate.ValidateCronWorkflow(wftmplGetter, cwftmplGetter, woc.cronWf)
+	if err != nil {
+		woc.reportCronWorkflowError(v1alpha1.ConditionTypeSpecError, fmt.Sprint(err))
+	} else {
+		woc.cronWf.Status.Conditions.RemoveCondition(v1alpha1.ConditionTypeSpecError)
+		woc.persistUpdate()
+	}
+	return err
+}
+
 func getWorkflowObjectReference(wf *v1alpha1.Workflow, runWf *v1alpha1.Workflow) corev1.ObjectReference {
 	// This is a bit of a hack. Ideally we'd use ref.GetReference, but for some reason the `runWf` object is coming back
 	// without `Kind` and `APIVersion` set (even though it it set on `wf`). To fix this, we hard code those values.
@@ -306,10 +326,10 @@ func (woc *cronWfOperationCtx) deleteOldestWorkflows(jobList []v1alpha1.Workflow
 	return nil
 }
 
-func (woc *cronWfOperationCtx) reportCronWorkflowError(errString string) {
-	woc.log.Errorf(errString)
+func (woc *cronWfOperationCtx) reportCronWorkflowError(conditionType v1alpha1.ConditionType, errString string) {
+	woc.log.Errorf("%s: %s", conditionType, errString)
 	woc.cronWf.Status.Conditions.UpsertCondition(v1alpha1.Condition{
-		Type:    v1alpha1.ConditionTypeSubmissionError,
+		Type:    conditionType,
 		Message: errString,
 		Status:  v1.ConditionTrue,
 	})


Test patch:
diff --git a/config/node_events_test.go b/config/node_events_test.go
new file mode 100644
index 000000000..394e343a2
--- /dev/null
+++ b/config/node_events_test.go
@@ -0,0 +1,14 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/utils/pointer"
+)
+
+func TestNodeEvents_IsEnabled(t *testing.T) {
+	assert.True(t, NodeEvents{}.IsEnabled())
+	assert.False(t, NodeEvents{Enabled: pointer.BoolPtr(false)}.IsEnabled())
+	assert.True(t, NodeEvents{Enabled: pointer.BoolPtr(true)}.IsEnabled())
+}
\ No newline at end of file
diff --git a/server/auth/sso/sso_test.go b/server/auth/sso/sso_test.go
new file mode 100644
index 000000000..4aef64431
--- /dev/null
+++ b/server/auth/sso/sso_test.go
@@ -0,0 +1,104 @@
+package sso
+
+import (
+	"context"
+	"testing"
+
+	"github.com/coreos/go-oidc"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/oauth2"
+	apiv1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/fake"
+)
+
+const testNamespace = "argo"
+
+type fakeOidcProvider struct{}
+
+func (fakeOidcProvider) Endpoint() oauth2.Endpoint {
+	return oauth2.Endpoint{}
+}
+
+func (fakeOidcProvider) Verifier(config *oidc.Config) *oidc.IDTokenVerifier {
+	return nil
+}
+
+func fakeOidcFactory(ctx context.Context, issuer string) (providerInterface, error) {
+	return fakeOidcProvider{}, nil
+}
+
+func getSecretKeySelector(secret, key string) apiv1.SecretKeySelector {
+	return apiv1.SecretKeySelector{
+		LocalObjectReference: apiv1.LocalObjectReference{
+			Name: secret,
+		},
+		Key: key,
+	}
+}
+
+var ssoConfigSecret = &apiv1.Secret{
+	ObjectMeta: metav1.ObjectMeta{
+		Namespace: testNamespace,
+		Name:      "argo-sso-secret",
+	},
+	Type: apiv1.SecretTypeOpaque,
+	Data: map[string][]byte{
+		"client-id":     []byte("sso-client-id-value"),
+		"client-secret": []byte("sso-client-secret-value"),
+	},
+}
+
+func TestLoadSsoClientIdFromSecret(t *testing.T) {
+	fakeClient := fake.NewSimpleClientset(ssoConfigSecret).CoreV1().Secrets(testNamespace)
+	config := Config{
+		Issuer:       "https://test-issuer",
+		ClientID:     getSecretKeySelector("argo-sso-secret", "client-id"),
+		ClientSecret: getSecretKeySelector("argo-sso-secret", "client-secret"),
+		RedirectURL:  "https://dummy",
+	}
+	ssoInterface, err := newSso(fakeOidcFactory, config, fakeClient, "/", false)
+	require.NoError(t, err)
+	ssoObject := ssoInterface.(*sso)
+	assert.Equal(t, "sso-client-id-value", ssoObject.config.ClientID)
+	assert.Equal(t, "sso-client-secret-value", ssoObject.config.ClientSecret)
+}
+
+func TestLoadSsoClientIdFromDifferentSecret(t *testing.T) {
+	clientIDSecret := &apiv1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: testNamespace,
+			Name:      "other-secret",
+		},
+		Type: apiv1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"client-id": []byte("sso-client-id-value"),
+		},
+	}
+
+	fakeClient := fake.NewSimpleClientset(ssoConfigSecret, clientIDSecret).CoreV1().Secrets(testNamespace)
+	config := Config{
+		Issuer:       "https://test-issuer",
+		ClientID:     getSecretKeySelector("other-secret", "client-id"),
+		ClientSecret: getSecretKeySelector("argo-sso-secret", "client-secret"),
+		RedirectURL:  "https://dummy",
+	}
+	ssoInterface, err := newSso(fakeOidcFactory, config, fakeClient, "/", false)
+	require.NoError(t, err)
+	ssoObject := ssoInterface.(*sso)
+	assert.Equal(t, "sso-client-id-value", ssoObject.config.ClientID)
+}
+
+func TestLoadSsoClientIdFromSecretNoKeyFails(t *testing.T) {
+	fakeClient := fake.NewSimpleClientset(ssoConfigSecret).CoreV1().Secrets(testNamespace)
+	config := Config{
+		Issuer:       "https://test-issuer",
+		ClientID:     getSecretKeySelector("argo-sso-secret", "nonexistent"),
+		ClientSecret: getSecretKeySelector("argo-sso-secret", "client-secret"),
+		RedirectURL:  "https://dummy",
+	}
+	_, err := newSso(fakeOidcFactory, config, fakeClient, "/", false)
+	require.Error(t, err)
+	assert.Regexp(t, "key nonexistent missing in secret argo-sso-secret", err.Error())
+}
diff --git a/server/workflow/workflow_server_test.go b/server/workflow/workflow_server_test.go
index c7d1cdf4c..ad85a759f 100644
--- a/server/workflow/workflow_server_test.go
+++ b/server/workflow/workflow_server_test.go
@@ -6,6 +6,12 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/argoproj/argo/pkg/client/clientset/versioned"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/argoproj/argo/util"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	corev1 "k8s.io/api/core/v1"
@@ -621,6 +627,24 @@ func TestWatchWorkflows(t *testing.T) {
 	cancel()
 }
 
+func TestWatchLatestWorkflow(t *testing.T) {
+	server, ctx := getWorkflowServer()
+	wf := &v1alpha1.Workflow{
+		Status: v1alpha1.WorkflowStatus{Phase: v1alpha1.NodeSucceeded},
+	}
+	assert.NoError(t, json.Unmarshal([]byte(wf1), &wf))
+	ctx, cancel := context.WithCancel(ctx)
+	go func() {
+		err := server.WatchWorkflows(&workflowpkg.WatchWorkflowsRequest{
+			ListOptions: &metav1.ListOptions{
+				FieldSelector: util.GenerateFieldSelectorFromWorkflowName("@latest"),
+			},
+		}, &testWatchWorkflowServer{testServerStream{ctx}})
+		assert.EqualError(t, err, "context canceled")
+	}()
+	cancel()
+}
+
 func TestGetWorkflowWithNotFound(t *testing.T) {
 	server, ctx := getWorkflowServer()
 	t.Run("Labelled", func(t *testing.T) {
@@ -633,6 +657,36 @@ func TestGetWorkflowWithNotFound(t *testing.T) {
 		_, err := getWorkflow(ctx, server, "test", "unlabelled")
 		assert.Error(t, err)
 	})
+
+}
+
+func TestGetLatestWorkflow(t *testing.T) {
+	_, ctx := getWorkflowServer()
+	wfClient := ctx.Value(auth.WfKey).(versioned.Interface)
+	wf, err := getLatestWorkflow(wfClient, "test")
+	if assert.NoError(t, err) {
+		assert.Equal(t, wf.Name, "hello-world-9tql2-test")
+	}
+}
+
+func TestGetWorkflow(t *testing.T) {
+	server, ctx := getWorkflowServer()
+	s := server.(*workflowServer)
+	wfClient := auth.GetWfClient(ctx)
+	wf, err := s.getWorkflow(wfClient, "test", "hello-world-9tql2-test", metav1.GetOptions{})
+	if assert.NoError(t, err) {
+		assert.NotNil(t, wf)
+	}
+}
+
+func TestValidateWorkflow(t *testing.T) {
+	server, ctx := getWorkflowServer()
+	s := server.(*workflowServer)
+	wfClient := auth.GetWfClient(ctx)
+	wf, err := s.getWorkflow(wfClient, "test", "hello-world-9tql2-test", metav1.GetOptions{})
+	if assert.NoError(t, err) {
+		assert.NoError(t, s.validateWorkflow(wf))
+	}
 }
 
 func TestListWorkflow(t *testing.T) {
@@ -661,6 +715,10 @@ func TestDeleteWorkflow(t *testing.T) {
 		_, err := server.DeleteWorkflow(ctx, &workflowpkg.WorkflowDeleteRequest{Name: "unlabelled", Namespace: "workflows"})
 		assert.Error(t, err)
 	})
+	t.Run("Latest", func(t *testing.T) {
+		_, err := server.DeleteWorkflow(ctx, &workflowpkg.WorkflowDeleteRequest{Name: "@latest", Namespace: "workflows"})
+		assert.NoError(t, err)
+	})
 }
 
 func TestRetryWorkflow(t *testing.T) {
@@ -675,6 +733,10 @@ func TestRetryWorkflow(t *testing.T) {
 		_, err := server.RetryWorkflow(ctx, &workflowpkg.WorkflowRetryRequest{Name: "unlabelled", Namespace: "workflows"})
 		assert.Error(t, err)
 	})
+	t.Run("Latest", func(t *testing.T) {
+		_, err := server.RetryWorkflow(ctx, &workflowpkg.WorkflowRetryRequest{Name: "latest", Namespace: "workflows"})
+		assert.Error(t, err)
+	})
 }
 
 func TestSuspendResumeWorkflow(t *testing.T) {
@@ -757,6 +819,12 @@ func TestResubmitWorkflow(t *testing.T) {
 		_, err := server.ResubmitWorkflow(ctx, &workflowpkg.WorkflowResubmitRequest{Name: "unlabelled", Namespace: "workflows"})
 		assert.Error(t, err)
 	})
+	t.Run("Latest", func(t *testing.T) {
+		wf, err := server.ResubmitWorkflow(ctx, &workflowpkg.WorkflowResubmitRequest{Name: "@latest", Namespace: "workflows"})
+		if assert.NoError(t, err) {
+			assert.NotNil(t, wf)
+		}
+	})
 }
 
 func TestLintWorkflow(t *testing.T) {
diff --git a/test/e2e/fixtures/then.go b/test/e2e/fixtures/then.go
index 91362fb9e..f87273e36 100644
--- a/test/e2e/fixtures/then.go
+++ b/test/e2e/fixtures/then.go
@@ -2,9 +2,9 @@ package fixtures
 
 import (
 	"testing"
+	"time"
 
 	log "github.com/sirupsen/logrus"
-	"github.com/stretchr/testify/assert"
 	apiv1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -85,36 +85,34 @@ func (t *Then) ExpectWorkflowList(listOptions metav1.ListOptions, block func(t *
 	return t
 }
 
-func (t *Then) expectAuditEvents(block func(*testing.T, []apiv1.Event)) *Then {
-	eventList, err := t.kubeClient.CoreV1().Events(Namespace).List(metav1.ListOptions{})
+func (t *Then) ExpectAuditEvents(blocks ...func(*testing.T, apiv1.Event)) *Then {
+	eventList, err := t.kubeClient.CoreV1().Events(Namespace).Watch(metav1.ListOptions{})
 	if err != nil {
 		t.t.Fatal(err)
 	}
-	var events []apiv1.Event
-	for _, e := range eventList.Items {
-		if e.Namespace == Namespace && e.InvolvedObject.Kind == workflow.WorkflowKind {
-			events = append(events, e)
+	ticker := time.NewTicker(5 * time.Second)
+	defer ticker.Stop()
+	for len(blocks) > 0 {
+		select {
+		case <-ticker.C:
+			t.t.Fatal("timeout waiting for events")
+		case event := <-eventList.ResultChan():
+			e, ok := event.Object.(*apiv1.Event)
+			if !ok {
+				t.t.Fatal("event is not an event")
+			}
+			if e.InvolvedObject.Name == t.workflowName && e.Namespace == Namespace && e.InvolvedObject.Kind == workflow.WorkflowKind {
+				blocks[0](t.t, *e)
+				blocks = blocks[1:]
+				if t.t.Failed() {
+					t.t.FailNow()
+				}
+			}
 		}
 	}
-	log.WithFields(log.Fields{"event": events}).Debug("Events")
-	block(t.t, events)
-	if t.t.Failed() {
-		t.t.FailNow()
-	}
 	return t
 }
 
-func (t *Then) ExpectAuditEvent(f func(apiv1.Event) bool) *Then {
-	return t.expectAuditEvents(func(t *testing.T, events []apiv1.Event) {
-		for _, item := range events {
-			if f(item) {
-				return
-			}
-		}
-		assert.Fail(t, "did not see expected event")
-	})
-}
-
 func (t *Then) RunCli(args []string, block func(t *testing.T, output string, err error)) *Then {
 	output, err := runCli("../../dist/argo", append([]string{"-n", Namespace}, args...)...)
 	block(t.t, output, err)
diff --git a/test/e2e/functional_test.go b/test/e2e/functional_test.go
index 25b69119b..a68a3c91e 100644
--- a/test/e2e/functional_test.go
+++ b/test/e2e/functional_test.go
@@ -2,7 +2,6 @@ package e2e
 
 import (
 	"regexp"
-	"strings"
 	"testing"
 	"time"
 
@@ -11,10 +10,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/argoproj/argo/pkg/apis/workflow"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/test/e2e/fixtures"
-	"github.com/argoproj/argo/util/argo"
 )
 
 type FunctionalSuite struct {
@@ -187,20 +184,23 @@ func (s *FunctionalSuite) TestEventOnNodeFail() {
 		Workflow("@expectedfailures/failed-step-event.yaml").
 		When().
 		SubmitWorkflow().
-		WaitForWorkflow(30 * time.Second).
+		WaitForWorkflow(30*time.Second).
 		Then().
-		ExpectAuditEvent(func(e corev1.Event) bool {
-			return strings.HasPrefix(e.InvolvedObject.Name, "failed-step-event-") &&
-				e.Reason == argo.EventReasonWorkflowFailed &&
-				e.Message == "failed with exit code 1"
-		}).
-		ExpectAuditEvent(func(e corev1.Event) bool {
-			return e.InvolvedObject.Kind == workflow.WorkflowKind &&
-				e.Reason == argo.EventReasonWorkflowNodeFailed &&
-				strings.HasPrefix(e.Message, "Failed node failed-step-event-") &&
-				e.Annotations["workflows.argoproj.io/node-type"] == "Pod" &&
-				strings.Contains(e.Annotations["workflows.argoproj.io/node-name"], "failed-step-event-")
-		})
+		ExpectAuditEvents(
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowRunning", e.Reason)
+			},
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, e.Reason, "WorkflowNodeFailed")
+				assert.Contains(t, e.Message, "Failed node failed-step-event-")
+				assert.Equal(t, e.Annotations["workflows.argoproj.io/node-type"], "Pod")
+				assert.Contains(t, e.Annotations["workflows.argoproj.io/node-name"], "failed-step-event-")
+			},
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowFailed", e.Reason)
+				assert.Equal(t, "failed with exit code 1", e.Message)
+			},
+		)
 }
 
 func (s *FunctionalSuite) TestEventOnWorkflowSuccess() {
@@ -209,20 +209,23 @@ func (s *FunctionalSuite) TestEventOnWorkflowSuccess() {
 		Workflow("@functional/success-event.yaml").
 		When().
 		SubmitWorkflow().
-		WaitForWorkflow(60 * time.Second).
+		WaitForWorkflow(60*time.Second).
 		Then().
-		ExpectAuditEvent(func(e corev1.Event) bool {
-			return strings.HasPrefix(e.InvolvedObject.Name, "success-event-") &&
-				e.Reason == argo.EventReasonWorkflowSucceeded &&
-				e.Message == "Workflow completed"
-		}).
-		ExpectAuditEvent(func(e corev1.Event) bool {
-			return e.InvolvedObject.Kind == workflow.WorkflowKind &&
-				e.Reason == argo.EventReasonWorkflowNodeSucceeded &&
-				strings.HasPrefix(e.Message, "Succeeded node success-event-") &&
-				e.Annotations["workflows.argoproj.io/node-type"] == "Pod" &&
-				strings.Contains(e.Annotations["workflows.argoproj.io/node-name"], "success-event-")
-		})
+		ExpectAuditEvents(
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowRunning", e.Reason)
+			},
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowNodeSucceeded", e.Reason)
+				assert.Contains(t, e.Message, "Succeeded node success-event-")
+				assert.Equal(t, "Pod", e.Annotations["workflows.argoproj.io/node-type"])
+				assert.Contains(t, e.Annotations["workflows.argoproj.io/node-name"], "success-event-")
+			},
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowSucceeded", e.Reason)
+				assert.Equal(t, "Workflow completed", e.Message)
+			},
+		)
 }
 
 func (s *FunctionalSuite) TestEventOnPVCFail() {
@@ -231,13 +234,17 @@ func (s *FunctionalSuite) TestEventOnPVCFail() {
 		Workflow("@expectedfailures/volumes-pvc-fail-event.yaml").
 		When().
 		SubmitWorkflow().
-		WaitForWorkflow(120 * time.Second).
+		WaitForWorkflow(120*time.Second).
 		Then().
-		ExpectAuditEvent(func(e corev1.Event) bool {
-			return strings.HasPrefix(e.InvolvedObject.Name, "volumes-pvc-fail-event-") &&
-				e.Reason == argo.EventReasonWorkflowFailed &&
-				strings.Contains(e.Message, "pvc create error")
-		})
+		ExpectAuditEvents(
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowRunning", e.Reason)
+			},
+			func(t *testing.T, e corev1.Event) {
+				assert.Equal(t, "WorkflowFailed", e.Reason)
+				assert.Contains(t, e.Message, "pvc create error")
+			},
+		)
 }
 
 func (s *FunctionalSuite) TestArtifactRepositoryRef() {
diff --git a/test/e2e/manifests/minimal/kustomization.yaml b/test/e2e/manifests/minimal/kustomization.yaml
new file mode 100644
index 000000000..67314bc19
--- /dev/null
+++ b/test/e2e/manifests/minimal/kustomization.yaml
@@ -0,0 +1,14 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+  - ../../../../manifests/quick-start/minimal
+
+patchesStrategicMerge:
+  - ../mixins/argo-server-deployment.yaml
+  - ../mixins/workflow-controller-configmap.yaml
+  - ../mixins/workflow-controller-deployment.yaml
+  - ../mixins/cluster-workflow-template-rbac.yaml
+
+commonLabels:
+  "app.kubernetes.io/part-of": "argo"
diff --git a/test/e2e/manifests/mysql/kustomization.yaml b/test/e2e/manifests/mysql/kustomization.yaml
index b915c54ee..7c7e873be 100644
--- a/test/e2e/manifests/mysql/kustomization.yaml
+++ b/test/e2e/manifests/mysql/kustomization.yaml
@@ -1,10 +1,14 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
   - ../../../../manifests/quick-start/mysql
+
 patchesStrategicMerge:
   - ../mixins/argo-server-deployment.yaml
   - ../mixins/workflow-controller-configmap.yaml
   - ../mixins/workflow-controller-deployment.yaml
   - ../mixins/cluster-workflow-template-rbac.yaml
+
+commonLabels:
+  "app.kubernetes.io/part-of": "argo"
diff --git a/test/e2e/manifests/postgres/kustomization.yaml b/test/e2e/manifests/postgres/kustomization.yaml
index 56c6f5b5b..2108ccc27 100644
--- a/test/e2e/manifests/postgres/kustomization.yaml
+++ b/test/e2e/manifests/postgres/kustomization.yaml
@@ -1,10 +1,14 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
-bases:
+resources:
   - ../../../../manifests/quick-start/postgres
+
 patchesStrategicMerge:
   - ../mixins/argo-server-deployment.yaml
   - ../mixins/workflow-controller-configmap.yaml
   - ../mixins/workflow-controller-deployment.yaml
   - ../mixins/cluster-workflow-template-rbac.yaml
+
+commonLabels:
+  "app.kubernetes.io/part-of": "argo"
diff --git a/test/e2e/manifests/no-db/kustomization.yaml b/test/e2e/manifests/sso/kustomization.yaml
similarity index 72%
rename from test/e2e/manifests/no-db/kustomization.yaml
rename to test/e2e/manifests/sso/kustomization.yaml
index 82872d676..aab7b7d9f 100644
--- a/test/e2e/manifests/no-db/kustomization.yaml
+++ b/test/e2e/manifests/sso/kustomization.yaml
@@ -1,9 +1,14 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
-bases:
-  - ../../../../manifests/quick-start/no-db
+
+resources:
+  - ../../../../manifests/quick-start/sso
+
 patchesStrategicMerge:
   - ../mixins/argo-server-deployment.yaml
   - ../mixins/workflow-controller-configmap.yaml
   - ../mixins/workflow-controller-deployment.yaml
   - ../mixins/cluster-workflow-template-rbac.yaml
+
+commonLabels:
+  "app.kubernetes.io/part-of": "argo"
diff --git a/util/argo/audit_logger_test.go b/util/argo/audit_logger_test.go
deleted file mode 100644
index 6ad7ec1ef..000000000
--- a/util/argo/audit_logger_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package argo
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes/fake"
-
-	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
-)
-
-func TestAuditLogger_logEvent(t *testing.T) {
-	kube := fake.NewSimpleClientset()
-	l := AuditLogger{kIf: kube, component: "my-component"}
-	l.LogWorkflowNodeEvent(
-		&wfv1.Workflow{
-			ObjectMeta: metav1.ObjectMeta{Namespace: "my-ns", Name: "my-wf", UID: "my-uid", ResourceVersion: "1234"},
-		},
-		&wfv1.NodeStatus{Type: wfv1.NodeTypePod, Phase: wfv1.NodeSucceeded, Name: "my-node", Message: "my message"},
-	)
-	list, err := kube.CoreV1().Events("my-ns").List(metav1.ListOptions{})
-	if assert.NoError(t, err) && assert.Len(t, list.Items, 1) {
-		e := list.Items[0]
-		assert.NotEmpty(t, e.Name)
-		assert.Len(t, e.Annotations, 2)
-		assert.Equal(t, "my-node", e.Annotations["workflows.argoproj.io/node-name"])
-		assert.Equal(t, "Pod", e.Annotations["workflows.argoproj.io/node-type"])
-		assert.Equal(t, "my-component", e.Source.Component)
-		assert.Equal(t, corev1.ObjectReference{
-			Kind:            "Workflow",
-			Namespace:       "my-ns",
-			Name:            "my-wf",
-			UID:             "my-uid",
-			APIVersion:      "argoproj.io/v1alpha1",
-			ResourceVersion: "1234",
-		}, e.InvolvedObject)
-		assert.NotEmpty(t, e.FirstTimestamp)
-		assert.NotEmpty(t, e.LastTimestamp)
-		assert.Equal(t, int32(1), e.Count)
-		assert.Equal(t, "Succeeded node my-node: my message", e.Message)
-		assert.Equal(t, "Normal", e.Type)
-		assert.Equal(t, "WorkflowNodeSucceeded", e.Reason)
-	}
-}
-
-func Test_eventType(t *testing.T) {
-	assert.Equal(t, corev1.EventTypeWarning, eventType(wfv1.NodeError))
-	assert.Equal(t, corev1.EventTypeWarning, eventType(wfv1.NodeFailed))
-	assert.Equal(t, corev1.EventTypeNormal, eventType(wfv1.NodeSucceeded))
-}
-
-func Test_nodePhaseReason(t *testing.T) {
-	assert.Equal(t, EventReasonWorkflowNodeError, nodePhaseReason(wfv1.NodeError))
-	assert.Equal(t, EventReasonWorkflowNodeFailed, nodePhaseReason(wfv1.NodeFailed))
-	assert.Equal(t, EventReasonWorkflowNodeSucceeded, nodePhaseReason(wfv1.NodeSucceeded))
-}
-
-func Test_nodeMessage(t *testing.T) {
-	assert.Equal(t, "Succeeded node my-node", nodeMessage(&wfv1.NodeStatus{Phase: wfv1.NodeSucceeded, Name: "my-node"}))
-	assert.Equal(t, "Succeeded node my-node: my-message", nodeMessage(&wfv1.NodeStatus{Phase: wfv1.NodeSucceeded, Name: "my-node", Message: "my-message"}))
-}
diff --git a/util/util_test.go b/util/util_test.go
index 1fad3b1dd..378548f97 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -2,59 +2,44 @@ package util
 
 import (
 	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 )
 
-func TestMergeParameters(t *testing.T) {
-	one := "one"
-	two := "two"
-	param1 := []wfv1.Parameter{
-		{
-			Name:  "p1",
-			Value: &one,
-		},
-		{
-			Name: "p2",
-		},
+func TestGenerateFieldSelectorFromWorkflowName(t *testing.T) {
+	type args struct {
+		wfName string
 	}
-	param2 := []wfv1.Parameter{
-		{
-			Name:  "p1",
-			Value: &two,
-		},
-		{
-			Name: "p3",
-		},
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"TestGenerateFieldSelectorFromWorkflowName", args{"whalesay"}, "metadata.name=whalesay"},
 	}
-	t.Run("MergeParameter-1", func(t *testing.T) {
-		result := MergeParameters(param1, param2)
-		assert.Equal(t, len(result), 3)
-		for _, item := range result {
-			if item.Name == "p1" {
-				assert.Equal(t, "one", *item.Value)
-			}
-		}
-	})
-	t.Run("MergeParameter-2", func(t *testing.T) {
-		result := MergeParameters(param2, param1)
-		assert.Equal(t, len(result), 3)
-		for _, item := range result {
-			if item.Name == "p1" {
-				assert.Equal(t, "two", *item.Value)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GenerateFieldSelectorFromWorkflowName(tt.args.wfName); got != tt.want {
+				t.Errorf("GenerateFieldSelectorFromWorkflowName() = %v, want %v", got, tt.want)
 			}
-		}
-	})
-
+		})
+	}
 }
 
-func TestRecoverIndexFromNodeName(t *testing.T) {
-	out := RecoverIndexFromNodeName("sleep(10:ten)")
-	assert.Equal(t, 10, out)
-	out = RecoverIndexFromNodeName("sleep(17:[foobar]])")
-	assert.Equal(t, 17, out)
-	out = RecoverIndexFromNodeName("sleep(1:a;skldfja)")
-	assert.Equal(t, 1, out)
+func TestRecoverWorkflowNameFromSelectorString(t *testing.T) {
+	type args struct {
+		selector string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"TestRecoverWorkflowNameFromSelectorString", args{"metadata.name=whalesay"}, "whalesay"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := RecoverWorkflowNameFromSelectorString(tt.args.selector); got != tt.want {
+				t.Errorf("RecoverWorkflowNameFromSelectorString() = %v, want %v", got, tt.want)
+			}
+		})
+	}
 }
diff --git a/workflow/controller/controller_test.go b/workflow/controller/controller_test.go
index 46c3b8335..185f7eedc 100644
--- a/workflow/controller/controller_test.go
+++ b/workflow/controller/controller_test.go
@@ -14,6 +14,7 @@ import (
 	"k8s.io/client-go/kubernetes/fake"
 	k8stesting "k8s.io/client-go/testing"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 	"sigs.k8s.io/yaml"
 
@@ -102,6 +103,7 @@ spec:
 func newController(objects ...runtime.Object) (context.CancelFunc, *WorkflowController) {
 	wfclientset := fakewfclientset.NewSimpleClientset(objects...)
 	informerFactory := wfextv.NewSharedInformerFactory(wfclientset, 10*time.Minute)
+	wfInformer := cache.NewSharedIndexInformer(nil, nil, 0, nil)
 	wftmplInformer := informerFactory.Argoproj().V1alpha1().WorkflowTemplates()
 	cwftmplInformer := informerFactory.Argoproj().V1alpha1().ClusterWorkflowTemplates()
 	ctx, cancel := context.WithCancel(context.Background())
@@ -120,7 +122,8 @@ func newController(objects ...runtime.Object) (context.CancelFunc, *WorkflowCont
 		},
 		kubeclientset:   kube,
 		wfclientset:     wfclientset,
-		completedPods:   make(chan string, 512),
+		completedPods:   make(chan string, 16),
+		wfInformer:      wfInformer,
 		wftmplInformer:  wftmplInformer,
 		cwftmplInformer: cwftmplInformer,
 		wfQueue:         workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
@@ -128,6 +131,7 @@ func newController(objects ...runtime.Object) (context.CancelFunc, *WorkflowCont
 		wfArchive:       sqldb.NullWorkflowArchive,
 		hydrator:        hydratorfake.Noop,
 		metrics:         metrics.New(metrics.ServerConfig{}, metrics.ServerConfig{}),
+		eventRecorder:   record.NewFakeRecorder(16),
 	}
 	return cancel, controller
 }
diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go
index 31cb386e5..f6a617710 100644
--- a/workflow/controller/operator_test.go
+++ b/workflow/controller/operator_test.go
@@ -13,12 +13,12 @@ import (
 	apiv1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/yaml"
 
 	"github.com/argoproj/argo/config"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/test"
-	"github.com/argoproj/argo/util/argo"
 	"github.com/argoproj/argo/workflow/common"
 	hydratorfake "github.com/argoproj/argo/workflow/hydrator/fake"
 	"github.com/argoproj/argo/workflow/util"
@@ -2730,54 +2730,18 @@ func TestEventInvalidSpec(t *testing.T) {
 	assert.NoError(t, err)
 	woc := newWorkflowOperationCtx(wf, controller)
 	woc.operate()
-	events, err := controller.kubeclientset.CoreV1().Events("").List(metav1.ListOptions{})
-	assert.NoError(t, err)
-	assert.Equal(t, 2, len(events.Items))
-	runningEvent := events.Items[0]
-	assert.Equal(t, argo.EventReasonWorkflowRunning, runningEvent.Reason)
-	invalidSpecEvent := events.Items[1]
-	assert.Equal(t, argo.EventReasonWorkflowFailed, invalidSpecEvent.Reason)
-	assert.Equal(t, "invalid spec: template name '123' undefined", invalidSpecEvent.Message)
+	events := getEvents(controller, 2)
+	assert.Equal(t, "Normal WorkflowRunning Workflow Running", events[0])
+	assert.Equal(t, "Warning WorkflowFailed invalid spec: template name '123' undefined", events[1])
 }
 
-var timeout = `
-apiVersion: argoproj.io/v1alpha1
-kind: Workflow
-metadata:
-  name: timeout-template
-spec:
-  entrypoint: sleep
-  activeDeadlineSeconds: 1
-  templates:
-  - name: sleep
-    container:
-      image: alpine:latest
-      command: [sh, -c, sleep 10]
-`
-
-func TestEventTimeout(t *testing.T) {
-	// Test whether a WorkflowTimedOut event is emitted in case of timeout
-	cancel, controller := newController()
-	defer cancel()
-	wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("")
-	wf := unmarshalWF(timeout)
-	wf, err := wfcset.Create(wf)
-	assert.NoError(t, err)
-	woc := newWorkflowOperationCtx(wf, controller)
-	woc.operate()
-	makePodsPhase(t, apiv1.PodRunning, controller.kubeclientset, wf.ObjectMeta.Namespace)
-	wf, err = wfcset.Get(wf.ObjectMeta.Name, metav1.GetOptions{})
-	assert.NoError(t, err)
-	woc = newWorkflowOperationCtx(wf, controller)
-	time.Sleep(10 * time.Second)
-	woc.operate()
-	events, err := controller.kubeclientset.CoreV1().Events("").List(metav1.ListOptions{})
-	assert.NoError(t, err)
-	assert.Equal(t, 2, len(events.Items))
-	runningEvent := events.Items[0]
-	assert.Equal(t, argo.EventReasonWorkflowRunning, runningEvent.Reason)
-	timeoutEvent := events.Items[1]
-	assert.Equal(t, argo.EventReasonWorkflowFailed, timeoutEvent.Reason)
+func getEvents(controller *WorkflowController, num int) []string {
+	c := controller.eventRecorder.(*record.FakeRecorder).Events
+	events := make([]string, num)
+	for i := 0; i < num; i++ {
+		events[i] = <-c
+	}
+	return events
 }
 
 var failLoadArtifactRepoCm = `
@@ -2812,14 +2776,9 @@ func TestEventFailArtifactRepoCm(t *testing.T) {
 	assert.NoError(t, err)
 	woc := newWorkflowOperationCtx(wf, controller)
 	woc.operate()
-	events, err := controller.kubeclientset.CoreV1().Events("").List(metav1.ListOptions{})
-	assert.NoError(t, err)
-	assert.Equal(t, 2, len(events.Items))
-	runningEvent := events.Items[0]
-	assert.Equal(t, argo.EventReasonWorkflowRunning, runningEvent.Reason)
-	failEvent := events.Items[1]
-	assert.Equal(t, argo.EventReasonWorkflowFailed, failEvent.Reason)
-	assert.Equal(t, "Failed to load artifact repository configMap: failed to find artifactory ref {,}/artifact-repository#config", failEvent.Message)
+	events := getEvents(controller, 2)
+	assert.Equal(t, "Normal WorkflowRunning Workflow Running", events[0])
+	assert.Equal(t, "Warning WorkflowFailed Failed to load artifact repository configMap: failed to find artifactory ref {,}/artifact-repository#config", events[1])
 }
 
 var pdbwf = `
diff --git a/workflow/cron/operator_test.go b/workflow/cron/operator_test.go
index 925a7051a..7acea475c 100644
--- a/workflow/cron/operator_test.go
+++ b/workflow/cron/operator_test.go
@@ -183,6 +183,63 @@ func TestCronWorkflowConditionSubmissionError(t *testing.T) {
 	assert.Len(t, woc.cronWf.Status.Conditions, 1)
 	submissionErrorCond := woc.cronWf.Status.Conditions[0]
 	assert.Equal(t, v1.ConditionTrue, submissionErrorCond.Status)
-	assert.Equal(t, v1alpha1.ConditionTypeSubmissionError, submissionErrorCond.Type)
+	assert.Equal(t, v1alpha1.ConditionTypeSpecError, submissionErrorCond.Type)
 	assert.Contains(t, submissionErrorCond.Message, "'bad template name' is invalid")
 }
+
+var specError = `
+apiVersion: argoproj.io/v1alpha1
+kind: CronWorkflow
+metadata:
+  name: hello-world
+spec:
+  concurrencyPolicy: Replace
+  failedJobsHistoryLimit: 4
+  schedule: 10 * * 12737123 *
+  startingDeadlineSeconds: 0
+  successfulJobsHistoryLimit: 4
+  timezone: America/Los_Angeles
+  workflowSpec:
+    arguments: {}
+    entrypoint: whalesay
+    templates:
+    - arguments: {}
+      container:
+        args:
+        - "\U0001F553 hello world"
+        command:
+        - cowsay
+        image: docker/whalesay:latest
+        name: ""
+        resources: {}
+      inputs: {}
+      metadata: {}
+      name: whalesay
+      outputs: {}
+`
+
+func TestSpecError(t *testing.T) {
+	var cronWf v1alpha1.CronWorkflow
+	err := yaml.Unmarshal([]byte(specError), &cronWf)
+	assert.NoError(t, err)
+
+	cs := fake.NewSimpleClientset()
+	testMetrics := metrics.New(metrics.ServerConfig{}, metrics.ServerConfig{})
+	woc := &cronWfOperationCtx{
+		wfClientset: cs,
+		wfClient:    cs.ArgoprojV1alpha1().Workflows(""),
+		cronWfIf:    cs.ArgoprojV1alpha1().CronWorkflows(""),
+		wfLister:    &fakeLister{},
+		cronWf:      &cronWf,
+		log:         logrus.WithFields(logrus.Fields{}),
+		metrics:     &testMetrics,
+	}
+
+	err = woc.validateCronWorkflow()
+	assert.Error(t, err)
+	assert.Len(t, woc.cronWf.Status.Conditions, 1)
+	submissionErrorCond := woc.cronWf.Status.Conditions[0]
+	assert.Equal(t, v1.ConditionTrue, submissionErrorCond.Status)
+	assert.Equal(t, v1alpha1.ConditionTypeSpecError, submissionErrorCond.Type)
+	assert.Contains(t, submissionErrorCond.Message, "cron schedule is malformed: End of range (12737123) above maximum (12): 12737123")
+}