mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
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
This commit is contained in:
parent
dfb64ae813
commit
d79dd835af
@ -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
|
||||
|
@ -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
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 {
|
||||
@ -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) (Route53Changes, Route53Changes) {
|
||||
if queue == nil {
|
||||
|
@ -583,6 +583,42 @@ func TestAWSRecords(t *testing.T) {
|
||||
SubdivisionCode: aws.String("NY"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("test-set-1"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
AWSRegion: aws.String("us-west-2"),
|
||||
Bias: aws.Int32(10),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("test-set-1"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
LocalZoneGroup: aws.String("usw2-pdx1-az1"),
|
||||
Bias: aws.Int32(10),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("test-set-1"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
Coordinates: &route53types.Coordinates{
|
||||
Latitude: aws.String("90"),
|
||||
Longitude: aws.String("90"),
|
||||
},
|
||||
Bias: aws.Int32(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeCname,
|
||||
@ -636,6 +672,9 @@ func TestAWSRecords(t *testing.T) {
|
||||
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
|
||||
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
|
||||
endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"),
|
||||
endpoint.NewEndpointWithTTL("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
|
||||
endpoint.NewEndpointWithTTL("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-pdx1-az1").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
|
||||
endpoint.NewEndpointWithTTL("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "90,90").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"),
|
||||
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"),
|
||||
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
|
||||
endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(defaultTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"),
|
||||
@ -670,6 +709,7 @@ func TestAWSAdjustEndpoints(t *testing.T) {
|
||||
endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
|
||||
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
|
||||
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"),
|
||||
}
|
||||
|
||||
records, err := provider.AdjustEndpoints(records)
|
||||
@ -687,6 +727,7 @@ func TestAWSAdjustEndpoints(t *testing.T) {
|
||||
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
|
||||
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"),
|
||||
})
|
||||
}
|
||||
|
||||
@ -845,6 +886,27 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}, {Value: aws.String("2606:4700:4700::1001")}},
|
||||
},
|
||||
{
|
||||
Name: aws.String("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("geoproximity-delete"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
AWSRegion: aws.String("us-west-2"),
|
||||
Bias: aws.Int32(10),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("geoproximity-update"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
LocalZoneGroup: aws.String("usw2-lax1-az2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
@ -915,6 +977,13 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
|
||||
endpoint.NewEndpoint("create-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
|
||||
endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").
|
||||
WithSetIdentifier("geoproximity-region").
|
||||
WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").
|
||||
WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
|
||||
endpoint.NewEndpoint("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").
|
||||
WithSetIdentifier("geoproximity-coordinates").
|
||||
WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "60,60"),
|
||||
}
|
||||
|
||||
currentRecords := []*endpoint.Endpoint{
|
||||
@ -930,6 +999,9 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "bar.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
|
||||
endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
|
||||
endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").
|
||||
WithSetIdentifier("geoproximity-update").
|
||||
WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-lax1-az2"),
|
||||
endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("weighted-to-simple").WithProviderSpecific(providerSpecificWeight, "10"),
|
||||
endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
|
||||
endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"),
|
||||
@ -951,6 +1023,9 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "baz.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
|
||||
endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001", "2606:4700:4700::1111"),
|
||||
endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").
|
||||
WithSetIdentifier("geoproximity-update").
|
||||
WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-phx2-az1"),
|
||||
endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
|
||||
endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("simple-to-weighted").WithProviderSpecific(providerSpecificWeight, "10"),
|
||||
endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
|
||||
@ -969,6 +1044,7 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "qux.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
|
||||
endpoint.NewEndpoint("delete-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
|
||||
endpoint.NewEndpoint("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("geoproximity-delete").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
|
||||
endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"),
|
||||
}
|
||||
|
||||
@ -1118,6 +1194,40 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}},
|
||||
},
|
||||
{
|
||||
Name: aws.String("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}},
|
||||
SetIdentifier: aws.String("geoproximity-region"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
AWSRegion: aws.String("us-west-2"),
|
||||
Bias: aws.Int32(10),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
|
||||
SetIdentifier: aws.String("geoproximity-update"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
LocalZoneGroup: aws.String("usw2-phx2-az1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: route53types.RRTypeA,
|
||||
TTL: aws.Int64(defaultTTL),
|
||||
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}},
|
||||
SetIdentifier: aws.String("geoproximity-coordinates"),
|
||||
GeoProximityLocation: &route53types.GeoProximityLocation{
|
||||
Coordinates: &route53types.Coordinates{
|
||||
Latitude: aws.String("60"),
|
||||
Longitude: aws.String("60"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
validateRecords(t, listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []route53types.ResourceRecordSet{
|
||||
{
|
||||
@ -1902,7 +2012,7 @@ func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoin
|
||||
|
||||
normalized, err := provider.AdjustEndpoints(endpoints)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized)
|
||||
assert.True(t, testutils.SameEndpoints(normalized, expected), "normalized and expected endpoints don't match. %+v:%+v", normalized, expected)
|
||||
}
|
||||
|
||||
func validateAWSZones(t *testing.T, zones map[string]*route53types.HostedZone, expected map[string]*route53types.HostedZone) {
|
||||
@ -2370,3 +2480,340 @@ func TestConvertOctalToAscii(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoProximityWithAWSRegion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
region string
|
||||
hasRegion bool
|
||||
expectedSet bool
|
||||
expectedRegion string
|
||||
}{
|
||||
{
|
||||
name: "valid AWS region",
|
||||
region: "us-west-2",
|
||||
hasRegion: true,
|
||||
expectedSet: true,
|
||||
expectedRegion: "us-west-2",
|
||||
},
|
||||
{
|
||||
name: "another valid AWS region",
|
||||
region: "eu-central-1",
|
||||
hasRegion: true,
|
||||
expectedSet: true,
|
||||
expectedRegion: "eu-central-1",
|
||||
},
|
||||
{
|
||||
name: "empty region string",
|
||||
region: "",
|
||||
hasRegion: true,
|
||||
expectedSet: true,
|
||||
expectedRegion: "",
|
||||
},
|
||||
{
|
||||
name: "no region property set",
|
||||
region: "",
|
||||
hasRegion: false,
|
||||
expectedSet: false,
|
||||
expectedRegion: "",
|
||||
},
|
||||
{
|
||||
name: "region with special characters",
|
||||
region: "us-gov-west-1",
|
||||
hasRegion: true,
|
||||
expectedSet: true,
|
||||
expectedRegion: "us-gov-west-1",
|
||||
},
|
||||
{
|
||||
name: "region with numbers",
|
||||
region: "ap-southeast-3",
|
||||
hasRegion: true,
|
||||
expectedSet: true,
|
||||
expectedRegion: "ap-southeast-3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: "test.example.com",
|
||||
SetIdentifier: "test-set",
|
||||
}
|
||||
|
||||
if tt.hasRegion {
|
||||
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion, tt.region)
|
||||
}
|
||||
|
||||
gp := newGeoProximity(ep)
|
||||
result := gp.withAWSRegion()
|
||||
|
||||
assert.Equal(t, tt.expectedSet, result.isSet)
|
||||
|
||||
if tt.expectedSet {
|
||||
assert.NotNil(t, result.location.AWSRegion)
|
||||
assert.Equal(t, tt.expectedRegion, *result.location.AWSRegion)
|
||||
} else {
|
||||
assert.Nil(t, result.location.AWSRegion)
|
||||
}
|
||||
|
||||
// Verify the method returns the same instance for chaining
|
||||
assert.Equal(t, gp, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoProximityWithLocalZoneGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
localZoneGroup string
|
||||
hasLocalZoneGroup bool
|
||||
expectedSet bool
|
||||
expectedLocalZoneGroup string
|
||||
}{
|
||||
{
|
||||
name: "valid local zone group",
|
||||
localZoneGroup: "usw2-lax1-az1",
|
||||
hasLocalZoneGroup: true,
|
||||
expectedSet: true,
|
||||
expectedLocalZoneGroup: "usw2-lax1-az1",
|
||||
},
|
||||
{
|
||||
name: "empty local zone group",
|
||||
localZoneGroup: "",
|
||||
hasLocalZoneGroup: true,
|
||||
expectedSet: true,
|
||||
expectedLocalZoneGroup: "",
|
||||
},
|
||||
{
|
||||
name: "no local zone group property",
|
||||
localZoneGroup: "",
|
||||
hasLocalZoneGroup: false,
|
||||
expectedSet: false,
|
||||
expectedLocalZoneGroup: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: "test.example.com",
|
||||
SetIdentifier: "test-set",
|
||||
}
|
||||
|
||||
if tt.hasLocalZoneGroup {
|
||||
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup, tt.localZoneGroup)
|
||||
}
|
||||
|
||||
gp := newGeoProximity(ep)
|
||||
result := gp.withLocalZoneGroup()
|
||||
|
||||
assert.Equal(t, tt.expectedSet, result.isSet)
|
||||
|
||||
if tt.expectedSet {
|
||||
assert.NotNil(t, result.location.LocalZoneGroup)
|
||||
assert.Equal(t, tt.expectedLocalZoneGroup, *result.location.LocalZoneGroup)
|
||||
} else {
|
||||
assert.Nil(t, result.location.LocalZoneGroup)
|
||||
}
|
||||
|
||||
// Verify method returns same instance for chaining
|
||||
assert.Equal(t, gp, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoProximityWithCoordinates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
coordinates string
|
||||
expectedSet bool
|
||||
expectedLat string
|
||||
expectedLong string
|
||||
shouldHaveCoords bool
|
||||
}{
|
||||
{
|
||||
name: "valid coordinates",
|
||||
coordinates: "45.0,90.0",
|
||||
expectedSet: true,
|
||||
expectedLat: "45.0",
|
||||
expectedLong: "90.0",
|
||||
shouldHaveCoords: true,
|
||||
},
|
||||
{
|
||||
name: "edge case min coordinates",
|
||||
coordinates: "-90.0,-180.0",
|
||||
expectedSet: true,
|
||||
expectedLat: "-90.0",
|
||||
expectedLong: "-180.0",
|
||||
shouldHaveCoords: true,
|
||||
},
|
||||
{
|
||||
name: "edge case max coordinates",
|
||||
coordinates: "90.0,180.0",
|
||||
expectedSet: true,
|
||||
expectedLat: "90.0",
|
||||
expectedLong: "180.0",
|
||||
shouldHaveCoords: true,
|
||||
},
|
||||
{
|
||||
name: "invalid latitude too high",
|
||||
coordinates: "91.0,90.0",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
{
|
||||
name: "invalid longitude too low",
|
||||
coordinates: "45.0,-181.0",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format - single value",
|
||||
coordinates: "45.0",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format - three values",
|
||||
coordinates: "45.0,90.0,10.0",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format - non-numeric",
|
||||
coordinates: "abc,def",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
{
|
||||
name: "no coordinates property",
|
||||
coordinates: "",
|
||||
expectedSet: false,
|
||||
shouldHaveCoords: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ep := &endpoint.Endpoint{}
|
||||
if tt.coordinates != "" {
|
||||
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates, tt.coordinates)
|
||||
}
|
||||
|
||||
gp := newGeoProximity(ep)
|
||||
result := gp.withCoordinates()
|
||||
|
||||
assert.Equal(t, tt.expectedSet, result.isSet)
|
||||
|
||||
if tt.shouldHaveCoords {
|
||||
assert.NotNil(t, result.location.Coordinates)
|
||||
assert.Equal(t, tt.expectedLat, *result.location.Coordinates.Latitude)
|
||||
assert.Equal(t, tt.expectedLong, *result.location.Coordinates.Longitude)
|
||||
} else {
|
||||
assert.Nil(t, result.location.Coordinates)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoProximityWithBias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bias string
|
||||
hasBias bool
|
||||
expectedSet bool
|
||||
expectedBias int32
|
||||
}{
|
||||
{
|
||||
name: "valid positive bias",
|
||||
bias: "10",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 10,
|
||||
},
|
||||
{
|
||||
name: "valid negative bias",
|
||||
bias: "-5",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: -5,
|
||||
},
|
||||
{
|
||||
name: "zero bias",
|
||||
bias: "0",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 0,
|
||||
},
|
||||
{
|
||||
name: "large positive bias",
|
||||
bias: "99",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 99,
|
||||
},
|
||||
{
|
||||
name: "large negative bias",
|
||||
bias: "-99",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: -99,
|
||||
},
|
||||
{
|
||||
name: "invalid bias - non-numeric",
|
||||
bias: "abc",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 0, // defaults to 0 on error
|
||||
},
|
||||
{
|
||||
name: "invalid bias - float",
|
||||
bias: "10.5",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 0, // defaults to 0 on error
|
||||
},
|
||||
{
|
||||
name: "empty bias string",
|
||||
bias: "",
|
||||
hasBias: true,
|
||||
expectedSet: true,
|
||||
expectedBias: 0, // defaults to 0 on error
|
||||
},
|
||||
{
|
||||
name: "no bias property",
|
||||
bias: "",
|
||||
hasBias: false,
|
||||
expectedSet: false,
|
||||
expectedBias: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: "test.example.com",
|
||||
SetIdentifier: "test-set",
|
||||
}
|
||||
|
||||
if tt.hasBias {
|
||||
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, tt.bias)
|
||||
}
|
||||
|
||||
gp := newGeoProximity(ep)
|
||||
result := gp.withBias()
|
||||
|
||||
assert.Equal(t, tt.expectedSet, result.isSet)
|
||||
|
||||
if tt.expectedSet {
|
||||
assert.NotNil(t, result.location.Bias)
|
||||
assert.Equal(t, tt.expectedBias, *result.location.Bias)
|
||||
} else {
|
||||
assert.Nil(t, result.location.Bias)
|
||||
}
|
||||
|
||||
// Verify method returns same instance for chaining
|
||||
assert.Equal(t, gp, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user