Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions charts/external-dns/tests/rbac_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,97 @@ tests:
- isKind:
of: RoleBinding
template: clusterrolebinding.yaml

- it: should create only Role when namespaced=true for istio sources
set:
namespaced: true
sources:
- istio-virtualservice
- istio-gateway
asserts:
- isKind:
of: Role
template: clusterrole.yaml
- isKind:
of: RoleBinding
template: clusterrolebinding.yaml
- equal:
path: rules
value:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
- apiGroups: ["networking.istio.io"]
resources: ["virtualservices"]
verbs: ["get","watch","list"]
template: clusterrole.yaml

- it: should create only ClusterRole for istio-gateway with ingress permissions
set:
namespaced: false
sources:
- istio-gateway
asserts:
- isKind:
of: ClusterRole
template: clusterrole.yaml
- isKind:
of: ClusterRoleBinding
template: clusterrolebinding.yaml
- equal:
path: rules
value:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
template: clusterrole.yaml

- it: should create only ClusterRole for istio-virtualservice with ingress permissions required
set:
namespaced: false
sources:
- istio-virtualservice
asserts:
- isKind:
of: ClusterRole
template: clusterrole.yaml
- isKind:
of: ClusterRoleBinding
template: clusterrolebinding.yaml
- equal:
path: rules
value:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
- apiGroups: ["networking.istio.io"]
resources: ["virtualservices"]
verbs: ["get","watch","list"]
template: clusterrole.yaml
50 changes: 31 additions & 19 deletions docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ ExternalDNS sources support a number of annotations on the Kubernetes resources

The following table documents which sources support which annotations:

| Source | controller | hostname | internal-hostname | target | ttl | (provider-specific) |
|--------------|------------|----------|-------------------|---------|---------|---------------------|
| Ambassador | | | | Yes | Yes | Yes |
| Connector | | | | | | |
| Contour | Yes | Yes[^1] | | Yes | Yes | Yes |
| CloudFoundry | | | | | | |
| CRD | | | | | | |
| F5 | | | | Yes | Yes | |
| Gateway | Yes | Yes[^1] | | Yes[^4] | Yes | Yes |
| Gloo | | | | Yes | Yes[^5] | Yes[^5] |
| Ingress | Yes | Yes[^1] | | Yes | Yes | Yes |
| Istio | Yes | Yes[^1] | | Yes | Yes | Yes |
| Kong | | Yes[^1] | | Yes | Yes | Yes |
| Node | Yes | | | Yes | Yes | |
| OpenShift | Yes | Yes[^1] | | Yes | Yes | Yes |
| Pod | | Yes | Yes | Yes | | |
| Service | Yes | Yes[^1] | Yes[^1][^2] | Yes[^3] | Yes | Yes |
| Skipper | Yes | Yes[^1] | | Yes | Yes | Yes |
| Traefik | | Yes[^1] | | Yes | Yes | Yes |
| Source | controller | hostname | internal-hostname | target | ttl | (provider-specific) | ingress |
|--------------|------------|----------|-------------------|---------|---------|---------------------|:-------:|
| Ambassador | | | | Yes | Yes | Yes | No |
| Connector | | | | | | | No |
| Contour | Yes | Yes[^1] | | Yes | Yes | Yes | No |
| CloudFoundry | | | | | | | No |
| CRD | | | | | | | No |
| F5 | | | | Yes | Yes | | No |
| Gateway | Yes | Yes[^1] | | Yes[^4] | Yes | Yes | No |
| Gloo | | | | Yes | Yes[^5] | Yes[^5] | No |
| Ingress | Yes | Yes[^1] | | Yes | Yes | Yes | No |
| Istio | Yes | Yes[^1] | | Yes | Yes | Yes | Yes |
| Kong | | Yes[^1] | | Yes | Yes | Yes | No |
| Node | Yes | | | Yes | Yes | | No |
| OpenShift | Yes | Yes[^1] | | Yes | Yes | Yes | No |
| Pod | | Yes | Yes | Yes | | | No |
| Service | Yes | Yes[^1] | Yes[^1][^2] | Yes[^3] | Yes | Yes | No |
| Skipper | Yes | Yes[^1] | | Yes | Yes | Yes | No |
| Traefik | | Yes[^1] | | Yes | Yes | Yes | No |

