diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index b3ef006ce..6dbcbced7 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -65,6 +65,9 @@ rules: - apiGroups: ["gateway.networking.k8s.io"] resources: ["gateways"] verbs: ["get","watch","list"] + - apiGroups: ["gateway.networking.x-k8s.io"] + resources: ["xlistenersets"] + verbs: ["get","watch","list"] {{- end }} {{- if not .Values.namespaced }} - apiGroups: [""] @@ -158,6 +161,9 @@ rules: - apiGroups: ["gateway.networking.k8s.io"] resources: ["gateways"] verbs: ["get","watch","list"] + - apiGroups: ["gateway.networking.x-k8s.io"] + resources: ["xlistenersets"] + verbs: ["get","watch","list"] {{- end }} {{- end }} {{- end }} diff --git a/docs/flags.md b/docs/flags.md index 2a1795407..25c3401ee 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -32,6 +32,7 @@ | `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) | | `--[no-]expose-internal-ipv6` | When using the node source, expose internal IPv6 addresses (optional, default: false) | | `--fqdn-template=""` | A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN. | +| `--[no-]gateway-enable-experimental` | Enable Gateway API experimental (gateway.networking.x-k8s.io) resources (currently only XListenerSet support for routes) | | `--gateway-label-filter=""` | Filter Gateways of Route endpoints via label selector (default: all gateways) | | `--gateway-name=""` | Limit Gateways of Route endpoints to a specific name (default: all names) | | `--gateway-namespace=""` | Limit Gateways of Route endpoints to a specific namespace (default: all namespaces) | diff --git a/docs/sources/gateway-api.md b/docs/sources/gateway-api.md index 424d4c9f4..6a5009352 100644 --- a/docs/sources/gateway-api.md +++ b/docs/sources/gateway-api.md @@ -202,6 +202,8 @@ spec: - --source=gateway-tlsroute - --source=gateway-tcproute - --source=gateway-udproute + # Optionally, enable Gateway API Experimental resource support (XListenerSet) + - --gateway-enable-experimental # Optionally, limit Routes to those in the given namespace. - --namespace=my-route-namespace # Optionally, limit Routes to those matching the given label selector. diff --git a/docs/sources/gateway.md b/docs/sources/gateway.md index 059ccedfa..040bfcbb0 100644 --- a/docs/sources/gateway.md +++ b/docs/sources/gateway.md @@ -3,6 +3,12 @@ The gateway-grpcroute, gateway-httproute, gateway-tcproute, gateway-tlsroute, and gateway-udproute sources create DNS entries based on their respective `gateway.networking.k8s.io` resources. +## Gateway experimental support + +Optionally, [ListenerSet](https://gateway-api.sigs.k8s.io/geps/gep-1713) support can be enabled +with the flag `--gateway-enable-experimental`. In this case, the `xlistenerset.gateway.networking.x-k8s.io` +resource listener entries will be appended to the `gateway.networking.k8s.io` listeners. + ## Filtering the Routes considered These sources support the `--label-filter` flag, which filters \*Route resources @@ -46,6 +52,9 @@ Matching Gateways are discovered by iterating over the \*Route's `status.parents - If the `--gateway-name` flag was specified, ignores parents with a `parentRef.name` other than the specified value. +- When [experimental](#gateway-experimental-support) support is enabled the Gateway is expanded from + the `parentRef` of the `XListenerSet`. + For example, given the following HTTPRoute: ```yaml diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index fc5d047de..9ea3ab38a 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -60,6 +60,7 @@ type Config struct { IgnoreIngressRulesSpec bool ListenEndpointEvents bool ExposeInternalIPV6 bool + GatewayEnableExperimental bool GatewayName string GatewayNamespace string GatewayLabelFilter string @@ -288,6 +289,7 @@ var defaultConfig = &Config{ ExoscaleAPIZone: "ch-gva-2", ExposeInternalIPV6: false, FQDNTemplate: "", + GatewayEnableExperimental: false, GatewayLabelFilter: "", GatewayName: "", GatewayNamespace: "", @@ -626,6 +628,7 @@ func bindFlags(b FlagBinder, cfg *Config) { b.BoolVar("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)", defaultConfig.ExcludeUnschedulable, &cfg.ExcludeUnschedulable) b.BoolVar("expose-internal-ipv6", "When using the node source, expose internal IPv6 addresses (optional, default: false)", false, &cfg.ExposeInternalIPV6) b.StringVar("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.", defaultConfig.FQDNTemplate, &cfg.FQDNTemplate) + b.BoolVar("gateway-enable-experimental", "Enable Gateway API experimental (gateway.networking.x-k8s.io) resources (currently only XListenerSet support for routes)", defaultConfig.GatewayEnableExperimental, &cfg.GatewayEnableExperimental) b.StringVar("gateway-label-filter", "Filter Gateways of Route endpoints via label selector (default: all gateways)", defaultConfig.GatewayLabelFilter, &cfg.GatewayLabelFilter) b.StringVar("gateway-name", "Limit Gateways of Route endpoints to a specific name (default: all names)", defaultConfig.GatewayName, &cfg.GatewayName) b.StringVar("gateway-namespace", "Limit Gateways of Route endpoints to a specific namespace (default: all namespaces)", defaultConfig.GatewayNamespace, &cfg.GatewayNamespace) diff --git a/source/gateway.go b/source/gateway.go index 95a551f5e..e38d9b0a2 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -35,9 +35,11 @@ import ( "k8s.io/client-go/tools/cache" v1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/apisx/v1alpha1" gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" gwinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" informers_v1beta1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1beta1" + informers_x_v1alpha1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apisx/v1alpha1" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -46,8 +48,10 @@ import ( ) const ( - gatewayGroup = "gateway.networking.k8s.io" - gatewayKind = "Gateway" + gatewayGroup = "gateway.networking.k8s.io" + gatewayKind = "Gateway" + gatewayXGroup = "gateway.networking.x-k8s.io" + listenerSetKind = "XListenerSet" ) type gatewayRoute interface { @@ -87,10 +91,12 @@ func newGatewayInformerFactory(client gateway.Interface, namespace string, label } type gatewayRouteSource struct { - gwName string - gwNamespace string - gwLabels labels.Selector - gwInformer informers_v1beta1.GatewayInformer + gwEnableExperimental bool + gwName string + gwNamespace string + gwLabels labels.Selector + gwInformer informers_v1beta1.GatewayInformer + lsInformer informers_x_v1alpha1.XListenerSetInformer rtKind string rtNamespace string @@ -134,6 +140,14 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, gwInformer := informerFactory.Gateway().V1beta1().Gateways() // TODO: Gateway informer should be shared across gateway sources. gwInformer.Informer() // Register with factory before starting. + // EXPERIMENTAL support for XListenerSet, requires gateway.networking.x-k8s.io CRDs installed. + var lsInformer informers_x_v1alpha1.XListenerSetInformer + if config.GatewayEnableExperimental { + log.Debugf("Gateway API experimental support enabled for gateway.networking.x-k8s.io") + lsInformer = informerFactory.Experimental().V1alpha1().XListenerSets() // TODO: XListenerSet informer should be shared across gateway sources. + lsInformer.Informer() // Register with factory before starting. + } + rtInformerFactory := informerFactory if config.Namespace != config.GatewayNamespace || !selectorsEqual(rtLabels, gwLabels) { rtInformerFactory = newGatewayInformerFactory(client, config.Namespace, rtLabels) @@ -167,10 +181,12 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, } src := &gatewayRouteSource{ - gwName: config.GatewayName, - gwNamespace: config.GatewayNamespace, - gwLabels: gwLabels, - gwInformer: gwInformer, + gwEnableExperimental: config.GatewayEnableExperimental, + gwName: config.GatewayName, + gwNamespace: config.GatewayNamespace, + gwLabels: gwLabels, + gwInformer: gwInformer, + lsInformer: lsInformer, rtKind: kind, rtNamespace: config.Namespace, @@ -184,6 +200,7 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, combineFQDNAnnotation: config.CombineFQDNAndAnnotation, ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation, } + return src, nil } @@ -193,6 +210,10 @@ func (src *gatewayRouteSource) AddEventHandler(ctx context.Context, handler func src.gwInformer.Informer().AddEventHandler(eventHandler) src.rtInformer.Informer().AddEventHandler(eventHandler) src.nsInformer.Informer().AddEventHandler(eventHandler) + + if src.gwEnableExperimental && src.lsInformer != nil { + src.lsInformer.Informer().AddEventHandler(eventHandler) + } } func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { @@ -205,12 +226,19 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo if err != nil { return nil, err } + var listenersets []*v1alpha1.XListenerSet + if src.gwEnableExperimental && src.lsInformer != nil { + listenersets, err = src.lsInformer.Lister().XListenerSets(src.gwNamespace).List(src.gwLabels) + if err != nil { + return nil, err + } + } namespaces, err := src.nsInformer.Lister().List(labels.Everything()) if err != nil { return nil, err } kind := strings.ToLower(src.rtKind) - resolver := newGatewayRouteResolver(src, gateways, namespaces) + resolver := newGatewayRouteResolver(src, gateways, listenersets, namespaces) for _, rt := range routes { // Filter by annotations. meta := rt.Metadata() @@ -258,6 +286,7 @@ func namespacedName(namespace, name string) types.NamespacedName { type gatewayRouteResolver struct { src *gatewayRouteSource gws map[types.NamespacedName]gatewayListeners + lss map[types.NamespacedName]listenerSetListeners nss map[string]*corev1.Namespace } @@ -266,7 +295,12 @@ type gatewayListeners struct { listeners map[v1.SectionName][]v1.Listener } -func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gateway, namespaces []*corev1.Namespace) *gatewayRouteResolver { +type listenerSetListeners struct { + listenerset *v1alpha1.XListenerSet + listeners map[v1.SectionName][]v1alpha1.ListenerEntry +} + +func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gateway, listenersets []*v1alpha1.XListenerSet, namespaces []*corev1.Namespace) *gatewayRouteResolver { // Create Gateway Listener lookup table. gws := make(map[types.NamespacedName]gatewayListeners, len(gateways)) for _, gw := range gateways { @@ -280,6 +314,19 @@ func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gatewa listeners: lss, } } + // Create ListenerSet lookup table. + lss := make(map[types.NamespacedName]listenerSetListeners, len(listenersets)) + for _, ls := range listenersets { + lsl := make(map[v1.SectionName][]v1alpha1.ListenerEntry, len(ls.Spec.Listeners)+1) + for i, lis := range ls.Spec.Listeners { + lsl[lis.Name] = ls.Spec.Listeners[i : i+1] + } + lsl[""] = ls.Spec.Listeners + lss[namespacedName(ls.Namespace, ls.Name)] = listenerSetListeners{ + listenerset: ls, + listeners: lsl, + } + } // Create Namespace lookup table. nss := make(map[string]*corev1.Namespace, len(namespaces)) for _, ns := range namespaces { @@ -288,6 +335,7 @@ func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gatewa return &gatewayRouteResolver{ src: src, gws: gws, + lss: lss, nss: nss, } } @@ -308,43 +356,106 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar meta := rt.Metadata() for _, rps := range rt.RouteStatus().Parents { - // Confirm the Parent is the standard Gateway kind. - ref := rps.ParentRef - namespace := strVal((*string)(ref.Namespace), meta.Namespace) + refRoute := rps.ParentRef + refRouteNamespace := strVal((*string)(refRoute.Namespace), meta.Namespace) // Ensure that the parent reference is in the routeParentRefs list - if !gwRouteHasParentRef(routeParentRefs, ref, meta) { - log.Debugf("Parent reference %s/%s not found in routeParentRefs for %s %s/%s", namespace, string(ref.Name), c.src.rtKind, meta.Namespace, meta.Name) + if !gwRouteHasParentRef(routeParentRefs, refRoute, meta) { + log.Debugf("Parent reference %s/%s not found in routeParentRefs for %s %s/%s", refRouteNamespace, string(refRoute.Name), c.src.rtKind, meta.Namespace, meta.Name) continue } - group := strVal((*string)(ref.Group), gatewayGroup) - kind := strVal((*string)(ref.Kind), gatewayKind) - if group != gatewayGroup || kind != gatewayKind { + // Confirm the Parent is the standard Gateway kind or experimental ListenerSet (if enabled). + group := strVal((*string)(refRoute.Group), gatewayGroup) + kind := strVal((*string)(refRoute.Kind), gatewayKind) + if !routeParentRefIsType(refRoute, gatewayGroup, gatewayKind) && (!c.src.gwEnableExperimental || !routeParentRefIsType(refRoute, gatewayXGroup, listenerSetKind)) { log.Debugf("Unsupported parent %s/%s for %s %s/%s", group, kind, c.src.rtKind, meta.Namespace, meta.Name) continue } + + // If Gateway, use direct reference from Route. If ListenerSet, expand to its referenced Gateway. + refGateway := refRoute + refGatewayNamespace := refRouteNamespace + var listenerSetRoute *v1alpha1.XListenerSet + if routeParentRefIsType(refRoute, gatewayXGroup, listenerSetKind) { + ls, ok := c.lss[namespacedName(refRouteNamespace, string(refRoute.Name))] + if !ok { + log.Debugf("XListenerSet %s/%s not found for %s %s/%s", refRouteNamespace, refRoute.Name, c.src.rtKind, meta.Namespace, meta.Name) + continue + } + refGateway = v1.ParentReference{ + Group: ls.listenerset.Spec.ParentRef.Group, + Kind: ls.listenerset.Spec.ParentRef.Kind, + Name: ls.listenerset.Spec.ParentRef.Name, + Namespace: ls.listenerset.Spec.ParentRef.Namespace, + } + refGatewayNamespace = strVal((*string)(refGateway.Namespace), meta.Namespace) + if !routeParentRefIsType(refGateway, gatewayGroup, gatewayKind) { + group := strVal((*string)(refGateway.Group), "") + kind := strVal((*string)(refGateway.Kind), "") + log.Debugf("Unsupported parent %s/%s in XListenerSet %s/%s for %s %s/%s", group, kind, refRouteNamespace, refRoute.Name, c.src.rtKind, meta.Namespace, meta.Name) + continue + } + listenerSetRoute = ls.listenerset + } + // Lookup the Gateway and its Listeners. - gw, ok := c.gws[namespacedName(namespace, string(ref.Name))] + gw, ok := c.gws[namespacedName(refGatewayNamespace, string(refGateway.Name))] if !ok { - log.Debugf("Gateway %s/%s not found for %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name) + log.Debugf("Gateway %s/%s not found for %s %s/%s", refGatewayNamespace, refGateway.Name, c.src.rtKind, meta.Namespace, meta.Name) continue } // Confirm the Gateway has the correct name, if specified. if c.src.gwName != "" && c.src.gwName != gw.gateway.Name { - log.Debugf("Gateway %s/%s does not match %s %s/%s", namespace, ref.Name, c.src.gwName, meta.Namespace, meta.Name) + log.Debugf("Gateway %s/%s does not match %s %s/%s", refGatewayNamespace, refGateway.Name, c.src.gwName, meta.Namespace, meta.Name) continue } - // Confirm the Gateway has accepted the Route. + // Confirm the Gateway/ListenerSet has accepted the Route. if !gwRouteIsAccepted(rps.Conditions) { - log.Debugf("Gateway %s/%s has not accepted the current generation %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name) + log.Debugf("%s %s/%s has not accepted the current generation %s %s/%s", kind, refGatewayNamespace, refGateway.Name, c.src.rtKind, meta.Namespace, meta.Name) continue } // Match the Route to all possible Listeners. match := false - section := sectionVal(ref.SectionName, "") + section := sectionVal(refGateway.SectionName, "") listeners := gw.listeners[section] + + // If ListenerSet, get its Listeners for the section and merge into Gateway listeners. + if routeParentRefIsType(refRoute, gatewayXGroup, listenerSetKind) { + ls, ok := c.lss[namespacedName(refRouteNamespace, string(refRoute.Name))] + if !ok { + log.Debugf("XListenerSet %s/%s not found for %s %s/%s", refRouteNamespace, refRoute.Name, c.src.rtKind, meta.Namespace, meta.Name) + continue + } + + lsListeners, ok := ls.listeners[section] + if !ok { + log.Debugf("XListenerSet %s/%s has no listeners for section %q for %s %s/%s", refRouteNamespace, refRoute.Name, section, c.src.rtKind, meta.Namespace, meta.Name) + continue + } + lsListenersv1 := make([]v1.Listener, len(lsListeners)) + for i, lis := range lsListeners { + v1listener := v1.Listener{ + Name: lis.Name, + Port: lis.Port, + Protocol: lis.Protocol, + } + if lis.Hostname != nil { + v1listener.Hostname = lis.Hostname + } + if lis.TLS != nil { + v1listener.TLS = lis.TLS + } + if lis.AllowedRoutes != nil { + v1listener.AllowedRoutes = lis.AllowedRoutes + } + lsListenersv1[i] = v1listener + } + log.Debugf("Appending %d XListenerSet listeners to Gateway %s/%s", len(lsListenersv1), refGatewayNamespace, refGateway.Name) + listeners = append(listeners, lsListenersv1...) + } + for i := range listeners { lis := &listeners[i] // Confirm that the Listener and Route protocols match. @@ -353,11 +464,11 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar } // 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 { + if refGateway.Port != nil && *refGateway.Port != lis.Port { continue } // Confirm that the Listener allows the Route (based on namespace and kind). - if !c.routeIsAllowed(gw.gateway, lis, rt) { + if !c.gwRouteIsAllowed(gw.gateway, lis, rt) && !c.lsRouteIsAllowed(listenerSetRoute, lis, rt) { continue } // Find all overlapping hostnames between the Route and Listener. @@ -388,7 +499,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar } } 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", refGatewayNamespace, refGateway.Name, section, c.src.rtKind, meta.Namespace, meta.Name, rtHosts) } } // If a Gateway has multiple matching Listeners for the same host, then we'll @@ -426,7 +537,7 @@ func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) { return hostnames, nil } -func (c *gatewayRouteResolver) routeIsAllowed(gw *v1beta1.Gateway, lis *v1.Listener, rt gatewayRoute) bool { +func (c *gatewayRouteResolver) gwRouteIsAllowed(gw *v1beta1.Gateway, lis *v1.Listener, rt gatewayRoute) bool { meta := rt.Metadata() allow := lis.AllowedRoutes @@ -478,6 +589,70 @@ func (c *gatewayRouteResolver) routeIsAllowed(gw *v1beta1.Gateway, lis *v1.Liste return false } +// Validate whether a route is allowed by the given ListenerSet Listener. +// EXPERIMENTAL support for XListenerSet. +func (c *gatewayRouteResolver) lsRouteIsAllowed(ls *v1alpha1.XListenerSet, lis *v1.Listener, rt gatewayRoute) bool { + if ls == nil { + // No ListenerSet, so nothing to check. + return false + } + + meta := rt.Metadata() + allow := lis.AllowedRoutes + + // Check the route's namespace. + from := v1.NamespacesFromSame + if allow != nil && allow.Namespaces != nil && allow.Namespaces.From != nil { + from = *allow.Namespaces.From + } + switch from { + case v1.NamespacesFromAll: + // OK + case v1.NamespacesFromSame: + if ls.Namespace != meta.Namespace { + return false + } + case v1.NamespacesFromSelector: + selector, err := metav1.LabelSelectorAsSelector(allow.Namespaces.Selector) + if err != nil { + log.Debugf("Gateway %s/%s section %q has invalid namespace selector: %v", ls.Namespace, ls.Name, lis.Name, err) + return false + } + // Get namespace. + ns, ok := c.nss[meta.Namespace] + if !ok { + log.Errorf("Namespace not found for %s %s/%s", c.src.rtKind, meta.Namespace, meta.Name) + return false + } + if !selector.Matches(labels.Set(ns.Labels)) { + return false + } + default: + log.Debugf("Gateway %s/%s section %q has unknown namespace from %q", ls.Namespace, ls.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 false +} + +// Test whether a route parent reference is of the given group and kind. +func routeParentRefIsType(ref v1.ParentReference, group, kind string) bool { + return strVal((*string)(ref.Group), "") == group && strVal((*string)(ref.Kind), "") == kind +} + func gwRouteHasParentRef(routeParentRefs []v1.ParentReference, ref v1.ParentReference, meta *metav1.ObjectMeta) bool { // Ensure that the parent reference is in the routeParentRefs list namespace := strVal((*string)(ref.Namespace), meta.Namespace) diff --git a/source/gateway_httproute_test.go b/source/gateway_httproute_test.go index 031ae93cf..9b2768065 100644 --- a/source/gateway_httproute_test.go +++ b/source/gateway_httproute_test.go @@ -28,6 +28,7 @@ import ( kubefake "k8s.io/client-go/kubernetes/fake" v1 "sigs.k8s.io/gateway-api/apis/v1" v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + v1alpha1 "sigs.k8s.io/gateway-api/apisx/v1alpha1" gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake" "sigs.k8s.io/external-dns/endpoint" @@ -52,6 +53,10 @@ func gatewayStatus(ips ...string) v1.GatewayStatus { return v1.GatewayStatus{Addresses: addrs} } +func listenerSetStatus(ips ...string) v1alpha1.ListenerSetStatus { + return v1alpha1.ListenerSetStatus{} +} + func httpRouteStatus(refs ...v1.ParentReference) v1.HTTPRouteStatus { return v1.HTTPRouteStatus{RouteStatus: gwRouteStatus(refs...)} } @@ -117,6 +122,33 @@ func gwParentRef(namespace, name string, options ...gwParentRefOption) v1.Parent return ref } +func lsParentRef(namespace, name string, options ...gwParentRefOption) v1.ParentReference { + group := v1alpha1.Group("gateway.networking.x-k8s.io") + kind := v1alpha1.Kind("XListenerSet") + ref := v1.ParentReference{ + Group: &group, + Kind: &kind, + Name: v1.ObjectName(name), + Namespace: (*v1.Namespace)(&namespace), + } + for _, opt := range options { + opt(&ref) + } + return ref +} + +func lsGwParentRef(namespace, name string) v1alpha1.ParentGatewayReference { + group := v1.Group("gateway.networking.k8s.io") + kind := v1.Kind("Gateway") + ref := v1alpha1.ParentGatewayReference{ + Group: &group, + Kind: &kind, + Name: v1.ObjectName(name), + Namespace: (*v1.Namespace)(&namespace), + } + return ref +} + type gwParentRefOption func(*v1.ParentReference) func withSectionName(name v1.SectionName) gwParentRefOption { @@ -169,6 +201,7 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { config Config namespaces []*corev1.Namespace gateways []*v1beta1.Gateway + listenerSets []*v1alpha1.XListenerSet routes []*v1beta1.HTTPRoute endpoints []*endpoint.Endpoint logExpectations []string @@ -1540,6 +1573,151 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { "Parent reference gateway-namespace/other-gateway not found in routeParentRefs for HTTPRoute route-namespace/test", }, }, + { + title: "XListenerSet", + config: Config{ + GatewayEnableExperimental: true, + }, + namespaces: namespaces("default"), + gateways: []*v1beta1.Gateway{{ + ObjectMeta: objectMeta("default", "lsgateway"), + Spec: v1.GatewaySpec{ + Listeners: []v1.Listener{ + { + Name: "foo", + Protocol: v1.HTTPProtocolType, + }, + }, + }, + Status: gatewayStatus("1.2.3.4"), + }}, + listenerSets: []*v1alpha1.XListenerSet{{ + ObjectMeta: objectMeta("default", "test"), + Spec: v1alpha1.ListenerSetSpec{ + ParentRef: lsGwParentRef("default", "lsgateway"), + Listeners: []v1alpha1.ListenerEntry{ + { + Name: "bar", + Protocol: v1.HTTPProtocolType, + Hostname: hostnamePtr("bar.example.internal"), + AllowedRoutes: allowAllNamespaces, + }, + }, + }, + }}, + routes: []*v1beta1.HTTPRoute{{ + ObjectMeta: objectMeta("default", "lstestroute"), + Spec: v1.HTTPRouteSpec{ + Hostnames: hostnames("bar.example.internal"), + CommonRouteSpec: v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + lsParentRef("default", "test"), + }, + }, + }, + Status: httpRouteStatus( + lsParentRef("default", "test"), + ), + }}, + endpoints: []*endpoint.Endpoint{ + newTestEndpoint("bar.example.internal", "A", "1.2.3.4"), + }, + }, + { + title: "XListenerSetAllowedRoutes", + config: Config{ + GatewayEnableExperimental: true, + }, + namespaces: namespaces("default"), + gateways: []*v1beta1.Gateway{{ + ObjectMeta: objectMeta("default", "lsgateway"), + Spec: v1.GatewaySpec{ + Listeners: []v1.Listener{ + { + Name: "foo", + Protocol: v1.HTTPProtocolType, + }, + }, + }, + Status: gatewayStatus("1.2.3.4"), + }}, + listenerSets: []*v1alpha1.XListenerSet{{ + ObjectMeta: objectMeta("default", "test"), + Spec: v1alpha1.ListenerSetSpec{ + ParentRef: lsGwParentRef("default", "lsgateway"), + Listeners: []v1alpha1.ListenerEntry{ + { + Name: "bar", + Protocol: v1.HTTPProtocolType, + Hostname: hostnamePtr("bar.example.internal"), + AllowedRoutes: &v1.AllowedRoutes{ + Namespaces: &v1.RouteNamespaces{ + From: &fromSame, + }, + }, + }, + }, + }, + }}, + routes: []*v1beta1.HTTPRoute{{ + ObjectMeta: objectMeta("other", "lstestroute"), + Spec: v1.HTTPRouteSpec{ + Hostnames: hostnames("bar.example.internal"), + CommonRouteSpec: v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + lsParentRef("default", "test"), + }, + }, + }, + Status: rsWithoutAccepted(httpRouteStatus(lsParentRef("default", "test"))), + }}, + endpoints: []*endpoint.Endpoint{}, + }, + { + title: "XListenerSetNotEnabled", + config: Config{}, + namespaces: namespaces("default"), + gateways: []*v1beta1.Gateway{{ + ObjectMeta: objectMeta("default", "lsgateway"), + Spec: v1.GatewaySpec{ + Listeners: []v1.Listener{ + { + Name: "foo", + Protocol: v1.HTTPProtocolType, + }, + }, + }, + Status: gatewayStatus("1.2.3.4"), + }}, + listenerSets: []*v1alpha1.XListenerSet{{ + ObjectMeta: objectMeta("default", "test"), + Spec: v1alpha1.ListenerSetSpec{ + ParentRef: lsGwParentRef("default", "lsgateway"), + Listeners: []v1alpha1.ListenerEntry{ + { + Name: "bar", + Protocol: v1.HTTPProtocolType, + Hostname: hostnamePtr("bar.example.internal"), + }, + }, + }, + }}, + routes: []*v1beta1.HTTPRoute{{ + ObjectMeta: objectMeta("default", "lstestroute"), + Spec: v1.HTTPRouteSpec{ + Hostnames: hostnames("bar.example.internal"), + CommonRouteSpec: v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + lsParentRef("default", "test"), + }, + }, + }, + Status: httpRouteStatus( + lsParentRef("default", "test"), + ), + }}, + endpoints: []*endpoint.Endpoint{}, + }, } for _, tt := range tests { t.Run(tt.title, func(t *testing.T) { @@ -1552,7 +1730,10 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { for _, gw := range tt.gateways { _, err := gwClient.GatewayV1beta1().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{}) require.NoError(t, err, "failed to create Gateway") - + } + for _, ls := range tt.listenerSets { + _, err := gwClient.ExperimentalV1alpha1().XListenerSets(ls.Namespace).Create(ctx, ls, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create XListenerSet") } for _, rt := range tt.routes { _, err := gwClient.GatewayV1beta1().HTTPRoutes(rt.Namespace).Create(ctx, rt, metav1.CreateOptions{}) diff --git a/source/store.go b/source/store.go index efa08aba4..c4c2be2be 100644 --- a/source/store.go +++ b/source/store.go @@ -72,6 +72,7 @@ type Config struct { IgnoreIngressTLSSpec bool IgnoreIngressRulesSpec bool ListenEndpointEvents bool + GatewayEnableExperimental bool GatewayName string GatewayNamespace string GatewayLabelFilter string @@ -118,6 +119,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { IgnoreIngressTLSSpec: cfg.IgnoreIngressTLSSpec, IgnoreIngressRulesSpec: cfg.IgnoreIngressRulesSpec, ListenEndpointEvents: cfg.ListenEndpointEvents, + GatewayEnableExperimental: cfg.GatewayEnableExperimental, GatewayName: cfg.GatewayName, GatewayNamespace: cfg.GatewayNamespace, GatewayLabelFilter: cfg.GatewayLabelFilter,