external-dns/source/gateway.go
2022-02-24 21:30:10 -08:00

328 lines
10 KiB
Go

/*
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
}