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

     1  /*
     2  Copyright 2018 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  	"os"
    23  	"strings"
    24  
    25  	"k8s.io/apimachinery/pkg/util/wait"
    26  	"k8s.io/apimachinery/pkg/watch"
    27  	"k8s.io/client-go/tools/cache"
    28  
    29  	log "github.com/sirupsen/logrus"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/runtime/serializer"
    35  	"k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/rest"
    37  	"k8s.io/client-go/tools/clientcmd"
    38  
    39  	"sigs.k8s.io/external-dns/endpoint"
    40  )
    41  
    42  // crdSource is an implementation of Source that provides endpoints by listing
    43  // specified CRD and fetching Endpoints embedded in Spec.
    44  type crdSource struct {
    45  	crdClient        rest.Interface
    46  	namespace        string
    47  	crdResource      string
    48  	codec            runtime.ParameterCodec
    49  	annotationFilter string
    50  	labelSelector    labels.Selector
    51  	informer         *cache.SharedInformer
    52  }
    53  
    54  func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error {
    55  	scheme.AddKnownTypes(groupVersion,
    56  		&endpoint.DNSEndpoint{},
    57  		&endpoint.DNSEndpointList{},
    58  	)
    59  	metav1.AddToGroupVersion(scheme, groupVersion)
    60  	return nil
    61  }
    62  
    63  // NewCRDClientForAPIVersionKind return rest client for the given apiVersion and kind of the CRD
    64  func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiServerURL, apiVersion, kind string) (*rest.RESTClient, *runtime.Scheme, error) {
    65  	if kubeConfig == "" {
    66  		if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
    67  			kubeConfig = clientcmd.RecommendedHomeFile
    68  		}
    69  	}
    70  
    71  	config, err := clientcmd.BuildConfigFromFlags(apiServerURL, kubeConfig)
    72  	if err != nil {
    73  		return nil, nil, err
    74  	}
    75  
    76  	groupVersion, err := schema.ParseGroupVersion(apiVersion)
    77  	if err != nil {
    78  		return nil, nil, err
    79  	}
    80  	apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(groupVersion.String())
    81  	if err != nil {
    82  		return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %w", groupVersion.String(), err)
    83  	}
    84  
    85  	var crdAPIResource *metav1.APIResource
    86  	for _, apiResource := range apiResourceList.APIResources {
    87  		if apiResource.Kind == kind {
    88  			crdAPIResource = &apiResource
    89  			break
    90  		}
    91  	}
    92  	if crdAPIResource == nil {
    93  		return nil, nil, fmt.Errorf("unable to find Resource Kind %q in GroupVersion %q", kind, apiVersion)
    94  	}
    95  
    96  	scheme := runtime.NewScheme()
    97  	addKnownTypes(scheme, groupVersion)
    98  
    99  	config.ContentConfig.GroupVersion = &groupVersion
   100  	config.APIPath = "/apis"
   101  	config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)}
   102  
   103  	crdClient, err := rest.UnversionedRESTClientFor(config)
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  	return crdClient, scheme, nil
   108  }
   109  
   110  // NewCRDSource creates a new crdSource with the given config.
   111  func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme, startInformer bool) (Source, error) {
   112  	sourceCrd := crdSource{
   113  		crdResource:      strings.ToLower(kind) + "s",
   114  		namespace:        namespace,
   115  		annotationFilter: annotationFilter,
   116  		labelSelector:    labelSelector,
   117  		crdClient:        crdClient,
   118  		codec:            runtime.NewParameterCodec(scheme),
   119  	}
   120  	if startInformer {
   121  		// external-dns already runs its sync-handler periodically (controlled by `--interval` flag) to ensure any
   122  		// missed or dropped events are handled.  specify a resync period 0 to avoid unnecessary sync handler invocations.
   123  		informer := cache.NewSharedInformer(
   124  			&cache.ListWatch{
   125  				ListFunc: func(lo metav1.ListOptions) (result runtime.Object, err error) {
   126  					return sourceCrd.List(context.TODO(), &lo)
   127  				},
   128  				WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
   129  					return sourceCrd.watch(context.TODO(), &lo)
   130  				},
   131  			},
   132  			&endpoint.DNSEndpoint{},
   133  			0)
   134  		sourceCrd.informer = &informer
   135  		go informer.Run(wait.NeverStop)
   136  	}
   137  	return &sourceCrd, nil
   138  }
   139  
   140  func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) {
   141  	if cs.informer != nil {
   142  		log.Debug("Adding event handler for CRD")
   143  		// Right now there is no way to remove event handler from informer, see:
   144  		// https://github.com/kubernetes/kubernetes/issues/79610
   145  		informer := *cs.informer
   146  		informer.AddEventHandler(
   147  			cache.ResourceEventHandlerFuncs{
   148  				AddFunc: func(obj interface{}) {
   149  					handler()
   150  				},
   151  				UpdateFunc: func(old interface{}, new interface{}) {
   152  					handler()
   153  				},
   154  				DeleteFunc: func(obj interface{}) {
   155  					handler()
   156  				},
   157  			},
   158  		)
   159  	}
   160  }
   161  
   162  // Endpoints returns endpoint objects.
   163  func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   164  	endpoints := []*endpoint.Endpoint{}
   165  
   166  	var (
   167  		result *endpoint.DNSEndpointList
   168  		err    error
   169  	)
   170  
   171  	result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelSelector.String()})
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	result, err = cs.filterByAnnotations(result)
   177  
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	for _, dnsEndpoint := range result.Items {
   183  		// Make sure that all endpoints have targets for A or CNAME type
   184  		crdEndpoints := []*endpoint.Endpoint{}
   185  		for _, ep := range dnsEndpoint.Spec.Endpoints {
   186  			if (ep.RecordType == "CNAME" || ep.RecordType == "A" || ep.RecordType == "AAAA") && len(ep.Targets) < 1 {
   187  				log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.ObjectMeta.Name, ep.DNSName)
   188  				continue
   189  			}
   190  
   191  			illegalTarget := false
   192  			for _, target := range ep.Targets {
   193  				if ep.RecordType != "NAPTR" && strings.HasSuffix(target, ".") {
   194  					illegalTarget = true
   195  					break
   196  				}
   197  				if ep.RecordType == "NAPTR" && !strings.HasSuffix(target, ".") {
   198  					illegalTarget = true
   199  					break
   200  				}
   201  			}
   202  			if illegalTarget {
   203  				log.Warnf("Endpoint %s with DNSName %s has an illegal target. The subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com')", dnsEndpoint.ObjectMeta.Name, ep.DNSName)
   204  				continue
   205  			}
   206  
   207  			if ep.Labels == nil {
   208  				ep.Labels = endpoint.NewLabels()
   209  			}
   210  
   211  			crdEndpoints = append(crdEndpoints, ep)
   212  		}
   213  
   214  		cs.setResourceLabel(&dnsEndpoint, crdEndpoints)
   215  		endpoints = append(endpoints, crdEndpoints...)
   216  
   217  		if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation {
   218  			continue
   219  		}
   220  
   221  		dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation
   222  		// Update the ObservedGeneration
   223  		_, err = cs.UpdateStatus(ctx, &dnsEndpoint)
   224  		if err != nil {
   225  			log.Warnf("Could not update ObservedGeneration of the CRD: %v", err)
   226  		}
   227  	}
   228  
   229  	return endpoints, nil
   230  }
   231  
   232  func (cs *crdSource) setResourceLabel(crd *endpoint.DNSEndpoint, endpoints []*endpoint.Endpoint) {
   233  	for _, ep := range endpoints {
   234  		ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("crd/%s/%s", crd.ObjectMeta.Namespace, crd.ObjectMeta.Name)
   235  	}
   236  }
   237  
   238  func (cs *crdSource) watch(ctx context.Context, opts *metav1.ListOptions) (watch.Interface, error) {
   239  	opts.Watch = true
   240  	return cs.crdClient.Get().
   241  		Namespace(cs.namespace).
   242  		Resource(cs.crdResource).
   243  		VersionedParams(opts, cs.codec).
   244  		Watch(ctx)
   245  }
   246  
   247  func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *endpoint.DNSEndpointList, err error) {
   248  	result = &endpoint.DNSEndpointList{}
   249  	err = cs.crdClient.Get().
   250  		Namespace(cs.namespace).
   251  		Resource(cs.crdResource).
   252  		VersionedParams(opts, cs.codec).
   253  		Do(ctx).
   254  		Into(result)
   255  	return
   256  }
   257  
   258  func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *endpoint.DNSEndpoint) (result *endpoint.DNSEndpoint, err error) {
   259  	result = &endpoint.DNSEndpoint{}
   260  	err = cs.crdClient.Put().
   261  		Namespace(dnsEndpoint.Namespace).
   262  		Resource(cs.crdResource).
   263  		Name(dnsEndpoint.Name).
   264  		SubResource("status").
   265  		Body(dnsEndpoint).
   266  		Do(ctx).
   267  		Into(result)
   268  	return
   269  }
   270  
   271  // filterByAnnotations filters a list of dnsendpoints by a given annotation selector.
   272  func (cs *crdSource) filterByAnnotations(dnsendpoints *endpoint.DNSEndpointList) (*endpoint.DNSEndpointList, error) {
   273  	labelSelector, err := metav1.ParseToLabelSelector(cs.annotationFilter)
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	// empty filter returns original list
   283  	if selector.Empty() {
   284  		return dnsendpoints, nil
   285  	}
   286  
   287  	filteredList := endpoint.DNSEndpointList{}
   288  
   289  	for _, dnsendpoint := range dnsendpoints.Items {
   290  		// convert the dnsendpoint' annotations to an equivalent label selector
   291  		annotations := labels.Set(dnsendpoint.Annotations)
   292  
   293  		// include dnsendpoint if its annotations match the selector
   294  		if selector.Matches(annotations) {
   295  			filteredList.Items = append(filteredList.Items, dnsendpoint)
   296  		}
   297  	}
   298  
   299  	return &filteredList, nil
   300  }