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 }