Repository: argoproj-labs/applicationset
Pull request #198 (2021-04-12 20:36:12)

Race condition on cluster creation/deletion causes unexpected behaviour 
See details on parent issue.

The vast majority of this PR is test changes, the new files in `pkg/utils` are the only substantial changes to product code.

This PR:
- Forks a few of the `util/db/cluster.go` functions from Argo CD, and replaces their use of the race-condition-prone `ApplicationLister`, with a simple kubeclient query.
- Forks the test cases that test those functions, as well, to make sure nothing has regressed.
- As I state in the PR, ideally we would upstream these changes to Argo CD in some form, but in the mean time (and until Argo CD has adopted the fix in all the versions we support), this fixes the issue here.

Fixes https://github.com/argoproj-labs/applicationset/issues/176


Patch:
diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml
index cf9ea5c..8ba0303 100644
--- a/.github/workflows/ci-build.yaml
+++ b/.github/workflows/ci-build.yaml
@@ -44,10 +44,7 @@ jobs:
         uses: actions/checkout@v2
       - name: Regenerate the manifests, git diff should report no changes.
         run: |
-          # Use a fixed version of kustomize, rather than the version on the 'runs-on' image
-          wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.9.4/kustomize_v3.9.4_linux_amd64.tar.gz
-          tar xzf kustomize_v3.9.4_linux_amd64.tar.gz
-          KUSTOMIZE=${PWD}/kustomize make manifests
+          make manifests
           git diff --exit-code -- .
 
   build-go:
diff --git a/Makefile b/Makefile
index 639fc05..34d6085 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,8 @@ MKDOCS_RUN_ARGS?=
 
 CURRENT_DIR=$(shell pwd)
 
+KUSTOMIZE = $(shell pwd)/bin/kustomize
+
 
 ifdef IMAGE_NAMESPACE
 
@@ -47,15 +49,15 @@ image-push: image
 	docker push ${IMAGE_PREFIX}${IMAGE_NAME}:${IMAGE_TAG}
 
 .PHONY: deploy
-deploy: manifests
-	kustomize build manifests/namespace-install | kubectl apply -f -
+deploy: kustomize manifests
+	${KUSTOMIZE} build manifests/namespace-install | kubectl apply -f -
 	kubectl patch deployment -n argocd argocd-applicationset-controller --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "$(IMAGE)"}]'
 
 # Generate manifests e.g. CRD, RBAC etc.
 .PHONY: manifests
-manifests: generate
+manifests: kustomize generate
 	$(CONTROLLER_GEN) crd:crdVersions=v1 paths="./..." output:crd:artifacts:config=./manifests/crds/
-	hack/generate-manifests.sh
+	KUSTOMIZE=${KUSTOMIZE} hack/generate-manifests.sh
 
 .PHONY: lint
 lint:
@@ -86,23 +88,6 @@ test-e2e:
 generate: controller-gen
 	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
 
-# find or download controller-gen
-# download controller-gen if necessary
-controller-gen:
-ifeq (, $(shell which controller-gen))
-	@{ \
-	set -e ;\
-	CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
-	cd $$CONTROLLER_GEN_TMP_DIR ;\
-	go mod init tmp ;\
-	go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
-	rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
-	}
-CONTROLLER_GEN=$(GOBIN)/controller-gen
-else
-CONTROLLER_GEN=$(shell which controller-gen)
-endif
-
 .PHONY: build-docs-local
 build-docs-local:
 	mkdocs build
@@ -125,3 +110,37 @@ lint-docs:
 	find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat docs/assets/broken-link-ignore-list.txt | grep -v "#" | tr "\n" ','` --skip-save-results --
 
 
+
+# find or download controller-gen
+# download controller-gen if necessary
+controller-gen:
+ifeq (, $(shell which controller-gen))
+	@{ \
+	set -e ;\
+	CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
+	cd $$CONTROLLER_GEN_TMP_DIR ;\
+	go mod init tmp ;\
+	go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
+	rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
+	}
+CONTROLLER_GEN=$(GOBIN)/controller-gen
+else
+CONTROLLER_GEN=$(shell which controller-gen)
+endif
+
+kustomize: ## Download kustomize locally if necessary.
+	$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.9.4)
+
+# go-get-tool will 'go get' any package $2 and install it to $1.
+PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
+define go-get-tool
+@[ -f $(1) ] || { \
+set -e ;\
+TMP_DIR=$$(mktemp -d) ;\
+cd $$TMP_DIR ;\
+go mod init tmp ;\
+echo "Downloading $(2)" ;\
+GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
+rm -rf $$TMP_DIR ;\
+}
+endef
diff --git a/api/v1alpha1/applicationset_types.go b/api/v1alpha1/applicationset_types.go
index ed4ed1b..07ec147 100644
--- a/api/v1alpha1/applicationset_types.go
+++ b/api/v1alpha1/applicationset_types.go
@@ -23,6 +23,7 @@ import (
 
 // ApplicationSet is a set of Application resources
 // +kubebuilder:object:root=true
+// +kubebuilder:resource:path=applicationsets,shortName=appset;appsets
 type ApplicationSet struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata"`
