Repository: kubernetes-sigs/external-dns (language: go)

Issue:
Allow to configure a ingressClassName filter
<!-- Please only use this template for submitting enhancement requests. This can be something like a new provider or a new gateway.  -->

**What would you like to be added**:
Currently there is the option to configure a `--annotation-filter` and a `--source` filter [0]. Since Kubernetes 1.19 the Ingress Resource was promoted to GA and got a new ingressClassName field.

I'd like to have a option in External DNS to configure a `--ingressClassName-filter`.


**Why is this needed**:

We use 2 external dns and 2 ingress deployments. One for private DNS zones, One for public DNS zones. We configured one external dns to filter for the internal ingress and the other one to filter for the external ingress, just like its described in the FAQ [0].

Now with the GA of the Ingress API there is a dedicated field to configure your Ingress called `ingressClassName`. And the old annotation is now deprecated. To avoid configuration errors (e.g. setting annotations AND ingressClassName) it would be much better if external dns supports a new kind of filter based on based on `ingressClassNames`

[0] https://github.com/kubernetes-sigs/external-dns/blob/master/docs/faq.md#running-an-internal-and-external-dns-service
[1] https://kubernetes.io/docs/concepts/services-networking/ingress/#deprecated-annotation



Discussion / hints:

Fix patch:
diff --git a/docs/faq.md b/docs/faq.md
index 044ee7aa86..43731a3712 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -257,24 +257,33 @@ spec:
 ### Running an internal and external dns service
 
 Sometimes you need to run an internal and an external dns service.
-The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external
-one to expose DNS to the internet.
+The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external one to expose DNS to the internet.
 
-To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to
-an instance of an ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
-then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-internal)`
-and one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)`.
+To do this with ExternalDNS you can use the `--ingress-class` flag to specifically tie an instance of ExternalDNS to an instance of a ingress controller. 
+Let's assume you have two ingress controllers, `internal` and `external`.
+You can then start two ExternalDNS providers, one with `--ingress-class=internal` and one with `--ingress-class=external`.
 
-If you need to search for multiple values of said annotation, you can provide a comma separated list, like so:
-`--annotation-filter=kubernetes.io/ingress.class in (nginx-internal, alb-ingress-internal)`.
+If you need to search for multiple ingress classes, you can specify the flag multiple times, like so:
+`--ingress-class=internal --ingress-class=external`.
 
-Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects.
-If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source`  and `--annotation-filter`.
+The `--ingress-class` flag will check both the `spec.ingressClassName` field and the deprecated `kubernetes.io/ingress.class` annotation.
+The `spec.ingressClassName` tasks precedence over the annotation if both are supplied.
 
-**Note:** Filtering based on annotation means that the external-dns controller will receive all resources of that kind and then filter on the client-side.
-In larger clusters with many resources which change frequently this can cause performance issues. If only some resources need to be managed by an instance
-of external-dns then label filtering can be used instead of annotation filtering. This means that only those resources which match the selector specified
-in `--label-filter` will be passed to the controller.
+**Backward compatibility**
+
+The previous `--annotation-filter` flag can still be used to restrict which objects ExternalDNS considers; for example, `--annotation-filter=kubernetes.io/ingress.class in (public,dmz)`.
+
+However, beware when using annotation filters with multiple sources, e.g. `--source=service --source=ingress`, since `--annotation-filter` will filter every given source object.
+If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted `--source`  and `--annotation-filter`.
+
+Note: the `--ingress-class` flag cannot be used at the same time as the `--annotation-filter=kubernetes.io/ingress.class in (...)` flag; if you do this an error will be raised.
+
+**Performance considerations**
+
+Filtering based on ingress class name or annotations means that the external-dns controller will receive all resources of that kind and then filter on the client-side.
+In larger clusters with many resources which change frequently this can cause performance issues. 
+If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering). 
+This means that only those resources which match the selector specified in `--label-filter` will be passed to the controller.
 
 ### How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both?
 
diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md
index ee0477edea..f7653237e4 100644
--- a/docs/tutorials/alibabacloud.md
+++ b/docs/tutorials/alibabacloud.md
@@ -233,9 +233,8 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: foo
-  annotations:
-    kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller.
 spec:
+  ingressClassName: nginx # use the one that corresponds to your ingress controller.
   rules:
   - host: foo.external-dns-test.com
     http:
diff --git a/docs/tutorials/aws-load-balancer-controller.md b/docs/tutorials/aws-load-balancer-controller.md
index 9e66b1e6da..98bc5da693 100644
--- a/docs/tutorials/aws-load-balancer-controller.md
+++ b/docs/tutorials/aws-load-balancer-controller.md
@@ -24,7 +24,7 @@ as Kubernetes does with the AWS cloud provider.
 In the examples that follow, it is assumed that you configured the ALB Ingress
 Controller with the `ingress-class=alb` argument (not to be confused with the
 same argument to ExternalDNS) so that the controller will only respect Ingress
-objects with the `kubernetes.io/ingress.class` annotation set to "alb".
+objects with the `ingressClassName` field set to "alb".
 
 ## Deploy an example application
 
@@ -80,7 +80,6 @@ kind: Ingress
 metadata:
   annotations:
     alb.ingress.kubernetes.io/scheme: internet-facing
-    kubernetes.io/ingress.class: alb
   name: echoserver
 spec:
   ingressClassName: alb
@@ -120,7 +119,6 @@ metadata:
   annotations:
     alb.ingress.kubernetes.io/scheme: internet-facing
     external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
-    kubernetes.io/ingress.class: alb
   name: echoserver
 spec:
   ingressClassName: alb
@@ -159,7 +157,6 @@ metadata:
   annotations:
     alb.ingress.kubernetes.io/scheme: internet-facing
     alb.ingress.kubernetes.io/ip-address-type: dualstack
-    kubernetes.io/ingress.class: alb
   name: echoserver
 spec:
   ingressClassName: alb
diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md
index d5e7ad92b4..f77f8017f2 100644
--- a/docs/tutorials/aws.md
+++ b/docs/tutorials/aws.md
@@ -739,9 +739,8 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller.
 spec:
+  ingressClassName: nginx
   rules:
     - host: server.example.com
       http:
@@ -936,7 +935,7 @@ Running several fast polling ExternalDNS instances in a given account can easily
   * `--source=ingress --source=service` - specify multiple times for multiple sources
   * `--namespace=my-app`
   * `--label-filter=app in (my-app)`
-  * `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)` - note that this filter would apply to services too..
+  * `--ingress-class=nginx-external`
 * Limit services watched by type (not applicable to ingress or other types)
   * `--service-type-filter=LoadBalancer` default `all`
 * Limit the hosted zones considered
diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md
index 640036e461..0e85d90267 100644
--- a/docs/tutorials/azure-private-dns.md
+++ b/docs/tutorials/azure-private-dns.md
@@ -416,9 +416,8 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: nginx
 spec:
+  ingressClassName: nginx
   rules:
   - host: server.example.com
     http:
diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md
index f2c11b8c2f..5cd1223f29 100644
--- a/docs/tutorials/coredns.md
+++ b/docs/tutorials/coredns.md
@@ -198,9 +198,8 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: "nginx"
 spec:
+  ingressClassName: nginx
   rules:
   - host: nginx.example.org
     http:
diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md
index d1e93cb0f0..047c1d6b8b 100644
--- a/docs/tutorials/exoscale.md
+++ b/docs/tutorials/exoscale.md
@@ -109,9 +109,9 @@ kind: Ingress
 metadata:
   name: nginx
   annotations:
-    kubernetes.io/ingress.class: nginx
     external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }}
 spec:
+  ingressClassName: nginx
   rules:
   - host: via-ingress.example.com
     http:
diff --git a/docs/tutorials/kube-ingress-aws.md b/docs/tutorials/kube-ingress-aws.md
index bea5e56ac1..5cf37d4ec9 100644
--- a/docs/tutorials/kube-ingress-aws.md
+++ b/docs/tutorials/kube-ingress-aws.md
@@ -141,8 +141,6 @@ Create the following Ingress to expose the echoserver application to the Interne
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
-  annotations:
-    kubernetes.io/ingress.class: skipper
   name: echoserver
 spec:
   ingressClassName: skipper
@@ -181,7 +179,6 @@ kind: Ingress
 metadata:
   annotations:
     external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
-    kubernetes.io/ingress.class: skipper
   name: echoserver
 spec:
   ingressClassName: skipper
@@ -218,7 +215,6 @@ kind: Ingress
 metadata:
   annotations:
     alb.ingress.kubernetes.io/ip-address-type: dualstack
-    kubernetes.io/ingress.class: skipper
   name: echoserver
 spec:
   ingressClassName: skipper
@@ -256,7 +252,6 @@ kind: Ingress
 metadata:
   annotations:
     zalando.org/aws-load-balancer-type: nlb
-    kubernetes.io/ingress.class: skipper
   name: echoserver
 spec:
   ingressClassName: skipper
diff --git a/docs/tutorials/nginx-ingress.md b/docs/tutorials/nginx-ingress.md
index f6c170b4c4..ce79f24d92 100644
--- a/docs/tutorials/nginx-ingress.md
+++ b/docs/tutorials/nginx-ingress.md
@@ -294,8 +294,6 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: nginx
 spec:
   ingressClassName: nginx
   rules:
@@ -595,8 +593,6 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: nginx
 spec:
   ingressClassName: nginx
   rules:
diff --git a/docs/tutorials/public-private-route53.md b/docs/tutorials/public-private-route53.md
index 9a4f7be6f4..66071e13d2 100644
--- a/docs/tutorials/public-private-route53.md
+++ b/docs/tutorials/public-private-route53.md
@@ -213,7 +213,7 @@ spec:
 
 Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines.
 
-In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`:
+In ExternalDNS containers args, make sure to specify `aws-zone-type` and `ingress-class`:
 
 ```yaml
 apiVersion: apps/v1beta2
@@ -241,7 +241,7 @@ spec:
         - --provider=aws
         - --registry=txt
         - --txt-owner-id=external-dns
-        - --annotation-filter=kubernetes.io/ingress.class in (external-ingress)
+        - --ingress-class=external-ingress
         - --aws-zone-type=public
         image: registry.k8s.io/external-dns/external-dns:v0.13.4
         name: external-dns-public
@@ -251,7 +251,7 @@ spec:
 
 Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines.
 
-In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`:
+In ExternalDNS containers args, make sure to specify `aws-zone-type` and `ingress-class`:
 
 ```yaml
 apiVersion: apps/v1beta2
@@ -279,7 +279,7 @@ spec:
         - --provider=aws
         - --registry=txt
         - --txt-owner-id=dev.k8s.nexus
-        - --annotation-filter=kubernetes.io/ingress.class in (internal-ingress)
+        - --ingress-class=internal-ingress
         - --aws-zone-type=private
         image: registry.k8s.io/external-dns/external-dns:v0.13.4
         name: external-dns-private
@@ -287,20 +287,19 @@ spec:
 
 ## Create application Service definitions
 
-For this setup to work, you've to create two Service definitions for your application.
+For this setup to work, you need to create two Ingress definitions for your application.
 
-At first, create public Service definition:
+At first, create a public Ingress definition:
 
 ```yaml
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
-  annotations:
-    kubernetes.io/ingress.class: "external-ingress"
   labels:
     app: app
   name: app-public
 spec:
