Repository: argoproj/argo
Pull request #3251 (2020-06-18 12:01:44)

fix(jqFilter)!: remove extra quotes around output parameter value
When playing with jqFilter, the return value begins with double quotes and ends in double quotes, also followed by a newline: i.e.:

```
 "parameter value"\n
```

Related PR : argoproj#1232

This commit ensure we do not insert extra double quotes and trailing new line when using
valueFrom: jqFilter to set the value of an output parameter for
resource templates.

Checklist:

* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this is a chore.
* [ ] The title of the PR is (a) [conventional](https://www.conventionalcommits.org/en/v1.0.0/), (b) states what changed, and (c) suffixes the related issues number. E.g. `"fix(controller): Updates such and such. Fixes #1234"`.  
* [ ] I've signed the CLA.
* [ ] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
* [ ] My builds are green. Try syncing with master if they are not. 
* [ ] My organization is added to [USERS.md](https://github.com/argoproj/argo/blob/master/USERS.md).

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/argoproj/argo?pullRequest=3251) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/argoproj/argo?pullRequest=3251) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/argoproj/argo?pullRequest=3251) it.</sub>
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=argoproj_argo&pullRequest=3251&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=argoproj_argo&pullRequest=3251&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=argoproj_argo&pullRequest=3251&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=argoproj_argo&pullRequest=3251&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=argoproj_argo&pullRequest=3251&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 90fc4e238..026ae1d24 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,77 +1,16 @@
 version: 2.1
-orbs:
-  sonarcloud: sonarsource/sonarcloud@1.0.1
-commands:
-  restore_go_mod:
-    steps:
-      - restore_cache:
-          name: Restore go mod cache
-          keys:
-            - go-mod-v3-{{ checksum "go.sum" }}-{{ .Environment.CIRCLE_JOB }}
-  save_go_mod:
-    steps:
-      - save_cache:
-          name: Save go mod cache
-          key:  go-mod-v3-{{ checksum "go.sum" }}-{{ .Environment.CIRCLE_JOB }}
-          paths:
-            - /home/circleci/.go_workspace/go/pkg/mod
-          when: always
-  install_golang:
+jobs:
+  ci:
+    docker:
+      - image: cimg/base:2020.01
     steps:
       - run:
-          name: Install Golang v1.13.4
+          name: hello
           command: |
-            go get golang.org/dl/go1.13.4
-            [ -e /home/circleci/sdk/go1.13.4 ] || go1.13.4 download
-            echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
-            echo "export PATH=/home/circleci/sdk/go1.13.4/bin:\$PATH" | tee -a $BASH_ENV
-  save_go_cache:
-    steps:
-      - save_cache:
-          name: Save Golang cache
-          key: go-v7-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
-          paths:
-            - /home/circleci/.cache/go-build
-            - /home/circleci/sdk/go1.13.4
-          when: always
-  restore_go_cache:
-    steps:
-      - restore_cache:
-          name: Restore Golang cache
-          keys:
-            - go-v7-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
-            - go-v7-master-{{ .Environment.CIRCLE_JOB }}
-jobs:
-  sonar:
-    working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo
-    machine:
-      image: ubuntu-1604:201903-01
-    steps:
-      - checkout
-      - restore_go_cache
-      - install_golang
-      - restore_go_mod
-      - run: |
-          mkdir dist
-          cp sonar-project.properties dist/
-          if [ "$(echo "$CIRCLE_BRANCH" | grep 'master\|release-')" = '' ]; then
-            echo "sonar.pullrequest.branch=$CIRCLE_BRANCH" >> sonar-project.properties
-            echo "sonar.pullrequest.key=$CIRCLE_PR_NUMBER" >> sonar-project.properties
-          fi
-      - sonarcloud/scan
-      - run: cp dist/sonar-project.properties .
-      - store_test_results:
-          path: test-results
-          when: always
-      - store_artifacts:
-          path: test-results
-          destination: .
-          when: always
-      - save_go_cache
-      - save_go_mod
+            echo "Hello, world"
 
 workflows:
   version: 2
-  CI:
+  workflow:
     jobs:
-      - sonar
+      - ci
diff --git a/Makefile b/Makefile
index 2019e9d8b..ee90b12d0 100644
--- a/Makefile
+++ b/Makefile
@@ -511,13 +511,13 @@ pre-commit: test lint codegen manifests start smoke test-api test-cli
 ifneq ($(findstring release,$(GIT_BRANCH)),)
 
 .PHONY: prepare-release
-prepare-release: clean codegen manifests
+prepare-release: check-version-warning clean codegen manifests
 	# Commit if any changes
 	git diff --quiet || git commit -am "Update manifests to $(VERSION)"
 	git tag $(VERSION)
 
 .PHONY: publish-release
-publish-release: build
+publish-release: check-version-warning build
 	# Push images to Docker Hub
 	docker push $(IMAGE_NAMESPACE)/argocli:$(VERSION)
 	docker push $(IMAGE_NAMESPACE)/argoexec:$(VERSION)
@@ -525,3 +525,7 @@ publish-release: build
 	git push
 	git push $(GIT_REMOTE) $(VERSION)
 endif