@@ -103,7 +104,8 @@ type GitGenerator struct {
 }
 
 type GitDirectoryGeneratorItem struct {
-	Path string `json:"path"`
+	Path    string `json:"path"`
+	Exclude bool   `json:"exclude,omitempty"`
 }
 
 type GitFileGeneratorItem struct {
diff --git a/docs/Generators.md b/docs/Generators.md
index 876202b..7fe5002 100644
--- a/docs/Generators.md
+++ b/docs/Generators.md
@@ -209,6 +209,84 @@ Whenever a new Helm chart/Kustomize YAML/Application/plain subfolder is added to
 
 As with other generators, clusters *must* already be defined within Argo CD, in order to generate Applications for them.
 
+### Exclude directories
+
+The Git directory generator also supports an `exclude` option in order to exclude directories in the repository from being scanned by the ApplicationSet controller:
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: ApplicationSet
+metadata:
+  name: cluster-addons
+spec:
+  generators:
+  - git:
+      repoURL: https://github.com/argoproj-labs/applicationset.git
+      revision: HEAD
+      directories:
+      - path: examples/git-generator-directory/excludes/cluster-addons/*
+      - exclude: true
+        path: examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook
+  template:
+    metadata:
+      name: '{{path.basename}}'
+    spec:
+      project: default
+      source:
+        repoURL: https://github.com/argoproj-labs/applicationset.git
+        targetRevision: HEAD
+        path: '{{path}}'
+      destination:
+        server: https://kubernetes.default.svc
+        namespace: '{{path.basename}}'
+```
+(*The full example can be found [here](https://github.com/argoproj-labs/applicationset/tree/master/examples/git-generator-directory/excludes).*)
+
+This example excludes the `exclude-helm-guestbook` directory from the list of directories scanned for this `ApplictionSet` resource.
+
+!!! note "Exclude rules have higher priority than include rules"
+
+Every directory that matches at least one `exclude` pattern will always be excluded. Or, said another way, *exclude rules take precedence over include rules.*
+
+As a corollary, the order of `path`s in the `directories` field does not change which directories are included/excluded (because, as above, exclude rules always take precedence over include rules). 
+
+For example, with these directories:
+
+```
+.
+└── d
+    ├── e
+    ├── f
+    └── g
+```
+Say you want to include `/d/e`, but exclude `/d/f` and `/d/g`. This will *not* work:
+
+```yaml
+- path: /d/e
+  exclude: false
+- path: /d/*
+  exclude: true
+```
+Why? Because the exclude `/d/*` exclude rule will take precedence over the `/d/e` include rule. When the `/d/e` path in the Git repository is processed by the ApplicationSet controller, the controller detects that at least one exclude rule is matched, and thus that directory should not be scanned.
+
+You would instead need to do:
+
+```yaml
+- path: /d/*
+- path: /d/f
+  exclude: true
+- path: /d/g
+  exclude: true
+```
+
+Or, a shorter way (using [path.Match](https://golang.org/pkg/path/#Match) syntax) would be:
+
+```yaml
+- path: /d/*
+- path: /d/[f|g]
+  exclude: true
+```
+
 ## Git Generator: Files
 
 The Git file generator is the second subtype of the Git generator. The Git file generator generates parameters using the contents of JSON files found within a specified repository.
diff --git a/docs/index.md b/docs/index.md
index 23b842f..b932eb3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -24,7 +24,7 @@ metadata:
 spec:
   generators:
   - list:
-      items:
+      elements:
       - cluster: engineering-dev
         url: https://1.2.3.4
       - cluster: engineering-prod
diff --git a/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/kustomization.yaml b/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/kustomization.yaml
new file mode 100644
index 0000000..68cd552
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/kustomization.yaml
@@ -0,0 +1,6 @@
+#namePrefix: kustomize-
+
+resources:
+- namespace-install.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
diff --git a/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/namespace-install.yaml b/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/namespace-install.yaml
new file mode 100644
index 0000000..c140cd6
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/namespace-install.yaml
@@ -0,0 +1,417 @@
+# This is an auto-generated file. DO NOT EDIT
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: clusterworkflowtemplates.argoproj.io
+spec:
+  group: argoproj.io
+  names:
+    kind: ClusterWorkflowTemplate
+    listKind: ClusterWorkflowTemplateList
+    plural: clusterworkflowtemplates
+    shortNames:
+    - clusterwftmpl
+    - cwft
+    singular: clusterworkflowtemplate
+  scope: Cluster
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: cronworkflows.argoproj.io
+spec:
+  group: argoproj.io
+  names:
+    kind: CronWorkflow
+    listKind: CronWorkflowList
+    plural: cronworkflows
+    shortNames:
+    - cwf
+    - cronwf
+    singular: cronworkflow
+  scope: Namespaced
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: workfloweventbindings.argoproj.io
+spec:
+  group: argoproj.io
+  names:
+    kind: WorkflowEventBinding
+    listKind: WorkflowEventBindingList
+    plural: workfloweventbindings
+    shortNames:
+    - wfeb
+    singular: workfloweventbinding
+  scope: Namespaced
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: workflows.argoproj.io
+spec:
+  additionalPrinterColumns:
+  - JSONPath: .status.phase
+    description: Status of the workflow
+    name: Status
+    type: string
+  - JSONPath: .status.startedAt
+    description: When the workflow was started
+    format: date-time
+    name: Age
+    type: date
+  group: argoproj.io
+  names:
+    kind: Workflow
+    listKind: WorkflowList
+    plural: workflows
+    shortNames:
+    - wf
+    singular: workflow
+  scope: Namespaced
+  subresources: {}
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: workflowtemplates.argoproj.io
+spec:
+  group: argoproj.io
+  names:
+    kind: WorkflowTemplate
+    listKind: WorkflowTemplateList
+    plural: workflowtemplates
+    shortNames:
+    - wftmpl
+    singular: workflowtemplate
+  scope: Namespaced
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: argo
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: argo-server
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: argo-role
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  - pods/exec
+  verbs:
+  - create
+  - get
+  - list
+  - watch
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - watch
+  - list
+- apiGroups:
+  - ""
+  resources:
+  - persistentvolumeclaims
+  verbs:
+  - create
+  - delete
+  - get
+- apiGroups:
+  - argoproj.io
+  resources:
+  - workflows
+  - workflows/finalizers
+  verbs:
+  - get
+  - list
+  - watch
+  - update
+  - patch
+  - delete
+  - create
+- apiGroups:
+  - argoproj.io
+  resources:
+  - workflowtemplates
+  - workflowtemplates/finalizers
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - serviceaccounts
+  verbs:
+  - get
+  - list
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - get
+- apiGroups:
+  - argoproj.io
+  resources:
+  - cronworkflows
+  - cronworkflows/finalizers
+  verbs:
+  - get
+  - list
+  - watch
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - policy
+  resources:
+  - poddisruptionbudgets
+  verbs:
+  - create
+  - get
+  - delete
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: argo-server-role
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - watch
+  - list
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - get
+  - create
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  - pods/exec
+  - pods/log
+  verbs:
+  - get
+  - list
+  - watch
+  - delete
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - watch
+  - create
+  - patch
+- apiGroups:
+  - ""
+  resources:
+  - serviceaccounts
+  verbs:
+  - get
+  - list
+- apiGroups:
+  - argoproj.io
+  resources:
+  - workflows
+  - workfloweventbindings
+  - workflowtemplates
+  - cronworkflows
+  - cronworkflows/finalizers
+  verbs:
+  - create
+  - get
+  - list
+  - watch
+  - update
+  - patch
+  - delete
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: argo-binding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: argo-role
+subjects:
+- kind: ServiceAccount
+  name: argo
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: argo-server-binding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: argo-server-role
+subjects:
+- kind: ServiceAccount
+  name: argo-server
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: workflow-controller-configmap
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: argo-server
+spec:
+  ports:
+  - name: web
+    port: 2746
+    targetPort: 2746
+  selector:
+    app: argo-server
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: workflow-controller-metrics
+spec:
+  ports:
+  - name: metrics
+    port: 9090
+    protocol: TCP
+    targetPort: 9090
+  selector:
+    app: workflow-controller
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: argo-server
+spec:
+  selector:
+    matchLabels:
+      app: argo-server
+  template:
+    metadata:
+      labels:
+        app: argo-server
+    spec:
+      containers:
+      - args:
+        - server
+        - --namespaced
+        image: argoproj/argocli:v2.12.5
+        name: argo-server
+        ports:
+        - containerPort: 2746
+          name: web
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 2746
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 20
+        volumeMounts:
+        - mountPath: /tmp
+          name: tmp
+      nodeSelector:
+        kubernetes.io/os: linux
+      securityContext:
+        runAsNonRoot: true
+      serviceAccountName: argo-server
+      volumes:
+      - emptyDir: {}
+        name: tmp
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: workflow-controller
+spec:
+  selector:
+    matchLabels:
+      app: workflow-controller
+  template:
+    metadata:
+      labels:
+        app: workflow-controller
+    spec:
+      containers:
+      - args:
+        - --configmap
+        - workflow-controller-configmap
+        - --executor-image
+        - argoproj/argoexec:v2.12.5
+        - --namespaced
+        command:
+        - workflow-controller
+        image: argoproj/workflow-controller:v2.12.5
+        livenessProbe:
+          httpGet:
+            path: /metrics
+            port: metrics
+          initialDelaySeconds: 30
+          periodSeconds: 30
+        name: workflow-controller
+        ports:
+        - containerPort: 9090
+          name: metrics
+      nodeSelector:
+        kubernetes.io/os: linux
+      securityContext:
+        runAsNonRoot: true
+      serviceAccountName: argo
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/Chart.yaml b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/Chart.yaml
new file mode 100644
index 0000000..6fac831
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/Chart.yaml
@@ -0,0 +1,23 @@
+apiVersion: v2
+name: helm-guestbook
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+appVersion: "1.0"
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/NOTES.txt b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/NOTES.txt
new file mode 100644
index 0000000..37a1485
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/NOTES.txt
@@ -0,0 +1,19 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range .Values.ingress.hosts }}
+  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "helm-guestbook.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+  echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+           You can watch the status of by running 'kubectl get svc -w {{ template "helm-guestbook.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "helm-guestbook.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+  echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "helm-guestbook.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  echo "Visit http://127.0.0.1:8080 to use your application"
+  kubectl port-forward $POD_NAME 8080:80
+{{- end }}
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/_helpers.tpl b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/_helpers.tpl
new file mode 100644
index 0000000..20f5d8e
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/_helpers.tpl
@@ -0,0 +1,32 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "helm-guestbook.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "helm-guestbook.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "helm-guestbook.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/deployment.yaml b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/deployment.yaml
new file mode 100644
index 0000000..980df25
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/deployment.yaml
@@ -0,0 +1,52 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ template "helm-guestbook.fullname" . }}
+  labels:
+    app: {{ template "helm-guestbook.name" . }}
+    chart: {{ template "helm-guestbook.chart" . }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app: {{ template "helm-guestbook.name" . }}
+      release: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app: {{ template "helm-guestbook.name" . }}
+        release: {{ .Release.Name }}
+    spec:
+      containers:
+        - name: {{ .Chart.Name }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 80
+              protocol: TCP
+          livenessProbe:
+            httpGet:
+              path: /
+              port: http
+          readinessProbe:
+            httpGet:
+              path: /
+              port: http
+          resources:
+{{ toYaml .Values.resources | indent 12 }}
+    {{- with .Values.nodeSelector }}
+      nodeSelector:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.affinity }}
+      affinity:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.tolerations }}
+      tolerations:
+{{ toYaml . | indent 8 }}
+    {{- end }}
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/service.yaml b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/service.yaml
new file mode 100644
index 0000000..b7aab0b
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/service.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ template "helm-guestbook.fullname" . }}
+  labels:
+    app: {{ template "helm-guestbook.name" . }}
+    chart: {{ template "helm-guestbook.chart" . }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    app: {{ template "helm-guestbook.name" . }}
+    release: {{ .Release.Name }}
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values-production.yaml b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values-production.yaml
new file mode 100644
index 0000000..42838b7
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values-production.yaml
@@ -0,0 +1,2 @@
+service:
+  type: LoadBalancer
diff --git a/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values.yaml b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values.yaml
new file mode 100644
index 0000000..3666712
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values.yaml
@@ -0,0 +1,45 @@
+# Default values for helm-guestbook.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+replicaCount: 1
+
+image:
+  repository: gcr.io/heptio-images/ks-guestbook-demo
+  tag: 0.1
+  pullPolicy: IfNotPresent
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: false
+  annotations: {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  path: /
+  hosts:
+    - chart-example.local
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #  cpu: 100m
+  #  memory: 128Mi
+  # requests:
+  #  cpu: 100m
+  #  memory: 128Mi
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/Chart.yaml b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/Chart.yaml
new file mode 100644
index 0000000..10aa9d0
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/Chart.yaml
@@ -0,0 +1,1 @@
+name: helm-prometheus-operator
diff --git a/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/requirements.yaml b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/requirements.yaml
new file mode 100644
index 0000000..2e34cfc
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/requirements.yaml
@@ -0,0 +1,4 @@
+dependencies:
+- name: kube-prometheus-stack
+  version: 9.4.10
+  repository: https://prometheus-community.github.io/helm-charts
diff --git a/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/values.yaml b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/values.yaml
new file mode 100644
index 0000000..f09e904
--- /dev/null
+++ b/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/values.yaml
@@ -0,0 +1,1 @@
+# Blank values.yaml
diff --git a/examples/git-generator-directory/excludes/git-directories-exclude-example.yaml b/examples/git-generator-directory/excludes/git-directories-exclude-example.yaml
new file mode 100644
index 0000000..742ebf2
--- /dev/null
+++ b/examples/git-generator-directory/excludes/git-directories-exclude-example.yaml
@@ -0,0 +1,25 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ApplicationSet
+metadata:
+  name: cluster-addons
+spec:
+  generators:
+  - git:
+      repoURL: https://github.com/argoproj-labs/applicationset.git
+      revision: HEAD
+      directories:
+      - path: examples/git-generator-directory/excludes/cluster-addons/*
+      - exclude: true
+        path: examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook
+  template:
+    metadata:
+      name: '{{path.basename}}'
+    spec:
+      project: default
+      source:
+        repoURL: https://github.com/argoproj-labs/applicationset.git
+        targetRevision: HEAD
+        path: '{{path}}'
+      destination:
+        server: https://kubernetes.default.svc
+        namespace: '{{path.basename}}'
diff --git a/go.mod b/go.mod
index 4e9ff03..5854193 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible
+	k8s.io/utils v0.0.0-20200912215256-4140de9c8800
 	sigs.k8s.io/controller-runtime v0.7.0
 )
 
diff --git a/main.go b/main.go
index 6adda40..2781e6e 100644
--- a/main.go
+++ b/main.go
@@ -147,6 +147,7 @@ func main() {
 		Renderer:         &utils.Render{},
 		Policy:           policyObj,
 		ArgoAppClientset: appclientset,
+		KubeClientset:    k8s,
 		ArgoDB:           argoCDDB,
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "ApplicationSet")
diff --git a/manifests/crds/argoproj.io_applicationsets.yaml b/manifests/crds/argoproj.io_applicationsets.yaml
index e15bc0f..a540245 100644
--- a/manifests/crds/argoproj.io_applicationsets.yaml
+++ b/manifests/crds/argoproj.io_applicationsets.yaml
@@ -13,6 +13,9 @@ spec:
     kind: ApplicationSet
     listKind: ApplicationSetList
     plural: applicationsets
+    shortNames:
+    - appset
+    - appsets
     singular: applicationset
   scope: Namespaced
   versions:
@@ -499,6 +502,8 @@ spec:
                         directories:
                           items:
                             properties:
+                              exclude:
+                                type: boolean
                               path:
                                 type: string
                             required:
diff --git a/manifests/install-with-argo-cd.yaml b/manifests/install-with-argo-cd.yaml
index a5b8ea6..0a1f51e 100644
--- a/manifests/install-with-argo-cd.yaml
+++ b/manifests/install-with-argo-cd.yaml
@@ -1773,6 +1773,9 @@ spec:
     kind: ApplicationSet
     listKind: ApplicationSetList
     plural: applicationsets
+    shortNames:
+    - appset
+    - appsets
     singular: applicationset
   scope: Namespaced
   versions:
@@ -2155,6 +2158,8 @@ spec:
                         directories:
                           items:
                             properties:
+                              exclude:
+                                type: boolean
                               path:
                                 type: string
                             required:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index d88632b..f666d1a 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -12,6 +12,9 @@ spec:
     kind: ApplicationSet
     listKind: ApplicationSetList
     plural: applicationsets
+    shortNames:
+    - appset
+    - appsets
     singular: applicationset
   scope: Namespaced
   versions:
@@ -394,6 +397,8 @@ spec:
                         directories:
                           items:
                             properties:
+                              exclude:
+                                type: boolean
                               path:
                                 type: string
                             required:
diff --git a/pkg/controllers/applicationset_controller.go b/pkg/controllers/applicationset_controller.go
index 7c659f4..a612105 100644
--- a/pkg/controllers/applicationset_controller.go
+++ b/pkg/controllers/applicationset_controller.go
@@ -1,6 +1,4 @@
 /*
-
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
@@ -29,11 +27,11 @@ import (
 	"github.com/argoproj-labs/applicationset/pkg/utils"
 	"github.com/argoproj/argo-cd/common"
 	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
-	"github.com/argoproj/argo-cd/util/argo"
 	"github.com/argoproj/argo-cd/util/db"
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -53,6 +51,13 @@ import (
 	"github.com/imdario/mergo"
 )
 
+const (
+	// Rather than importing the whole argocd-notifications controller, just copying the const here
+	//   https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/subscriptions.go#L12
+	//   https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17
+	NotifiedAnnotationKey = "notified.notifications.argoproj.io"
+)
+
 // ApplicationSetReconciler reconciles a ApplicationSet object
 type ApplicationSetReconciler struct {
 	client.Client
@@ -62,6 +67,7 @@ type ApplicationSetReconciler struct {
 	Generators       map[string]generators.Generator
 	ArgoDB           db.ArgoDB
 	ArgoAppClientset appclientset.Interface
+	KubeClientset    kubernetes.Interface
 	utils.Policy
 	utils.Renderer
 }
@@ -156,7 +162,7 @@ func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Con
 			return err
 		}
 
-		if err := argoutil.ValidateDestination(ctx, &app.Spec.Destination, r.ArgoDB); err != nil {
+		if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, namespace); err != nil {
 			return fmt.Errorf("application destination spec is invalid: %s", err.Error())
 		}
 
@@ -165,7 +171,7 @@ func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Con
 			return err
 		}
 		if len(conditions) > 0 {
-			return fmt.Errorf("application spec is invalid: %s", argo.FormatAppConditions(conditions))
+			return fmt.Errorf("application spec is invalid: %s", argoutil.FormatAppConditions(conditions))
 		}
 
 	}
@@ -438,7 +444,16 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
 		action, err := utils.CreateOrUpdate(ctx, r.Client, found, func() error {
 			// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
 			found.Spec = generatedApp.Spec
+
+			// Preserve argo cd notifications state (https://github.com/argoproj-labs/applicationset/issues/180)
+			if state, exists := found.ObjectMeta.Annotations[NotifiedAnnotationKey]; exists {
+				if generatedApp.Annotations == nil {
+					generatedApp.Annotations = map[string]string{}
+				}
+				generatedApp.Annotations[NotifiedAnnotationKey] = state
+			}
 			found.ObjectMeta.Annotations = generatedApp.Annotations
+
 			found.ObjectMeta.Finalizers = generatedApp.Finalizers
 			found.ObjectMeta.Labels = generatedApp.Labels
 			return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme)
@@ -557,7 +572,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
 
 	// If the destination is invalid (for example the cluster is no longer defined), then remove
 	// the application finalizers to avoid triggering Argo CD bug #5817
-	if err := argoutil.ValidateDestination(ctx, &app.Spec.Destination, r.ArgoDB); err != nil {
+	if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, applicationSet.Namespace); err != nil {
 
 		// Filter out the Argo CD finalizer from the finalizer list
 		newFinalizers := []string{}
diff --git a/pkg/generators/cluster.go b/pkg/generators/cluster.go
index be89a57..1920728 100644
--- a/pkg/generators/cluster.go
+++ b/pkg/generators/cluster.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/argoproj/argo-cd/util/db"
 	"github.com/argoproj/argo-cd/util/settings"
 	log "github.com/sirupsen/logrus"
 
@@ -15,6 +14,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	"github.com/argoproj-labs/applicationset/pkg/utils"
 )
 
 const (
@@ -32,7 +32,6 @@ type ClusterGenerator struct {
 	// namespace is the Argo CD namespace
 	namespace       string
 	settingsManager *settings.SettingsManager
-	argoDB          db.ArgoDB
 }
 
 func NewClusterGenerator(c client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
@@ -45,7 +44,6 @@ func NewClusterGenerator(c client.Client, ctx context.Context, clientset kuberne
 		clientset:       clientset,
 		namespace:       namespace,
 		settingsManager: settingsManager,
-		argoDB:          db.NewDB(namespace, settingsManager, clientset),
 	}
 	return g
 }
@@ -74,7 +72,7 @@ func (g *ClusterGenerator) GenerateParams(
 	ignoreLocalClusters := len(appSetGenerator.Clusters.Selector.MatchExpressions) > 0 || len(appSetGenerator.Clusters.Selector.MatchLabels) > 0
 
 	// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
-	clustersFromArgoCD, err := g.argoDB.ListClusters(g.ctx)
+	clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/generators/git.go b/pkg/generators/git.go
index 4f00617..d93234a 100644
--- a/pkg/generators/git.go
+++ b/pkg/generators/git.go
@@ -174,19 +174,29 @@ func (g *GitGenerator) generateParamsFromGitFile(appSetGenerator *argoprojiov1al
 
 }
 
-func (g *GitGenerator) filterApps(Directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allApps []string) []string {
+func (g *GitGenerator) filterApps(Directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allPaths []string) []string {
 	res := []string{}
-	for _, requestedPath := range Directories {
-		for _, appPath := range allApps {
+	for _, appPath := range allPaths {
+		appInclude := false
+		appExclude := false
+		// Iterating over each appPath and check whether directories object has requestedPath that matches the appPath
+		for _, requestedPath := range Directories {
 			match, err := path.Match(requestedPath.Path, appPath)
 			if err != nil {
 				log.WithError(err).WithField("requestedPath", requestedPath).
 					WithField("appPath", appPath).Error("error while matching appPath to requestedPath")
 				continue
 			}
-			if match {
-				res = append(res, appPath)
+			if match && !requestedPath.Exclude {
+				appInclude = true
 			}
+			if match && requestedPath.Exclude {
+				appExclude = true
+			}
+		}
+		// Whenever there is a path with exclude: true it wont be included, even if it is included in a different path pattern
+		if appInclude && !appExclude {
+			res = append(res, appPath)
 		}
 	}
 	return res
diff --git a/pkg/services/repo_service.go b/pkg/services/repo_service.go
index f8728fe..eded55e 100644
--- a/pkg/services/repo_service.go
+++ b/pkg/services/repo_service.go
@@ -5,6 +5,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/reposerver/apiclient"
@@ -99,7 +100,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revi
 		}
 
 		fname := info.Name()
-		if fname == ".git" { // Skip repository metadata
+		if strings.HasPrefix(fname, ".") { // Skip all folders starts with "."
 			return filepath.SkipDir
 		}
 
diff --git a/pkg/utils/clusterUtils.go b/pkg/utils/clusterUtils.go
new file mode 100644
index 0000000..987111d
--- /dev/null
+++ b/pkg/utils/clusterUtils.go
@@ -0,0 +1,186 @@
+package utils
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/argoproj/argo-cd/common"
+	appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+	log "github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/utils/pointer"
+)
+
+// The contents of this file are from
+// github.com/argoproj/argo-cd/util/db/cluster.go
+//
+// The main difference is that ListClusters(...) calls the kubeclient directly,
+// via `g.clientset.CoreV1().Secrets`, rather than using the `db.listClusterSecrets()``
+// which appears to have a race condition on when it is called.
+//
+// I was reminded of this issue that I opened, which might be related:
+// https://github.com/argoproj/argo-cd/issues/4755
+//
+// I hope to upstream this change in some form, so that we do not need to worry about
+// Argo CD changing the logic on us.
+
+var (
+	localCluster = appv1.Cluster{
+		Name:            "in-cluster",
+		Server:          common.KubernetesInternalAPIServerAddr,
+		ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful},
+	}
+	initLocalCluster sync.Once
+)
+
+const (
+	ArgoCDSecretTypeLabel   = "argocd.argoproj.io/secret-type"
+	ArgoCDSecretTypeCluster = "cluster"
+)
+
+func ListClusters(ctx context.Context, clientset kubernetes.Interface, namespace string) (*appv1.ClusterList, error) {
+
+	clusterSecretsList, err := clientset.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})
+	if err != nil {
+		return nil, err
+	}
+
+	if clusterSecretsList == nil {
+		return nil, nil
+	}
+
+	clusterSecrets := clusterSecretsList.Items
+
+	clusterList := appv1.ClusterList{
+		Items: make([]appv1.Cluster, len(clusterSecrets)),
+	}
+	hasInClusterCredentials := false
+	for i, clusterSecret := range clusterSecrets {
+		cluster := *secretToCluster(&clusterSecret)
+		clusterList.Items[i] = cluster
+		if cluster.Server == common.KubernetesInternalAPIServerAddr {
+			hasInClusterCredentials = true
+		}
+	}
+	if !hasInClusterCredentials {
+		localCluster := getLocalCluster(clientset)
+		if localCluster != nil {
+			clusterList.Items = append(clusterList.Items, *localCluster)
+		}
+	}
+	return &clusterList, nil
+}
+
+func getLocalCluster(clientset kubernetes.Interface) *appv1.Cluster {
+	initLocalCluster.Do(func() {
+		info, err := clientset.Discovery().ServerVersion()
+		if err == nil {
+			localCluster.ServerVersion = fmt.Sprintf("%s.%s", info.Major, info.Minor)
+			localCluster.ConnectionState = appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful}
+		} else {
+			localCluster.ConnectionState = appv1.ConnectionState{
+				Status:  appv1.ConnectionStatusFailed,
+				Message: err.Error(),
+			}
+		}
+	})
+	cluster := localCluster.DeepCopy()
+	now := metav1.Now()
+	cluster.ConnectionState.ModifiedAt = &now
+	return cluster
+}
+
+// secretToCluster converts a secret into a Cluster object
+func secretToCluster(s *corev1.Secret) *appv1.Cluster {
+	var config appv1.ClusterConfig
+	if len(s.Data["config"]) > 0 {
+		err := json.Unmarshal(s.Data["config"], &config)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	var namespaces []string
+	for _, ns := range strings.Split(string(s.Data["namespaces"]), ",") {
+		if ns = strings.TrimSpace(ns); ns != "" {
+			namespaces = append(namespaces, ns)
+		}
+	}
+	var refreshRequestedAt *metav1.Time
+	if v, found := s.Annotations[common.AnnotationKeyRefresh]; found {
+		requestedAt, err := time.Parse(time.RFC3339, v)
+		if err != nil {
+			log.Warnf("Error while parsing date in cluster secret '%s': %v", s.Name, err)
+		} else {
+			refreshRequestedAt = &metav1.Time{Time: requestedAt}
+		}
+	}
+	var shard *int64
+	if shardStr := s.Data["shard"]; shardStr != nil {
+		if val, err := strconv.Atoi(string(shardStr)); err != nil {
+			log.Warnf("Error while parsing shard in cluster secret '%s': %v", s.Name, err)
+		} else {
+			shard = pointer.Int64Ptr(int64(val))
+		}
+	}
+	cluster := appv1.Cluster{
+		ID:                 string(s.UID),
+		Server:             strings.TrimRight(string(s.Data["server"]), "/"),
+		Name:               string(s.Data["name"]),
+		Namespaces:         namespaces,
+		Config:             config,
+		RefreshRequestedAt: refreshRequestedAt,
+		Shard:              shard,
+	}
+	return &cluster
+}
+
+// ValidateDestination checks:
+// if we used destination name we infer the server url
+// if we used both name and server then we return an invalid spec error
+func ValidateDestination(ctx context.Context, dest *appv1.ApplicationDestination, clientset kubernetes.Interface, namespace string) error {
+	if dest.Name != "" {
+		if dest.Server == "" {
+			server, err := getDestinationServer(ctx, dest.Name, clientset, namespace)
+			if err != nil {
+				return fmt.Errorf("unable to find destination server: %v", err)
+			}
+			if server == "" {
+				return fmt.Errorf("application references destination cluster %s which does not exist", dest.Name)
+			}
+			dest.SetInferredServer(server)
+		} else {
+			if !dest.IsServerInferred() {
+				return fmt.Errorf("application destination can't have both name and server defined: %s %s", dest.Name, dest.Server)
+			}
+		}
+	}
+	return nil
+}
+
+func getDestinationServer(ctx context.Context, clusterName string, clientset kubernetes.Interface, namespace string) (string, error) {
+
+	clusterList, err := ListClusters(ctx, clientset, namespace)
+	if err != nil {
+		return "", err
+	}
+	var servers []string
+	for _, c := range clusterList.Items {
+		if c.Name == clusterName {
+			servers = append(servers, c.Server)
+		}
+	}
+	if len(servers) > 1 {
+		return "", fmt.Errorf("there are %d clusters with the same name: %v", len(servers), servers)
+	} else if len(servers) == 0 {
+		return "", fmt.Errorf("there are no clusters with this name: %s", clusterName)
+	}
+	return servers[0], nil
+}


Test patch:
diff --git a/pkg/controllers/applicationset_controller_test.go b/pkg/controllers/applicationset_controller_test.go
index 3c4c78c..50a06b8 100644
--- a/pkg/controllers/applicationset_controller_test.go
+++ b/pkg/controllers/applicationset_controller_test.go
@@ -11,17 +11,15 @@ import (
 
 	"github.com/argoproj-labs/applicationset/pkg/generators"
 	"github.com/argoproj/argo-cd/common"
-	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	"github.com/sirupsen/logrus"
-	log "github.com/sirupsen/logrus"
 	logtest "github.com/sirupsen/logrus/hooks/test"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/tools/record"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	crtclient "sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -29,6 +27,7 @@ import (
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
 	dbmocks "github.com/argoproj/argo-cd/util/db/mocks"
+	kubefake "k8s.io/client-go/kubernetes/fake"
 )
 
 type generatorMock struct {
@@ -163,7 +162,8 @@ func TestExtractApplications(t *testing.T) {
 				Generators: map[string]generators.Generator{
 					"List": &generatorMock,
 				},
-				Renderer: &rendererMock,
+				Renderer:      &rendererMock,
+				KubeClientset: kubefake.NewSimpleClientset(),
 			}
 
 			got, err := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
@@ -274,7 +274,8 @@ func TestMergeTemplateApplications(t *testing.T) {
 				Generators: map[string]generators.Generator{
 					"List": &generatorMock,
 				},
-				Renderer: &rendererMock,
+				Renderer:      &rendererMock,
+				KubeClientset: kubefake.NewSimpleClientset(),
 			}
 
 			got, _ := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
@@ -736,11 +737,77 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "Ensure that argocd notifications state annotation is preserved from an existing app",
+			appSet: argoprojiov1alpha1.ApplicationSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "name",
+					Namespace: "namespace",
+				},
+				Spec: argoprojiov1alpha1.ApplicationSetSpec{
+					Template: argoprojiov1alpha1.ApplicationSetTemplate{
+						Spec: argov1alpha1.ApplicationSpec{
+							Project: "project",
+						},
+					},
+				},
+			},
+			existingApps: []argov1alpha1.Application{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "2",
+						Labels:          map[string]string{"label-key": "label-value"},
+						Annotations: map[string]string{
+							"annot-key":           "annot-value",
+							NotifiedAnnotationKey: `{"b620d4600c771a6f4cxxxxxxx:on-deployed:[0].y7b5sbwa2Q329JYHxxxxxx-fBs:slack:slack-test":1617144614}`,
+						},
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+			desiredApps: []argov1alpha1.Application{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "app1",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+			expected: []argov1alpha1.Application{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "3",
+						Annotations: map[string]string{
+							NotifiedAnnotationKey: `{"b620d4600c771a6f4cxxxxxxx:on-deployed:[0].y7b5sbwa2Q329JYHxxxxxx-fBs:slack:slack-test":1617144614}`,
+						},
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+		},
 	} {
 
 		t.Run(c.name, func(t *testing.T) {
 
-			initObjs := []client.Object{&c.appSet}
+			initObjs := []crtclient.Object{&c.appSet}
 
 			for _, a := range c.existingApps {
 				err = controllerutil.SetControllerReference(&c.appSet, &a, scheme)
@@ -783,18 +850,12 @@ func TestRemoveFinalizerOnInvalidDestination(t *testing.T) {
 	err = argov1alpha1.AddToScheme(scheme)
 	assert.Nil(t, err)
 
-	myCluster := argov1alpha1.Cluster{
-		Server: "https://kubernetes.default.svc",
-		Name:   "my-cluster2",
-	}
-
 	for _, c := range []struct {
 		// name is human-readable test name
 		name               string
 		existingFinalizers []string
 		expectedFinalizers []string
 	}{
-
 		{
 			name:               "no finalizers",
 			existingFinalizers: []string{},
@@ -805,7 +866,6 @@ func TestRemoveFinalizerOnInvalidDestination(t *testing.T) {
 			existingFinalizers: []string{common.ResourcesFinalizerName},
 			expectedFinalizers: nil,
 		},
-
 		{
 			name:               "contains only non-argo finalizer",
 			existingFinalizers: []string{"non-argo-finalizer"},
@@ -846,25 +906,37 @@ func TestRemoveFinalizerOnInvalidDestination(t *testing.T) {
 				},
 			}
 
-			initObjs := []client.Object{&app, &appSet}
+			initObjs := []crtclient.Object{&app, &appSet}
 
 			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).Build()
+			secret := &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "my-secret",
+					Namespace: "namespace",
+					Labels: map[string]string{
+						generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
+					},
+				},
+				Data: map[string][]byte{
+					// Since this test requires the cluster to be an invalid destination, we
+					// always return a cluster named 'my-cluster2' (different from app 'my-cluster', above)
+					"name":   []byte("mycluster2"),
+					"server": []byte("https://kubernetes.default.svc"),
+					"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
+				},
+			}
 
-			// Always return a cluster named 'my-cluster2' (different from app 'my-cluster')
-			dbMock := &dbmocks.ArgoDB{}
-			dbMock.On("GetCluster", mock.Anything, "https://kubernetes.default.svc").Return(&myCluster, nil)
-			dbMock.On("ListClusters", mock.Anything).Return(&v1alpha1.ClusterList{Items: []argov1alpha1.Cluster{
-				myCluster,
-			}}, nil)
+			objects := append([]runtime.Object{}, secret)
+			kubeclientset := kubefake.NewSimpleClientset(objects...)
 
 			r := ApplicationSetReconciler{
-				Client:   client,
-				Scheme:   scheme,
-				Recorder: record.NewFakeRecorder(10),
-				ArgoDB:   dbMock,
+				Client:        client,
+				Scheme:        scheme,
+				Recorder:      record.NewFakeRecorder(10),
+				KubeClientset: kubeclientset,
 			}
 
-			appLog := log.WithFields(log.Fields{"app": app.Name, "appSet": ""})
+			appLog := logrus.WithFields(logrus.Fields{"app": app.Name, "appSet": ""})
 
 			appInputParam := app.DeepCopy()
 
@@ -1047,7 +1119,7 @@ func TestCreateApplications(t *testing.T) {
 			},
 		},
 	} {
-		initObjs := []client.Object{&c.appSet}
+		initObjs := []crtclient.Object{&c.appSet}
 		for _, a := range c.existsApps {
 			err = controllerutil.SetControllerReference(&c.appSet, &a, scheme)
 			assert.Nil(t, err)
@@ -1189,7 +1261,7 @@ func TestDeleteInCluster(t *testing.T) {
 			},
 		},
 	} {
-		initObjs := []client.Object{&c.appSet}
+		initObjs := []crtclient.Object{&c.appSet}
 		for _, a := range c.existingApps {
 			temp := a
 			err = controllerutil.SetControllerReference(&c.appSet, &temp, scheme)
@@ -1875,9 +1947,27 @@ func TestValidateGeneratedApplications(t *testing.T) {
 
 		t.Run(cc.name, func(t *testing.T) {
 
+			secret := &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "my-secret",
+					Namespace: "namespace",
+					Labels: map[string]string{
+						generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
+					},
+				},
+				Data: map[string][]byte{
+					"name":   []byte("my-cluster"),
+					"server": []byte("https://kubernetes.default.svc"),
+					"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
+				},
+			}
+
+			objects := append([]runtime.Object{}, secret)
+			kubeclientset := kubefake.NewSimpleClientset(objects...)
+
 			argoDBMock := dbmocks.ArgoDB{}
 			argoDBMock.On("GetCluster", mock.Anything, "https://kubernetes.default.svc").Return(&myCluster, nil)
-			argoDBMock.On("ListClusters", mock.Anything).Return(&v1alpha1.ClusterList{Items: []argov1alpha1.Cluster{
+			argoDBMock.On("ListClusters", mock.Anything).Return(&argov1alpha1.ClusterList{Items: []argov1alpha1.Cluster{
 				myCluster,
 			}}, nil)
 
@@ -1893,6 +1983,7 @@ func TestValidateGeneratedApplications(t *testing.T) {
 				Generators:       map[string]generators.Generator{},
 				ArgoDB:           &argoDBMock,
 				ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
+				KubeClientset:    kubeclientset,
 			}
 
 			appSetInfo := argoprojiov1alpha1.ApplicationSet{}
diff --git a/pkg/generators/git_test.go b/pkg/generators/git_test.go
index f34bd33..ee0c859 100644
--- a/pkg/generators/git_test.go
+++ b/pkg/generators/git_test.go
@@ -87,6 +87,42 @@ func TestGitGenerateParamsFromDirectories(t *testing.T) {
 			},
 			expectedError: nil,
 		},
+		{
+			name:        "It filters application according to the paths with Exclude",
+			directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*", Exclude: true}, {Path: "*"}, {Path: "*/*"}},
+			repoApps: []string{
+				"app1",
+				"app2",
+				"p1/app2",
+				"p1/app3",
+				"p2/app3",
+			},
+			repoError: nil,
+			expected: []map[string]string{
+				{"path": "app1", "path.basename": "app1"},
+				{"path": "app2", "path.basename": "app2"},
+				{"path": "p2/app3", "path.basename": "app3"},
+			},
+			expectedError: nil,
+		},
+		{
+			name:        "Expecting same exclude behavior with different order",
+			directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}, {Path: "*/*"}, {Path: "p1/*", Exclude: true}},
+			repoApps: []string{
+				"app1",
+				"app2",
+				"p1/app2",
+				"p1/app3",
+				"p2/app3",
+			},
+			repoError: nil,
+			expected: []map[string]string{
+				{"path": "app1", "path.basename": "app1"},
+				{"path": "app2", "path.basename": "app2"},
+				{"path": "p2/app3", "path.basename": "app3"},
+			},
+			expectedError: nil,
+		},
 		{
 			name:          "handles empty response from repo server",
 			directories:   []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
diff --git a/pkg/utils/clusterUtils_test.go b/pkg/utils/clusterUtils_test.go
new file mode 100644
index 0000000..06c7d37
--- /dev/null
+++ b/pkg/utils/clusterUtils_test.go
@@ -0,0 +1,176 @@
+package utils
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/argoproj-labs/applicationset/test/e2e/fixture/applicationsets/utils"
+	argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/fake"
+	kubetesting "k8s.io/client-go/testing"
+)
+
+const (
+	fakeNamespace = "fake-ns"
+)
+
+// From Argo CD util/db/cluster_test.go
+func Test_secretToCluster(t *testing.T) {
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "mycluster",
+			Namespace: fakeNamespace,
+		},
+		Data: map[string][]byte{
+			"name":   []byte("test"),
+			"server": []byte("http://mycluster"),
+			"config": []byte("{\"username\":\"foo\"}"),
+		},
+	}
+	cluster := secretToCluster(secret)
+	assert.Equal(t, *cluster, argoappv1.Cluster{
+		Name:   "test",
+		Server: "http://mycluster",
+		Config: argoappv1.ClusterConfig{
+			Username: "foo",
+		},
+	})
+}
+
+// From Argo CD util/db/cluster_test.go
+func Test_secretToCluster_NoConfig(t *testing.T) {
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "mycluster",
+			Namespace: fakeNamespace,
+		},
+		Data: map[string][]byte{
+			"name":   []byte("test"),
+			"server": []byte("http://mycluster"),
+		},
+	}
+	cluster := secretToCluster(secret)
+	assert.Equal(t, *cluster, argoappv1.Cluster{
+		Name:   "test",
+		Server: "http://mycluster",
+	})
+}
+
+func createClusterSecret(secretName string, clusterName string, clusterServer string) *corev1.Secret {
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: utils.ArgoCDNamespace,
+			Labels: map[string]string{
+				ArgoCDSecretTypeLabel: ArgoCDSecretTypeCluster,
+			},
+		},
+		Data: map[string][]byte{
+			"name":   []byte(clusterName),
+			"server": []byte(clusterServer),
+			"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
+		},
+	}
+
+	return secret
+
+}
+
+// From util/argo/argo_test.go
+// (ported to use kubeclientset)
+func TestValidateDestination(t *testing.T) {
+
+	t.Run("Validate destination with server url", func(t *testing.T) {
+
+		dest := argoappv1.ApplicationDestination{
+			Server:    "https://127.0.0.1:6443",
+			Namespace: "default",
+		}
+
+		appCond := ValidateDestination(context.Background(), &dest, nil, fakeNamespace)
+		assert.Nil(t, appCond)
+		assert.False(t, dest.IsServerInferred())
+	})
+
+	t.Run("Validate destination with server name", func(t *testing.T) {
+		dest := argoappv1.ApplicationDestination{
+			Name: "minikube",
+		}
+
+		secret := createClusterSecret("my-secret", "minikube", "https://127.0.0.1:6443")
+		objects := []runtime.Object{}
+		objects = append(objects, secret)
+		kubeclientset := fake.NewSimpleClientset(objects...)
+
+		appCond := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
+		assert.Nil(t, appCond)
+		assert.Equal(t, "https://127.0.0.1:6443", dest.Server)
+		assert.True(t, dest.IsServerInferred())
+	})
+
+	t.Run("Error when having both server url and name", func(t *testing.T) {
+		dest := argoappv1.ApplicationDestination{
+			Server:    "https://127.0.0.1:6443",
+			Name:      "minikube",
+			Namespace: "default",
+		}
+
+		err := ValidateDestination(context.Background(), &dest, nil, utils.ArgoCDNamespace)
+		assert.Equal(t, "application destination can't have both name and server defined: minikube https://127.0.0.1:6443", err.Error())
+		assert.False(t, dest.IsServerInferred())
+	})
+
+	t.Run("List clusters fails", func(t *testing.T) {
+		dest := argoappv1.ApplicationDestination{
+			Name: "minikube",
+		}
+		kubeclientset := fake.NewSimpleClientset()
+
+		kubeclientset.PrependReactor("list", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
+			return true, nil, errors.New("an error occurred")
+		})
+
+		err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
+		assert.Equal(t, "unable to find destination server: an error occurred", err.Error())
+		assert.False(t, dest.IsServerInferred())
+	})
+
+	t.Run("Destination cluster does not exist", func(t *testing.T) {
+		dest := argoappv1.ApplicationDestination{
+			Name: "minikube",
+		}
+
+		secret := createClusterSecret("dind", "dind", "https://127.0.0.1:6443")
+		objects := []runtime.Object{}
+		objects = append(objects, secret)
+		kubeclientset := fake.NewSimpleClientset(objects...)
+
+		err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
+		assert.Equal(t, "unable to find destination server: there are no clusters with this name: minikube", err.Error())
+		assert.False(t, dest.IsServerInferred())
+	})
+
+	t.Run("Validate too many clusters with the same name", func(t *testing.T) {
+		dest := argoappv1.ApplicationDestination{
+			Name: "dind",
+		}
+
+		secret := createClusterSecret("dind", "dind", "https://127.0.0.1:2443")
+		secret2 := createClusterSecret("dind2", "dind", "https://127.0.0.1:8443")
+
+		objects := []runtime.Object{}
+		objects = append(objects, secret, secret2)
+		kubeclientset := fake.NewSimpleClientset(objects...)
+
+		err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
+		assert.Equal(t, "unable to find destination server: there are 2 clusters with the same name: [https://127.0.0.1:2443 https://127.0.0.1:8443]", err.Error())
+		assert.False(t, dest.IsServerInferred())
+	})
+
+}
diff --git a/test/e2e/fixture/applicationsets/actions.go b/test/e2e/fixture/applicationsets/actions.go
index 75117f8..e6e3617 100644
--- a/test/e2e/fixture/applicationsets/actions.go
+++ b/test/e2e/fixture/applicationsets/actions.go
@@ -67,7 +67,8 @@ func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clu
 		},
 	}
 
