Skip to content

Commit 4a62691

Browse files
committed
Honour annotations specified on Gateway resources.
1 parent 1e5efd0 commit 4a62691

File tree

3 files changed

+257
-81
lines changed

3 files changed

+257
-81
lines changed

source/gateway.go

+71-35
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ package source
1818

1919
import (
2020
"context"
21-
"fmt"
2221
"net/netip"
22+
"slices"
2323
"sort"
2424
"strings"
2525
"text/template"
@@ -206,41 +206,42 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
206206
if err != nil {
207207
return nil, err
208208
}
209-
kind := strings.ToLower(src.rtKind)
210209
resolver := newGatewayRouteResolver(src, gateways, namespaces)
211210
for _, rt := range routes {
212211
// Filter by annotations.
213212
meta := rt.Metadata()
214-
annots := meta.Annotations
215-
if !src.rtAnnotations.Matches(labels.Set(annots)) {
213+
if !src.rtAnnotations.Matches(labels.Set(meta.Annotations)) {
216214
continue
217215
}
218-
219216
// Check controller annotation to see if we are responsible.
220-
if v, ok := annots[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
217+
if v, ok := meta.Annotations[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
221218
log.Debugf("Skipping %s %s/%s because controller value does not match, found: %s, required: %s",
222219
src.rtKind, meta.Namespace, meta.Name, v, controllerAnnotationValue)
223220
continue
224221
}
225-
226222
// Get Gateway Listeners associated with Route.
227223
gwListeners := resolver.resolve(rt)
228224
if len(gwListeners) == 0 {
229225
log.Debugf("No endpoints could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
230226
continue
231227
}
232-
233228
// Create endpoints for Route and associated Gateway Listeners
234229
rtHosts := rt.Hostnames()
235230
if len(rtHosts) == 0 {
236231
// This means that the route doesn't specify a hostname and should use any provided by
237232
// attached Gateway Listeners.
238233
rtHosts = []v1.Hostname{""}
239234
}
240-
241-
hostTargets := make(map[string]endpoint.Targets)
235+
resource := strings.Join([]string{strings.ToLower(src.rtKind), meta.Namespace, meta.Name}, "/")
236+
hostGateways := map[string][]*v1beta1.Gateway{}
237+
ttl := getTTLFromAnnotations(meta.Annotations, resource)
238+
dualstack := false
239+
if v, ok := meta.Annotations[gatewayAPIDualstackAnnotationKey]; ok && v == gatewayAPIDualstackAnnotationValue {
240+
dualstack = true
241+
}
242+
providerSpecific, setIdentifier := getProviderSpecificAnnotations(meta.Annotations)
242243
for gateway, listeners := range gwListeners {
243-
var hosts []string
244+
hosts := map[string]struct{}{}
244245
for _, listener := range listeners {
245246
// Find all overlapping hostnames between the Route and Listener.
246247
gwHost := getVal(listener.Hostname, "")
@@ -249,40 +250,85 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
249250
if !ok || host == "" {
250251
continue
251252
}
252-
hosts = append(hosts, host)
253+
hosts[host] = struct{}{}
253254
}
254255
}
255256
// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
256257
// but other sources don't check if fqdn-template is set. Which should it be?
257258
if !src.ignoreHostnameAnnotation {
258-
hosts = append(hosts, getHostnamesFromAnnotations(annots)...)
259+
for _, host := range getHostnamesFromAnnotations(gateway.Annotations) {
260+
hosts[host] = struct{}{}
261+
}
262+
for _, host := range getHostnamesFromAnnotations(meta.Annotations) {
263+
hosts[host] = struct{}{}
264+
}
259265
}
260266
// TODO: The combine-fqdn-annotation flag is similarly vague.
261267
if src.fqdnTemplate != nil && (len(hosts) == 0 || src.combineFQDNAnnotation) {
262268
templated, err := execTemplate(src.fqdnTemplate, rt.Object())
263269
if err != nil {
264270
return nil, err
265271
}
266-
hosts = append(hosts, templated...)
272+
for _, host := range templated {
273+
hosts[host] = struct{}{}
274+
}
275+
}
276+
if len(hosts) == 0 {
277+
continue
278+
}
279+
for host, _ := range hosts {
280+
hostGateways[host] = append(hostGateways[host], gateway)
281+
}
282+
// Merge Gateway annotations
283+
gwTTL := getTTLFromAnnotations(gateway.Annotations, strings.Join([]string{strings.ToLower(gateway.Kind), gateway.Namespace, gateway.Name}, "/"))
284+
if gwTTL.IsConfigured() {
285+
if !ttl.IsConfigured() || ttl > gwTTL {
286+
ttl = gwTTL
287+
}
288+
}
289+
if v, ok := gateway.Annotations[gatewayAPIDualstackAnnotationKey]; ok && v == gatewayAPIDualstackAnnotationValue {
290+
dualstack = true
291+
}
292+
gwProviderSpecific, gwSetIdentifier := getProviderSpecificAnnotations(gateway.Annotations)
293+
for _, gwProperty := range gwProviderSpecific {
294+
present := false
295+
for _, property := range providerSpecific {
296+
if property.Name == gwProperty.Name {
297+
present = true
298+
break
299+
}
300+
}
301+
if !present {
302+
providerSpecific = append(providerSpecific, gwProperty)
303+
}
267304
}
268-
for _, host := range hosts {
305+
if setIdentifier == "" {
306+
setIdentifier = gwSetIdentifier
307+
}
308+
}
309+
for host, gateways := range hostGateways {
310+
var targets endpoint.Targets
311+
for _, gateway := range gateways {
269312
override := getTargetsFromTargetAnnotation(gateway.Annotations)
270-
hostTargets[host] = append(hostTargets[host], override...)
313+
targets = append(targets, override...)
271314
if len(override) == 0 {
272315
for _, addr := range gateway.Status.Addresses {
273-
hostTargets[host] = append(hostTargets[host], addr.Value)
316+
targets = append(targets, addr.Value)
274317
}
275318
}
276319
}
320+
origin := resource
321+
if len(gateways) == 1 && !src.ignoreHostnameAnnotation && slices.Contains(getHostnamesFromAnnotations(gateways[0].Annotations), host) {
322+
// Annotated hostnames from a single Gateway are attributed to the Gateway rather than the Route
323+
origin = strings.Join([]string{strings.ToLower(gateways[0].Kind), gateways[0].Namespace, gateways[0].Name}, "/")
324+
}
325+
for _, ep := range endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, origin) {
326+
if dualstack {
327+
ep.Labels[endpoint.DualstackLabelKey] = "true"
328+
}
329+
endpoints = append(endpoints, ep)
330+
}
277331
}
278-
279-
resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
280-
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
281-
ttl := getTTLFromAnnotations(annots, resource)
282-
for host, targets := range hostTargets {
283-
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
284-
}
285-
setDualstackLabel(rt, endpoints)
286332
log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, endpoints)
287333
}
288334
return endpoints, nil
@@ -585,13 +631,3 @@ func selectorsEqual(a, b labels.Selector) bool {
585631
}
586632
return true
587633
}
588-
589-
func setDualstackLabel(rt gatewayRoute, endpoints []*endpoint.Endpoint) {
590-
val, ok := rt.Metadata().Annotations[gatewayAPIDualstackAnnotationKey]
591-
if ok && val == gatewayAPIDualstackAnnotationValue {
592-
log.Debugf("Adding dualstack label to GatewayRoute %s/%s.", rt.Metadata().Namespace, rt.Metadata().Name)
593-
for _, ep := range endpoints {
594-
ep.Labels[endpoint.DualstackLabelKey] = "true"
595-
}
596-
}
597-
}

source/gateway_httproute_test.go

+186-1
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,16 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
677677
config: Config{},
678678
namespaces: namespaces("default"),
679679
gateways: []*v1beta1.Gateway{{
680-
ObjectMeta: objectMeta("default", "test"),
680+
TypeMeta: metav1.TypeMeta{
681+
Kind: "Gateway",
682+
},
683+
ObjectMeta: metav1.ObjectMeta{
684+
Name: "test",
685+
Namespace: "default",
686+
Annotations: map[string]string{
687+
hostnameAnnotationKey: "annotation.gateway.internal",
688+
},
689+
},
681690
Spec: v1.GatewaySpec{
682691
Listeners: []v1.Listener{{Protocol: v1.HTTPProtocolType}},
683692
},
@@ -712,6 +721,10 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
712721
},
713722
},
714723
endpoints: []*endpoint.Endpoint{
724+
newTestEndpoint("annotation.gateway.internal", "A", "1.2.3.4").
725+
WithLabel(endpoint.ResourceLabelKey, "gateway/default/test"),
726+
newTestEndpoint("annotation.gateway.internal", "A", "1.2.3.4").
727+
WithLabel(endpoint.ResourceLabelKey, "gateway/default/test"),
715728
newTestEndpoint("annotation.without-hostname.internal", "A", "1.2.3.4").
716729
WithLabel(endpoint.ResourceLabelKey, "httproute/default/without-hostname"),
717730
newTestEndpoint("annotation.with-hostname.internal", "A", "1.2.3.4").
@@ -861,6 +874,64 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
861874
WithLabel(endpoint.ResourceLabelKey, "httproute/default/valid-ttl"),
862875
},
863876
},
877+
{
878+
title: "TTLGateway",
879+
config: Config{},
880+
namespaces: namespaces("default"),
881+
gateways: []*v1beta1.Gateway{{
882+
ObjectMeta: metav1.ObjectMeta{
883+
Name: "test",
884+
Namespace: "default",
885+
Annotations: map[string]string{ttlAnnotationKey: "15s"},
886+
},
887+
Spec: v1.GatewaySpec{
888+
Listeners: []v1.Listener{{Protocol: v1.HTTPProtocolType}},
889+
},
890+
Status: gatewayStatus("1.2.3.4"),
891+
}},
892+
routes: []*v1beta1.HTTPRoute{
893+
{
894+
ObjectMeta: metav1.ObjectMeta{
895+
Name: "no-ttl",
896+
Namespace: "default",
897+
},
898+
Spec: v1.HTTPRouteSpec{
899+
Hostnames: hostnames("no-ttl.internal"),
900+
},
901+
Status: httpRouteStatus(gwParentRef("default", "test")),
902+
},
903+
{
904+
ObjectMeta: metav1.ObjectMeta{
905+
Name: "longer-ttl",
906+
Namespace: "default",
907+
Annotations: map[string]string{ttlAnnotationKey: "20s"},
908+
},
909+
Spec: v1.HTTPRouteSpec{
910+
Hostnames: hostnames("longer-ttl.internal"),
911+
},
912+
Status: httpRouteStatus(gwParentRef("default", "test")),
913+
},
914+
{
915+
ObjectMeta: metav1.ObjectMeta{
916+
Name: "shorter-ttl",
917+
Namespace: "default",
918+
Annotations: map[string]string{ttlAnnotationKey: "5s"},
919+
},
920+
Spec: v1.HTTPRouteSpec{
921+
Hostnames: hostnames("shorter-ttl.internal"),
922+
},
923+
Status: httpRouteStatus(gwParentRef("default", "test")),
924+
},
925+
},
926+
endpoints: []*endpoint.Endpoint{
927+
newTestEndpointWithTTL("no-ttl.internal", "A", 15, "1.2.3.4").
928+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/no-ttl"),
929+
newTestEndpointWithTTL("longer-ttl.internal", "A", 15, "1.2.3.4").
930+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/longer-ttl"),
931+
newTestEndpointWithTTL("shorter-ttl.internal", "A", 5, "1.2.3.4").
932+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/shorter-ttl"),
933+
},
934+
},
864935
{
865936
title: "ProviderAnnotations",
866937
config: Config{},
@@ -893,6 +964,61 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
893964
WithSetIdentifier("test-set-identifier"),
894965
},
895966
},
967+
{
968+
title: "ProviderAnnotationsGateway",
969+
config: Config{},
970+
namespaces: namespaces("default"),
971+
gateways: []*v1beta1.Gateway{{
972+
ObjectMeta: metav1.ObjectMeta{
973+
Name: "test",
974+
Namespace: "default",
975+
Annotations: map[string]string{
976+
SetIdentifierKey: "gateway",
977+
"external-dns.alpha.kubernetes.io/webhook-property": "gateway",
978+
},
979+
},
980+
Spec: v1.GatewaySpec{
981+
Listeners: []v1.Listener{{Protocol: v1.HTTPProtocolType}},
982+
},
983+
Status: gatewayStatus("1.2.3.4"),
984+
}},
985+
routes: []*v1beta1.HTTPRoute{
986+
{
987+
ObjectMeta: metav1.ObjectMeta{
988+
Name: "with-provider-annotations",
989+
Namespace: "default",
990+
Annotations: map[string]string{
991+
SetIdentifierKey: "route",
992+
"external-dns.alpha.kubernetes.io/webhook-property": "route",
993+
},
994+
},
995+
Spec: v1.HTTPRouteSpec{
996+
Hostnames: hostnames("with-provider-annotations.internal"),
997+
},
998+
Status: httpRouteStatus(gwParentRef("default", "test")),
999+
},
1000+
{
1001+
ObjectMeta: metav1.ObjectMeta{
1002+
Name: "without-provider-annotations",
1003+
Namespace: "default",
1004+
},
1005+
Spec: v1.HTTPRouteSpec{
1006+
Hostnames: hostnames("without-provider-annotations.internal"),
1007+
},
1008+
Status: httpRouteStatus(gwParentRef("default", "test")),
1009+
},
1010+
},
1011+
endpoints: []*endpoint.Endpoint{
1012+
newTestEndpoint("with-provider-annotations.internal", "A", "1.2.3.4").
1013+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/with-provider-annotations").
1014+
WithProviderSpecific("webhook/property", "route").
1015+
WithSetIdentifier("route"),
1016+
newTestEndpoint("without-provider-annotations.internal", "A", "1.2.3.4").
1017+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/without-provider-annotations").
1018+
WithProviderSpecific("webhook/property", "gateway").
1019+
WithSetIdentifier("gateway"),
1020+
},
1021+
},
8961022
{
8971023
title: "DifferentHostnameDifferentGateway",
8981024
config: Config{},
@@ -1153,6 +1279,65 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
11531279
WithLabel(endpoint.ResourceLabelKey, "httproute/route-namespace/test"),
11541280
},
11551281
},
1282+
{
1283+
title: "DualstackAnnotation",
1284+
config: Config{},
1285+
namespaces: namespaces("default"),
1286+
gateways: []*v1beta1.Gateway{{
1287+
ObjectMeta: objectMeta("default", "test"),
1288+
Spec: v1.GatewaySpec{
1289+
Listeners: []v1.Listener{{Protocol: v1.HTTPProtocolType}},
1290+
},
1291+
Status: gatewayStatus("1.2.3.4"),
1292+
}},
1293+
routes: []*v1beta1.HTTPRoute{
1294+
{
1295+
ObjectMeta: metav1.ObjectMeta{
1296+
Name: "invalid-dualstack-annotation",
1297+
Namespace: "default",
1298+
Annotations: map[string]string{
1299+
gatewayAPIDualstackAnnotationKey: "invalid",
1300+
},
1301+
},
1302+
Spec: v1.HTTPRouteSpec{
1303+
Hostnames: hostnames("invalid-dualstack-annotation.internal"),
1304+
},
1305+
Status: httpRouteStatus(gwParentRef("default", "test")),
1306+
},
1307+
{
1308+
ObjectMeta: metav1.ObjectMeta{
1309+
Name: "with-dualstack-annotation",
1310+
Namespace: "default",
1311+
Annotations: map[string]string{
1312+
gatewayAPIDualstackAnnotationKey: gatewayAPIDualstackAnnotationValue,
1313+
},
1314+
},
1315+
Spec: v1.HTTPRouteSpec{
1316+
Hostnames: hostnames("with-dualstack-annotation.internal"),
1317+
},
1318+
Status: httpRouteStatus(gwParentRef("default", "test")),
1319+
},
1320+
{
1321+
ObjectMeta: metav1.ObjectMeta{
1322+
Name: "without-dualstack-annotation",
1323+
Namespace: "default",
1324+
},
1325+
Spec: v1.HTTPRouteSpec{
1326+
Hostnames: hostnames("without-dualstack-annotation.internal"),
1327+
},
1328+
Status: httpRouteStatus(gwParentRef("default", "test")),
1329+
},
1330+
},
1331+
endpoints: []*endpoint.Endpoint{
1332+
newTestEndpoint("invalid-dualstack-annotation.internal", "A", "1.2.3.4").
1333+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/invalid-dualstack-annotation"),
1334+
newTestEndpoint("with-dualstack-annotation.internal", "A", "1.2.3.4").
1335+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/with-dualstack-annotation").
1336+
WithLabel(endpoint.DualstackLabelKey, "true"),
1337+
newTestEndpoint("without-dualstack-annotation.internal", "A", "1.2.3.4").
1338+
WithLabel(endpoint.ResourceLabelKey, "httproute/default/without-dualstack-annotation"),
1339+
},
1340+
},
11561341
}
11571342
for _, tt := range tests {
11581343
tt := tt

0 commit comments

Comments
 (0)