sigs.k8s.io/external-dns@v0.14.1/source/kong_tcpingress.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package source
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  
    24  	"github.com/pkg/errors"
    25  	log "github.com/sirupsen/logrus"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/client-go/dynamic"
    33  	"k8s.io/client-go/dynamic/dynamicinformer"
    34  	"k8s.io/client-go/informers"
    35  	"k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/kubernetes/scheme"
    37  	"k8s.io/client-go/tools/cache"
    38  
    39  	"sigs.k8s.io/external-dns/endpoint"
    40  )
    41  
    42  var kongGroupdVersionResource = schema.GroupVersionResource{
    43  	Group:    "configuration.konghq.com",
    44  	Version:  "v1beta1",
    45  	Resource: "tcpingresses",
    46  }
    47  
    48  // kongTCPIngressSource is an implementation of Source for Kong TCPIngress objects.
    49  type kongTCPIngressSource struct {
    50  	annotationFilter         string
    51  	ignoreHostnameAnnotation bool
    52  	dynamicKubeClient        dynamic.Interface
    53  	kongTCPIngressInformer   informers.GenericInformer
    54  	kubeClient               kubernetes.Interface
    55  	namespace                string
    56  	unstructuredConverter    *unstructuredConverter
    57  }
    58  
    59  // NewKongTCPIngressSource creates a new kongTCPIngressSource with the given config.
    60  func NewKongTCPIngressSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string, ignoreHostnameAnnotation bool) (Source, error) {
    61  	var err error
    62  
    63  	// Use shared informer to listen for add/update/delete of Host in the specified namespace.
    64  	// Set resync period to 0, to prevent processing when nothing has changed.
    65  	informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
    66  	kongTCPIngressInformer := informerFactory.ForResource(kongGroupdVersionResource)
    67  
    68  	// Add default resource event handlers to properly initialize informer.
    69  	kongTCPIngressInformer.Informer().AddEventHandler(
    70  		cache.ResourceEventHandlerFuncs{
    71  			AddFunc: func(obj interface{}) {
    72  			},
    73  		},
    74  	)
    75  
    76  	informerFactory.Start(ctx.Done())
    77  
    78  	// wait for the local cache to be populated.
    79  	if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	uc, err := newKongUnstructuredConverter()
    84  	if err != nil {
    85  		return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
    86  	}
    87  
    88  	return &kongTCPIngressSource{
    89  		annotationFilter:         annotationFilter,
    90  		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
    91  		dynamicKubeClient:        dynamicKubeClient,
    92  		kongTCPIngressInformer:   kongTCPIngressInformer,
    93  		kubeClient:               kubeClient,
    94  		namespace:                namespace,
    95  		unstructuredConverter:    uc,
    96  	}, nil
    97  }
    98  
    99  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   100  // Retrieves all TCPIngresses in the source's namespace(s).
   101  func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   102  	tis, err := sc.kongTCPIngressInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	var tcpIngresses []*TCPIngress
   108  	for _, tcpIngressObj := range tis {
   109  		unstructuredHost, ok := tcpIngressObj.(*unstructured.Unstructured)
   110  		if !ok {
   111  			return nil, errors.New("could not convert")
   112  		}
   113  
   114  		tcpIngress := &TCPIngress{}
   115  		err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, tcpIngress, nil)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  		tcpIngresses = append(tcpIngresses, tcpIngress)
   120  	}
   121  
   122  	tcpIngresses, err = sc.filterByAnnotations(tcpIngresses)
   123  	if err != nil {
   124  		return nil, errors.Wrap(err, "failed to filter TCPIngresses")
   125  	}
   126  
   127  	var endpoints []*endpoint.Endpoint
   128  	for _, tcpIngress := range tcpIngresses {
   129  		targets := getTargetsFromTargetAnnotation(tcpIngress.Annotations)
   130  		if len(targets) == 0 {
   131  			for _, lb := range tcpIngress.Status.LoadBalancer.Ingress {
   132  				if lb.IP != "" {
   133  					targets = append(targets, lb.IP)
   134  				}
   135  				if lb.Hostname != "" {
   136  					targets = append(targets, lb.Hostname)
   137  				}
   138  			}
   139  		}
   140  
   141  		fullname := fmt.Sprintf("%s/%s", tcpIngress.Namespace, tcpIngress.Name)
   142  
   143  		ingressEndpoints, err := sc.endpointsFromTCPIngress(tcpIngress, targets)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		if len(ingressEndpoints) == 0 {
   148  			log.Debugf("No endpoints could be generated from Host %s", fullname)
   149  			continue
   150  		}
   151  
   152  		log.Debugf("Endpoints generated from TCPIngress: %s: %v", fullname, ingressEndpoints)
   153  		sc.setDualstackLabel(tcpIngress, ingressEndpoints)
   154  		endpoints = append(endpoints, ingressEndpoints...)
   155  	}
   156  
   157  	for _, ep := range endpoints {
   158  		sort.Sort(ep.Targets)
   159  	}
   160  
   161  	return endpoints, nil
   162  }
   163  
   164  // filterByAnnotations filters a list of TCPIngresses by a given annotation selector.
   165  func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) {
   166  	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	// empty filter returns original list
   176  	if selector.Empty() {
   177  		return tcpIngresses, nil
   178  	}
   179  
   180  	filteredList := []*TCPIngress{}
   181  
   182  	for _, tcpIngress := range tcpIngresses {
   183  		// convert the TCPIngress's annotations to an equivalent label selector
   184  		annotations := labels.Set(tcpIngress.Annotations)
   185  
   186  		// include TCPIngress if its annotations match the selector
   187  		if selector.Matches(annotations) {
   188  			filteredList = append(filteredList, tcpIngress)
   189  		}
   190  	}
   191  
   192  	return filteredList, nil
   193  }
   194  
   195  func (sc *kongTCPIngressSource) setDualstackLabel(tcpIngress *TCPIngress, endpoints []*endpoint.Endpoint) {
   196  	val, ok := tcpIngress.Annotations[ALBDualstackAnnotationKey]
   197  	if ok && val == ALBDualstackAnnotationValue {
   198  		log.Debugf("Adding dualstack label to TCPIngress %s/%s.", tcpIngress.Namespace, tcpIngress.Name)
   199  		for _, ep := range endpoints {
   200  			ep.Labels[endpoint.DualstackLabelKey] = "true"
   201  		}
   202  	}
   203  }
   204  
   205  // endpointsFromTCPIngress extracts the endpoints from a TCPIngress object
   206  func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
   207  	var endpoints []*endpoint.Endpoint
   208  
   209  	resource := fmt.Sprintf("tcpingress/%s/%s", tcpIngress.Namespace, tcpIngress.Name)
   210  
   211  	ttl := getTTLFromAnnotations(tcpIngress.Annotations, resource)
   212  
   213  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(tcpIngress.Annotations)
   214  
   215  	if !sc.ignoreHostnameAnnotation {
   216  		hostnameList := getHostnamesFromAnnotations(tcpIngress.Annotations)
   217  		for _, hostname := range hostnameList {
   218  			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   219  		}
   220  	}
   221  
   222  	if tcpIngress.Spec.Rules != nil {
   223  		for _, rule := range tcpIngress.Spec.Rules {
   224  			if rule.Host != "" {
   225  				endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   226  			}
   227  		}
   228  	}
   229  
   230  	return endpoints, nil
   231  }
   232  
   233  func (sc *kongTCPIngressSource) AddEventHandler(ctx context.Context, handler func()) {
   234  	log.Debug("Adding event handler for TCPIngress")
   235  
   236  	// Right now there is no way to remove event handler from informer, see:
   237  	// https://github.com/kubernetes/kubernetes/issues/79610
   238  	sc.kongTCPIngressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
   239  }
   240  
   241  // newUnstructuredConverter returns a new unstructuredConverter initialized
   242  func newKongUnstructuredConverter() (*unstructuredConverter, error) {
   243  	uc := &unstructuredConverter{
   244  		scheme: runtime.NewScheme(),
   245  	}
   246  
   247  	// Add the core types we need
   248  	uc.scheme.AddKnownTypes(kongGroupdVersionResource.GroupVersion(), &TCPIngress{}, &TCPIngressList{})
   249  	if err := scheme.AddToScheme(uc.scheme); err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	return uc, nil
   254  }
   255  
   256  // Kong types based on https://github.com/Kong/kubernetes-ingress-controller/blob/v1.2.0/pkg/apis/configuration/v1beta1/types.go to facilitate testing
   257  // When trying to import them from the Kong repo as a dependency it required upgrading the k8s.io/client-go and k8s.io/apimachinery which seemed
   258  // cause several changes in how the mock clients were working that resulted in a bunch of failures in other tests
   259  // If that is dealt with at some point the below can be removed and replaced with an actual import
   260  type TCPIngress struct {
   261  	metav1.TypeMeta   `json:",inline"`
   262  	metav1.ObjectMeta `json:"metadata,omitempty"`
   263  
   264  	Spec   tcpIngressSpec   `json:"spec,omitempty"`
   265  	Status tcpIngressStatus `json:"status,omitempty"`
   266  }
   267  
   268  type TCPIngressList struct {
   269  	metav1.TypeMeta `json:",inline"`
   270  	metav1.ListMeta `json:"metadata,omitempty"`
   271  	Items           []TCPIngress `json:"items"`
   272  }
   273  
   274  type tcpIngressSpec struct {
   275  	Rules []tcpIngressRule `json:"rules,omitempty"`
   276  	TLS   []tcpIngressTLS  `json:"tls,omitempty"`
   277  }
   278  
   279  type tcpIngressTLS struct {
   280  	Hosts      []string `json:"hosts,omitempty"`
   281  	SecretName string   `json:"secretName,omitempty"`
   282  }
   283  
   284  type tcpIngressStatus struct {
   285  	LoadBalancer corev1.LoadBalancerStatus `json:"loadBalancer,omitempty"`
   286  }
   287  
   288  type tcpIngressRule struct {
   289  	Host    string            `json:"host,omitempty"`
   290  	Port    int               `json:"port,omitempty"`
   291  	Backend tcpIngressBackend `json:"backend"`
   292  }
   293  
   294  type tcpIngressBackend struct {
   295  	ServiceName string `json:"serviceName"`
   296  	ServicePort int    `json:"servicePort"`
   297  }
   298  
   299  func (in *tcpIngressBackend) DeepCopyInto(out *tcpIngressBackend) {
   300  	*out = *in
   301  }
   302  
   303  func (in *tcpIngressBackend) DeepCopy() *tcpIngressBackend {
   304  	if in == nil {
   305  		return nil
   306  	}
   307  	out := new(tcpIngressBackend)
   308  	in.DeepCopyInto(out)
   309  	return out
   310  }
   311  
   312  func (in *tcpIngressRule) DeepCopyInto(out *tcpIngressRule) {
   313  	*out = *in
   314  	out.Backend = in.Backend
   315  }
   316  
   317  func (in *tcpIngressRule) DeepCopy() *tcpIngressRule {
   318  	if in == nil {
   319  		return nil
   320  	}
   321  	out := new(tcpIngressRule)
   322  	in.DeepCopyInto(out)
   323  	return out
   324  }
   325  
   326  func (in *tcpIngressSpec) DeepCopyInto(out *tcpIngressSpec) {
   327  	*out = *in
   328  	if in.Rules != nil {
   329  		in, out := &in.Rules, &out.Rules
   330  		*out = make([]tcpIngressRule, len(*in))
   331  		copy(*out, *in)
   332  	}
   333  	if in.TLS != nil {
   334  		in, out := &in.TLS, &out.TLS
   335  		*out = make([]tcpIngressTLS, len(*in))
   336  		for i := range *in {
   337  			(*in)[i].DeepCopyInto(&(*out)[i])
   338  		}
   339  	}
   340  }
   341  
   342  func (in *tcpIngressSpec) DeepCopy() *tcpIngressSpec {
   343  	if in == nil {
   344  		return nil
   345  	}
   346  	out := new(tcpIngressSpec)
   347  	in.DeepCopyInto(out)
   348  	return out
   349  }
   350  
   351  func (in *tcpIngressStatus) DeepCopyInto(out *tcpIngressStatus) {
   352  	*out = *in
   353  	in.LoadBalancer.DeepCopyInto(&out.LoadBalancer)
   354  }
   355  
   356  func (in *tcpIngressStatus) DeepCopy() *tcpIngressStatus {
   357  	if in == nil {
   358  		return nil
   359  	}
   360  	out := new(tcpIngressStatus)
   361  	in.DeepCopyInto(out)
   362  	return out
   363  }
   364  
   365  func (in *tcpIngressTLS) DeepCopyInto(out *tcpIngressTLS) {
   366  	*out = *in
   367  	if in.Hosts != nil {
   368  		in, out := &in.Hosts, &out.Hosts
   369  		*out = make([]string, len(*in))
   370  		copy(*out, *in)
   371  	}
   372  }
   373  
   374  func (in *tcpIngressTLS) DeepCopy() *tcpIngressTLS {
   375  	if in == nil {
   376  		return nil
   377  	}
   378  	out := new(tcpIngressTLS)
   379  	in.DeepCopyInto(out)
   380  	return out
   381  }
   382  
   383  func (in *TCPIngress) DeepCopyInto(out *TCPIngress) {
   384  	*out = *in
   385  	out.TypeMeta = in.TypeMeta
   386  	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
   387  	in.Spec.DeepCopyInto(&out.Spec)
   388  	in.Status.DeepCopyInto(&out.Status)
   389  }
   390  
   391  func (in *TCPIngress) DeepCopy() *TCPIngress {
   392  	if in == nil {
   393  		return nil
   394  	}
   395  	out := new(TCPIngress)
   396  	in.DeepCopyInto(out)
   397  	return out
   398  }
   399  
   400  func (in *TCPIngress) DeepCopyObject() runtime.Object {
   401  	if c := in.DeepCopy(); c != nil {
   402  		return c
   403  	}
   404  	return nil
   405  }
   406  
   407  func (in *TCPIngressList) DeepCopyInto(out *TCPIngressList) {
   408  	*out = *in
   409  	out.TypeMeta = in.TypeMeta
   410  	in.ListMeta.DeepCopyInto(&out.ListMeta)
   411  	if in.Items != nil {
   412  		in, out := &in.Items, &out.Items
   413  		*out = make([]TCPIngress, len(*in))
   414  		for i := range *in {
   415  			(*in)[i].DeepCopyInto(&(*out)[i])
   416  		}
   417  	}
   418  }
   419  
   420  func (in *TCPIngressList) DeepCopy() *TCPIngressList {
   421  	if in == nil {
   422  		return nil
   423  	}
   424  	out := new(TCPIngressList)
   425  	in.DeepCopyInto(out)
   426  	return out
   427  }
   428  
   429  func (in *TCPIngressList) DeepCopyObject() runtime.Object {
   430  	if c := in.DeepCopy(); c != nil {
   431  		return c
   432  	}
   433  	return nil
   434  }