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  }