+  ingressClassName: external-ingress
   rules:
   - host: app.domain.com
     http:
@@ -313,18 +312,17 @@ spec:
         pathType: Prefix
 ```
 
-Then create private Service definition:
+Then create a private Ingress definition:
 
 ```yaml
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
-  annotations:
-    kubernetes.io/ingress.class: "internal-ingress"
   labels:
     app: app
   name: app-private
 spec:
+  ingressClassName: internal-ingress
   rules:
   - host: app.domain.com
     http:
@@ -347,12 +345,12 @@ metadata:
     certmanager.k8s.io/acme-challenge-type: "dns01"
     certmanager.k8s.io/acme-dns01-provider: "route53"
     certmanager.k8s.io/cluster-issuer: "letsencrypt-production"
-    kubernetes.io/ingress.class: "external-ingress"
     kubernetes.io/tls-acme: "true"
   labels:
     app: app
   name: app-public
 spec:
+  ingressClassName: "external-ingress"
   rules:
   - host: app.domain.com
     http:
@@ -375,12 +373,11 @@ And reuse the requested certificate in private Service definition:
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
-  annotations:
-    kubernetes.io/ingress.class: "internal-ingress"
   labels:
     app: app
   name: app-private
 spec:
+  ingressClassName: "internal-ingress"
   rules:
   - host: app.domain.com
     http:
