Skip to content

Commit d79dd83

Browse files
authored
feat(aws): add support for geoproximity routing (#5347)
* feat(aws): add support for geoproximity routing * remove the invalid test * make some changes based on review comments * fix linting errors * make changes based on review feedback * add more tests to get better coverage * update docs * make the linter happy * address review feedback This commit addresses the review feedback by making the following changes: - use a more object-oriented approach for geoProximity handling - change log levels to warnings instead of errors - add more test cases for geoProximity * fix linting error * use shorter annotation names
1 parent dfb64ae commit d79dd83

File tree

3 files changed

+614
-12
lines changed

3 files changed

+614
-12
lines changed

docs/tutorials/aws.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,11 @@ For any given DNS name, only **one** of the following routing policies can be us
894894
- `external-dns.alpha.kubernetes.io/aws-geolocation-continent-code`
895895
- `external-dns.alpha.kubernetes.io/aws-geolocation-country-code`
896896
- `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
897+
- Geoproximity routing:
898+
- `external-dns.alpha.kubernetes.io/aws-geoproximity-region`
899+
- `external-dns.alpha.kubernetes.io/aws-geoproximity-local-zone-group`
900+
- `external-dns.alpha.kubernetes.io/aws-geoproximity-coordinates`
901+
- `external-dns.alpha.kubernetes.io/aws-geoproximity-bias`
897902
- Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`
898903

899904
### Associating DNS records with healthchecks

provider/aws/aws.go

Lines changed: 161 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,27 @@ const (
5353
// providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record
5454
// has the EvaluateTargetHealth field set to true. Present iff the endpoint
5555
// has a `providerSpecificAlias` value of `true`.
56-
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
57-
providerSpecificWeight = "aws/weight"
58-
providerSpecificRegion = "aws/region"
59-
providerSpecificFailover = "aws/failover"
60-
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
61-
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
62-
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
63-
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
64-
providerSpecificHealthCheckID = "aws/health-check-id"
65-
sameZoneAlias = "same-zone"
56+
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
57+
providerSpecificWeight = "aws/weight"
58+
providerSpecificRegion = "aws/region"
59+
providerSpecificFailover = "aws/failover"
60+
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
61+
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
62+
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
63+
providerSpecificGeoProximityLocationAWSRegion = "aws/geoproximity-region"
64+
providerSpecificGeoProximityLocationBias = "aws/geoproximity-bias"
65+
providerSpecificGeoProximityLocationCoordinates = "aws/geoproximity-coordinates"
66+
providerSpecificGeoProximityLocationLocalZoneGroup = "aws/geoproximity-local-zone-group"
67+
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
68+
providerSpecificHealthCheckID = "aws/health-check-id"
69+
sameZoneAlias = "same-zone"
6670
// Currently supported up to 10 health checks or hosted zones.
6771
// https://docs.aws.amazon.com/Route53/latest/APIReference/API_ListTagsForResources.html#API_ListTagsForResources_RequestSyntax
68-
batchSize = 10
72+
batchSize = 10
73+
minLatitude = -90.0
74+
maxLatitude = 90.0
75+
minLongitude = -180.0
76+
maxLongitude = 180.0
6977
)
7078

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

242+
type geoProximity struct {
243+
location *route53types.GeoProximityLocation
244+
endpoint *endpoint.Endpoint
245+
isSet bool
246+
}
247+
234248
func (cs Route53Changes) Route53Changes() []route53types.Change {
235249
var ret []route53types.Change
236250
for _, c := range cs {
@@ -542,6 +556,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
542556
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, *r.GeoLocation.SubdivisionCode)
543557
}
544558
}
559+
case r.GeoProximityLocation != nil:
560+
handleGeoProximityLocationRecord(&r, ep)
545561
default:
546562
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
547563
}
@@ -560,6 +576,25 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
560576
return endpoints, nil
561577
}
562578

579+
func handleGeoProximityLocationRecord(r *route53types.ResourceRecordSet, ep *endpoint.Endpoint) {
580+
if region := aws.ToString(r.GeoProximityLocation.AWSRegion); region != "" {
581+
ep.WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, region)
582+
}
583+
584+
if bias := r.GeoProximityLocation.Bias; bias != nil {
585+
ep.WithProviderSpecific(providerSpecificGeoProximityLocationBias, fmt.Sprintf("%d", aws.ToInt32(bias)))
586+
}
587+
588+
if coords := r.GeoProximityLocation.Coordinates; coords != nil {
589+
coordinates := fmt.Sprintf("%s,%s", aws.ToString(coords.Latitude), aws.ToString(coords.Longitude))
590+
ep.WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, coordinates)
591+
}
592+
593+
if localZoneGroup := aws.ToString(r.GeoProximityLocation.LocalZoneGroup); localZoneGroup != "" {
594+
ep.WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, localZoneGroup)
595+
}
596+
}
597+
563598
// Identify if old and new endpoints require DELETE/CREATE instead of UPDATE.
564599
func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, newE *endpoint.Endpoint) bool {
565600
// a change of a record type
@@ -832,12 +867,32 @@ func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoi
832867
} else {
833868
ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth)
834869
}
870+
871+
adjustGeoProximityLocationEndpoint(ep)
835872
}
836873

