sigs.k8s.io/external-dns@v0.14.1/provider/azure/azure_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 azure
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
    24  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
    25  	dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
    26  	"github.com/stretchr/testify/assert"
    27  
    28  	"sigs.k8s.io/external-dns/endpoint"
    29  	"sigs.k8s.io/external-dns/internal/testutils"
    30  	"sigs.k8s.io/external-dns/plan"
    31  	"sigs.k8s.io/external-dns/provider"
    32  )
    33  
    34  // mockZonesClient implements the methods of the Azure DNS Zones Client which are used in the Azure Provider
    35  // and returns static results which are defined per test
    36  type mockZonesClient struct {
    37  	pagingHandler azcoreruntime.PagingHandler[dns.ZonesClientListByResourceGroupResponse]
    38  }
    39  
    40  func newMockZonesClient(zones []*dns.Zone) mockZonesClient {
    41  	pagingHandler := azcoreruntime.PagingHandler[dns.ZonesClientListByResourceGroupResponse]{
    42  		More: func(resp dns.ZonesClientListByResourceGroupResponse) bool {
    43  			return false
    44  		},
    45  		Fetcher: func(context.Context, *dns.ZonesClientListByResourceGroupResponse) (dns.ZonesClientListByResourceGroupResponse, error) {
    46  			return dns.ZonesClientListByResourceGroupResponse{
    47  				ZoneListResult: dns.ZoneListResult{
    48  					Value: zones,
    49  				},
    50  			}, nil
    51  		},
    52  	}
    53  	return mockZonesClient{
    54  		pagingHandler: pagingHandler,
    55  	}
    56  }
    57  
    58  func (client *mockZonesClient) NewListByResourceGroupPager(resourceGroupName string, options *dns.ZonesClientListByResourceGroupOptions) *azcoreruntime.Pager[dns.ZonesClientListByResourceGroupResponse] {
    59  	return azcoreruntime.NewPager(client.pagingHandler)
    60  }
    61  
    62  // mockZonesClient implements the methods of the Azure DNS RecordSet Client which are used in the Azure Provider
    63  // and returns static results which are defined per test
    64  type mockRecordSetsClient struct {
    65  	pagingHandler    azcoreruntime.PagingHandler[dns.RecordSetsClientListAllByDNSZoneResponse]
    66  	deletedEndpoints []*endpoint.Endpoint
    67  	updatedEndpoints []*endpoint.Endpoint
    68  }
    69  
    70  func newMockRecordSetsClient(recordSets []*dns.RecordSet) mockRecordSetsClient {
    71  	pagingHandler := azcoreruntime.PagingHandler[dns.RecordSetsClientListAllByDNSZoneResponse]{
    72  		More: func(resp dns.RecordSetsClientListAllByDNSZoneResponse) bool {
    73  			return false
    74  		},
    75  		Fetcher: func(context.Context, *dns.RecordSetsClientListAllByDNSZoneResponse) (dns.RecordSetsClientListAllByDNSZoneResponse, error) {
    76  			return dns.RecordSetsClientListAllByDNSZoneResponse{
    77  				RecordSetListResult: dns.RecordSetListResult{
    78  					Value: recordSets,
    79  				},
    80  			}, nil
    81  		},
    82  	}
    83  	return mockRecordSetsClient{
    84  		pagingHandler: pagingHandler,
    85  	}
    86  }
    87  
    88  func (client *mockRecordSetsClient) NewListAllByDNSZonePager(resourceGroupName string, zoneName string, options *dns.RecordSetsClientListAllByDNSZoneOptions) *azcoreruntime.Pager[dns.RecordSetsClientListAllByDNSZoneResponse] {
    89  	return azcoreruntime.NewPager(client.pagingHandler)
    90  }
    91  
    92  func (client *mockRecordSetsClient) Delete(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType dns.RecordType, options *dns.RecordSetsClientDeleteOptions) (dns.RecordSetsClientDeleteResponse, error) {
    93  	client.deletedEndpoints = append(
    94  		client.deletedEndpoints,
    95  		endpoint.NewEndpoint(
    96  			formatAzureDNSName(relativeRecordSetName, zoneName),
    97  			string(recordType),
    98  			"",
    99  		),
   100  	)
   101  	return dns.RecordSetsClientDeleteResponse{}, nil
   102  }
   103  
   104  func (client *mockRecordSetsClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType dns.RecordType, parameters dns.RecordSet, options *dns.RecordSetsClientCreateOrUpdateOptions) (dns.RecordSetsClientCreateOrUpdateResponse, error) {
   105  	var ttl endpoint.TTL
   106  	if parameters.Properties.TTL != nil {
   107  		ttl = endpoint.TTL(*parameters.Properties.TTL)
   108  	}
   109  	client.updatedEndpoints = append(
   110  		client.updatedEndpoints,
   111  		endpoint.NewEndpointWithTTL(
   112  			formatAzureDNSName(relativeRecordSetName, zoneName),
   113  			string(recordType),
   114  			ttl,
   115  			extractAzureTargets(&parameters)...,
   116  		),
   117  	)
   118  	return dns.RecordSetsClientCreateOrUpdateResponse{}, nil
   119  }
   120  
   121  func createMockZone(zone string, id string) *dns.Zone {
   122  	return &dns.Zone{
   123  		ID:   to.Ptr(id),
   124  		Name: to.Ptr(zone),
   125  	}
   126  }
   127  
   128  func aRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   129  	aRecords := make([]*dns.ARecord, len(values))
   130  	for i, value := range values {
   131  		aRecords[i] = &dns.ARecord{
   132  			IPv4Address: to.Ptr(value),
   133  		}
   134  	}
   135  	return &dns.RecordSetProperties{
   136  		TTL:      to.Ptr(ttl),
   137  		ARecords: aRecords,
   138  	}
   139  }
   140  
   141  func aaaaRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   142  	aaaaRecords := make([]*dns.AaaaRecord, len(values))
   143  	for i, value := range values {
   144  		aaaaRecords[i] = &dns.AaaaRecord{
   145  			IPv6Address: to.Ptr(value),
   146  		}
   147  	}
   148  	return &dns.RecordSetProperties{
   149  		TTL:         to.Ptr(ttl),
   150  		AaaaRecords: aaaaRecords,
   151  	}
   152  }
   153  
   154  func cNameRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   155  	return &dns.RecordSetProperties{
   156  		TTL: to.Ptr(ttl),
   157  		CnameRecord: &dns.CnameRecord{
   158  			Cname: to.Ptr(values[0]),
   159  		},
   160  	}
   161  }
   162  
   163  func mxRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   164  	mxRecords := make([]*dns.MxRecord, len(values))
   165  	for i, target := range values {
   166  		mxRecord, _ := parseMxTarget[dns.MxRecord](target)
   167  		mxRecords[i] = &mxRecord
   168  	}
   169  	return &dns.RecordSetProperties{
   170  		TTL:       to.Ptr(ttl),
   171  		MxRecords: mxRecords,
   172  	}
   173  }
   174  
   175  func txtRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   176  	return &dns.RecordSetProperties{
   177  		TTL: to.Ptr(ttl),
   178  		TxtRecords: []*dns.TxtRecord{
   179  			{
   180  				Value: []*string{to.Ptr(values[0])},
   181  			},
   182  		},
   183  	}
   184  }
   185  
   186  func othersRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
   187  	return &dns.RecordSetProperties{
   188  		TTL: to.Ptr(ttl),
   189  	}
   190  }
   191  
   192  func createMockRecordSet(name, recordType string, values ...string) *dns.RecordSet {
   193  	return createMockRecordSetMultiWithTTL(name, recordType, 0, values...)
   194  }
   195  
   196  func createMockRecordSetWithTTL(name, recordType, value string, ttl int64) *dns.RecordSet {
   197  	return createMockRecordSetMultiWithTTL(name, recordType, ttl, value)
   198  }
   199  
   200  func createMockRecordSetMultiWithTTL(name, recordType string, ttl int64, values ...string) *dns.RecordSet {
   201  	var getterFunc func(values []string, ttl int64) *dns.RecordSetProperties
   202  
   203  	switch recordType {
   204  	case endpoint.RecordTypeA:
   205  		getterFunc = aRecordSetPropertiesGetter
   206  	case endpoint.RecordTypeAAAA:
   207  		getterFunc = aaaaRecordSetPropertiesGetter
   208  	case endpoint.RecordTypeCNAME:
   209  		getterFunc = cNameRecordSetPropertiesGetter
   210  	case endpoint.RecordTypeMX:
   211  		getterFunc = mxRecordSetPropertiesGetter
   212  	case endpoint.RecordTypeTXT:
   213  		getterFunc = txtRecordSetPropertiesGetter
   214  	default:
   215  		getterFunc = othersRecordSetPropertiesGetter
   216  	}
   217  	return &dns.RecordSet{
   218  		Name:       to.Ptr(name),
   219  		Type:       to.Ptr("Microsoft.Network/dnszones/" + recordType),
   220  		Properties: getterFunc(values, ttl),
   221  	}
   222  }
   223  
   224  // newMockedAzureProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
   225  func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones []*dns.Zone, recordSets []*dns.RecordSet) (*AzureProvider, error) {
   226  	zonesClient := newMockZonesClient(zones)
   227  	recordSetsClient := newMockRecordSetsClient(recordSets)
   228  	return newAzureProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil
   229  }
   230  
   231  func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
   232  	return &AzureProvider{
   233  		domainFilter:                 domainFilter,
   234  		zoneNameFilter:               zoneNameFilter,
   235  		zoneIDFilter:                 zoneIDFilter,
   236  		dryRun:                       dryRun,
   237  		resourceGroup:                resourceGroup,
   238  		userAssignedIdentityClientID: userAssignedIdentityClientID,
   239  		zonesClient:                  zonesClient,
   240  		recordSetsClient:             recordsClient,
   241  	}
   242  }
   243  
   244  func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
   245  	assert.True(t, testutils.SameEndpoints(endpoints, expected), "expected and actual endpoints don't match. %s:%s", endpoints, expected)
   246  }
   247  
   248  func TestAzureRecord(t *testing.T) {
   249  	provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
   250  		[]*dns.Zone{
   251  			createMockZone("example.com", "/dnszones/example.com"),
   252  		},
   253  		[]*dns.RecordSet{
   254  			createMockRecordSet("@", "NS", "ns1-03.azure-dns.com."),
   255  			createMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"),
   256  			createMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122"),
   257  			createMockRecordSet("@", endpoint.RecordTypeAAAA, "2001::123:123:123:122"),
   258  			createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
   259  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600),
   260  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeAAAA, "2001::123:123:123:123", 3600),
   261  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
   262  			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
   263  			createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com"),
   264  		})
   265  	if err != nil {
   266  		t.Fatal(err)
   267  	}
   268  
   269  	ctx := context.Background()
   270  	actual, err := provider.Records(ctx)
   271  	if err != nil {
   272  		t.Fatal(err)
   273  	}
   274  	expected := []*endpoint.Endpoint{
   275  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122"),
   276  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::123:123:123:122"),
   277  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
   278  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
   279  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123"),
   280  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
   281  		endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"),
   282  		endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com"),
   283  	}
   284  
   285  	validateAzureEndpoints(t, actual, expected)
   286  }
   287  
   288  func TestAzureMultiRecord(t *testing.T) {
   289  	provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
   290  		[]*dns.Zone{
   291  			createMockZone("example.com", "/dnszones/example.com"),
   292  		},
   293  		[]*dns.RecordSet{
   294  			createMockRecordSet("@", "NS", "ns1-03.azure-dns.com."),
   295  			createMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"),
   296  			createMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"),
   297  			createMockRecordSet("@", endpoint.RecordTypeAAAA, "2001::123:123:123:122", "2001::234:234:234:233"),
   298  			createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
   299  			createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"),
   300  			createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"),
   301  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
   302  			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
   303  			createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"),
   304  		})
   305  	if err != nil {
   306  		t.Fatal(err)
   307  	}
   308  
   309  	ctx := context.Background()
   310  	actual, err := provider.Records(ctx)
   311  	if err != nil {
   312  		t.Fatal(err)
   313  	}
   314  	expected := []*endpoint.Endpoint{
   315  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"),
   316  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::123:123:123:122", "2001::234:234:234:233"),
   317  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
   318  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"),
   319  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"),
   320  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
   321  		endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"),
   322  		endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"),
   323  	}
   324  
   325  	validateAzureEndpoints(t, actual, expected)
   326  }
   327  
   328  func TestAzureApplyChanges(t *testing.T) {
   329  	recordsClient := mockRecordSetsClient{}
   330  
   331  	testAzureApplyChangesInternal(t, false, &recordsClient)
   332  
   333  	validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{
   334  		endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
   335  		endpoint.NewEndpoint("deletedaaaa.example.com", endpoint.RecordTypeAAAA, ""),
   336  		endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
   337  	})
   338  
   339  	validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{
   340  		endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
   341  		endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4"),
   342  		endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   343  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"),
   344  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"),
   345  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   346  		endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "other.com"),
   347  		endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   348  		endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "5.6.7.8"),
   349  		endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::5:6:7:8"),
   350  		endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   351  		endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
   352  		endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"),
   353  		endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
   354  		endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"),
   355  		endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"),
   356  		endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   357  	})
   358  }
   359  
   360  func TestAzureApplyChangesDryRun(t *testing.T) {
   361  	recordsClient := mockRecordSetsClient{}
   362  
   363  	testAzureApplyChangesInternal(t, true, &recordsClient)
   364  
   365  	validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{})
   366  
   367  	validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{})
   368  }
   369  
   370  func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsClient) {
   371  	zones := []*dns.Zone{
   372  		createMockZone("example.com", "/dnszones/example.com"),
   373  		createMockZone("other.com", "/dnszones/other.com"),
   374  	}
   375  	zonesClient := newMockZonesClient(zones)
   376  
   377  	provider := newAzureProvider(
   378  		endpoint.NewDomainFilter([]string{""}),
   379  		endpoint.NewDomainFilter([]string{""}),
   380  		provider.NewZoneIDFilter([]string{""}),
   381  		dryRun,
   382  		"group",
   383  		"",
   384  		&zonesClient,
   385  		client,
   386  	)
   387  
   388  	createRecords := []*endpoint.Endpoint{
   389  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
   390  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:4"),
   391  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
   392  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.5", "1.2.3.4"),
   393  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:5", "2001::1:2:3:4"),
   394  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
   395  		endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
   396  		endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
   397  		endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
   398  		endpoint.NewEndpoint("other.com", endpoint.RecordTypeAAAA, "2001::5:6:7:8"),
   399  		endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
   400  		endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
   401  		endpoint.NewEndpoint("nope.com", endpoint.RecordTypeAAAA, "2001::4:4:4:4"),
   402  		endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
   403  		endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeMX, "10 other.com"),
   404  		endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeTXT, "tag"),
   405  	}
   406  
   407  	currentRecords := []*endpoint.Endpoint{
   408  		endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
   409  		endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
   410  		endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"),
   411  		endpoint.NewEndpoint("oldmail.example.com", endpoint.RecordTypeMX, "20 foo.other.com"),
   412  	}
   413  	updatedRecords := []*endpoint.Endpoint{
   414  		endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
   415  		endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"),
   416  		endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
   417  		endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
   418  		endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"),
   419  		endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"),
   420  	}
   421  
   422  	deleteRecords := []*endpoint.Endpoint{
   423  		endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "111.222.111.222"),
   424  		endpoint.NewEndpoint("deletedaaaa.example.com", endpoint.RecordTypeAAAA, "2001::111:222:111:222"),
   425  		endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
   426  		endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
   427  		endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"),
   428  	}
   429  
   430  	changes := &plan.Changes{
   431  		Create:    createRecords,
   432  		UpdateNew: updatedRecords,
   433  		UpdateOld: currentRecords,
   434  		Delete:    deleteRecords,
   435  	}
   436  
   437  	if err := provider.ApplyChanges(context.Background(), changes); err != nil {
   438  		t.Fatal(err)
   439  	}
   440  }
   441  
   442  func TestAzureNameFilter(t *testing.T) {
   443  	provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"nginx.example.com"}), endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
   444  		[]*dns.Zone{
   445  			createMockZone("example.com", "/dnszones/example.com"),
   446  		},
   447  
   448  		[]*dns.RecordSet{
   449  			createMockRecordSet("@", "NS", "ns1-03.azure-dns.com."),
   450  			createMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"),
   451  			createMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122"),
   452  			createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
   453  			createMockRecordSetWithTTL("test.nginx", endpoint.RecordTypeA, "123.123.123.123", 3600),
   454  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600),
   455  			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
   456  			createMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL),
   457  			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
   458  		})
   459  	if err != nil {
   460  		t.Fatal(err)
   461  	}
   462  
   463  	ctx := context.Background()
   464  	actual, err := provider.Records(ctx)
   465  	if err != nil {
   466  		t.Fatal(err)
   467  	}
   468  	expected := []*endpoint.Endpoint{
   469  		endpoint.NewEndpointWithTTL("test.nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
   470  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
   471  		endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
   472  		endpoint.NewEndpointWithTTL("mail.nginx.example.com", endpoint.RecordTypeMX, recordTTL, "20 example.com"),
   473  	}
   474  
   475  	validateAzureEndpoints(t, actual, expected)
   476  }
   477  
   478  func TestAzureApplyChangesZoneName(t *testing.T) {
   479  	recordsClient := mockRecordSetsClient{}
   480  
   481  	testAzureApplyChangesInternalZoneName(t, false, &recordsClient)
   482  
   483  	validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{
   484  		endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, ""),
   485  		endpoint.NewEndpoint("deletedaaaa.foo.example.com", endpoint.RecordTypeAAAA, ""),
   486  		endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, ""),
   487  	})
   488  
   489  	validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{
   490  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"),
   491  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"),
   492  		endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
   493  		endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
   494  		endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"),
   495  		endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
   496  	})
   497  }
   498  
   499  func testAzureApplyChangesInternalZoneName(t *testing.T, dryRun bool, client RecordSetsClient) {
   500  	zonesClient := newMockZonesClient([]*dns.Zone{createMockZone("example.com", "/dnszones/example.com")})
   501  
   502  	provider := newAzureProvider(
   503  		endpoint.NewDomainFilter([]string{"foo.example.com"}),
   504  		endpoint.NewDomainFilter([]string{"example.com"}),
   505  		provider.NewZoneIDFilter([]string{""}),
   506  		dryRun,
   507  		"group",
   508  		"",
   509  		&zonesClient,
   510  		client,
   511  	)
   512  
   513  	createRecords := []*endpoint.Endpoint{
   514  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
   515  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:4"),
   516  		endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
   517  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.5", "1.2.3.4"),
   518  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:5", "2001::1:2:3:4"),
   519  		endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
   520  		endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
   521  		endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
   522  		endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
   523  		endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
   524  		endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
   525  		endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
   526  	}
   527  
   528  	currentRecords := []*endpoint.Endpoint{
   529  		endpoint.NewEndpoint("old.foo.example.com", endpoint.RecordTypeA, "121.212.121.212"),
   530  		endpoint.NewEndpoint("oldcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"),
   531  		endpoint.NewEndpoint("old.nope.example.com", endpoint.RecordTypeA, "121.212.121.212"),
   532  	}
   533  	updatedRecords := []*endpoint.Endpoint{
   534  		endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
   535  		endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"),
   536  		endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
   537  		endpoint.NewEndpoint("new.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"),
   538  		endpoint.NewEndpoint("new.nope.example.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"),
   539  	}
   540  
   541  	deleteRecords := []*endpoint.Endpoint{
   542  		endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, "111.222.111.222"),
   543  		endpoint.NewEndpoint("deletedaaaa.foo.example.com", endpoint.RecordTypeAAAA, "2001::111:222:111:222"),
   544  		endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"),
   545  		endpoint.NewEndpoint("deleted.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"),
   546  	}
   547  
   548  	changes := &plan.Changes{
   549  		Create:    createRecords,
   550  		UpdateNew: updatedRecords,
   551  		UpdateOld: currentRecords,
   552  		Delete:    deleteRecords,
   553  	}
   554  
   555  	if err := provider.ApplyChanges(context.Background(), changes); err != nil {
   556  		t.Fatal(err)
   557  	}
   558  }