gateway-httproute: add source

This commit is contained in:
Andy Bursavich 2021-08-15 14:24:52 -07:00
parent 48203e64c9
commit 3a1d86be20
6 changed files with 1059 additions and 1 deletions

View File

@ -114,6 +114,8 @@ func main() {
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
IgnoreIngressTLSSpec: cfg.IgnoreIngressTLSSpec,
IgnoreIngressRulesSpec: cfg.IgnoreIngressRulesSpec,
GatewayNamespace: cfg.GatewayNamespace,
GatewayLabelFilter: cfg.GatewayLabelFilter,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,

View File

@ -60,6 +60,8 @@ type Config struct {
IgnoreHostnameAnnotation bool
IgnoreIngressTLSSpec bool
IgnoreIngressRulesSpec bool
GatewayNamespace string
GatewayLabelFilter string
Compatibility string
PublishInternal bool
PublishHostIP bool
@ -204,6 +206,8 @@ var defaultConfig = &Config{
IgnoreHostnameAnnotation: false,
IgnoreIngressTLSSpec: false,
IgnoreIngressRulesSpec: false,
GatewayNamespace: "",
GatewayLabelFilter: "",
Compatibility: "",
PublishInternal: false,
PublishHostIP: false,
@ -375,7 +379,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing source
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress")
app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName)
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
@ -384,6 +388,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
app.Flag("ignore-ingress-tls-spec", "Ignore tls spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressTLSSpec)
app.Flag("gateway-namespace", "Limit Gateways of Route endpoints to a specific namespace (default: all namespaces)").StringVar(&cfg.GatewayNamespace)
app.Flag("gateway-label-filter", "Filter Gateways of Route endpoints via label selector (default: all gateways)").StringVar(&cfg.GatewayLabelFilter)
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule", "kops-dns-controller")
app.Flag("ignore-ingress-rules-spec", "Ignore rules spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressRulesSpec)
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)

327
source/gateway.go Normal file
View File

@ -0,0 +1,327 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"context"
"fmt"
"sort"
"strings"
"text/template"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
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"
informers "sigs.k8s.io/gateway-api/pkg/client/informers/gateway/externalversions"
informers_v1a2 "sigs.k8s.io/gateway-api/pkg/client/informers/gateway/externalversions/apis/v1alpha2"
"sigs.k8s.io/external-dns/endpoint"
)
type gatewayRoute interface {
// Object returns the underlying Route object to be used by templates.
Object() kubeObject
// Metadata returns the Route's metadata.
Metadata() *metav1.ObjectMeta
// Hostnames returns the Route's specified hostnames.
Hostnames() []v1alpha2.Hostname
// Status returns the Route's status, including associated gateways.
Status() v1alpha2.RouteStatus
}
type newGatewayRouteInformerFunc func(informers.SharedInformerFactory) gatewayRouteInfomer
type gatewayRouteInfomer interface {
List(namespace string, selector labels.Selector) ([]gatewayRoute, error)
Informer() cache.SharedIndexInformer
}
func newGatewayInformerFactory(client gateway.Interface, namespace string, labelSelector labels.Selector) informers.SharedInformerFactory {
var opts []informers.SharedInformerOption
if namespace != "" {
opts = append(opts, informers.WithNamespace(namespace))
}
if labelSelector != nil && !labelSelector.Empty() {
lbls := labelSelector.String()
opts = append(opts, informers.WithTweakListOptions(func(o *metav1.ListOptions) {
o.LabelSelector = lbls
}))
}
return informers.NewSharedInformerFactoryWithOptions(client, 0, opts...)
}
type gatewayRouteSource struct {
gwNamespace string
gwLabels labels.Selector
gwInformer informers_v1a2.GatewayInformer
rtKind string
rtNamespace string
rtLabels labels.Selector
rtAnnotations labels.Selector
rtInformer gatewayRouteInfomer
fqdnTemplate *template.Template
combineFQDNAnnotation bool
ignoreHostnameAnnotation bool
}
func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, newInformerFn newGatewayRouteInformerFunc) (Source, error) {
gwLabels, err := getLabelSelector(config.GatewayLabelFilter)
if err != nil {
return nil, err
}
rtLabels := config.LabelFilter
if rtLabels == nil {
rtLabels = labels.Everything()
}
rtAnnotations, err := getLabelSelector(config.AnnotationFilter)
if err != nil {
return nil, err
}
tmpl, err := parseTemplate(config.FQDNTemplate)
if err != nil {
return nil, err
}
client, err := clients.GatewayClient()
if err != nil {
return nil, err
}
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
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
informerFactory.Start(wait.NeverStop)
if rtInformerFactory != informerFactory {
rtInformerFactory.Start(wait.NeverStop)
if err := waitForCacheSync(context.Background(), rtInformerFactory); err != nil {
return nil, err
}
}
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {
return nil, err
}
src := &gatewayRouteSource{
gwNamespace: config.GatewayNamespace,
gwLabels: gwLabels,
gwInformer: gwInformer,
rtKind: kind,
rtNamespace: config.Namespace,
rtLabels: rtLabels,
rtAnnotations: rtAnnotations,
rtInformer: rtInformer,
fqdnTemplate: tmpl,
combineFQDNAnnotation: config.CombineFQDNAndAnnotation,
ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation,
}
return src, nil
}
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))
}
func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
routes, err := src.rtInformer.List(src.rtNamespace, src.rtLabels)
if err != nil {
return nil, err
}
gwList, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
if err != nil {
return nil, err
}
gateways := gatewaysByRef(gwList)
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
}
// 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
}
// Get hostnames.
hostnames, err := src.hostnames(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
}
// 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)
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)...)
}
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) {
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)...)
}
// 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 err != nil {
return nil, err
}
hostnames = append(hostnames, hosts...)
}
return hostnames, nil
}
func (src *gatewayRouteSource) targets(rt gatewayRoute, gateways map[types.NamespacedName]*v1alpha2.Gateway) endpoint.Targets {
var targets endpoint.Targets
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
}
namespace := meta.Namespace
if ref.Namespace != nil {
namespace = string(*ref.Namespace)
}
gw, ok := gateways[types.NamespacedName{
Namespace: namespace,
Name: string(ref.Name),
}]
if !ok {
log.Debugf("Gateway %s/%s not found for %s %s/%s", namespace, ref.Name, src.rtKind, meta.Namespace, meta.Name)
continue
}
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
}
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
}
func gwRouteIsAdmitted(conds []metav1.Condition) bool {
for _, c := range conds {
if v1alpha2.RouteConditionType(c.Type) == v1alpha2.ConditionRouteAccepted {
return c.Status == metav1.ConditionTrue
}
}
return false
}
func gatewaysByRef(list []*v1alpha2.Gateway) map[types.NamespacedName]*v1alpha2.Gateway {
if len(list) == 0 {
return nil
}
set := make(map[types.NamespacedName]*v1alpha2.Gateway, len(list))
for _, gw := range list {
set[types.NamespacedName{Namespace: gw.Namespace, Name: gw.Name}] = gw
}
return set
}
func selectorsEqual(a, b labels.Selector) bool {
if a == nil || b == nil {
return a == b
}
aReq, aOK := a.DeepCopySelector().Requirements()
bReq, bOK := b.DeepCopySelector().Requirements()
if aOK != bOK || len(aReq) != len(bReq) {
return false
}
sort.Stable(labels.ByKey(aReq))
sort.Stable(labels.ByKey(bReq))
for i, r := range aReq {
if !r.Equal(bReq[i]) {
return false
}
}
return true
}

