diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 428a7961b..f4c9405da 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -30,6 +30,8 @@ import ( const ( // RecordTypeA is a RecordType enum value RecordTypeA = "A" + // RecordTypeAAAA is a RecordType enum value + RecordTypeAAAA = "AAAA" // RecordTypeCNAME is a RecordType enum value RecordTypeCNAME = "CNAME" // RecordTypeTXT is a RecordType enum value diff --git a/plan/plan.go b/plan/plan.go index d7bac7090..9eefeb207 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -78,12 +78,12 @@ bar.com | | [->191.1.1.1, ->190.1.1.1] | = create (bar.com -> 1 "=", i.e. result of calculation relies on supplied ConflictResolver */ type planTable struct { - rows map[string]map[string]*planTableRow + rows map[string]map[string]map[string]*planTableRow resolver ConflictResolver } func newPlanTable() planTable { // TODO: make resolver configurable - return planTable{map[string]map[string]*planTableRow{}, PerResource{}} + return planTable{map[string]map[string]map[string]*planTableRow{}, PerResource{}} } // planTableRow @@ -101,23 +101,29 @@ func (t planTableRow) String() string { func (t planTable) addCurrent(e *endpoint.Endpoint) { dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) + t.rows[dnsName] = make(map[string]map[string]*planTableRow) } if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} + t.rows[dnsName][e.SetIdentifier] = make(map[string]*planTableRow) } - t.rows[dnsName][e.SetIdentifier].current = e + if _, ok := t.rows[e.SetIdentifier][e.RecordType]; !ok { + t.rows[dnsName][e.SetIdentifier][e.RecordType] = &planTableRow{} + } + t.rows[dnsName][e.SetIdentifier][e.RecordType].current = e } func (t planTable) addCandidate(e *endpoint.Endpoint) { dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) + t.rows[dnsName] = make(map[string]map[string]*planTableRow) } if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} + t.rows[dnsName][e.SetIdentifier] = make(map[string]*planTableRow) } - t.rows[dnsName][e.SetIdentifier].candidates = append(t.rows[dnsName][e.SetIdentifier].candidates, e) + if _, ok := t.rows[dnsName][e.SetIdentifier][e.RecordType]; !ok { + t.rows[dnsName][e.SetIdentifier][e.RecordType] = &planTableRow{} + } + t.rows[dnsName][e.SetIdentifier][e.RecordType].candidates = append(t.rows[dnsName][e.SetIdentifier][e.RecordType].candidates, e) } func (c *Changes) HasChanges() bool { @@ -147,24 +153,26 @@ func (p *Plan) Calculate() *Plan { changes := &Changes{} for _, topRow := range t.rows { - for _, row := range topRow { - if row.current == nil { // dns name not taken - changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) - } - if row.current != nil && len(row.candidates) == 0 { - changes.Delete = append(changes.Delete, row.current) - } - - // TODO: allows record type change, which might not be supported by all dns providers - if row.current != nil && len(row.candidates) > 0 { // dns name is taken - update := t.resolver.ResolveUpdate(row.current, row.candidates) - // compare "update" to "current" to figure out if actual update is required - if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { - inheritOwner(row.current, update) - changes.UpdateNew = append(changes.UpdateNew, update) - changes.UpdateOld = append(changes.UpdateOld, row.current) + for _, midRow := range topRow { + for _, row := range midRow { + if row.current == nil { // dns name not taken + changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) + } + if row.current != nil && len(row.candidates) == 0 { + changes.Delete = append(changes.Delete, row.current) + } + + // TODO: allows record type change, which might not be supported by all dns providers + if row.current != nil && len(row.candidates) > 0 { // dns name is taken + update := t.resolver.ResolveUpdate(row.current, row.candidates) + // compare "update" to "current" to figure out if actual update is required + if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { + inheritOwner(row.current, update) + changes.UpdateNew = append(changes.UpdateNew, update) + changes.UpdateOld = append(changes.UpdateOld, row.current) + } + continue } - continue } } } @@ -181,7 +189,7 @@ func (p *Plan) Calculate() *Plan { Current: p.Current, Desired: p.Desired, Changes: changes, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, } return plan diff --git a/plan/plan_test.go b/plan/plan_test.go index d34c93249..2b107d0e8 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -35,6 +35,9 @@ type PlanTestSuite struct { fooV2CnameNoLabel *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint fooA5 *endpoint.Endpoint + fooAAAA *endpoint.Endpoint + dsA *endpoint.Endpoint + dsAAAA *endpoint.Endpoint bar127A *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint @@ -106,6 +109,30 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/foo-5", }, } + suite.fooAAAA = &endpoint.Endpoint{ + DNSName: "foo", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/foo-AAAA", + }, + } + suite.dsA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "A", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds", + }, + } + suite.dsAAAA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds-AAAAA", + }, + } suite.bar127A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, @@ -438,9 +465,9 @@ func (suite *PlanTestSuite) TestIdempotency() { func (suite *PlanTestSuite) TestDifferentTypes() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooA5} - expectedCreate := []*endpoint.Endpoint{} + expectedCreate := []*endpoint.Endpoint{suite.fooA5} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname} - expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5} + expectedUpdateNew := []*endpoint.Endpoint{suite.fooV2Cname} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ @@ -544,52 +571,6 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() { validateEntries(suite.T(), changes.Delete, expectedDelete) } -// TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() { - current := []*endpoint.Endpoint{suite.fooV3CnameSameResource, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{suite.fooV3CnameSameResource} - expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - -// TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() { - current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{} - expectedUpdateNew := []*endpoint.Endpoint{} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() { current := []*endpoint.Endpoint{suite.multiple1} desired := []*endpoint.Endpoint{suite.multiple2, suite.multiple3} @@ -695,6 +676,39 @@ func (suite *PlanTestSuite) TestMissing() { validateEntries(suite.T(), changes.Create, expectedCreate) } +func (suite *PlanTestSuite) TestAAAARecords() { + + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.fooAAAA} + expectedCreate := []*endpoint.Endpoint{suite.fooAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + +func (suite *PlanTestSuite) TestDualStackRecords() { + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + expectedCreate := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + func TestPlan(t *testing.T) { suite.Run(t, new(PlanTestSuite)) }