1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Refactoring diff package interface (#22)

* initial refactoring of diffing

* making cloudflare and others compile

* gandi and gcloud. no idea if gandi works anymore.

* r53

* namedotcom wasn't working.
This commit is contained in:
Craig Peterson
2017-01-11 12:38:07 -07:00
committed by GitHub
parent 1f8b0a11e0
commit 12f006441b
15 changed files with 322 additions and 391 deletions

View File

@ -66,23 +66,19 @@ type RecordConfig struct {
Metadata map[string]string `json:"meta,omitempty"` Metadata map[string]string `json:"meta,omitempty"`
NameFQDN string `json:"-"` // Must end with ".$origin". See below. NameFQDN string `json:"-"` // Must end with ".$origin". See below.
Priority uint16 `json:"priority,omitempty"` Priority uint16 `json:"priority,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
} }
func (r *RecordConfig) GetName() string { func (r *RecordConfig) String() string {
return r.NameFQDN content := fmt.Sprintf("%s %s %s %d", r.Type, r.NameFQDN, r.Target, r.TTL)
}
func (r *RecordConfig) GetType() string {
return r.Type
}
func (r *RecordConfig) GetContent() string {
return r.Target
}
func (r *RecordConfig) GetComparisionData() string {
mxPrio := ""
if r.Type == "MX" { if r.Type == "MX" {
mxPrio = fmt.Sprintf(" %d ", r.Priority) content += fmt.Sprintf(" priority=%d", r.Priority)
} }
return fmt.Sprintf("%d%s", r.TTL, mxPrio) for k, v := range r.Metadata {
content += fmt.Sprintf(" %s=%s", k, v)
}
return content
} }
/// Convert RecordConfig -> dns.RR. /// Convert RecordConfig -> dns.RR.

View File

@ -200,6 +200,9 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
// Normalize Records. // Normalize Records.
for _, rec := range domain.Records { for _, rec := range domain.Records {
if rec.TTL == 0 {
rec.TTL = models.DefaultTTL
}
// Validate the unmodified inputs: // Validate the unmodified inputs:
if err := validateRecordTypes(rec, domain.Name); err != nil { if err := validateRecordTypes(rec, domain.Name); err != nil {
errs = append(errs, err) errs = append(errs, err)

View File

@ -38,23 +38,8 @@ func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Co
return nil, fmt.Errorf("c.getExistingRecords(%v) failed: %v", dc.Name, err) return nil, fmt.Errorf("c.getExistingRecords(%v) failed: %v", dc.Name, err)
} }
// Read expectedRecords: differ := diff.New(dc)
//expectedRecords := make([]*models.RecordConfig, len(dc.Records)) _, creates, dels, modifications := differ.IncrementalDiff(foundRecords)
expectedRecords := make([]diff.Record, len(dc.Records))
for i, r := range dc.Records {
if r.TTL == 0 {
r.TTL = models.DefaultTTL
}
expectedRecords[i] = r
}
// Convert to []diff.Records and compare:
foundDiffRecords := make([]diff.Record, 0, len(foundRecords))
for _, rec := range foundRecords {
foundDiffRecords = append(foundDiffRecords, rec)
}
_, creates, dels, modifications := diff.IncrementalDiff(foundDiffRecords, expectedRecords)
// NOTE(tlim): This provider does not delete records. If // NOTE(tlim): This provider does not delete records. If
// you need to delete a record, either delete it manually // you need to delete a record, either delete it manually
// or see providers/activedir/doc.md for implementation tips. // or see providers/activedir/doc.md for implementation tips.
@ -65,10 +50,10 @@ func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Co
if dc.KeepUnknown { if dc.KeepUnknown {
break break
} }
corrections = append(corrections, c.deleteRec(dc.Name, del.Existing.(*models.RecordConfig))) corrections = append(corrections, c.deleteRec(dc.Name, del.Existing))
} }
for _, cre := range creates { for _, cre := range creates {
corrections = append(corrections, c.createRec(dc.Name, cre.Desired.(*models.RecordConfig))...) corrections = append(corrections, c.createRec(dc.Name, cre.Desired)...)
} }
for _, m := range modifications { for _, m := range modifications {
corrections = append(corrections, c.modifyRec(dc.Name, m)) corrections = append(corrections, c.modifyRec(dc.Name, m))
@ -298,15 +283,11 @@ func (c *adProvider) createRec(domainname string, rec *models.RecordConfig) []*m
} }
func (c *adProvider) modifyRec(domainname string, m diff.Correlation) *models.Correction { func (c *adProvider) modifyRec(domainname string, m diff.Correlation) *models.Correction {
old, rec := m.Existing, m.Desired
old, rec := m.Existing.(*models.RecordConfig), m.Desired.(*models.RecordConfig)
oldContent := old.GetContent()
newContent := rec.GetContent()
return &models.Correction{ return &models.Correction{
Msg: m.String(), Msg: m.String(),
F: func() error { F: func() error {
return powerShellDoCommand(c.generatePowerShellModify(domainname, rec.Name, rec.Type, oldContent, newContent, old.TTL, rec.TTL)) return powerShellDoCommand(c.generatePowerShellModify(domainname, rec.Name, rec.Type, old.Target, rec.Target, old.TTL, rec.TTL))
}, },
} }
} }

View File

@ -158,14 +158,6 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
// Default SOA record. If we see one in the zone, this will be replaced. // Default SOA record. If we see one in the zone, this will be replaced.
soa_rec := makeDefaultSOA(c.Default_Soa, dc.Name) soa_rec := makeDefaultSOA(c.Default_Soa, dc.Name)
// Read expectedRecords:
expectedRecords := make([]*models.RecordConfig, 0, len(dc.Records))
for _, r := range dc.Records {
if r.TTL == 0 {
r.TTL = models.DefaultTTL
}
expectedRecords = append(expectedRecords, r)
}
// Read foundRecords: // Read foundRecords:
foundRecords := make([]*models.RecordConfig, 0) foundRecords := make([]*models.RecordConfig, 0)
var old_serial, new_serial uint32 var old_serial, new_serial uint32
@ -198,22 +190,13 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
} }
} }
// Add SOA record: // Add SOA record to expected set:
if !dc.HasRecordTypeName("SOA", "@") { if !dc.HasRecordTypeName("SOA", "@") {
expectedRecords = append(expectedRecords, soa_rec)
dc.Records = append(dc.Records, soa_rec) dc.Records = append(dc.Records, soa_rec)
} }
// Convert to []diff.Records and compare: differ := diff.New(dc)
foundDiffRecords := make([]diff.Record, len(foundRecords)) _, create, del, mod := differ.IncrementalDiff(foundRecords)
for i := range foundRecords {
foundDiffRecords[i] = foundRecords[i]
}
expectedDiffRecords := make([]diff.Record, len(expectedRecords))
for i := range expectedRecords {
expectedDiffRecords[i] = expectedRecords[i]
}
_, create, del, mod := diff.IncrementalDiff(foundDiffRecords, expectedDiffRecords)
// Print a list of changes. Generate an actual change that is the zone // Print a list of changes. Generate an actual change that is the zone
changes := false changes := false

View File

@ -31,7 +31,6 @@ Domain level metadata availible:
Provider level metadata availible: Provider level metadata availible:
- ip_conversions - ip_conversions
- secret_ips
*/ */
type CloudflareApi struct { type CloudflareApi struct {
@ -40,7 +39,6 @@ type CloudflareApi struct {
domainIndex map[string]string domainIndex map[string]string
nameservers map[string][]string nameservers map[string][]string
ipConversions []transform.IpConversion ipConversions []transform.IpConversion
secretIPs []net.IP
ignoredLabels []string ignoredLabels []string
} }
@ -79,7 +77,7 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
if err := c.preprocessConfig(dc); err != nil { if err := c.preprocessConfig(dc); err != nil {
return nil, err return nil, err
} }
records, err := c.getRecordsForDomain(id) records, err := c.getRecordsForDomain(id, dc.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -87,50 +85,52 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
for i := len(records) - 1; i >= 0; i-- { for i := len(records) - 1; i >= 0; i-- {
rec := records[i] rec := records[i]
// Delete ignore labels // Delete ignore labels
if labelMatches(dnsutil.TrimDomainName(rec.(*cfRecord).Name, dc.Name), c.ignoredLabels) { if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
fmt.Printf("ignored_label: %s\n", rec.(*cfRecord).Name) fmt.Printf("ignored_label: %s\n", rec.Original.(*cfRecord).Name)
records = append(records[:i], records[i+1:]...) records = append(records[:i], records[i+1:]...)
} }
//normalize cname,mx,ns records with dots to be consistent with our config format.
t := rec.(*cfRecord).Type
if t == "CNAME" || t == "MX" || t == "NS" {
rec.(*cfRecord).Content = dnsutil.AddOrigin(rec.(*cfRecord).Content+".", dc.Name)
} }
}
expectedRecords := make([]diff.Record, 0, len(dc.Records))
for _, rec := range dc.Records { for _, rec := range dc.Records {
if labelMatches(rec.Name, c.ignoredLabels) { if labelMatches(rec.Name, c.ignoredLabels) {
log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.Name, c.ignoredLabels) log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.Name, c.ignoredLabels)
// Since we log.Fatalf, we don't need to be clean here. // Since we log.Fatalf, we don't need to be clean here.
} }
}
checkNSModifications(dc)
differ := diff.New(dc, getProxyMetadata)
_, create, del, mod := differ.IncrementalDiff(records)
corrections := []*models.Correction{}
for _, d := range del {
corrections = append(corrections, c.deleteRec(d.Existing.Original.(*cfRecord), id))
}
for _, d := range create {
corrections = append(corrections, c.createRec(d.Desired, id)...)
}
for _, d := range mod {
e, rec := d.Existing.Original.(*cfRecord), d.Desired
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
corrections = append(corrections, &models.Correction{
Msg: d.String(),
F: func() error { return c.modifyRecord(id, e.ID, proxy, rec) },
})
}
return corrections, nil
}
func checkNSModifications(dc *models.DomainConfig) {
newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.Type == "NS" && rec.NameFQDN == dc.Name { if rec.Type == "NS" && rec.NameFQDN == dc.Name {
if !strings.HasSuffix(rec.Target, ".ns.cloudflare.com.") { if !strings.HasSuffix(rec.Target, ".ns.cloudflare.com.") {
log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.Target) log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.Target)
} }
continue continue
} }
expectedRecords = append(expectedRecords, recordWrapper{rec}) newList = append(newList, rec)
} }
_, create, del, mod := diff.IncrementalDiff(records, expectedRecords) dc.Records = newList
corrections := []*models.Correction{}
for _, d := range del {
corrections = append(corrections, c.deleteRec(d.Existing.(*cfRecord), id))
}
for _, d := range create {
corrections = append(corrections, c.createRec(d.Desired.(recordWrapper).RecordConfig, id)...)
}
for _, d := range mod {
e, rec := d.Existing.(*cfRecord), d.Desired.(recordWrapper)
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
corrections = append(corrections, &models.Correction{
Msg: fmt.Sprintf("MODIFY record %s %s: (%s %s) => (%s %s)", rec.Name, rec.Type, e.Content, e.GetComparisionData(), rec.Target, rec.GetComparisionData()),
F: func() error { return c.modifyRecord(id, e.ID, proxy, rec.RecordConfig) },
})
}
return corrections, nil
} }
const ( const (
@ -138,7 +138,6 @@ const (
metaProxyDefault = metaProxy + "_default" metaProxyDefault = metaProxy + "_default"
metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means. metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means.
metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules. metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules.
metaSecretIPs = "secret_ips" // TODO(tlim): Rename to obscured_cidrs.
) )
func checkProxyVal(v string) (string, error) { func checkProxyVal(v string) (string, error) {
@ -167,6 +166,9 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
// A and CNAMEs: Validate. If null, set to default. // A and CNAMEs: Validate. If null, set to default.
// else: Make sure it wasn't set. Set to default. // else: Make sure it wasn't set. Set to default.
for _, rec := range dc.Records { for _, rec := range dc.Records {
if rec.TTL == 0 || rec.TTL == 300 {
rec.TTL = 1
}
if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" { if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" {
if rec.Metadata[metaProxy] != "" { if rec.Metadata[metaProxy] != "" {
return fmt.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.Name, rec.Metadata[metaProxy]) return fmt.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.Name, rec.Metadata[metaProxy])
@ -188,9 +190,6 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
// look for ip conversions and transform records // look for ip conversions and transform records
for _, rec := range dc.Records { for _, rec := range dc.Records {
if rec.TTL == 0 {
rec.TTL = 1
}
if rec.Type != "A" { if rec.Type != "A" {
continue continue
} }
@ -224,7 +223,6 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
if len(metadata) > 0 { if len(metadata) > 0 {
parsedMeta := &struct { parsedMeta := &struct {
IPConversions string `json:"ip_conversions"` IPConversions string `json:"ip_conversions"`
SecretIps []interface{} `json:"secret_ips"`
IgnoredLabels []string `json:"ignored_labels"` IgnoredLabels []string `json:"ignored_labels"`
}{} }{}
err := json.Unmarshal([]byte(metadata), parsedMeta) err := json.Unmarshal([]byte(metadata), parsedMeta)
@ -240,15 +238,6 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
if err != nil { if err != nil {
return nil, err return nil, err
} }
ips := []net.IP{}
for _, ipStr := range parsedMeta.SecretIps {
var ip net.IP
if ip, err = models.InterfaceToIP(ipStr); err != nil {
return nil, err
}
ips = append(ips, ip)
}
api.secretIPs = ips
} }
return api, nil return api, nil
} }
@ -265,58 +254,42 @@ type cfRecord struct {
Content string `json:"content"` Content string `json:"content"`
Proxiable bool `json:"proxiable"` Proxiable bool `json:"proxiable"`
Proxied bool `json:"proxied"` Proxied bool `json:"proxied"`
TTL int `json:"ttl"` TTL uint32 `json:"ttl"`
Locked bool `json:"locked"` Locked bool `json:"locked"`
ZoneID string `json:"zone_id"` ZoneID string `json:"zone_id"`
ZoneName string `json:"zone_name"` ZoneName string `json:"zone_name"`
CreatedOn time.Time `json:"created_on"` CreatedOn time.Time `json:"created_on"`
ModifiedOn time.Time `json:"modified_on"` ModifiedOn time.Time `json:"modified_on"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
Priority int `json:"priority"` Priority uint16 `json:"priority"`
} }
func (c *cfRecord) GetName() string { func (c *cfRecord) toRecord(domain string) *models.RecordConfig {
return c.Name //normalize cname,mx,ns records with dots to be consistent with our config format.
if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" {
c.Content = dnsutil.AddOrigin(c.Content+".", domain)
}
return &models.RecordConfig{
NameFQDN: c.Name,
Type: c.Type,
Target: c.Content,
Priority: c.Priority,
TTL: c.TTL,
Original: c,
}
} }
func (c *cfRecord) GetType() string { func getProxyMetadata(r *models.RecordConfig) map[string]string {
return c.Type if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" {
return nil
} }
proxied := false
func (c *cfRecord) GetContent() string { if r.Original != nil {
return c.Content proxied = r.Original.(*cfRecord).Proxied
} else {
proxied = r.Metadata[metaProxy] != "off"
} }
return map[string]string{
func (c *cfRecord) GetComparisionData() string { "proxy": fmt.Sprint(proxied),
mxPrio := ""
if c.Type == "MX" {
mxPrio = fmt.Sprintf(" %d ", c.Priority)
} }
proxy := ""
if c.Type == "A" || c.Type == "CNAME" || c.Type == "AAAA" {
proxy = fmt.Sprintf(" proxy=%v ", c.Proxied)
}
return fmt.Sprintf("%d%s%s", c.TTL, mxPrio, proxy)
}
// Used on the "expected" records.
type recordWrapper struct {
*models.RecordConfig
}
func (c recordWrapper) GetComparisionData() string {
mxPrio := ""
if c.Type == "MX" {
mxPrio = fmt.Sprintf(" %d ", c.Priority)
}
proxy := ""
if c.Type == "A" || c.Type == "AAAA" || c.Type == "CNAME" {
proxy = fmt.Sprintf(" proxy=%v ", c.Metadata[metaProxy] != "off")
}
ttl := c.TTL
if ttl == 0 {
ttl = 1
}
return fmt.Sprintf("%d%s%s", ttl, mxPrio, proxy)
} }