View File

@ -0,0 +1,55 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
informers "sigs.k8s.io/gateway-api/pkg/client/informers/gateway/externalversions"
informers_v1a2 "sigs.k8s.io/gateway-api/pkg/client/informers/gateway/externalversions/apis/v1alpha2"
)
// 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 &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 }
type gatewayHTTPRouteInformer struct {
informers_v1a2.HTTPRouteInformer
}
func (inf gatewayHTTPRouteInformer) List(namespace string, selector labels.Selector) ([]gatewayRoute, error) {
list, err := inf.HTTPRouteInformer.Lister().HTTPRoutes(namespace).List(selector)
if err != nil {
return nil, err
}
routes := make([]gatewayRoute, len(list))
for i, rt := range list {
routes[i] = &gatewayHTTPRoute{rt}
}
return routes, nil
}

View File

@ -0,0 +1,664 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"context"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
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"
)
func mustGetLabelSelector(s string) labels.Selector {
v, err := getLabelSelector(s)
if err != nil {
panic(err)
}
return v
}
func gatewayStatus(ips ...string) v1alpha2.GatewayStatus {
typ := v1alpha2.IPAddressType
addrs := make([]v1alpha2.GatewayAddress, len(ips))
for i, ip := range ips {
addrs[i] = v1alpha2.GatewayAddress{Type: &typ, Value: ip}
}
return v1alpha2.GatewayStatus{Addresses: addrs}
}
func routeStatus(refs ...v1alpha2.ParentRef) v1alpha2.RouteStatus {
var v v1alpha2.RouteStatus
for _, ref := range refs {
v.Parents = append(v.Parents, v1alpha2.RouteParentStatus{
ParentRef: ref,
Conditions: []metav1.Condition{
{
Type: string(v1alpha2.ConditionRouteAccepted),
Status: metav1.ConditionTrue,
},
},
})
}
return v
}
func httpRouteStatus(refs ...v1alpha2.ParentRef) v1alpha2.HTTPRouteStatus {
return v1alpha2.HTTPRouteStatus{RouteStatus: routeStatus(refs...)}
}
func gatewayParentRef(namespace, name string) v1alpha2.ParentRef {
group := v1alpha2.Group("gateway.networking.k8s.io")
kind := v1alpha2.Kind("Gateway")
return v1alpha2.ParentRef{
Group: &group,
Kind: &kind,
Name: v1alpha2.ObjectName(name),
Namespace: (*v1alpha2.Namespace)(&namespace),
}
}
func newTestEndpoint(dnsName, recordType string, targets ...string) *endpoint.Endpoint {
return newTestEndpointWithTTL(dnsName, recordType, 0, targets...)
}
func newTestEndpointWithTTL(dnsName, recordType string, ttl int64, targets ...string) *endpoint.Endpoint {
return &endpoint.Endpoint{
DNSName: dnsName,
Targets: append([]string(nil), targets...), // clone targets
RecordType: recordType,
RecordTTL: endpoint.TTL(ttl),
}
}
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()
objectMeta := func(namespace, name string) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: name,
Namespace: namespace,
}
}
namespaces := func(names ...string) []*corev1.Namespace {
v := make([]*corev1.Namespace, len(names))
for i, name := range names {
v[i] = &corev1.Namespace{ObjectMeta: objectMeta("", name)}
}
return v
}
hostnames := func(names ...v1alpha2.Hostname) []v1alpha2.Hostname { return names }
tests := []struct {
title string
config Config
namespaces []*corev1.Namespace
gateways []*v1alpha2.Gateway
routes []*v1alpha2.HTTPRoute
endpoints []*endpoint.Endpoint
}{
{
title: "GatewayNamespace",
config: Config{
GatewayNamespace: "gateway-namespace",
},
namespaces: namespaces("gateway-namespace", "not-gateway-namespace", "route-namespace"),
gateways: []*v1alpha2.Gateway{
{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
},
{
ObjectMeta: objectMeta("not-gateway-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("2.3.4.5"),
},
},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("route-namespace", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("test.example.internal"),
},
Status: httpRouteStatus( // The route is attached to both gateways.
gatewayParentRef("gateway-namespace", "test"),
gatewayParentRef("not-gateway-namespace", "test"),
),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.example.internal", "A", "1.2.3.4"),
},
},
{
title: "RouteNamespace",
config: Config{
Namespace: "route-namespace",
},
namespaces: namespaces("gateway-namespace", "route-namespace", "not-route-namespace"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: objectMeta("route-namespace", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("route-namespace.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("gateway-namespace", "test")),
},
{
ObjectMeta: objectMeta("not-route-namespace", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("not-route-namespace.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("gateway-namespace", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("route-namespace.example.internal", "A", "1.2.3.4"),
},
},
{
title: "GatewayLabelFilter",
config: Config{
GatewayLabelFilter: "foo=bar",
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{
{
ObjectMeta: metav1.ObjectMeta{
Name: "labels-match",
Namespace: "default",
Labels: map[string]string{"foo": "bar"},
},
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "labels-dont-match",
Namespace: "default",
Labels: map[string]string{"foo": "qux"},
},
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("2.3.4.5"),
},
},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("test.example.internal"),
},
Status: httpRouteStatus( // The route is attached to both gateways.
gatewayParentRef("default", "labels-match"),
gatewayParentRef("default", "labels-dont-match"),
),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.example.internal", "A", "1.2.3.4"),
},
},
{
title: "RouteLabelFilter",
config: Config{
LabelFilter: mustGetLabelSelector("foo=bar"),
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "labels-match",
Namespace: "default",
Labels: map[string]string{"foo": "bar"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("labels-match.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "labels-dont-match",
Namespace: "default",
Labels: map[string]string{"foo": "qux"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("labels-dont-match.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("labels-match.example.internal", "A", "1.2.3.4"),
},
},
{
title: "RouteAnnotationFilter",
config: Config{
AnnotationFilter: "foo=bar",
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "annotations-match",
Namespace: "default",
Annotations: map[string]string{"foo": "bar"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("annotations-match.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "annotations-dont-match",
Namespace: "default",
Annotations: map[string]string{"foo": "qux"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("annotations-dont-match.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("annotations-match.example.internal", "A", "1.2.3.4"),
},
},
{
title: "SkipControllerAnnotation",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: metav1.ObjectMeta{
Name: "api",
Namespace: "default",
Annotations: map[string]string{
controllerAnnotationKey: "something-else",
},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("api.example.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: nil,
},
{
title: "MultipleGateways",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{
{
ObjectMeta: objectMeta("default", "one"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
},
{
ObjectMeta: objectMeta("default", "two"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("2.3.4.5"),
},
},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("test.example.internal"),
},
Status: httpRouteStatus(
gatewayParentRef("default", "one"),
gatewayParentRef("default", "two"),
),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.example.internal", "A", "1.2.3.4", "2.3.4.5"),
},
},
{
title: "NoGateways",
config: Config{},
namespaces: namespaces("default"),
gateways: nil,
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("example.internal"),
},
Status: httpRouteStatus(),
}},
endpoints: nil,
},
{
title: "NoHostnames",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "no-hostame"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: nil,
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: nil,
},
{
title: "HostnameAnnotation",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "without-hostame",
Namespace: "default",
Annotations: map[string]string{
hostnameAnnotationKey: "annotation.without-hostname.internal",
},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: nil,
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "with-hostame",
Namespace: "default",
Annotations: map[string]string{
hostnameAnnotationKey: "annotation.with-hostname.internal",
},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("with-hostname.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("annotation.without-hostname.internal", "A", "1.2.3.4"),
newTestEndpoint("annotation.with-hostname.internal", "A", "1.2.3.4"),
newTestEndpoint("with-hostname.internal", "A", "1.2.3.4"),
},
},
{
title: "IgnoreHostnameAnnotation",
config: Config{
IgnoreHostnameAnnotation: true,
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: metav1.ObjectMeta{
Name: "with-hostame",
Namespace: "default",
Annotations: map[string]string{
hostnameAnnotationKey: "annotation.with-hostname.internal",
},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("with-hostname.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("with-hostname.internal", "A", "1.2.3.4"),
},
},
{
title: "FQDNTemplate",
config: Config{
FQDNTemplate: "{{.Name}}.zero.internal, {{.Name}}.one.internal. , {{.Name}}.two.internal ",
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: objectMeta("default", "fqdn-with-hostnames"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("fqdn-with-hostnames.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: objectMeta("default", "fqdn-without-hostnames"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: nil,
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("fqdn-without-hostnames.zero.internal", "A", "1.2.3.4"),
newTestEndpoint("fqdn-without-hostnames.one.internal", "A", "1.2.3.4"),
newTestEndpoint("fqdn-without-hostnames.two.internal", "A", "1.2.3.4"),
newTestEndpoint("fqdn-with-hostnames.internal", "A", "1.2.3.4"),
},
},
{
title: "CombineFQDN",
config: Config{
FQDNTemplate: "combine-{{.Name}}.internal",
CombineFQDNAndAnnotation: true,
},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: objectMeta("default", "fqdn-with-hostnames"),
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("fqdn-with-hostnames.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("fqdn-with-hostnames.internal", "A", "1.2.3.4"),
newTestEndpoint("combine-fqdn-with-hostnames.internal", "A", "1.2.3.4"),
},
},
{
title: "TTL",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-ttl",
Namespace: "default",
Annotations: map[string]string{ttlAnnotationKey: "15s"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("valid-ttl.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-ttl",
Namespace: "default",
Annotations: map[string]string{ttlAnnotationKey: "abc"},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("invalid-ttl.internal"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("invalid-ttl.internal", "A", "1.2.3.4"),
newTestEndpointWithTTL("valid-ttl.internal", "A", 15, "1.2.3.4"),
},
},
{
title: "ProviderAnnotations",
config: Config{},
namespaces: namespaces("default"),
gateways: []*v1alpha2.Gateway{{
ObjectMeta: objectMeta("default", "test"),
Spec: v1alpha2.GatewaySpec{
Listeners: []v1alpha2.Listener{{Protocol: v1alpha2.HTTPProtocolType}},
},
Status: gatewayStatus("1.2.3.4"),
}},
routes: []*v1alpha2.HTTPRoute{{
ObjectMeta: metav1.ObjectMeta{
Name: "provider-annotations",
Namespace: "default",
Annotations: map[string]string{
SetIdentifierKey: "test-set-identifier",
aliasAnnotationKey: "true",
},
},
Spec: v1alpha2.HTTPRouteSpec{
Hostnames: hostnames("provider-annotations.com"),
},
Status: httpRouteStatus(gatewayParentRef("default", "test")),
}},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("provider-annotations.com", "A", "1.2.3.4").
WithProviderSpecific("alias", "true").
WithSetIdentifier("test-set-identifier"),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.title, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
gwClient := gatewayfake.NewSimpleClientset()
for _, gw := range tt.gateways {
_, err := gwClient.GatewayV1alpha2().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Gateway")
}
for _, rt := range tt.routes {
_, err := gwClient.GatewayV1alpha2().HTTPRoutes(rt.Namespace).Create(ctx, rt, metav1.CreateOptions{})
require.NoError(t, err, "failed to create HTTPRoute")
}
kubeClient := kubefake.NewSimpleClientset()
for _, ns := range tt.namespaces {
_, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err, "failed to create Namespace")
}
clients := new(MockClientGenerator)
clients.On("GatewayClient").Return(gwClient, nil)
clients.On("KubeClient").Return(kubeClient, nil)
src, err := NewGatewayHTTPRouteSource(clients, &tt.config)
require.NoError(t, err, "failed to create Gateway HTTPRoute Source")
endpoints, err := src.Endpoints(ctx)
require.NoError(t, err, "failed to get Endpoints")
validateEndpoints(t, endpoints, tt.endpoints)
})
}
}
func strPtr(val string) *string { return &val }
func hostnamePtr(val v1alpha2.Hostname) *v1alpha2.Hostname { return &val }

View File

@ -51,6 +51,8 @@ type Config struct {
IgnoreHostnameAnnotation bool
IgnoreIngressTLSSpec bool
IgnoreIngressRulesSpec bool
GatewayNamespace string
GatewayLabelFilter string
Compatibility string
PublishInternal bool
PublishHostIP bool
@ -225,6 +227,8 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
return nil, err
}
return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility)
case "gateway-httproute":
return NewGatewayHTTPRouteSource(p, cfg)
case "istio-gateway":
kubernetesClient, err := p.KubeClient()
if err != nil {