diff --git a/source/gateway.go b/source/gateway.go index 93614f714..500962e67 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -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) + // Filter by annotations. + meta := rt.Metadata() + annots := meta.Annotations + if !src.rtAnnotations.Matches(labels.Set(annots)) { + continue + } + + // Check controller annotation to see if we are responsible. + 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) + continue + } + + // Get Route hostnames and their targets. + hostTargets, err := resolver.resolve(rt) if err != nil { return nil, err } - endpoints = append(endpoints, eps...) - } - for _, ep := range endpoints { - sort.Sort(ep.Targets) + if len(hostTargets) == 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. + 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) + } + 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) } 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 - } +func namespacedName(namespace, name string) types.NamespacedName { + return types.NamespacedName{Namespace: namespace, Name: name} +} - // Check controller annotation to see if we are responsible. - 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) - return nil, nil - } +type gatewayRouteResolver struct { + src *gatewayRouteSource + gws map[types.NamespacedName]gatewayListeners + nss map[string]*corev1.Namespace +} - // Get hostnames. - hostnames, err := src.hostnames(rt) +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 } - if len(hostnames) == 0 { - log.Debugf("No hostnames could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name) - return nil, nil - } + hostTargets := make(map[string]endpoint.Targets) - // 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 + 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) + } } - - // Create endpoints. - ttl, err := getTTLFromAnnotations(annotations) - if err != nil { - log.Warn(err) + // 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) } - providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) - var endpoints []*endpoint.Endpoint - for _, hostname := range hostnames { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) - } - 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 + return hostTargets, nil } -func (src *gatewayRouteSource) hostnames(rt gatewayRoute) ([]string, error) { +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 + } + switch from { + case v1alpha2.NamespacesFromAll: + // OK + case v1alpha2.NamespacesFromSame: + if gw.Namespace != meta.Namespace { + return false } - namespace := meta.Namespace - if ref.Namespace != nil { - namespace = string(*ref.Namespace) + 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 } - gw, ok := gateways[types.NamespacedName{ - Namespace: namespace, - Name: string(ref.Name), - }] + // 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) + default: + log.Debugf("Gateway %s/%s section %q has unknown namespace from %q", gw.Namespace, gw.Name, lis.Name, from) + return false + } + + // 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 targets + return false } -func gwRouteIsAdmitted(conds []metav1.Condition) bool { +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 + } + prev = v + targets[n] = v + n++ } - return set + 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 { diff --git a/source/gateway_hostname.go b/source/gateway_hostname.go new file mode 100644 index 000000000..294fad1ca --- /dev/null +++ b/source/gateway_hostname.go @@ -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) +} diff --git a/source/gateway_httproute.go b/source/gateway_httproute.go index 3d8d345b2..a0650ac64 100644 --- a/source/gateway_httproute.go +++ b/source/gateway_httproute.go @@ -26,17 +26,18 @@ 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()} }) } 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) 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) Protocol() v1alpha2.ProtocolType { return v1alpha2.HTTPProtocolType } +func (rt *gatewayHTTPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus } type gatewayHTTPRouteInformer struct { informers_v1a2.HTTPRouteInformer diff --git a/source/gateway_httproute_test.go b/source/gateway_httproute_test.go index c9614bba3..b1290ba6f 100644 --- a/source/gateway_httproute_test.go +++ b/source/gateway_httproute_test.go @@ -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 } diff --git a/source/gateway_tcproute.go b/source/gateway_tcproute.go index dac16d01c..809072c1d 100644 --- a/source/gateway_tcproute.go +++ b/source/gateway_tcproute.go @@ -26,17 +26,18 @@ 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()} }) } 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) 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) Protocol() v1alpha2.ProtocolType { return v1alpha2.TCPProtocolType } +func (rt *gatewayTCPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus } type gatewayTCPRouteInformer struct { informers_v1a2.TCPRouteInformer diff --git a/source/gateway_tcproute_test.go b/source/gateway_tcproute_test.go index 13490b3c0..1f8b59931 100644 --- a/source/gateway_tcproute_test.go +++ b/source/gateway_tcproute_test.go @@ -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{ diff --git a/source/gateway_tlsroute.go b/source/gateway_tlsroute.go index cf9dd857f..5fc4c12f1 100644 --- a/source/gateway_tlsroute.go +++ b/source/gateway_tlsroute.go @@ -26,17 +26,18 @@ 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()} }) } 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) 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) Protocol() v1alpha2.ProtocolType { return v1alpha2.TLSProtocolType } +func (rt *gatewayTLSRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus } type gatewayTLSRouteInformer struct { informers_v1a2.TLSRouteInformer diff --git a/source/gateway_tlsroute_test.go b/source/gateway_tlsroute_test.go index ee944c3bc..46be23b7b 100644 --- a/source/gateway_tlsroute_test.go +++ b/source/gateway_tlsroute_test.go @@ -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{ diff --git a/source/gateway_udproute.go b/source/gateway_udproute.go index f634a0fc3..08d9c987d 100644 --- a/source/gateway_udproute.go +++ b/source/gateway_udproute.go @@ -26,17 +26,18 @@ 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()} }) } 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) 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) Protocol() v1alpha2.ProtocolType { return v1alpha2.UDPProtocolType } +func (rt *gatewayUDPRoute) RouteStatus() v1alpha2.RouteStatus { return rt.route.Status.RouteStatus } type gatewayUDPRouteInformer struct { informers_v1a2.UDPRouteInformer diff --git a/source/gateway_udproute_test.go b/source/gateway_udproute_test.go index 289159a98..c17cee435 100644 --- a/source/gateway_udproute_test.go +++ b/source/gateway_udproute_test.go @@ -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{