sigs.k8s.io/external-dns@v0.14.1/provider/google/google_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 google 18 19 import ( 20 "fmt" 21 "net/http" 22 "sort" 23 "strings" 24 "testing" 25 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 "golang.org/x/net/context" 29 dns "google.golang.org/api/dns/v1" 30 "google.golang.org/api/googleapi" 31 32 "sigs.k8s.io/external-dns/endpoint" 33 "sigs.k8s.io/external-dns/internal/testutils" 34 "sigs.k8s.io/external-dns/plan" 35 "sigs.k8s.io/external-dns/provider" 36 ) 37 38 var ( 39 testZones = map[string]*dns.ManagedZone{} 40 testRecords = map[string]map[string]*dns.ResourceRecordSet{} 41 googleDefaultBatchChangeSize = 4000 42 ) 43 44 type mockManagedZonesCreateCall struct { 45 project string 46 managedZone *dns.ManagedZone 47 } 48 49 func (m *mockManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) { 50 zoneKey := zoneKey(m.project, m.managedZone.Name) 51 52 if _, ok := testZones[zoneKey]; ok { 53 return nil, &googleapi.Error{Code: http.StatusConflict} 54 } 55 56 testZones[zoneKey] = m.managedZone 57 58 return m.managedZone, nil 59 } 60 61 type mockManagedZonesListCall struct { 62 project string 63 } 64 65 func (m *mockManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error { 66 zones := []*dns.ManagedZone{} 67 68 for k, v := range testZones { 69 if strings.HasPrefix(k, m.project+"/") { 70 zones = append(zones, v) 71 } 72 } 73 74 return f(&dns.ManagedZonesListResponse{ManagedZones: zones}) 75 } 76 77 type mockManagedZonesClient struct{} 78 79 func (m *mockManagedZonesClient) Create(project string, managedZone *dns.ManagedZone) managedZonesCreateCallInterface { 80 return &mockManagedZonesCreateCall{project: project, managedZone: managedZone} 81 } 82 83 func (m *mockManagedZonesClient) List(project string) managedZonesListCallInterface { 84 return &mockManagedZonesListCall{project: project} 85 } 86 87 type mockResourceRecordSetsListCall struct { 88 project string 89 managedZone string 90 } 91 92 func (m *mockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error { 93 zoneKey := zoneKey(m.project, m.managedZone) 94 95 if _, ok := testZones[zoneKey]; !ok { 96 return &googleapi.Error{Code: http.StatusNotFound} 97 } 98 99 resp := []*dns.ResourceRecordSet{} 100 101 for _, v := range testRecords[zoneKey] { 102 resp = append(resp, v) 103 } 104 105 return f(&dns.ResourceRecordSetsListResponse{Rrsets: resp}) 106 } 107 108 type mockResourceRecordSetsClient struct{} 109 110 func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface { 111 return &mockResourceRecordSetsListCall{project: project, managedZone: managedZone} 112 } 113 114 type mockChangesCreateCall struct { 115 project string 116 managedZone string 117 change *dns.Change 118 } 119 120 func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) { 121 zoneKey := zoneKey(m.project, m.managedZone) 122 123 if _, ok := testZones[zoneKey]; !ok { 124 return nil, &googleapi.Error{Code: http.StatusNotFound} 125 } 126 127 if _, ok := testRecords[zoneKey]; !ok { 128 testRecords[zoneKey] = make(map[string]*dns.ResourceRecordSet) 129 } 130 131 for _, c := range append(m.change.Additions, m.change.Deletions...) { 132 if !isValidRecordSet(c) { 133 return nil, &googleapi.Error{ 134 Code: http.StatusBadRequest, 135 Message: fmt.Sprintf("invalid record: %v", c), 136 } 137 } 138 } 139 140 for _, del := range m.change.Deletions { 141 recordKey := recordKey(del.Type, del.Name) 142 delete(testRecords[zoneKey], recordKey) 143 } 144 145 for _, add := range m.change.Additions { 146 recordKey := recordKey(add.Type, add.Name) 147 testRecords[zoneKey][recordKey] = add 148 } 149 150 return m.change, nil 151 } 152 153 type mockChangesClient struct{} 154 155 func (m *mockChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface { 156 return &mockChangesCreateCall{project: project, managedZone: managedZone, change: change} 157 } 158 159 func zoneKey(project, zoneName string) string { 160 return project + "/" + zoneName 161 } 162 163 func recordKey(recordType, recordName string) string { 164 return recordType + "/" + recordName 165 } 166 167 func isValidRecordSet(recordSet *dns.ResourceRecordSet) bool { 168 if !hasTrailingDot(recordSet.Name) { 169 return false 170 } 171 172 switch recordSet.Type { 173 case endpoint.RecordTypeCNAME: 174 for _, rrd := range recordSet.Rrdatas { 175 if !hasTrailingDot(rrd) { 176 return false 177 } 178 } 179 case endpoint.RecordTypeA, endpoint.RecordTypeTXT: 180 for _, rrd := range recordSet.Rrdatas { 181 if hasTrailingDot(rrd) { 182 return false 183 } 184 } 185 default: 186 panic("unhandled record type") 187 } 188 189 return true 190 } 191 192 func hasTrailingDot(target string) bool { 193 return strings.HasSuffix(target, ".") 194 } 195 196 func TestGoogleZonesIDFilter(t *testing.T) { 197 provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"10002"}), provider.NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) 198 199 zones, err := provider.Zones(context.Background()) 200 require.NoError(t, err) 201 202 validateZones(t, zones, map[string]*dns.ManagedZone{ 203 "internal-2": {Name: "internal-2", DnsName: "cluster.local.", Id: 10002, Visibility: "private"}, 204 }) 205 } 206 207 func TestGoogleZonesNameFilter(t *testing.T) { 208 provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"internal-2"}), provider.NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) 209 210 zones, err := provider.Zones(context.Background()) 211 require.NoError(t, err) 212 213 validateZones(t, zones, map[string]*dns.ManagedZone{ 214 "internal-2": {Name: "internal-2", DnsName: "cluster.local.", Id: 10002, Visibility: "private"}, 215 }) 216 } 217 218 func TestGoogleZonesVisibilityFilterPublic(t *testing.T) { 219 provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"split-horizon-1"}), provider.NewZoneTypeFilter("public"), false, []*endpoint.Endpoint{}) 220 221 zones, err := provider.Zones(context.Background()) 222 require.NoError(t, err) 223 224 validateZones(t, zones, map[string]*dns.ManagedZone{ 225 "split-horizon-1": {Name: "split-horizon-1", DnsName: "cluster.local.", Id: 10001, Visibility: "public"}, 226 }) 227 } 228 229 func TestGoogleZonesVisibilityFilterPrivate(t *testing.T) { 230 provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"split-horizon-1"}), provider.NewZoneTypeFilter("public"), false, []*endpoint.Endpoint{}) 231 232 zones, err := provider.Zones(context.Background()) 233 require.NoError(t, err) 234 235 validateZones(t, zones, map[string]*dns.ManagedZone{ 236 "split-horizon-1": {Name: "split-horizon-1", DnsName: "cluster.local.", Id: 10001, Visibility: "public"}, 237 }) 238 } 239 240 func TestGoogleZonesVisibilityFilterPrivatePeering(t *testing.T) { 241 provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"svc.local."}), provider.NewZoneIDFilter([]string{""}), provider.NewZoneTypeFilter("private"), false, []*endpoint.Endpoint{}) 242 243 zones, err := provider.Zones(context.Background()) 244 require.NoError(t, err) 245 246 validateZones(t, zones, map[string]*dns.ManagedZone{ 247 "svc-local": {Name: "svc-local", DnsName: "svc.local.", Id: 1005, Visibility: "private"}, 248 }) 249 } 250 251 func TestGoogleZones(t *testing.T) { 252 provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) 253 254 zones, err := provider.Zones(context.Background()) 255 require.NoError(t, err) 256 257 validateZones(t, zones, map[string]*dns.ManagedZone{ 258 "zone-1-ext-dns-test-2-gcp-zalan-do": {Name: "zone-1-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-1.ext-dns-test-2.gcp.zalan.do."}, 259 "zone-2-ext-dns-test-2-gcp-zalan-do": {Name: "zone-2-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-2.ext-dns-test-2.gcp.zalan.do."}, 260 "zone-3-ext-dns-test-2-gcp-zalan-do": {Name: "zone-3-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do."}, 261 }) 262 } 263 264 func TestGoogleRecords(t *testing.T) { 265 originalEndpoints := []*endpoint.Endpoint{ 266 endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(1), "1.2.3.4"), 267 endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(2), "8.8.8.8"), 268 endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(3), "foo.elb.amazonaws.com"), 269 } 270 271 provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints) 272 273 records, err := provider.Records(context.Background()) 274 require.NoError(t, err) 275 276 validateEndpoints(t, records, originalEndpoints) 277 } 278 279 func TestGoogleRecordsFilter(t *testing.T) { 280 originalEndpoints := []*endpoint.Endpoint{ 281 endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 282 endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 283 endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.4.4"), 284 endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.4.4"), 285 endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "bar.elb.amazonaws.com"), 286 endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"), 287 } 288 289 provider := newGoogleProvider( 290 t, 291 endpoint.NewDomainFilter([]string{ 292 // our two valid zones 293 "zone-1.ext-dns-test-2.gcp.zalan.do.", 294 "zone-2.ext-dns-test-2.gcp.zalan.do.", 295 // we filter for a zone that doesn't exist, should have no effect. 296 "zone-0.ext-dns-test-2.gcp.zalan.do.", 297 // there exists a third zone "zone-3" that we want to exclude from being managed. 298 }), 299 provider.NewZoneIDFilter([]string{""}), 300 false, 301 originalEndpoints, 302 ) 303 304 // these records should be filtered out since they don't match a hosted zone or domain filter. 305 ignoredEndpoints := []*endpoint.Endpoint{ 306 endpoint.NewEndpoint("filter-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 307 endpoint.NewEndpoint("filter-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 308 endpoint.NewEndpoint("filter-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 309 endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 310 endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 311 endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 312 } 313 314 require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ 315 Create: ignoredEndpoints, 316 })) 317 318 records, err := provider.Records(context.Background()) 319 require.NoError(t, err) 320 321 // assert that due to filtering no changes were made. 322 validateEndpoints(t, records, originalEndpoints) 323 } 324 325 func TestGoogleApplyChanges(t *testing.T) { 326 provider := newGoogleProvider( 327 t, 328 endpoint.NewDomainFilter([]string{ 329 // our two valid zones 330 "zone-1.ext-dns-test-2.gcp.zalan.do.", 331 "zone-2.ext-dns-test-2.gcp.zalan.do.", 332 // we filter for a zone that doesn't exist, should have no effect. 333 "zone-0.ext-dns-test-2.gcp.zalan.do.", 334 // there exists a third zone "zone-3" that we want to exclude from being managed. 335 }), 336 provider.NewZoneIDFilter([]string{""}), 337 false, 338 []*endpoint.Endpoint{ 339 endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 340 endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 341 endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(10), "8.8.4.4"), 342 endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.4.4"), 343 endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "bar.elb.amazonaws.com"), 344 endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"), 345 }, 346 ) 347 348 createRecords := []*endpoint.Endpoint{ 349 endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 350 endpoint.NewEndpointWithTTL("create-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(15), "8.8.4.4"), 351 endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 352 endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 353 endpoint.NewEndpoint("nomatch-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.1"), 354 } 355 356 currentRecords := []*endpoint.Endpoint{ 357 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 358 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 359 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 360 endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 361 } 362 updatedRecords := []*endpoint.Endpoint{ 363 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 364 endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(25), "4.3.2.1"), 365 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 366 endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "5.6.7.8"), 367 endpoint.NewEndpoint("nomatch-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.7.6.5"), 368 } 369 370 deleteRecords := []*endpoint.Endpoint{ 371 endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 372 endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 373 endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 374 endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"), 375 endpoint.NewEndpoint("nomatch-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.1"), 376 } 377 378 changes := &plan.Changes{ 379 Create: createRecords, 380 UpdateNew: updatedRecords, 381 UpdateOld: currentRecords, 382 Delete: deleteRecords, 383 } 384 385 require.NoError(t, provider.ApplyChanges(context.Background(), changes)) 386 387 records, err := provider.Records(context.Background()) 388 require.NoError(t, err) 389 390 validateEndpoints(t, records, []*endpoint.Endpoint{ 391 endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 392 endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "1.2.3.4"), 393 endpoint.NewEndpointWithTTL("create-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(15), "8.8.4.4"), 394 endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(25), "4.3.2.1"), 395 endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "foo.elb.amazonaws.com"), 396 endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "baz.elb.amazonaws.com"), 397 }) 398 } 399 400 func TestGoogleApplyChangesDryRun(t *testing.T) { 401 originalEndpoints := []*endpoint.Endpoint{ 402 endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 403 endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 404 endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.4.4"), 405 endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.4.4"), 406 endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "bar.elb.amazonaws.com"), 407 endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"), 408 } 409 410 provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), true, originalEndpoints) 411 412 createRecords := []*endpoint.Endpoint{ 413 endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 414 endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 415 endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), 416 } 417 418 currentRecords := []*endpoint.Endpoint{ 419 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 420 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 421 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), 422 } 423 updatedRecords := []*endpoint.Endpoint{ 424 endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 425 endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), 426 endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), 427 } 428 429 deleteRecords := []*endpoint.Endpoint{ 430 endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 431 endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), 432 endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 433 } 434 435 changes := &plan.Changes{ 436 Create: createRecords, 437 UpdateNew: updatedRecords, 438 UpdateOld: currentRecords, 439 Delete: deleteRecords, 440 } 441 442 ctx := context.Background() 443 require.NoError(t, provider.ApplyChanges(ctx, changes)) 444 445 records, err := provider.Records(ctx) 446 require.NoError(t, err) 447 448 validateEndpoints(t, records, originalEndpoints) 449 } 450 451 func TestGoogleApplyChangesEmpty(t *testing.T) { 452 provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) 453 assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{})) 454 } 455 456 func TestNewFilteredRecords(t *testing.T) { 457 provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) 458 459 records := provider.newFilteredRecords([]*endpoint.Endpoint{ 460 endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 1, "8.8.4.4"), 461 endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 120, "8.8.4.4"), 462 endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, 4000, "bar.elb.amazonaws.com"), 463 // test fallback to Ttl:300 when Ttl==0 : 464 endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 0, "8.8.8.8"), 465 endpoint.NewEndpointWithTTL("update-test-mx.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeMX, 6000, "10 mail.elb.amazonaws.com"), 466 endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 467 endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), 468 }) 469 470 validateChangeRecords(t, records, []*dns.ResourceRecordSet{ 471 {Name: "update-test.zone-2.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.4.4"}, Type: "A", Ttl: 1}, 472 {Name: "delete-test.zone-2.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.4.4"}, Type: "A", Ttl: 120}, 473 {Name: "update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"bar.elb.amazonaws.com."}, Type: "CNAME", Ttl: 4000}, 474 {Name: "update-test.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, 475 {Name: "update-test-mx.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"10 mail.elb.amazonaws.com."}, Type: "MX", Ttl: 6000}, 476 {Name: "delete-test.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, 477 {Name: "delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"qux.elb.amazonaws.com."}, Type: "CNAME", Ttl: 300}, 478 }) 479 } 480 481 func TestSeparateChanges(t *testing.T) { 482 change := &dns.Change{ 483 Additions: []*dns.ResourceRecordSet{ 484 {Name: "qux.foo.example.org.", Ttl: 1}, 485 {Name: "qux.bar.example.org.", Ttl: 2}, 486 }, 487 Deletions: []*dns.ResourceRecordSet{ 488 {Name: "wambo.foo.example.org.", Ttl: 10}, 489 {Name: "wambo.bar.example.org.", Ttl: 20}, 490 }, 491 } 492 493 zones := map[string]*dns.ManagedZone{ 494 "foo-example-org": { 495 Name: "foo-example-org", 496 DnsName: "foo.example.org.", 497 }, 498 "bar-example-org": { 499 Name: "bar-example-org", 500 DnsName: "bar.example.org.", 501 }, 502 "baz-example-org": { 503 Name: "baz-example-org", 504 DnsName: "baz.example.org.", 505 }, 506 } 507 508 changes := separateChange(zones, change) 509 require.Len(t, changes, 2) 510 511 validateChange(t, changes["foo-example-org"], &dns.Change{ 512 Additions: []*dns.ResourceRecordSet{ 513 {Name: "qux.foo.example.org.", Ttl: 1}, 514 }, 515 Deletions: []*dns.ResourceRecordSet{ 516 {Name: "wambo.foo.example.org.", Ttl: 10}, 517 }, 518 }) 519 520 validateChange(t, changes["bar-example-org"], &dns.Change{ 521 Additions: []*dns.ResourceRecordSet{ 522 {Name: "qux.bar.example.org.", Ttl: 2}, 523 }, 524 Deletions: []*dns.ResourceRecordSet{ 525 {Name: "wambo.bar.example.org.", Ttl: 20}, 526 }, 527 }) 528 } 529 530 func TestGoogleBatchChangeSet(t *testing.T) { 531 cs := &dns.Change{} 532 533 for i := 1; i <= googleDefaultBatchChangeSize; i += 2 { 534 cs.Additions = append(cs.Additions, &dns.ResourceRecordSet{ 535 Name: fmt.Sprintf("host-%d.example.org.", i), 536 Ttl: 2, 537 }) 538 cs.Deletions = append(cs.Deletions, &dns.ResourceRecordSet{ 539 Name: fmt.Sprintf("host-%d.example.org.", i), 540 Ttl: 20, 541 }) 542 } 543 544 batchCs := batchChange(cs, googleDefaultBatchChangeSize) 545 546 require.Equal(t, 1, len(batchCs)) 547 548 sortChangesByName(cs) 549 validateChange(t, batchCs[0], cs) 550 } 551 552 func TestGoogleBatchChangeSetExceeding(t *testing.T) { 553 cs := &dns.Change{} 554 const testCount = 50 555 const testLimit = 11 556 const expectedBatchCount = 5 557 558 for i := 1; i <= testCount; i += 2 { 559 cs.Additions = append(cs.Additions, &dns.ResourceRecordSet{ 560 Name: fmt.Sprintf("host-%d.example.org.", i), 561 Ttl: 2, 562 }) 563 cs.Deletions = append(cs.Deletions, &dns.ResourceRecordSet{ 564 Name: fmt.Sprintf("host-%d.example.org.", i), 565 Ttl: 20, 566 }) 567 } 568 569 batchCs := batchChange(cs, testLimit) 570 571 require.Equal(t, expectedBatchCount, len(batchCs)) 572 573 dnsChange := &dns.Change{} 574 for _, c := range batchCs { 575 dnsChange.Additions = append(dnsChange.Additions, c.Additions...) 576 dnsChange.Deletions = append(dnsChange.Deletions, c.Deletions...) 577 } 578 579 require.Equal(t, len(cs.Additions), len(dnsChange.Additions)) 580 require.Equal(t, len(cs.Deletions), len(dnsChange.Deletions)) 581 582 sortChangesByName(cs) 583 sortChangesByName(dnsChange) 584 585 validateChange(t, dnsChange, cs) 586 } 587 588 func TestGoogleBatchChangeSetExceedingNameChange(t *testing.T) { 589 cs := &dns.Change{} 590 const testLimit = 1 591 592 cs.Additions = append(cs.Additions, &dns.ResourceRecordSet{ 593 Name: "host-1.example.org.", 594 Ttl: 2, 595 }) 596 cs.Deletions = append(cs.Deletions, &dns.ResourceRecordSet{ 597 Name: "host-1.example.org.", 598 Ttl: 20, 599 }) 600 601 batchCs := batchChange(cs, testLimit) 602 603 require.Equal(t, 0, len(batchCs)) 604 } 605 606 func sortChangesByName(cs *dns.Change) { 607 sort.SliceStable(cs.Additions, func(i, j int) bool { 608 return cs.Additions[i].Name < cs.Additions[j].Name 609 }) 610 611 sort.SliceStable(cs.Deletions, func(i, j int) bool { 612 return cs.Deletions[i].Name < cs.Deletions[j].Name 613 }) 614 } 615 616 func validateZones(t *testing.T, zones map[string]*dns.ManagedZone, expected map[string]*dns.ManagedZone) { 617 require.Len(t, zones, len(expected)) 618 619 for i, zone := range zones { 620 validateZone(t, zone, expected[i]) 621 } 622 } 623 624 func validateZone(t *testing.T, zone *dns.ManagedZone, expected *dns.ManagedZone) { 625 assert.Equal(t, expected.Name, zone.Name) 626 assert.Equal(t, expected.DnsName, zone.DnsName) 627 assert.Equal(t, expected.Visibility, zone.Visibility) 628 } 629 630 func validateChange(t *testing.T, change *dns.Change, expected *dns.Change) { 631 validateChangeRecords(t, change.Additions, expected.Additions) 632 validateChangeRecords(t, change.Deletions, expected.Deletions) 633 } 634 635 func validateChangeRecords(t *testing.T, records []*dns.ResourceRecordSet, expected []*dns.ResourceRecordSet) { 636 require.Len(t, records, len(expected)) 637 638 for i := range records { 639 validateChangeRecord(t, records[i], expected[i]) 640 } 641 } 642 643 func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected *dns.ResourceRecordSet) { 644 assert.Equal(t, expected.Name, record.Name) 645 assert.Equal(t, expected.Rrdatas, record.Rrdatas) 646 assert.Equal(t, expected.Ttl, record.Ttl) 647 assert.Equal(t, expected.Type, record.Type) 648 } 649 650 func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { 651 provider := &GoogleProvider{ 652 project: "zalando-external-dns-test", 653 dryRun: false, 654 domainFilter: domainFilter, 655 zoneIDFilter: zoneIDFilter, 656 zoneTypeFilter: zoneTypeFilter, 657 resourceRecordSetsClient: &mockResourceRecordSetsClient{}, 658 managedZonesClient: &mockManagedZonesClient{}, 659 changesClient: &mockChangesClient{}, 660 } 661 662 createZone(t, provider, &dns.ManagedZone{ 663 Name: "internal-1", 664 DnsName: "cluster.local.", 665 Id: 10001, 666 Visibility: "private", 667 }) 668 669 createZone(t, provider, &dns.ManagedZone{ 670 Name: "internal-2", 671 DnsName: "cluster.local.", 672 Id: 10002, 673 Visibility: "private", 674 }) 675 676 createZone(t, provider, &dns.ManagedZone{ 677 Name: "internal-3", 678 DnsName: "cluster.local.", 679 Id: 10003, 680 Visibility: "private", 681 }) 682 683 createZone(t, provider, &dns.ManagedZone{ 684 Name: "split-horizon-1", 685 DnsName: "cluster.local.", 686 Id: 10004, 687 Visibility: "public", 688 }) 689 690 createZone(t, provider, &dns.ManagedZone{ 691 Name: "split-horizon-1", 692 DnsName: "cluster.local.", 693 Id: 10004, 694 Visibility: "private", 695 }) 696 697 698 createZone(t, provider, &dns.ManagedZone{ 699 Name: "svc-local", 700 DnsName: "svc.local.", 701 Id: 10005, 702 Visibility: "private", 703 }) 704 705 createZone(t, provider, &dns.ManagedZone{ 706 Name: "svc-local-peer", 707 DnsName: "svc.local.", 708 Id: 10006, 709 Visibility: "private", 710 PeeringConfig: &dns.ManagedZonePeeringConfig{TargetNetwork: nil}, 711 }) 712 713 provider.dryRun = dryRun 714 715 return provider 716 } 717 718 func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { 719 provider := &GoogleProvider{ 720 project: "zalando-external-dns-test", 721 dryRun: false, 722 domainFilter: domainFilter, 723 zoneIDFilter: zoneIDFilter, 724 resourceRecordSetsClient: &mockResourceRecordSetsClient{}, 725 managedZonesClient: &mockManagedZonesClient{}, 726 changesClient: &mockChangesClient{}, 727 } 728 729 createZone(t, provider, &dns.ManagedZone{ 730 Name: "zone-1-ext-dns-test-2-gcp-zalan-do", 731 DnsName: "zone-1.ext-dns-test-2.gcp.zalan.do.", 732 }) 733 734 createZone(t, provider, &dns.ManagedZone{ 735 Name: "zone-2-ext-dns-test-2-gcp-zalan-do", 736 DnsName: "zone-2.ext-dns-test-2.gcp.zalan.do.", 737 }) 738 739 createZone(t, provider, &dns.ManagedZone{ 740 Name: "zone-3-ext-dns-test-2-gcp-zalan-do", 741 DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do.", 742 }) 743 744 // filtered out by domain filter 745 createZone(t, provider, &dns.ManagedZone{ 746 Name: "zone-4-ext-dns-test-3-gcp-zalan-do", 747 DnsName: "zone-4.ext-dns-test-3.gcp.zalan.do.", 748 }) 749 750 setupGoogleRecords(t, provider, records) 751 752 provider.dryRun = dryRun 753 754 return provider 755 } 756 757 func createZone(t *testing.T, provider *GoogleProvider, zone *dns.ManagedZone) { 758 zone.Description = "Testing zone for kubernetes.io/external-dns" 759 760 if _, err := provider.managedZonesClient.Create("zalando-external-dns-test", zone).Do(); err != nil { 761 if err, ok := err.(*googleapi.Error); !ok || err.Code != http.StatusConflict { 762 require.NoError(t, err) 763 } 764 } 765 } 766 767 func setupGoogleRecords(t *testing.T, provider *GoogleProvider, endpoints []*endpoint.Endpoint) { 768 clearGoogleRecords(t, provider, "zone-1-ext-dns-test-2-gcp-zalan-do") 769 clearGoogleRecords(t, provider, "zone-2-ext-dns-test-2-gcp-zalan-do") 770 clearGoogleRecords(t, provider, "zone-3-ext-dns-test-2-gcp-zalan-do") 771 772 ctx := context.Background() 773 records, err := provider.Records(ctx) 774 require.NoError(t, err) 775 776 validateEndpoints(t, records, []*endpoint.Endpoint{}) 777 778 require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ 779 Create: endpoints, 780 })) 781 782 records, err = provider.Records(ctx) 783 require.NoError(t, err) 784 785 validateEndpoints(t, records, endpoints) 786 } 787 788 func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) { 789 recordSets := []*dns.ResourceRecordSet{} 790 require.NoError(t, provider.resourceRecordSetsClient.List(provider.project, zone).Pages(context.Background(), func(resp *dns.ResourceRecordSetsListResponse) error { 791 for _, r := range resp.Rrsets { 792 switch r.Type { 793 case endpoint.RecordTypeA, endpoint.RecordTypeCNAME: 794 recordSets = append(recordSets, r) 795 } 796 } 797 return nil 798 })) 799 800 if len(recordSets) != 0 { 801 _, err := provider.changesClient.Create(provider.project, zone, &dns.Change{ 802 Deletions: recordSets, 803 }).Do() 804 require.NoError(t, err) 805 } 806 } 807 808 func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { 809 assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected) 810 }