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

     1  /*
     2  Copyright 2020 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  	"strings"
    24  
    25  	ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
    26  	"github.com/pkg/errors"
    27  	log "github.com/sirupsen/logrus"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/client-go/dynamic"
    34  	"k8s.io/client-go/dynamic/dynamicinformer"
    35  	"k8s.io/client-go/informers"
    36  	"k8s.io/client-go/kubernetes"
    37  	"k8s.io/client-go/kubernetes/scheme"
    38  	"k8s.io/client-go/tools/cache"
    39  
    40  	"sigs.k8s.io/external-dns/endpoint"
    41  )
    42  
    43  // ambHostAnnotation is the annotation in the Host that maps to a Service
    44  const ambHostAnnotation = "external-dns.ambassador-service"
    45  
    46  // groupName is the group name for the Ambassador API
    47  const groupName = "getambassador.io"
    48  
    49  var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
    50  
    51  var ambHostGVR = schemeGroupVersion.WithResource("hosts")
    52  
    53  // ambassadorHostSource is an implementation of Source for Ambassador Host objects.
    54  // The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
    55  // Use targetAnnotationKey to explicitly set Endpoint.
    56  type ambassadorHostSource struct {
    57  	dynamicKubeClient      dynamic.Interface
    58  	kubeClient             kubernetes.Interface
    59  	namespace              string
    60  	ambassadorHostInformer informers.GenericInformer
    61  	unstructuredConverter  *unstructuredConverter
    62  }
    63  
    64  // NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
    65  func NewAmbassadorHostSource(
    66  	ctx context.Context,
    67  	dynamicKubeClient dynamic.Interface,
    68  	kubeClient kubernetes.Interface,
    69  	namespace string,
    70  ) (Source, error) {
    71  	var err error
    72  
    73  	// Use shared informer to listen for add/update/delete of Host in the specified namespace.
    74  	// Set resync period to 0, to prevent processing when nothing has changed.
    75  	informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
    76  	ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
    77  
    78  	// Add default resource event handlers to properly initialize informer.
    79  	ambassadorHostInformer.Informer().AddEventHandler(
    80  		cache.ResourceEventHandlerFuncs{
    81  			AddFunc: func(obj interface{}) {
    82  			},
    83  		},
    84  	)
    85  
    86  	informerFactory.Start(ctx.Done())
    87  
    88  	if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	uc, err := newUnstructuredConverter()
    93  	if err != nil {
    94  		return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
    95  	}
    96  
    97  	return &ambassadorHostSource{
    98  		dynamicKubeClient:      dynamicKubeClient,
    99  		kubeClient:             kubeClient,
   100  		namespace:              namespace,
   101  		ambassadorHostInformer: ambassadorHostInformer,
   102  		unstructuredConverter:  uc,
   103  	}, nil
   104  }
   105  
   106  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   107  // Retrieves all Hosts in the source's namespace(s).
   108  func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   109  	hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	endpoints := []*endpoint.Endpoint{}
   115  	for _, hostObj := range hosts {
   116  		unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
   117  		if !ok {
   118  			return nil, errors.New("could not convert")
   119  		}
   120  
   121  		host := &ambassador.Host{}
   122  		err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  
   127  		fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
   128  
   129  		// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
   130  		service, found := host.Annotations[ambHostAnnotation]
   131  		if !found {
   132  			log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
   133  			continue
   134  		}
   135  
   136  		targets := getTargetsFromTargetAnnotation(host.Annotations)
   137  		if len(targets) == 0 {
   138  			targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service)
   139  			if err != nil {
   140  				log.Warningf("Could not find targets for service %s for Host %s: %v", service, fullname, err)
   141  				continue
   142  			}
   143  		}
   144  
   145  		hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
   146  		if err != nil {
   147  			log.Warningf("Could not get endpoints for Host %s", err)
   148  			continue
   149  		}
   150  		if len(hostEndpoints) == 0 {
   151  			log.Debugf("No endpoints could be generated from Host %s", fullname)
   152  			continue
   153  		}
   154  
   155  		log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
   156  		endpoints = append(endpoints, hostEndpoints...)
   157  	}
   158  
   159  	for _, ep := range endpoints {
   160  		sort.Sort(ep.Targets)
   161  	}
   162  
   163  	return endpoints, nil
   164  }
   165  
   166  // endpointsFromHost extracts the endpoints from a Host object
   167  func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
   168  	var endpoints []*endpoint.Endpoint
   169  	annotations := host.Annotations
   170  
   171  	resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name)
   172  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations)
   173  	ttl := getTTLFromAnnotations(annotations, resource)
   174  
   175  	if host.Spec != nil {
   176  		hostname := host.Spec.Hostname
   177  		if hostname != "" {
   178  			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   179  		}
   180  	}
   181  
   182  	return endpoints, nil
   183  }
   184  
   185  func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (endpoint.Targets, error) {
   186  	lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	var targets = extractLoadBalancerTargets(svc, false)
   197  
   198  	return targets, nil
   199  }
   200  
   201  // parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
   202  // an Ambassador Host CRD
   203  //
   204  // This is a thing because Ambassador has historically supported cross-namespace
   205  // references using a name.namespace syntax, but here we want to also support
   206  // namespace/name.
   207  //
   208  // Returns namespace, name, error.
   209  
   210  func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
   211  	// Start by assuming that we have namespace/name.
   212  	parts := strings.Split(service, "/")
   213  
   214  	if len(parts) == 1 {
   215  		// No "/" at all, so let's try for name.namespace. To be consistent with the
   216  		// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
   217  		// svc.foo.bar uses service "svc" in namespace "foo.bar".
   218  		parts = strings.SplitN(service, ".", 2)
   219  
   220  		if len(parts) == 2 {
   221  			// We got a namespace, great.
   222  			name := parts[0]
   223  			namespace := parts[1]
   224  
   225  			return namespace, name, nil
   226  		}
   227  
   228  		// If here, we have no separator, so the whole string is the service, and
   229  		// we can assume the default namespace.
   230  		name := service
   231  		namespace := "default"
   232  
   233  		return namespace, name, nil
   234  	} else if len(parts) == 2 {
   235  		// This is "namespace/name". Note that the name could be qualified,
   236  		// which is fine.
   237  		namespace := parts[0]
   238  		name := parts[1]
   239  
   240  		return namespace, name, nil
   241  	}
   242  
   243  	// If we got here, this string is simply ill-formatted. Return an error.
   244  	return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
   245  }
   246  
   247  func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
   248  }
   249  
   250  // unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
   251  type unstructuredConverter struct {
   252  	// scheme holds an initializer for converting Unstructured to a type
   253  	scheme *runtime.Scheme
   254  }
   255  
   256  // newUnstructuredConverter returns a new unstructuredConverter initialized
   257  func newUnstructuredConverter() (*unstructuredConverter, error) {
   258  	uc := &unstructuredConverter{
   259  		scheme: runtime.NewScheme(),
   260  	}
   261  
   262  	// Setup converter to understand custom CRD types
   263  	ambassador.AddToScheme(uc.scheme)
   264  
   265  	// Add the core types we need
   266  	if err := scheme.AddToScheme(uc.scheme); err != nil {
   267  		return nil, err
   268  	}
   269  
   270  	return uc, nil
   271  }