Skip to content
Merged
5 changes: 5 additions & 0 deletions docs/tutorials/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,11 @@ For any given DNS name, only **one** of the following routing policies can be us
- `external-dns.alpha.kubernetes.io/aws-geolocation-continent-code`
- `external-dns.alpha.kubernetes.io/aws-geolocation-country-code`
- `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
- Geoproximity routing:
- `external-dns.alpha.kubernetes.io/aws-geoproximity-region`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-local-zone-group`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-coordinates`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-bias`
- Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`

### Associating DNS records with healthchecks
Expand Down
172 changes: 161 additions & 11 deletions provider/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,27 @@ const (
// providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record
// has the EvaluateTargetHealth field set to true. Present iff the endpoint
// has a `providerSpecificAlias` value of `true`.
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
providerSpecificFailover = "aws/failover"
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
providerSpecificHealthCheckID = "aws/health-check-id"
sameZoneAlias = "same-zone"
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
providerSpecificFailover = "aws/failover"
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificGeoProximityLocationAWSRegion = "aws/geoproximity-region"
providerSpecificGeoProximityLocationBias = "aws/geoproximity-bias"
providerSpecificGeoProximityLocationCoordinates = "aws/geoproximity-coordinates"
providerSpecificGeoProximityLocationLocalZoneGroup = "aws/geoproximity-local-zone-group"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
providerSpecificHealthCheckID = "aws/health-check-id"
sameZoneAlias = "same-zone"
// Currently supported up to 10 health checks or hosted zones.
// https://docs.aws.amazon.com/Route53/latest/APIReference/API_ListTagsForResources.html#API_ListTagsForResources_RequestSyntax
batchSize = 10
batchSize = 10
minLatitude = -90.0
maxLatitude = 90.0
minLongitude = -180.0
maxLongitude = 180.0
)

// see elb: https://docs.aws.amazon.com/general/latest/gr/elb.html
Expand Down Expand Up @@ -231,6 +239,12 @@ type profiledZone struct {
zone *route53types.HostedZone
}

type geoProximity struct {
location *route53types.GeoProximityLocation
endpoint *endpoint.Endpoint
isSet bool
}

func (cs Route53Changes) Route53Changes() []route53types.Change {
var ret []route53types.Change
for _, c := range cs {
Expand Down Expand Up @@ -542,6 +556,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, *r.GeoLocation.SubdivisionCode)
}
}
case r.GeoProximityLocation != nil:
handleGeoProximityLocationRecord(&r, ep)
default:
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
Expand All @@ -560,6 +576,25 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
return endpoints, nil
}

func handleGeoProximityLocationRecord(r *route53types.ResourceRecordSet, ep *endpoint.Endpoint) {
if region := aws.ToString(r.GeoProximityLocation.AWSRegion); region != "" {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, region)
}

if bias := r.GeoProximityLocation.Bias; bias != nil {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationBias, fmt.Sprintf("%d", aws.ToInt32(bias)))
}

if coords := r.GeoProximityLocation.Coordinates; coords != nil {
coordinates := fmt.Sprintf("%s,%s", aws.ToString(coords.Latitude), aws.ToString(coords.Longitude))
ep.WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, coordinates)
}

if localZoneGroup := aws.ToString(r.GeoProximityLocation.LocalZoneGroup); localZoneGroup != "" {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, localZoneGroup)
}
}

// Identify if old and new endpoints require DELETE/CREATE instead of UPDATE.
func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, newE *endpoint.Endpoint) bool {
// a change of a record type
Expand Down Expand Up @@ -832,12 +867,32 @@ func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoi
} else {
ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth)
}

adjustGeoProximityLocationEndpoint(ep)
}

endpoints = append(endpoints, aliasCnameAaaaEndpoints...)
return endpoints, nil
}

// if the endpoint is using geoproximity, set the bias to 0 if not set
// this is needed to avoid unnecessary Upserts if the desired endpoint doesn't specify a bias
func adjustGeoProximityLocationEndpoint(ep *endpoint.Endpoint) {
if ep.SetIdentifier == "" {
return
}
_, ok1 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion)
_, ok2 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup)
_, ok3 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates)

if ok1 || ok2 || ok3 {
// check if ep has bias property and if not, set it to 0
if _, ok := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); !ok {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, "0")
}
}
}

// newChange returns a route53 Change
// returned Change is based on the given record by the given action, e.g.
// action=ChangeActionCreate returns a change for creation of the record and
Expand Down Expand Up @@ -926,6 +981,8 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E
if useGeolocation {
change.ResourceRecordSet.GeoLocation = geolocation
}

withChangeForGeoProximityEndpoint(change, ep)
}

if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok {
Expand All @@ -939,6 +996,99 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E
return change
}

func newGeoProximity(ep *endpoint.Endpoint) *geoProximity {
return &geoProximity{
location: &route53types.GeoProximityLocation{},
endpoint: ep,
isSet: false,
}
}

func (gp *geoProximity) withAWSRegion() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion); ok {
gp.location.AWSRegion = aws.String(prop)
gp.isSet = true
}
return gp
}

// add a method to set the local zone group for the geoproximity location
func (gp *geoProximity) withLocalZoneGroup() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup); ok {
gp.location.LocalZoneGroup = aws.String(prop)
gp.isSet = true
}
return gp
}

// add a method to set the bias for the geoproximity location
func (gp *geoProximity) withBias() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); ok {
bias, err := strconv.ParseInt(prop, 10, 32)
if err != nil {
log.Warnf("Failed parsing value of %s: %s: %v; using bias of 0", providerSpecificGeoProximityLocationBias, prop, err)
bias = 0
}
gp.location.Bias = aws.Int32(int32(bias))
gp.isSet = true
}
return gp
}

// validateCoordinates checks if the given latitude and longitude are valid.
func validateCoordinates(lat, long string) error {
latitude, err := strconv.ParseFloat(lat, 64)
if err != nil || latitude < minLatitude || latitude > maxLatitude {
return fmt.Errorf("invalid latitude: must be a number between %f and %f", minLatitude, maxLatitude)
}

longitude, err := strconv.ParseFloat(long, 64)
if err != nil || longitude < minLongitude || longitude > maxLongitude {
return fmt.Errorf("invalid longitude: must be a number between %f and %f", minLongitude, maxLongitude)
}

return nil
}

func (gp *geoProximity) withCoordinates() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates); ok {
coordinates := strings.Split(prop, ",")
if len(coordinates) == 2 {
latitude := coordinates[0]
longitude := coordinates[1]
if err := validateCoordinates(latitude, longitude); err != nil {
log.Warnf("Invalid coordinates %s for name=%s setIdentifier=%s; %v", prop, gp.endpoint.DNSName, gp.endpoint.SetIdentifier, err)
} else {
gp.location.Coordinates = &route53types.Coordinates{
Latitude: aws.String(latitude),
Longitude: aws.String(longitude),
}
gp.isSet = true
}
} else {
log.Warnf("Invalid coordinates format for %s: %s; expected format 'latitude,longitude'", providerSpecificGeoProximityLocationCoordinates, prop)
}
}
return gp
}

func (gp *geoProximity) build() *route53types.GeoProximityLocation {
if gp.isSet {
return gp.location
}
return nil
}

func withChangeForGeoProximityEndpoint(change *Route53Change, ep *endpoint.Endpoint) {
geoProx := newGeoProximity(ep).
withAWSRegion().
withCoordinates().
withLocalZoneGroup().
withBias()

change.ResourceRecordSet.GeoProximityLocation = geoProx.build()
}

// searches for `changes` that are contained in `queue` and returns the `changes` separated by whether they were found in the queue (`foundChanges`) or not (`notFoundChanges`)
func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChanges, notFoundChanges Route53Changes) {
if queue == nil {
Expand Down
Loading
Loading