Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Gateway API): improve annotations support #4870

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
6 changes: 3 additions & 3 deletions docs/sources/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ The set of domain names from a \*Route is sourced from the following places:
- Adds the hostnames from any `external-dns.alpha.kubernetes.io/hostname` annotation on the \*Route.
This behavior is suppressed if the `--ignore-hostname-annotation` flag was specified.

- Adds the hostnames from any `external-dns.alpha.kubernetes.io/hostname` annotation on associated Listeners' Gateways.
This behavior is suppressed if the `--ignore-hostname-annotation` flag was specified.

- If no endpoints were produced by the previous steps
or the `--combine-fqdn-annotation` flag was specified, then adds hostnames
generated from any`--fqdn-template` flag.

- If no endpoints were produced by the previous steps, each
attached Gateway listener will use its `hostname`, if present.

### Matching Gateways

Matching Gateways are discovered by iterating over the \*Route's `status.parents`:
Expand Down
6 changes: 6 additions & 0 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string)
}
}

// WithLabel attaches a label key/value pair to the Endpoint and returns the Endpoint.
func (e *Endpoint) WithLabel(key, value string) *Endpoint {
e.Labels[key] = value
return e
}

// WithSetIdentifier applies the given set identifier to the endpoint.
func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
e.SetIdentifier = setIdentifier
Expand Down
260 changes: 133 additions & 127 deletions source/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ package source