View File

@ -91,7 +91,7 @@ func TestIpRewriting(t *testing.T) {
} }
cf := &CloudflareApi{} cf := &CloudflareApi{}
domain := newDomainConfig() domain := newDomainConfig()
cf.ipConversions = []transform.IpConversion{{net.ParseIP("1.2.3.0"), net.ParseIP("1.2.3.40"), net.ParseIP("255.255.255.0"), nil}} cf.ipConversions = []transform.IpConversion{{net.ParseIP("1.2.3.0"), net.ParseIP("1.2.3.40"), []net.IP{net.ParseIP("255.255.255.0")}, nil}}
for _, tst := range tests { for _, tst := range tests {
rec := &models.RecordConfig{Type: "A", Target: tst.Given, Metadata: map[string]string{metaProxy: tst.Proxy}} rec := &models.RecordConfig{Type: "A", Target: tst.Given, Metadata: map[string]string{metaProxy: tst.Proxy}}
domain.Records = append(domain.Records, rec) domain.Records = append(domain.Records, rec)
@ -110,7 +110,3 @@ func TestIpRewriting(t *testing.T) {
} }
} }
} }
func TestCnameValidation(t *testing.T) {
}

View File

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff"
) )
const ( const (
@ -47,10 +46,10 @@ func (c *CloudflareApi) fetchDomainList() error {
} }
// get all records for a domain // get all records for a domain
func (c *CloudflareApi) getRecordsForDomain(id string) ([]diff.Record, error) { func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) {
url := fmt.Sprintf(recordsURL, id) url := fmt.Sprintf(recordsURL, id)
page := 1 page := 1
records := []diff.Record{} records := []*models.RecordConfig{}
for { for {
reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
var data recordsResponse var data recordsResponse
@ -61,7 +60,7 @@ func (c *CloudflareApi) getRecordsForDomain(id string) ([]diff.Record, error) {
return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors)) return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors))
} }
for _, rec := range data.Result { for _, rec := range data.Result {
records = append(records, rec) records = append(records, rec.toRecord(domain))
} }
ri := data.ResultInfo ri := data.ResultInfo
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount { if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {

View File

@ -3,69 +3,83 @@ package diff
import ( import (
"fmt" "fmt"
"sort" "sort"
"github.com/StackExchange/dnscontrol/models"
) )
type Record interface {
GetName() string
GetType() string
GetContent() string
// Get relevant comparision data. Default implentation uses "ttl [mx priority]", but providers may insert
// provider specific metadata if needed.
GetComparisionData() string
}
type Correlation struct { type Correlation struct {
Existing Record d *differ
Desired Record Existing *models.RecordConfig
Desired *models.RecordConfig
} }
type Changeset []Correlation type Changeset []Correlation
func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, toDelete, modify Changeset) { type Differ interface {
IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset)
}
func New(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[string]string) Differ {
return &differ{
dc: dc,
extraValues: extraValues,
}
}
type differ struct {
dc *models.DomainConfig
extraValues []func(*models.RecordConfig) map[string]string
}
// get normalized content for record. target, ttl, mxprio, and specified metadata
func (d *differ) content(r *models.RecordConfig) string {
content := fmt.Sprintf("%s %d", r.Target, r.TTL)
if r.Type == "MX" {
content += fmt.Sprintf(" priority=%d", r.Priority)
}
for _, f := range d.extraValues {
for k, v := range f(r) {
content += fmt.Sprintf(" %s=%s", k, v)
}
}
return content
}
func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset) {
unchanged = Changeset{} unchanged = Changeset{}
create = Changeset{} create = Changeset{}
toDelete = Changeset{} toDelete = Changeset{}
modify = Changeset{} modify = Changeset{}
desired := d.dc.Records
// log.Printf("ID existing records: (%d)\n", len(existing))
// for i, d := range existing {
// log.Printf("\t%d\t%v\n", i, d)
// }
// log.Printf("ID desired records: (%d)\n", len(desired))
// for i, d := range desired {
// log.Printf("\t%d\t%v\n", i, d)
// }
//sort existing and desired by name //sort existing and desired by name
type key struct { type key struct {
name, rType string name, rType string
} }
existingByNameAndType := map[key][]Record{} existingByNameAndType := map[key][]*models.RecordConfig{}
desiredByNameAndType := map[key][]Record{} desiredByNameAndType := map[key][]*models.RecordConfig{}
for _, e := range existing { for _, e := range existing {
k := key{e.GetName(), e.GetType()} k := key{e.NameFQDN, e.Type}
existingByNameAndType[k] = append(existingByNameAndType[k], e) existingByNameAndType[k] = append(existingByNameAndType[k], e)
} }
for _, d := range desired { for _, d := range desired {
k := key{d.GetName(), d.GetType()} k := key{d.NameFQDN, d.Type}
desiredByNameAndType[k] = append(desiredByNameAndType[k], d) desiredByNameAndType[k] = append(desiredByNameAndType[k], d)
} }
// Look through existing records. This will give us changes and deletions and some additions.
// Look through existing records. This will give us changes and deletions and some additions // Each iteration is only for a single type/name record set
for key, existingRecords := range existingByNameAndType { for key, existingRecords := range existingByNameAndType {
desiredRecords := desiredByNameAndType[key] desiredRecords := desiredByNameAndType[key]
//first look through records that are the same target on both sides. Those are either modifications or unchanged
//first look through records that are the same content on both sides. Those are either modifications or unchanged
for i := len(existingRecords) - 1; i >= 0; i-- { for i := len(existingRecords) - 1; i >= 0; i-- {
ex := existingRecords[i] ex := existingRecords[i]
for j, de := range desiredRecords { for j, de := range desiredRecords {
if de.GetContent() == ex.GetContent() { if de.Target == ex.Target {
//they're either identical or should be a modification of each other //they're either identical or should be a modification of each other (ttl or metadata changes)
if de.GetComparisionData() == ex.GetComparisionData() { if d.content(de) == d.content(ex) {
unchanged = append(unchanged, Correlation{ex, de}) unchanged = append(unchanged, Correlation{d, ex, de})
} else { } else {
modify = append(modify, Correlation{ex, de}) modify = append(modify, Correlation{d, ex, de})
} }
// remove from both slices by index // remove from both slices by index
existingRecords = existingRecords[:i+copy(existingRecords[i:], existingRecords[i+1:])] existingRecords = existingRecords[:i+copy(existingRecords[i:], existingRecords[i+1:])]
@ -75,18 +89,18 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
} }
} }
desiredLookup := map[string]Record{} desiredLookup := map[string]*models.RecordConfig{}
existingLookup := map[string]Record{} existingLookup := map[string]*models.RecordConfig{}
// build index based on normalized value/ttl // build index based on normalized content data
for _, ex := range existingRecords { for _, ex := range existingRecords {
normalized := fmt.Sprintf("%s %s", ex.GetContent(), ex.GetComparisionData()) normalized := d.content(ex)
if existingLookup[normalized] != nil { if existingLookup[normalized] != nil {
panic(fmt.Sprintf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized)) panic(fmt.Sprintf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized))
} }
existingLookup[normalized] = ex existingLookup[normalized] = ex
} }
for _, de := range desiredRecords { for _, de := range desiredRecords {
normalized := fmt.Sprintf("%s %s", de.GetContent(), de.GetComparisionData()) normalized := d.content(de)
if desiredLookup[normalized] != nil { if desiredLookup[normalized] != nil {
panic(fmt.Sprintf("DUPLICATE D_RECORD FOUND: %s %s", key, normalized)) panic(fmt.Sprintf("DUPLICATE D_RECORD FOUND: %s %s", key, normalized))
} }
@ -95,36 +109,28 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
// if a record is in both, it is unchanged // if a record is in both, it is unchanged
for norm, ex := range existingLookup { for norm, ex := range existingLookup {
if de, ok := desiredLookup[norm]; ok { if de, ok := desiredLookup[norm]; ok {
unchanged = append(unchanged, Correlation{ex, de}) unchanged = append(unchanged, Correlation{d, ex, de})
delete(existingLookup, norm) delete(existingLookup, norm)
delete(desiredLookup, norm) delete(desiredLookup, norm)
} }
} }
//sort records by normalized text. Keeps behaviour deterministic //sort records by normalized text. Keeps behaviour deterministic
existingStrings, desiredStrings := []string{}, []string{} existingStrings, desiredStrings := sortedKeys(existingLookup), sortedKeys(desiredLookup)
for norm := range existingLookup {
existingStrings = append(existingStrings, norm)
}
for norm := range desiredLookup {
desiredStrings = append(desiredStrings, norm)
}
sort.Strings(existingStrings)
sort.Strings(desiredStrings)
// Modifications. Take 1 from each side. // Modifications. Take 1 from each side.
for len(desiredStrings) > 0 && len(existingStrings) > 0 { for len(desiredStrings) > 0 && len(existingStrings) > 0 {
modify = append(modify, Correlation{existingLookup[existingStrings[0]], desiredLookup[desiredStrings[0]]}) modify = append(modify, Correlation{d, existingLookup[existingStrings[0]], desiredLookup[desiredStrings[0]]})
existingStrings = existingStrings[1:] existingStrings = existingStrings[1:]
desiredStrings = desiredStrings[1:] desiredStrings = desiredStrings[1:]
} }
// If desired still has things they are additions // If desired still has things they are additions
for _, norm := range desiredStrings { for _, norm := range desiredStrings {
rec := desiredLookup[norm] rec := desiredLookup[norm]
create = append(create, Correlation{nil, rec}) create = append(create, Correlation{d, nil, rec})
} }
// if found , but not desired, delete it // if found , but not desired, delete it
for _, norm := range existingStrings { for _, norm := range existingStrings {
rec := existingLookup[norm] rec := existingLookup[norm]
toDelete = append(toDelete, Correlation{rec, nil}) toDelete = append(toDelete, Correlation{d, rec, nil})
} }
// remove this set from the desired list to indicate we have processed it. // remove this set from the desired list to indicate we have processed it.
delete(desiredByNameAndType, key) delete(desiredByNameAndType, key)
@ -136,7 +142,7 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
} }
for _, desiredList := range desiredByNameAndType { for _, desiredList := range desiredByNameAndType {
for _, rec := range desiredList { for _, rec := range desiredList {
create = append(create, Correlation{nil, rec}) create = append(create, Correlation{d, nil, rec})
} }
} }
return return
@ -144,10 +150,19 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
func (c Correlation) String() string { func (c Correlation) String() string {
if c.Existing == nil { if c.Existing == nil {
return fmt.Sprintf("CREATE %s %s %s %s", c.Desired.GetType(), c.Desired.GetName(), c.Desired.GetContent(), c.Desired.GetComparisionData()) return fmt.Sprintf("CREATE %s %s %s", c.Desired.Type, c.Desired.NameFQDN, c.d.content(c.Desired))
} }
if c.Desired == nil { if c.Desired == nil {
return fmt.Sprintf("DELETE %s %s %s %s", c.Existing.GetType(), c.Existing.GetName(), c.Existing.GetContent(), c.Existing.GetComparisionData()) return fmt.Sprintf("DELETE %s %s %s", c.Existing.Type, c.Existing.NameFQDN, c.d.content(c.Existing))
} }
return fmt.Sprintf("MODIFY %s %s: (%s %s) -> (%s %s)", c.Existing.GetType(), c.Existing.GetName(), c.Existing.GetContent(), c.Existing.GetComparisionData(), c.Desired.GetContent(), c.Desired.GetComparisionData()) return fmt.Sprintf("MODIFY %s %s: (%s) -> (%s)", c.Existing.Type, c.Existing.NameFQDN, c.d.content(c.Existing), c.d.content(c.Desired))
}
func sortedKeys(m map[string]*models.RecordConfig) []string {
s := []string{}
for v := range m {
s = append(s, v)
}
sort.Strings(s)
return s
} }

View File

@ -1,58 +1,52 @@
package diff package diff
import ( import (
"fmt" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/StackExchange/dnscontrol/models"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
) )
type myRecord string //@ A 1 1.2.3.4 func myRecord(s string) *models.RecordConfig {
parts := strings.Split(s, " ")
func (m myRecord) GetName() string { ttl, _ := strconv.ParseUint(parts[2], 10, 32)
name := strings.SplitN(string(m), " ", 4)[0] return &models.RecordConfig{
return dnsutil.AddOrigin(name, "example.com") NameFQDN: dnsutil.AddOrigin(parts[0], "example.com"),
Type: parts[1],
TTL: uint32(ttl),
Target: parts[3],
Metadata: map[string]string{},
} }
func (m myRecord) GetType() string {
return strings.SplitN(string(m), " ", 4)[1]
}
func (m myRecord) GetContent() string {
return strings.SplitN(string(m), " ", 4)[3]
}
func (m myRecord) GetComparisionData() string {
return fmt.Sprint(strings.SplitN(string(m), " ", 4)[2])
} }
func TestAdditionsOnly(t *testing.T) { func TestAdditionsOnly(t *testing.T) {
desired := []Record{ desired := []*models.RecordConfig{
myRecord("@ A 1 1.2.3.4"), myRecord("@ A 1 1.2.3.4"),
} }
existing := []Record{} existing := []*models.RecordConfig{}
checkLengths(t, existing, desired, 0, 1, 0, 0) checkLengths(t, existing, desired, 0, 1, 0, 0)
} }
func TestDeletionsOnly(t *testing.T) { func TestDeletionsOnly(t *testing.T) {
existing := []Record{ existing := []*models.RecordConfig{
myRecord("@ A 1 1.2.3.4"), myRecord("@ A 1 1.2.3.4"),
} }
desired := []Record{} desired := []*models.RecordConfig{}
checkLengths(t, existing, desired, 0, 0, 1, 0) checkLengths(t, existing, desired, 0, 0, 1, 0)
} }
func TestModification(t *testing.T) { func TestModification(t *testing.T) {
existing := []Record{ existing := []*models.RecordConfig{
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
myRecord("@ A 1 1.2.3.4"), myRecord("@ A 1 1.2.3.4"),
} }
desired := []Record{ desired := []*models.RecordConfig{
myRecord("@ A 32 1.2.3.4"), myRecord("@ A 32 1.2.3.4"),
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
} }
un, _, _, mod := checkLengths(t, existing, desired, 1, 0, 0, 1) un, _, _, mod := checkLengths(t, existing, desired, 1, 0, 0, 1)
if t.Failed() {
return
}
if un[0].Desired != desired[1] || un[0].Existing != existing[0] { if un[0].Desired != desired[1] || un[0].Existing != existing[0] {
t.Error("Expected unchanged records to be correlated") t.Error("Expected unchanged records to be correlated")
} }
@ -62,10 +56,10 @@ func TestModification(t *testing.T) {
} }
func TestUnchangedWithAddition(t *testing.T) { func TestUnchangedWithAddition(t *testing.T) {
existing := []Record{ existing := []*models.RecordConfig{
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
} }
desired := []Record{ desired := []*models.RecordConfig{
myRecord("www A 1 1.2.3.4"), myRecord("www A 1 1.2.3.4"),
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
} }
@ -76,12 +70,12 @@ func TestUnchangedWithAddition(t *testing.T) {
} }
func TestOutOfOrderRecords(t *testing.T) { func TestOutOfOrderRecords(t *testing.T) {
existing := []Record{ existing := []*models.RecordConfig{
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
myRecord("www A 1 2.2.2.2"), myRecord("www A 1 2.2.2.2"),
myRecord("www A 1 3.3.3.3"), myRecord("www A 1 3.3.3.3"),
} }
desired := []Record{ desired := []*models.RecordConfig{
myRecord("www A 1 1.1.1.1"), myRecord("www A 1 1.1.1.1"),
myRecord("www A 1 2.2.2.2"), myRecord("www A 1 2.2.2.2"),
myRecord("www A 1 2.2.2.3"), myRecord("www A 1 2.2.2.3"),
@ -91,11 +85,55 @@ func TestOutOfOrderRecords(t *testing.T) {
if mods[0].Desired != desired[3] || mods[0].Existing != existing[2] { if mods[0].Desired != desired[3] || mods[0].Existing != existing[2] {
t.Fatalf("Expected to match %s and %s, but matched %s and %s", existing[2], desired[3], mods[0].Existing, mods[0].Desired) t.Fatalf("Expected to match %s and %s, but matched %s and %s", existing[2], desired[3], mods[0].Existing, mods[0].Desired)
} }
} }
func checkLengths(t *testing.T, existing, desired []Record, unCount, createCount, delCount, modCount int) (un, cre, del, mod Changeset) { func TestMxPrio(t *testing.T) {
un, cre, del, mod = IncrementalDiff(existing, desired) existing := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
desired := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
existing[0].Priority = 10
desired[0].Priority = 20
checkLengths(t, existing, desired, 0, 0, 0, 1)
}
func TestTTLChange(t *testing.T) {
existing := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
desired := []*models.RecordConfig{
myRecord("www MX 10 1.1.1.1"),
}
checkLengths(t, existing, desired, 0, 0, 0, 1)
}
func TestMetaChange(t *testing.T) {
existing := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
desired := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
existing[0].Metadata["k"] = "aa"
desired[0].Metadata["k"] = "bb"
checkLengths(t, existing, desired, 1, 0, 0, 0)
getMeta := func(r *models.RecordConfig) map[string]string {
return map[string]string{
"k": r.Metadata["k"],
}
}
checkLengths(t, existing, desired, 0, 0, 0, 1, getMeta)
}
func checkLengths(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) {
dc := &models.DomainConfig{
Name: "example.com",
Records: desired,
}
d := New(dc, valFuncs...)
un, cre, del, mod = d.IncrementalDiff(existing)
if len(un) != unCount { if len(un) != unCount {
t.Errorf("Got %d unchanged records, but expected %d", len(un), unCount) t.Errorf("Got %d unchanged records, but expected %d", len(un), unCount)
} }
@ -108,5 +146,8 @@ func checkLengths(t *testing.T, existing, desired []Record, unCount, createCount
if len(mod) != modCount { if len(mod) != modCount {
t.Errorf("Got %d records to modify, but expected %d", len(mod), modCount) t.Errorf("Got %d records to modify, but expected %d", len(mod), modCount)
} }
if t.Failed() {
t.FailNow()
}
return return
} }

View File

@ -3,8 +3,6 @@ package gandi
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers" "github.com/StackExchange/dnscontrol/providers"
@ -30,49 +28,10 @@ type GandiApi struct {
ZoneId int64 ZoneId int64
} }
type cfRecord struct { type gandiRecord struct {
gandirecord.RecordInfo gandirecord.RecordInfo
} }
func (c *cfRecord) GetName() string {
return c.Name
}
func (c *cfRecord) GetType() string {
return c.Type
}
func (c *cfRecord) GetTtl() int64 {
return c.Ttl
}
func (c *cfRecord) GetValue() string {
return c.Value
}
func (c *cfRecord) GetContent() string {
switch c.Type {
case "MX":
parts := strings.SplitN(c.Value, " ", 2)
// TODO(tlim): This should check for more errors.
return strings.Join(parts[1:], " ")
default:
}
return c.Value
}
func (c *cfRecord) GetComparisionData() string {
if c.Type == "MX" {
parts := strings.SplitN(c.Value, " ", 2)
priority, err := strconv.Atoi(parts[0])
if err != nil {
return fmt.Sprintf("%s %#v", c.Ttl, parts[0])
}
return fmt.Sprintf("%d %d", c.Ttl, priority)
}
return fmt.Sprintf("%d", c.Ttl)
}
func (c *GandiApi) getDomainInfo(domain string) (*gandidomain.DomainInfo, error) { func (c *GandiApi) getDomainInfo(domain string) (*gandidomain.DomainInfo, error) {
if err := c.fetchDomainList(); err != nil { if err := c.fetchDomainList(); err != nil {
return nil, err return nil, err
@ -95,6 +54,7 @@ func (c *GandiApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
} }
return ns, nil return ns, nil
} }
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
domaininfo, err := c.getDomainInfo(dc.Name) domaininfo, err := c.getDomainInfo(dc.Name)
if err != nil { if err != nil {
@ -104,46 +64,23 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Convert to []diff.Records and compare:
foundDiffRecords := make([]diff.Record, len(foundRecords))
for i, rec := range foundRecords {
n := &cfRecord{}
n.Id = 0
n.Name = rec.Name
n.Ttl = int64(rec.Ttl)
n.Type = rec.Type
n.Value = rec.Value
foundDiffRecords[i] = n
}
expectedDiffRecords := make([]diff.Record, len(dc.Records))
expectedRecordSets := make([]gandirecord.RecordSet, len(dc.Records)) expectedRecordSets := make([]gandirecord.RecordSet, len(dc.Records))
for i, rec := range dc.Records { for i, rec := range dc.Records {
n := &cfRecord{} if rec.Type == "MX" {
n.Id = 0 rec.Target = fmt.Sprintf("%d %s", rec.Priority, rec.Target)
n.Name = rec.Name
n.Ttl = int64(rec.TTL)
if n.Ttl == 0 {
n.Ttl = 3600
} }
n.Type = rec.Type if rec.Type == "TXT" {
switch n.Type { rec.Target = "\"" + rec.Target + "\"" // FIXME(tlim): Should do proper quoting.
case "MX":
n.Value = fmt.Sprintf("%d %s", rec.Priority, rec.Target)
case "TXT":
n.Value = "\"" + rec.Target + "\"" // FIXME(tlim): Should do proper quoting.
default:
n.Value = rec.Target
} }
expectedDiffRecords[i] = n
expectedRecordSets[i] = gandirecord.RecordSet{} expectedRecordSets[i] = gandirecord.RecordSet{}
expectedRecordSets[i]["type"] = n.Type expectedRecordSets[i]["type"] = rec.Type
expectedRecordSets[i]["name"] = n.Name expectedRecordSets[i]["name"] = rec.Name
expectedRecordSets[i]["value"] = n.Value expectedRecordSets[i]["value"] = rec.Target
if n.Ttl != 0 { expectedRecordSets[i]["ttl"] = rec.TTL
expectedRecordSets[i]["ttl"] = n.Ttl
} }
} differ := diff.New(dc)
_, create, del, mod := diff.IncrementalDiff(foundDiffRecords, expectedDiffRecords) _, create, del, mod := differ.IncrementalDiff(foundRecords)
// Print a list of changes. Generate an actual change that is the zone // Print a list of changes. Generate an actual change that is the zone
changes := false changes := false
@ -160,7 +97,7 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
fmt.Println(i) fmt.Println(i)
} }
msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)", dc.Name, len(expectedDiffRecords)) msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)", dc.Name, len(dc.Records))
corrections := []*models.Correction{} corrections := []*models.Correction{}
if changes { if changes {
corrections = append(corrections, corrections = append(corrections,

View File

@ -3,15 +3,13 @@ package gandi
import ( import (
"fmt" "fmt"
"github.com/StackExchange/dnscontrol/providers/diff"
)
import (
gandiclient "github.com/prasmussen/gandi-api/client" gandiclient "github.com/prasmussen/gandi-api/client"
gandidomain "github.com/prasmussen/gandi-api/domain" gandidomain "github.com/prasmussen/gandi-api/domain"
gandizone "github.com/prasmussen/gandi-api/domain/zone" gandizone "github.com/prasmussen/gandi-api/domain/zone"
gandirecord "github.com/prasmussen/gandi-api/domain/zone/record" gandirecord "github.com/prasmussen/gandi-api/domain/zone/record"
gandiversion "github.com/prasmussen/gandi-api/domain/zone/version" gandiversion "github.com/prasmussen/gandi-api/domain/zone/version"
"github.com/StackExchange/dnscontrol/models"
) )
// fetchDomainList gets list of domains for account. Cache ids for easy lookup. // fetchDomainList gets list of domains for account. Cache ids for easy lookup.
@ -41,10 +39,18 @@ func (c *GandiApi) fetchDomainInfo(fqdn string) (*gandidomain.DomainInfo, error)
} }
// getRecordsForDomain returns a list of records for a zone. // getRecordsForDomain returns a list of records for a zone.
func (c *GandiApi) getZoneRecords(zoneid int64) ([]*gandirecord.RecordInfo, error) { func (c *GandiApi) getZoneRecords(zoneid int64) ([]*models.RecordConfig, error) {
gc := gandiclient.New(c.ApiKey, gandiclient.Production) gc := gandiclient.New(c.ApiKey, gandiclient.Production)
record := gandirecord.New(gc) record := gandirecord.New(gc)
return record.List(zoneid, 0) recs, err := record.List(zoneid, 0)
if err != nil {
return nil, err
}
rcs := make([]*models.RecordConfig, 0, len(recs))
for _, r := range recs {
rcs = append(rcs, convert(r))
}
return rcs, nil
} }
// listZones retrieves the list of zones. // listZones retrieves the list of zones.
@ -75,11 +81,6 @@ func (c *GandiApi) createZone(name string) (*gandizone.ZoneInfo, error) {
return zone.Create(name) return zone.Create(name)
} }
// replaceZoneContents
func (c *GandiApi) replaceZoneContents(zone_id int64, version_id int64, records []diff.Record) error {
return fmt.Errorf("replaceZoneContents unimplemented")
}
func (c *GandiApi) getEditableZone(domainname string, zoneinfo *gandizone.ZoneInfo) (int64, error) { func (c *GandiApi) getEditableZone(domainname string, zoneinfo *gandizone.ZoneInfo) (int64, error) {
var zone_id int64 var zone_id int64
if zoneinfo.Domains < 2 { if zoneinfo.Domains < 2 {
@ -169,3 +170,13 @@ func (c *GandiApi) createGandiZone(domainname string, zone_id int64, records []g
return nil return nil
} }
func convert(r *gandirecord.RecordInfo) *models.RecordConfig {
return &models.RecordConfig{
NameFQDN: r.Name,
Type: r.Type,
Original: r,
Target: r.Value,
TTL: uint32(r.Ttl),
}
}

View File

@ -104,7 +104,7 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
return nil, err return nil, err
} }
//convert to dnscontrol RecordConfig format //convert to dnscontrol RecordConfig format
existingRecords := []diff.Record{} existingRecords := []*models.RecordConfig{}
oldRRs := map[key]*dns.ResourceRecordSet{} oldRRs := map[key]*dns.ResourceRecordSet{}
for _, set := range rrs { for _, set := range rrs {
nameWithoutDot := set.Name nameWithoutDot := set.Name
@ -123,11 +123,7 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
} }
} }
w := []diff.Record{}
for _, want := range dc.Records { for _, want := range dc.Records {
if want.TTL == 0 {
want.TTL = 300
}
if want.Type == "MX" { if want.Type == "MX" {
want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target) want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target)
want.Priority = 0 want.Priority = 0
@ -135,24 +131,24 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
//add quotes to txts //add quotes to txts
want.Target = fmt.Sprintf(`"%s"`, want.Target) want.Target = fmt.Sprintf(`"%s"`, want.Target)
} }
w = append(w, want)
} }
// first collect keys that have changed // first collect keys that have changed
_, create, delete, modify := diff.IncrementalDiff(existingRecords, w) differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
changedKeys := map[key]bool{} changedKeys := map[key]bool{}
desc := "" desc := ""
for _, c := range create { for _, c := range create {
desc += fmt.Sprintln(c) desc += fmt.Sprintln(c)
changedKeys[keyForRec(c.Desired.(*models.RecordConfig))] = true changedKeys[keyForRec(c.Desired)] = true
} }
for _, d := range delete { for _, d := range delete {
desc += fmt.Sprintln(d) desc += fmt.Sprintln(d)
changedKeys[keyForRec(d.Existing.(*models.RecordConfig))] = true changedKeys[keyForRec(d.Existing)] = true
} }
for _, m := range modify { for _, m := range modify {
desc += fmt.Sprintln(m) desc += fmt.Sprintln(m)
changedKeys[keyForRec(m.Existing.(*models.RecordConfig))] = true changedKeys[keyForRec(m.Existing)] = true
} }
if len(changedKeys) == 0 { if len(changedKeys) == 0 {
return nil, nil return nil, nil

View File

@ -175,7 +175,11 @@ func TestGetNameservers(t *testing.T) {
t.Errorf("Test %d: %s", i, err) t.Errorf("Test %d: %s", i, err)
continue continue
} }
if strings.Join(found, ",") != test.expected { fStrs := []string{}
for _, n := range found {
fStrs = append(fStrs, n.Name)
}
if strings.Join(fStrs, ",") != test.expected {
t.Errorf("Test %d: Expected '%s', but found '%s'", i, test.expected, found) t.Errorf("Test %d: Expected '%s', but found '%s'", i, test.expected, found)
} }
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff" "github.com/StackExchange/dnscontrol/providers/diff"
"strconv"
) )
var defaultNameservers = []*models.Nameserver{ var defaultNameservers = []*models.Nameserver{
@ -23,42 +24,30 @@ func (n *nameDotCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Co
if err != nil { if err != nil {
return nil, err return nil, err
} }
actual := make([]diff.Record, len(records)) actual := make([]*models.RecordConfig, len(records))
for i := range records { for i, r := range records {
actual[i] = records[i] actual[i] = r.toRecord()
} }
desired := make([]diff.Record, 0, len(dc.Records)) checkNSModifications(dc)
for _, rec := range dc.Records {
if rec.TTL == 0 {
rec.TTL = 300
}
if rec.Type == "NS" && rec.NameFQDN == dc.Name {
// name.com does change base domain NS records. dnscontrol will print warnings if you try to set them to anything besides the name.com defaults.
if !strings.HasSuffix(rec.Target, ".name.com.") {
log.Printf("Warning: name.com does not allow NS records on base domain to be modified. %s will not be added.", rec.Target)
}
continue
}
desired = append(desired, rec)
}
_, create, del, mod := diff.IncrementalDiff(actual, desired) differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(actual)
corrections := []*models.Correction{} corrections := []*models.Correction{}
for _, d := range del { for _, d := range del {
rec := d.Existing.(*nameComRecord) rec := d.Existing.Original.(*nameComRecord)
c := &models.Correction{Msg: d.String(), F: func() error { return n.deleteRecord(rec.RecordID, dc.Name) }} c := &models.Correction{Msg: d.String(), F: func() error { return n.deleteRecord(rec.RecordID, dc.Name) }}
corrections = append(corrections, c) corrections = append(corrections, c)
} }
for _, cre := range create { for _, cre := range create {
rec := cre.Desired.(*models.RecordConfig) rec := cre.Desired.Original.(*models.RecordConfig)
c := &models.Correction{Msg: cre.String(), F: func() error { return n.createRecord(rec, dc.Name) }} c := &models.Correction{Msg: cre.String(), F: func() error { return n.createRecord(rec, dc.Name) }}
corrections = append(corrections, c) corrections = append(corrections, c)
} }
for _, chng := range mod { for _, chng := range mod {
old := chng.Existing.(*nameComRecord) old := chng.Existing.Original.(*nameComRecord)
new := chng.Desired.(*models.RecordConfig) new := chng.Desired
c := &models.Correction{Msg: chng.String(), F: func() error { c := &models.Correction{Msg: chng.String(), F: func() error {
err := n.deleteRecord(old.RecordID, dc.Name) err := n.deleteRecord(old.RecordID, dc.Name)
if err != nil { if err != nil {
@ -90,21 +79,32 @@ type nameComRecord struct {
Priority string `json:"priority"` Priority string `json:"priority"`
} }
func (r *nameComRecord) GetName() string { func checkNSModifications(dc *models.DomainConfig) {
return r.Name newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.Type == "NS" && rec.NameFQDN == dc.Name {
// name.com does change base domain NS records. dnscontrol will print warnings if you try to set them to anything besides the name.com defaults.
if !strings.HasSuffix(rec.Target, ".name.com.") {
log.Printf("Warning: name.com does not allow NS records on base domain to be modified. %s will not be added.", rec.Target)
} }
func (r *nameComRecord) GetType() string { continue
return r.Type
} }
func (r *nameComRecord) GetContent() string { newList = append(newList, rec)
return r.Content
} }
func (r *nameComRecord) GetComparisionData() string { dc.Records = newList
mxPrio := "" }
if r.Type == "MX" {
mxPrio = fmt.Sprintf(" %s ", r.Priority) func (r *nameComRecord) toRecord() *models.RecordConfig {
ttl, _ := strconv.ParseUint(r.TTL, 10, 32)
prio, _ := strconv.ParseUint(r.Priority, 10, 16)
return &models.RecordConfig{
NameFQDN: r.Name,
Type: r.Type,
Target: r.Content,
TTL: uint32(ttl),
Priority: uint16(prio),
Original: r,
} }
return fmt.Sprintf("%s%s", r.TTL, mxPrio)
} }
type listRecordsResponse struct { type listRecordsResponse struct {

View File

@ -40,6 +40,7 @@ func init() {
func sPtr(s string) *string { func sPtr(s string) *string {
return &s return &s
} }
func (r *route53Provider) getZones() error { func (r *route53Provider) getZones() error {
if r.zones != nil { if r.zones != nil {
return nil return nil
@ -73,8 +74,8 @@ type key struct {
Name, Type string Name, Type string
} }
func getKey(r diff.Record) key { func getKey(r *models.RecordConfig) key {
return key{r.GetName(), r.GetType()} return key{r.NameFQDN, r.Type}
} }
type errNoExist struct { type errNoExist struct {
@ -121,8 +122,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
return nil, err return nil, err
} }
//convert to dnscontrol RecordConfig format var existingRecords = []*models.RecordConfig{}
var existingRecords = []diff.Record{}
for _, set := range records { for _, set := range records {
for _, rec := range set.ResourceRecords { for _, rec := range set.ResourceRecords {
if *set.Type == "SOA" { if *set.Type == "SOA" {
@ -137,23 +137,19 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
existingRecords = append(existingRecords, r) existingRecords = append(existingRecords, r)
} }
} }
w := []diff.Record{}
for _, want := range dc.Records { for _, want := range dc.Records {
if want.TTL == 0 {
want.TTL = 300
}
if want.Type == "MX" { if want.Type == "MX" {
want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target) want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target)
want.Priority = 0 want.Priority = 0
} else if want.Type == "TXT" { } else if want.Type == "TXT" {
want.Target = fmt.Sprintf(`"%s"`, want.Target) //FIXME: better escaping/quoting want.Target = fmt.Sprintf(`"%s"`, want.Target) //FIXME: better escaping/quoting
} }
w = append(w, want)
} }
//diff //diff
changeDesc := "" changeDesc := ""
_, create, delete, modify := diff.IncrementalDiff(existingRecords, w) differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
namesToUpdate := map[key]bool{} namesToUpdate := map[key]bool{}
for _, c := range create { for _, c := range create {