From d5afada5c20aed745e79aca1e516ddb2d265bc4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Thu, 30 Jun 2022 23:35:30 +0100 Subject: [PATCH] Update all the test in the civo provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro J. Nuñez Madrazo --- go.mod | 6 +- go.sum | 17 +- provider/civo/civo.go | 395 ++++++++++++++++++++----------------- provider/civo/civo_test.go | 371 ++++++++++++++++++++++++++++++++-- 4 files changed, 582 insertions(+), 207 deletions(-) diff --git a/go.mod b/go.mod index 1427ec9c5..b123fed7d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.61.1483 github.com/aws/aws-sdk-go v1.42.52 github.com/bodgit/tsig v0.0.2 + github.com/civo/civogo v0.2.90 github.com/cloudflare/cloudflare-go v0.25.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.6.0 @@ -52,7 +53,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 go.uber.org/ratelimit v0.2.0 - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f + golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c google.golang.org/api v0.66.0 @@ -81,7 +82,6 @@ require ( github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect - github.com/civo/civogo v0.2.66 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -140,7 +140,7 @@ require ( go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 7e3367e1d..5a23c7d09 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/civo/civogo v0.2.66 h1:4TXj2DmTg0mhKdRkxy3tbDTiAP+EqPZ50v3iuB58pDE= -github.com/civo/civogo v0.2.66/go.mod h1:SR0ZOhABfQHjgNQE3UyfX4gaYsrfslkPFRFMx5P29rg= +github.com/civo/civogo v0.2.89 h1:+7LZoOafk5qRHlKxKJR6GSmODj6AEgGjOljmrP2+YsQ= +github.com/civo/civogo v0.2.89/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= +github.com/civo/civogo v0.2.90 h1:S5jOTAonqeG0p3mqXxbKRUefxHgfM9VYOIcfyVOItcE= +github.com/civo/civogo v0.2.90/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.20.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI= @@ -969,6 +971,7 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -978,8 +981,9 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1440,8 +1444,9 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1573,13 +1578,15 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/provider/civo/civo.go b/provider/civo/civo.go index 117d533fa..db55a56e9 100644 --- a/provider/civo/civo.go +++ b/provider/civo/civo.go @@ -40,15 +40,15 @@ type CivoProvider struct { // CivoChanges All API calls calculated from the plan type CivoChanges struct { - Creates []CivoChangeCreate - Deletes []CivoChangeDelete - Updates []CivoChangeUpdate + Creates []*CivoChangeCreate + Deletes []*CivoChangeDelete + Updates []*CivoChangeUpdate } // CivoChangeCreate Civo Domain Record Creates type CivoChangeCreate struct { Domain civogo.DNSDomain - Options civogo.DNSRecordConfig + Options *civogo.DNSRecordConfig } // CivoChangeUpdate Civo Domain Record Updates @@ -171,7 +171,7 @@ func (p *CivoProvider) submitChanges(ctx context.Context, changes CivoChanges) e if p.DryRun { log.WithFields(logFields).Info("Would create record.") - } else if _, err := p.Client.CreateDNSRecord(change.Domain.ID, &change.Options); err != nil { + } else if _, err := p.Client.CreateDNSRecord(change.Domain.ID, change.Options); err != nil { log.WithFields(logFields).Errorf( "Failed to Create record: %v", err, @@ -226,8 +226,205 @@ func (p *CivoProvider) submitChanges(ctx context.Context, changes CivoChanges) e return nil } +// processCreateActions return a list of changes to create records. +func processCreateActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, createsByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, creates := range createsByZone { + zone := zonesByID[zoneID] + + if len(creates) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Debug("Skipping Zone, no creates found.") + continue + } + + records := recordsByZoneID[zoneID] + + // Generate Create + for _, ep := range creates { + matchedRecords := getRecordID(records, zone, *ep) + + if len(matchedRecords) != 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + "dnsName": ep.DNSName, + "recordType": ep.RecordType, + }).Warn("Records found which should not exist") + } + + recordType, err := convertRecordType(ep.RecordType) + if err != nil { + return err + } + + for _, target := range ep.Targets { + civoChange.Creates = append(civoChange.Creates, &CivoChangeCreate{ + Domain: zone, + Options: &civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + } + } + } + + return nil +} + +// processUpdateActions return a list of changes to update records. +func processUpdateActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, updatesByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, updates := range updatesByZone { + zone := zonesByID[zoneID] + + if len(updates) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Debug("Skipping Zone, no updates found.") + continue + } + + records := recordsByZoneID[zoneID] + + for _, ep := range updates { + matchedRecords := getRecordID(records, zone, *ep) + + if len(matchedRecords) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + }).Warn("Update Records not found.") + } + + recordType, err := convertRecordType(ep.RecordType) + + if err != nil { + return err + } + + matchedRecordsByTarget := make(map[string]civogo.DNSRecord) + + for _, record := range matchedRecords { + matchedRecordsByTarget[record.Value] = record + } + + for _, target := range ep.Targets { + if record, ok := matchedRecordsByTarget[target]; ok { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + "target": target, + }).Warn("Updating Existing Target") + + civoChange.Updates = append(civoChange.Updates, &CivoChangeUpdate{ + Domain: zone, + DomainRecord: record, + Options: civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + + delete(matchedRecordsByTarget, target) + } else { + // Record did not previously exist, create new 'target' + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + "target": target, + }).Warn("Creating New Target") + + civoChange.Creates = append(civoChange.Creates, &CivoChangeCreate{ + Domain: zone, + Options: &civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + } + } + + // Any remaining records have been removed, delete them + for _, record := range matchedRecordsByTarget { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "recordType": ep.RecordType, + "target": record.Value, + }).Warn("Deleting target") + + civoChange.Deletes = append(civoChange.Deletes, &CivoChangeDelete{ + Domain: zone, + DomainRecord: record, + }) + } + + } + } + + return nil +} + +// processDeleteActions return a list of changes to delete records. +func processDeleteActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, deletesByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, deletes := range deletesByZone { + zone := zonesByID[zoneID] + + if len(deletes) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Debug("Skipping Zone, no deletes found.") + continue + } + + records := recordsByZoneID[zoneID] + + for _, ep := range deletes { + matchedRecords := getRecordID(records, zone, *ep) + + if len(matchedRecords) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + }).Warn("Records to Delete not found.") + } + + for _, record := range matchedRecords { + civoChange.Deletes = append(civoChange.Deletes, &CivoChangeDelete{ + Domain: zone, + DomainRecord: record, + }) + } + } + } + return nil +} + + // ApplyChanges applies a given set of changes in a given zone. func (p *CivoProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + var civoChange CivoChanges recordsByZoneID := make(map[string][]civogo.DNSRecord) zones, err := p.fetchZones(ctx) @@ -260,187 +457,29 @@ func (p *CivoProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew) deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete) - var civoCreates []CivoChangeCreate - var civoUpdates []CivoChangeUpdate - var civoDeletes []CivoChangeDelete - // Generate Creates - for zoneID, creates := range createsByZone { - zone := zonesByID[zoneID] - - if len(creates) == 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "zoneName": zone.Name, - }).Debug("Skipping Zone, no creates found.") - continue - } - - records := recordsByZoneID[zoneID] - - // Generate Create - for _, ep := range creates { - matchedRecords := getRecordID(records, zone, ep) - - if len(matchedRecords) != 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "zoneName": zone.Name, - "dnsName": ep.DNSName, - "recordType": ep.RecordType, - }).Warn("Records found which should not exist") - } - - recordType, err := convertRecordType(ep.RecordType) - if err != nil { - return err - } - - for _, target := range ep.Targets { - civoCreates = append(civoCreates, CivoChangeCreate{ - Domain: zone, - Options: civogo.DNSRecordConfig{ - Value: target, - Name: getStrippedRecordName(zone, ep), - Type: recordType, - Priority: 0, - TTL: int(ep.RecordTTL), - }, - }) - } - } + err = processCreateActions(zonesByID, recordsByZoneID, createsByZone, &civoChange) + if err != nil { + return err } // Generate Updates - for zoneID, updates := range updatesByZone { - zone := zonesByID[zoneID] - - if len(updates) == 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "zoneName": zone.Name, - }).Debug("Skipping Zone, no updates found.") - continue - } - - records := recordsByZoneID[zoneID] - - for _, ep := range updates { - matchedRecords := getRecordID(records, zone, ep) - - if len(matchedRecords) == 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "dnsName": ep.DNSName, - "zoneName": zone.Name, - "recordType": ep.RecordType, - }).Warn("Update Records not found.") - } - - recordType, err := convertRecordType(ep.RecordType) - - if err != nil { - return err - } - - matchedRecordsByTarget := make(map[string]civogo.DNSRecord) - - for _, record := range matchedRecords { - matchedRecordsByTarget[record.DNSDomainID] = record - } - - for _, target := range ep.Targets { - if record, ok := matchedRecordsByTarget[target]; ok { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "dnsName": ep.DNSName, - "zoneName": zone.Name, - "recordType": ep.RecordType, - "target": target, - }).Warn("Updating Existing Target") - - civoUpdates = append(civoUpdates, CivoChangeUpdate{ - Domain: zone, - DomainRecord: record, - Options: civogo.DNSRecordConfig{ - Value: target, - Name: getStrippedRecordName(zone, ep), - Type: recordType, - Priority: 0, - TTL: int(ep.RecordTTL), - }, - }) - - delete(matchedRecordsByTarget, target) - } else { - // Record did not previously exist, create new 'target' - log.WithFields(log.Fields{ - "zoneID": zoneID, - "dnsName": ep.DNSName, - "zoneName": zone.Name, - "recordType": ep.RecordType, - "target": target, - }).Warn("Creating New Target") - - civoCreates = append(civoCreates, CivoChangeCreate{ - Domain: zone, - Options: civogo.DNSRecordConfig{ - Value: target, - Name: getStrippedRecordName(zone, ep), - Type: recordType, - Priority: 0, - TTL: int(ep.RecordTTL), - }, - }) - } - } - } + err = processUpdateActions(zonesByID, recordsByZoneID, updatesByZone, &civoChange) + if err != nil { + return err } - + // Generate Deletes - for zoneID, deletes := range deletesByZone { - zone := zonesByID[zoneID] + err = processDeleteActions(zonesByID, recordsByZoneID, deletesByZone, &civoChange) + if err != nil { + return err + } - if len(deletes) == 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "zoneName": zone.Name, - }).Debug("Skipping Zone, no deletes found.") - continue - } - - records := recordsByZoneID[zoneID] - - for _, ep := range deletes { - matchedRecords := getRecordID(records, zone, ep) - - if len(matchedRecords) == 0 { - log.WithFields(log.Fields{ - "zoneID": zoneID, - "dnsName": ep.DNSName, - "zoneName": zone.Name, - "recordType": ep.RecordType, - }).Warn("Records to Delete not found.") - } - - for _, record := range matchedRecords { - civoDeletes = append(civoDeletes, CivoChangeDelete{ - Domain: zone, - DomainRecord: record, - }) - } - } - } - - return p.submitChanges(ctx, CivoChanges{ - Creates: civoCreates, - Deletes: civoDeletes, - Updates: civoUpdates, - }) + return p.submitChanges(ctx, civoChange) } -func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]endpoint.Endpoint { - endpointsByZone := make(map[string][]endpoint.Endpoint) +func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint { + endpointsByZone := make(map[string][]*endpoint.Endpoint) for _, ep := range endpoints { zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName) @@ -448,7 +487,7 @@ func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName) continue } - endpointsByZone[zoneID] = append(endpointsByZone[zoneID], *ep) + endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep) } return endpointsByZone diff --git a/provider/civo/civo_test.go b/provider/civo/civo_test.go index ca53ceef2..fc1224f28 100644 --- a/provider/civo/civo_test.go +++ b/provider/civo/civo_test.go @@ -18,18 +18,22 @@ package civo import ( "context" + "fmt" "os" "reflect" "strings" "testing" "github.com/civo/civogo" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gotest.tools/assert" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" ) +type mockCivoClient struct{} + func TestNewCivoProvider(t *testing.T) { _ = os.Setenv("CIVO_TOKEN", "xxxxxxxxxxxxxxx") _, err := NewCivoProvider(endpoint.NewDomainFilter([]string{"test.civo.com"}), true) @@ -98,34 +102,295 @@ func TestCivoProviderRecords(t *testing.T) { } -func TestCivoProviderApplyChanges(t *testing.T) { - changes := &plan.Changes{} - client, server, _ := civogo.NewClientForTesting(map[string]string{ - "/v2/dns/12345/records": `[ - {"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600}, - {"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600} - ]`, - "/v2/dns": `[ - {"id": "12345", "account_id": "1", "name": "example.com"}, - {"id": "12346", "account_id": "1", "name": "example.net"} - ]`, +func TestCivoProcessCreateActions(t *testing.T) { + zoneByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": { + { + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "12.12.12.1", + Type: "A", + TTL: 600, + }, + }, + } + + createsByDomain := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"), + }, + } + + var changes CivoChanges + err := processCreateActions(zoneByID, recordsByZoneID, createsByDomain, &changes) + require.NoError(t, err) + + assert.Equal(t, 2, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 0, len(changes.Deletes)) + + expectedCreates := []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "A", + Name: "foo", + Value: "1.2.3.4", + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "CNAME", + Name: "txt", + Value: "foo.example.com", + }, + }, + } + + if !elementsMatch(t, expectedCreates, changes.Creates) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates)) + } +} + +func TestCivoProcessUpdateActions(t *testing.T) { + zoneByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": { + { + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "1.2.3.4", + Type: "A", + TTL: 600, + }, + { + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Name: "foo", + Value: "foo.example.com", + Type: "CNAME", + TTL: 600, + }, + { + ID: "3", + AccountID: "1", + DNSDomainID: "1", + Name: "bar", + Value: "10.10.10.1", + Type: "A", + TTL: 600, + }, + }, + } + + updatesByDomain := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "10.20.30.40"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeCNAME, "bar.example.com"), + }, + } + + var changes CivoChanges + err := processUpdateActions(zoneByID, recordsByZoneID, updatesByDomain, &changes) + require.NoError(t, err) + + assert.Equal(t, 2, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 2, len(changes.Deletes)) + + expectedUpdate := []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "A", + Name: "txt", + Value: "10.20.30.40", + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "CNAME", + Name: "foo", + Value: "bar.example.com", + }, + }, + } + + if !elementsMatch(t, expectedUpdate, changes.Creates) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedUpdate, changes.Creates)) + } +} + +func TestCivoProcessDeleteAction(t *testing.T) { + zoneByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": { + { + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "1.2.3.4", + Type: "A", + TTL: 600, + }, + { + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Name: "foo", + Value: "5.6.7.8", + Type: "A", + TTL: 600, + }, + { + ID: "3", + AccountID: "1", + DNSDomainID: "1", + Name: "bar", + Value: "10.10.10.1", + Type: "A", + TTL: 600, + }, + }, + } + + deleteByDomain := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "5.6.7.8"), + }, + } + + var changes CivoChanges + err := processDeleteActions(zoneByID, recordsByZoneID, deleteByDomain, &changes) + require.NoError(t, err) + + assert.Equal(t, 0, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 2, len(changes.Deletes)) + + expectedDelete := []*CivoChangeDelete{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "1.2.3.4", + Type: "A", + TTL: 600, + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Type: "A", + Name: "foo", + Value: "5.6.7.8", + TTL: 600, + }, + }, + } + + if !elementsMatch(t, expectedDelete, changes.Deletes) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedDelete, changes.Deletes)) + } +} + +func TestCivoApplyChanges(t *testing.T) { + client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{ + { + Method: "GET", + Value: []civogo.ValueAdvanceClientForTesting{ + { + RequestBody: "", + URL: "/v2/dns", + ResponseBody: `[{"id": "12345", "account_id": "1", "name": "example.com"}]`, + }, + { + RequestBody: "", + URL: "/v2/dns/12345/records", + ResponseBody: `[]`, + }, + }, + }, }) defer server.Close() + + changes := &plan.Changes{} provider := &CivoProvider{ Client: *client, } - changes.Create = []*endpoint.Endpoint{ - {DNSName: "test.com", Targets: endpoint.Targets{"target"}}, - {DNSName: "ttl.test.com", Targets: endpoint.Targets{"target"}, RecordTTL: 600}, + {DNSName: "new.ext-dns-test.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "new.ext-dns-test-with-ttl.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100}, } - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 600}} - changes.Delete = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target"}, RecordType: "A"}} + changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target"}}} + changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.de", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target-old"}}} + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType:endpoint.RecordTypeCNAME, RecordTTL: 100}} err := provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } - require.NoError(t, err) } func TestCivoProviderFetchZones(t *testing.T) { @@ -148,7 +413,7 @@ func TestCivoProviderFetchZones(t *testing.T) { if err != nil { t.Fatal(err) } - assert.DeepEqual(t, zones, expected) + assert.ElementsMatch(t, zones, expected) } func TestCivoProviderFetchZonesWithFilter(t *testing.T) { client, server, _ := civogo.NewClientForTesting(map[string]string{ @@ -171,7 +436,7 @@ func TestCivoProviderFetchZonesWithFilter(t *testing.T) { if err != nil { t.Fatal(err) } - assert.DeepEqual(t, expected, actual) + assert.ElementsMatch(t, expected, actual) } func TestCivoProviderFetchRecords(t *testing.T) { @@ -194,7 +459,7 @@ func TestCivoProviderFetchRecords(t *testing.T) { if err != nil { t.Fatal(err) } - assert.DeepEqual(t, expected, actual) + assert.ElementsMatch(t, expected, actual) } func TestCivoGetStrippedRecordName(t *testing.T) { @@ -263,3 +528,67 @@ func TestCivoProviderGetRecordID(t *testing.T) { assert.Equal(t, id[0].ID, record[0].ID) } + +// This function is an adapted copy of the testify package's ElementsMatch function with the +// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to +// other structs. It also ignores ordering when comparing unlike cmp.Equal. +func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if listA == nil && listB == nil { + return true + } else if listA == nil { + return isEmpty(listB) + } else if listB == nil { + return isEmpty(listA) + } + + aKind := reflect.TypeOf(listA).Kind() + bKind := reflect.TypeOf(listB).Kind() + + if aKind != reflect.Array && aKind != reflect.Slice { + return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) + } + + if bKind != reflect.Array && bKind != reflect.Slice { + return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + if aLen != bLen { + return assert.Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...) + } + + // Mark indexes in bValue that we already used + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + if cmp.Equal(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + return assert.Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...) + } + } + + return true +} + +func isEmpty(xs interface{}) bool { + if xs != nil { + objValue := reflect.ValueOf(xs) + return objValue.Len() == 0 + } + return true +}