gateway: enforce listener matching

This commit is contained in:
Andy Bursavich 2022-02-08 12:17:20 -08:00
parent 71e45ce1d3
commit 4371589a14
10 changed files with 772 additions and 139 deletions

View File

@ -24,10 +24,13 @@ import (
"text/template"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
cache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/gateway/versioned"
@ -37,20 +40,27 @@ import (
"sigs.k8s.io/external-dns/endpoint"
)
const (
gatewayGroup = "gateway.networking.k8s.io"
gatewayKind = "Gateway"
)
type gatewayRoute interface {
// Object returns the underlying Route object to be used by templates.
// Object returns the underlying route object to be used by templates.
Object() kubeObject
// Metadata returns the Route's metadata.
// Metadata returns the route's metadata.
Metadata() *metav1.ObjectMeta
// Hostnames returns the Route's specified hostnames.
// Hostnames returns the route's specified hostnames.
Hostnames() []v1alpha2.Hostname
// Status returns the Route's status, including associated gateways.
Status() v1alpha2.RouteStatus
// Protocol returns the route's protocol type.
Protocol() v1alpha2.ProtocolType
// RouteStatus returns the route's common status.
RouteStatus() v1alpha2.RouteStatus
}
type newGatewayRouteInformerFunc func(informers.SharedInformerFactory) gatewayRouteInfomer
type newGatewayRouteInformerFunc func(informers.SharedInformerFactory) gatewayRouteInformer
type gatewayRouteInfomer interface {
type gatewayRouteInformer interface {
List(namespace string, selector labels.Selector) ([]gatewayRoute, error)
Informer() cache.SharedIndexInformer
}
@ -78,7 +88,9 @@ type gatewayRouteSource struct {
rtNamespace string
rtLabels labels.Selector
rtAnnotations labels.Selector
rtInformer gatewayRouteInfomer
rtInformer gatewayRouteInformer
nsInformer coreinformers.NamespaceInformer
fqdnTemplate *template.Template
combineFQDNAnnotation bool
@ -86,6 +98,8 @@ type gatewayRouteSource struct {
}
func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, newInformerFn newGatewayRouteInformerFunc) (Source, error) {
ctx := context.TODO()
gwLabels, err := getLabelSelector(config.GatewayLabelFilter)
if err != nil {
return nil, err
@ -109,25 +123,38 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string,
}
informerFactory := newGatewayInformerFactory(client, config.GatewayNamespace, gwLabels)
gwInformer := informerFactory.Gateway().V1alpha2().Gateways() // TODO: gateway informer should be shared across gateway sources
gwInformer.Informer() // Register with factory before starting
gwInformer := informerFactory.Gateway().V1alpha2().Gateways() // TODO: Gateway informer should be shared across gateway sources.
gwInformer.Informer() // Register with factory before starting.
rtInformerFactory := informerFactory
if config.Namespace != config.GatewayNamespace || !selectorsEqual(rtLabels, gwLabels) {
rtInformerFactory = newGatewayInformerFactory(client, config.Namespace, rtLabels)
}
rtInformer := newInformerFn(rtInformerFactory)
rtInformer.Informer() // Register with factory before starting
rtInformer.Informer() // Register with factory before starting.
kubeClient, err := clients.KubeClient()
if err != nil {
return nil, err
}
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
nsInformer := kubeInformerFactory.Core().V1().Namespaces() // TODO: Namespace informer should be shared across gateway sources.
nsInformer.Informer() // Register with factory before starting.
informerFactory.Start(wait.NeverStop)
kubeInformerFactory.Start(wait.NeverStop)
if rtInformerFactory != informerFactory {
rtInformerFactory.Start(wait.NeverStop)
if err := waitForCacheSync(context.Background(), rtInformerFactory); err != nil {
if err := waitForCacheSync(ctx, rtInformerFactory); err != nil {
return nil, err
}
}
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {
if err := waitForCacheSync(ctx, informerFactory); err != nil {
return nil, err
}
if err := waitForCacheSync(ctx, kubeInformerFactory); err != nil {
return nil, err
}
@ -142,6 +169,8 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string,
rtAnnotations: rtAnnotations,
rtInformer: rtInformer,
nsInformer: nsInformer,
fqdnTemplate: tmpl,
combineFQDNAnnotation: config.CombineFQDNAndAnnotation,
ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation,
@ -150,9 +179,11 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string,
}
func (src *gatewayRouteSource) AddEventHandler(ctx context.Context, handler func()) {
log.Debugf("Adding event handler for %s", src.rtKind)
src.gwInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
src.rtInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
log.Debugf("Adding event handlers for %s", src.rtKind)
eventHandler := eventHandlerFunc(handler)
src.gwInformer.Informer().AddEventHandler(eventHandler)
src.rtInformer.Informer().AddEventHandler(eventHandler)
src.nsInformer.Informer().AddEventHandler(eventHandler)
}
func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
@ -161,133 +192,255 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
if err != nil {
return nil, err
}
gwList, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
gateways, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
if err != nil {
return nil, err
}
gateways := gatewaysByRef(gwList)
namespaces, err := src.nsInformer.Lister().List(labels.Everything())
if err != nil {
return nil, err
}
kind := strings.ToLower(src.rtKind)
resolver := newGatewayRouteResolver(src, gateways, namespaces)
for _, rt := range routes {
eps, err := src.endpoints(rt, gateways)
if err != nil {
return nil, err
}
endpoints = append(endpoints, eps...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
func (src *gatewayRouteSource) endpoints(rt gatewayRoute, gateways map[types.NamespacedName]*v1alpha2.Gateway) ([]*endpoint.Endpoint, error) {
// Filter by annotations.
meta := rt.Metadata()
annotations := meta.Annotations
if !src.rtAnnotations.Matches(labels.Set(meta.Annotations)) {
return nil, nil
annots := meta.Annotations
if !src.rtAnnotations.Matches(labels.Set(annots)) {
continue
}
// Check controller annotation to see if we are responsible.
if v, ok := meta.Annotations[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
if v, ok := annots[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)
return nil, nil
continue
}
// Get hostnames.
hostnames, err := src.hostnames(rt)
// Get Route hostnames and their targets.
hostTargets, err := resolver.resolve(rt)
if err != nil {
return nil, err
}
if len(hostnames) == 0 {
log.Debugf("No hostnames could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
return nil, nil
if len(hostTargets) == 0 {
log.Debugf("No endpoints could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
continue
}
// Get targets.
targets := src.targets(rt, gateways)
if len(targets) == 0 {
log.Debugf("No targets could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
return nil, nil
}
// Create endpoints.
ttl, err := getTTLFromAnnotations(annotations)
// Create endpoints from hostnames and targets.
resourceKey := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
ttl, err := getTTLFromAnnotations(annots)
if err != nil {
log.Warn(err)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations)
var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
for host, targets := range hostTargets {
eps := endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)
for _, ep := range eps {
ep.Labels[endpoint.ResourceLabelKey] = resourceKey
}
endpoints = append(endpoints, eps...)
}
log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, endpoints)
kind := strings.ToLower(src.rtKind)
resourceKey := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
for _, ep := range endpoints {
ep.Labels[endpoint.ResourceLabelKey] = resourceKey
}
return endpoints, nil
}
func (src *gatewayRouteSource) hostnames(rt gatewayRoute) ([]string, error) {
func namespacedName(namespace, name string) types.NamespacedName {
return types.NamespacedName{Namespace: namespace, Name: name}
}
type gatewayRouteResolver struct {
src *gatewayRouteSource
gws map[types.NamespacedName]gatewayListeners
nss map[string]*corev1.Namespace
}
type gatewayListeners struct {
gateway *v1alpha2.Gateway
listeners map[v1alpha2.SectionName][]v1alpha2.Listener
}
func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1alpha2.Gateway, namespaces []*corev1.Namespace) *gatewayRouteResolver {
// Create Gateway Listener lookup table.
gws := make(map[types.NamespacedName]gatewayListeners, len(gateways))
for _, gw := range gateways {
lss := make(map[v1alpha2.SectionName][]v1alpha2.Listener, len(gw.Spec.Listeners)+1)
for i, lis := range gw.Spec.Listeners {
lss[lis.Name] = gw.Spec.Listeners[i : i+1]
}
lss[""] = gw.Spec.Listeners
gws[namespacedName(gw.Namespace, gw.Name)] = gatewayListeners{
gateway: gw,
listeners: lss,
}
}
// Create Namespace lookup table.
nss := make(map[string]*corev1.Namespace, len(namespaces))
for _, ns := range namespaces {
nss[ns.Name] = ns
}
return &gatewayRouteResolver{
src: src,
gws: gws,
nss: nss,
}
}
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)
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)
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)
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)
continue
}
// Confirm the Gateway has accepted the Route.
if !gwRouteIsAccepted(rps.Conditions) {
log.Debugf("Gateway %s/%s has not accepted %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name)
continue
}
// Match the Route to all possible Listeners.
match := false
section := sectionVal(ref.SectionName, "")
listeners := gw.listeners[section]
for i := range listeners {
// Confirm that the protocols match and the Listener allows the Route (based on namespace and kind).
lis := &listeners[i]
if !gwProtocolMatches(rt.Protocol(), lis.Protocol) || !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
}
for _, addr := range gw.gateway.Status.Addresses {
hostTargets[host] = append(hostTargets[host], addr.Value)
}
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)
}
}
// 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))
}
meta := rt.Metadata()
// 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 {
hostnames = append(hostnames, getHostnamesFromAnnotations(meta.Annotations)...)
if !c.src.ignoreHostnameAnnotation {
hostnames = append(hostnames, getHostnamesFromAnnotations(rt.Metadata().Annotations)...)
}
// TODO: The combine-fqdn-annotation flag is similarly vague.
if src.fqdnTemplate != nil && (len(hostnames) == 0 || src.combineFQDNAnnotation) {
hosts, err := execTemplate(src.fqdnTemplate, rt.Object())
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
}
func (src *gatewayRouteSource) targets(rt gatewayRoute, gateways map[types.NamespacedName]*v1alpha2.Gateway) endpoint.Targets {
var targets endpoint.Targets
func (c *gatewayRouteResolver) routeIsAllowed(gw *v1alpha2.Gateway, lis *v1alpha2.Listener, rt gatewayRoute) bool {
meta := rt.Metadata()
for _, rps := range rt.Status().Parents {
ref := rps.ParentRef
if (ref.Group != nil && *ref.Group != "gateway.networking.k8s.io") || (ref.Kind != nil && *ref.Kind != "Gateway") {
log.Debugf("Unsupported parent %v/%v for %s %s/%s", ref.Group, ref.Kind, src.rtKind, meta.Namespace, meta.Name)
continue
allow := lis.AllowedRoutes
// Check the route's namespace.
from := v1alpha2.NamespacesFromSame
if allow != nil && allow.Namespaces != nil && allow.Namespaces.From != nil {
from = *allow.Namespaces.From
}
namespace := meta.Namespace
if ref.Namespace != nil {
namespace = string(*ref.Namespace)
switch from {
case v1alpha2.NamespacesFromAll:
// OK
case v1alpha2.NamespacesFromSame:
if gw.Namespace != meta.Namespace {
return false
}
gw, ok := gateways[types.NamespacedName{
Namespace: namespace,
Name: string(ref.Name),
}]
case v1alpha2.NamespacesFromSelector:
selector, err := metav1.LabelSelectorAsSelector(allow.Namespaces.Selector)
if err != nil {
log.Debugf("Gateway %s/%s section %q has invalid namespace selector: %v", gw.Namespace, gw.Name, lis.Name, err)
return false
}
// Get namespace.
ns, ok := c.nss[meta.Namespace]
if !ok {
log.Debugf("Gateway %s/%s not found for %s %s/%s", namespace, ref.Name, src.rtKind, meta.Namespace, meta.Name)
continue
log.Errorf("Namespace not found for %s %s/%s", c.src.rtKind, meta.Namespace, meta.Name)
return false
}
if !gwRouteIsAdmitted(rps.Conditions) {
log.Debugf("Gateway %s/%s has not admitted %s %s/%s", namespace, ref.Name, src.rtKind, meta.Namespace, meta.Name)
continue
if !selector.Matches(labels.Set(ns.Labels)) {
return false
}
for _, addr := range gw.Status.Addresses {
// TODO: Should we validate address type?
// The spec says it should always be an IP.
targets = append(targets, addr.Value)
}
}
return targets
default:
log.Debugf("Gateway %s/%s section %q has unknown namespace from %q", gw.Namespace, gw.Name, lis.Name, from)
return false
}
func gwRouteIsAdmitted(conds []metav1.Condition) bool {
// Check the route's kind, if any are specified by the listener.
// TODO: Do we need to consider SupportedKinds in the ListenerStatus instead of the Spec?
// We only support core kinds and already check the protocol... Does this matter at all?
if allow == nil || len(allow.Kinds) == 0 {
return true
}
gvk := rt.Object().GetObjectKind().GroupVersionKind()
for _, gk := range allow.Kinds {
group := strVal((*string)(gk.Group), gatewayGroup)
if gvk.Group == group && gvk.Kind == string(gk.Kind) {
return true
}
}
return false
}
func gwRouteIsAccepted(conds []metav1.Condition) bool {
for _, c := range conds {
if v1alpha2.RouteConditionType(c.Type) == v1alpha2.ConditionRouteAccepted {
return c.Status == metav1.ConditionTrue
@ -296,15 +449,84 @@ func gwRouteIsAdmitted(conds []metav1.Condition) bool {
return false
}
func gatewaysByRef(list []*v1alpha2.Gateway) map[types.NamespacedName]*v1alpha2.Gateway {
if len(list) == 0 {
return nil
func uniqueTargets(targets endpoint.Targets) endpoint.Targets {
if len(targets) < 2 {
return targets
}
set := make(map[types.NamespacedName]*v1alpha2.Gateway, len(list))
for _, gw := range list {
set[types.NamespacedName{Namespace: gw.Namespace, Name: gw.Name}] = gw
sort.Strings([]string(targets))
prev := targets[0]
n := 1
for _, v := range targets[1:] {
if v == prev {
continue
}
return set
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.
func gwProtocolMatches(a, b v1alpha2.ProtocolType) bool {
if a == v1alpha2.HTTPSProtocolType {
a = v1alpha2.HTTPProtocolType
}
if b == v1alpha2.HTTPSProtocolType {
b = v1alpha2.HTTPProtocolType
}
return a == b
}
// gwMatchingHost returns the most-specific overlapping host and a bool indicating if one was found.
// For example, if one host is "*.foo.com" and the other is "bar.foo.com", "bar.foo.com" will be returned.
// An empty string matches anything.
func gwMatchingHost(gwHost, rtHost string) (string, bool) {
gwHost = toLowerCaseASCII(gwHost) // TODO: trim "." suffix?
rtHost = toLowerCaseASCII(rtHost) // TODO: trim "." suffix?
if gwHost == "" {
return rtHost, true
}
if rtHost == "" {
return gwHost, true
}
gwParts := strings.Split(gwHost, ".")
rtParts := strings.Split(rtHost, ".")
if len(gwParts) != len(rtParts) {
return "", false
}
host := rtHost
for i, gwPart := range gwParts {
switch rtPart := rtParts[i]; {
case rtPart == gwPart:
// continue
case i == 0 && gwPart == "*":
// continue
case i == 0 && rtPart == "*":
host = gwHost // gwHost is more specific
default:
return "", false
}
}
return host, true
}
func strVal(ptr *string, def string) string {
if ptr == nil || *ptr == "" {
return def
}
return *ptr
}
func sectionVal(ptr *v1alpha2.SectionName, def v1alpha2.SectionName) v1alpha2.SectionName {
if ptr == nil || *ptr == "" {
return def
}
return *ptr
}
func selectorsEqual(a, b labels.Selector) bool {

View File

@ -0,0 +1,45 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// See:
// - https://golang.org/LICENSE
// - https://golang.org/src/crypto/x509/verify.go
package source
import (
"unicode/utf8"
)
// toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use
// an explicitly ASCII function to avoid any sharp corners resulting from
// performing Unicode operations on DNS labels.
func toLowerCaseASCII(in string) string {
// If the string is already lower-case then there's nothing to do.
isAlreadyLowerCase := true
for _, c := range in {
if c == utf8.RuneError {
// If we get a UTF-8 error then there might be
// upper-case ASCII bytes in the invalid sequence.
isAlreadyLowerCase = false
break
}
if 'A' <= c && c <= 'Z' {
isAlreadyLowerCase = false
break
}
}
if isAlreadyLowerCase {
return in
}
out := []byte(in)
for i, c := range out {
if 'A' <= c && c <= 'Z' {
out[i] += 'a' - 'A'
}
}
return string(out)
}

View File

@ -26,7 +26,7 @@ import (
// NewGatewayHTTPRouteSource creates a new Gateway HTTPRoute source with the given config.
func NewGatewayHTTPRouteSource(clients ClientGenerator, config *Config) (Source, error) {
return newGatewayRouteSource(clients, config, "HTTPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInfomer {
return newGatewayRouteSource(clients, config, "HTTPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInformer {
return &gatewayHTTPRouteInformer{factory.Gateway().V1alpha2().HTTPRoutes()}
})
}
@ -36,7 +36,8 @@ type gatewayHTTPRoute struct{ route *v1alpha2.HTTPRoute }
func (rt *gatewayHTTPRoute) Object() kubeObject { return rt.route }
func (rt *gatewayHTTPRoute) Metadata() *metav1.ObjectMeta { return &rt.route.ObjectMeta }
func (rt *gatewayHTTPRoute) Hostnames() []v1alpha2.Hostname { return rt.route.Spec.Hostnames }
func (rt *gatewayHTTPRoute) Status() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
func (rt *gatewayHTTPRoute) Protocol() v1alpha2.ProtocolType { return v1alpha2.HTTPProtocolType }
func (rt *gatewayHTTPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
type gatewayHTTPRouteInformer struct {
informers_v1a2.HTTPRouteInformer

View File

@ -91,17 +91,17 @@ func newTestEndpointWithTTL(dnsName, recordType string, ttl int64, targets ...st
}
}
func joinTargets(targets ...[]string) []string {
var s []string
for _, v := range targets {
s = append(s, v...)
}
return s
}
func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
t.Parallel()
fromAll := v1alpha2.NamespacesFromAll
fromSame := v1alpha2.NamespacesFromSame
fromSelector := v1alpha2.NamespacesFromSelector
allowAllNamespaces := &v1alpha2.AllowedRoutes{
Namespaces: &v1alpha2.RouteNamespaces{
From: &fromAll,
},
}
objectMeta := func(namespace, name string) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: name,
@ -135,7 +135,10 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
AllowedRoutes: allowAllNamespaces,
}},
},
Status: gatewayStatus("1.2.3.4"),
},
@ -170,7 +173,10 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
AllowedRoutes: allowAllNamespaces,
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
@ -380,6 +386,144 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
newTestEndpoint("test.example.internal", "A", "1.2.3.4", "2.3.4.5"),
},
},
{
title: "MultipleListeners",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "one"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{
{
Name: "foo",
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("foo.example.internal"),
},
{
Name: "bar",
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("bar.example.internal"),
},
},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("*.example.internal"),
},
Status: httpRouteStatus(
gatewayParentRef("default", "one"),
),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("foo.example.internal", "A", "1.2.3.4"),
newTestEndpoint("bar.example.internal", "A", "1.2.3.4"),
},
},
{
title: "WildcardInGateway",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("*.example.internal"),
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostname"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: []v1alpha2.Hostname{
"foo.example.internal",
},
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("foo.example.internal", "A", "1.2.3.4")},
},
{
title: "WildcardInRoute",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("foo.example.internal"),
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostname"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: []v1alpha2.Hostname{
"*.example.internal",
},
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("foo.example.internal", "A", "1.2.3.4")},
},
{
title: "WildcardInRouteAndGateway",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("*.example.internal"),
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostname"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: []v1alpha2.Hostname{
"*.example.internal",
},
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("*.example.internal", "A", "1.2.3.4")},
},
{
title: "NoRouteHostname",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
Hostname: hostnamePtr("foo.example.internal"),
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostname"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: nil,
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("foo.example.internal", "A", "1.2.3.4")},
},
{
title: "NoGateways",
config: Config{},
@ -406,7 +550,7 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostame"),
ObjectMeta: objectMeta("default", "no-hostname"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: nil,
},
@ -622,6 +766,175 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
WithSetIdentifier("test-set-identifier"),
},
},
{
title: "DifferentHostnameDifferentGateway",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{
{
ObjectMeta: objectMeta("default", "one"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Hostname: hostnamePtr("*.one.internal"),
Protocol: v1alpha2.HTTPProtocolType,
}},
},
Status: gatewayStatus("1.2.3.4"),
},
{
ObjectMeta: objectMeta("default", "two"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Hostname: hostnamePtr("*.two.internal"),
Protocol: v1alpha2.HTTPProtocolType,
}},
},
Status: gatewayStatus("2.3.4.5"),
},
},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("test.one.internal", "test.two.internal"),
},
Status: httpRouteStatus(
gatewayParentRef("default", "one"),
gatewayParentRef("default", "two"),
),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.one.internal", "A", "1.2.3.4"),
newTestEndpoint("test.two.internal", "A", "2.3.4.5"),
},
},
{
title: "AllowedRoutesSameNamespace",
config: Config{},
namespaces: namespaces("same-namespace", "other-namespace"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("same-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
AllowedRoutes: &v1alpha2.AllowedRoutes{
Namespaces: &v1alpha2.RouteNamespaces{
From: &fromSame,
},
},
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: objectMeta("same-namespace", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("same-namespace.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("same-namespace", "test")),
},
{
ObjectMeta: objectMeta("other-namespace", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("other-namespace.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("same-namespace", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("same-namespace.example.internal", "A", "1.2.3.4"),
},
},
{
title: "AllowedRoutesNamespaceSelector",
config: Config{},
namespaces: []*corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{"team": "foo"},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Labels: map[string]string{"team": "bar"},
},
},
},
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
AllowedRoutes: &v1alpha2.AllowedRoutes{
Namespaces: &v1alpha2.RouteNamespaces{
From: &fromSelector,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"team": "foo"},
},
},
},
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: objectMeta("foo", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("foo.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: objectMeta("bar", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("bar.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("foo.example.internal", "A", "1.2.3.4"),
},
},
{
title: "MissingNamespace",
config: Config{},
namespaces: nil,
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.HTTPProtocolType,
AllowedRoutes: &v1alpha2.AllowedRoutes{
Namespaces: &v1alpha2.RouteNamespaces{
// Namespace selector triggers namespace lookup.
From: &fromSelector,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
},
},
}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: nil,
},
}
for _, tt := range tests {
tt := tt
@ -659,6 +972,4 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
}
}
func strPtr(val string) *string { return &val }
func hostnamePtr(val v1alpha2.Hostname) *v1alpha2.Hostname { return &val }

