Skip to content

Commit 1ffb257

Browse files
committed
feat: add support for ingress backed GlooEdge Gateway
1 parent 6e9d459 commit 1ffb257

File tree

4 files changed

+344
-20
lines changed

4 files changed

+344
-20
lines changed

docs/annotations/annotations.md

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,108 @@ If the annotation is not present, use the domains from both the spec and annotat
150150

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

153-
This annotation allows ExternalDNS to work with Istio Gateways that don't have a public IP.
153+
This annotation allows ExternalDNS to work with Istio & GlooEdge Gateways that don't have a public IP.
154154

155-
It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to the Istio Gateway:
155+
It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to an Istio or GlooEdge Gateway:
156156

157157
- **The Challenge**: By default, ExternalDNS sources the public IP address for a DNS record from a Service of type LoadBalancer.
158-
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.
158+
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.
159159

160-
- **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.
160+
- **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.
161+
162+
### Use Cases for `external-dns.alpha.kubernetes.io/ingress` annotation
163+
164+
#### Getting target from Ingress backed Gloo Gateway
165+
166+
```yml
167+
apiVersion: gateway.solo.io/v1
168+
kind: Gateway
169+
metadata:
170+
annotations:
171+
external-dns.alpha.kubernetes.io/ingress: gateway-proxy
172+
labels:
173+
app: gloo
174+
name: gateway-proxy
175+
namespace: gloo-system
176+
spec:
177+
bindAddress: '::'
178+
bindPort: 8080
179+
options: {}
180+
proxyNames:
181+
- gateway-proxy
182+
ssl: false
183+
useProxyProto: false
184+
---
185+
apiVersion: networking.k8s.io/v1
186+
kind: Ingress
187+
metadata:
188+
name: gateway-proxy
189+
namespace: gloo-system
190+
spec:
191+
ingressClassName: alb
192+
rules:
193+
- host: cool-service.example.com
194+
http:
195+
paths:
196+
- backend:
197+
service:
198+
name: gateway-proxy
199+
port:
200+
name: http
201+
path: /
202+
pathType: Prefix
203+
status:
204+
loadBalancer:
205+
ingress:
206+
- hostname: k8s-alb-c4aa37c880-740590208.us-east-1.elb.amazonaws.com
207+
---
208+
# This object is generated by GlooEdge Control Plane from Gateway and VirtualService.
209+
# We have no direct control on this resource
210+
apiVersion: gloo.solo.io/v1
211+
kind: Proxy
212+
metadata:
213+
labels:
214+
created_by: gloo-gateway
215+
name: gateway-proxy
216+
namespace: gloo-system
217+
spec:
218+
listeners:
219+
- bindAddress: '::'
220+
bindPort: 8080
221+
httpListener:
222+
virtualHosts:
223+
- domains:
224+
- cool-service.example.com
225+
metadataStatic:
226+
sources:
227+
- observedGeneration: "6652"
228+
resourceKind: '*v1.VirtualService'
229+
resourceRef:
230+
name: cool-service
231+
namespace: gloo-system
232+
name: cool-service
233+
routes:
234+
- matchers:
235+
- prefix: /
236+
metadataStatic:
237+
sources:
238+
- observedGeneration: "6652"
239+
resourceKind: '*v1.VirtualService'
240+
resourceRef:
241+
name: cool-service
242+
namespace: gloo-system
243+
upgrades:
244+
- websocket: {}
245+
metadataStatic:
246+
sources:
247+
- observedGeneration: "6111"
248+
resourceKind: '*v1.Gateway'
249+
resourceRef:
250+
name: gateway-proxy
251+
namespace: gloo-system
252+
name: listener-::-8080
253+
useProxyProto: false
254+
```
161255

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

