sigs.k8s.io/external-dns@v0.14.1/provider/oci/oci_test.go (about)

     1  /*
     2  Copyright 2018 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 oci
    18  
    19  import (
    20  	"context"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/oracle/oci-go-sdk/v65/common"
    27  	"github.com/oracle/oci-go-sdk/v65/dns"
    28  	"github.com/pkg/errors"
    29  	"github.com/stretchr/testify/require"
    30  
    31  	"sigs.k8s.io/external-dns/endpoint"
    32  	"sigs.k8s.io/external-dns/plan"
    33  	"sigs.k8s.io/external-dns/provider"
    34  )
    35  
    36  type mockOCIDNSClient struct {
    37  }
    38  
    39  var (
    40  	zoneIdQux                 = "ocid1.dns-zone.oc1..123456ef0bfbb5c251b9713fd7bf8959"
    41  	zoneNameQux               = "qux.com"
    42  	testPrivateZoneSummaryQux = dns.ZoneSummary{
    43  		Id:   &zoneIdQux,
    44  		Name: &zoneNameQux,
    45  	}
    46  	zoneIdBaz                 = "ocid1.dns-zone.oc1..789012ef0bfbb5c251b9713fd7bf8959"
    47  	zoneNameBaz               = "baz.com"
    48  	testPrivateZoneSummaryBaz = dns.ZoneSummary{
    49  		Id:   &zoneIdBaz,
    50  		Name: &zoneNameBaz,
    51  	}
    52  	testGlobalZoneSummaryFoo = dns.ZoneSummary{
    53  		Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
    54  		Name: common.String("foo.com"),
    55  	}
    56  	testGlobalZoneSummaryBar = dns.ZoneSummary{
    57  		Id:   common.String("ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"),
    58  		Name: common.String("bar.com"),
    59  	}
    60  )
    61  
    62  func buildZoneResponseItems(scope dns.ListZonesScopeEnum, privateZones, globalZones []dns.ZoneSummary) []dns.ZoneSummary {
    63  	switch string(scope) {
    64  	case "PRIVATE":
    65  		return privateZones
    66  	case "GLOBAL":
    67  		return globalZones
    68  	default:
    69  		return append(privateZones, globalZones...)
    70  	}
    71  }
    72  
    73  func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
    74  	if request.Page == nil || *request.Page == "0" {
    75  		return dns.ListZonesResponse{
    76  			Items:       buildZoneResponseItems(request.Scope, []dns.ZoneSummary{testPrivateZoneSummaryBaz}, []dns.ZoneSummary{testGlobalZoneSummaryFoo}),
    77  			OpcNextPage: common.String("1"),
    78  		}, nil
    79  	}
    80  	return dns.ListZonesResponse{
    81  		Items: buildZoneResponseItems(request.Scope, []dns.ZoneSummary{testPrivateZoneSummaryQux}, []dns.ZoneSummary{testGlobalZoneSummaryBar}),
    82  	}, nil
    83  }
    84  
    85  func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
    86  	if request.ZoneNameOrId == nil {
    87  		return
    88  	}
    89  
    90  	switch *request.ZoneNameOrId {
    91  	case "ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959":
    92  		if request.Page == nil || *request.Page == "0" {
    93  			response.Items = []dns.Record{{
    94  				Domain: common.String("foo.foo.com"),
    95  				Rdata:  common.String("127.0.0.1"),
    96  				Rtype:  common.String(endpoint.RecordTypeA),
    97  				Ttl:    common.Int(ociRecordTTL),
    98  			}, {
    99  				Domain: common.String("foo.foo.com"),
   100  				Rdata:  common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   101  				Rtype:  common.String(endpoint.RecordTypeTXT),
   102  				Ttl:    common.Int(ociRecordTTL),
   103  			}}
   104  			response.OpcNextPage = common.String("1")
   105  		} else {
   106  			response.Items = []dns.Record{{
   107  				Domain: common.String("bar.foo.com"),
   108  				Rdata:  common.String("bar.com."),
   109  				Rtype:  common.String(endpoint.RecordTypeCNAME),
   110  				Ttl:    common.Int(ociRecordTTL),
   111  			}}
   112  		}
   113  	case "ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404":
   114  		if request.Page == nil || *request.Page == "0" {
   115  			response.Items = []dns.Record{{
   116  				Domain: common.String("foo.bar.com"),
   117  				Rdata:  common.String("127.0.0.1"),
   118  				Rtype:  common.String(endpoint.RecordTypeA),
   119  				Ttl:    common.Int(ociRecordTTL),
   120  			}}
   121  		}
   122  	}
   123  
   124  	return
   125  }
   126  
   127  func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
   128  	return // Provider does not use the response so nothing to do here.
   129  }
   130  
   131  // newOCIProvider creates an OCI provider with API calls mocked out.
   132  func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneScope string, dryRun bool) *OCIProvider {
   133  	return &OCIProvider{
   134  		client: client,
   135  		cfg: OCIConfig{
   136  			CompartmentID: "ocid1.compartment.oc1..aaaaaaaaujjg4lf3v6uaqeml7xfk7stzvrxeweaeyolhh75exuoqxpqjb4qq",
   137  		},
   138  		domainFilter: domainFilter,
   139  		zoneIDFilter: zoneIDFilter,
   140  		zoneScope:    zoneScope,
   141  		zoneCache: &zoneCache{
   142  			duration: 0 * time.Second,
   143  		},
   144  		dryRun: dryRun,
   145  	}
   146  }
   147  
   148  func validateOCIZones(t *testing.T, actual, expected map[string]dns.ZoneSummary) {
   149  	require.Len(t, actual, len(expected))
   150  
   151  	for k, a := range actual {
   152  		e, ok := expected[k]
   153  		require.True(t, ok, "unexpected zone %q (%q)", *a.Name, *a.Id)
   154  		require.Equal(t, e, a)
   155  	}
   156  }
   157  
   158  func TestNewOCIProvider(t *testing.T) {
   159  	testCases := map[string]struct {
   160  		config OCIConfig
   161  		err    error
   162  	}{
   163  		"valid": {
   164  			config: OCIConfig{
   165  				Auth: OCIAuthConfig{
   166  					TenancyID:   "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
   167  					UserID:      "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
   168  					Region:      "us-ashburn-1",
   169  					Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
   170  					PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
   171  MIIEowIBAAKCAQEAv2JspZyO14kqcO/X4iz3ZdcyAf1GQJqYsBb6wyrlU0PB9Fee
   172  H23/HLtMSqeqo+2KQHmdV1OHFQ/S6tx7zcBaby/+2b+z3/gJO4PGxohe2812AJ/J
   173  W8Fp/4EnwbaRqDhoLN7ms0/e566zE3z40kCSW0NAIzv/F+0nNaka1xrypBqzvaNm
   174  N49dAGvqWRpzFFUb8CbvKmgE6c/H4a2zVNW3G7/K6Og4HQGeEP3NKSVvi0BiQlvd
   175  tVJTg7084kKcrngsS2N3qI3pzsr5wgpzPPefuPHWRKokZ20kpu8tXdFt+mAC2NHh
   176  eWbtY3jsR6JFaXCyZLMXInwDvRgdP0T5+uh8WwIDAQABAoIBAG0rr94omDLKw7L4
   177  naUfEWC+iIAqAdEIXuDTuudpqLb+h7zh3gj/re6tyK8tRWGNNrfgp6gQtZWGGUJv
   178  0w9jEjMqpa2AdRLlYh7Y5KKLV9D6Or3QaAQ3KEffXNZbVmsnAgXWgLL4dKakOPJ8
   179  71LAEryMeCGhL7puRVeOxwi9Dnwc4pcloimdggw/uwVHMK9eY5ylyt5ziiiWfhAo
   180  cnNJNPHRSTqSiCoEhk/8BLZT5gxf1YX0hVSEdQh2WNyxmPmVSC9uuzKOqcEBfHf5
   181  hmLnsUET1REM9IxCLqC9ebW263lIO/KdGiCu+YgIdwIi3wrLhaKXAZQmp4oMvWlE
   182  n5eYlcECgYEA5AhctPWCQBCJhcD39pSWgnSq1O9bt8yQi2P2stqlxKV9ZBepCK49
   183  OT42OYPUgWn7/y//6/LLzsPY58VTDHF3xZN1qu+fU0IM22D3Jqc19pnfVEb6TXSc
   184  0jJIiaYCWTdqRQ4p2DuDcI+EzRB+V1Z7tFWxshZWXwNvtMXNoYPOYaUCgYEA1ttn
   185  R3pCuGYJ5XbBwPzD5J+hvdZ6TQf8oTDraUBPxjtFOr7ea42T6KeYRFvnK2AQDnKL
   186  Mw3I55lNO4I2W9gahUFG28dhxEuxeyvXGqXEJvPCUYePstab/BkUrm7/jkS3CLcJ
   187  dlRXjqOfGwi5+NPUZMoOkZ54ZR4ZpdhIAeEpBf8CgYEAyMyMRlVCowNs9jkcoSfq
   188  +Wme3O8BhvI9/mDCZnCfNHC94Bvtn1U/WF7uBOuPf35Ch05PQAiHa8WOBVn/bZ+l
   189  ZngZT7K+S+SHyc6zFHh9zm9k96Og2f/r8DSTJ5Ll0oY3sCNuuZh+f+oBeUoi1umy
   190  +PPVDAsbd4NhJIBiOO4GGHkCgYA1p4i9Es0Cm4ixItzzwqtwtmR/scXM4se1wS+o
   191  kwTY7gg1yWBl328mVGPz/jdWX6Di2rvkPfcDzwa4a6YDfY3x5QE69Sl3CagCqEoJ
   192  P4giahEGpyG9eVZuuBywCswKzSIgLQVR5XIQDtA2whEfEFcj7EmDF93c8o1ZGw+w
   193  WHgUJQKBgEXr0HgxGG+v8bsXdrJ87Avx/nuA2rrFfECDPa4zuPkEK+cSFibdAq/H
   194  u6OIV+z59AD2s84gxR+KLzEDfQAqBt7cVA5ZH6hrO+bkCtK9ycLL+koOuB+1EV+Y
   195  hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
   196  -----END RSA PRIVATE KEY-----
   197  `,
   198  				},
   199  			},
   200  		},
   201  		"instance-principal": {
   202  			// testing the InstancePrincipalConfigurationProvider is tricky outside of an OCI context, because it tries
   203  			// to request a token from the internal OCI systems; this test-case just confirms that the expected error is
   204  			// observed, confirming that the instance-principal provider was instantiated.
   205  			config: OCIConfig{
   206  				Auth: OCIAuthConfig{
   207  					UseInstancePrincipal: true,
   208  				},
   209  			},
   210  			err: errors.New("error creating OCI instance principal config provider: failed to create a new key provider for instance principal"),
   211  		},
   212  		"invalid": {
   213  			config: OCIConfig{
   214  				Auth: OCIAuthConfig{
   215  					TenancyID:   "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
   216  					UserID:      "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
   217  					Region:      "us-ashburn-1",
   218  					Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
   219  					PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
   220  `,
   221  				},
   222  			},
   223  			err: errors.New("initializing OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"),
   224  		},
   225  		"invalid-auth-methods": {
   226  			config: OCIConfig{
   227  				Auth: OCIAuthConfig{
   228  					Region:               "us-ashburn-1",
   229  					UseInstancePrincipal: true,
   230  					UseWorkloadIdentity:  true,
   231  				},
   232  			},
   233  			err: errors.New("only one of 'useInstancePrincipal' and 'useWorkloadIdentity' may be enabled for Oracle authentication"),
   234  		},
   235  	}
   236  	for name, tc := range testCases {
   237  		t.Run(name, func(t *testing.T) {
   238  			_, err := NewOCIProvider(
   239  				tc.config,
   240  				endpoint.NewDomainFilter([]string{"com"}),
   241  				provider.NewZoneIDFilter([]string{""}),
   242  				string(dns.GetZoneScopeGlobal),
   243  				false,
   244  			)
   245  			if err == nil {
   246  				require.NoError(t, err)
   247  			} else {
   248  				// have to use prefix testing because the expected instance-principal error strings vary after a known prefix
   249  				require.Truef(t, strings.HasPrefix(err.Error(), tc.err.Error()), "observed: %s", err.Error())
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  func TestOCIZones(t *testing.T) {
   256  	fooZoneId := "ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"
   257  	barZoneId := "ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"
   258  	testCases := []struct {
   259  		name         string
   260  		domainFilter endpoint.DomainFilter
   261  		zoneIDFilter provider.ZoneIDFilter
   262  		zoneScope    string
   263  		expected     map[string]dns.ZoneSummary
   264  	}{
   265  		{
   266  			name:         "AllZones",
   267  			domainFilter: endpoint.NewDomainFilter([]string{"com"}),
   268  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   269  			zoneScope:    "",
   270  			expected: map[string]dns.ZoneSummary{
   271  				fooZoneId: testGlobalZoneSummaryFoo,
   272  				barZoneId: testGlobalZoneSummaryBar,
   273  				zoneIdBaz: testPrivateZoneSummaryBaz,
   274  				zoneIdQux: testPrivateZoneSummaryQux,
   275  			},
   276  		},
   277  		{
   278  			name:         "Privatezones",
   279  			domainFilter: endpoint.NewDomainFilter([]string{"com"}),
   280  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   281  			zoneScope:    "PRIVATE",
   282  			expected: map[string]dns.ZoneSummary{
   283  				zoneIdBaz: testPrivateZoneSummaryBaz,
   284  				zoneIdQux: testPrivateZoneSummaryQux,
   285  			},
   286  		},
   287  		{
   288  			name:         "DomainFilter_com",
   289  			domainFilter: endpoint.NewDomainFilter([]string{"com"}),
   290  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   291  			zoneScope:    "GLOBAL",
   292  			expected: map[string]dns.ZoneSummary{
   293  				fooZoneId: testGlobalZoneSummaryFoo,
   294  				barZoneId: testGlobalZoneSummaryBar,
   295  			},
   296  		}, {
   297  			name:         "DomainFilter_foo.com",
   298  			domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
   299  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   300  			zoneScope:    "GLOBAL",
   301  			expected: map[string]dns.ZoneSummary{
   302  				fooZoneId: {
   303  					Id:   common.String(fooZoneId),
   304  					Name: common.String("foo.com"),
   305  				},
   306  			},
   307  		}, {
   308  			name:         "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959",
   309  			domainFilter: endpoint.NewDomainFilter([]string{""}),
   310  			zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}),
   311  			zoneScope:    "GLOBAL",
   312  			expected: map[string]dns.ZoneSummary{
   313  				fooZoneId: {
   314  					Id:   common.String(fooZoneId),
   315  					Name: common.String("foo.com"),
   316  				},
   317  			},
   318  		},
   319  	}
   320  	for _, tc := range testCases {
   321  		t.Run(tc.name, func(t *testing.T) {
   322  			provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, tc.zoneScope, false)
   323  			zones, err := provider.zones(context.Background())
   324  			require.NoError(t, err)
   325  			validateOCIZones(t, zones, tc.expected)
   326  		})
   327  	}
   328  }
   329  
   330  func TestOCIRecords(t *testing.T) {
   331  	testCases := []struct {
   332  		name         string
   333  		domainFilter endpoint.DomainFilter
   334  		zoneIDFilter provider.ZoneIDFilter
   335  		expected     []*endpoint.Endpoint
   336  	}{
   337  		{
   338  			name:         "unfiltered",
   339  			domainFilter: endpoint.NewDomainFilter([]string{""}),
   340  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   341  			expected: []*endpoint.Endpoint{
   342  				endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
   343  				endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   344  				endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
   345  				endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
   346  			},
   347  		}, {
   348  			name:         "DomainFilter_foo.com",
   349  			domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
   350  			zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   351  			expected: []*endpoint.Endpoint{
   352  				endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
   353  				endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   354  				endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
   355  			},
   356  		}, {
   357  			name:         "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404",
   358  			domainFilter: endpoint.NewDomainFilter([]string{""}),
   359  			zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
   360  			expected: []*endpoint.Endpoint{
   361  				endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
   362  			},
   363  		},
   364  	}
   365  	for _, tc := range testCases {
   366  		t.Run(tc.name, func(t *testing.T) {
   367  			provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, "", false)
   368  			endpoints, err := provider.Records(context.Background())
   369  			require.NoError(t, err)
   370  			require.ElementsMatch(t, tc.expected, endpoints)
   371  		})
   372  	}
   373  }
   374  
   375  func TestNewRecordOperation(t *testing.T) {
   376  	testCases := []struct {
   377  		name     string
   378  		ep       *endpoint.Endpoint
   379  		opType   dns.RecordOperationOperationEnum
   380  		expected dns.RecordOperation
   381  	}{
   382  		{
   383  			name:   "A_record",
   384  			opType: dns.RecordOperationOperationAdd,
   385  			ep: endpoint.NewEndpointWithTTL(
   386  				"foo.foo.com",
   387  				endpoint.RecordTypeA,
   388  				endpoint.TTL(ociRecordTTL),
   389  				"127.0.0.1"),
   390  			expected: dns.RecordOperation{
   391  				Domain:    common.String("foo.foo.com"),
   392  				Rdata:     common.String("127.0.0.1"),
   393  				Rtype:     common.String("A"),
   394  				Ttl:       common.Int(300),
   395  				Operation: dns.RecordOperationOperationAdd,
   396  			},
   397  		}, {
   398  			name:   "TXT_record",
   399  			opType: dns.RecordOperationOperationAdd,
   400  			ep: endpoint.NewEndpointWithTTL(
   401  				"foo.foo.com",
   402  				endpoint.RecordTypeTXT,
   403  				endpoint.TTL(ociRecordTTL),
   404  				"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   405  			expected: dns.RecordOperation{
   406  				Domain:    common.String("foo.foo.com"),
   407  				Rdata:     common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   408  				Rtype:     common.String("TXT"),
   409  				Ttl:       common.Int(300),
   410  				Operation: dns.RecordOperationOperationAdd,
   411  			},
   412  		}, {
   413  			name:   "CNAME_record",
   414  			opType: dns.RecordOperationOperationAdd,
   415  			ep: endpoint.NewEndpointWithTTL(
   416  				"foo.foo.com",
   417  				endpoint.RecordTypeCNAME,
   418  				endpoint.TTL(ociRecordTTL),
   419  				"bar.com."),
   420  			expected: dns.RecordOperation{
   421  				Domain:    common.String("foo.foo.com"),
   422  				Rdata:     common.String("bar.com."),
   423  				Rtype:     common.String("CNAME"),
   424  				Ttl:       common.Int(300),
   425  				Operation: dns.RecordOperationOperationAdd,
   426  			},
   427  		},
   428  	}
   429  
   430  	for _, tc := range testCases {
   431  		t.Run(tc.name, func(t *testing.T) {
   432  			op := newRecordOperation(tc.ep, tc.opType)
   433  			require.Equal(t, tc.expected, op)
   434  		})
   435  	}
   436  }
   437  
   438  func TestOperationsByZone(t *testing.T) {
   439  	testCases := []struct {
   440  		name     string
   441  		zones    map[string]dns.ZoneSummary
   442  		ops      []dns.RecordOperation
   443  		expected map[string][]dns.RecordOperation
   444  	}{
   445  		{
   446  			name: "basic",
   447  			zones: map[string]dns.ZoneSummary{
   448  				"foo": {
   449  					Id:   common.String("foo"),
   450  					Name: common.String("foo.com"),
   451  				},
   452  				"bar": {
   453  					Id:   common.String("bar"),
   454  					Name: common.String("bar.com"),
   455  				},
   456  			},
   457  			ops: []dns.RecordOperation{
   458  				{
   459  					Domain:    common.String("foo.foo.com"),
   460  					Rdata:     common.String("127.0.0.1"),
   461  					Rtype:     common.String("A"),
   462  					Ttl:       common.Int(300),
   463  					Operation: dns.RecordOperationOperationAdd,
   464  				},
   465  				{
   466  					Domain:    common.String("foo.bar.com"),
   467  					Rdata:     common.String("127.0.0.1"),
   468  					Rtype:     common.String("A"),
   469  					Ttl:       common.Int(300),
   470  					Operation: dns.RecordOperationOperationAdd,
   471  				},
   472  			},
   473  			expected: map[string][]dns.RecordOperation{
   474  				"foo": {
   475  					{
   476  						Domain:    common.String("foo.foo.com"),
   477  						Rdata:     common.String("127.0.0.1"),
   478  						Rtype:     common.String("A"),
   479  						Ttl:       common.Int(300),
   480  						Operation: dns.RecordOperationOperationAdd,
   481  					},
   482  				},
   483  				"bar": {
   484  					{
   485  						Domain:    common.String("foo.bar.com"),
   486  						Rdata:     common.String("127.0.0.1"),
   487  						Rtype:     common.String("A"),
   488  						Ttl:       common.Int(300),
   489  						Operation: dns.RecordOperationOperationAdd,
   490  					},
   491  				},
   492  			},
   493  		}, {
   494  			name: "does_not_include_zones_with_no_changes",
   495  			zones: map[string]dns.ZoneSummary{
   496  				"foo": {
   497  					Id:   common.String("foo"),
   498  					Name: common.String("foo.com"),
   499  				},
   500  				"bar": {
   501  					Id:   common.String("bar"),
   502  					Name: common.String("bar.com"),
   503  				},
   504  			},
   505  			ops: []dns.RecordOperation{
   506  				{
   507  					Domain:    common.String("foo.foo.com"),
   508  					Rdata:     common.String("127.0.0.1"),
   509  					Rtype:     common.String("A"),
   510  					Ttl:       common.Int(300),
   511  					Operation: dns.RecordOperationOperationAdd,
   512  				},
   513  			},
   514  			expected: map[string][]dns.RecordOperation{
   515  				"foo": {
   516  					{
   517  						Domain:    common.String("foo.foo.com"),
   518  						Rdata:     common.String("127.0.0.1"),
   519  						Rtype:     common.String("A"),
   520  						Ttl:       common.Int(300),
   521  						Operation: dns.RecordOperationOperationAdd,
   522  					},
   523  				},
   524  			},
   525  		},
   526  	}
   527  
   528  	for _, tc := range testCases {
   529  		t.Run(tc.name, func(t *testing.T) {
   530  			result := operationsByZone(tc.zones, tc.ops)
   531  			require.Equal(t, tc.expected, result)
   532  		})
   533  	}
   534  }
   535  
   536  type mutableMockOCIDNSClient struct {
   537  	zones   map[string]dns.ZoneSummary
   538  	records map[string]map[string]dns.Record
   539  }
   540  
   541  func newMutableMockOCIDNSClient(zones []dns.ZoneSummary, recordsByZone map[string][]dns.Record) *mutableMockOCIDNSClient {
   542  	c := &mutableMockOCIDNSClient{
   543  		zones:   make(map[string]dns.ZoneSummary),
   544  		records: make(map[string]map[string]dns.Record),
   545  	}
   546  
   547  	for _, zone := range zones {
   548  		c.zones[*zone.Id] = zone
   549  		c.records[*zone.Id] = make(map[string]dns.Record)
   550  	}
   551  
   552  	for zoneID, records := range recordsByZone {
   553  		for _, record := range records {
   554  			c.records[zoneID][ociRecordKey(*record.Rtype, *record.Domain)] = record
   555  		}
   556  	}
   557  
   558  	return c
   559  }
   560  
   561  func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
   562  	var zones []dns.ZoneSummary
   563  	for _, v := range c.zones {
   564  		zones = append(zones, v)
   565  	}
   566  	return dns.ListZonesResponse{Items: zones}, nil
   567  }
   568  
   569  func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
   570  	if request.ZoneNameOrId == nil {
   571  		err = errors.New("no name or id")
   572  		return
   573  	}
   574  
   575  	records, ok := c.records[*request.ZoneNameOrId]
   576  	if !ok {
   577  		err = errors.New("zone not found")
   578  		return
   579  	}
   580  
   581  	var items []dns.Record
   582  	for _, v := range records {
   583  		items = append(items, v)
   584  	}
   585  
   586  	response.Items = items
   587  	return
   588  }
   589  
   590  func ociRecordKey(rType, domain string) string {
   591  	return rType + "/" + domain
   592  }
   593  
   594  func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
   595  	if request.ZoneNameOrId == nil {
   596  		err = errors.New("no name or id")
   597  		return
   598  	}
   599  
   600  	records, ok := c.records[*request.ZoneNameOrId]
   601  	if !ok {
   602  		err = errors.New("zone not found")
   603  		return
   604  	}
   605  
   606  	// Ensure that ADD operations occur after REMOVE.
   607  	sort.Slice(request.Items, func(i, j int) bool {
   608  		return request.Items[i].Operation > request.Items[j].Operation
   609  	})
   610  
   611  	for _, op := range request.Items {
   612  		k := ociRecordKey(*op.Rtype, *op.Domain)
   613  		switch op.Operation {
   614  		case dns.RecordOperationOperationAdd:
   615  			records[k] = dns.Record{
   616  				Domain: op.Domain,
   617  				Rtype:  op.Rtype,
   618  				Rdata:  op.Rdata,
   619  				Ttl:    op.Ttl,
   620  			}
   621  		case dns.RecordOperationOperationRemove:
   622  			delete(records, k)
   623  		default:
   624  			err = errors.Errorf("unsupported operation %q", op.Operation)
   625  			return
   626  		}
   627  	}
   628  	return
   629  }
   630  
   631  // TestMutableMockOCIDNSClient exists because one must always test one's tests
   632  // right...?
   633  func TestMutableMockOCIDNSClient(t *testing.T) {
   634  	zones := []dns.ZoneSummary{{
   635  		Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   636  		Name: common.String("foo.com"),
   637  	}}
   638  	records := map[string][]dns.Record{
   639  		"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
   640  			Domain: common.String("foo.foo.com"),
   641  			Rdata:  common.String("127.0.0.1"),
   642  			Rtype:  common.String(endpoint.RecordTypeA),
   643  			Ttl:    common.Int(ociRecordTTL),
   644  		}, {
   645  			Domain: common.String("foo.foo.com"),
   646  			Rdata:  common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   647  			Rtype:  common.String(endpoint.RecordTypeTXT),
   648  			Ttl:    common.Int(ociRecordTTL),
   649  		}},
   650  	}
   651  	client := newMutableMockOCIDNSClient(zones, records)
   652  
   653  	// First ListZones.
   654  	zonesResponse, err := client.ListZones(context.Background(), dns.ListZonesRequest{})
   655  	require.NoError(t, err)
   656  	require.Len(t, zonesResponse.Items, 1)
   657  	require.Equal(t, zonesResponse.Items, zones)
   658  
   659  	// GetZoneRecords for that zone.
   660  	recordsResponse, err := client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
   661  		ZoneNameOrId: zones[0].Id,
   662  	})
   663  	require.NoError(t, err)
   664  	require.Len(t, recordsResponse.Items, 2)
   665  	require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
   666  
   667  	// Remove the A record.
   668  	_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
   669  		ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   670  		PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
   671  			Items: []dns.RecordOperation{{
   672  				Domain:    common.String("foo.foo.com"),
   673  				Rdata:     common.String("127.0.0.1"),
   674  				Rtype:     common.String("A"),
   675  				Ttl:       common.Int(300),
   676  				Operation: dns.RecordOperationOperationRemove,
   677  			}},
   678  		},
   679  	})
   680  	require.NoError(t, err)
   681  
   682  	// GetZoneRecords again and check the A record was removed.
   683  	recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
   684  		ZoneNameOrId: zones[0].Id,
   685  	})
   686  	require.NoError(t, err)
   687  	require.Len(t, recordsResponse.Items, 1)
   688  	require.Equal(t, recordsResponse.Items[0], records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"][1])
   689  
   690  	// Add the A record back.
   691  	_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
   692  		ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   693  		PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
   694  			Items: []dns.RecordOperation{{
   695  				Domain:    common.String("foo.foo.com"),
   696  				Rdata:     common.String("127.0.0.1"),
   697  				Rtype:     common.String("A"),
   698  				Ttl:       common.Int(300),
   699  				Operation: dns.RecordOperationOperationAdd,
   700  			}},
   701  		},
   702  	})
   703  	require.NoError(t, err)
   704  
   705  	// GetZoneRecords and check we're back in the original state
   706  	recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
   707  		ZoneNameOrId: zones[0].Id,
   708  	})
   709  	require.NoError(t, err)
   710  	require.Len(t, recordsResponse.Items, 2)
   711  	require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
   712  }
   713  
   714  func TestOCIApplyChanges(t *testing.T) {
   715  	testCases := []struct {
   716  		name              string
   717  		zones             []dns.ZoneSummary
   718  		records           map[string][]dns.Record
   719  		changes           *plan.Changes
   720  		dryRun            bool
   721  		err               error
   722  		expectedEndpoints []*endpoint.Endpoint
   723  	}{
   724  		{
   725  			name: "add",
   726  			zones: []dns.ZoneSummary{{
   727  				Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   728  				Name: common.String("foo.com"),
   729  			}},
   730  			changes: &plan.Changes{
   731  				Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   732  					"foo.foo.com",
   733  					endpoint.RecordTypeA,
   734  					endpoint.TTL(ociRecordTTL),
   735  					"127.0.0.1",
   736  				)},
   737  			},
   738  			expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   739  				"foo.foo.com",
   740  				endpoint.RecordTypeA,
   741  				endpoint.TTL(ociRecordTTL),
   742  				"127.0.0.1",
   743  			)},
   744  		}, {
   745  			name: "remove",
   746  			zones: []dns.ZoneSummary{{
   747  				Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   748  				Name: common.String("foo.com"),
   749  			}},
   750  			records: map[string][]dns.Record{
   751  				"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
   752  					Domain: common.String("foo.foo.com"),
   753  					Rdata:  common.String("127.0.0.1"),
   754  					Rtype:  common.String(endpoint.RecordTypeA),
   755  					Ttl:    common.Int(ociRecordTTL),
   756  				}, {
   757  					Domain: common.String("foo.foo.com"),
   758  					Rdata:  common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
   759  					Rtype:  common.String(endpoint.RecordTypeTXT),
   760  					Ttl:    common.Int(ociRecordTTL),
   761  				}},
   762  			},
   763  			changes: &plan.Changes{
   764  				Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   765  					"foo.foo.com",
   766  					endpoint.RecordTypeTXT,
   767  					endpoint.TTL(ociRecordTTL),
   768  					"127.0.0.1",
   769  				)},
   770  			},
   771  			expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   772  				"foo.foo.com",
   773  				endpoint.RecordTypeA,
   774  				endpoint.TTL(ociRecordTTL),
   775  				"127.0.0.1",
   776  			)},
   777  		}, {
   778  			name: "update",
   779  			zones: []dns.ZoneSummary{{
   780  				Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   781  				Name: common.String("foo.com"),
   782  			}},
   783  			records: map[string][]dns.Record{
   784  				"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
   785  					Domain: common.String("foo.foo.com"),
   786  					Rdata:  common.String("127.0.0.1"),
   787  					Rtype:  common.String(endpoint.RecordTypeA),
   788  					Ttl:    common.Int(ociRecordTTL),
   789  				}},
   790  			},
   791  			changes: &plan.Changes{
   792  				UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   793  					"foo.foo.com",
   794  					endpoint.RecordTypeA,
   795  					endpoint.TTL(ociRecordTTL),
   796  					"127.0.0.1",
   797  				)},
   798  				UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   799  					"foo.foo.com",
   800  					endpoint.RecordTypeA,
   801  					endpoint.TTL(ociRecordTTL),
   802  					"10.0.0.1",
   803  				)},
   804  			},
   805  			expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   806  				"foo.foo.com",
   807  				endpoint.RecordTypeA,
   808  				endpoint.TTL(ociRecordTTL),
   809  				"10.0.0.1",
   810  			)},
   811  		}, {
   812  			name: "dry_run_no_changes",
   813  			zones: []dns.ZoneSummary{{
   814  				Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   815  				Name: common.String("foo.com"),
   816  			}},
   817  			records: map[string][]dns.Record{
   818  				"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
   819  					Domain: common.String("foo.foo.com"),
   820  					Rdata:  common.String("127.0.0.1"),
   821  					Rtype:  common.String(endpoint.RecordTypeA),
   822  					Ttl:    common.Int(ociRecordTTL),
   823  				}},
   824  			},
   825  			changes: &plan.Changes{
   826  				Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   827  					"foo.foo.com",
   828  					endpoint.RecordTypeA,
   829  					endpoint.TTL(ociRecordTTL),
   830  					"127.0.0.1",
   831  				)},
   832  			},
   833  			dryRun: true,
   834  			expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   835  				"foo.foo.com",
   836  				endpoint.RecordTypeA,
   837  				endpoint.TTL(ociRecordTTL),
   838  				"127.0.0.1",
   839  			)},
   840  		}, {
   841  			name: "add_remove_update",
   842  			zones: []dns.ZoneSummary{{
   843  				Id:   common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
   844  				Name: common.String("foo.com"),
   845  			}},
   846  			records: map[string][]dns.Record{
   847  				"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
   848  					Domain: common.String("foo.foo.com"),
   849  					Rdata:  common.String("127.0.0.1"),
   850  					Rtype:  common.String(endpoint.RecordTypeA),
   851  					Ttl:    common.Int(ociRecordTTL),
   852  				}, {
   853  					Domain: common.String("bar.foo.com"),
   854  					Rdata:  common.String("bar.com."),
   855  					Rtype:  common.String(endpoint.RecordTypeCNAME),
   856  					Ttl:    common.Int(ociRecordTTL),
   857  				}},
   858  			},
   859  			changes: &plan.Changes{
   860  				Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   861  					"foo.foo.com",
   862  					endpoint.RecordTypeA,
   863  					endpoint.TTL(ociRecordTTL),
   864  					"baz.com.",
   865  				)},
   866  				UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   867  					"bar.foo.com",
   868  					endpoint.RecordTypeCNAME,
   869  					endpoint.TTL(ociRecordTTL),
   870  					"baz.com.",
   871  				)},
   872  				UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   873  					"bar.foo.com",
   874  					endpoint.RecordTypeCNAME,
   875  					endpoint.TTL(ociRecordTTL),
   876  					"foo.bar.com.",
   877  				)},
   878  				Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
   879  					"baz.foo.com",
   880  					endpoint.RecordTypeA,
   881  					endpoint.TTL(ociRecordTTL),
   882  					"127.0.0.1",
   883  				)},
   884  			},
   885  			expectedEndpoints: []*endpoint.Endpoint{
   886  				endpoint.NewEndpointWithTTL(
   887  					"bar.foo.com",
   888  					endpoint.RecordTypeCNAME,
   889  					endpoint.TTL(ociRecordTTL),
   890  					"foo.bar.com.",
   891  				),
   892  				endpoint.NewEndpointWithTTL(
   893  					"baz.foo.com",
   894  					endpoint.RecordTypeA,
   895  					endpoint.TTL(ociRecordTTL),
   896  					"127.0.0.1"),
   897  			},
   898  		},
   899  	}
   900  
   901  	for _, tc := range testCases {
   902  		t.Run(tc.name, func(t *testing.T) {
   903  			client := newMutableMockOCIDNSClient(tc.zones, tc.records)
   904  			provider := newOCIProvider(
   905  				client,
   906  				endpoint.NewDomainFilter([]string{""}),
   907  				provider.NewZoneIDFilter([]string{""}),
   908  				"",
   909  				tc.dryRun,
   910  			)
   911  
   912  			ctx := context.Background()
   913  			err := provider.ApplyChanges(ctx, tc.changes)
   914  			require.Equal(t, tc.err, err)
   915  			endpoints, err := provider.Records(ctx)
   916  			require.NoError(t, err)
   917  			require.ElementsMatch(t, tc.expectedEndpoints, endpoints)
   918  		})
   919  	}
   920  }