sigs.k8s.io/external-dns@v0.14.1/provider/aws/aws_test.go (about) 1 /* 2 Copyright 2017 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 aws 18 19 import ( 20 "context" 21 "fmt" 22 "math" 23 "net" 24 "sort" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/aws/aws-sdk-go/aws" 30 "github.com/aws/aws-sdk-go/aws/request" 31 "github.com/aws/aws-sdk-go/service/route53" 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/mock" 34 "github.com/stretchr/testify/require" 35 36 "sigs.k8s.io/external-dns/endpoint" 37 "sigs.k8s.io/external-dns/internal/testutils" 38 "sigs.k8s.io/external-dns/plan" 39 "sigs.k8s.io/external-dns/provider" 40 ) 41 42 const ( 43 defaultBatchChangeSize = 4000 44 defaultBatchChangeSizeBytes = 32000 45 defaultBatchChangeSizeValues = 1000 46 defaultBatchChangeInterval = time.Second 47 defaultEvaluateTargetHealth = true 48 ) 49 50 // Compile time check for interface conformance 51 var _ Route53API = &Route53APIStub{} 52 53 // Route53APIStub is a minimal implementation of Route53API, used primarily for unit testing. 54 // See http://http://docs.aws.amazon.com/sdk-for-go/api/service/route53.html for descriptions 55 // of all of its methods. 56 // mostly taken from: https://github.com/kubernetes/kubernetes/blob/853167624edb6bc0cfdcdfb88e746e178f5db36c/federation/pkg/dnsprovider/providers/aws/route53/stubs/route53api.go 57 type Route53APIStub struct { 58 zones map[string]*route53.HostedZone 59 recordSets map[string]map[string][]*route53.ResourceRecordSet 60 zoneTags map[string][]*route53.Tag 61 m dynamicMock 62 t *testing.T 63 } 64 65 // MockMethod starts a description of an expectation of the specified method 66 // being called. 67 // 68 // Route53APIStub.MockMethod("MyMethod", arg1, arg2) 69 func (r *Route53APIStub) MockMethod(method string, args ...interface{}) *mock.Call { 70 return r.m.On(method, args...) 71 } 72 73 // NewRoute53APIStub returns an initialized Route53APIStub 74 func NewRoute53APIStub(t *testing.T) *Route53APIStub { 75 return &Route53APIStub{ 76 zones: make(map[string]*route53.HostedZone), 77 recordSets: make(map[string]map[string][]*route53.ResourceRecordSet), 78 zoneTags: make(map[string][]*route53.Tag), 79 t: t, 80 } 81 } 82 83 func (r *Route53APIStub) ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error { 84 output := route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args. 85 require.NotNil(r.t, input.MaxItems) 86 assert.EqualValues(r.t, route53PageSize, *input.MaxItems) 87 if len(r.recordSets) == 0 { 88 output.ResourceRecordSets = []*route53.ResourceRecordSet{} 89 } else if _, ok := r.recordSets[aws.StringValue(input.HostedZoneId)]; !ok { 90 output.ResourceRecordSets = []*route53.ResourceRecordSet{} 91 } else { 92 for _, rrsets := range r.recordSets[aws.StringValue(input.HostedZoneId)] { 93 output.ResourceRecordSets = append(output.ResourceRecordSets, rrsets...) 94 } 95 } 96 lastPage := true 97 fn(&output, lastPage) 98 return nil 99 } 100 101 type Route53APICounter struct { 102 wrapped Route53API 103 calls map[string]int 104 } 105 106 func NewRoute53APICounter(w Route53API) *Route53APICounter { 107 return &Route53APICounter{ 108 wrapped: w, 109 calls: map[string]int{}, 110 } 111 } 112 113 func (c *Route53APICounter) ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error { 114 c.calls["ListResourceRecordSetsPages"]++ 115 return c.wrapped.ListResourceRecordSetsPagesWithContext(ctx, input, fn) 116 } 117 118 func (c *Route53APICounter) ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error) { 119 c.calls["ChangeResourceRecordSets"]++ 120 return c.wrapped.ChangeResourceRecordSetsWithContext(ctx, input) 121 } 122 123 func (c *Route53APICounter) CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error) { 124 c.calls["CreateHostedZone"]++ 125 return c.wrapped.CreateHostedZoneWithContext(ctx, input) 126 } 127 128 func (c *Route53APICounter) ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error { 129 c.calls["ListHostedZonesPages"]++ 130 return c.wrapped.ListHostedZonesPagesWithContext(ctx, input, fn) 131 } 132 133 func (c *Route53APICounter) ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error) { 134 c.calls["ListTagsForResource"]++ 135 return c.wrapped.ListTagsForResourceWithContext(ctx, input) 136 } 137 138 // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk 139 func wildcardEscape(s string) string { 140 if strings.Contains(s, "*") { 141 s = strings.Replace(s, "*", "\\052", 1) 142 } 143 return s 144 } 145 146 func (r *Route53APIStub) ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error) { 147 if aws.StringValue(input.ResourceType) == "hostedzone" { 148 tags := r.zoneTags[aws.StringValue(input.ResourceId)] 149 return &route53.ListTagsForResourceOutput{ 150 ResourceTagSet: &route53.ResourceTagSet{ 151 ResourceId: input.ResourceId, 152 ResourceType: input.ResourceType, 153 Tags: tags, 154 }, 155 }, nil 156 } 157 return &route53.ListTagsForResourceOutput{}, nil 158 } 159 160 func (r *Route53APIStub) ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error) { 161 if r.m.isMocked("ChangeResourceRecordSets", input) { 162 return r.m.ChangeResourceRecordSets(input) 163 } 164 165 _, ok := r.zones[aws.StringValue(input.HostedZoneId)] 166 if !ok { 167 return nil, fmt.Errorf("Hosted zone doesn't exist: %s", aws.StringValue(input.HostedZoneId)) 168 } 169 170 if len(input.ChangeBatch.Changes) == 0 { 171 return nil, fmt.Errorf("ChangeBatch doesn't contain any changes") 172 } 173 174 output := &route53.ChangeResourceRecordSetsOutput{} 175 recordSets, ok := r.recordSets[aws.StringValue(input.HostedZoneId)] 176 if !ok { 177 recordSets = make(map[string][]*route53.ResourceRecordSet) 178 } 179 180 for _, change := range input.ChangeBatch.Changes { 181 if aws.StringValue(change.ResourceRecordSet.Type) == route53.RRTypeA { 182 for _, rrs := range change.ResourceRecordSet.ResourceRecords { 183 if net.ParseIP(aws.StringValue(rrs.Value)) == nil { 184 return nil, fmt.Errorf("A records must point to IPs") 185 } 186 } 187 } 188 189 change.ResourceRecordSet.Name = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name)))) 190 191 if change.ResourceRecordSet.AliasTarget != nil { 192 change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName)))) 193 } 194 195 setID := "" 196 if change.ResourceRecordSet.SetIdentifier != nil { 197 setID = aws.StringValue(change.ResourceRecordSet.SetIdentifier) 198 } 199 key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setID 200 switch aws.StringValue(change.Action) { 201 case route53.ChangeActionCreate: 202 if _, found := recordSets[key]; found { 203 return nil, fmt.Errorf("Attempt to create duplicate rrset %s", key) // TODO: Return AWS errors with codes etc 204 } 205 recordSets[key] = append(recordSets[key], change.ResourceRecordSet) 206 case route53.ChangeActionDelete: 207 if _, found := recordSets[key]; !found { 208 return nil, fmt.Errorf("Attempt to delete non-existent rrset %s", key) // TODO: Check other fields too 209 } 210 delete(recordSets, key) 211 case route53.ChangeActionUpsert: 212 recordSets[key] = []*route53.ResourceRecordSet{change.ResourceRecordSet} 213 } 214 } 215 r.recordSets[aws.StringValue(input.HostedZoneId)] = recordSets 216 return output, nil // TODO: We should ideally return status etc, but we don't' use that yet. 217 } 218 219 func (r *Route53APIStub) ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(p *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error { 220 output := &route53.ListHostedZonesOutput{} 221 for _, zone := range r.zones { 222 output.HostedZones = append(output.HostedZones, zone) 223 } 224 lastPage := true 225 fn(output, lastPage) 226 return nil 227 } 228 229 func (r *Route53APIStub) CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error) { 230 name := aws.StringValue(input.Name) 231 id := "/hostedzone/" + name 232 if _, ok := r.zones[id]; ok { 233 return nil, fmt.Errorf("Error creating hosted DNS zone: %s already exists", id) 234 } 235 r.zones[id] = &route53.HostedZone{ 236 Id: aws.String(id), 237 Name: aws.String(name), 238 Config: input.HostedZoneConfig, 239 } 240 return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil 241 } 242 243 type dynamicMock struct { 244 mock.Mock 245 } 246 247 func (m *dynamicMock) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) { 248 args := m.Called(input) 249 if args.Get(0) != nil { 250 return args.Get(0).(*route53.ChangeResourceRecordSetsOutput), args.Error(1) 251 } 252 return nil, args.Error(1) 253 } 254 255 func (m *dynamicMock) isMocked(method string, arguments ...interface{}) bool { 256 for _, call := range m.ExpectedCalls { 257 if call.Method == method && call.Repeatability > -1 { 258 _, diffCount := call.Arguments.Diff(arguments) 259 if diffCount == 0 { 260 return true 261 } 262 } 263 } 264 return false 265 } 266 267 func TestAWSZones(t *testing.T) { 268 publicZones := map[string]*route53.HostedZone{ 269 "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.": { 270 Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), 271 Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), 272 }, 273 "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.": { 274 Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), 275 Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), 276 }, 277 } 278 279 privateZones := map[string]*route53.HostedZone{ 280 "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.": { 281 Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), 282 Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), 283 }, 284 } 285 286 allZones := map[string]*route53.HostedZone{} 287 for k, v := range publicZones { 288 allZones[k] = v 289 } 290 for k, v := range privateZones { 291 allZones[k] = v 292 } 293 294 noZones := map[string]*route53.HostedZone{} 295 296 for _, ti := range []struct { 297 msg string 298 zoneIDFilter provider.ZoneIDFilter 299 zoneTypeFilter provider.ZoneTypeFilter 300 zoneTagFilter provider.ZoneTagFilter 301 expectedZones map[string]*route53.HostedZone 302 }{ 303 {"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones}, 304 {"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones}, 305 {"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones}, 306 {"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones}, 307 {"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones}, 308 {"tag filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones}, 309 } { 310 provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, nil) 311 312 zones, err := provider.Zones(context.Background()) 313 require.NoError(t, err) 314 315 validateAWSZones(t, zones, ti.expectedZones) 316 } 317 } 318 319 func TestAWSRecordsFilter(t *testing.T) { 320 provider, _ := newAWSProvider(t, endpoint.DomainFilter{}, provider.ZoneIDFilter{}, provider.ZoneTypeFilter{}, false, false, nil) 321 domainFilter := provider.GetDomainFilter() 322 assert.NotNil(t, domainFilter) 323 require.IsType(t, endpoint.DomainFilter{}, domainFilter) 324 count := 0 325 filters := domainFilter.Filters 326 for _, tld := range []string{ 327 "zone-4.ext-dns-test-3.teapot.zalan.do", 328 ".zone-4.ext-dns-test-3.teapot.zalan.do", 329 "zone-2.ext-dns-test-2.teapot.zalan.do", 330 ".zone-2.ext-dns-test-2.teapot.zalan.do", 331 "zone-3.ext-dns-test-2.teapot.zalan.do", 332 ".zone-3.ext-dns-test-2.teapot.zalan.do", 333 "zone-4.ext-dns-test-3.teapot.zalan.do", 334 ".zone-4.ext-dns-test-3.teapot.zalan.do", 335 } { 336 assert.Contains(t, filters, tld) 337 count++ 338 } 339 assert.Len(t, filters, count) 340 } 341 342 func TestAWSRecords(t *testing.T) { 343 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*route53.ResourceRecordSet{ 344 { 345 Name: aws.String("list-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 346 Type: aws.String(route53.RRTypeA), 347 TTL: aws.Int64(recordTTL), 348 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 349 }, 350 { 351 Name: aws.String("list-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 352 Type: aws.String(route53.RRTypeA), 353 TTL: aws.Int64(recordTTL), 354 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 355 }, 356 { 357 Name: aws.String("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 358 Type: aws.String(route53.RRTypeA), 359 TTL: aws.Int64(recordTTL), 360 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 361 }, 362 { 363 Name: aws.String("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 364 Type: aws.String(route53.RRTypeA), 365 AliasTarget: &route53.AliasTarget{ 366 DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), 367 EvaluateTargetHealth: aws.Bool(false), 368 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 369 }, 370 }, 371 { 372 Name: aws.String("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 373 Type: aws.String(route53.RRTypeA), 374 AliasTarget: &route53.AliasTarget{ 375 DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), 376 EvaluateTargetHealth: aws.Bool(false), 377 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 378 }, 379 }, 380 { 381 Name: aws.String("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do."), 382 Type: aws.String(route53.RRTypeA), 383 AliasTarget: &route53.AliasTarget{ 384 DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), 385 EvaluateTargetHealth: aws.Bool(true), 386 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 387 }, 388 }, 389 { 390 Name: aws.String("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), 391 Type: aws.String(route53.RRTypeA), 392 TTL: aws.Int64(recordTTL), 393 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, 394 }, 395 { 396 Name: aws.String("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do."), 397 Type: aws.String(route53.RRTypeTxt), 398 TTL: aws.Int64(recordTTL), 399 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("random")}}, 400 }, 401 { 402 Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 403 Type: aws.String(route53.RRTypeA), 404 TTL: aws.Int64(recordTTL), 405 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 406 SetIdentifier: aws.String("test-set-1"), 407 Weight: aws.Int64(10), 408 }, 409 { 410 Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 411 Type: aws.String(route53.RRTypeA), 412 TTL: aws.Int64(recordTTL), 413 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, 414 SetIdentifier: aws.String("test-set-2"), 415 Weight: aws.Int64(20), 416 }, 417 { 418 Name: aws.String("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 419 Type: aws.String(route53.RRTypeA), 420 TTL: aws.Int64(recordTTL), 421 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 422 SetIdentifier: aws.String("test-set"), 423 Region: aws.String("us-east-1"), 424 }, 425 { 426 Name: aws.String("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 427 Type: aws.String(route53.RRTypeA), 428 TTL: aws.Int64(recordTTL), 429 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 430 SetIdentifier: aws.String("test-set"), 431 Failover: aws.String("PRIMARY"), 432 }, 433 { 434 Name: aws.String("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 435 Type: aws.String(route53.RRTypeA), 436 TTL: aws.Int64(recordTTL), 437 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 438 SetIdentifier: aws.String("test-set"), 439 MultiValueAnswer: aws.Bool(true), 440 }, 441 { 442 Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 443 Type: aws.String(route53.RRTypeA), 444 TTL: aws.Int64(recordTTL), 445 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 446 SetIdentifier: aws.String("test-set-1"), 447 GeoLocation: &route53.GeoLocation{ 448 ContinentCode: aws.String("EU"), 449 }, 450 }, 451 { 452 Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 453 Type: aws.String(route53.RRTypeA), 454 TTL: aws.Int64(recordTTL), 455 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, 456 SetIdentifier: aws.String("test-set-2"), 457 GeoLocation: &route53.GeoLocation{ 458 CountryCode: aws.String("DE"), 459 }, 460 }, 461 { 462 Name: aws.String("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 463 Type: aws.String(route53.RRTypeA), 464 TTL: aws.Int64(recordTTL), 465 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 466 SetIdentifier: aws.String("test-set-1"), 467 GeoLocation: &route53.GeoLocation{ 468 SubdivisionCode: aws.String("NY"), 469 }, 470 }, 471 { 472 Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 473 Type: aws.String(route53.RRTypeCname), 474 TTL: aws.Int64(recordTTL), 475 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.example.com")}}, 476 SetIdentifier: aws.String("test-set-1"), 477 HealthCheckId: aws.String("foo-bar-healthcheck-id"), 478 Weight: aws.Int64(10), 479 }, 480 { 481 Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 482 Type: aws.String(route53.RRTypeA), 483 TTL: aws.Int64(recordTTL), 484 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, 485 SetIdentifier: aws.String("test-set-2"), 486 HealthCheckId: aws.String("abc-def-healthcheck-id"), 487 Weight: aws.Int64(20), 488 }, 489 { 490 Name: aws.String("mail.zone-1.ext-dns-test-2.teapot.zalan.do."), 491 Type: aws.String(route53.RRTypeMx), 492 TTL: aws.Int64(recordTTL), 493 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.com")}}, 494 }, 495 }) 496 497 records, err := provider.Records(context.Background()) 498 require.NoError(t, err) 499 500 validateEndpoints(t, provider, records, []*endpoint.Endpoint{ 501 endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), 502 endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), 503 endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), 504 endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), 505 endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), 506 endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), 507 endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), 508 endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"), 509 endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"), 510 endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20"), 511 endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"), 512 endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"), 513 endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""), 514 endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"), 515 endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"), 516 endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"), 517 endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"), 518 endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"), 519 endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"), 520 }) 521 } 522 523 func TestAWSAdjustEndpoints(t *testing.T) { 524 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 525 526 records := []*endpoint.Endpoint{ 527 endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 528 endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), 529 endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, 60, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), 530 endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), 531 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"), 532 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 533 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"), 534 } 535 536 records, err := provider.AdjustEndpoints(records) 537 assert.NoError(t, err) 538 539 validateEndpoints(t, provider, records, []*endpoint.Endpoint{ 540 endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 541 endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithProviderSpecific(providerSpecificAlias, "false"), 542 endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, 300, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), 543 endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), 544 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"), 545 endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health 546 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"), 547 }) 548 } 549 550 func TestAWSApplyChanges(t *testing.T) { 551 tests := []struct { 552 name string 553 setup func(p *AWSProvider) context.Context 554 listRRSets int 555 }{ 556 {"no cache", func(p *AWSProvider) context.Context { return context.Background() }, 0}, 557 {"cached", func(p *AWSProvider) context.Context { 558 ctx := context.Background() 559 records, err := p.Records(ctx) 560 require.NoError(t, err) 561 return context.WithValue(ctx, provider.RecordsContextKey, records) 562 }, 0}, 563 } 564 565 for _, tt := range tests { 566 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*route53.ResourceRecordSet{ 567 { 568 Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 569 Type: aws.String(route53.RRTypeA), 570 TTL: aws.Int64(recordTTL), 571 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 572 }, 573 { 574 Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 575 Type: aws.String(route53.RRTypeA), 576 TTL: aws.Int64(recordTTL), 577 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 578 }, 579 { 580 Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 581 Type: aws.String(route53.RRTypeA), 582 TTL: aws.Int64(recordTTL), 583 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, 584 }, 585 { 586 Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 587 Type: aws.String(route53.RRTypeA), 588 TTL: aws.Int64(recordTTL), 589 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, 590 }, 591 { 592 Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 593 Type: aws.String(route53.RRTypeA), 594 TTL: aws.Int64(recordTTL), 595 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}}, 596 }, 597 { 598 Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 599 Type: aws.String(route53.RRTypeA), 600 AliasTarget: &route53.AliasTarget{ 601 DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), 602 EvaluateTargetHealth: aws.Bool(true), 603 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 604 }, 605 }, 606 { 607 Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 608 Type: aws.String(route53.RRTypeCname), 609 TTL: aws.Int64(recordTTL), 610 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, 611 }, 612 { 613 Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 614 Type: aws.String(route53.RRTypeCname), 615 TTL: aws.Int64(recordTTL), 616 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, 617 }, 618 { 619 Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 620 Type: aws.String(route53.RRTypeCname), 621 TTL: aws.Int64(recordTTL), 622 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, 623 }, 624 { 625 Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 626 Type: aws.String(route53.RRTypeCname), 627 TTL: aws.Int64(recordTTL), 628 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, 629 }, 630 { 631 Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 632 Type: aws.String(route53.RRTypeA), 633 TTL: aws.Int64(recordTTL), 634 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, 635 }, 636 { 637 Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 638 Type: aws.String(route53.RRTypeA), 639 TTL: aws.Int64(recordTTL), 640 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, 641 }, 642 { 643 Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), 644 Type: aws.String(route53.RRTypeA), 645 TTL: aws.Int64(recordTTL), 646 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 647 SetIdentifier: aws.String("weighted-to-simple"), 648 Weight: aws.Int64(10), 649 }, 650 { 651 Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), 652 Type: aws.String(route53.RRTypeA), 653 TTL: aws.Int64(recordTTL), 654 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 655 }, 656 { 657 Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 658 Type: aws.String(route53.RRTypeA), 659 TTL: aws.Int64(recordTTL), 660 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 661 SetIdentifier: aws.String("policy-change"), 662 Weight: aws.Int64(10), 663 }, 664 { 665 Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 666 Type: aws.String(route53.RRTypeA), 667 TTL: aws.Int64(recordTTL), 668 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 669 SetIdentifier: aws.String("before"), 670 Weight: aws.Int64(10), 671 }, 672 { 673 Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 674 Type: aws.String(route53.RRTypeA), 675 TTL: aws.Int64(recordTTL), 676 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 677 SetIdentifier: aws.String("no-change"), 678 Weight: aws.Int64(10), 679 }, 680 { 681 Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), 682 Type: aws.String(route53.RRTypeMx), 683 TTL: aws.Int64(recordTTL), 684 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost2.bar.elb.amazonaws.com")}}, 685 }, 686 { 687 Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), 688 Type: aws.String(route53.RRTypeMx), 689 TTL: aws.Int64(recordTTL), 690 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("30 mailhost1.foo.elb.amazonaws.com")}}, 691 }, 692 }) 693 694 createRecords := []*endpoint.Endpoint{ 695 endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 696 endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 697 endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 698 endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 699 endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), 700 endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"), 701 } 702 703 currentRecords := []*endpoint.Endpoint{ 704 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 705 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 706 endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"), 707 endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), 708 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 709 endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 710 endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), 711 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"), 712 endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 713 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"), 714 endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("before").WithProviderSpecific(providerSpecificWeight, "10"), 715 endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "10"), 716 endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost2.bar.elb.amazonaws.com"), 717 } 718 updatedRecords := []*endpoint.Endpoint{ 719 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 720 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), 721 endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), 722 endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "my-internal-host.example.com"), 723 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 724 endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 725 endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), 726 endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 727 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"), 728 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"), 729 endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("after").WithProviderSpecific(providerSpecificWeight, "10"), 730 endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "20"), 731 endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mailhost3.foo.elb.amazonaws.com"), 732 } 733 734 deleteRecords := []*endpoint.Endpoint{ 735 endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 736 endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 737 endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 738 endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 739 endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), 740 endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"), 741 } 742 743 changes := &plan.Changes{ 744 Create: createRecords, 745 UpdateNew: updatedRecords, 746 UpdateOld: currentRecords, 747 Delete: deleteRecords, 748 } 749 750 ctx := tt.setup(provider) 751 752 provider.zonesCache = &zonesListCache{duration: 0 * time.Minute} 753 counter := NewRoute53APICounter(provider.client) 754 provider.client = counter 755 require.NoError(t, provider.ApplyChanges(ctx, changes)) 756 757 assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name) 758 assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name) 759 760 validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ 761 { 762 Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 763 Type: aws.String(route53.RRTypeA), 764 TTL: aws.Int64(recordTTL), 765 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 766 }, 767 { 768 Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 769 Type: aws.String(route53.RRTypeA), 770 TTL: aws.Int64(recordTTL), 771 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 772 }, 773 { 774 Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 775 Type: aws.String(route53.RRTypeA), 776 AliasTarget: &route53.AliasTarget{ 777 DNSName: aws.String("foo.elb.amazonaws.com."), 778 EvaluateTargetHealth: aws.Bool(true), 779 HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), 780 }, 781 }, 782 { 783 Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 784 Type: aws.String(route53.RRTypeCname), 785 TTL: aws.Int64(recordTTL), 786 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("my-internal-host.example.com")}}, 787 }, 788 { 789 Name: aws.String("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 790 Type: aws.String(route53.RRTypeCname), 791 TTL: aws.Int64(recordTTL), 792 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, 793 }, 794 { 795 Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 796 Type: aws.String(route53.RRTypeCname), 797 TTL: aws.Int64(recordTTL), 798 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, 799 }, 800 { 801 Name: aws.String("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 802 Type: aws.String(route53.RRTypeCname), 803 TTL: aws.Int64(recordTTL), 804 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, 805 }, 806 { 807 Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 808 Type: aws.String(route53.RRTypeCname), 809 TTL: aws.Int64(recordTTL), 810 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, 811 }, 812 { 813 Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), 814 Type: aws.String(route53.RRTypeA), 815 TTL: aws.Int64(recordTTL), 816 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 817 }, 818 { 819 Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), 820 Type: aws.String(route53.RRTypeA), 821 TTL: aws.Int64(recordTTL), 822 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 823 SetIdentifier: aws.String("simple-to-weighted"), 824 Weight: aws.Int64(10), 825 }, 826 { 827 Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 828 Type: aws.String(route53.RRTypeA), 829 TTL: aws.Int64(recordTTL), 830 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 831 SetIdentifier: aws.String("policy-change"), 832 Region: aws.String("us-east-1"), 833 }, 834 { 835 Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 836 Type: aws.String(route53.RRTypeA), 837 TTL: aws.Int64(recordTTL), 838 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 839 SetIdentifier: aws.String("after"), 840 Weight: aws.Int64(10), 841 }, 842 { 843 Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), 844 Type: aws.String(route53.RRTypeA), 845 TTL: aws.Int64(recordTTL), 846 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, 847 SetIdentifier: aws.String("no-change"), 848 Weight: aws.Int64(20), 849 }, 850 { 851 Name: aws.String("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), 852 Type: aws.String(route53.RRTypeMx), 853 TTL: aws.Int64(recordTTL), 854 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, 855 }, 856 }) 857 validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ 858 { 859 Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 860 Type: aws.String(route53.RRTypeA), 861 TTL: aws.Int64(recordTTL), 862 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, 863 }, 864 { 865 Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 866 Type: aws.String(route53.RRTypeA), 867 TTL: aws.Int64(recordTTL), 868 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, 869 }, 870 { 871 Name: aws.String("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 872 Type: aws.String(route53.RRTypeA), 873 TTL: aws.Int64(recordTTL), 874 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, 875 }, 876 { 877 Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 878 Type: aws.String(route53.RRTypeA), 879 TTL: aws.Int64(recordTTL), 880 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, 881 }, 882 { 883 Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), 884 Type: aws.String(route53.RRTypeMx), 885 TTL: aws.Int64(recordTTL), 886 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mailhost3.foo.elb.amazonaws.com")}}, 887 }, 888 }) 889 } 890 } 891 892 func TestAWSApplyChangesDryRun(t *testing.T) { 893 originalRecords := []*route53.ResourceRecordSet{ 894 { 895 Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 896 Type: aws.String(route53.RRTypeA), 897 TTL: aws.Int64(recordTTL), 898 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 899 }, 900 { 901 Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 902 Type: aws.String(route53.RRTypeA), 903 TTL: aws.Int64(recordTTL), 904 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, 905 }, 906 { 907 Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 908 Type: aws.String(route53.RRTypeA), 909 TTL: aws.Int64(recordTTL), 910 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, 911 }, 912 { 913 Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), 914 Type: aws.String(route53.RRTypeA), 915 TTL: aws.Int64(recordTTL), 916 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, 917 }, 918 { 919 Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 920 Type: aws.String(route53.RRTypeA), 921 TTL: aws.Int64(recordTTL), 922 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}}, 923 }, 924 { 925 Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 926 Type: aws.String(route53.RRTypeCname), 927 TTL: aws.Int64(recordTTL), 928 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, 929 }, 930 { 931 Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), 932 Type: aws.String(route53.RRTypeCname), 933 TTL: aws.Int64(recordTTL), 934 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, 935 }, 936 { 937 Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 938 Type: aws.String(route53.RRTypeCname), 939 TTL: aws.Int64(recordTTL), 940 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, 941 }, 942 { 943 Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), 944 Type: aws.String(route53.RRTypeCname), 945 TTL: aws.Int64(recordTTL), 946 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, 947 }, 948 { 949 Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 950 Type: aws.String(route53.RRTypeA), 951 TTL: aws.Int64(recordTTL), 952 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, 953 }, 954 { 955 Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), 956 Type: aws.String(route53.RRTypeA), 957 TTL: aws.Int64(recordTTL), 958 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, 959 }, 960 { 961 Name: aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), 962 Type: aws.String(route53.RRTypeMx), 963 TTL: aws.Int64(recordTTL), 964 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mail.foo.elb.amazonaws.com")}}, 965 }, 966 { 967 Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), 968 Type: aws.String(route53.RRTypeMx), 969 TTL: aws.Int64(recordTTL), 970 ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mail.bar.elb.amazonaws.com")}}, 971 }, 972 } 973 974 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalRecords) 975 976 createRecords := []*endpoint.Endpoint{ 977 endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 978 endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 979 endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 980 endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 981 endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), 982 endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mail.foo.elb.amazonaws.com"), 983 } 984 985 currentRecords := []*endpoint.Endpoint{ 986 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 987 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 988 endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"), 989 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 990 endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 991 endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), 992 endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mail.foo.elb.amazonaws.com"), 993 } 994 updatedRecords := []*endpoint.Endpoint{ 995 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 996 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), 997 endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 998 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 999 endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 1000 endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), 1001 endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), 1002 } 1003 1004 deleteRecords := []*endpoint.Endpoint{ 1005 endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 1006 endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 1007 endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 1008 endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 1009 endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), 1010 endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), 1011 } 1012 1013 changes := &plan.Changes{ 1014 Create: createRecords, 1015 UpdateNew: updatedRecords, 1016 UpdateOld: currentRecords, 1017 Delete: deleteRecords, 1018 } 1019 1020 ctx := context.Background() 1021 1022 require.NoError(t, provider.ApplyChanges(ctx, changes)) 1023 1024 validateRecords(t, 1025 append( 1026 listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), 1027 listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...), 1028 originalRecords) 1029 } 1030 1031 func TestAWSChangesByZones(t *testing.T) { 1032 changes := Route53Changes{ 1033 { 1034 Change: route53.Change{ 1035 Action: aws.String(route53.ChangeActionCreate), 1036 ResourceRecordSet: &route53.ResourceRecordSet{ 1037 Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), 1038 }, 1039 }, 1040 }, 1041 { 1042 Change: route53.Change{ 1043 Action: aws.String(route53.ChangeActionCreate), 1044 ResourceRecordSet: &route53.ResourceRecordSet{ 1045 Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), 1046 }, 1047 }, 1048 }, 1049 { 1050 Change: route53.Change{ 1051 Action: aws.String(route53.ChangeActionDelete), 1052 ResourceRecordSet: &route53.ResourceRecordSet{ 1053 Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), 1054 }, 1055 }, 1056 }, 1057 { 1058 Change: route53.Change{ 1059 Action: aws.String(route53.ChangeActionDelete), 1060 ResourceRecordSet: &route53.ResourceRecordSet{ 1061 Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), 1062 }, 1063 }, 1064 }, 1065 } 1066 1067 zones := map[string]*route53.HostedZone{ 1068 "foo-example-org": { 1069 Id: aws.String("foo-example-org"), 1070 Name: aws.String("foo.example.org."), 1071 }, 1072 "bar-example-org": { 1073 Id: aws.String("bar-example-org"), 1074 Name: aws.String("bar.example.org."), 1075 }, 1076 "bar-example-org-private": { 1077 Id: aws.String("bar-example-org-private"), 1078 Name: aws.String("bar.example.org."), 1079 Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}, 1080 }, 1081 "baz-example-org": { 1082 Id: aws.String("baz-example-org"), 1083 Name: aws.String("baz.example.org."), 1084 }, 1085 } 1086 1087 changesByZone := changesByZone(zones, changes) 1088 require.Len(t, changesByZone, 3) 1089 1090 validateAWSChangeRecords(t, changesByZone["foo-example-org"], Route53Changes{ 1091 { 1092 Change: route53.Change{ 1093 Action: aws.String(route53.ChangeActionCreate), 1094 ResourceRecordSet: &route53.ResourceRecordSet{ 1095 Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), 1096 }, 1097 }, 1098 }, 1099 { 1100 Change: route53.Change{ 1101 Action: aws.String(route53.ChangeActionDelete), 1102 ResourceRecordSet: &route53.ResourceRecordSet{ 1103 Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), 1104 }, 1105 }, 1106 }, 1107 }) 1108 1109 validateAWSChangeRecords(t, changesByZone["bar-example-org"], Route53Changes{ 1110 { 1111 Change: route53.Change{ 1112 Action: aws.String(route53.ChangeActionCreate), 1113 ResourceRecordSet: &route53.ResourceRecordSet{ 1114 Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), 1115 }, 1116 }, 1117 }, 1118 { 1119 Change: route53.Change{ 1120 Action: aws.String(route53.ChangeActionDelete), 1121 ResourceRecordSet: &route53.ResourceRecordSet{ 1122 Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), 1123 }, 1124 }, 1125 }, 1126 }) 1127 1128 validateAWSChangeRecords(t, changesByZone["bar-example-org-private"], Route53Changes{ 1129 { 1130 Change: route53.Change{ 1131 Action: aws.String(route53.ChangeActionCreate), 1132 ResourceRecordSet: &route53.ResourceRecordSet{ 1133 Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), 1134 }, 1135 }, 1136 }, 1137 { 1138 Change: route53.Change{ 1139 Action: aws.String(route53.ChangeActionDelete), 1140 ResourceRecordSet: &route53.ResourceRecordSet{ 1141 Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), 1142 }, 1143 }, 1144 }, 1145 }) 1146 } 1147 1148 func TestAWSsubmitChanges(t *testing.T) { 1149 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1150 const subnets = 16 1151 const hosts = defaultBatchChangeSize / subnets 1152 1153 endpoints := make([]*endpoint.Endpoint, 0) 1154 for i := 0; i < subnets; i++ { 1155 for j := 1; j < (hosts + 1); j++ { 1156 hostname := fmt.Sprintf("subnet%dhost%d.zone-1.ext-dns-test-2.teapot.zalan.do", i, j) 1157 ip := fmt.Sprintf("1.1.%d.%d", i, j) 1158 ep := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, endpoint.TTL(recordTTL), ip) 1159 endpoints = append(endpoints, ep) 1160 } 1161 } 1162 1163 ctx := context.Background() 1164 zones, _ := provider.Zones(ctx) 1165 records, _ := provider.Records(ctx) 1166 cs := make(Route53Changes, 0, len(endpoints)) 1167 cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...) 1168 1169 require.NoError(t, provider.submitChanges(ctx, cs, zones)) 1170 1171 records, err := provider.Records(ctx) 1172 require.NoError(t, err) 1173 1174 validateEndpoints(t, provider, records, endpoints) 1175 } 1176 1177 func TestAWSsubmitChangesError(t *testing.T) { 1178 provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1179 clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) 1180 1181 ctx := context.Background() 1182 zones, err := provider.Zones(ctx) 1183 require.NoError(t, err) 1184 1185 ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1") 1186 cs := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep}) 1187 1188 require.Error(t, provider.submitChanges(ctx, cs, zones)) 1189 } 1190 1191 func TestAWSsubmitChangesRetryOnError(t *testing.T) { 1192 provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1193 1194 ctx := context.Background() 1195 zones, err := provider.Zones(ctx) 1196 require.NoError(t, err) 1197 1198 ep1 := endpoint.NewEndpointWithTTL("success.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1") 1199 ep2 := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.2") 1200 ep3 := endpoint.NewEndpointWithTTL("success2.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.3") 1201 1202 ep2txt := endpoint.NewEndpointWithTTL("fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "something") // "__edns_housekeeping" is the TXT suffix 1203 ep2txt.Labels = map[string]string{ 1204 endpoint.OwnedRecordLabelKey: "fail.zone-1.ext-dns-test-2.teapot.zalan.do", 1205 } 1206 1207 // "success" and "fail" are created in the first step, both are submitted in the same batch; this should fail 1208 cs1 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep1}) 1209 input1 := &route53.ChangeResourceRecordSetsInput{ 1210 HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), 1211 ChangeBatch: &route53.ChangeBatch{ 1212 Changes: cs1.Route53Changes(), 1213 }, 1214 } 1215 clientStub.MockMethod("ChangeResourceRecordSets", input1).Return(nil, fmt.Errorf("Mock route53 failure")) 1216 1217 // because of the failure, changes will be retried one by one; make "fail" submitted in its own batch fail as well 1218 cs2 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt}) 1219 input2 := &route53.ChangeResourceRecordSetsInput{ 1220 HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), 1221 ChangeBatch: &route53.ChangeBatch{ 1222 Changes: cs2.Route53Changes(), 1223 }, 1224 } 1225 clientStub.MockMethod("ChangeResourceRecordSets", input2).Return(nil, fmt.Errorf("Mock route53 failure")) 1226 1227 // "success" should have been created, verify that we still get an error because "fail" failed 1228 require.Error(t, provider.submitChanges(ctx, cs1, zones)) 1229 1230 // assert that "success" was successfully created and "fail" and its TXT record were not 1231 records, err := provider.Records(ctx) 1232 require.NoError(t, err) 1233 require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do")) 1234 require.False(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do")) 1235 require.False(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do")) 1236 1237 // next batch should contain "fail" and "success2", should succeed this time 1238 cs3 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep3}) 1239 require.NoError(t, provider.submitChanges(ctx, cs3, zones)) 1240 1241 // verify all records are there 1242 records, err = provider.Records(ctx) 1243 require.NoError(t, err) 1244 require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do")) 1245 require.True(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do")) 1246 require.True(t, containsRecordWithDNSName(records, "success2.zone-1.ext-dns-test-2.teapot.zalan.do")) 1247 require.True(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do")) 1248 } 1249 1250 func TestAWSBatchChangeSet(t *testing.T) { 1251 var cs Route53Changes 1252 1253 for i := 1; i <= defaultBatchChangeSize; i += 2 { 1254 cs = append(cs, &Route53Change{ 1255 Change: route53.Change{ 1256 Action: aws.String(route53.ChangeActionCreate), 1257 ResourceRecordSet: &route53.ResourceRecordSet{ 1258 Name: aws.String(fmt.Sprintf("host-%d", i)), 1259 Type: aws.String("A"), 1260 }, 1261 }, 1262 }) 1263 cs = append(cs, &Route53Change{ 1264 Change: route53.Change{ 1265 Action: aws.String(route53.ChangeActionCreate), 1266 ResourceRecordSet: &route53.ResourceRecordSet{ 1267 Name: aws.String(fmt.Sprintf("host-%d", i)), 1268 Type: aws.String("TXT"), 1269 }, 1270 }, 1271 }) 1272 } 1273 1274 batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) 1275 1276 require.Equal(t, 1, len(batchCs)) 1277 1278 // sorting cs not needed as it should be returned as is 1279 validateAWSChangeRecords(t, batchCs[0], cs) 1280 } 1281 1282 func TestAWSBatchChangeSetExceeding(t *testing.T) { 1283 var cs Route53Changes 1284 const testCount = 50 1285 const testLimit = 11 1286 const expectedBatchCount = 5 1287 const expectedChangesCount = 10 1288 1289 for i := 1; i <= testCount; i += 2 { 1290 cs = append(cs, 1291 &Route53Change{ 1292 Change: route53.Change{ 1293 Action: aws.String(route53.ChangeActionCreate), 1294 ResourceRecordSet: &route53.ResourceRecordSet{ 1295 Name: aws.String(fmt.Sprintf("host-%d", i)), 1296 Type: aws.String("A"), 1297 }, 1298 }, 1299 }, 1300 &Route53Change{ 1301 Change: route53.Change{ 1302 Action: aws.String(route53.ChangeActionCreate), 1303 ResourceRecordSet: &route53.ResourceRecordSet{ 1304 Name: aws.String(fmt.Sprintf("host-%d", i)), 1305 Type: aws.String("TXT"), 1306 }, 1307 }, 1308 }, 1309 ) 1310 } 1311 1312 batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) 1313 1314 require.Equal(t, expectedBatchCount, len(batchCs)) 1315 1316 // sorting cs needed to match batchCs 1317 for i, batch := range batchCs { 1318 validateAWSChangeRecords(t, batch, sortChangesByActionNameType(cs)[i*expectedChangesCount:expectedChangesCount*(i+1)]) 1319 } 1320 } 1321 1322 func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) { 1323 var cs Route53Changes 1324 const testCount = 10 1325 const testLimit = 1 1326 1327 for i := 1; i <= testCount; i += 2 { 1328 cs = append(cs, 1329 &Route53Change{ 1330 Change: route53.Change{ 1331 Action: aws.String(route53.ChangeActionCreate), 1332 ResourceRecordSet: &route53.ResourceRecordSet{ 1333 Name: aws.String(fmt.Sprintf("host-%d", i)), 1334 Type: aws.String("A"), 1335 }, 1336 }, 1337 }, 1338 &Route53Change{ 1339 Change: route53.Change{ 1340 Action: aws.String(route53.ChangeActionCreate), 1341 ResourceRecordSet: &route53.ResourceRecordSet{ 1342 Name: aws.String(fmt.Sprintf("host-%d", i)), 1343 Type: aws.String("TXT"), 1344 }, 1345 }, 1346 }, 1347 ) 1348 } 1349 1350 batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) 1351 1352 require.Equal(t, 0, len(batchCs)) 1353 } 1354 1355 func TestAWSBatchChangeSetExceedingBytesLimit(t *testing.T) { 1356 const ( 1357 testCount = 50 1358 testLimit = 100 1359 groupSize = 2 1360 ) 1361 1362 var ( 1363 cs Route53Changes 1364 // Bytes for each name 1365 testBytes = len([]byte("1.2.3.4")) + len([]byte("test-record")) 1366 // testCount / groupSize / (testLimit // bytes) 1367 expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes) 1368 // Round up 1369 expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) 1370 ) 1371 1372 for i := 1; i <= testCount; i += groupSize { 1373 cs = append(cs, 1374 &Route53Change{ 1375 Change: route53.Change{ 1376 Action: aws.String(route53.ChangeActionCreate), 1377 ResourceRecordSet: &route53.ResourceRecordSet{ 1378 Name: aws.String(fmt.Sprintf("host-%d", i)), 1379 Type: aws.String("A"), 1380 ResourceRecords: []*route53.ResourceRecord{ 1381 { 1382 Value: aws.String("1.2.3.4"), 1383 }, 1384 }, 1385 }, 1386 }, 1387 sizeBytes: len([]byte("1.2.3.4")), 1388 sizeValues: 1, 1389 }, 1390 &Route53Change{ 1391 Change: route53.Change{ 1392 Action: aws.String(route53.ChangeActionCreate), 1393 ResourceRecordSet: &route53.ResourceRecordSet{ 1394 Name: aws.String(fmt.Sprintf("host-%d", i)), 1395 Type: aws.String("TXT"), 1396 ResourceRecords: []*route53.ResourceRecord{ 1397 { 1398 Value: aws.String("txt-record"), 1399 }, 1400 }, 1401 }, 1402 }, 1403 sizeBytes: len([]byte("txt-record")), 1404 sizeValues: 1, 1405 }, 1406 ) 1407 } 1408 1409 batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues) 1410 1411 require.Equal(t, expectedBatchCount, len(batchCs)) 1412 } 1413 1414 func TestAWSBatchChangeSetExceedingBytesLimitUpsert(t *testing.T) { 1415 const ( 1416 testCount = 50 1417 testLimit = 100 1418 groupSize = 2 1419 ) 1420 1421 var ( 1422 cs Route53Changes 1423 // Bytes for each name multiplied by 2 for Upsert records 1424 testBytes = (len([]byte("1.2.3.4")) + len([]byte("test-record"))) * 2 1425 // testCount / groupSize / (testLimit // bytes) 1426 expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes) 1427 // Round up 1428 expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) 1429 ) 1430 1431 for i := 1; i <= testCount; i += groupSize { 1432 cs = append(cs, 1433 &Route53Change{ 1434 Change: route53.Change{ 1435 Action: aws.String(route53.ChangeActionUpsert), 1436 ResourceRecordSet: &route53.ResourceRecordSet{ 1437 Name: aws.String(fmt.Sprintf("host-%d", i)), 1438 Type: aws.String("A"), 1439 ResourceRecords: []*route53.ResourceRecord{ 1440 { 1441 Value: aws.String("1.2.3.4"), 1442 }, 1443 }, 1444 }, 1445 }, 1446 sizeBytes: len([]byte("1.2.3.4")) * 2, 1447 sizeValues: 1, 1448 }, 1449 &Route53Change{ 1450 Change: route53.Change{ 1451 Action: aws.String(route53.ChangeActionUpsert), 1452 ResourceRecordSet: &route53.ResourceRecordSet{ 1453 Name: aws.String(fmt.Sprintf("host-%d", i)), 1454 Type: aws.String("TXT"), 1455 ResourceRecords: []*route53.ResourceRecord{ 1456 { 1457 Value: aws.String("txt-record"), 1458 }, 1459 }, 1460 }, 1461 }, 1462 sizeBytes: len([]byte("txt-record")) * 2, 1463 sizeValues: 1, 1464 }, 1465 ) 1466 } 1467 1468 batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues) 1469 1470 require.Equal(t, expectedBatchCount, len(batchCs)) 1471 } 1472 1473 func TestAWSBatchChangeSetExceedingValuesLimit(t *testing.T) { 1474 const ( 1475 testCount = 50 1476 testLimit = 100 1477 groupSize = 2 1478 // Values for each group 1479 testValues = 2 1480 ) 1481 1482 var ( 1483 cs Route53Changes 1484 // testCount / groupSize / (testLimit // bytes) 1485 expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues) 1486 // Round up 1487 expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) 1488 ) 1489 1490 for i := 1; i <= testCount; i += groupSize { 1491 cs = append(cs, 1492 &Route53Change{ 1493 Change: route53.Change{ 1494 Action: aws.String(route53.ChangeActionCreate), 1495 ResourceRecordSet: &route53.ResourceRecordSet{ 1496 Name: aws.String(fmt.Sprintf("host-%d", i)), 1497 Type: aws.String("A"), 1498 ResourceRecords: []*route53.ResourceRecord{ 1499 { 1500 Value: aws.String("1.2.3.4"), 1501 }, 1502 }, 1503 }, 1504 }, 1505 sizeBytes: len([]byte("1.2.3.4")), 1506 sizeValues: 1, 1507 }, 1508 &Route53Change{ 1509 Change: route53.Change{ 1510 Action: aws.String(route53.ChangeActionCreate), 1511 ResourceRecordSet: &route53.ResourceRecordSet{ 1512 Name: aws.String(fmt.Sprintf("host-%d", i)), 1513 Type: aws.String("TXT"), 1514 ResourceRecords: []*route53.ResourceRecord{ 1515 { 1516 Value: aws.String("txt-record"), 1517 }, 1518 }, 1519 }, 1520 }, 1521 sizeBytes: len([]byte("txt-record")), 1522 sizeValues: 1, 1523 }, 1524 ) 1525 } 1526 1527 batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit) 1528 1529 require.Equal(t, expectedBatchCount, len(batchCs)) 1530 } 1531 1532 func TestAWSBatchChangeSetExceedingValuesLimitUpsert(t *testing.T) { 1533 const ( 1534 testCount = 50 1535 testLimit = 100 1536 groupSize = 2 1537 // Values for each group multiplied by 2 for Upsert records 1538 testValues = 2 * 2 1539 ) 1540 1541 var ( 1542 cs Route53Changes 1543 // testCount / groupSize / (testLimit // bytes) 1544 expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues) 1545 // Round up 1546 expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) 1547 ) 1548 1549 for i := 1; i <= testCount; i += groupSize { 1550 cs = append(cs, 1551 &Route53Change{ 1552 Change: route53.Change{ 1553 Action: aws.String(route53.ChangeActionUpsert), 1554 ResourceRecordSet: &route53.ResourceRecordSet{ 1555 Name: aws.String(fmt.Sprintf("host-%d", i)), 1556 Type: aws.String("A"), 1557 ResourceRecords: []*route53.ResourceRecord{ 1558 { 1559 Value: aws.String("1.2.3.4"), 1560 }, 1561 }, 1562 }, 1563 }, 1564 sizeBytes: len([]byte("1.2.3.4")) * 2, 1565 sizeValues: 1, 1566 }, 1567 &Route53Change{ 1568 Change: route53.Change{ 1569 Action: aws.String(route53.ChangeActionUpsert), 1570 ResourceRecordSet: &route53.ResourceRecordSet{ 1571 Name: aws.String(fmt.Sprintf("host-%d", i)), 1572 Type: aws.String("TXT"), 1573 ResourceRecords: []*route53.ResourceRecord{ 1574 { 1575 Value: aws.String("txt-record"), 1576 }, 1577 }, 1578 }, 1579 }, 1580 sizeBytes: len([]byte("txt-record")) * 2, 1581 sizeValues: 1, 1582 }, 1583 ) 1584 } 1585 1586 batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit) 1587 1588 require.Equal(t, expectedBatchCount, len(batchCs)) 1589 } 1590 1591 func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { 1592 assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected) 1593 1594 normalized, err := provider.AdjustEndpoints(endpoints) 1595 assert.NoError(t, err) 1596 assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized) 1597 } 1598 1599 func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) { 1600 require.Len(t, zones, len(expected)) 1601 1602 for i, zone := range zones { 1603 validateAWSZone(t, zone, expected[i]) 1604 } 1605 } 1606 1607 func validateAWSZone(t *testing.T, zone *route53.HostedZone, expected *route53.HostedZone) { 1608 assert.Equal(t, aws.StringValue(expected.Id), aws.StringValue(zone.Id)) 1609 assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(zone.Name)) 1610 } 1611 1612 func validateAWSChangeRecords(t *testing.T, records Route53Changes, expected Route53Changes) { 1613 require.Len(t, records, len(expected)) 1614 1615 for i := range records { 1616 validateAWSChangeRecord(t, records[i], expected[i]) 1617 } 1618 } 1619 1620 func validateAWSChangeRecord(t *testing.T, record *Route53Change, expected *Route53Change) { 1621 assert.Equal(t, aws.StringValue(expected.Action), aws.StringValue(record.Action)) 1622 assert.Equal(t, aws.StringValue(expected.ResourceRecordSet.Name), aws.StringValue(record.ResourceRecordSet.Name)) 1623 assert.Equal(t, aws.StringValue(expected.ResourceRecordSet.Type), aws.StringValue(record.ResourceRecordSet.Type)) 1624 } 1625 1626 func TestAWSCreateRecordsWithCNAME(t *testing.T) { 1627 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1628 1629 records := []*endpoint.Endpoint{ 1630 {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME}, 1631 } 1632 1633 adjusted, err := provider.AdjustEndpoints(records) 1634 require.NoError(t, err) 1635 require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ 1636 Create: adjusted, 1637 })) 1638 1639 recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") 1640 1641 validateRecords(t, recordSets, []*route53.ResourceRecordSet{ 1642 { 1643 Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 1644 Type: aws.String(endpoint.RecordTypeCNAME), 1645 TTL: aws.Int64(300), 1646 ResourceRecords: []*route53.ResourceRecord{ 1647 { 1648 Value: aws.String("foo.example.org"), 1649 }, 1650 }, 1651 }, 1652 }) 1653 } 1654 1655 func TestAWSCreateRecordsWithALIAS(t *testing.T) { 1656 for key, evaluateTargetHealth := range map[string]bool{ 1657 "true": true, 1658 "false": false, 1659 "": false, 1660 } { 1661 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1662 1663 // Test dualstack and ipv4 load balancer targets 1664 records := []*endpoint.Endpoint{ 1665 { 1666 DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", 1667 Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, 1668 RecordType: endpoint.RecordTypeA, 1669 ProviderSpecific: endpoint.ProviderSpecific{ 1670 endpoint.ProviderSpecificProperty{ 1671 Name: providerSpecificAlias, 1672 Value: "true", 1673 }, 1674 endpoint.ProviderSpecificProperty{ 1675 Name: providerSpecificEvaluateTargetHealth, 1676 Value: key, 1677 }, 1678 }, 1679 }, 1680 { 1681 DNSName: "create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do", 1682 Targets: endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"}, 1683 RecordType: endpoint.RecordTypeA, 1684 ProviderSpecific: endpoint.ProviderSpecific{ 1685 endpoint.ProviderSpecificProperty{ 1686 Name: providerSpecificAlias, 1687 Value: "true", 1688 }, 1689 endpoint.ProviderSpecificProperty{ 1690 Name: providerSpecificEvaluateTargetHealth, 1691 Value: key, 1692 }, 1693 }, 1694 Labels: map[string]string{endpoint.DualstackLabelKey: "true"}, 1695 }, 1696 } 1697 adjusted, err := provider.AdjustEndpoints(records) 1698 require.NoError(t, err) 1699 require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ 1700 Create: adjusted, 1701 })) 1702 1703 recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") 1704 1705 validateRecords(t, recordSets, []*route53.ResourceRecordSet{ 1706 { 1707 AliasTarget: &route53.AliasTarget{ 1708 DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), 1709 EvaluateTargetHealth: aws.Bool(evaluateTargetHealth), 1710 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 1711 }, 1712 Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), 1713 Type: aws.String(route53.RRTypeA), 1714 }, 1715 { 1716 AliasTarget: &route53.AliasTarget{ 1717 DNSName: aws.String("bar.eu-central-1.elb.amazonaws.com."), 1718 EvaluateTargetHealth: aws.Bool(evaluateTargetHealth), 1719 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 1720 }, 1721 Name: aws.String("create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do."), 1722 Type: aws.String(route53.RRTypeA), 1723 }, 1724 { 1725 AliasTarget: &route53.AliasTarget{ 1726 DNSName: aws.String("bar.eu-central-1.elb.amazonaws.com."), 1727 EvaluateTargetHealth: aws.Bool(evaluateTargetHealth), 1728 HostedZoneId: aws.String("Z215JYRZR1TBD5"), 1729 }, 1730 Name: aws.String("create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do."), 1731 Type: aws.String(route53.RRTypeAaaa), 1732 }, 1733 }) 1734 } 1735 } 1736 1737 func TestAWSisLoadBalancer(t *testing.T) { 1738 for _, tc := range []struct { 1739 target string 1740 recordType string 1741 preferCNAME bool 1742 expected bool 1743 }{ 1744 {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, false, true}, 1745 {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, true, false}, 1746 {"foo.example.org", endpoint.RecordTypeCNAME, false, false}, 1747 {"foo.example.org", endpoint.RecordTypeCNAME, true, false}, 1748 } { 1749 ep := &endpoint.Endpoint{ 1750 Targets: endpoint.Targets{tc.target}, 1751 RecordType: tc.recordType, 1752 } 1753 assert.Equal(t, tc.expected, useAlias(ep, tc.preferCNAME)) 1754 } 1755 } 1756 1757 func TestAWSisAWSAlias(t *testing.T) { 1758 for _, tc := range []struct { 1759 target string 1760 recordType string 1761 alias bool 1762 hz string 1763 }{ 1764 {"foo.example.org", endpoint.RecordTypeA, false, ""}, // normal A record 1765 {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeA, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name 1766 {"foobar.example.org", endpoint.RecordTypeA, true, "Z1234567890ABC"}, // HZID retrieved by Route53 1767 {"baz.example.org", endpoint.RecordTypeA, true, sameZoneAlias}, // record to be created 1768 } { 1769 ep := &endpoint.Endpoint{ 1770 Targets: endpoint.Targets{tc.target}, 1771 RecordType: tc.recordType, 1772 } 1773 if tc.alias { 1774 ep = ep.WithProviderSpecific(providerSpecificAlias, "true") 1775 ep = ep.WithProviderSpecific(providerSpecificTargetHostedZone, tc.hz) 1776 } 1777 assert.Equal(t, tc.hz, isAWSAlias(ep), "%v", tc) 1778 } 1779 } 1780 1781 func TestAWSCanonicalHostedZone(t *testing.T) { 1782 for suffix, id := range canonicalHostedZones { 1783 zone := canonicalHostedZone(fmt.Sprintf("foo.%s", suffix)) 1784 assert.Equal(t, id, zone) 1785 } 1786 1787 zone := canonicalHostedZone("foo.example.org") 1788 assert.Equal(t, "", zone, "no canonical zone should be returned for a non-aws hostname") 1789 } 1790 1791 func TestAWSSuitableZones(t *testing.T) { 1792 zones := map[string]*route53.HostedZone{ 1793 // Public domain 1794 "example-org": {Id: aws.String("example-org"), Name: aws.String("example.org.")}, 1795 // Public subdomain 1796 "bar-example-org": {Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}}, 1797 // Public subdomain 1798 "longfoo-bar-example-org": {Id: aws.String("longfoo-bar-example-org"), Name: aws.String("longfoo.bar.example.org.")}, 1799 // Private domain 1800 "example-org-private": {Id: aws.String("example-org-private"), Name: aws.String("example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}, 1801 // Private subdomain 1802 "bar-example-org-private": {Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}, 1803 } 1804 1805 for _, tc := range []struct { 1806 hostname string 1807 expected []*route53.HostedZone 1808 }{ 1809 // bar.example.org is NOT suitable 1810 {"foobar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}}, 1811 1812 // all matching private zones are suitable 1813 // https://github.com/kubernetes-sigs/external-dns/pull/356 1814 {"bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, 1815 1816 {"foo.bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, 1817 {"foo.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}}, 1818 {"foo.kubernetes.io.", nil}, 1819 } { 1820 suitableZones := suitableZones(tc.hostname, zones) 1821 sort.Slice(suitableZones, func(i, j int) bool { 1822 return *suitableZones[i].Id < *suitableZones[j].Id 1823 }) 1824 sort.Slice(tc.expected, func(i, j int) bool { 1825 return *tc.expected[i].Id < *tc.expected[j].Id 1826 }) 1827 assert.Equal(t, tc.expected, suitableZones) 1828 } 1829 } 1830 1831 func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) { 1832 params := &route53.CreateHostedZoneInput{ 1833 CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), 1834 Name: zone.Name, 1835 HostedZoneConfig: zone.Config, 1836 } 1837 1838 if _, err := provider.client.CreateHostedZoneWithContext(context.Background(), params); err != nil { 1839 require.EqualError(t, err, route53.ErrCodeHostedZoneAlreadyExists) 1840 } 1841 } 1842 1843 func setAWSRecords(t *testing.T, provider *AWSProvider, records []*route53.ResourceRecordSet) { 1844 dryRun := provider.dryRun 1845 provider.dryRun = false 1846 defer func() { 1847 provider.dryRun = dryRun 1848 }() 1849 1850 ctx := context.Background() 1851 endpoints, err := provider.Records(ctx) 1852 require.NoError(t, err) 1853 1854 validateEndpoints(t, provider, endpoints, []*endpoint.Endpoint{}) 1855 1856 var changes Route53Changes 1857 for _, record := range records { 1858 changes = append(changes, &Route53Change{ 1859 Change: route53.Change{ 1860 Action: aws.String(route53.ChangeActionCreate), 1861 ResourceRecordSet: record, 1862 }, 1863 }) 1864 } 1865 1866 zones, err := provider.Zones(ctx) 1867 require.NoError(t, err) 1868 err = provider.submitChanges(ctx, changes, zones) 1869 require.NoError(t, err) 1870 1871 _, err = provider.Records(ctx) 1872 require.NoError(t, err) 1873 } 1874 1875 func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet { 1876 recordSets := []*route53.ResourceRecordSet{} 1877 require.NoError(t, client.ListResourceRecordSetsPagesWithContext(context.Background(), &route53.ListResourceRecordSetsInput{ 1878 HostedZoneId: aws.String(zone), 1879 MaxItems: aws.String(route53PageSize), 1880 }, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool { 1881 recordSets = append(recordSets, resp.ResourceRecordSets...) 1882 return true 1883 })) 1884 1885 return recordSets 1886 } 1887 1888 func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { 1889 return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) 1890 } 1891 1892 func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { 1893 client := NewRoute53APIStub(t) 1894 1895 provider := &AWSProvider{ 1896 client: client, 1897 batchChangeSize: defaultBatchChangeSize, 1898 batchChangeSizeBytes: defaultBatchChangeSizeBytes, 1899 batchChangeSizeValues: defaultBatchChangeSizeValues, 1900 batchChangeInterval: defaultBatchChangeInterval, 1901 evaluateTargetHealth: evaluateTargetHealth, 1902 domainFilter: domainFilter, 1903 zoneIDFilter: zoneIDFilter, 1904 zoneTypeFilter: zoneTypeFilter, 1905 zoneTagFilter: zoneTagFilter, 1906 dryRun: false, 1907 zonesCache: &zonesListCache{duration: 1 * time.Minute}, 1908 failedChangesQueue: make(map[string]Route53Changes), 1909 } 1910 1911 createAWSZone(t, provider, &route53.HostedZone{ 1912 Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), 1913 Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), 1914 Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, 1915 }) 1916 1917 createAWSZone(t, provider, &route53.HostedZone{ 1918 Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), 1919 Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), 1920 Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, 1921 }) 1922 1923 createAWSZone(t, provider, &route53.HostedZone{ 1924 Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), 1925 Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), 1926 Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}, 1927 }) 1928 1929 // filtered out by domain filter 1930 createAWSZone(t, provider, &route53.HostedZone{ 1931 Id: aws.String("/hostedzone/zone-4.ext-dns-test-3.teapot.zalan.do."), 1932 Name: aws.String("zone-4.ext-dns-test-3.teapot.zalan.do."), 1933 Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, 1934 }) 1935 1936 setupZoneTags(provider.client.(*Route53APIStub)) 1937 1938 setAWSRecords(t, provider, records) 1939 1940 provider.dryRun = dryRun 1941 1942 return provider, client 1943 } 1944 1945 func setupZoneTags(client *Route53APIStub) { 1946 addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{ 1947 "zone-1-tag-1": "tag-1-value", 1948 "domain": "test-2", 1949 "zone": "1", 1950 }) 1951 addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{ 1952 "zone-2-tag-1": "tag-1-value", 1953 "domain": "test-2", 1954 "zone": "2", 1955 }) 1956 addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{ 1957 "zone-3-tag-1": "tag-1-value", 1958 "domain": "test-2", 1959 "zone": "3", 1960 }) 1961 addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{ 1962 "zone-4-tag-1": "tag-1-value", 1963 "domain": "test-3", 1964 "zone": "4", 1965 }) 1966 } 1967 1968 func addZoneTags(tagMap map[string][]*route53.Tag, zoneID string, tags map[string]string) { 1969 tagList := make([]*route53.Tag, 0, len(tags)) 1970 for k, v := range tags { 1971 tagList = append(tagList, &route53.Tag{ 1972 Key: aws.String(k), 1973 Value: aws.String(v), 1974 }) 1975 } 1976 tagMap[zoneID] = tagList 1977 } 1978 1979 func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) { 1980 assert.ElementsMatch(t, expected, records) 1981 } 1982 1983 func containsRecordWithDNSName(records []*endpoint.Endpoint, dnsName string) bool { 1984 for _, record := range records { 1985 if record.DNSName == dnsName { 1986 return true 1987 } 1988 } 1989 return false 1990 } 1991 1992 func TestRequiresDeleteCreate(t *testing.T) { 1993 provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) 1994 1995 oldRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8") 1996 newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false") 1997 1998 assert.False(t, provider.requiresDeleteCreate(oldRecordType, oldRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, oldRecordType) 1999 assert.True(t, provider.requiresDeleteCreate(oldRecordType, newRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, newRecordType) 2000 2001 oldAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1") 2002 newAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true") 2003 2004 assert.False(t, provider.requiresDeleteCreate(oldAtoAlias, oldAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, oldAtoAlias.DNSName) 2005 assert.True(t, provider.requiresDeleteCreate(oldAtoAlias, newAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, newAtoAlias) 2006 2007 oldPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificRegion, "us-east-1") 2008 newPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificWeight, "10") 2009 2010 assert.False(t, provider.requiresDeleteCreate(oldPolicy, oldPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, oldPolicy) 2011 assert.True(t, provider.requiresDeleteCreate(oldPolicy, newPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, newPolicy) 2012 2013 oldSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("old") 2014 newSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("new") 2015 2016 assert.False(t, provider.requiresDeleteCreate(oldSetIdentifier, oldSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, oldSetIdentifier) 2017 assert.True(t, provider.requiresDeleteCreate(oldSetIdentifier, newSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, newSetIdentifier) 2018 }