import (
"context"
"fmt"
"net/netip"
"slices"
"sort"
"strings"
"text/template"
Expand Down Expand Up @@ -206,41 +206,129 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
if err != nil {
return nil, err
}
kind := strings.ToLower(src.rtKind)
resolver := newGatewayRouteResolver(src, gateways, namespaces)
for _, rt := range routes {
// Filter by annotations.
meta := rt.Metadata()
annots := meta.Annotations
if !src.rtAnnotations.Matches(labels.Set(annots)) {
if !src.rtAnnotations.Matches(labels.Set(meta.Annotations)) {
continue
}

// Check controller annotation to see if we are responsible.
if v, ok := annots[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
if v, ok := meta.Annotations[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
log.Debugf("Skipping %s %s/%s because controller value does not match, found: %s, required: %s",
src.rtKind, meta.Namespace, meta.Name, v, controllerAnnotationValue)
continue
}

// Get Route hostnames and their targets.
hostTargets, err := resolver.resolve(rt)
if err != nil {
return nil, err
}
if len(hostTargets) == 0 {
// Get Gateway Listeners associated with Route.
gwListeners := resolver.resolve(rt)
if len(gwListeners) == 0 {
log.Debugf("No endpoints could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
continue
}

// Create endpoints from hostnames and targets.
resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
ttl := getTTLFromAnnotations(annots, resource)
for host, targets := range hostTargets {
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
// Create endpoints for Route and associated Gateway Listeners
rtHosts := rt.Hostnames()
if len(rtHosts) == 0 {
// This means that the route doesn't specify a hostname and should use any provided by
// attached Gateway Listeners.
rtHosts = []v1.Hostname{""}
}
resource := strings.Join([]string{strings.ToLower(src.rtKind), meta.Namespace, meta.Name}, "/")
hostGateways := map[string][]*v1beta1.Gateway{}
ttl := getTTLFromAnnotations(meta.Annotations, resource)
dualstack := false
if v, ok := meta.Annotations[gatewayAPIDualstackAnnotationKey]; ok && v == gatewayAPIDualstackAnnotationValue {
dualstack = true
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(meta.Annotations)
for gateway, listeners := range gwListeners {
hosts := map[string]struct{}{}
for _, listener := range listeners {
// Find all overlapping hostnames between the Route and Listener.
gwHost := getVal(listener.Hostname, "")
for _, rtHost := range rtHosts {
host, ok := gwMatchingHost(string(gwHost), string(rtHost))
if !ok || host == "" {
continue
}
hosts[host] = struct{}{}
}
}
// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
// but other sources don't check if fqdn-template is set. Which should it be?
if !src.ignoreHostnameAnnotation {
for _, host := range getHostnamesFromAnnotations(gateway.Annotations) {
hosts[host] = struct{}{}
}
for _, host := range getHostnamesFromAnnotations(meta.Annotations) {
hosts[host] = struct{}{}
}
}
// TODO: The combine-fqdn-annotation flag is similarly vague.
if src.fqdnTemplate != nil && (len(hosts) == 0 || src.combineFQDNAnnotation) {
templated, err := execTemplate(src.fqdnTemplate, rt.Object())
if err != nil {
return nil, err
}
for _, host := range templated {
hosts[host] = struct{}{}
}
}
if len(hosts) == 0 {
continue
}
for host := range hosts {
hostGateways[host] = append(hostGateways[host], gateway)
}
// Merge Gateway annotations
gwTTL := getTTLFromAnnotations(gateway.Annotations, strings.Join([]string{strings.ToLower(gateway.Kind), gateway.Namespace, gateway.Name}, "/"))
if gwTTL.IsConfigured() {
if !ttl.IsConfigured() || ttl > gwTTL {
ttl = gwTTL
}
}
if v, ok := gateway.Annotations[gatewayAPIDualstackAnnotationKey]; ok && v == gatewayAPIDualstackAnnotationValue {
dualstack = true
}
gwProviderSpecific, gwSetIdentifier := getProviderSpecificAnnotations(gateway.Annotations)
for _, gwProperty := range gwProviderSpecific {
present := false
for _, property := range providerSpecific {
if property.Name == gwProperty.Name {
present = true
break
}
}
if !present {
providerSpecific = append(providerSpecific, gwProperty)
}
}
if setIdentifier == "" {
setIdentifier = gwSetIdentifier
}
}
for host, gateways := range hostGateways {
var targets endpoint.Targets
for _, gateway := range gateways {
override := getTargetsFromTargetAnnotation(gateway.Annotations)
targets = append(targets, override...)
if len(override) == 0 {
for _, addr := range gateway.Status.Addresses {
targets = append(targets, addr.Value)
}
}
}
origin := resource
if len(gateways) == 1 && !src.ignoreHostnameAnnotation && slices.Contains(getHostnamesFromAnnotations(gateways[0].Annotations), host) {
// Annotated hostnames from a single Gateway are attributed to the Gateway rather than the Route
origin = strings.Join([]string{strings.ToLower(gateways[0].Kind), gateways[0].Namespace, gateways[0].Name}, "/")
}
for _, ep := range endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, origin) {
if dualstack {
ep.Labels[endpoint.DualstackLabelKey] = "true"
}
endpoints = append(endpoints, ep)
}
}
setDualstackLabel(rt, endpoints)
log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, endpoints)
}
return endpoints, nil
Expand Down Expand Up @@ -287,25 +375,21 @@ func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gatewa
}
}

func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Targets, error) {
rtHosts, err := c.hosts(rt)
if err != nil {
return nil, err
}
hostTargets := make(map[string]endpoint.Targets)
func (c *gatewayRouteResolver) resolve(rt gatewayRoute) map[*v1beta1.Gateway][]*v1.Listener {
gwListeners := map[*v1beta1.Gateway][]*v1.Listener{}

meta := rt.Metadata()
for _, rps := range rt.RouteStatus().Parents {
// Confirm the Parent is the standard Gateway kind.
ref := rps.ParentRef
group := strVal((*string)(ref.Group), gatewayGroup)
kind := strVal((*string)(ref.Kind), gatewayKind)
group := getVal(ref.Group, gatewayGroup)
kind := getVal(ref.Kind, gatewayKind)
if group != gatewayGroup || kind != gatewayKind {
log.Debugf("Unsupported parent %s/%s for %s %s/%s", group, kind, c.src.rtKind, meta.Namespace, meta.Name)
continue
}
// Lookup the Gateway and its Listeners.
namespace := strVal((*string)(ref.Namespace), meta.Namespace)
namespace := getVal((*string)(ref.Namespace), meta.Namespace)
gw, ok := c.gws[namespacedName(namespace, string(ref.Name))]
if !ok {
log.Debugf("Gateway %s/%s not found for %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name)
Expand All @@ -318,7 +402,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
}
// Match the Route to all possible Listeners.
match := false
section := sectionVal(ref.SectionName, "")
section := getVal(ref.SectionName, "")
listeners := gw.listeners[section]
for i := range listeners {
lis := &listeners[i]
Expand All @@ -327,78 +411,35 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
continue
}
// Confirm that the Listener and Route ports match, if specified.
// EXPERIMENTAL: https://gateway-api.sigs.k8s.io/geps/gep-957/
if ref.Port != nil && *ref.Port != lis.Port {
continue
}
// Confirm that the Listener allows the Route (based on namespace and kind).
if !c.routeIsAllowed(gw.gateway, lis, rt) {
continue
}
// Find all overlapping hostnames between the Route and Listener.
// For {TCP,UDP}Routes, all annotation-generated hostnames should match since the Listener doesn't specify a hostname.
// For {HTTP,TLS}Routes, hostnames (including any annotation-generated) will be required to match any Listeners specified hostname.
gwHost := ""
if lis.Hostname != nil {
gwHost = string(*lis.Hostname)
}
for _, rtHost := range rtHosts {
if gwHost == "" && rtHost == "" {
// For {HTTP,TLS}Routes, this means the Route and the Listener both allow _any_ hostnames.
// For {TCP,UDP}Routes, this should always happen since neither specifies hostnames.
continue
}
host, ok := gwMatchingHost(gwHost, rtHost)
if !ok {
continue
}
override := getTargetsFromTargetAnnotation(gw.gateway.Annotations)
hostTargets[host] = append(hostTargets[host], override...)
if len(override) == 0 {
for _, addr := range gw.gateway.Status.Addresses {
hostTargets[host] = append(hostTargets[host], addr.Value)
// Confirm that the Listener and Route hostnames overlap, if specified.
overlap := true
if len(rt.Hostnames()) != 0 {
gwHost := getVal(lis.Hostname, "")
for _, rtHost := range rt.Hostnames() {
_, overlap = gwMatchingHost(string(gwHost), string(rtHost))
if overlap {
break
}
}
match = true
}
if !overlap {
continue
}
gwListeners[gw.gateway] = append(gwListeners[gw.gateway], lis)
match = true
}
if !match {
log.Debugf("Gateway %s/%s section %q does not match %s %s/%s hostnames %q", namespace, ref.Name, section, c.src.rtKind, meta.Namespace, meta.Name, rtHosts)
log.Debugf("Gateway %s/%s section %q does not match %s %s/%s hostnames %q", namespace, ref.Name, section, c.src.rtKind, meta.Namespace, meta.Name, rt.Hostnames())
}
}
// If a Gateway has multiple matching Listeners for the same host, then we'll
// add its IPs to the target list multiple times and should dedupe them.
for host, targets := range hostTargets {
hostTargets[host] = uniqueTargets(targets)
}
return hostTargets, nil
}

func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
var hostnames []string
for _, name := range rt.Hostnames() {
hostnames = append(hostnames, string(name))
}
// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
// but other sources don't check if fqdn-template is set. Which should it be?
if !c.src.ignoreHostnameAnnotation {
hostnames = append(hostnames, getHostnamesFromAnnotations(rt.Metadata().Annotations)...)
}
// TODO: The combine-fqdn-annotation flag is similarly vague.
if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) {
hosts, err := execTemplate(c.src.fqdnTemplate, rt.Object())
if err != nil {
return nil, err
}
hostnames = append(hostnames, hosts...)
}
// This means that the route doesn't specify a hostname and should use any provided by
// attached Gateway Listeners. This is only useful for {HTTP,TLS}Routes, but it doesn't
// break {TCP,UDP}Routes.
if len(rt.Hostnames()) == 0 {
hostnames = append(hostnames, "")
}
return hostnames, nil
return gwListeners
}

func (c *gatewayRouteResolver) routeIsAllowed(gw *v1beta1.Gateway, lis *v1.Listener, rt gatewayRoute) bool {
Expand Down Expand Up @@ -445,7 +486,7 @@ func (c *gatewayRouteResolver) routeIsAllowed(gw *v1beta1.Gateway, lis *v1.Liste
}
gvk := rt.Object().GetObjectKind().GroupVersionKind()
for _, gk := range allow.Kinds {
group := strVal((*string)(gk.Group), gatewayGroup)
group := string(getVal(gk.Group, gatewayGroup))
if gvk.Group == group && gvk.Kind == string(gk.Kind) {
return true
}
Expand All @@ -462,24 +503,6 @@ func gwRouteIsAccepted(conds []metav1.Condition) bool {
return false
}

func uniqueTargets(targets endpoint.Targets) endpoint.Targets {
if len(targets) < 2 {
return targets
}
sort.Strings([]string(targets))
prev := targets[0]
n := 1
for _, v := range targets[1:] {
if v == prev {
continue
}
prev = v
targets[n] = v
n++
}
return targets[:n]
}

// gwProtocolMatches returns whether a and b are the same protocol,
// where HTTP and HTTPS are considered the same.
// and TLS and TCP are considered the same.
Expand Down Expand Up @@ -583,15 +606,8 @@ func isAlphaNum(b byte) bool {
}
}

func strVal(ptr *string, def string) string {
if ptr == nil || *ptr == "" {
return def
}
return *ptr
}

func sectionVal(ptr *v1.SectionName, def v1.SectionName) v1.SectionName {
if ptr == nil || *ptr == "" {
func getVal[T any](ptr *T, def T) T {
if ptr == nil {
return def
}
return *ptr
Expand All @@ -615,13 +631,3 @@ func selectorsEqual(a, b labels.Selector) bool {
}
return true
}

func setDualstackLabel(rt gatewayRoute, endpoints []*endpoint.Endpoint) {
val, ok := rt.Metadata().Annotations[gatewayAPIDualstackAnnotationKey]
if ok && val == gatewayAPIDualstackAnnotationValue {
log.Debugf("Adding dualstack label to GatewayRoute %s/%s.", rt.Metadata().Namespace, rt.Metadata().Name)
for _, ep := range endpoints {
ep.Labels[endpoint.DualstackLabelKey] = "true"
}
}
}
Loading