Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
102 changes: 98 additions & 4 deletions docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,108 @@ If the annotation is not present, use the domains from both the spec and annotat

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

This annotation allows ExternalDNS to work with Istio Gateways that don't have a public IP.
This annotation allows ExternalDNS to work with Istio/GlooEdge Gateways that don't have a public IP.

It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to the Istio Gateway:
It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to the Istio/GlooEdge Gateway:

- **The Challenge**: By default, ExternalDNS sources the public IP address for a DNS record from a Service of type LoadBalancer.
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.
However, in some setups, the 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.
- **The Solution**: The annotation on the Istio/GlooEdge 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Author

@cucxabong cucxabong Oct 17, 2025

Choose a reason for hiding this comment

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

I added our use case


### Use Cases for `external-dns.alpha.kubernetes.io/ingress` annotation

#### Getting target from Ingress backed Gloo Gateway

```yml
apiVersion: gateway.solo.io/v1
kind: Gateway
metadata:
annotations:
external-dns.alpha.kubernetes.io/ingress: gateway-proxy
labels:
app: gloo
name: gateway-proxy
namespace: gloo-system
spec:
bindAddress: '::'
bindPort: 8080
options: {}
proxyNames:
- gateway-proxy
ssl: false
useProxyProto: false
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway-proxy
namespace: gloo-system
spec:
ingressClassName: alb
rules:
- host: cool-service.example.com
http:
paths:
- backend:
service:
name: gateway-proxy
port:
name: http
path: /
pathType: Prefix
status:
loadBalancer:
ingress:
- hostname: k8s-alb-c4aa37c880-740590208.us-east-1.elb.amazonaws.com
---
# This object is generated by GlooEdge Control Plane from Gateway and VirtualService.
# We have no direct control on this resource
apiVersion: gloo.solo.io/v1
kind: Proxy
metadata:
labels:
created_by: gloo-gateway
name: gateway-proxy
namespace: gloo-system
spec:
listeners:
- bindAddress: '::'
bindPort: 8080
httpListener:
virtualHosts:
- domains:
- cool-service.example.com
metadataStatic:
sources:
- observedGeneration: "6652"
resourceKind: '*v1.VirtualService'
resourceRef:
name: cool-service
namespace: gloo-system
name: cool-service
routes:
- matchers:
- prefix: /
metadataStatic:
sources:
- observedGeneration: "6652"
resourceKind: '*v1.VirtualService'
resourceRef:
name: cool-service
namespace: gloo-system
upgrades:
- websocket: {}
metadataStatic:
sources:
- observedGeneration: "6111"
resourceKind: '*v1.Gateway'
resourceRef:
name: gateway-proxy
namespace: gloo-system
name: listener-::-8080
useProxyProto: false
```

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

Expand Down
49 changes: 49 additions & 0 deletions docs/sources/gloo-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,52 @@ spec:
- --registry=txt
- --txt-owner-id=my-identifier
```

## Gateway Annotation

To support setups where an Ingress resource is used provision an external LB you can add the following annotation to your Gateway

**Note:** The Ingress namespace can be omitted if its in the same namespace as the gateway

```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: gloo.solo.io/v1
kind: Proxy
metadata:
labels:
created_by: gloo-gateway
name: gateway-proxy
namespace: gloo-system
spec:
listeners:
- bindAddress: '::'
metadataStatic:
sources:
- resourceKind: '*v1.Gateway'
resourceRef:
name: gateway-proxy
namespace: gloo-system
---
apiVersion: gateway.solo.io/v1
kind: Gateway
metadata:
annotations:
external-dns.alpha.kubernetes.io/ingress: "$ingressNamespace/$ingressName"
labels:
app: gloo
name: gateway-proxy
namespace: gloo-system
spec: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
labels:
gateway-proxy-id: gateway-proxy
gloo: gateway-proxy
name: gateway-proxy
namespace: gloo-system
spec:
ingressClassName: alb
EOF
```
76 changes: 75 additions & 1 deletion source/gloo_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import (
"sigs.k8s.io/external-dns/source/annotations"
)

// GlooGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
// instead of a standard LoadBalancer service type
const GlooGatewayIngressSource = annotations.Ingress

var (
proxyGVR = schema.GroupVersionResource{
Group: "gloo.solo.io",
Expand All @@ -44,6 +48,11 @@ var (
Version: "v1",
Resource: "virtualservices",
}
gatewayGVR = schema.GroupVersionResource{
Group: "gateway.solo.io",
Version: "v1",
Resource: "gateways",
}
)

// Basic redefinition of "Proxy" CRD : https://github.com/solo-io/gloo/blob/v1.4.6/projects/gloo/pkg/api/v1/proxy.pb.go
Expand All @@ -58,7 +67,22 @@ type proxySpec struct {
}

type proxySpecListener struct {
HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"`
HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"`
MetadataStatic proxyMetadataStatic `json:"metadataStatic,omitempty"`
}

type proxyMetadataStatic struct {
Source []proxyMetadataStaticSource `json:"sources,omitempty"`
}

type proxyMetadataStaticSource struct {
ResourceKind string `json:"resourceKind,omitempty"`
ResourceRef proxyMetadataStaticSourceResourceRef `json:"resourceRef,omitempty"`
}

type proxyMetadataStaticSourceResourceRef struct {
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
}

type proxySpecHTTPListener struct {
Expand Down Expand Up @@ -136,6 +160,14 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name)

proxyTargets := annotations.TargetsFromTargetAnnotation(proxy.Metadata.Annotations)
if len(proxyTargets) == 0 {
proxyTargets, err = gs.targetsFromGatewayIngress(ctx, &proxy)
if err != nil {
log.Error(err)
return nil, err
}
}

if len(proxyTargets) == 0 {
proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
if err != nil {
Expand Down Expand Up @@ -228,6 +260,48 @@ func (gs *glooSource) proxyTargets(ctx context.Context, name string, namespace s
return targets, nil
}

func (gs *glooSource) targetsFromGatewayIngress(ctx context.Context, proxy *proxy) (endpoint.Targets, error) {
targets := make(endpoint.Targets, 0)

for _, listener := range proxy.Spec.Listeners {
for _, source := range listener.MetadataStatic.Source {
if source.ResourceKind != "*v1.Gateway" {
log.Debugf("Unsupported listener source. Expecting '*v1.Gateway', got (%s)", source.ResourceKind)
continue
}
gateway, err := gs.dynamicKubeClient.Resource(gatewayGVR).Namespace(source.ResourceRef.Namespace).Get(ctx, source.ResourceRef.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
ingressStr, ok := gateway.GetAnnotations()[GlooGatewayIngressSource]
if ok && ingressStr != "" {
namespace, name, err := ParseIngress(ingressStr)
if err != nil {
return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.GetNamespace(), gateway.GetName(), err)
}
if namespace == "" {
namespace = gateway.GetNamespace()
}

ingress, err := gs.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
log.Error(err)
return nil, err
}

for _, lb := range ingress.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
} else if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
}
}
return targets, nil
}

func sourceKind(kind string) *schema.GroupVersionResource {
if kind == "*v1.VirtualService" {
return &virtualServiceGVR
Expand Down
Loading
Loading