-	_, err := utils.KubeClientset.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+	_, err := fixtureClient.KubeClientset.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
 
 	a.describeAction = fmt.Sprintf("creating cluster Secret '%s'", secretName)
 	a.lastOutput, a.lastError = "", err
@@ -83,7 +84,8 @@ func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
 	appSet.APIVersion = "argoproj.io/v1alpha1"
 	appSet.Kind = "ApplicationSet"
 
-	newResource, err := utils.AppSetClientset.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+	newResource, err := fixtureClient.AppSetClientset.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
 
 	if err == nil {
 		a.context.name = newResource.GetName()
@@ -100,8 +102,10 @@ func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
 func (a *Actions) Delete() *Actions {
 	a.context.t.Helper()
 
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+
 	deleteProp := metav1.DeletePropagationForeground
-	err := utils.AppSetClientset.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp})
+	err := fixtureClient.AppSetClientset.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp})
 	a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s' %v", a.context.name, err)
 	a.lastOutput, a.lastError = "", err
 	a.verifyAction()
@@ -113,7 +117,8 @@ func (a *Actions) Delete() *Actions {
 func (a *Actions) get() (*v1alpha1.ApplicationSet, error) {
 	appSet := v1alpha1.ApplicationSet{}
 
-	newResource, err := utils.AppSetClientset.Get(context.Background(), a.context.name, metav1.GetOptions{})
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+	newResource, err := fixtureClient.AppSetClientset.Get(context.Background(), a.context.name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -142,7 +147,8 @@ func (a *Actions) Update(toUpdate func(*v1alpha1.ApplicationSet)) *Actions {
 		toUpdate(appSet)
 		a.describeAction = fmt.Sprintf("updating ApplicationSet '%s'", appSet.Name)
 
-		_, err = utils.AppSetClientset.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
+		fixtureClient := utils.GetE2EFixtureK8sClient()
+		_, err = fixtureClient.AppSetClientset.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
 	}
 	a.lastOutput, a.lastError = "", err
 	a.verifyAction()
diff --git a/test/e2e/fixture/applicationsets/consequences.go b/test/e2e/fixture/applicationsets/consequences.go
index c2e43f9..ab9df2b 100644
--- a/test/e2e/fixture/applicationsets/consequences.go
+++ b/test/e2e/fixture/applicationsets/consequences.go
@@ -66,7 +66,9 @@ func (c *Consequences) app(name string) *argov1alpha1.Application {
 }
 
 func (c *Consequences) apps() []argov1alpha1.Application {
-	list, err := utils.AppClientset.ArgoprojV1alpha1().Applications(utils.ArgoCDNamespace).List(context.Background(), v1.ListOptions{})
+
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+	list, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(utils.ArgoCDNamespace).List(context.Background(), v1.ListOptions{})
 	errors.CheckError(err)
 
 	if list == nil {
diff --git a/test/e2e/fixture/applicationsets/utils/fixture.go b/test/e2e/fixture/applicationsets/utils/fixture.go
index 91f9913..b112f4d 100644
--- a/test/e2e/fixture/applicationsets/utils/fixture.go
+++ b/test/e2e/fixture/applicationsets/utils/fixture.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"regexp"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
@@ -30,26 +31,59 @@ const (
 )
 
 var (
-	id               string
+	id string
+
+	// call GetClientVars() to retrieve the Kubernetes client data for E2E test fixtures
+	clientInitialized  sync.Once
+	internalClientVars *E2EFixtureK8sClient
+)
+
+// E2EFixtureK8sClient contains Kubernetes clients initialized from local k8s configuration
+type E2EFixtureK8sClient struct {
 	KubeClientset    kubernetes.Interface
 	DynamicClientset dynamic.Interface
 	AppClientset     appclientset.Interface
 	AppSetClientset  dynamic.ResourceInterface
-)
+}
+
+// GetE2EFixtureK8sClient initializes the Kubernetes clients (if needed), and returns the most recently initalized value.
+// Note: this requires a local Kubernetes configuration (for example, while running the E2E tests).
+func GetE2EFixtureK8sClient() *E2EFixtureK8sClient {
+
+	// Initialize the Kubernetes clients only on first use
+	clientInitialized.Do(func() {
+
+		// set-up variables
+		config := getKubeConfig("", clientcmd.ConfigOverrides{})
 
+		internalClientVars = &E2EFixtureK8sClient{
+			AppClientset:     appclientset.NewForConfigOrDie(config),
+			DynamicClientset: dynamic.NewForConfigOrDie(config),
+			KubeClientset:    kubernetes.NewForConfigOrDie(config),
+		}
+
+		internalClientVars.AppSetClientset = internalClientVars.DynamicClientset.Resource(v1alpha1.GroupVersion.WithResource("applicationsets")).Namespace(ArgoCDNamespace)
+
+	})
+	return internalClientVars
+}
+
+// EnsureCleanSlate ensures that the Kubernetes resources on the cluster are are in a 'clean' state, before a test is run.
 func EnsureCleanState(t *testing.T) {
 
 	start := time.Now()
 
-	policy := v1.DeletePropagationBackground
+	fixtureClient := GetE2EFixtureK8sClient()
+
+	policy := v1.DeletePropagationForeground
 	// delete resources
 	// kubectl delete applicationsets --all
-	CheckError(AppSetClientset.DeleteCollection(context.Background(), v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{}))
+	CheckError(fixtureClient.AppSetClientset.DeleteCollection(context.Background(), v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{}))
 	// kubectl delete apps --all
-	CheckError(AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).DeleteCollection(context.Background(), v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{}))
+	CheckError(fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).DeleteCollection(context.Background(), v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{}))
 
 	// kubectl delete secrets -l e2e.argoproj.io=true
-	CheckError(KubeClientset.CoreV1().Secrets(ArgoCDNamespace).DeleteCollection(context.Background(),
+	CheckError(fixtureClient.KubeClientset.CoreV1().Secrets(ArgoCDNamespace).DeleteCollection(context.Background(),
 		v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{LabelSelector: TestingLabel + "=true"}))
 
 	// remove tmp dir
@@ -78,14 +112,6 @@ func init() {
 
 	// ensure we log all shell execs
 	log.SetLevel(log.DebugLevel)
-
-	// set-up variables
-	config := getKubeConfig("", clientcmd.ConfigOverrides{})
-	AppClientset = appclientset.NewForConfigOrDie(config)
-	KubeClientset = kubernetes.NewForConfigOrDie(config)
-	DynamicClientset = dynamic.NewForConfigOrDie(config)
-	AppSetClientset = DynamicClientset.Resource(v1alpha1.GroupVersion.WithResource("applicationsets")).Namespace(ArgoCDNamespace)
-
 }
 
 // PrettyPrintJson is a utility function for debugging purposes