View File

@ -26,7 +26,7 @@ import (
// NewGatewayTCPRouteSource creates a new Gateway TCPRoute source with the given config.
func NewGatewayTCPRouteSource(clients ClientGenerator, config *Config) (Source, error) {
return newGatewayRouteSource(clients, config, "TCPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInfomer {
return newGatewayRouteSource(clients, config, "TCPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInformer {
return &gatewayTCPRouteInformer{factory.Gateway().V1alpha2().TCPRoutes()}
})
}
@ -36,7 +36,8 @@ type gatewayTCPRoute struct{ route *v1alpha2.TCPRoute }
func (rt *gatewayTCPRoute) Object() kubeObject { return rt.route }
func (rt *gatewayTCPRoute) Metadata() *metav1.ObjectMeta { return &rt.route.ObjectMeta }
func (rt *gatewayTCPRoute) Hostnames() []v1alpha2.Hostname { return nil }
func (rt *gatewayTCPRoute) Status() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
func (rt *gatewayTCPRoute) Protocol() v1alpha2.ProtocolType { return v1alpha2.TCPProtocolType }
func (rt *gatewayTCPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
type gatewayTCPRouteInformer struct {
informers_v1a2.TCPRouteInformer

View File

@ -21,7 +21,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/gateway/versioned/fake"
@ -31,19 +33,34 @@ func TestGatewayTCPRouteSourceEndpoints(t *testing.T) {
t.Parallel()
gwClient := gatewayfake.NewSimpleClientset()
kubeClient := kubefake.NewSimpleClientset()
clients := new(MockClientGenerator)
clients.On("GatewayClient").Return(gwClient, nil)
clients.On("KubeClient").Return(kubeClient, nil)
ctx := context.Background()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
}
_, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Namespace")
ips := []string{"10.64.0.1", "10.64.0.2"}
gw := &v1alpha2.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "internal",
Namespace: "default",
},
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.TCPProtocolType,
}},
},
Status: gatewayStatus(ips...),
}
_, err := gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
_, err = gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Gateway")
rt := &v1alpha2.TCPRoute{

View File

@ -26,7 +26,7 @@ import (
// NewGatewayTLSRouteSource creates a new Gateway TLSRoute source with the given config.
func NewGatewayTLSRouteSource(clients ClientGenerator, config *Config) (Source, error) {
return newGatewayRouteSource(clients, config, "TLSRoute", func(factory informers.SharedInformerFactory) gatewayRouteInfomer {
return newGatewayRouteSource(clients, config, "TLSRoute", func(factory informers.SharedInformerFactory) gatewayRouteInformer {
return &gatewayTLSRouteInformer{factory.Gateway().V1alpha2().TLSRoutes()}
})
}
@ -36,7 +36,8 @@ type gatewayTLSRoute struct{ route *v1alpha2.TLSRoute }
func (rt *gatewayTLSRoute) Object() kubeObject { return rt.route }
func (rt *gatewayTLSRoute) Metadata() *metav1.ObjectMeta { return &rt.route.ObjectMeta }
func (rt *gatewayTLSRoute) Hostnames() []v1alpha2.Hostname { return rt.route.Spec.Hostnames }
func (rt *gatewayTLSRoute) Status() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
func (rt *gatewayTLSRoute) Protocol() v1alpha2.ProtocolType { return v1alpha2.TLSProtocolType }
func (rt *gatewayTLSRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
type gatewayTLSRouteInformer struct {
informers_v1a2.TLSRouteInformer

View File

@ -21,7 +21,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/gateway/versioned/fake"
@ -31,19 +33,34 @@ func TestGatewayTLSRouteSourceEndpoints(t *testing.T) {
t.Parallel()
gwClient := gatewayfake.NewSimpleClientset()
kubeClient := kubefake.NewSimpleClientset()
clients := new(MockClientGenerator)
clients.On("GatewayClient").Return(gwClient, nil)
clients.On("KubeClient").Return(kubeClient, nil)
ctx := context.Background()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
}
_, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Namespace")
ips := []string{"10.64.0.1", "10.64.0.2"}
gw := &v1alpha2.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "internal",
Namespace: "default",
},
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.TLSProtocolType,
}},
},
Status: gatewayStatus(ips...),
}
_, err := gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
_, err = gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Gateway")
rt := &v1alpha2.TLSRoute{

View File

@ -26,7 +26,7 @@ import (
// NewGatewayUDPRouteSource creates a new Gateway UDPRoute source with the given config.
func NewGatewayUDPRouteSource(clients ClientGenerator, config *Config) (Source, error) {
return newGatewayRouteSource(clients, config, "UDPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInfomer {
return newGatewayRouteSource(clients, config, "UDPRoute", func(factory informers.SharedInformerFactory) gatewayRouteInformer {
return &gatewayUDPRouteInformer{factory.Gateway().V1alpha2().UDPRoutes()}
})
}
@ -36,7 +36,8 @@ type gatewayUDPRoute struct{ route *v1alpha2.UDPRoute }
func (rt *gatewayUDPRoute) Object() kubeObject { return rt.route }
func (rt *gatewayUDPRoute) Metadata() *metav1.ObjectMeta { return &rt.route.ObjectMeta }
func (rt *gatewayUDPRoute) Hostnames() []v1alpha2.Hostname { return nil }
func (rt *gatewayUDPRoute) Status() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
func (rt *gatewayUDPRoute) Protocol() v1alpha2.ProtocolType { return v1alpha2.UDPProtocolType }
func (rt *gatewayUDPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus }
type gatewayUDPRouteInformer struct {
informers_v1a2.UDPRouteInformer

View File

@ -21,7 +21,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/gateway/versioned/fake"
@ -31,19 +33,34 @@ func TestGatewayUDPRouteSourceEndpoints(t *testing.T) {
t.Parallel()
gwClient := gatewayfake.NewSimpleClientset()
kubeClient := kubefake.NewSimpleClientset()
clients := new(MockClientGenerator)
clients.On("GatewayClient").Return(gwClient, nil)
clients.On("KubeClient").Return(kubeClient, nil)
ctx := context.Background()
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
}
_, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Namespace")
ips := []string{"10.64.0.1", "10.64.0.2"}
gw := &v1alpha2.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "internal",
Namespace: "default",
},
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{
Protocol: v1alpha2.UDPProtocolType,
}},
},
Status: gatewayStatus(ips...),
}
_, err := gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
_, err = gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Gateway")
rt := &v1alpha2.UDPRoute{