[^1]: Unless the `--ignore-hostname-annotation` flag is specified.
[^2]: Only behaves differently than `hostname` for `Service`s of type `ClusterIP` or `LoadBalancer`.
Expand Down Expand Up @@ -143,6 +143,18 @@ If the value is `annotation-only`, use only the domains from the `Ingress` annot

If the annotation is not present, use the domains from both the spec and annotations.

## external-dns.alpha.kubernetes.io/ingress

This annotation allows ExternalDNS to work with Istio Gateways that don't have a public IP.
This is a common setup in modern cloud environments, like on GCP's Service Mesh, where a Kubernetes Ingress directs all public traffic to the Istio Gateway.

Is used to address a specific architectural pattern:

@mloiseleur mloiseleur Aug 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is a common setup in modern cloud environments, like on GCP's Service Mesh, where a Kubernetes Ingress directs all public traffic to the Istio Gateway.
Is used to address a specific architectural pattern:
It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to the Istio Gateway:


- **The Problem**: By default, ExternalDNS sources the public IP address for a DNS record from a Service of type LoadBalancer.
Comment thread
ivankatliarchuk marked this conversation as resolved.
Outdated
However, in some service mesh setups, the Istio Gateway's Service is of type ClusterIP, with all public traffic routed to it via a separate Kubernetes Ingress object. This setup leaves the Gateway without a public IP that ExternalDNS can discover.

- **The Solution**: The annotation on the Istio Gateway tells ExternalDNS to ignore the Gateway's Service IP. Instead, it directs ExternalDNS to a specified Ingress resource to find the target LoadBalancer IP address.

## external-dns.alpha.kubernetes.io/internal-hostname

Specifies the domain for the resource's DNS records that are for use from internal networks.
Expand Down
16 changes: 9 additions & 7 deletions source/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,21 @@ const (
SetIdentifierKey = AnnotationKeyPrefix + "set-identifier"
AliasKey = AnnotationKeyPrefix + "alias"
TargetKey = AnnotationKeyPrefix + "target"
// The annotation used for figuring out which controller is responsible
// ControllerKey The annotation used for figuring out which controller is responsible
ControllerKey = AnnotationKeyPrefix + "controller"
// The annotation used for defining the desired hostname
// HostnameKey The annotation used for defining the desired hostname
HostnameKey = AnnotationKeyPrefix + "hostname"
// The annotation used for specifying whether the public or private interface address is used
// AccessKey The annotation used for specifying whether the public or private interface address is used
AccessKey = AnnotationKeyPrefix + "access"
// The annotation used for specifying the type of endpoints to use for headless services
// EndpointsTypeKey The annotation used for specifying the type of endpoints to use for headless services
EndpointsTypeKey = AnnotationKeyPrefix + "endpoints-type"
// The annotation used to determine the source of hostnames for ingresses. This is an optional field - all
// Ingress the annotation used to determine if the gateway is implemented by an Ingress object
Ingress = AnnotationKeyPrefix + "ingress"
// IngressHostnameSourceKey The annotation used to determine the source of hostnames for ingresses. This is an optional field - all

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a new annotation to me, a new feature, not directly related a "replace kube API calls with caching and ingress informers".

Wdyt of moving the code and documentation of this new feature into a separate PR ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. This is not a new annotations, it was added here #3842

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved out documentation

// available hostname sources are used if not specified.
IngressHostnameSourceKey = AnnotationKeyPrefix + "ingress-hostname-source"
// The value of the controller annotation so that we feel responsible
// ControllerValue The value of the controller annotation so that we feel responsible
ControllerValue = "dns-controller"
// The annotation used for defining the desired hostname
// InternalHostnameKey The annotation used for defining the desired hostname
InternalHostnameKey = AnnotationKeyPrefix + "internal-hostname"
)
10 changes: 2 additions & 8 deletions source/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
kubeinformers "k8s.io/client-go/informers"
netinformers "k8s.io/client-go/informers/networking/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"

"sigs.k8s.io/external-dns/source/informers"

