sigs.k8s.io/external-dns@v0.14.1/registry/dynamodb_test.go (about)

     1  /*
     2  Copyright 2023 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 registry
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/aws/aws-sdk-go/aws"
    26  	"github.com/aws/aws-sdk-go/aws/request"
    27  	"github.com/aws/aws-sdk-go/service/dynamodb"
    28  	"github.com/stretchr/testify/assert"
    29  	"github.com/stretchr/testify/require"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"sigs.k8s.io/external-dns/endpoint"
    32  	"sigs.k8s.io/external-dns/internal/testutils"
    33  	"sigs.k8s.io/external-dns/plan"
    34  	"sigs.k8s.io/external-dns/provider"
    35  	"sigs.k8s.io/external-dns/provider/inmemory"
    36  )
    37  
    38  func TestDynamoDBRegistryNew(t *testing.T) {
    39  	api, p := newDynamoDBAPIStub(t, nil)
    40  
    41  	_, err := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, []byte(""), time.Hour)
    42  	require.NoError(t, err)
    43  
    44  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "testPrefix", "", "", []string{}, []string{}, []byte(""), time.Hour)
    45  	require.NoError(t, err)
    46  
    47  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "testSuffix", "", []string{}, []string{}, []byte(""), time.Hour)
    48  	require.NoError(t, err)
    49  
    50  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "testWildcard", []string{}, []string{}, []byte(""), time.Hour)
    51  	require.NoError(t, err)
    52  
    53  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "testWildcard", []string{}, []string{}, []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^"), time.Hour)
    54  	require.NoError(t, err)
    55  
    56  	_, err = NewDynamoDBRegistry(p, "", api, "test-table", "", "", "", []string{}, []string{}, []byte(""), time.Hour)
    57  	require.EqualError(t, err, "owner id cannot be empty")
    58  
    59  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "", "", "", "", []string{}, []string{}, []byte(""), time.Hour)
    60  	require.EqualError(t, err, "table cannot be empty")
    61  
    62  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^x"), time.Hour)
    63  	require.EqualError(t, err, "the AES Encryption key must have a length of 32 bytes")
    64  
    65  	_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "testPrefix", "testSuffix", "", []string{}, []string{}, []byte(""), time.Hour)
    66  	require.EqualError(t, err, "txt-prefix and txt-suffix are mutually exclusive")
    67  }
    68  
    69  func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
    70  	for _, tc := range []struct {
    71  		name     string
    72  		setup    func(desc *dynamodb.TableDescription)
    73  		expected string
    74  	}{
    75  		{
    76  			name: "missing attribute k",
    77  			setup: func(desc *dynamodb.TableDescription) {
    78  				desc.AttributeDefinitions[0].AttributeName = aws.String("wrong")
    79  			},
    80  			expected: "table \"test-table\" must have attribute \"k\" of type \"S\"",
    81  		},
    82  		{
    83  			name: "wrong attribute type",
    84  			setup: func(desc *dynamodb.TableDescription) {
    85  				desc.AttributeDefinitions[0].AttributeType = aws.String("SS")
    86  			},
    87  			expected: "table \"test-table\" attribute \"k\" must have type \"S\"",
    88  		},
    89  		{
    90  			name: "wrong key",
    91  			setup: func(desc *dynamodb.TableDescription) {
    92  				desc.KeySchema[0].AttributeName = aws.String("wrong")
    93  			},
    94  			expected: "table \"test-table\" must have hash key \"k\"",
    95  		},
    96  		{
    97  			name: "has range key",
    98  			setup: func(desc *dynamodb.TableDescription) {
    99  				desc.AttributeDefinitions = append(desc.AttributeDefinitions, &dynamodb.AttributeDefinition{
   100  					AttributeName: aws.String("o"),
   101  					AttributeType: aws.String("S"),
   102  				})
   103  				desc.KeySchema = append(desc.KeySchema, &dynamodb.KeySchemaElement{
   104  					AttributeName: aws.String("o"),
   105  					KeyType:       aws.String("RANGE"),
   106  				})
   107  			},
   108  			expected: "table \"test-table\" must not have a range key",
   109  		},
   110  	} {
   111  		t.Run(tc.name, func(t *testing.T) {
   112  			api, p := newDynamoDBAPIStub(t, nil)
   113  			tc.setup(&api.tableDescription)
   114  
   115  			r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, nil, time.Hour)
   116  
   117  			_, err := r.Records(context.Background())
   118  			assert.EqualError(t, err, tc.expected)
   119  		})
   120  	}
   121  }
   122  
   123  func TestDynamoDBRegistryRecords(t *testing.T) {
   124  	api, p := newDynamoDBAPIStub(t, nil)
   125  
   126  	ctx := context.Background()
   127  	expectedRecords := []*endpoint.Endpoint{
   128  		{
   129  			DNSName:    "foo.test-zone.example.org",
   130  			Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   131  			RecordType: endpoint.RecordTypeCNAME,
   132  			Labels: map[string]string{
   133  				endpoint.OwnerLabelKey: "",
   134  			},
   135  		},
   136  		{
   137  			DNSName:    "bar.test-zone.example.org",
   138  			Targets:    endpoint.Targets{"my-domain.com"},
   139  			RecordType: endpoint.RecordTypeCNAME,
   140  			Labels: map[string]string{
   141  				endpoint.OwnerLabelKey:    "test-owner",
   142  				endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   143  			},
   144  		},
   145  		{
   146  			DNSName:       "baz.test-zone.example.org",
   147  			Targets:       endpoint.Targets{"1.1.1.1"},
   148  			RecordType:    endpoint.RecordTypeA,
   149  			SetIdentifier: "set-1",
   150  			Labels: map[string]string{
   151  				endpoint.OwnerLabelKey:    "test-owner",
   152  				endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   153  			},
   154  		},
   155  		{
   156  			DNSName:       "baz.test-zone.example.org",
   157  			Targets:       endpoint.Targets{"2.2.2.2"},
   158  			RecordType:    endpoint.RecordTypeA,
   159  			SetIdentifier: "set-2",
   160  			Labels: map[string]string{
   161  				endpoint.OwnerLabelKey:    "test-owner",
   162  				endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   163  			},
   164  		},
   165  		{
   166  			DNSName:       "migrate.test-zone.example.org",
   167  			Targets:       endpoint.Targets{"3.3.3.3"},
   168  			RecordType:    endpoint.RecordTypeA,
   169  			SetIdentifier: "set-3",
   170  			Labels: map[string]string{
   171  				endpoint.OwnerLabelKey:    "test-owner",
   172  				endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   173  			},
   174  			ProviderSpecific: endpoint.ProviderSpecific{
   175  				{
   176  					Name:  dynamodbAttributeMigrate,
   177  					Value: "true",
   178  				},
   179  			},
   180  		},
   181  		{
   182  			DNSName:       "txt.orphaned.test-zone.example.org",
   183  			Targets:       endpoint.Targets{"\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/other-ingress\""},
   184  			RecordType:    endpoint.RecordTypeTXT,
   185  			SetIdentifier: "set-3",
   186  			Labels: map[string]string{
   187  				endpoint.OwnerLabelKey: "test-owner",
   188  			},
   189  		},
   190  		{
   191  			DNSName:       "txt.baz.test-zone.example.org",
   192  			Targets:       endpoint.Targets{"\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/other-ingress\""},
   193  			RecordType:    endpoint.RecordTypeTXT,
   194  			SetIdentifier: "set-2",
   195  			Labels: map[string]string{
   196  				endpoint.OwnerLabelKey: "test-owner",
   197  			},
   198  		},
   199  	}
   200  
   201  	r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "txt.", "", "", []string{}, []string{}, nil, time.Hour)
   202  	_ = p.(*wrappedProvider).Provider.ApplyChanges(context.Background(), &plan.Changes{
   203  		Create: []*endpoint.Endpoint{
   204  			endpoint.NewEndpoint("migrate.test-zone.example.org", endpoint.RecordTypeA, "3.3.3.3").WithSetIdentifier("set-3"),
   205  			endpoint.NewEndpoint("txt.migrate.test-zone.example.org", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/other-ingress\"").WithSetIdentifier("set-3"),
   206  			endpoint.NewEndpoint("txt.orphaned.test-zone.example.org", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/other-ingress\"").WithSetIdentifier("set-3"),
   207  			endpoint.NewEndpoint("txt.baz.test-zone.example.org", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/other-ingress\"").WithSetIdentifier("set-2"),
   208  		},
   209  	})
   210  
   211  	records, err := r.Records(ctx)
   212  	require.Nil(t, err)
   213  
   214  	assert.True(t, testutils.SameEndpoints(records, expectedRecords))
   215  }
   216  
   217  func TestDynamoDBRegistryApplyChanges(t *testing.T) {
   218  	for _, tc := range []struct {
   219  		name            string
   220  		maxBatchSize    uint8
   221  		stubConfig      DynamoDBStubConfig
   222  		addRecords      []*endpoint.Endpoint
   223  		changes         plan.Changes
   224  		expectedError   string
   225  		expectedRecords []*endpoint.Endpoint
   226  	}{
   227  		{
   228  			name: "create",
   229  			changes: plan.Changes{
   230  				Create: []*endpoint.Endpoint{
   231  					{
   232  						DNSName:       "new.test-zone.example.org",
   233  						Targets:       endpoint.Targets{"new.loadbalancer.com"},
   234  						RecordType:    endpoint.RecordTypeCNAME,
   235  						SetIdentifier: "set-new",
   236  						Labels: map[string]string{
   237  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   238  						},
   239  					},
   240  				},
   241  			},
   242  			stubConfig: DynamoDBStubConfig{
   243  				ExpectInsert: map[string]map[string]string{
   244  					"new.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
   245  				},
   246  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   247  			},
   248  			expectedRecords: []*endpoint.Endpoint{
   249  				{
   250  					DNSName:    "foo.test-zone.example.org",
   251  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   252  					RecordType: endpoint.RecordTypeCNAME,
   253  					Labels: map[string]string{
   254  						endpoint.OwnerLabelKey: "",
   255  					},
   256  				},
   257  				{
   258  					DNSName:    "bar.test-zone.example.org",
   259  					Targets:    endpoint.Targets{"my-domain.com"},
   260  					RecordType: endpoint.RecordTypeCNAME,
   261  					Labels: map[string]string{
   262  						endpoint.OwnerLabelKey:    "test-owner",
   263  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   264  					},
   265  				},
   266  				{
   267  					DNSName:       "baz.test-zone.example.org",
   268  					Targets:       endpoint.Targets{"1.1.1.1"},
   269  					RecordType:    endpoint.RecordTypeA,
   270  					SetIdentifier: "set-1",
   271  					Labels: map[string]string{
   272  						endpoint.OwnerLabelKey:    "test-owner",
   273  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   274  					},
   275  				},
   276  				{
   277  					DNSName:       "baz.test-zone.example.org",
   278  					Targets:       endpoint.Targets{"2.2.2.2"},
   279  					RecordType:    endpoint.RecordTypeA,
   280  					SetIdentifier: "set-2",
   281  					Labels: map[string]string{
   282  						endpoint.OwnerLabelKey:    "test-owner",
   283  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   284  					},
   285  				},
   286  				{
   287  					DNSName:       "new.test-zone.example.org",
   288  					Targets:       endpoint.Targets{"new.loadbalancer.com"},
   289  					RecordType:    endpoint.RecordTypeCNAME,
   290  					SetIdentifier: "set-new",
   291  					Labels: map[string]string{
   292  						endpoint.OwnerLabelKey:    "test-owner",
   293  						endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   294  					},
   295  				},
   296  			},
   297  		},
   298  		{
   299  			name:         "create more entries than DynamoDB batch size limit",
   300  			maxBatchSize: 2,
   301  			changes: plan.Changes{
   302  				Create: []*endpoint.Endpoint{
   303  					{
   304  						DNSName:       "new1.test-zone.example.org",
   305  						Targets:       endpoint.Targets{"new1.loadbalancer.com"},
   306  						RecordType:    endpoint.RecordTypeCNAME,
   307  						SetIdentifier: "set-new",
   308  						Labels: map[string]string{
   309  							endpoint.ResourceLabelKey: "ingress/default/new1-ingress",
   310  						},
   311  					},
   312  					{
   313  						DNSName:       "new2.test-zone.example.org",
   314  						Targets:       endpoint.Targets{"new2.loadbalancer.com"},
   315  						RecordType:    endpoint.RecordTypeCNAME,
   316  						SetIdentifier: "set-new",
   317  						Labels: map[string]string{
   318  							endpoint.ResourceLabelKey: "ingress/default/new2-ingress",
   319  						},
   320  					},
   321  					{
   322  						DNSName:       "new3.test-zone.example.org",
   323  						Targets:       endpoint.Targets{"new3.loadbalancer.com"},
   324  						RecordType:    endpoint.RecordTypeCNAME,
   325  						SetIdentifier: "set-new",
   326  						Labels: map[string]string{
   327  							endpoint.ResourceLabelKey: "ingress/default/new3-ingress",
   328  						},
   329  					},
   330  				},
   331  			},
   332  			stubConfig: DynamoDBStubConfig{
   333  				ExpectInsert: map[string]map[string]string{
   334  					"new1.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new1-ingress"},
   335  					"new2.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new2-ingress"},
   336  					"new3.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new3-ingress"},
   337  				},
   338  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   339  			},
   340  			expectedRecords: []*endpoint.Endpoint{
   341  				{
   342  					DNSName:    "foo.test-zone.example.org",
   343  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   344  					RecordType: endpoint.RecordTypeCNAME,
   345  					Labels: map[string]string{
   346  						endpoint.OwnerLabelKey: "",
   347  					},
   348  				},
   349  				{
   350  					DNSName:    "bar.test-zone.example.org",
   351  					Targets:    endpoint.Targets{"my-domain.com"},
   352  					RecordType: endpoint.RecordTypeCNAME,
   353  					Labels: map[string]string{
   354  						endpoint.OwnerLabelKey:    "test-owner",
   355  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   356  					},
   357  				},
   358  				{
   359  					DNSName:       "baz.test-zone.example.org",
   360  					Targets:       endpoint.Targets{"1.1.1.1"},
   361  					RecordType:    endpoint.RecordTypeA,
   362  					SetIdentifier: "set-1",
   363  					Labels: map[string]string{
   364  						endpoint.OwnerLabelKey:    "test-owner",
   365  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   366  					},
   367  				},
   368  				{
   369  					DNSName:       "baz.test-zone.example.org",
   370  					Targets:       endpoint.Targets{"2.2.2.2"},
   371  					RecordType:    endpoint.RecordTypeA,
   372  					SetIdentifier: "set-2",
   373  					Labels: map[string]string{
   374  						endpoint.OwnerLabelKey:    "test-owner",
   375  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   376  					},
   377  				},
   378  				{
   379  					DNSName:       "new1.test-zone.example.org",
   380  					Targets:       endpoint.Targets{"new1.loadbalancer.com"},
   381  					RecordType:    endpoint.RecordTypeCNAME,
   382  					SetIdentifier: "set-new",
   383  					Labels: map[string]string{
   384  						endpoint.OwnerLabelKey:    "test-owner",
   385  						endpoint.ResourceLabelKey: "ingress/default/new1-ingress",
   386  					},
   387  				},
   388  				{
   389  					DNSName:       "new2.test-zone.example.org",
   390  					Targets:       endpoint.Targets{"new2.loadbalancer.com"},
   391  					RecordType:    endpoint.RecordTypeCNAME,
   392  					SetIdentifier: "set-new",
   393  					Labels: map[string]string{
   394  						endpoint.OwnerLabelKey:    "test-owner",
   395  						endpoint.ResourceLabelKey: "ingress/default/new2-ingress",
   396  					},
   397  				},
   398  				{
   399  					DNSName:       "new3.test-zone.example.org",
   400  					Targets:       endpoint.Targets{"new3.loadbalancer.com"},
   401  					RecordType:    endpoint.RecordTypeCNAME,
   402  					SetIdentifier: "set-new",
   403  					Labels: map[string]string{
   404  						endpoint.OwnerLabelKey:    "test-owner",
   405  						endpoint.ResourceLabelKey: "ingress/default/new3-ingress",
   406  					},
   407  				},
   408  			},
   409  		},
   410  		{
   411  			name: "create orphaned",
   412  			changes: plan.Changes{
   413  				Create: []*endpoint.Endpoint{
   414  					{
   415  						DNSName:       "quux.test-zone.example.org",
   416  						Targets:       endpoint.Targets{"5.5.5.5"},
   417  						RecordType:    endpoint.RecordTypeA,
   418  						SetIdentifier: "set-2",
   419  						Labels: map[string]string{
   420  							endpoint.ResourceLabelKey: "ingress/default/quux-ingress",
   421  						},
   422  					},
   423  				},
   424  			},
   425  			stubConfig: DynamoDBStubConfig{},
   426  			expectedRecords: []*endpoint.Endpoint{
   427  				{
   428  					DNSName:    "foo.test-zone.example.org",
   429  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   430  					RecordType: endpoint.RecordTypeCNAME,
   431  					Labels: map[string]string{
   432  						endpoint.OwnerLabelKey: "",
   433  					},
   434  				},
   435  				{
   436  					DNSName:    "bar.test-zone.example.org",
   437  					Targets:    endpoint.Targets{"my-domain.com"},
   438  					RecordType: endpoint.RecordTypeCNAME,
   439  					Labels: map[string]string{
   440  						endpoint.OwnerLabelKey:    "test-owner",
   441  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   442  					},
   443  				},
   444  				{
   445  					DNSName:       "baz.test-zone.example.org",
   446  					Targets:       endpoint.Targets{"1.1.1.1"},
   447  					RecordType:    endpoint.RecordTypeA,
   448  					SetIdentifier: "set-1",
   449  					Labels: map[string]string{
   450  						endpoint.OwnerLabelKey:    "test-owner",
   451  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   452  					},
   453  				},
   454  				{
   455  					DNSName:       "baz.test-zone.example.org",
   456  					Targets:       endpoint.Targets{"2.2.2.2"},
   457  					RecordType:    endpoint.RecordTypeA,
   458  					SetIdentifier: "set-2",
   459  					Labels: map[string]string{
   460  						endpoint.OwnerLabelKey:    "test-owner",
   461  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   462  					},
   463  				},
   464  				{
   465  					DNSName:       "quux.test-zone.example.org",
   466  					Targets:       endpoint.Targets{"5.5.5.5"},
   467  					RecordType:    endpoint.RecordTypeA,
   468  					SetIdentifier: "set-2",
   469  					Labels: map[string]string{
   470  						endpoint.OwnerLabelKey:    "test-owner",
   471  						endpoint.ResourceLabelKey: "ingress/default/quux-ingress",
   472  					},
   473  				},
   474  			},
   475  		},
   476  		{
   477  			name: "create orphaned change",
   478  			changes: plan.Changes{
   479  				Create: []*endpoint.Endpoint{
   480  					{
   481  						DNSName:       "quux.test-zone.example.org",
   482  						Targets:       endpoint.Targets{"5.5.5.5"},
   483  						RecordType:    endpoint.RecordTypeA,
   484  						SetIdentifier: "set-2",
   485  						Labels: map[string]string{
   486  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   487  						},
   488  					},
   489  				},
   490  			},
   491  			stubConfig: DynamoDBStubConfig{
   492  				ExpectUpdate: map[string]map[string]string{
   493  					"quux.test-zone.example.org#A#set-2": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
   494  				},
   495  			},
   496  			expectedRecords: []*endpoint.Endpoint{
   497  				{
   498  					DNSName:    "foo.test-zone.example.org",
   499  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   500  					RecordType: endpoint.RecordTypeCNAME,
   501  					Labels: map[string]string{
   502  						endpoint.OwnerLabelKey: "",
   503  					},
   504  				},
   505  				{
   506  					DNSName:    "bar.test-zone.example.org",
   507  					Targets:    endpoint.Targets{"my-domain.com"},
   508  					RecordType: endpoint.RecordTypeCNAME,
   509  					Labels: map[string]string{
   510  						endpoint.OwnerLabelKey:    "test-owner",
   511  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   512  					},
   513  				},
   514  				{
   515  					DNSName:       "baz.test-zone.example.org",
   516  					Targets:       endpoint.Targets{"1.1.1.1"},
   517  					RecordType:    endpoint.RecordTypeA,
   518  					SetIdentifier: "set-1",
   519  					Labels: map[string]string{
   520  						endpoint.OwnerLabelKey:    "test-owner",
   521  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   522  					},
   523  				},
   524  				{
   525  					DNSName:       "baz.test-zone.example.org",
   526  					Targets:       endpoint.Targets{"2.2.2.2"},
   527  					RecordType:    endpoint.RecordTypeA,
   528  					SetIdentifier: "set-2",
   529  					Labels: map[string]string{
   530  						endpoint.OwnerLabelKey:    "test-owner",
   531  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   532  					},
   533  				},
   534  				{
   535  					DNSName:       "quux.test-zone.example.org",
   536  					Targets:       endpoint.Targets{"5.5.5.5"},
   537  					RecordType:    endpoint.RecordTypeA,
   538  					SetIdentifier: "set-2",
   539  					Labels: map[string]string{
   540  						endpoint.OwnerLabelKey:    "test-owner",
   541  						endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   542  					},
   543  				},
   544  			},
   545  		},
   546  		{
   547  			name: "create duplicate",
   548  			changes: plan.Changes{
   549  				Create: []*endpoint.Endpoint{
   550  					{
   551  						DNSName:       "new.test-zone.example.org",
   552  						Targets:       endpoint.Targets{"new.loadbalancer.com"},
   553  						RecordType:    endpoint.RecordTypeCNAME,
   554  						SetIdentifier: "set-new",
   555  						Labels: map[string]string{
   556  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   557  						},
   558  					},
   559  				},
   560  			},
   561  			stubConfig: DynamoDBStubConfig{
   562  				ExpectInsertError: map[string]string{
   563  					"new.test-zone.example.org#CNAME#set-new": "DuplicateItem",
   564  				},
   565  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   566  			},
   567  			expectedRecords: []*endpoint.Endpoint{
   568  				{
   569  					DNSName:    "foo.test-zone.example.org",
   570  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   571  					RecordType: endpoint.RecordTypeCNAME,
   572  					Labels: map[string]string{
   573  						endpoint.OwnerLabelKey: "",
   574  					},
   575  				},
   576  				{
   577  					DNSName:    "bar.test-zone.example.org",
   578  					Targets:    endpoint.Targets{"my-domain.com"},
   579  					RecordType: endpoint.RecordTypeCNAME,
   580  					Labels: map[string]string{
   581  						endpoint.OwnerLabelKey:    "test-owner",
   582  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   583  					},
   584  				},
   585  				{
   586  					DNSName:       "baz.test-zone.example.org",
   587  					Targets:       endpoint.Targets{"1.1.1.1"},
   588  					RecordType:    endpoint.RecordTypeA,
   589  					SetIdentifier: "set-1",
   590  					Labels: map[string]string{
   591  						endpoint.OwnerLabelKey:    "test-owner",
   592  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   593  					},
   594  				},
   595  				{
   596  					DNSName:       "baz.test-zone.example.org",
   597  					Targets:       endpoint.Targets{"2.2.2.2"},
   598  					RecordType:    endpoint.RecordTypeA,
   599  					SetIdentifier: "set-2",
   600  					Labels: map[string]string{
   601  						endpoint.OwnerLabelKey:    "test-owner",
   602  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   603  					},
   604  				},
   605  			},
   606  		},
   607  		{
   608  			name: "create error",
   609  			changes: plan.Changes{
   610  				Create: []*endpoint.Endpoint{
   611  					{
   612  						DNSName:       "new.test-zone.example.org",
   613  						Targets:       endpoint.Targets{"new.loadbalancer.com"},
   614  						RecordType:    endpoint.RecordTypeCNAME,
   615  						SetIdentifier: "set-new",
   616  						Labels: map[string]string{
   617  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   618  						},
   619  					},
   620  				},
   621  			},
   622  			stubConfig: DynamoDBStubConfig{
   623  				ExpectInsertError: map[string]string{
   624  					"new.test-zone.example.org#CNAME#set-new": "TestingError",
   625  				},
   626  			},
   627  			expectedError: "inserting dynamodb record \"new.test-zone.example.org#CNAME#set-new\": TestingError: testing error",
   628  			expectedRecords: []*endpoint.Endpoint{
   629  				{
   630  					DNSName:    "foo.test-zone.example.org",
   631  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   632  					RecordType: endpoint.RecordTypeCNAME,
   633  					Labels: map[string]string{
   634  						endpoint.OwnerLabelKey: "",
   635  					},
   636  				},
   637  				{
   638  					DNSName:    "bar.test-zone.example.org",
   639  					Targets:    endpoint.Targets{"my-domain.com"},
   640  					RecordType: endpoint.RecordTypeCNAME,
   641  					Labels: map[string]string{
   642  						endpoint.OwnerLabelKey:    "test-owner",
   643  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   644  					},
   645  				},
   646  				{
   647  					DNSName:       "baz.test-zone.example.org",
   648  					Targets:       endpoint.Targets{"1.1.1.1"},
   649  					RecordType:    endpoint.RecordTypeA,
   650  					SetIdentifier: "set-1",
   651  					Labels: map[string]string{
   652  						endpoint.OwnerLabelKey:    "test-owner",
   653  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   654  					},
   655  				},
   656  				{
   657  					DNSName:       "baz.test-zone.example.org",
   658  					Targets:       endpoint.Targets{"2.2.2.2"},
   659  					RecordType:    endpoint.RecordTypeA,
   660  					SetIdentifier: "set-2",
   661  					Labels: map[string]string{
   662  						endpoint.OwnerLabelKey:    "test-owner",
   663  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   664  					},
   665  				},
   666  			},
   667  		},
   668  		{
   669  			name: "update",
   670  			changes: plan.Changes{
   671  				UpdateOld: []*endpoint.Endpoint{
   672  					{
   673  						DNSName:    "bar.test-zone.example.org",
   674  						Targets:    endpoint.Targets{"my-domain.com"},
   675  						RecordType: endpoint.RecordTypeCNAME,
   676  						Labels: map[string]string{
   677  							endpoint.OwnerLabelKey:    "test-owner",
   678  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   679  						},
   680  					},
   681  				},
   682  				UpdateNew: []*endpoint.Endpoint{
   683  					{
   684  						DNSName:    "bar.test-zone.example.org",
   685  						Targets:    endpoint.Targets{"new-domain.com"},
   686  						RecordType: endpoint.RecordTypeCNAME,
   687  						Labels: map[string]string{
   688  							endpoint.OwnerLabelKey:    "test-owner",
   689  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   690  						},
   691  					},
   692  				},
   693  			},
   694  			stubConfig: DynamoDBStubConfig{
   695  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   696  			},
   697  			expectedRecords: []*endpoint.Endpoint{
   698  				{
   699  					DNSName:    "foo.test-zone.example.org",
   700  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   701  					RecordType: endpoint.RecordTypeCNAME,
   702  					Labels: map[string]string{
   703  						endpoint.OwnerLabelKey: "",
   704  					},
   705  				},
   706  				{
   707  					DNSName:    "bar.test-zone.example.org",
   708  					Targets:    endpoint.Targets{"new-domain.com"},
   709  					RecordType: endpoint.RecordTypeCNAME,
   710  					Labels: map[string]string{
   711  						endpoint.OwnerLabelKey:    "test-owner",
   712  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   713  					},
   714  				},
   715  				{
   716  					DNSName:       "baz.test-zone.example.org",
   717  					Targets:       endpoint.Targets{"1.1.1.1"},
   718  					RecordType:    endpoint.RecordTypeA,
   719  					SetIdentifier: "set-1",
   720  					Labels: map[string]string{
   721  						endpoint.OwnerLabelKey:    "test-owner",
   722  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   723  					},
   724  				},
   725  				{
   726  					DNSName:       "baz.test-zone.example.org",
   727  					Targets:       endpoint.Targets{"2.2.2.2"},
   728  					RecordType:    endpoint.RecordTypeA,
   729  					SetIdentifier: "set-2",
   730  					Labels: map[string]string{
   731  						endpoint.OwnerLabelKey:    "test-owner",
   732  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   733  					},
   734  				},
   735  			},
   736  		},
   737  		{
   738  			name: "update change",
   739  			changes: plan.Changes{
   740  				UpdateOld: []*endpoint.Endpoint{
   741  					{
   742  						DNSName:    "bar.test-zone.example.org",
   743  						Targets:    endpoint.Targets{"my-domain.com"},
   744  						RecordType: endpoint.RecordTypeCNAME,
   745  						Labels: map[string]string{
   746  							endpoint.OwnerLabelKey:    "test-owner",
   747  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   748  						},
   749  					},
   750  				},
   751  				UpdateNew: []*endpoint.Endpoint{
   752  					{
   753  						DNSName:    "bar.test-zone.example.org",
   754  						Targets:    endpoint.Targets{"new-domain.com"},
   755  						RecordType: endpoint.RecordTypeCNAME,
   756  						Labels: map[string]string{
   757  							endpoint.OwnerLabelKey:    "test-owner",
   758  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   759  						},
   760  					},
   761  				},
   762  			},
   763  			stubConfig: DynamoDBStubConfig{
   764  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   765  				ExpectUpdate: map[string]map[string]string{
   766  					"bar.test-zone.example.org#CNAME#": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
   767  				},
   768  			},
   769  			expectedRecords: []*endpoint.Endpoint{
   770  				{
   771  					DNSName:    "foo.test-zone.example.org",
   772  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   773  					RecordType: endpoint.RecordTypeCNAME,
   774  					Labels: map[string]string{
   775  						endpoint.OwnerLabelKey: "",
   776  					},
   777  				},
   778  				{
   779  					DNSName:    "bar.test-zone.example.org",
   780  					Targets:    endpoint.Targets{"new-domain.com"},
   781  					RecordType: endpoint.RecordTypeCNAME,
   782  					Labels: map[string]string{
   783  						endpoint.OwnerLabelKey:    "test-owner",
   784  						endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   785  					},
   786  				},
   787  				{
   788  					DNSName:       "baz.test-zone.example.org",
   789  					Targets:       endpoint.Targets{"1.1.1.1"},
   790  					RecordType:    endpoint.RecordTypeA,
   791  					SetIdentifier: "set-1",
   792  					Labels: map[string]string{
   793  						endpoint.OwnerLabelKey:    "test-owner",
   794  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   795  					},
   796  				},
   797  				{
   798  					DNSName:       "baz.test-zone.example.org",
   799  					Targets:       endpoint.Targets{"2.2.2.2"},
   800  					RecordType:    endpoint.RecordTypeA,
   801  					SetIdentifier: "set-2",
   802  					Labels: map[string]string{
   803  						endpoint.OwnerLabelKey:    "test-owner",
   804  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   805  					},
   806  				},
   807  			},
   808  		},
   809  		{
   810  			name: "update migrate",
   811  			addRecords: []*endpoint.Endpoint{
   812  				{
   813  					DNSName:       "txt.bar.test-zone.example.org",
   814  					Targets:       endpoint.Targets{"\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/new-ingress\""},
   815  					RecordType:    endpoint.RecordTypeTXT,
   816  					SetIdentifier: "set-1",
   817  				},
   818  			},
   819  			changes: plan.Changes{
   820  				UpdateOld: []*endpoint.Endpoint{
   821  					{
   822  						DNSName:    "bar.test-zone.example.org",
   823  						Targets:    endpoint.Targets{"my-domain.com"},
   824  						RecordType: endpoint.RecordTypeCNAME,
   825  						Labels: map[string]string{
   826  							endpoint.OwnerLabelKey:    "test-owner",
   827  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   828  						},
   829  						ProviderSpecific: endpoint.ProviderSpecific{
   830  							{
   831  								Name:  dynamodbAttributeMigrate,
   832  								Value: "true",
   833  							},
   834  						},
   835  					},
   836  				},
   837  				UpdateNew: []*endpoint.Endpoint{
   838  					{
   839  						DNSName:    "bar.test-zone.example.org",
   840  						Targets:    endpoint.Targets{"my-domain.com"},
   841  						RecordType: endpoint.RecordTypeCNAME,
   842  						Labels: map[string]string{
   843  							endpoint.OwnerLabelKey:    "test-owner",
   844  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   845  						},
   846  					},
   847  				},
   848  			},
   849  			stubConfig: DynamoDBStubConfig{
   850  				ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
   851  				ExpectInsert: map[string]map[string]string{
   852  					"bar.test-zone.example.org#CNAME#": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
   853  				},
   854  			},
   855  			expectedRecords: []*endpoint.Endpoint{
   856  				{
   857  					DNSName:    "foo.test-zone.example.org",
   858  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   859  					RecordType: endpoint.RecordTypeCNAME,
   860  					Labels: map[string]string{
   861  						endpoint.OwnerLabelKey: "",
   862  					},
   863  				},
   864  				{
   865  					DNSName:    "bar.test-zone.example.org",
   866  					Targets:    endpoint.Targets{"my-domain.com"},
   867  					RecordType: endpoint.RecordTypeCNAME,
   868  					Labels: map[string]string{
   869  						endpoint.OwnerLabelKey:    "test-owner",
   870  						endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   871  					},
   872  				},
   873  				{
   874  					DNSName:       "baz.test-zone.example.org",
   875  					Targets:       endpoint.Targets{"1.1.1.1"},
   876  					RecordType:    endpoint.RecordTypeA,
   877  					SetIdentifier: "set-1",
   878  					Labels: map[string]string{
   879  						endpoint.OwnerLabelKey:    "test-owner",
   880  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   881  					},
   882  				},
   883  				{
   884  					DNSName:       "baz.test-zone.example.org",
   885  					Targets:       endpoint.Targets{"2.2.2.2"},
   886  					RecordType:    endpoint.RecordTypeA,
   887  					SetIdentifier: "set-2",
   888  					Labels: map[string]string{
   889  						endpoint.OwnerLabelKey:    "test-owner",
   890  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   891  					},
   892  				},
   893  				{
   894  					DNSName:       "txt.bar.test-zone.example.org",
   895  					Targets:       endpoint.Targets{"\"heritage=external-dns,external-dns/owner=test-owner,external-dns/resource=ingress/default/new-ingress\""},
   896  					RecordType:    endpoint.RecordTypeTXT,
   897  					SetIdentifier: "set-1",
   898  					Labels: map[string]string{
   899  						endpoint.OwnerLabelKey: "test-owner",
   900  					},
   901  				},
   902  			},
   903  		},
   904  		{
   905  			name: "update error",
   906  			changes: plan.Changes{
   907  				UpdateOld: []*endpoint.Endpoint{
   908  					{
   909  						DNSName:    "bar.test-zone.example.org",
   910  						Targets:    endpoint.Targets{"my-domain.com"},
   911  						RecordType: endpoint.RecordTypeCNAME,
   912  						Labels: map[string]string{
   913  							endpoint.OwnerLabelKey:    "test-owner",
   914  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   915  						},
   916  					},
   917  				},
   918  				UpdateNew: []*endpoint.Endpoint{
   919  					{
   920  						DNSName:    "bar.test-zone.example.org",
   921  						Targets:    endpoint.Targets{"new-domain.com"},
   922  						RecordType: endpoint.RecordTypeCNAME,
   923  						Labels: map[string]string{
   924  							endpoint.OwnerLabelKey:    "test-owner",
   925  							endpoint.ResourceLabelKey: "ingress/default/new-ingress",
   926  						},
   927  					},
   928  				},
   929  			},
   930  			stubConfig: DynamoDBStubConfig{
   931  				ExpectUpdateError: map[string]string{
   932  					"bar.test-zone.example.org#CNAME#": "TestingError",
   933  				},
   934  			},
   935  			expectedError: "updating dynamodb record \"bar.test-zone.example.org#CNAME#\": TestingError: testing error",
   936  			expectedRecords: []*endpoint.Endpoint{
   937  				{
   938  					DNSName:    "foo.test-zone.example.org",
   939  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   940  					RecordType: endpoint.RecordTypeCNAME,
   941  					Labels: map[string]string{
   942  						endpoint.OwnerLabelKey: "",
   943  					},
   944  				},
   945  				{
   946  					DNSName:    "bar.test-zone.example.org",
   947  					Targets:    endpoint.Targets{"my-domain.com"},
   948  					RecordType: endpoint.RecordTypeCNAME,
   949  					Labels: map[string]string{
   950  						endpoint.OwnerLabelKey:    "test-owner",
   951  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   952  					},
   953  				},
   954  				{
   955  					DNSName:       "baz.test-zone.example.org",
   956  					Targets:       endpoint.Targets{"1.1.1.1"},
   957  					RecordType:    endpoint.RecordTypeA,
   958  					SetIdentifier: "set-1",
   959  					Labels: map[string]string{
   960  						endpoint.OwnerLabelKey:    "test-owner",
   961  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   962  					},
   963  				},
   964  				{
   965  					DNSName:       "baz.test-zone.example.org",
   966  					Targets:       endpoint.Targets{"2.2.2.2"},
   967  					RecordType:    endpoint.RecordTypeA,
   968  					SetIdentifier: "set-2",
   969  					Labels: map[string]string{
   970  						endpoint.OwnerLabelKey:    "test-owner",
   971  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
   972  					},
   973  				},
   974  			},
   975  		},
   976  		{
   977  			name: "delete",
   978  			changes: plan.Changes{
   979  				Delete: []*endpoint.Endpoint{
   980  					{
   981  						DNSName:    "bar.test-zone.example.org",
   982  						Targets:    endpoint.Targets{"my-domain.com"},
   983  						RecordType: endpoint.RecordTypeCNAME,
   984  						Labels: map[string]string{
   985  							endpoint.OwnerLabelKey:    "test-owner",
   986  							endpoint.ResourceLabelKey: "ingress/default/my-ingress",
   987  						},
   988  					},
   989  				},
   990  			},
   991  			stubConfig: DynamoDBStubConfig{
   992  				ExpectDelete: sets.New("bar.test-zone.example.org#CNAME#", "quux.test-zone.example.org#A#set-2"),
   993  			},
   994  			expectedRecords: []*endpoint.Endpoint{
   995  				{
   996  					DNSName:    "foo.test-zone.example.org",
   997  					Targets:    endpoint.Targets{"foo.loadbalancer.com"},
   998  					RecordType: endpoint.RecordTypeCNAME,
   999  					Labels: map[string]string{
  1000  						endpoint.OwnerLabelKey: "",
  1001  					},
  1002  				},
  1003  				{
  1004  					DNSName:       "baz.test-zone.example.org",
  1005  					Targets:       endpoint.Targets{"1.1.1.1"},
  1006  					RecordType:    endpoint.RecordTypeA,
  1007  					SetIdentifier: "set-1",
  1008  					Labels: map[string]string{
  1009  						endpoint.OwnerLabelKey:    "test-owner",
  1010  						endpoint.ResourceLabelKey: "ingress/default/my-ingress",
  1011  					},
  1012  				},
  1013  				{
  1014  					DNSName:       "baz.test-zone.example.org",
  1015  					Targets:       endpoint.Targets{"2.2.2.2"},
  1016  					RecordType:    endpoint.RecordTypeA,
  1017  					SetIdentifier: "set-2",
  1018  					Labels: map[string]string{
  1019  						endpoint.OwnerLabelKey:    "test-owner",
  1020  						endpoint.ResourceLabelKey: "ingress/default/other-ingress",
  1021  					},
  1022  				},
  1023  			},
  1024  		},
  1025  	} {
  1026  		t.Run(tc.name, func(t *testing.T) {
  1027  			originalMaxBatchSize := dynamodbMaxBatchSize
  1028  			if tc.maxBatchSize > 0 {
  1029  				dynamodbMaxBatchSize = tc.maxBatchSize
  1030  			}
  1031  
  1032  			api, p := newDynamoDBAPIStub(t, &tc.stubConfig)
  1033  			if len(tc.addRecords) > 0 {
  1034  				_ = p.(*wrappedProvider).Provider.ApplyChanges(context.Background(), &plan.Changes{
  1035  					Create: tc.addRecords,
  1036  				})
  1037  			}
  1038  
  1039  			ctx := context.Background()
  1040  
  1041  			r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "txt.", "", "", []string{}, []string{}, nil, time.Hour)
  1042  			_, err := r.Records(ctx)
  1043  			require.Nil(t, err)
  1044  
  1045  			err = r.ApplyChanges(ctx, &tc.changes)
  1046  			if tc.expectedError == "" {
  1047  				assert.Nil(t, err)
  1048  			} else {
  1049  				assert.EqualError(t, err, tc.expectedError)
  1050  			}
  1051  
  1052  			assert.Empty(t, tc.stubConfig.ExpectInsert, "all expected inserts made")
  1053  			assert.Empty(t, tc.stubConfig.ExpectDelete, "all expected deletions made")
  1054  
  1055  			records, err := r.Records(ctx)
  1056  			require.Nil(t, err)
  1057  			assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords))
  1058  
  1059  			r.recordsCache = nil
  1060  			records, err = r.Records(ctx)
  1061  			require.Nil(t, err)
  1062  			assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords))
  1063  			if tc.expectedError == "" {
  1064  				assert.Empty(t, r.orphanedLabels)
  1065  			}
  1066  
  1067  			dynamodbMaxBatchSize = originalMaxBatchSize
  1068  		})
  1069  	}
  1070  }
  1071  
  1072  // DynamoDBAPIStub is a minimal implementation of DynamoDBAPI, used primarily for unit testing.
  1073  type DynamoDBStub struct {
  1074  	t                *testing.T
  1075  	stubConfig       *DynamoDBStubConfig
  1076  	tableDescription dynamodb.TableDescription
  1077  	changesApplied   bool
  1078  }
  1079  
  1080  type DynamoDBStubConfig struct {
  1081  	ExpectInsert      map[string]map[string]string
  1082  	ExpectInsertError map[string]string
  1083  	ExpectUpdate      map[string]map[string]string
  1084  	ExpectUpdateError map[string]string
  1085  	ExpectDelete      sets.Set[string]
  1086  }
  1087  
  1088  type wrappedProvider struct {
  1089  	provider.Provider
  1090  	stub *DynamoDBStub
  1091  }
  1092  
  1093  func (w *wrappedProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
  1094  	assert.False(w.stub.t, w.stub.changesApplied, "ApplyChanges already called")
  1095  	w.stub.changesApplied = true
  1096  	return w.Provider.ApplyChanges(ctx, changes)
  1097  }
  1098  
  1099  func newDynamoDBAPIStub(t *testing.T, stubConfig *DynamoDBStubConfig) (*DynamoDBStub, provider.Provider) {
  1100  	stub := &DynamoDBStub{
  1101  		t:          t,
  1102  		stubConfig: stubConfig,
  1103  		tableDescription: dynamodb.TableDescription{
  1104  			AttributeDefinitions: []*dynamodb.AttributeDefinition{
  1105  				{
  1106  					AttributeName: aws.String("k"),
  1107  					AttributeType: aws.String("S"),
  1108  				},
  1109  			},
  1110  			KeySchema: []*dynamodb.KeySchemaElement{
  1111  				{
  1112  					AttributeName: aws.String("k"),
  1113  					KeyType:       aws.String("HASH"),
  1114  				},
  1115  			},
  1116  		},
  1117  	}
  1118  	p := inmemory.NewInMemoryProvider()
  1119  	_ = p.CreateZone(testZone)
  1120  	_ = p.ApplyChanges(context.Background(), &plan.Changes{
  1121  		Create: []*endpoint.Endpoint{
  1122  			endpoint.NewEndpoint("foo.test-zone.example.org", endpoint.RecordTypeCNAME, "foo.loadbalancer.com"),
  1123  			endpoint.NewEndpoint("bar.test-zone.example.org", endpoint.RecordTypeCNAME, "my-domain.com"),
  1124  			endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "1.1.1.1").WithSetIdentifier("set-1"),
  1125  			endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "2.2.2.2").WithSetIdentifier("set-2"),
  1126  		},
  1127  	})
  1128  	return stub, &wrappedProvider{
  1129  		Provider: p,
  1130  		stub:     stub,
  1131  	}
  1132  }
  1133  
  1134  func (r *DynamoDBStub) DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error) {
  1135  	assert.NotNil(r.t, ctx)
  1136  	assert.Equal(r.t, "test-table", *input.TableName, "table name")
  1137  	return &dynamodb.DescribeTableOutput{
  1138  		Table: &r.tableDescription,
  1139  	}, nil
  1140  }
  1141  
  1142  func (r *DynamoDBStub) ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error {
  1143  	assert.NotNil(r.t, ctx)
  1144  	assert.Equal(r.t, "test-table", *input.TableName, "table name")
  1145  	assert.Equal(r.t, "o = :ownerval", *input.FilterExpression)
  1146  	assert.Len(r.t, input.ExpressionAttributeValues, 1)
  1147  	assert.Equal(r.t, "test-owner", *input.ExpressionAttributeValues[":ownerval"].S)
  1148  	assert.Equal(r.t, "k,l", *input.ProjectionExpression)
  1149  	assert.True(r.t, *input.ConsistentRead)
  1150  	fn(&dynamodb.ScanOutput{
  1151  		Items: []map[string]*dynamodb.AttributeValue{
  1152  			{
  1153  				"k": &dynamodb.AttributeValue{S: aws.String("bar.test-zone.example.org#CNAME#")},
  1154  				"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
  1155  					endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")},
  1156  				}},
  1157  			},
  1158  			{
  1159  				"k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-1")},
  1160  				"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
  1161  					endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")},
  1162  				}},
  1163  			},
  1164  			{
  1165  				"k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-2")},
  1166  				"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
  1167  					endpoint.ResourceLabelKey: {S: aws.String("ingress/default/other-ingress")},
  1168  				}},
  1169  			},
  1170  			{
  1171  				"k": &dynamodb.AttributeValue{S: aws.String("quux.test-zone.example.org#A#set-2")},
  1172  				"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
  1173  					endpoint.ResourceLabelKey: {S: aws.String("ingress/default/quux-ingress")},
  1174  				}},
  1175  			},
  1176  		},
  1177  	}, true)
  1178  	return nil
  1179  }
  1180  
  1181  func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, input *dynamodb.BatchExecuteStatementInput, option ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) {
  1182  	assert.NotNil(r.t, context)
  1183  	hasDelete := strings.HasPrefix(strings.ToLower(aws.StringValue(input.Statements[0].Statement)), "delete")
  1184  	assert.Equal(r.t, hasDelete, r.changesApplied, "delete after provider changes, everything else before")
  1185  	assert.LessOrEqual(r.t, len(input.Statements), 25)
  1186  	responses := make([]*dynamodb.BatchStatementResponse, 0, len(input.Statements))
  1187  
  1188  	for _, statement := range input.Statements {
  1189  		assert.Equal(r.t, hasDelete, strings.HasPrefix(strings.ToLower(aws.StringValue(statement.Statement)), "delete"))
  1190  		switch aws.StringValue(statement.Statement) {
  1191  		case "DELETE FROM \"test-table\" WHERE \"k\"=? AND \"o\"=?":
  1192  			assert.True(r.t, r.changesApplied, "unexpected delete before provider changes")
  1193  
  1194  			key := aws.StringValue(statement.Parameters[0].S)
  1195  			assert.True(r.t, r.stubConfig.ExpectDelete.Has(key), "unexpected delete for key %q", key)
  1196  			r.stubConfig.ExpectDelete.Delete(key)
  1197  
  1198  			assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S))
  1199  
  1200  			responses = append(responses, &dynamodb.BatchStatementResponse{})
  1201  
  1202  		case "INSERT INTO \"test-table\" VALUE {'k':?, 'o':?, 'l':?}":
  1203  			assert.False(r.t, r.changesApplied, "unexpected insert after provider changes")
  1204  
  1205  			key := aws.StringValue(statement.Parameters[0].S)
  1206  			if code, exists := r.stubConfig.ExpectInsertError[key]; exists {
  1207  				delete(r.stubConfig.ExpectInsertError, key)
  1208  				responses = append(responses, &dynamodb.BatchStatementResponse{
  1209  					Error: &dynamodb.BatchStatementError{
  1210  						Code:    aws.String(code),
  1211  						Message: aws.String("testing error"),
  1212  					},
  1213  				})
  1214  				break
  1215  			}
  1216  
  1217  			expectedLabels, found := r.stubConfig.ExpectInsert[key]
  1218  			assert.True(r.t, found, "unexpected insert for key %q", key)
  1219  			delete(r.stubConfig.ExpectInsert, key)
  1220  
  1221  			assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S))
  1222  
  1223  			for label, attribute := range statement.Parameters[2].M {
  1224  				value := aws.StringValue(attribute.S)
  1225  				expectedValue, found := expectedLabels[label]
  1226  				assert.True(r.t, found, "insert for key %q has unexpected label %q", key, label)
  1227  				delete(expectedLabels, label)
  1228  				assert.Equal(r.t, expectedValue, value, "insert for key %q label %q value", key, label)
  1229  			}
  1230  
  1231  			for label := range expectedLabels {
  1232  				r.t.Errorf("insert for key %q did not get expected label %q", key, label)
  1233  			}
  1234  
  1235  			responses = append(responses, &dynamodb.BatchStatementResponse{})
  1236  
  1237  		case "UPDATE \"test-table\" SET \"l\"=? WHERE \"k\"=?":
  1238  			assert.False(r.t, r.changesApplied, "unexpected update after provider changes")
  1239  
  1240  			key := aws.StringValue(statement.Parameters[1].S)
  1241  			if code, exists := r.stubConfig.ExpectUpdateError[key]; exists {
  1242  				delete(r.stubConfig.ExpectInsertError, key)
  1243  				responses = append(responses, &dynamodb.BatchStatementResponse{
  1244  					Error: &dynamodb.BatchStatementError{
  1245  						Code:    aws.String(code),
  1246  						Message: aws.String("testing error"),
  1247  					},
  1248  				})
  1249  				break
  1250  			}
  1251  
  1252  			expectedLabels, found := r.stubConfig.ExpectUpdate[key]
  1253  			assert.True(r.t, found, "unexpected update for key %q", key)
  1254  			delete(r.stubConfig.ExpectUpdate, key)
  1255  
  1256  			for label, attribute := range statement.Parameters[0].M {
  1257  				value := aws.StringValue(attribute.S)
  1258  				expectedValue, found := expectedLabels[label]
  1259  				assert.True(r.t, found, "update for key %q has unexpected label %q", key, label)
  1260  				delete(expectedLabels, label)
  1261  				assert.Equal(r.t, expectedValue, value, "update for key %q label %q value", key, label)
  1262  			}
  1263  
  1264  			for label := range expectedLabels {
  1265  				r.t.Errorf("update for key %q did not get expected label %q", key, label)
  1266  			}
  1267  
  1268  			responses = append(responses, &dynamodb.BatchStatementResponse{})
  1269  
  1270  		default:
  1271  			r.t.Errorf("unexpected statement: %s", aws.StringValue(statement.Statement))
  1272  		}
  1273  	}
  1274  
  1275  	return &dynamodb.BatchExecuteStatementOutput{
  1276  		Responses: responses,
  1277  	}, nil
  1278  }