mirror of
				https://github.com/kubernetes-sigs/external-dns.git
				synced 2025-10-31 18:50:59 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			277 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2020 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"
 | |
| 
 | |
| 	ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
 | |
| 	"github.com/pkg/errors"
 | |
| 	log "github.com/sirupsen/logrus"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | |
| 	"k8s.io/apimachinery/pkg/labels"
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/apimachinery/pkg/runtime/schema"
 | |
| 	"k8s.io/client-go/dynamic"
 | |
| 	"k8s.io/client-go/dynamic/dynamicinformer"
 | |
| 	"k8s.io/client-go/informers"
 | |
| 	"k8s.io/client-go/kubernetes"
 | |
| 	"k8s.io/client-go/kubernetes/scheme"
 | |
| 	"k8s.io/client-go/tools/cache"
 | |
| 
 | |
| 	"sigs.k8s.io/external-dns/endpoint"
 | |
| )
 | |
| 
 | |
| // ambHostAnnotation is the annotation in the Host that maps to a Service
 | |
| const ambHostAnnotation = "external-dns.ambassador-service"
 | |
| 
 | |
| // groupName is the group name for the Ambassador API
 | |
| const groupName = "getambassador.io"
 | |
| 
 | |
| var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
 | |
| 
 | |
| var ambHostGVR = schemeGroupVersion.WithResource("hosts")
 | |
| 
 | |
| // ambassadorHostSource is an implementation of Source for Ambassador Host objects.
 | |
| // The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
 | |
| // Use targetAnnotationKey to explicitly set Endpoint.
 | |
| type ambassadorHostSource struct {
 | |
| 	dynamicKubeClient      dynamic.Interface
 | |
| 	kubeClient             kubernetes.Interface
 | |
| 	namespace              string
 | |
| 	ambassadorHostInformer informers.GenericInformer
 | |
| 	unstructuredConverter  *unstructuredConverter
 | |
| }
 | |
| 
 | |
| // NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
 | |
