sigs.k8s.io/external-dns@v0.14.1/provider/awssd/aws_sd.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 awssd
    18  
    19  import (
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/hex"
    23  	"fmt"
    24  	"regexp"
    25  	"strings"
    26  
    27  	"github.com/aws/aws-sdk-go/aws"
    28  	"github.com/aws/aws-sdk-go/aws/request"
    29  	sd "github.com/aws/aws-sdk-go/service/servicediscovery"
    30  	log "github.com/sirupsen/logrus"
    31  
    32  	"sigs.k8s.io/external-dns/endpoint"
    33  	"sigs.k8s.io/external-dns/plan"
    34  	"sigs.k8s.io/external-dns/provider"
    35  )
    36  
    37  const (
    38  	sdDefaultRecordTTL = 300
    39  
    40  	sdNamespaceTypePublic  = "public"
    41  	sdNamespaceTypePrivate = "private"
    42  
    43  	sdInstanceAttrIPV4  = "AWS_INSTANCE_IPV4"
    44  	sdInstanceAttrCname = "AWS_INSTANCE_CNAME"
    45  	sdInstanceAttrAlias = "AWS_ALIAS_DNS_NAME"
    46  )
    47  
    48  var (
    49  	// matches ELB with hostname format load-balancer.us-east-1.elb.amazonaws.com
    50  	sdElbHostnameRegex = regexp.MustCompile(`.+\.[^.]+\.elb\.amazonaws\.com$`)
    51  
    52  	// matches NLB with hostname format load-balancer.elb.us-east-1.amazonaws.com
    53  	sdNlbHostnameRegex = regexp.MustCompile(`.+\.elb\.[^.]+\.amazonaws\.com$`)
    54  )
    55  
    56  // AWSSDClient is the subset of the AWS Cloud Map API that we actually use. Add methods as required.
    57  // Signatures must match exactly. Taken from https://github.com/aws/aws-sdk-go/blob/HEAD/service/servicediscovery/api.go
    58  type AWSSDClient interface {
    59  	CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error)
    60  	DeregisterInstance(input *sd.DeregisterInstanceInput) (*sd.DeregisterInstanceOutput, error)
    61  	DiscoverInstancesWithContext(ctx aws.Context, input *sd.DiscoverInstancesInput, opts ...request.Option) (*sd.DiscoverInstancesOutput, error)
    62  	ListNamespacesPages(input *sd.ListNamespacesInput, fn func(*sd.ListNamespacesOutput, bool) bool) error
    63  	ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error
    64  	RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error)
    65  	UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error)
    66  	DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error)
    67  }
    68  
    69  // AWSSDProvider is an implementation of Provider for AWS Cloud Map.
    70  type AWSSDProvider struct {
    71  	provider.BaseProvider
    72  	client AWSSDClient
    73  	dryRun bool
    74  	// only consider namespaces ending in this suffix
    75  	namespaceFilter endpoint.DomainFilter
    76  	// filter namespace by type (private or public)
    77  	namespaceTypeFilter *sd.NamespaceFilter
    78  	// enables service without instances cleanup
    79  	cleanEmptyService bool
    80  	// filter services for removal
    81  	ownerID string
    82  }
    83  
    84  // NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
    85  func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, client AWSSDClient) (*AWSSDProvider, error) {
    86  	provider := &AWSSDProvider{
    87  		client:              client,
    88  		dryRun:              dryRun,
    89  		namespaceFilter:     domainFilter,
    90  		namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
    91  		cleanEmptyService:   cleanEmptyService,
    92  		ownerID:             ownerID,
    93  	}
    94  
    95  	return provider, nil
    96  }
    97  
    98  // newSdNamespaceFilter initialized AWS SD Namespace Filter based on given string config
    99  func newSdNamespaceFilter(namespaceTypeConfig string) *sd.NamespaceFilter {
   100  	switch namespaceTypeConfig {
   101  	case sdNamespaceTypePublic:
   102  		return &sd.NamespaceFilter{
   103  			Name:   aws.String(sd.NamespaceFilterNameType),
   104  			Values: []*string{aws.String(sd.NamespaceTypeDnsPublic)},
   105  		}
   106  	case sdNamespaceTypePrivate:
   107  		return &sd.NamespaceFilter{
   108  			Name:   aws.String(sd.NamespaceFilterNameType),
   109  			Values: []*string{aws.String(sd.NamespaceTypeDnsPrivate)},
   110  		}
   111  	default:
   112  		return nil
   113  	}
   114  }
   115  
   116  // Records returns list of all endpoints.
   117  func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
   118  	namespaces, err := p.ListNamespaces()
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	for _, ns := range namespaces {
   124  		services, err := p.ListServicesByNamespaceID(ns.Id)
   125  		if err != nil {
   126  			return nil, err
   127  		}
   128  
   129  		for _, srv := range services {
   130  			resp, err := p.client.DiscoverInstancesWithContext(ctx, &sd.DiscoverInstancesInput{
   131  				NamespaceName: ns.Name,
   132  				ServiceName:   srv.Name,
   133  			})
   134  			if err != nil {
   135  				return nil, err
   136  			}
   137  
   138  			if len(resp.Instances) == 0 {
   139  				if err := p.DeleteService(srv); err != nil {
   140  					log.Errorf("Failed to delete service %q, error: %s", aws.StringValue(srv.Name), err)
   141  				}
   142  				continue
   143  			}
   144  
   145  			endpoints = append(endpoints, p.instancesToEndpoint(ns, srv, resp.Instances))
   146  		}
   147  	}
   148  
   149  	return endpoints, nil
   150  }
   151  
   152  func (p *AWSSDProvider) instancesToEndpoint(ns *sd.NamespaceSummary, srv *sd.Service, instances []*sd.HttpInstanceSummary) *endpoint.Endpoint {
   153  	// DNS name of the record is a concatenation of service and namespace
   154  	recordName := *srv.Name + "." + *ns.Name
   155  
   156  	labels := endpoint.NewLabels()
   157  	labels[endpoint.AWSSDDescriptionLabel] = aws.StringValue(srv.Description)
   158  
   159  	newEndpoint := &endpoint.Endpoint{
   160  		DNSName:   recordName,
   161  		RecordTTL: endpoint.TTL(aws.Int64Value(srv.DnsConfig.DnsRecords[0].TTL)),
   162  		Targets:   make(endpoint.Targets, 0, len(instances)),
   163  		Labels:    labels,
   164  	}
   165  
   166  	for _, inst := range instances {
   167  		// CNAME
   168  		if inst.Attributes[sdInstanceAttrCname] != nil && aws.StringValue(srv.DnsConfig.DnsRecords[0].Type) == sd.RecordTypeCname {
   169  			newEndpoint.RecordType = endpoint.RecordTypeCNAME
   170  			newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrCname]))
   171  
   172  			// ALIAS
   173  		} else if inst.Attributes[sdInstanceAttrAlias] != nil {
   174  			newEndpoint.RecordType = endpoint.RecordTypeCNAME
   175  			newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrAlias]))
   176  
   177  			// IP-based target
   178  		} else if inst.Attributes[sdInstanceAttrIPV4] != nil {
   179  			newEndpoint.RecordType = endpoint.RecordTypeA
   180  			newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrIPV4]))
   181  		} else {
   182  			log.Warnf("Invalid instance \"%v\" found in service \"%v\"", inst, srv.Name)
   183  		}
   184  	}
   185  
   186  	return newEndpoint
   187  }
   188  
   189  // ApplyChanges applies Kubernetes changes in endpoints to AWS API
   190  func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   191  	// return early if there is nothing to change
   192  	if len(changes.Create) == 0 && len(changes.Delete) == 0 && len(changes.UpdateNew) == 0 {
   193  		log.Info("All records are already up to date")
   194  		return nil
   195  	}
   196  
   197  	// convert updates to delete and create operation if applicable (updates not supported)
   198  	creates, deletes := p.updatesToCreates(changes)
   199  	changes.Delete = append(changes.Delete, deletes...)
   200  	changes.Create = append(changes.Create, creates...)
   201  
   202  	namespaces, err := p.ListNamespaces()
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	// Deletes must be executed first to support update case.
   208  	// When just list of targets is updated `[1.2.3.4] -> [1.2.3.4, 1.2.3.5]` it is translated to:
   209  	// ```
   210  	// deletes = [1.2.3.4]
   211  	// creates = [1.2.3.4, 1.2.3.5]
   212  	// ```
   213  	// then when deletes are executed after creates it will miss the `1.2.3.4` instance.
   214  	err = p.submitDeletes(namespaces, changes.Delete)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	err = p.submitCreates(namespaces, changes.Create)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) (creates []*endpoint.Endpoint, deletes []*endpoint.Endpoint) {
   228  	updateNewMap := map[string]*endpoint.Endpoint{}
   229  	for _, e := range changes.UpdateNew {
   230  		updateNewMap[e.DNSName] = e
   231  	}
   232  
   233  	for _, old := range changes.UpdateOld {
   234  		current := updateNewMap[old.DNSName]
   235  
   236  		if !old.Targets.Same(current.Targets) {
   237  			// when targets differ the old instances need to be de-registered first
   238  			deletes = append(deletes, old)
   239  		}
   240  
   241  		// always register (or re-register) instance with the current data
   242  		creates = append(creates, current)
   243  	}
   244  
   245  	return creates, deletes
   246  }
   247  
   248  func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) error {
   249  	changesByNamespaceID := p.changesByNamespaceID(namespaces, changes)
   250  
   251  	for nsID, changeList := range changesByNamespaceID {
   252  		services, err := p.ListServicesByNamespaceID(aws.String(nsID))
   253  		if err != nil {
   254  			return err
   255  		}
   256  
   257  		for _, ch := range changeList {
   258  			_, srvName := p.parseHostname(ch.DNSName)
   259  
   260  			srv := services[srvName]
   261  			if srv == nil {
   262  				// when service is missing create a new one
   263  				srv, err = p.CreateService(&nsID, &srvName, ch)
   264  				if err != nil {
   265  					return err
   266  				}
   267  				// update local list of services
   268  				services[*srv.Name] = srv
   269  			} else if ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL) {
   270  				// update service when TTL differ
   271  				err = p.UpdateService(srv, ch)
   272  				if err != nil {
   273  					return err
   274  				}
   275  			}
   276  
   277  			err = p.RegisterInstance(srv, ch)
   278  			if err != nil {
   279  				return err
   280  			}
   281  		}
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  func (p *AWSSDProvider) submitDeletes(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) error {
   288  	changesByNamespaceID := p.changesByNamespaceID(namespaces, changes)
   289  
   290  	for nsID, changeList := range changesByNamespaceID {
   291  		services, err := p.ListServicesByNamespaceID(aws.String(nsID))
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		for _, ch := range changeList {
   297  			hostname := ch.DNSName
   298  			_, srvName := p.parseHostname(hostname)
   299  
   300  			srv := services[srvName]
   301  			if srv == nil {
   302  				return fmt.Errorf("service \"%s\" is missing when trying to delete \"%v\"", srvName, hostname)
   303  			}
   304  
   305  			err := p.DeregisterInstance(srv, ch)
   306  			if err != nil {
   307  				return err
   308  			}
   309  		}
   310  	}
   311  
   312  	return nil
   313  }
   314  
   315  // ListNamespaces returns all namespaces matching defined namespace filter
   316  func (p *AWSSDProvider) ListNamespaces() ([]*sd.NamespaceSummary, error) {
   317  	namespaces := make([]*sd.NamespaceSummary, 0)
   318  
   319  	f := func(resp *sd.ListNamespacesOutput, lastPage bool) bool {
   320  		for _, ns := range resp.Namespaces {
   321  			if !p.namespaceFilter.Match(aws.StringValue(ns.Name)) {
   322  				continue
   323  			}
   324  			namespaces = append(namespaces, ns)
   325  		}
   326  
   327  		return true
   328  	}
   329  
   330  	err := p.client.ListNamespacesPages(&sd.ListNamespacesInput{
   331  		Filters: []*sd.NamespaceFilter{p.namespaceTypeFilter},
   332  	}, f)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	return namespaces, nil
   338  }
   339  
   340  // ListServicesByNamespaceID returns list of services in given namespace. Returns map[srv_name]*sd.Service
   341  func (p *AWSSDProvider) ListServicesByNamespaceID(namespaceID *string) (map[string]*sd.Service, error) {
   342  	services := make([]*sd.ServiceSummary, 0)
   343  
   344  	f := func(resp *sd.ListServicesOutput, lastPage bool) bool {
   345  		services = append(services, resp.Services...)
   346  		return true
   347  	}
   348  
   349  	err := p.client.ListServicesPages(&sd.ListServicesInput{
   350  		Filters: []*sd.ServiceFilter{{
   351  			Name:   aws.String(sd.ServiceFilterNameNamespaceId),
   352  			Values: []*string{namespaceID},
   353  		}},
   354  		MaxResults: aws.Int64(100),
   355  	}, f)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	servicesMap := make(map[string]*sd.Service)
   361  	for _, serviceSummary := range services {
   362  		service := &sd.Service{
   363  			Arn:                     serviceSummary.Arn,
   364  			CreateDate:              serviceSummary.CreateDate,
   365  			Description:             serviceSummary.Description,
   366  			DnsConfig:               serviceSummary.DnsConfig,
   367  			HealthCheckConfig:       serviceSummary.HealthCheckConfig,
   368  			HealthCheckCustomConfig: serviceSummary.HealthCheckCustomConfig,
   369  			Id:                      serviceSummary.Id,
   370  			InstanceCount:           serviceSummary.InstanceCount,
   371  			Name:                    serviceSummary.Name,
   372  			NamespaceId:             namespaceID,
   373  			Type:                    serviceSummary.Type,
   374  		}
   375  
   376  		servicesMap[aws.StringValue(service.Name)] = service
   377  	}
   378  	return servicesMap, nil
   379  }
   380  
   381  // CreateService creates a new service in AWS API. Returns the created service.
   382  func (p *AWSSDProvider) CreateService(namespaceID *string, srvName *string, ep *endpoint.Endpoint) (*sd.Service, error) {
   383  	log.Infof("Creating a new service \"%s\" in \"%s\" namespace", *srvName, *namespaceID)
   384  
   385  	srvType := p.serviceTypeFromEndpoint(ep)
   386  	routingPolicy := p.routingPolicyFromEndpoint(ep)
   387  
   388  	ttl := int64(sdDefaultRecordTTL)
   389  	if ep.RecordTTL.IsConfigured() {
   390  		ttl = int64(ep.RecordTTL)
   391  	}
   392  
   393  	if !p.dryRun {
   394  		out, err := p.client.CreateService(&sd.CreateServiceInput{
   395  			Name:        srvName,
   396  			Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]),
   397  			DnsConfig: &sd.DnsConfig{
   398  				RoutingPolicy: aws.String(routingPolicy),
   399  				DnsRecords: []*sd.DnsRecord{{
   400  					Type: aws.String(srvType),
   401  					TTL:  aws.Int64(ttl),
   402  				}},
   403  			},
   404  			NamespaceId: namespaceID,
   405  		})
   406  		if err != nil {
   407  			return nil, err
   408  		}
   409  
   410  		return out.Service, nil
   411  	}
   412  
   413  	// return mock service summary in case of dry run
   414  	return &sd.Service{Id: aws.String("dry-run-service"), Name: aws.String("dry-run-service")}, nil
   415  }
   416  
   417  // UpdateService updates the specified service with information from provided endpoint.
   418  func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint) error {
   419  	log.Infof("Updating service \"%s\"", *service.Name)
   420  
   421  	srvType := p.serviceTypeFromEndpoint(ep)
   422  
   423  	ttl := int64(sdDefaultRecordTTL)
   424  	if ep.RecordTTL.IsConfigured() {
   425  		ttl = int64(ep.RecordTTL)
   426  	}
   427  
   428  	if !p.dryRun {
   429  		_, err := p.client.UpdateService(&sd.UpdateServiceInput{
   430  			Id: service.Id,
   431  			Service: &sd.ServiceChange{
   432  				Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]),
   433  				DnsConfig: &sd.DnsConfigChange{
   434  					DnsRecords: []*sd.DnsRecord{{
   435  						Type: aws.String(srvType),
   436  						TTL:  aws.Int64(ttl),
   437  					}},
   438  				},
   439  			},
   440  		})
   441  		if err != nil {
   442  			return err
   443  		}
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  // DeleteService deletes empty Service from AWS API if its owner id match
   450  func (p *AWSSDProvider) DeleteService(service *sd.Service) error {
   451  	log.Debugf("Check if service \"%s\" owner id match and it can be deleted", *service.Name)
   452  	if !p.dryRun && p.cleanEmptyService {
   453  		// convert ownerID string to service description format
   454  		label := endpoint.NewLabels()
   455  		label[endpoint.OwnerLabelKey] = p.ownerID
   456  		label[endpoint.AWSSDDescriptionLabel] = label.SerializePlain(false)
   457  
   458  		if strings.HasPrefix(aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel]) {
   459  			log.Infof("Deleting service \"%s\"", *service.Name)
   460  			_, err := p.client.DeleteService(&sd.DeleteServiceInput{
   461  				Id: aws.String(*service.Id),
   462  			})
   463  			return err
   464  		}
   465  		log.Debugf("Skipping service removal %s because owner id does not match, found: \"%s\", required: \"%s\"", aws.StringValue(service.Name), aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel])
   466  	}
   467  	return nil
   468  }
   469  
   470  // RegisterInstance creates a new instance in given service.
   471  func (p *AWSSDProvider) RegisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
   472  	for _, target := range ep.Targets {
   473  		log.Infof("Registering a new instance \"%s\" for service \"%s\" (%s)", target, *service.Name, *service.Id)
   474  
   475  		attr := make(map[string]*string)
   476  
   477  		if ep.RecordType == endpoint.RecordTypeCNAME {
   478  			if p.isAWSLoadBalancer(target) {
   479  				attr[sdInstanceAttrAlias] = aws.String(target)
   480  			} else {
   481  				attr[sdInstanceAttrCname] = aws.String(target)
   482  			}
   483  		} else if ep.RecordType == endpoint.RecordTypeA {
   484  			attr[sdInstanceAttrIPV4] = aws.String(target)
   485  		} else {
   486  			return fmt.Errorf("invalid endpoint type (%v)", ep)
   487  		}
   488  
   489  		if !p.dryRun {
   490  			_, err := p.client.RegisterInstance(&sd.RegisterInstanceInput{
   491  				ServiceId:  service.Id,
   492  				Attributes: attr,
   493  				InstanceId: aws.String(p.targetToInstanceID(target)),
   494  			})
   495  			if err != nil {
   496  				return err
   497  			}
   498  		}
   499  	}
   500  
   501  	return nil
   502  }
   503  
   504  // DeregisterInstance removes an instance from given service.
   505  func (p *AWSSDProvider) DeregisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
   506  	for _, target := range ep.Targets {
   507  		log.Infof("De-registering an instance \"%s\" for service \"%s\" (%s)", target, *service.Name, *service.Id)
   508  
   509  		if !p.dryRun {
   510  			_, err := p.client.DeregisterInstance(&sd.DeregisterInstanceInput{
   511  				InstanceId: aws.String(p.targetToInstanceID(target)),
   512  				ServiceId:  service.Id,
   513  			})
   514  			if err != nil {
   515  				return err
   516  			}
   517  		}
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  // Instance ID length is limited by AWS API to 64 characters. For longer strings SHA-256 hash will be used instead of
   524  // the verbatim target to limit the length.
   525  func (p *AWSSDProvider) targetToInstanceID(target string) string {
   526  	if len(target) > 64 {
   527  		hash := sha256.Sum256([]byte(strings.ToLower(target)))
   528  		return hex.EncodeToString(hash[:])
   529  	}
   530  
   531  	return strings.ToLower(target)
   532  }
   533  
   534  // nolint: deadcode
   535  // used from unit test
   536  func namespaceToNamespaceSummary(namespace *sd.Namespace) *sd.NamespaceSummary {
   537  	if namespace == nil {
   538  		return nil
   539  	}
   540  
   541  	return &sd.NamespaceSummary{
   542  		Id:   namespace.Id,
   543  		Type: namespace.Type,
   544  		Name: namespace.Name,
   545  		Arn:  namespace.Arn,
   546  	}
   547  }
   548  
   549  // nolint: deadcode
   550  // used from unit test
   551  func serviceToServiceSummary(service *sd.Service) *sd.ServiceSummary {
   552  	if service == nil {
   553  		return nil
   554  	}
   555  
   556  	return &sd.ServiceSummary{
   557  		Arn:                     service.Arn,
   558  		CreateDate:              service.CreateDate,
   559  		Description:             service.Description,
   560  		DnsConfig:               service.DnsConfig,
   561  		HealthCheckConfig:       service.HealthCheckConfig,
   562  		HealthCheckCustomConfig: service.HealthCheckCustomConfig,
   563  		Id:                      service.Id,
   564  		InstanceCount:           service.InstanceCount,
   565  		Name:                    service.Name,
   566  		Type:                    service.Type,
   567  	}
   568  }
   569  
   570  func (p *AWSSDProvider) changesByNamespaceID(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
   571  	changesByNsID := make(map[string][]*endpoint.Endpoint)
   572  
   573  	for _, ns := range namespaces {
   574  		changesByNsID[*ns.Id] = []*endpoint.Endpoint{}
   575  	}
   576  
   577  	for _, c := range changes {
   578  		// trim the trailing dot from hostname if any
   579  		hostname := strings.TrimSuffix(c.DNSName, ".")
   580  		nsName, _ := p.parseHostname(hostname)
   581  
   582  		matchingNamespaces := matchingNamespaces(nsName, namespaces)
   583  		if len(matchingNamespaces) == 0 {
   584  			log.Warnf("Skipping record %s because no namespace matching record DNS Name was detected ", c.String())
   585  			continue
   586  		}
   587  		for _, ns := range matchingNamespaces {
   588  			changesByNsID[*ns.Id] = append(changesByNsID[*ns.Id], c)
   589  		}
   590  	}
   591  
   592  	// separating a change could lead to empty sub changes, remove them here.
   593  	for zone, change := range changesByNsID {
   594  		if len(change) == 0 {
   595  			delete(changesByNsID, zone)
   596  		}
   597  	}
   598  
   599  	return changesByNsID
   600  }
   601  
   602  // returns list of all namespaces matching given hostname
   603  func matchingNamespaces(hostname string, namespaces []*sd.NamespaceSummary) []*sd.NamespaceSummary {
   604  	matchingNamespaces := make([]*sd.NamespaceSummary, 0)
   605  
   606  	for _, ns := range namespaces {
   607  		if *ns.Name == hostname {
   608  			matchingNamespaces = append(matchingNamespaces, ns)
   609  		}
   610  	}
   611  
   612  	return matchingNamespaces
   613  }
   614  
   615  // parse hostname to namespace (domain) and service
   616  func (p *AWSSDProvider) parseHostname(hostname string) (namespace string, service string) {
   617  	parts := strings.Split(hostname, ".")
   618  	service = parts[0]
   619  	namespace = strings.Join(parts[1:], ".")
   620  	return
   621  }
   622  
   623  // determine service routing policy based on endpoint type
   624  func (p *AWSSDProvider) routingPolicyFromEndpoint(ep *endpoint.Endpoint) string {
   625  	if ep.RecordType == endpoint.RecordTypeA {
   626  		return sd.RoutingPolicyMultivalue
   627  	}
   628  
   629  	return sd.RoutingPolicyWeighted
   630  }
   631  
   632  // determine service type (A, CNAME) from given endpoint
   633  func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) string {
   634  	if ep.RecordType == endpoint.RecordTypeCNAME {
   635  		// FIXME service type is derived from the first target only. Theoretically this may be problem.
   636  		// But I don't see a scenario where one endpoint contains targets of different types.
   637  		if p.isAWSLoadBalancer(ep.Targets[0]) {
   638  			// ALIAS target uses DNS record type of A
   639  			return sd.RecordTypeA
   640  		}
   641  		return sd.RecordTypeCname
   642  	}
   643  	return sd.RecordTypeA
   644  }
   645  
   646  // determine if a given hostname belongs to an AWS load balancer
   647  func (p *AWSSDProvider) isAWSLoadBalancer(hostname string) bool {
   648  	matchElb := sdElbHostnameRegex.MatchString(hostname)
   649  	matchNlb := sdNlbHostnameRegex.MatchString(hostname)
   650  
   651  	return matchElb || matchNlb
   652  }