837874
endpoints = append(endpoints, aliasCnameAaaaEndpoints...)
838875
return endpoints, nil
839876
}
840877

878+
// if the endpoint is using geoproximity, set the bias to 0 if not set
879+
// this is needed to avoid unnecessary Upserts if the desired endpoint doesn't specify a bias
880+
func adjustGeoProximityLocationEndpoint(ep *endpoint.Endpoint) {
881+
if ep.SetIdentifier == "" {
882+
return
883+
}
884+
_, ok1 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion)
885+
_, ok2 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup)
886+
_, ok3 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates)
887+
888+
if ok1 || ok2 || ok3 {
889+
// check if ep has bias property and if not, set it to 0
890+
if _, ok := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); !ok {
891+
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, "0")
892+
}
893+
}
894+
}
895+
841896
// newChange returns a route53 Change
842897
// returned Change is based on the given record by the given action, e.g.
843898
// action=ChangeActionCreate returns a change for creation of the record and
@@ -926,6 +981,8 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E
926981
if useGeolocation {
927982
change.ResourceRecordSet.GeoLocation = geolocation
928983
}
984+
985+
withChangeForGeoProximityEndpoint(change, ep)
929986
}
930987

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

999+
func newGeoProximity(ep *endpoint.Endpoint) *geoProximity {
1000+
return &geoProximity{
1001+
location: &route53types.GeoProximityLocation{},
1002+
endpoint: ep,
1003+
isSet: false,
1004+
}
1005+
}
1006+
1007+
func (gp *geoProximity) withAWSRegion() *geoProximity {
1008+
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion); ok {
1009+
gp.location.AWSRegion = aws.String(prop)
1010+
gp.isSet = true
1011+
}
1012+
return gp
1013+
}
1014+
1015+
// add a method to set the local zone group for the geoproximity location
1016+
func (gp *geoProximity) withLocalZoneGroup() *geoProximity {
1017+
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup); ok {
1018+
gp.location.LocalZoneGroup = aws.String(prop)
1019+
gp.isSet = true
1020+
}
1021+
return gp
1022+
}
1023+
1024+
// add a method to set the bias for the geoproximity location
1025+
func (gp *geoProximity) withBias() *geoProximity {
1026+
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); ok {
1027+
bias, err := strconv.ParseInt(prop, 10, 32)
1028+
if err != nil {
1029+
log.Warnf("Failed parsing value of %s: %s: %v; using bias of 0", providerSpecificGeoProximityLocationBias, prop, err)
1030+
bias = 0
1031+
}
1032+
gp.location.Bias = aws.Int32(int32(bias))
1033+
gp.isSet = true
1034+
}
1035+
return gp
1036+
}
1037+
1038+
// validateCoordinates checks if the given latitude and longitude are valid.
1039+
func validateCoordinates(lat, long string) error {
1040+
latitude, err := strconv.ParseFloat(lat, 64)
1041+
if err != nil || latitude < minLatitude || latitude > maxLatitude {
1042+
return fmt.Errorf("invalid latitude: must be a number between %f and %f", minLatitude, maxLatitude)
1043+
}
1044+
1045+
longitude, err := strconv.ParseFloat(long, 64)
1046+
if err != nil || longitude < minLongitude || longitude > maxLongitude {
1047+
return fmt.Errorf("invalid longitude: must be a number between %f and %f", minLongitude, maxLongitude)
1048+
}
1049+
1050+
return nil
1051+
}
1052+
1053+
func (gp *geoProximity) withCoordinates() *geoProximity {
1054+
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates); ok {
1055+
coordinates := strings.Split(prop, ",")
1056+
if len(coordinates) == 2 {
1057+
latitude := coordinates[0]
1058+
longitude := coordinates[1]
1059+
if err := validateCoordinates(latitude, longitude); err != nil {
1060+
log.Warnf("Invalid coordinates %s for name=%s setIdentifier=%s; %v", prop, gp.endpoint.DNSName, gp.endpoint.SetIdentifier, err)
1061+
} else {
1062+
gp.location.Coordinates = &route53types.Coordinates{
1063+
Latitude: aws.String(latitude),
1064+
Longitude: aws.String(longitude),
1065+
}
1066+
gp.isSet = true
1067+
}
1068+
} else {
1069+
log.Warnf("Invalid coordinates format for %s: %s; expected format 'latitude,longitude'", providerSpecificGeoProximityLocationCoordinates, prop)
1070+
}
1071+
}
1072+
return gp
1073+
}
1074+
1075+
func (gp *geoProximity) build() *route53types.GeoProximityLocation {
1076+
if gp.isSet {
1077+
return gp.location
1078+
}
1079+
return nil
1080+
}
1081+
1082+
func withChangeForGeoProximityEndpoint(change *Route53Change, ep *endpoint.Endpoint) {
1083+
geoProx := newGeoProximity(ep).
1084+
withAWSRegion().
1085+
withCoordinates().
1086+
withLocalZoneGroup().
1087+
withBias()
1088+
1089+
change.ResourceRecordSet.GeoProximityLocation = geoProx.build()
1090+
}
1091+
9421092
// 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`)
9431093
func findChangesInQueue(changes Route53Changes, queue Route53Changes) (Route53Changes, Route53Changes) {
9441094
if queue == nil {

0 commit comments

Comments
 (0)