| func NewAmbassadorHostSource(
 | |
| 	ctx context.Context,
 | |
| 	dynamicKubeClient dynamic.Interface,
 | |
| 	kubeClient kubernetes.Interface,
 | |
| 	namespace string) (Source, error) {
 | |
| 	var err error
 | |
| 
 | |
| 	// Use shared informer to listen for add/update/delete of Host in the specified namespace.
 | |
| 	// Set resync period to 0, to prevent processing when nothing has changed.
 | |
| 	informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
 | |
| 	ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
 | |
| 
 | |
| 	// Add default resource event handlers to properly initialize informer.
 | |
| 	ambassadorHostInformer.Informer().AddEventHandler(
 | |
| 		cache.ResourceEventHandlerFuncs{
 | |
| 			AddFunc: func(obj interface{}) {
 | |
| 			},
 | |
| 		},
 | |
| 	)
 | |
| 
 | |
| 	informerFactory.Start(ctx.Done())
 | |
| 
 | |
| 	if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	uc, err := newUnstructuredConverter()
 | |
| 	if err != nil {
 | |
| 		return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
 | |
| 	}
 | |
| 
 | |
| 	return &ambassadorHostSource{
 | |
| 		dynamicKubeClient:      dynamicKubeClient,
 | |
| 		kubeClient:             kubeClient,
 | |
| 		namespace:              namespace,
 | |
| 		ambassadorHostInformer: ambassadorHostInformer,
 | |
| 		unstructuredConverter:  uc,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // Endpoints returns endpoint objects for each host-target combination that should be processed.
 | |
| // Retrieves all Hosts in the source's namespace(s).
 | |
| func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
 | |
| 	hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	endpoints := []*endpoint.Endpoint{}
 | |
| 	for _, hostObj := range hosts {
 | |
| 		unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
 | |
| 		if !ok {
 | |
| 			return nil, errors.New("could not convert")
 | |
| 		}
 | |
| 
 | |
| 		host := &ambassador.Host{}
 | |
| 		err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
 | |
| 
 | |
| 		// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
 | |
| 		service, found := host.Annotations[ambHostAnnotation]
 | |
| 		if !found {
 | |
| 			log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		if len(hostEndpoints) == 0 {
 | |
| 			log.Debugf("No endpoints could be generated from Host %s", fullname)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
 | |
| 		endpoints = append(endpoints, hostEndpoints...)
 | |
| 	}
 | |
| 
 | |
| 	for _, ep := range endpoints {
 | |
| 		sort.Sort(ep.Targets)
 | |
| 	}
 | |
| 
 | |
| 	return endpoints, nil
 | |
| }
 | |
| 
 | |
| // endpointsFromHost extracts the endpoints from a Host object
 | |
| func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
 | |
| 	var endpoints []*endpoint.Endpoint
 | |
| 
 | |
| 	providerSpecific := endpoint.ProviderSpecific{}
 | |
| 	setIdentifier := ""
 | |
| 
 | |
| 	annotations := host.Annotations
 | |
| 	ttl, err := getTTLFromAnnotations(annotations)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if host.Spec != nil {
 | |
| 		hostname := host.Spec.Hostname
 | |
| 		if hostname != "" {
 | |
| 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return endpoints, nil
 | |
| }
 | |
| 
 | |
| func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (targets endpoint.Targets, err error) {
 | |
| 	lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	for _, lb := range svc.Status.LoadBalancer.Ingress {
 | |
| 		if lb.IP != "" {
 | |
| 			targets = append(targets, lb.IP)
 | |
| 		}
 | |
| 		if lb.Hostname != "" {
 | |
| 			targets = append(targets, lb.Hostname)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
 | |
| // an Ambassador Host CRD
 | |
| //
 | |
| // This is a thing because Ambassador has historically supported cross-namespace
 | |
| // references using a name.namespace syntax, but here we want to also support
 | |
| // namespace/name.
 | |
| //
 | |
| // Returns namespace, name, error.
 | |
| 
 | |
| func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
 | |
| 	// Start by assuming that we have namespace/name.
 | |
| 	parts := strings.Split(service, "/")
 | |
| 
 | |
| 	if len(parts) == 1 {
 | |
| 		// No "/" at all, so let's try for name.namespace. To be consistent with the
 | |
| 		// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
 | |
| 		// svc.foo.bar uses service "svc" in namespace "foo.bar".
 | |
| 		parts = strings.SplitN(service, ".", 2)
 | |
| 
 | |
| 		if len(parts) == 2 {
 | |
| 			// We got a namespace, great.
 | |
| 			name := parts[0]
 | |
| 			namespace := parts[1]
 | |
| 
 | |
| 			return namespace, name, nil
 | |
| 		}
 | |
| 
 | |
| 		// If here, we have no separator, so the whole string is the service, and
 | |
| 		// we can assume the default namespace.
 | |
| 		name := service
 | |
| 		namespace := "default"
 | |
| 
 | |
| 		return namespace, name, nil
 | |
| 	} else if len(parts) == 2 {
 | |
| 		// This is "namespace/name". Note that the name could be qualified,
 | |
| 		// which is fine.
 | |
| 		namespace := parts[0]
 | |
| 		name := parts[1]
 | |
| 
 | |
| 		return namespace, name, nil
 | |
| 	}
 | |
| 
 | |
| 	// If we got here, this string is simply ill-formatted. Return an error.
 | |
| 	return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
 | |
| }
 | |
| 
 | |
| func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
 | |
| }
 | |
| 
 | |
| // unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
 | |
| type unstructuredConverter struct {
 | |
| 	// scheme holds an initializer for converting Unstructured to a type
 | |
| 	scheme *runtime.Scheme
 | |
| }
 | |
| 
 | |
| // newUnstructuredConverter returns a new unstructuredConverter initialized
 | |
| func newUnstructuredConverter() (*unstructuredConverter, error) {
 | |
| 	uc := &unstructuredConverter{
 | |
| 		scheme: runtime.NewScheme(),
 | |
| 	}
 | |
| 
 | |
| 	// Setup converter to understand custom CRD types
 | |
| 	ambassador.AddToScheme(uc.scheme)
 | |
| 
 | |
| 	// Add the core types we need
 | |
| 	if err := scheme.AddToScheme(uc.scheme); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return uc, nil
 | |
| }
 |