+
+.PHONY: check-version-warning
+check-version-warning:
+	@if [[ "$(VERSION)" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$  ]]; then echo -n "It looks like you're trying to use a SemVer version, but have not prepended it with a "v" (such as "v$(VERSION)"). The "v" is required for our releases. Do you wish to continue anyway? [y/N]" && read ans && [ $${ans:-N} = y ]; fi
diff --git a/USERS.md b/USERS.md
index 0c75333a9..dabd6b11e 100644
--- a/USERS.md
+++ b/USERS.md
@@ -25,6 +25,7 @@ Currently, the following organizations are **officially** using Argo Workflows:
 1. [CoreFiling](https://www.corefiling.com/)
 1. [Cratejoy](https://www.cratejoy.com/)
 1. [CoreWeave Cloud](https://www.coreweave.com)
+1. [Cruise](https://getcruise.com/)
 1. [CyberAgent](https://www.cyberagent.co.jp/en/)
 1. [Cyrus Biotechnology](https://cyrusbio.com/)
 1. [Datadog](https://www.datadoghq.com/)
diff --git a/config/config.go b/config/config.go
index f55ad6ce6..1ec28bba1 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,6 +2,8 @@ package config
 
 import (
 	apiv1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
 	"github.com/argoproj/argo/server/auth/sso"
@@ -83,8 +85,11 @@ type Config struct {
 	// WorkflowDefaults are values that will apply to all Workflows from this controller, unless overridden on the Workflow-level
 	WorkflowDefaults *wfv1.Workflow `json:"workflowDefaults,omitempty"`
 
-	// PodSpecLogStrategy enable the logging of podspec on controller log.
+	// PodSpecLogStrategy enables the logging of podspec on controller log.
 	PodSpecLogStrategy PodSpecLogStrategy `json:"podSpecLogStrategy,omitempty"`
+
+	// WorkflowRestrictions restricts the controller to executing Workflows that meet certain restrictions
+	WorkflowRestrictions *WorkflowRestrictions `json:"workflowRestrictions,omitempty"`
 }
 
 // PodSpecLogStrategy contains the configuration for logging the pod spec in controller log for debugging purpose
@@ -132,6 +137,8 @@ type PersistConfig struct {
 	NodeStatusOffload bool `json:"nodeStatusOffLoad,omitempty"`
 	// Archive workflows to persistence.
 	Archive bool `json:"archive,omitempty"`
+	// ArchivelabelSelector holds LabelSelector to determine workflow persistence.
+	ArchiveLabelSelector *metav1.LabelSelector `json:"archiveLabelSelector,omitempty"`
 	// in days
 	ArchiveTTL     TTL               `json:"archiveTTL,omitempty"`
 	ClusterName    string            `json:"clusterName,omitempty"`
@@ -140,6 +147,13 @@ type PersistConfig struct {
 	MySQL          *MySQLConfig      `json:"mysql,omitempty"`
 }
 
+func (c PersistConfig) GetArchiveLabelSelector() (labels.Selector, error) {
+	if c.ArchiveLabelSelector == nil {
+		return labels.Everything(), nil
+	}
+	return metav1.LabelSelectorAsSelector(c.ArchiveLabelSelector)
+}
+
 func (c PersistConfig) GetClusterName() string {
 	if c.ClusterName != "" {
 		return c.ClusterName
@@ -152,6 +166,7 @@ type ConnectionPool struct {
 	MaxOpenConns    int `json:"maxOpenConns,omitempty"`
 	ConnMaxLifetime TTL `json:"connMaxLifetime,omitempty"`
 }
+
 type PostgreSQLConfig struct {
 	Host           string                  `json:"host"`
 	Port           string                  `json:"port"`
@@ -235,3 +250,28 @@ type MetricsConfig struct {
 	// IgnoreErrors is a flag that instructs prometheus to ignore metric emission errors
 	IgnoreErrors bool `json:"ignoreErrors,omitempty"`
 }
+
+type WorkflowRestrictions struct {
+	TemplateReferencing TemplateReferencing `json:"templateReferencing"`
+}
+
+type TemplateReferencing string
+
+const (
+	TemplateReferencingStrict TemplateReferencing = "Strict"
+	TemplateReferencingSecure TemplateReferencing = "Secure"
+)
+
+func (req *WorkflowRestrictions) MustUseReference() bool {
+	if req == nil {
+		return false
+	}
+	return req.TemplateReferencing == TemplateReferencingStrict || req.TemplateReferencing == TemplateReferencingSecure
+}
+
+func (req *WorkflowRestrictions) MustNotChangeSpec() bool {
+	if req == nil {
+		return false
+	}
+	return req.TemplateReferencing == TemplateReferencingSecure
+}
diff --git a/docs/README.md b/docs/README.md
index e10d6db12..a1e6ba165 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -59,4 +59,5 @@ Some use-case specific documentation is available:
 * [Workflow Executors](workflow-executors.md)
 * [Workflow Notifications](workflow-notifications.md)
 * [Workfow RBAC](workflow-rbac.md)
+* [Workflow Restrictions](workflow-requirements.md)
 * [Workflow Templates](workflow-templates.md)
diff --git a/docs/fields.md b/docs/fields.md
index f69a72d90..9dc48663f 100644
--- a/docs/fields.md
+++ b/docs/fields.md
@@ -3164,6 +3164,8 @@ MetricLabel is a single label for a prometheus metric
 
 - [`dag-enhanced-depends.yaml`](../examples/dag-enhanced-depends.yaml)
 
+- [`hello-world.yaml`](../examples/hello-world.yaml)
+
 - [`pod-metadata.yaml`](../examples/pod-metadata.yaml)
 
 - [`resource-delete-with-flags.yaml`](../examples/resource-delete-with-flags.yaml)
diff --git a/docs/variables.md b/docs/variables.md
index 0f1cf8389..26bd00d0c 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -16,6 +16,7 @@ The following variables are made available to reference various metadata of a wo
 | `steps.<STEPNAME>.status` | Phase status of any previous step |
 | `steps.<STEPNAME>.exitCode` | Exit code of any previous script or container step |
 | `steps.<STEPNAME>.outputs.result` | Output result of any previous container or script step |
+| `steps.<STEPNAME>.outputs.parameters` | When the previous step uses 'withItems', this contains a JSON array of the output parameters of each invocation |
 | `steps.<STEPNAME>.outputs.parameters.<NAME>` | Output parameter of any previous step |
 | `steps.<STEPNAME>.outputs.artifacts.<NAME>` | Output artifact of any previous step |
 
@@ -26,6 +27,7 @@ The following variables are made available to reference various metadata of a wo
 | `tasks.<TASKNAME>.status` | Phase status of any previous task |
 | `tasks.<TASKNAME>.exitCode` | Exit code of any previous script or container task |
 | `tasks.<TASKNAME>.outputs.result` | Output result of any previous container or script task |
+| `tasks.<STEPNAME>.outputs.parameters` | When the previous task uses 'withItems', this contains a JSON array of the output parameters of each invocation |
 | `tasks.<TASKNAME>.outputs.parameters.<NAME>` | Output parameter of any previous task |
 | `tasks.<TASKNAME>.outputs.artifacts.<NAME>` | Output artifact of any previous task |
 
diff --git a/docs/workflow-controller-configmap.yaml b/docs/workflow-controller-configmap.yaml
index 118224c21..5e272bd93 100644
--- a/docs/workflow-controller-configmap.yaml
+++ b/docs/workflow-controller-configmap.yaml
@@ -220,3 +220,13 @@ data:
       # be in the form <argo-server-root-url>/oauth2/callback. It must be
       # browser-accessible.
       redirectUrl: https://argo-server/oauth2/callback
+
+    # workflowRequirements restricts the Workflows that the controller will process.
+    # Current options:
+    #   referenceOnly: Only Workflows using "workflowTemplateRef" will be processed. This allows the administrator of the controller
+    #     to set a "library" of templates that may be run by its opeartor, limiting arbitrary Workflow execution.
+    #   strictReferenceOnly: Only Workflows using "workflowTemplateRef" will be processed and the controller will enforce
+    #     that the WorkflowTemplate that is referenced hasn't changed between operations. If you want to make sure the operator of the
+    #     Workflow cannot run an arbitrary Workflow, use this option.
+    workflowRequirements:
+      referenceOnly: true
diff --git a/docs/workflow-requirements.md b/docs/workflow-requirements.md
new file mode 100644
index 000000000..9aef7c9c0
--- /dev/null
+++ b/docs/workflow-requirements.md
@@ -0,0 +1,31 @@
+# Workflow Restrictions
+
+![alpha](assets/alpha.svg)
+
+> v2.9 and after
+
+## Introduction
+
+As the administrator of the controller, you may want to limit which types of Workflows your users can run. Setting workflow restrictions allows you to ensure that Workflows comply with certain requirements. 
+
+## Available Restrictions
+
+* `templateReferencing: Strict`: Only Workflows using "workflowTemplateRef" will be processed. This allows the administrator of the controller to set a "library" of templates that may be run by its opeartor, limiting arbitrary Workflow execution.
+* `templateReferencing: Secure`: Only Workflows using "workflowTemplateRef" will be processed and the controller will enforce that the WorkflowTemplate that is referenced hasn't changed between operations. If you want to make sure the operator of the Workflow cannot run an arbitrary Workflow, use this option.
+
+## Setting Workflow Restrictions
+
+Workflow Restrictions can be specified by adding them under the `workflowRestrictions` key in the [`workflow-controller-configmap`](./workflow-controller-configmap.yaml).
+
+For example, to specify that Workflows may only run with `workflowTemplateRef`
+
+```yaml
+# This file describes the config settings available in the workflow controller configmap
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: workflow-controller-configmap
+data: |
+  workflowRestrictions:
+    templateReferencing: Secure
+```
diff --git a/examples/hello-world.yaml b/examples/hello-world.yaml
index 51a0994ad..05690abc4 100644
--- a/examples/hello-world.yaml
+++ b/examples/hello-world.yaml
@@ -2,6 +2,8 @@ apiVersion: argoproj.io/v1alpha1
 kind: Workflow
 metadata:
   generateName: hello-world-
+  labels:
+    workflows.argoproj.io/archive-strategy: false
 spec:
   entrypoint: whalesay
   templates:
diff --git a/manifests/quick-start-mysql.yaml b/manifests/quick-start-mysql.yaml
index 5e53202d4..75d9d586c 100644
--- a/manifests/quick-start-mysql.yaml
+++ b/manifests/quick-start-mysql.yaml
@@ -555,7 +555,7 @@ spec:
         image: mysql:8
         name: main
         ports:
-        - containerPort: 5432
+        - containerPort: 3306
         readinessProbe:
           exec:
             command:
diff --git a/manifests/quick-start/mysql/mysql-deployment.yaml b/manifests/quick-start/mysql/mysql-deployment.yaml
index 4013f9413..d41ad079f 100644
--- a/manifests/quick-start/mysql/mysql-deployment.yaml
+++ b/manifests/quick-start/mysql/mysql-deployment.yaml
@@ -27,7 +27,7 @@ spec:
             - name: MYSQL_RANDOM_ROOT_PASSWORD
               value: "yes"
           ports:
-            - containerPort: 5432
+            - containerPort: 3306
           readinessProbe:
             exec:
               command: ["mysql", "-u", "mysql", "-ppassword", "argo", "-e", "SELECT 1"]
diff --git a/pkg/apis/workflow/v1alpha1/workflow_types.go b/pkg/apis/workflow/v1alpha1/workflow_types.go
index fc63dc354..903041373 100644
--- a/pkg/apis/workflow/v1alpha1/workflow_types.go
+++ b/pkg/apis/workflow/v1alpha1/workflow_types.go
@@ -1835,6 +1835,13 @@ func (wf *Workflow) GetTemplateByName(name string) *Template {
 			return &t
 		}
 	}
+	if wf.Status.StoredWorkflowSpec != nil {
+		for _, t := range wf.Status.StoredWorkflowSpec.Templates {
+			if t.Name == name {
+				return &t
+			}
+		}
+	}
 	return nil
 }
 
diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx
index 47b3f6066..bc98da33e 100644
--- a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx
+++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx
@@ -14,6 +14,7 @@ import {ClusterWorkflowTemplateSummaryPanel} from '../cluster-workflow-template-
 require('../../../workflows/components/workflow-details/workflow-details.scss');
 
 interface State {
+    namespace?: string;
     template?: models.ClusterWorkflowTemplate;
     error?: Error;
 }
@@ -37,11 +38,13 @@ export class ClusterWorkflowTemplateDetails extends BasePage<RouteComponentProps
     }
 
     public componentDidMount(): void {
+        services.info
+            .getInfo()
+            .then(info => this.setState({namespace: info.managedNamespace || 'default'}))
+            .catch(error => this.setState(error));
         services.clusterWorkflowTemplate
             .get(this.name)
-            .then(template => {
-                this.setState({template});
-            })
+            .then(template => this.setState({template}))
             .catch(error => this.setState({error}));
     }
 
@@ -127,10 +130,10 @@ export class ClusterWorkflowTemplateDetails extends BasePage<RouteComponentProps
         return {
             metadata: {
                 generateName: template.metadata.name + '-',
-                namespace: '<enter the namespace>'
+                namespace: this.state.namespace
             },
             spec: {
-                entrypoint: template.spec.templates[0].name,
+                entrypoint: !!template.spec.templates ? template.spec.templates[0].name : '',
                 workflowTemplateRef: {
                     name: template.metadata.name,
                     clusterScope: true
diff --git a/ui/src/app/shared/duration.ts b/ui/src/app/shared/duration.ts
index 49ce544f0..94c7192da 100644
--- a/ui/src/app/shared/duration.ts
+++ b/ui/src/app/shared/duration.ts
@@ -3,27 +3,48 @@ import * as models from '../../models';
 /**
  * Format the given number number of seconds in the form _d_h_m_s.
  * @param seconds Number of seconds to format. Will be rounded to the nearest whole number.
+ * @param sigfigs Level of significant figures to show
  */
-export function formatDuration(seconds: number) {
+
+export function formatDuration(seconds: number, sigfigs?: number) {
     let remainingSeconds = Math.round(seconds);
     let formattedDuration = '';
+    const figs = [];
 
     if (remainingSeconds > 86400) {
-        formattedDuration += Math.floor(remainingSeconds / 86400) + 'd';
+        const days = Math.floor(remainingSeconds / 86400) + 'd';
+        figs.push(days);
+        formattedDuration += days;
         remainingSeconds = remainingSeconds % 86400;
     }
+
     if (remainingSeconds > 3600) {
-        formattedDuration += Math.floor(remainingSeconds / 3600) + 'h';
+        const hours = Math.floor(remainingSeconds / 3600) + 'h';
+        figs.push(hours);
+        formattedDuration += hours;
         remainingSeconds = remainingSeconds % 3600;
     }
+
     if (remainingSeconds > 60) {
-        formattedDuration += Math.floor(remainingSeconds / 60) + 'm';
+        const minutes = Math.floor(remainingSeconds / 60) + 'm';
+        figs.push(minutes);
+        formattedDuration += minutes;
         remainingSeconds = remainingSeconds % 60;
     }
+
     if (remainingSeconds > 0 || Math.round(seconds) === 0) {
+        figs.push(remainingSeconds + 's');
         formattedDuration += remainingSeconds + 's';
     }
 
+    if (sigfigs && sigfigs <= figs.length) {
+        formattedDuration = '';
+        for (let i = 0; i < sigfigs; i++) {
+            formattedDuration += figs[i];
+        }
+        return formattedDuration;
+    }
+
     return formattedDuration;
 }
 
diff --git a/ui/src/app/shared/resources-duration.tsx b/ui/src/app/shared/resources-duration.tsx
index dbe753e18..5959dd63c 100644
--- a/ui/src/app/shared/resources-duration.tsx
+++ b/ui/src/app/shared/resources-duration.tsx
@@ -11,7 +11,7 @@ export class ResourcesDuration extends React.Component<Props> {
             <>
                 {this.props.resourcesDuration &&
                     Object.entries(this.props.resourcesDuration)
-                        .map(([resource, duration]) => formatDuration(duration) + '*(' + denominator(resource) + ' ' + resource + ')')
+                        .map(([resource, duration]) => formatDuration(duration, 1) + '*(' + denominator(resource) + ' ' + resource + ')')
                         .join(',')}{' '}
                 <a href='https://github.com/argoproj/argo/blob/master/docs/resource-duration.md'>
                     <i className='fa fa-info-circle' />
diff --git a/ui/src/app/workflow-templates/components/workflow-template-details/workflow-template-details.tsx b/ui/src/app/workflow-templates/components/workflow-template-details/workflow-template-details.tsx
index 6353881a5..68028459c 100644
--- a/ui/src/app/workflow-templates/components/workflow-template-details/workflow-template-details.tsx
+++ b/ui/src/app/workflow-templates/components/workflow-template-details/workflow-template-details.tsx
@@ -43,9 +43,7 @@ export class WorkflowTemplateDetails extends BasePage<RouteComponentProps<any>,
     public componentDidMount(): void {
         services.workflowTemplate
             .get(this.name, this.namespace)
-            .then(template => {
-                this.setState({template});
-            })
+            .then(template => this.setState({template}))
             .catch(error => this.setState({error}));
     }
 
@@ -134,7 +132,7 @@ export class WorkflowTemplateDetails extends BasePage<RouteComponentProps<any>,
                 namespace: template.metadata.namespace
             },
             spec: {
-                entrypoint: template.spec.templates[0].name,
+                entrypoint: !!template.spec.templates ? template.spec.templates[0].name : '',
                 workflowTemplateRef: {
                     name: template.metadata.name
                 }
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 95dfbcc22..864a2b073 100644
--- a/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx
+++ b/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx
@@ -3,6 +3,7 @@ import {Workflow} from '../../../../models';
 
 import {Loading} from '../../../shared/components/loading';
 import {ConditionsPanel} from '../../../shared/conditions-panel';
+import {formatDuration} from '../../../shared/duration';
 import {services} from '../../../shared/services';
 import {WorkflowLabels} from '../workflow-labels/workflow-labels';
 
@@ -62,11 +63,11 @@ export class WorkflowDrawer extends React.Component<WorkflowDrawerProps, Workflo
                             </div>
                             <div className='workflow-drawer__resourcesDuration--container'>
                                 <div>
-                                    <span className='workflow-drawer__resourcesDuration--value'>{wf.status.resourcesDuration.cpu} sec</span>
+                                    <span className='workflow-drawer__resourcesDuration--value'>{formatDuration(wf.status.resourcesDuration.cpu, 1)}</span>
                                     <span className='workflow-drawer__resourcesDuration--label'>(*1 CPU)</span>
                                 </div>
                                 <div>
-                                    <span className='workflow-drawer__resourcesDuration--value'>{wf.status.resourcesDuration.memory} sec</span>
+                                    <span className='workflow-drawer__resourcesDuration--value'>{formatDuration(wf.status.resourcesDuration.memory, 1)}</span>
                                     <span className='workflow-drawer__resourcesDuration--label'>(*100Mi Memory)</span>
                                 </div>
                             </div>
diff --git a/workflow/common/common.go b/workflow/common/common.go
index f98076b24..eeddf0955 100644
--- a/workflow/common/common.go
+++ b/workflow/common/common.go
@@ -57,6 +57,8 @@ const (
 	LabelKeyWorkflow = workflow.WorkflowFullName + "/workflow"
 	// LabelKeyPhase is a label applied to workflows to indicate the current phase of the workflow (for filtering purposes)
 	LabelKeyPhase = workflow.WorkflowFullName + "/phase"
+	// LabelKeyPreviousWorkflowName is a label applied to resubmitted workflows
+	LabelKeyPreviousWorkflowName = workflow.WorkflowFullName + "/resubmitted-from-workflow"
 	// LabelKeyCronWorkflow is a label applied to Workflows that are started by a CronWorkflow
 	LabelKeyCronWorkflow = workflow.WorkflowFullName + "/cron-workflow"
 	// LabelKeyWorkflowTemplate is a label applied to Workflows that are submitted from Workflowtemplate
diff --git a/workflow/controller/config.go b/workflow/controller/config.go
index 02e6b2682..14d756d04 100644
--- a/workflow/controller/config.go
+++ b/workflow/controller/config.go
@@ -5,6 +5,7 @@ import (
 
 	log "github.com/sirupsen/logrus"
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"sigs.k8s.io/yaml"
 
 	"github.com/argoproj/argo/config"
@@ -33,6 +34,7 @@ func (wfc *WorkflowController) updateConfig(config config.Config) error {
 	wfc.session = nil
 	wfc.offloadNodeStatusRepo = sqldb.ExplosiveOffloadNodeStatusRepo
 	wfc.wfArchive = sqldb.NullWorkflowArchive
+	wfc.archiveLabelSelector = labels.Everything()
 	persistence := wfc.Config.Persistence
 	if persistence != nil {
 		log.Info("Persistence configuration enabled")
@@ -58,6 +60,11 @@ func (wfc *WorkflowController) updateConfig(config config.Config) error {
 		}
 		if persistence.Archive {
 			instanceIDService := instanceid.NewService(wfc.Config.InstanceID)
+
+			wfc.archiveLabelSelector, err = persistence.GetArchiveLabelSelector()
+			if err != nil {
+				return err
+			}
 			wfc.wfArchive = sqldb.NewWorkflowArchive(session, persistence.GetClusterName(), wfc.managedNamespace, instanceIDService)
 			log.Info("Workflow archiving is enabled")
 		} else {
@@ -88,4 +95,4 @@ func (wfc *WorkflowController) executorImagePullPolicy() apiv1.PullPolicy {
 	} else {
 		return apiv1.PullPolicy(wfc.Config.ExecutorImagePullPolicy)
 	}
-}
\ No newline at end of file
+}
diff --git a/workflow/controller/controller.go b/workflow/controller/controller.go
index e6d49b67d..184dd5411 100644
--- a/workflow/controller/controller.go
+++ b/workflow/controller/controller.go
@@ -85,6 +85,7 @@ type WorkflowController struct {
 	wfArchive             sqldb.WorkflowArchive
 	metrics               metrics.Metrics
 	eventRecorder         record.EventRecorder
+	archiveLabelSelector  labels.Selector
 }
 
 const (
@@ -95,17 +96,7 @@ const (
 )
 
 // NewWorkflowController instantiates a new WorkflowController
-func NewWorkflowController(
-	restConfig *rest.Config,
-	kubeclientset kubernetes.Interface,
-	wfclientset wfclientset.Interface,
-	namespace string,
-	managedNamespace string,
-	executorImage,
-	executorImagePullPolicy,
-	containerRuntimeExecutor,
-	configMap string,
-) *WorkflowController {
+func NewWorkflowController(restConfig *rest.Config, kubeclientset kubernetes.Interface, wfclientset wfclientset.Interface, namespace string, managedNamespace string, executorImage, executorImagePullPolicy, containerRuntimeExecutor, configMap string) *WorkflowController {
 	eventBroadcaster := record.NewBroadcaster()
 	eventBroadcaster.StartLogging(log.Debugf)
 	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events(namespace)})
@@ -765,3 +756,8 @@ func (wfc *WorkflowController) getMetricsServerConfig() (metrics.ServerConfig, m
 
 	return metricsConfig, telemetryConfig
 }
+
+func (wfc *WorkflowController) isArchivable(wf *wfv1.Workflow) bool {
+	return wfc.archiveLabelSelector.Matches(labels.Set(wf.Labels))
+
+}
diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go
index b0f22e1ea..37b5babc2 100644
--- a/workflow/controller/operator.go
+++ b/workflow/controller/operator.go
@@ -158,13 +158,13 @@ func (woc *wfOperationCtx) operate() {
 	}()
 	defer func() {
 		if r := recover(); r != nil {
+			woc.log.WithFields(log.Fields{"stack": debug.Stack(), "r": r}).Errorf("Recovered from panic")
 			if rerr, ok := r.(error); ok {
 				woc.markWorkflowError(rerr, true)
 			} else {
 				woc.markWorkflowPhase(wfv1.NodeError, true, fmt.Sprintf("%v", r))
 			}
 			woc.controller.metrics.OperationPanic()
-			woc.log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
 		}
 	}()
 
@@ -173,7 +173,7 @@ func (woc *wfOperationCtx) operate() {
 	// Load the WorkflowSpec for execution
 	execTmplRef, execArgs, err := woc.loadExecutionSpec()
 	if err != nil {
-		woc.log.Errorf("Unable to get Workflow Template Reference for workflow, %s error: %s", woc.wf.Name, err)
+		woc.log.WithError(err).Errorf("Unable to get Workflow Template Reference for workflow")
 		woc.markWorkflowError(err, true)
 		return
 	}
@@ -248,8 +248,7 @@ func (woc *wfOperationCtx) operate() {
 	// Create a starting template context.
 	tmplCtx, err := woc.createTemplateContext(wfv1.ResourceScopeLocal, "")
 	if err != nil {
-		msg := fmt.Sprintf("Failed to create a template context: %+v", err)
-		woc.log.Errorf(msg)
+		woc.log.WithError(err).Error("Failed to create a template context")
 		woc.markWorkflowError(err, true)
 		return
 	}
@@ -342,7 +341,7 @@ func (woc *wfOperationCtx) operate() {
 		onExitNode, err = woc.executeTemplate(onExitNodeName, &wfv1.WorkflowStep{Template: woc.wfSpec.OnExit}, tmplCtx, woc.wfSpec.Arguments, &executeTemplateOpts{onExitTemplate: true})
 		if err != nil {
 			// the error are handled in the callee so just log it.
-			woc.log.Errorf("error in exit template execution: %+v", err)
+			woc.log.WithError(err).Error("error in exit template execution")
 			return
 		}
 		if onExitNode == nil || !onExitNode.Fulfilled() {
@@ -831,6 +830,9 @@ func (woc *wfOperationCtx) podReconciliation() error {
 
 			node.Message = "pod deleted"
 			node.Phase = wfv1.NodeError
+			// FinishedAt must be set since retry strategy depends on it to determine the backoff duration.
+			// See processNodeRetries for more details.
+			node.FinishedAt = metav1.Time{Time: time.Now().UTC()}
 			woc.wf.Status.Nodes[nodeID] = node
 			woc.log.Warnf("pod %s deleted", nodeID)
 			woc.updated = true
@@ -1600,9 +1602,14 @@ func (woc *wfOperationCtx) markWorkflowPhase(phase wfv1.NodePhase, markCompleted
 				woc.updated = true
 				woc.wf.Status.Message = err.Error()
 			}
-			err = woc.controller.wfArchive.ArchiveWorkflow(woc.wf)
-			if err != nil {
-				woc.log.WithField("err", err).Error("Failed to archive workflow")
+
+			if woc.controller.isArchivable(woc.wf) {
+				err = woc.controller.wfArchive.ArchiveWorkflow(woc.wf)
+				if err != nil {
+					woc.log.WithField("err", err).Error("Failed to archive workflow")
+				}
+			} else {
+				woc.log.Infof("Does't match with archive label selector. Skipping Archive")
 			}
 			woc.updated = true
 		}
@@ -1710,7 +1717,7 @@ func (woc *wfOperationCtx) initializeNode(nodeName string, nodeType wfv1.NodeTyp
 func (woc *wfOperationCtx) markNodePhase(nodeName string, phase wfv1.NodePhase, message ...string) *wfv1.NodeStatus {
 	node := woc.getNodeByName(nodeName)
 	if node == nil {
-		panic(fmt.Sprintf("node %s uninitialized", nodeName))
+		panic(fmt.Sprintf("workflow '%s' node '%s' uninitialized when marking as %v: %s", woc.wf.Name, nodeName, phase, message))
 	}
 	if node.Phase != phase {
 		woc.log.Infof("node %s phase %s -> %s", node.ID, node.Phase, phase)
@@ -2692,20 +2699,22 @@ func (woc *wfOperationCtx) getArtifactRepositoryByRef(arRef *wfv1.ArtifactReposi
 }
 
 func (woc *wfOperationCtx) fetchWorkflowSpec() (*wfv1.WorkflowSpec, error) {
+	if woc.wf.Spec.WorkflowTemplateRef == nil {
+		return nil, fmt.Errorf("cannot fetch workflow spec without workflowTemplateRef")
+	}
+
 	var specHolder wfv1.WorkflowSpecHolder
 	var err error
 	// Logic for workflow refers Workflow template
-	if woc.wf.Spec.WorkflowTemplateRef != nil {
-		if woc.wf.Spec.WorkflowTemplateRef.ClusterScope {
-			specHolder, err = woc.controller.cwftmplInformer.Lister().Get(woc.wf.Spec.WorkflowTemplateRef.Name)
-		} else {
-			specHolder, err = woc.controller.wftmplInformer.Lister().WorkflowTemplates(woc.wf.Namespace).Get(woc.wf.Spec.WorkflowTemplateRef.Name)
-		}
-		if err != nil {
-			return nil, err
-		}
+	if woc.wf.Spec.WorkflowTemplateRef.ClusterScope {
+		specHolder, err = woc.controller.cwftmplInformer.Lister().Get(woc.wf.Spec.WorkflowTemplateRef.Name)
+	} else {
+		specHolder, err = woc.controller.wftmplInformer.Lister().WorkflowTemplates(woc.wf.Namespace).Get(woc.wf.Spec.WorkflowTemplateRef.Name)
+	}
+	if err != nil {
+		return nil, err
 	}
-	return specHolder.GetWorkflowSpec().DeepCopy(), nil
+	return specHolder.GetWorkflowSpec(), nil
 }
 
 func (woc *wfOperationCtx) loadExecutionSpec() (wfv1.TemplateReferenceHolder, wfv1.Arguments, error) {
@@ -2713,17 +2722,30 @@ func (woc *wfOperationCtx) loadExecutionSpec() (wfv1.TemplateReferenceHolder, wf
 	executionParameters := woc.wf.Spec.Arguments
 
 	if woc.wf.Spec.WorkflowTemplateRef == nil {
-		tmplRef := &wfv1.WorkflowStep{Template: woc.wfSpec.Entrypoint}
+		if woc.controller.Config.WorkflowRestrictions.MustUseReference() {
+			return nil, executionParameters, fmt.Errorf("workflows must use workflowTemplateRef to be executed when the controller is in reference mode")
+		}
+
+		tmplRef := &wfv1.WorkflowStep{Template: woc.wf.Spec.Entrypoint}
 		return tmplRef, executionParameters, nil
 	}
 
 	if woc.wf.Status.StoredWorkflowSpec == nil {
 		wftSpec, err := woc.fetchWorkflowSpec()
 		if err != nil {
-			return nil, wfv1.Arguments{}, err
+			return nil, executionParameters, err
 		}
 		woc.wf.Status.StoredWorkflowSpec = wftSpec
 		woc.updated = true
+	} else if woc.controller.Config.WorkflowRestrictions.MustNotChangeSpec() {
+		// If the controller is in reference mode, ensure that the stored spec is identical to the reference spec at every operation
+		wftSpec, err := woc.fetchWorkflowSpec()
+		if err != nil {
+			return nil, executionParameters, err
+		}
+		if woc.wf.Status.StoredWorkflowSpec.String() != wftSpec.String() {
+			return nil, executionParameters, fmt.Errorf("workflowTemplateRef reference may not change during execution when the controller is in reference mode")
+		}
 	}
 
 	woc.wfSpec = woc.wf.Status.StoredWorkflowSpec
diff --git a/workflow/executor/resource.go b/workflow/executor/resource.go
index 515a91ea2..9e64c7765 100644
--- a/workflow/executor/resource.go
+++ b/workflow/executor/resource.go
@@ -330,7 +330,7 @@ func (we *WorkflowExecutor) SaveResourceParameters(resourceNamespace string, res
 			if resourceNamespace != "" {
 				resArgs = append(resArgs, "-n", resourceNamespace)
 			}
-			cmdStr := fmt.Sprintf("kubectl get %s -o json | jq -c '%s'", strings.Join(resArgs, " "), param.ValueFrom.JQFilter)
+			cmdStr := fmt.Sprintf("kubectl get %s -o json | jq -rc '%s'", strings.Join(resArgs, " "), param.ValueFrom.JQFilter)
 			cmd = exec.Command("sh", "-c", cmdStr)
 		} else {
 			continue
diff --git a/workflow/util/util.go b/workflow/util/util.go
index 8abddbbfe..a405b8e52 100644
--- a/workflow/util/util.go
+++ b/workflow/util/util.go
@@ -493,19 +493,22 @@ func FormulateResubmitWorkflow(wf *wfv1.Workflow, memoized bool) (*wfv1.Workflow
 
 	// carry over user labels and annotations from previous workflow.
 	// skip any argoproj.io labels except for the controller instanceID label.
+	if newWF.ObjectMeta.Labels == nil {
+		newWF.ObjectMeta.Labels = make(map[string]string)
+	}
 	for key, val := range wf.ObjectMeta.Labels {
 		if strings.HasPrefix(key, workflow.WorkflowFullName+"/") && key != common.LabelKeyControllerInstanceID {
 			continue
 		}
-		if newWF.ObjectMeta.Labels == nil {
-			newWF.ObjectMeta.Labels = make(map[string]string)
-		}
 		newWF.ObjectMeta.Labels[key] = val
 	}
+	// Append an additional label so it's easy for user to see the
+	// name of the original workflow that has been resubmitted.
+	newWF.ObjectMeta.Labels[common.LabelKeyPreviousWorkflowName] = wf.ObjectMeta.Name
+	if newWF.ObjectMeta.Annotations == nil {
+		newWF.ObjectMeta.Annotations = make(map[string]string)
+	}
 	for key, val := range wf.ObjectMeta.Annotations {
-		if newWF.ObjectMeta.Annotations == nil {
-			newWF.ObjectMeta.Annotations = make(map[string]string)
-		}
 		newWF.ObjectMeta.Annotations[key] = val
 	}
 


Test patch:
diff --git a/test/e2e/functional/output-jqfilter-parameters.yaml b/test/e2e/functional/output-jqfilter-parameters.yaml
new file mode 100644
index 000000000..8dc0226aa
--- /dev/null
+++ b/test/e2e/functional/output-jqfilter-parameters.yaml
@@ -0,0 +1,103 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  generateName: output-jqfilter-parameters-
+spec:
+  entrypoint: main
+  templates:
+
+  # main template that:
+  #   * create a configmap with json payload
+  #   * extract data using jqFilter from it
+  #   * compare the output parameters are what we expect
+  - name: main
+    steps:
+    - - name: create-param
+        template: create-param
+
+    - - name: get-param
+        template: get-param
+
+    - - name: check-param-one-value
+        template: check-param
+        arguments:
+          parameters:
+          - name: key
+            value: "{{steps.get-param.outputs.parameters.one_value}}"
+          - name: value
+            value: "foo"
+      - name: check-param-one-dict
+        template: check-param
+        arguments:
+          parameters:
+          - name: key
+            value: "{{steps.get-param.outputs.parameters.one_dict}}"
+          - name: value
+            value: |
+              {"foo":"bar"}
+      - name: check-param-one-list
+        template: check-param
+        arguments:
+          parameters:
+          - name: key
+            value: "{{steps.get-param.outputs.parameters.one_list}}"
+          - name: value
+            value: |
+              [{"foo":"bar"}]
+
+  # check that key and value parameters are aligned
+  - name: check-param
+    inputs:
+      parameters:
+      - name: key
+      - name: value
+    container:
+      image: argoproj/argoexec:latest
+      command: [sh, -c]
+      args: ["
+        echo '<{{inputs.parameters.key}}>' > /tmp/input;
+        echo '<{{inputs.parameters.value}}>' > /tmp/expected;
+        diff /tmp/input /tmp/expected;
+      "]
+
+  # extract parameters from a configmap json payload using jqFilter
+  - name: get-param
+    resource:
+      action: get
+      manifest: |
+        apiVersion: v1
+        kind: ConfigMap
+        metadata:
+          name: cm-output-jqfilter-parameters
+    outputs:
+      parameters:
+      - name: one_value
+        valueFrom:
+          jqFilter: '.data.some | fromjson | .one_value'
+      - name: one_dict
+        valueFrom:
+          jqfilter: '.data.some | fromjson | .one_dict'
+      - name: one_list
+        valueFrom:
+          jqfilter: '.data.some | fromjson | .one_list'
+
+  # create a configmap with json payload
+  - name: create-param
+    resource:
+      action: create
+      manifest: |
+        apiVersion: v1
+        kind: ConfigMap
+        metadata:
+          name: cm-output-jqfilter-parameters
+        data:
+          some: |
+            {
+              "one_value": "foo",
+              "one_dict": {"foo":"bar"},
+              "one_list": [
+                {
+                  "foo":"bar"
+                }
+              ]
+            }
diff --git a/test/e2e/functional_test.go b/test/e2e/functional_test.go
index a68a3c91e..7c5bcdcd4 100644
--- a/test/e2e/functional_test.go
+++ b/test/e2e/functional_test.go
@@ -582,6 +582,23 @@ func (s *FunctionalSuite) TestOptionalInputArtifacts() {
 		})
 }
 
+func (s *FunctionalSuite) TestWorkflowTemplateRefWithExitHandler() {
+	s.Given().
+		WorkflowTemplate("@smoke/workflow-template-whalesay-template.yaml").
+		When().
+		CreateWorkflowTemplates()
+	s.Given().
+		Workflow("@testdata/workflow-template-ref-exithandler.yaml").
+		When().
+		SubmitWorkflow().
+		WaitForWorkflow(30 * time.Second).
+		Then().
+		ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
+			assert.Equal(t, wfv1.NodeSucceeded, status.Phase)
+		})
+
+}
+
 func TestFunctionalSuite(t *testing.T) {
 	suite.Run(t, new(FunctionalSuite))
 }
diff --git a/test/e2e/smoke/workflow-template-whalesay-template.yaml b/test/e2e/smoke/workflow-template-whalesay-template.yaml
index 704e79bad..fb810ae81 100644
--- a/test/e2e/smoke/workflow-template-whalesay-template.yaml
+++ b/test/e2e/smoke/workflow-template-whalesay-template.yaml
@@ -6,6 +6,7 @@ metadata:
     argo-e2e: true
 spec:
   entrypoint: whalesay-template
+  onExit: on-exit
   arguments:
     parameters:
       - name: message
@@ -19,3 +20,8 @@ spec:
         image: argoproj/argosay:v2
         args: ["echo", "{{inputs.parameters.message}}"]
         imagePullPolicy: IfNotPresent
+    - name: on-exit
+      container:
+        image: argoproj/argosay:v2
+        args: ["echo", "Exit Handler"]
+        imagePullPolicy: IfNotPresent
\ No newline at end of file
diff --git a/test/e2e/testdata/workflow-template-ref-exithandler.yaml b/test/e2e/testdata/workflow-template-ref-exithandler.yaml
new file mode 100644
index 000000000..4f717bc83
--- /dev/null
+++ b/test/e2e/testdata/workflow-template-ref-exithandler.yaml
@@ -0,0 +1,14 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  generateName: workflow-template-hello-world-
+  labels:
+    argo-e2e: "true"
+spec:
+  entrypoint: whalesay-template
+  arguments:
+    parameters:
+      - name: message
+        value: "hello world"
+  workflowTemplateRef:
+    name: workflow-template-whalesay-template
diff --git a/test/test.go b/test/test.go
index df2634076..836c61743 100644
--- a/test/test.go
+++ b/test/test.go
@@ -51,6 +51,25 @@ func LoadWorkflowFromBytes(yamlBytes []byte) *wfv1.Workflow {
 	return &wf
 }
 
+// LoadTestWorkflow returns a workflow relative to the test file
+func LoadTestWorkflowTemplate(path string) *wfv1.WorkflowTemplate {
+	yamlBytes, err := ioutil.ReadFile(path)
+	if err != nil {
+		panic(err)
+	}
+	return LoadWorkflowTemplateFromBytes(yamlBytes)
+}
+
+// LoadWorkflowFromBytes returns a workflow unmarshalled from an yaml byte array
+func LoadWorkflowTemplateFromBytes(yamlBytes []byte) *wfv1.WorkflowTemplate {
+	var wf wfv1.WorkflowTemplate
+	err := yaml.Unmarshal(yamlBytes, &wf)
+	if err != nil {
+		panic(err)
+	}
+	return &wf
+}
+
 // LoadUnstructuredFromBytes returns an Unstructured unmarshalled from an yaml byte array
 func LoadUnstructuredFromBytes(yamlBytes []byte) *unstructured.Unstructured {
 	var un unstructured.Unstructured
diff --git a/workflow/controller/controller_test.go b/workflow/controller/controller_test.go
index 185f7eedc..b85bc1405 100644
--- a/workflow/controller/controller_test.go
+++ b/workflow/controller/controller_test.go
@@ -5,6 +5,8 @@ import (
 	"testing"
 	"time"
 
+	"k8s.io/apimachinery/pkg/labels"
+
 	"github.com/stretchr/testify/assert"
 	authorizationv1 "k8s.io/api/authorization/v1"
 	apiv1 "k8s.io/api/core/v1"
@@ -120,18 +122,19 @@ func newController(objects ...runtime.Object) (context.CancelFunc, *WorkflowCont
 		Config: config.Config{
 			ExecutorImage: "executor:latest",
 		},
-		kubeclientset:   kube,
-		wfclientset:     wfclientset,
-		completedPods:   make(chan string, 16),
-		wfInformer:      wfInformer,
-		wftmplInformer:  wftmplInformer,
-		cwftmplInformer: cwftmplInformer,
-		wfQueue:         workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
-		podQueue:        workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
-		wfArchive:       sqldb.NullWorkflowArchive,
-		hydrator:        hydratorfake.Noop,
-		metrics:         metrics.New(metrics.ServerConfig{}, metrics.ServerConfig{}),
-		eventRecorder:   record.NewFakeRecorder(16),
+		kubeclientset:        kube,
+		wfclientset:          wfclientset,
+		completedPods:        make(chan string, 16),
+		wfInformer:           wfInformer,
+		wftmplInformer:       wftmplInformer,
+		cwftmplInformer:      cwftmplInformer,
+		wfQueue:              workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		podQueue:             workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		wfArchive:            sqldb.NullWorkflowArchive,
+		hydrator:             hydratorfake.Noop,
+		metrics:              metrics.New(metrics.ServerConfig{}, metrics.ServerConfig{}),
+		eventRecorder:        record.NewFakeRecorder(16),
+		archiveLabelSelector: labels.Everything(),
 	}
 	return cancel, controller
 }
@@ -367,18 +370,38 @@ spec:
 `
 
 func TestCheckAndInitWorkflowTmplRef(t *testing.T) {
-	//_, controller := newController()
 	wf := unmarshalWF(wfWithTmplRef)
 	wftmpl := unmarshalWFTmpl(wfTmpl)
 	_, controller := newController(wf, wftmpl)
-	//_, err := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("default").Create(wftmpl)
-	//assert.NoError(t, err)
 	woc := wfOperationCtx{controller: controller,
 		wf: wf}
-	t.Run("WithWorkflowTmplRef", func(t *testing.T) {
-		_, _, err := woc.loadExecutionSpec()
-		assert.NoError(t, err)
-		assert.Equal(t, &wftmpl.Spec.WorkflowSpec, woc.wfSpec)
+	_, _, err := woc.loadExecutionSpec()
+	assert.NoError(t, err)
+	assert.Equal(t, &wftmpl.Spec.WorkflowSpec, woc.wfSpec)
+}
 
+func TestIsArchivable(t *testing.T) {
+	_, controller := newController()
+	var lblSelector metav1.LabelSelector
+	lblSelector.MatchLabels = make(map[string]string)
+	lblSelector.MatchLabels["workflows.argoproj.io/archive-strategy"] = "true"
+
+	workflow := unmarshalWF(helloWorldWf)
+	t.Run("EverythingSelector", func(t *testing.T) {
+		controller.archiveLabelSelector = labels.Everything()
+		assert.True(t, controller.isArchivable(workflow))
+	})
+	t.Run("NothingSelector", func(t *testing.T) {
+		controller.archiveLabelSelector = labels.Nothing()
+		assert.False(t, controller.isArchivable(workflow))
+	})
+	t.Run("ConfiguredSelector", func(t *testing.T) {
+		selector, err := metav1.LabelSelectorAsSelector(&lblSelector)
+		assert.NoError(t, err)
+		controller.archiveLabelSelector = selector
+		assert.False(t, controller.isArchivable(workflow))
+		workflow.Labels = make(map[string]string)
+		workflow.Labels["workflows.argoproj.io/archive-strategy"] = "true"
+		assert.True(t, controller.isArchivable(workflow))
 	})
 }
diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go
index f6a617710..4981189ff 100644
--- a/workflow/controller/operator_test.go
+++ b/workflow/controller/operator_test.go
@@ -3687,3 +3687,55 @@ func TestPanicMetric(t *testing.T) {
 	}
 	assert.True(t, seen)
 }
+
+// Assert Workflows cannot be run without using workflowTemplateRef in reference mode
+func TestControllerReferenceMode(t *testing.T) {
+	wf := unmarshalWF(globalVariablePlaceholders)
+	cancel, controller := newController()
+	defer cancel()
+	controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{}
+	controller.Config.WorkflowRestrictions.TemplateReferencing = config.TemplateReferencingStrict
+	woc := newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	assert.Equal(t, wfv1.NodeError, woc.wf.Status.Phase)
+	assert.Equal(t, "workflows must use workflowTemplateRef to be executed when the controller is in reference mode", woc.wf.Status.Message)
+
+	controller.Config.WorkflowRestrictions.TemplateReferencing = config.TemplateReferencingSecure
+	woc = newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	assert.Equal(t, wfv1.NodeError, woc.wf.Status.Phase)
+	assert.Equal(t, "workflows must use workflowTemplateRef to be executed when the controller is in reference mode", woc.wf.Status.Message)
+
+	controller.Config.WorkflowRestrictions = nil
+	woc = newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	assert.Equal(t, wfv1.NodeRunning, woc.wf.Status.Phase)
+}
+
+func TestValidReferenceMode(t *testing.T) {
+	wf := test.LoadTestWorkflow("testdata/workflow-template-ref.yaml")
+	wfTmpl := test.LoadTestWorkflowTemplate("testdata/workflow-template-submittable.yaml")
+	cancel, controller := newController(wf, wfTmpl)
+	defer cancel()
+	controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{}
+	controller.Config.WorkflowRestrictions.TemplateReferencing = config.TemplateReferencingSecure
+	woc := newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	assert.Equal(t, wfv1.NodeRunning, woc.wf.Status.Phase)
+
+	// Change stored Workflow Spec
+	woc.wf.Status.StoredWorkflowSpec.Entrypoint = "different"
+	woc.operate()
+	assert.Equal(t, wfv1.NodeError, woc.wf.Status.Phase)
+	assert.Equal(t, "workflowTemplateRef reference may not change during execution when the controller is in reference mode", woc.wf.Status.Message)
+
+	controller.Config.WorkflowRestrictions.TemplateReferencing = config.TemplateReferencingStrict
+	woc = newWorkflowOperationCtx(wf, controller)
+	woc.operate()
+	assert.Equal(t, wfv1.NodeRunning, woc.wf.Status.Phase)
+
+	// Change stored Workflow Spec
+	woc.wf.Status.StoredWorkflowSpec.Entrypoint = "different"
+	woc.operate()
+	assert.Equal(t, wfv1.NodeRunning, woc.wf.Status.Phase)
+}
diff --git a/workflow/controller/testdata/workflow-template-ref.yaml b/workflow/controller/testdata/workflow-template-ref.yaml
new file mode 100644
index 000000000..c08109716
--- /dev/null
+++ b/workflow/controller/testdata/workflow-template-ref.yaml
@@ -0,0 +1,8 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  generateName: workflow-template-hello-world-
+  namespace: test
+spec:
+  workflowTemplateRef:
+    name: workflow-template-submittable
diff --git a/workflow/controller/testdata/workflow-template-submittable.yaml b/workflow/controller/testdata/workflow-template-submittable.yaml
new file mode 100644
index 000000000..a6b786863
--- /dev/null
+++ b/workflow/controller/testdata/workflow-template-submittable.yaml
@@ -0,0 +1,13 @@
+apiVersion: argoproj.io/v1alpha1
+kind: WorkflowTemplate
+metadata:
+  name: workflow-template-submittable
+  namespace: test
+spec:
+  entrypoint: whalesay-template
+  templates:
+    - name: whalesay-template
+      container:
+        image: docker/whalesay
+        command: [cowsay]
+        args: ['hello']