Repository: argoproj-labs/applicationset
Pull request #195 (2021-04-12 13:56:39)

add exclude to git directories

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/argoproj-labs/applicationset?pullRequest=195) <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-labs/applicationset?pullRequest=195) 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-labs/applicationset?pullRequest=195) it.</sub>

Patch:
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/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..d1064da 100644
--- a/pkg/controllers/applicationset_controller.go
+++ b/pkg/controllers/applicationset_controller.go
@@ -53,6 +53,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
@@ -438,7 +445,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)
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
 		}
 


Test patch:
diff --git a/pkg/controllers/applicationset_controller_test.go b/pkg/controllers/applicationset_controller_test.go
index 3c4c78c..723435a 100644
--- a/pkg/controllers/applicationset_controller_test.go
+++ b/pkg/controllers/applicationset_controller_test.go
@@ -736,6 +736,72 @@ 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) {
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: "*"}},