docs/sources/gloo-proxy.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,52 @@ spec:
104104
- --registry=txt
105105
- --txt-owner-id=my-identifier
106106
```
107+
108+
## Gateway Annotation
109+
110+
To support setups where an Ingress resource is used provision an external LB you can add the following annotation to your Gateway
111+
112+
**Note:** The Ingress namespace can be omitted if its in the same namespace as the gateway
113+
114+
```bash
115+
$ cat <<EOF | kubectl apply -f -
116+
apiVersion: gloo.solo.io/v1
117+
kind: Proxy
118+
metadata:
119+
labels:
120+
created_by: gloo-gateway
121+
name: gateway-proxy
122+
namespace: gloo-system
123+
spec:
124+
listeners:
125+
- bindAddress: '::'
126+
metadataStatic:
127+
sources:
128+
- resourceKind: '*v1.Gateway'
129+
resourceRef:
130+
name: gateway-proxy
131+
namespace: gloo-system
132+
---
133+
apiVersion: gateway.solo.io/v1
134+
kind: Gateway
135+
metadata:
136+
annotations:
137+
external-dns.alpha.kubernetes.io/ingress: "$ingressNamespace/$ingressName"
138+
labels:
139+
app: gloo
140+
name: gateway-proxy
141+
namespace: gloo-system
142+
spec: {}
143+
---
144+
apiVersion: networking.k8s.io/v1
145+
kind: Ingress
146+
metadata:
147+
labels:
148+
gateway-proxy-id: gateway-proxy
149+
gloo: gateway-proxy
150+
name: gateway-proxy
151+
namespace: gloo-system
152+
spec:
153+
ingressClassName: alb
154+
EOF
155+
```

source/gloo_proxy.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import (
3333
"sigs.k8s.io/external-dns/source/annotations"
3434
)
3535

36+
// GlooGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
37+
// instead of a standard LoadBalancer service type
38+
const GlooGatewayIngressSource = annotations.Ingress
39+
3640
var (
3741
proxyGVR = schema.GroupVersionResource{
3842
Group: "gloo.solo.io",
@@ -44,6 +48,11 @@ var (
4448
Version: "v1",
4549
Resource: "virtualservices",
4650
}
51+
gatewayGVR = schema.GroupVersionResource{
52+
Group: "gateway.solo.io",
53+
Version: "v1",
54+
Resource: "gateways",
55+
}
4756
)
4857

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

6069
type proxySpecListener struct {
61-
HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"`
70+
HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"`
71+
MetadataStatic proxyMetadataStatic `json:"metadataStatic,omitempty"`
72+
}
73+
74+
type proxyMetadataStatic struct {
75+
Source []proxyMetadataStaticSource `json:"sources,omitempty"`
76+
}
77+
78+
type proxyMetadataStaticSource struct {
79+
ResourceKind string `json:"resourceKind,omitempty"`
80+
ResourceRef proxyMetadataStaticSourceResourceRef `json:"resourceRef,omitempty"`
81+
}
82+
83+
type proxyMetadataStaticSourceResourceRef struct {
84+
Name string `json:"name,omitempty"`
85+
Namespace string `json:"namespace,omitempty"`
6286
}
6387

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

138162
proxyTargets := annotations.TargetsFromTargetAnnotation(proxy.Metadata.Annotations)
163+
if len(proxyTargets) == 0 {
164+
proxyTargets, err = gs.targetsFromGatewayIngress(ctx, &proxy)
165+
if err != nil {
166+
log.Error(err)
167+
return nil, err
168+
}
169+
}
170+
139171
if len(proxyTargets) == 0 {
140172
proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
141173
if err != nil {
@@ -228,6 +260,48 @@ func (gs *glooSource) proxyTargets(ctx context.Context, name string, namespace s
228260
return targets, nil
229261
}
230262

263+
func (gs *glooSource) targetsFromGatewayIngress(ctx context.Context, proxy *proxy) (endpoint.Targets, error) {
264+
targets := make(endpoint.Targets, 0)
265+
266+
for _, listener := range proxy.Spec.Listeners {
267+
for _, source := range listener.MetadataStatic.Source {
268+
if source.ResourceKind != "*v1.Gateway" {
269+
log.Debugf("Unsupported listener source. Expecting '*v1.Gateway', got (%s)", source.ResourceKind)
270+
continue
271+
}
272+
gateway, err := gs.dynamicKubeClient.Resource(gatewayGVR).Namespace(source.ResourceRef.Namespace).Get(ctx, source.ResourceRef.Name, metav1.GetOptions{})
273+
if err != nil {
274+
return nil, err
275+
}
276+
ingressStr, ok := gateway.GetAnnotations()[GlooGatewayIngressSource]
277+
if ok && ingressStr != "" {
278+
namespace, name, err := ParseIngress(ingressStr)
279+
if err != nil {
280+
return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.GetNamespace(), gateway.GetName(), err)
281+
}
282+
if namespace == "" {
283+
namespace = gateway.GetNamespace()
284+
}
285+
286+
ingress, err := gs.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
287+
if err != nil {
288+
log.Error(err)
289+
return nil, err
290+
}
291+
292+
for _, lb := range ingress.Status.LoadBalancer.Ingress {
293+
if lb.IP != "" {
294+
targets = append(targets, lb.IP)
295+
} else if lb.Hostname != "" {
296+
targets = append(targets, lb.Hostname)
297+
}
298+
}
299+
}
300+
}
301+
}
302+
return targets, nil
303+
}
304+
231305
func sourceKind(kind string) *schema.GroupVersionResource {
232306
if kind == "*v1.VirtualService" {
233307
return &virtualServiceGVR

0 commit comments

Comments
 (0)