Expand Down Expand Up @@ -100,12 +99,7 @@ func NewIngressSource(
ingressInformer := informerFactory.Networking().V1().Ingresses()

// Add default resource event handlers to properly initialize informer.
ingressInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
},
},
)
_, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler())

informerFactory.Start(ctx.Done())

Expand Down Expand Up @@ -360,5 +354,5 @@ func (sc *ingressSource) AddEventHandler(ctx context.Context, handler func()) {

// Right now there is no way to remove event handler from informer, see:
// https://github.com/kubernetes/kubernetes/issues/79610
sc.ingressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
_, _ = sc.ingressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
}
16 changes: 11 additions & 5 deletions source/istio_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
netinformers "k8s.io/client-go/informers/networking/v1"
"k8s.io/client-go/kubernetes"

"sigs.k8s.io/external-dns/endpoint"
Expand All @@ -43,7 +44,7 @@ import (

// IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
// instead of a standard LoadBalancer service type
const IstioGatewayIngressSource = "external-dns.alpha.kubernetes.io/ingress"
const IstioGatewayIngressSource = annotations.Ingress

// gatewaySource is an implementation of Source for Istio Gateway objects.
// The gateway implementation uses the spec.servers.hosts values for the hostnames.
Expand All @@ -58,6 +59,7 @@ type gatewaySource struct {
ignoreHostnameAnnotation bool
serviceInformer coreinformers.ServiceInformer
gatewayInformer networkingv1beta1informer.GatewayInformer
ingressInformer netinformers.IngressInformer
}

// NewIstioGatewaySource creates a new gatewaySource with the given config.
Expand All @@ -82,6 +84,9 @@ func NewIstioGatewaySource(
serviceInformer := informerFactory.Core().V1().Services()
istioInformerFactory := istioinformers.NewSharedInformerFactory(istioClient, 0)
gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways()
ingressInformer := informerFactory.Networking().V1().Ingresses()

_, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler())

// Add default resource event handlers to properly initialize informer.
_, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
Expand Down Expand Up @@ -117,6 +122,7 @@ func NewIstioGatewaySource(
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
serviceInformer: serviceInformer,
gatewayInformer: gatewayInformer,
ingressInformer: ingressInformer,
}, nil
}

Expand Down Expand Up @@ -196,7 +202,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
}

// AddEventHandler adds an event handler that should be triggered if the watched Istio Gateway changes.
func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
func (sc *gatewaySource) AddEventHandler(_ context.Context, handler func()) {
log.Debug("Adding event handler for Istio Gateway")

_, _ = sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
Expand Down Expand Up @@ -226,7 +232,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1beta1.Gatew
return filteredList, nil
}

func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) {
func (sc *gatewaySource) targetsFromIngress(ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) {
namespace, name, err := ParseIngress(ingressStr)
if err != nil {
return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
Expand All @@ -237,7 +243,7 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri

targets := make(endpoint.Targets, 0)

ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
ingress, err := sc.ingressInformer.Lister().Ingresses(namespace).Get(name)
if err != nil {
log.Error(err)
return nil, err
Expand All @@ -260,7 +266,7 @@ func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networ

ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
if ok && ingressStr != "" {
return sc.targetsFromIngress(ctx, ingressStr, gateway)
return sc.targetsFromIngress(ingressStr, gateway)
}

return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
Expand Down
6 changes: 5 additions & 1 deletion source/istio_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,7 @@ func testGatewayEndpoints(t *testing.T) {
t.Parallel()

fakeKubernetesClient := fake.NewClientset()
targetNamespace := ti.targetNamespace

for _, lb := range ti.lbServices {
service := lb.Service()
Expand All @@ -1490,6 +1491,9 @@ func testGatewayEndpoints(t *testing.T) {

for _, ing := range ti.ingresses {
ingress := ing.Ingress()
if ingress.Namespace != targetNamespace {
targetNamespace = v1.NamespaceAll
}
_, err := fakeKubernetesClient.NetworkingV1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{})
require.NoError(t, err)
}
Expand All @@ -1505,7 +1509,7 @@ func testGatewayEndpoints(t *testing.T) {
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
ti.targetNamespace,
targetNamespace,
ti.annotationFilter,
ti.fqdnTemplate,
ti.combineFQDNAndAnnotation,
Expand Down
Loading
Loading