diff --git a/docs/tutorials/rdns.md b/docs/tutorials/rdns.md
index 684c7c64d5..529b1765c2 100644
--- a/docs/tutorials/rdns.md
+++ b/docs/tutorials/rdns.md
@@ -142,9 +142,8 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: nginx
-  annotations:
-    kubernetes.io/ingress.class: "nginx"
 spec:
+  ingressClassName: nginx
   rules:
   - host: nginx.lb.rancher.cloud
     http:
diff --git a/main.go b/main.go
index 960045ce98..cc2e08bf4e 100644
--- a/main.go
+++ b/main.go
@@ -114,6 +114,7 @@ func main() {
 		Namespace:                      cfg.Namespace,
 		AnnotationFilter:               cfg.AnnotationFilter,
 		LabelFilter:                    labelSelector,
+		IngressClassNames:              cfg.IngressClassNames,
 		FQDNTemplate:                   cfg.FQDNTemplate,
 		CombineFQDNAndAnnotation:       cfg.CombineFQDNAndAnnotation,
 		IgnoreHostnameAnnotation:       cfg.IgnoreHostnameAnnotation,
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index eefd2b2bf0..faa4b88cba 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -54,6 +54,7 @@ type Config struct {
 	Namespace                          string
 	AnnotationFilter                   string
 	LabelFilter                        string
+	IngressClassNames                  []string
 	FQDNTemplate                       string
 	CombineFQDNAndAnnotation           bool
 	IgnoreHostnameAnnotation           bool
@@ -216,6 +217,7 @@ var defaultConfig = &Config{
 	Namespace:                   "",
 	AnnotationFilter:            "",
 	LabelFilter:                 labels.Everything().String(),
+	IngressClassNames:           nil,
 	FQDNTemplate:                "",
 	CombineFQDNAndAnnotation:    false,
 	IgnoreHostnameAnnotation:    false,
@@ -413,6 +415,7 @@ func (cfg *Config) ParseFlags(args []string) error {
 	app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
 	app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
 	app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently supported by source types CRD, ingress, service and openshift-route").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
+	app.Flag("ingress-class", "Require an ingress to have this class name (defaults to any class; specify multiple times to allow more than one class)").StringsVar(&cfg.IngressClassNames)
 	app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
 	app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
 	app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
diff --git a/source/ingress.go b/source/ingress.go
index 76d5429e7f..af7a9dc990 100644
--- a/source/ingress.go
+++ b/source/ingress.go
@@ -18,6 +18,7 @@ package source
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"sort"
 	"strings"
@@ -26,6 +27,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	networkv1 "k8s.io/api/networking/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/selection"
 	kubeinformers "k8s.io/client-go/informers"
 	netinformers "k8s.io/client-go/informers/networking/v1"
 	"k8s.io/client-go/kubernetes"
@@ -43,6 +45,8 @@ const (
 	// Possible values for the ingress-hostname-source annotation
 	IngressHostnameSourceAnnotationOnlyValue   = "annotation-only"
 	IngressHostnameSourceDefinedHostsOnlyValue = "defined-hosts-only"
+
+	IngressClassAnnotationKey = "kubernetes.io/ingress.class"
 )
 
 // ingressSource is an implementation of Source for Kubernetes ingress objects.
@@ -53,6 +57,7 @@ type ingressSource struct {
 	client                   kubernetes.Interface
 	namespace                string
 	annotationFilter         string
+	ingressClassNames        []string
 	fqdnTemplate             *template.Template
 	combineFQDNAnnotation    bool
 	ignoreHostnameAnnotation bool
@@ -63,12 +68,27 @@ type ingressSource struct {
 }
 
 // NewIngressSource creates a new ingressSource with the given config.
-func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector) (Source, error) {
+func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) {
 	tmpl, err := parseTemplate(fqdnTemplate)
 	if err != nil {
 		return nil, err
 	}
 
+	// ensure that ingress class is only set in either the ingressClassNames or
+	// annotationFilter but not both
+	if ingressClassNames != nil && annotationFilter != "" {
+		selector, err := getLabelSelector(annotationFilter)
+		if err != nil {
+			return nil, err
+		}
+
+		requirements, _ := selector.Requirements()
+		for _, requirement := range requirements {
+			if requirement.Key() == "kubernetes.io/ingress.class" {
+				return nil, errors.New("--ingress-class is mutually exclusive with the kubernetes.io/ingress.class annotation filter")
+			}
+		}
+	}
 	// Use shared informer to listen for add/update/delete of ingresses in the specified namespace.
 	// Set resync period to 0, to prevent processing when nothing has changed.
 	informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
@@ -93,6 +113,7 @@ func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, name
 		client:                   kubeClient,
 		namespace:                namespace,
 		annotationFilter:         annotationFilter,
+		ingressClassNames:        ingressClassNames,
 		fqdnTemplate:             tmpl,
 		combineFQDNAnnotation:    combineFqdnAnnotation,
 		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
@@ -116,6 +137,11 @@ func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
 		return nil, err
 	}
 
+	ingresses, err = sc.filterByIngressClass(ingresses)
+	if err != nil {
+		return nil, err
+	}
+
 	endpoints := []*endpoint.Endpoint{}
 
 	for _, ing := range ingresses {
@@ -210,6 +236,50 @@ func (sc *ingressSource) filterByAnnotations(ingresses []*networkv1.Ingress) ([]
 	return filteredList, nil
 }
 
+// filterByIngressClass filters a list of ingresses based on a required ingress
+// class
+func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) {
+	// if no class filter is specified then there's nothing to do
+	if len(sc.ingressClassNames) == 0 {
+		return ingresses, nil
+	}
+
+	classNameReq, err := labels.NewRequirement(IngressClassAnnotationKey, selection.In, sc.ingressClassNames)
+	if err != nil {
+		return nil, err
+	}
+
+	selector := labels.NewSelector()
+	selector = selector.Add(*classNameReq)
+
+	filteredList := []*networkv1.Ingress{}
+
+	for _, ingress := range ingresses {
+		var matched = false
+
+		for _, nameFilter := range sc.ingressClassNames {
+			if ingress.Spec.IngressClassName != nil && len(*ingress.Spec.IngressClassName) > 0 {
+				if nameFilter == *ingress.Spec.IngressClassName {
+					matched = true
+				}
+			} else if matchLabelSelector(selector, ingress.Annotations) {
+				matched = true
+			}
+
+			if matched {
+				filteredList = append(filteredList, ingress)
+				break
+			}
+		}
+
+		if !matched {
+			log.Debugf("Discarding ingress %s/%s because it does not match required ingress classes %v", ingress.Namespace, ingress.Name, sc.ingressClassNames)
+		}
+	}
+
+	return filteredList, nil
+}
+
 func (sc *ingressSource) setResourceLabel(ingress *networkv1.Ingress, endpoints []*endpoint.Endpoint) {
 	for _, ep := range endpoints {
 		ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingress/%s/%s", ingress.Namespace, ingress.Name)
diff --git a/source/store.go b/source/store.go
index d22e16ff87..ef4718e3b2 100644
--- a/source/store.go
+++ b/source/store.go
@@ -46,6 +46,7 @@ type Config struct {
 	Namespace                      string
 	AnnotationFilter               string
 	LabelFilter                    labels.Selector
+	IngressClassNames              []string
 	FQDNTemplate                   string
 	CombineFQDNAndAnnotation       bool
 	IgnoreHostnameAnnotation       bool
@@ -222,7 +223,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
 		if err != nil {
 			return nil, err
 		}
-		return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter)
+		return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames)
 	case "pod":
 		client, err := p.KubeClient()
 		if err != nil {


Test patch:
diff --git a/source/ingress_test.go b/source/ingress_test.go
index cdd23505a7..2be3eddfd0 100644
--- a/source/ingress_test.go
+++ b/source/ingress_test.go
@@ -65,6 +65,7 @@ func (suite *IngressSuite) SetupTest() {
 		false,
 		false,
 		labels.Everything(),
+		[]string{},
 	)
 	suite.NoError(err, "should initialize ingress source")
 }
@@ -101,6 +102,7 @@ func TestNewIngressSource(t *testing.T) {
 		fqdnTemplate             string
 		combineFQDNAndAnnotation bool
 		expectError              bool
+		ingressClassNames        []string
 	}{
 		{
 			title:        "invalid template",
@@ -132,6 +134,17 @@ func TestNewIngressSource(t *testing.T) {
 			expectError:      false,
 			annotationFilter: "kubernetes.io/ingress.class=nginx",
 		},
+		{
+			title:             "non-empty ingress class name list",
+			expectError:       false,
+			ingressClassNames: []string{"internal", "external"},
+		},
+		{
+			title:             "ingress class name and annotation filter jointly specified",
+			expectError:       true,
+			ingressClassNames: []string{"internal", "external"},
+			annotationFilter:  "kubernetes.io/ingress.class=nginx",
+		},
 	} {
 		ti := ti
 		t.Run(ti.title, func(t *testing.T) {
@@ -148,6 +161,7 @@ func TestNewIngressSource(t *testing.T) {
 				false,
 				false,
 				labels.Everything(),
+				ti.ingressClassNames,
 			)
 			if ti.expectError {
 				assert.Error(t, err)
@@ -374,6 +388,7 @@ func testIngressEndpoints(t *testing.T) {
 		ignoreIngressTLSSpec     bool
 		ignoreIngressRulesSpec   bool
 		ingressLabelSelector     labels.Selector
+		ingressClassNames        []string
 	}{
 		{
 			title:           "no ingress",
@@ -1220,6 +1235,116 @@ func testIngressEndpoints(t *testing.T) {
 				},
 			},
 		},
+		{
+			title:             "ingressClassName filtering",
+			targetNamespace:   "",
+			ingressClassNames: []string{"public", "dmz"},
+			ingressItems: []fakeIngress{
+				{
+					name:        "none",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"none.example.org"}},
+					ips:         []string{"1.0.0.0"},
+				},
+				{
+					name:             "fake-public",
+					namespace:        namespace,
+					tlsdnsnames:      [][]string{{"example.org"}},
+					ips:              []string{"1.2.3.4"},
+					ingressClassName: "public", // match
+				},
+				{
+					name:             "fake-internal",
+					namespace:        namespace,
+					tlsdnsnames:      [][]string{{"int.example.org"}},
+					ips:              []string{"2.3.4.5"},
+					ingressClassName: "internal",
+				},
+				{
+					name:             "fake-dmz",
+					namespace:        namespace,
+					tlsdnsnames:      [][]string{{"dmz.example.org"}},
+					ips:              []string{"3.4.5.6"},
+					ingressClassName: "dmz", // match
+				},
+				{
+					name:        "annotated-dmz",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"annodmz.example.org"}},
+					ips:         []string{"4.5.6.7"},
+					annotations: map[string]string{
+						"kubernetes.io/ingress.class": "dmz", // match
+					},
+				},
+				{
+					name:        "fake-internal-annotated-dmz",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"int-annodmz.example.org"}},
+					ips:         []string{"5.6.7.8"},
+					annotations: map[string]string{
+						"kubernetes.io/ingress.class": "dmz", // match but ignored (non-empty ingressClassName)
+					},
+					ingressClassName: "internal",
+				},
+				{
+					name:        "fake-dmz-annotated-internal",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"dmz-annoint.example.org"}},
+					ips:         []string{"6.7.8.9"},
+					annotations: map[string]string{
+						"kubernetes.io/ingress.class": "internal",
+					},
+					ingressClassName: "dmz", // match
+				},
+				{
+					name:        "empty-annotated-dmz",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"empty-annotdmz.example.org"}},
+					ips:         []string{"7.8.9.0"},
+					annotations: map[string]string{
+						"kubernetes.io/ingress.class": "dmz", // match (empty ingressClassName)
+					},
+					ingressClassName: "",
+				},
+				{
+					name:        "empty-annotated-internal",
+					namespace:   namespace,
+					tlsdnsnames: [][]string{{"empty-annotint.example.org"}},
+					ips:         []string{"8.9.0.1"},
+					annotations: map[string]string{
+						"kubernetes.io/ingress.class": "internal",
+					},
+					ingressClassName: "",
+				},
+			},
+			expected: []*endpoint.Endpoint{
+				{
+					DNSName:    "example.org",
+					RecordType: endpoint.RecordTypeA,
+					Targets:    endpoint.Targets{"1.2.3.4"},
+				},
+				{
+					DNSName:    "dmz.example.org",
+					RecordType: endpoint.RecordTypeA,
+					Targets:    endpoint.Targets{"3.4.5.6"},
+				},
+				{
+					DNSName:    "annodmz.example.org",
+					RecordType: endpoint.RecordTypeA,
+					Targets:    endpoint.Targets{"4.5.6.7"},
+				},
+				{
+					DNSName:    "dmz-annoint.example.org",
+					RecordType: endpoint.RecordTypeA,
+					Targets:    endpoint.Targets{"6.7.8.9"},
+				},
+				{
+					DNSName:    "empty-annotdmz.example.org",
+					RecordType: endpoint.RecordTypeA,
+					Targets:    endpoint.Targets{"7.8.9.0"},
+				},
+			},
+		},
 		{
 			ingressLabelSelector: labels.SelectorFromSet(labels.Set{"app": "web-external"}),
 			title:                "ingress with matching labels",
@@ -1283,6 +1408,7 @@ func testIngressEndpoints(t *testing.T) {
 				ti.ignoreIngressTLSSpec,
 				ti.ignoreIngressRulesSpec,
 				ti.ingressLabelSelector,
+				ti.ingressClassNames,
 			)
 			// Informer cache has all of the ingresses. Retrieve and validate their endpoints.
 			res, err := source.Endpoints(context.Background())
@@ -1298,14 +1424,15 @@ func testIngressEndpoints(t *testing.T) {
 
 // ingress specific helper functions
 type fakeIngress struct {
-	dnsnames    []string
-	tlsdnsnames [][]string
-	ips         []string
-	hostnames   []string
-	namespace   string
-	name        string
-	annotations map[string]string
-	labels      map[string]string
+	dnsnames         []string
+	tlsdnsnames      [][]string
+	ips              []string
+	hostnames        []string
+	namespace        string
+	name             string
+	annotations      map[string]string
+	labels           map[string]string
+	ingressClassName string
 }
 
 func (ing fakeIngress) Ingress() *networkv1.Ingress {
@@ -1317,7 +1444,8 @@ func (ing fakeIngress) Ingress() *networkv1.Ingress {
 			Labels:      ing.labels,
 		},
 		Spec: networkv1.IngressSpec{
-			Rules: []networkv1.IngressRule{},
+			Rules:            []networkv1.IngressRule{},
+			IngressClassName: &ing.ingressClassName,
 		},
 		Status: networkv1.IngressStatus{
 			LoadBalancer: networkv1.IngressLoadBalancerStatus{