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"`
NameFQDN string `json:"-"` // Must end with ".$origin". See below.
Priority uint16 `json:"priority,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
}
func (r *RecordConfig) GetName() string {
return r.NameFQDN
}
func (r *RecordConfig) GetType() string {
return r.Type
}
func (r *RecordConfig) GetContent() string {
return r.Target
}
func (r *RecordConfig) GetComparisionData() string {
mxPrio := ""
func (r *RecordConfig) String() string {
content := fmt.Sprintf("%s %s %s %d", r.Type, r.NameFQDN, r.Target, r.TTL)
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.

View file

@ -200,6 +200,9 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
// Normalize Records.
for _, rec := range domain.Records {
if rec.TTL == 0 {
rec.TTL = models.DefaultTTL
}
// Validate the unmodified inputs:
if err := validateRecordTypes(rec, domain.Name); err != nil {
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)
}
// Read expectedRecords:
//expectedRecords := make([]*models.RecordConfig, len(dc.Records))
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)
differ := diff.New(dc)
_, creates, dels, modifications := differ.IncrementalDiff(foundRecords)
// NOTE(tlim): This provider does not delete records. If
// you need to delete a record, either delete it manually
// 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 {
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 {
corrections = append(corrections, c.createRec(dc.Name, cre.Desired.(*models.RecordConfig))...)
corrections = append(corrections, c.createRec(dc.Name, cre.Desired)...)
}
for _, m := range modifications {
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 {
old, rec := m.Existing.(*models.RecordConfig), m.Desired.(*models.RecordConfig)
oldContent := old.GetContent()
newContent := rec.GetContent()
old, rec := m.Existing, m.Desired
return &models.Correction{
Msg: m.String(),
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.
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:
foundRecords := make([]*models.RecordConfig, 0)
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", "@") {
expectedRecords = append(expectedRecords, soa_rec)
dc.Records = append(dc.Records, soa_rec)
}
// Convert to []diff.Records and compare:
foundDiffRecords := make([]diff.Record, len(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)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(foundRecords)
// Print a list of changes. Generate an actual change that is the zone
changes := false

View file

@ -31,7 +31,6 @@ Domain level metadata availible:
Provider level metadata availible:
- ip_conversions
- secret_ips
*/
type CloudflareApi struct {
@ -40,7 +39,6 @@ type CloudflareApi struct {
domainIndex map[string]string
nameservers map[string][]string
ipConversions []transform.IpConversion
secretIPs []net.IP
ignoredLabels []string
}
@ -79,7 +77,7 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
if err := c.preprocessConfig(dc); err != nil {
return nil, err
}
records, err := c.getRecordsForDomain(id)
records, err := c.getRecordsForDomain(id, dc.Name)
if err != nil {
return nil, err
}
@ -87,50 +85,52 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
for i := len(records) - 1; i >= 0; i-- {
rec := records[i]
// Delete ignore labels
if labelMatches(dnsutil.TrimDomainName(rec.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
fmt.Printf("ignored_label: %s\n", rec.(*cfRecord).Name)
if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
fmt.Printf("ignored_label: %s\n", rec.Original.(*cfRecord).Name)
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 {
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)
// 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 !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)
}
continue
}
expectedRecords = append(expectedRecords, recordWrapper{rec})
newList = append(newList, rec)
}
_, create, del, mod := diff.IncrementalDiff(records, expectedRecords)
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
dc.Records = newList
}
const (
@ -138,7 +138,6 @@ const (
metaProxyDefault = metaProxy + "_default"
metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means.
metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules.
metaSecretIPs = "secret_ips" // TODO(tlim): Rename to obscured_cidrs.
)
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.
// else: Make sure it wasn't set. Set to default.
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.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
for _, rec := range dc.Records {
if rec.TTL == 0 {
rec.TTL = 1
}
if rec.Type != "A" {
continue
}
@ -223,9 +222,8 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
if len(metadata) > 0 {
parsedMeta := &struct {
IPConversions string `json:"ip_conversions"`
SecretIps []interface{} `json:"secret_ips"`
IgnoredLabels []string `json:"ignored_labels"`
IPConversions string `json:"ip_conversions"`
IgnoredLabels []string `json:"ignored_labels"`
}{}
err := json.Unmarshal([]byte(metadata), parsedMeta)
if err != nil {
@ -240,15 +238,6 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
if err != nil {
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
}
@ -265,58 +254,42 @@ type cfRecord struct {
Content string `json:"content"`
Proxiable bool `json:"proxiable"`
Proxied bool `json:"proxied"`
TTL int `json:"ttl"`
TTL uint32 `json:"ttl"`
Locked bool `json:"locked"`
ZoneID string `json:"zone_id"`
ZoneName string `json:"zone_name"`
CreatedOn time.Time `json:"created_on"`
ModifiedOn time.Time `json:"modified_on"`
Data interface{} `json:"data"`
Priority int `json:"priority"`
Priority uint16 `json:"priority"`
}
func (c *cfRecord) GetName() string {
return c.Name
}
func (c *cfRecord) GetType() string {
return c.Type
}
func (c *cfRecord) GetContent() string {
return c.Content
}
func (c *cfRecord) GetComparisionData() string {
mxPrio := ""
if c.Type == "MX" {
mxPrio = fmt.Sprintf(" %d ", c.Priority)
func (c *cfRecord) toRecord(domain string) *models.RecordConfig {
//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)
}
proxy := ""
if c.Type == "A" || c.Type == "CNAME" || c.Type == "AAAA" {
proxy = fmt.Sprintf(" proxy=%v ", c.Proxied)
return &models.RecordConfig{
NameFQDN: c.Name,
Type: c.Type,
Target: c.Content,
Priority: c.Priority,
TTL: c.TTL,
Original: c,
}
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)
func getProxyMetadata(r *models.RecordConfig) map[string]string {
if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" {
return nil
}
proxied := false
if r.Original != nil {
proxied = r.Original.(*cfRecord).Proxied
} else {
proxied = r.Metadata[metaProxy] != "off"
}
return map[string]string{
"proxy": fmt.Sprint(proxied),
}
}

View file

@ -91,7 +91,7 @@ func TestIpRewriting(t *testing.T) {
}
cf := &CloudflareApi{}
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 {
rec := &models.RecordConfig{Type: "A", Target: tst.Given, Metadata: map[string]string{metaProxy: tst.Proxy}}
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"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff"
)
const (
@ -47,10 +46,10 @@ func (c *CloudflareApi) fetchDomainList() error {
}
// 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)
page := 1
records := []diff.Record{}
records := []*models.RecordConfig{}
for {
reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
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))
}
for _, rec := range data.Result {
records = append(records, rec)
records = append(records, rec.toRecord(domain))
}
ri := data.ResultInfo
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {

View file

@ -3,69 +3,83 @@ package diff
import (
"fmt"
"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 {
Existing Record
Desired Record
d *differ
Existing *models.RecordConfig
Desired *models.RecordConfig
}
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{}
create = Changeset{}
toDelete = Changeset{}
modify = Changeset{}
// 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)
// }
desired := d.dc.Records
//sort existing and desired by name
type key struct {
name, rType string
}
existingByNameAndType := map[key][]Record{}
desiredByNameAndType := map[key][]Record{}
existingByNameAndType := map[key][]*models.RecordConfig{}
desiredByNameAndType := map[key][]*models.RecordConfig{}
for _, e := range existing {
k := key{e.GetName(), e.GetType()}
k := key{e.NameFQDN, e.Type}
existingByNameAndType[k] = append(existingByNameAndType[k], e)
}
for _, d := range desired {
k := key{d.GetName(), d.GetType()}
k := key{d.NameFQDN, d.Type}
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 {
desiredRecords := desiredByNameAndType[key]
//first look through records that are the same content on both sides. Those are either modifications or unchanged
//first look through records that are the same target on both sides. Those are either modifications or unchanged
for i := len(existingRecords) - 1; i >= 0; i-- {
ex := existingRecords[i]
for j, de := range desiredRecords {
if de.GetContent() == ex.GetContent() {
//they're either identical or should be a modification of each other
if de.GetComparisionData() == ex.GetComparisionData() {
unchanged = append(unchanged, Correlation{ex, de})
if de.Target == ex.Target {
//they're either identical or should be a modification of each other (ttl or metadata changes)
if d.content(de) == d.content(ex) {
unchanged = append(unchanged, Correlation{d, ex, de})
} else {
modify = append(modify, Correlation{ex, de})
modify = append(modify, Correlation{d, ex, de})
}
// remove from both slices by index
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{}
existingLookup := map[string]Record{}
// build index based on normalized value/ttl
desiredLookup := map[string]*models.RecordConfig{}
existingLookup := map[string]*models.RecordConfig{}
// build index based on normalized content data
for _, ex := range existingRecords {
normalized := fmt.Sprintf("%s %s", ex.GetContent(), ex.GetComparisionData())
normalized := d.content(ex)
if existingLookup[normalized] != nil {
panic(fmt.Sprintf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized))
}
existingLookup[normalized] = ex
}
for _, de := range desiredRecords {
normalized := fmt.Sprintf("%s %s", de.GetContent(), de.GetComparisionData())
normalized := d.content(de)
if desiredLookup[normalized] != nil {
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
for norm, ex := range existingLookup {
if de, ok := desiredLookup[norm]; ok {
unchanged = append(unchanged, Correlation{ex, de})
unchanged = append(unchanged, Correlation{d, ex, de})
delete(existingLookup, norm)
delete(desiredLookup, norm)
}
}
//sort records by normalized text. Keeps behaviour deterministic
existingStrings, desiredStrings := []string{}, []string{}
for norm := range existingLookup {
existingStrings = append(existingStrings, norm)
}
for norm := range desiredLookup {
desiredStrings = append(desiredStrings, norm)
}
sort.Strings(existingStrings)
sort.Strings(desiredStrings)
existingStrings, desiredStrings := sortedKeys(existingLookup), sortedKeys(desiredLookup)
// Modifications. Take 1 from each side.
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:]
desiredStrings = desiredStrings[1:]
}
// If desired still has things they are additions
for _, norm := range desiredStrings {
rec := desiredLookup[norm]
create = append(create, Correlation{nil, rec})
create = append(create, Correlation{d, nil, rec})
}
// if found , but not desired, delete it
for _, norm := range existingStrings {
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.
delete(desiredByNameAndType, key)
@ -136,7 +142,7 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
}
for _, desiredList := range desiredByNameAndType {
for _, rec := range desiredList {
create = append(create, Correlation{nil, rec})
create = append(create, Correlation{d, nil, rec})
}
}
return
@ -144,10 +150,19 @@ func IncrementalDiff(existing []Record, desired []Record) (unchanged, create, to
func (c Correlation) String() string {
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 {
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
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/StackExchange/dnscontrol/models"
"github.com/miekg/dns/dnsutil"
)
type myRecord string //@ A 1 1.2.3.4
func (m myRecord) GetName() string {
name := strings.SplitN(string(m), " ", 4)[0]
return dnsutil.AddOrigin(name, "example.com")
}
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 myRecord(s string) *models.RecordConfig {
parts := strings.Split(s, " ")
ttl, _ := strconv.ParseUint(parts[2], 10, 32)
return &models.RecordConfig{
NameFQDN: dnsutil.AddOrigin(parts[0], "example.com"),
Type: parts[1],
TTL: uint32(ttl),
Target: parts[3],
Metadata: map[string]string{},
}
}
func TestAdditionsOnly(t *testing.T) {
desired := []Record{
desired := []*models.RecordConfig{
myRecord("@ A 1 1.2.3.4"),
}
existing := []Record{}
existing := []*models.RecordConfig{}
checkLengths(t, existing, desired, 0, 1, 0, 0)
}
func TestDeletionsOnly(t *testing.T) {
existing := []Record{
existing := []*models.RecordConfig{
myRecord("@ A 1 1.2.3.4"),
}
desired := []Record{}
desired := []*models.RecordConfig{}
checkLengths(t, existing, desired, 0, 0, 1, 0)
}
func TestModification(t *testing.T) {
existing := []Record{
existing := []*models.RecordConfig{
myRecord("www A 1 1.1.1.1"),
myRecord("@ A 1 1.2.3.4"),
}
desired := []Record{
desired := []*models.RecordConfig{
myRecord("@ A 32 1.2.3.4"),
myRecord("www A 1 1.1.1.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] {
t.Error("Expected unchanged records to be correlated")
}
@ -62,10 +56,10 @@ func TestModification(t *testing.T) {
}
func TestUnchangedWithAddition(t *testing.T) {
existing := []Record{
existing := []*models.RecordConfig{
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.1.1.1"),
}
@ -76,12 +70,12 @@ func TestUnchangedWithAddition(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 2.2.2.2"),
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 2.2.2.2"),
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] {
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) {
un, cre, del, mod = IncrementalDiff(existing, desired)
func TestMxPrio(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].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 {
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 {
t.Errorf("Got %d records to modify, but expected %d", len(mod), modCount)
}
if t.Failed() {
t.FailNow()
}
return
}

View file

@ -3,8 +3,6 @@ package gandi
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
@ -30,49 +28,10 @@ type GandiApi struct {
ZoneId int64
}
type cfRecord struct {
type gandiRecord struct {
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) {
if err := c.fetchDomainList(); err != nil {
return nil, err
@ -95,6 +54,7 @@ func (c *GandiApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
}
return ns, nil
}
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
domaininfo, err := c.getDomainInfo(dc.Name)
if err != nil {
@ -104,46 +64,23 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
if err != nil {
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))
for i, rec := range dc.Records {
n := &cfRecord{}
n.Id = 0
n.Name = rec.Name
n.Ttl = int64(rec.TTL)
if n.Ttl == 0 {
n.Ttl = 3600
if rec.Type == "MX" {
rec.Target = fmt.Sprintf("%d %s", rec.Priority, rec.Target)
}
n.Type = rec.Type
switch n.Type {
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
if rec.Type == "TXT" {
rec.Target = "\"" + rec.Target + "\"" // FIXME(tlim): Should do proper quoting.
}
expectedDiffRecords[i] = n
expectedRecordSets[i] = gandirecord.RecordSet{}
expectedRecordSets[i]["type"] = n.Type
expectedRecordSets[i]["name"] = n.Name
expectedRecordSets[i]["value"] = n.Value
if n.Ttl != 0 {
expectedRecordSets[i]["ttl"] = n.Ttl
}
expectedRecordSets[i]["type"] = rec.Type
expectedRecordSets[i]["name"] = rec.Name
expectedRecordSets[i]["value"] = rec.Target
expectedRecordSets[i]["ttl"] = rec.TTL
}
_, create, del, mod := diff.IncrementalDiff(foundDiffRecords, expectedDiffRecords)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(foundRecords)
// Print a list of changes. Generate an actual change that is the zone
changes := false
@ -160,7 +97,7 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
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{}
if changes {
corrections = append(corrections,

View file

@ -3,15 +3,13 @@ package gandi
import (
"fmt"
"github.com/StackExchange/dnscontrol/providers/diff"
)
import (
gandiclient "github.com/prasmussen/gandi-api/client"
gandidomain "github.com/prasmussen/gandi-api/domain"
gandizone "github.com/prasmussen/gandi-api/domain/zone"
gandirecord "github.com/prasmussen/gandi-api/domain/zone/record"
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.
@ -41,10 +39,18 @@ func (c *GandiApi) fetchDomainInfo(fqdn string) (*gandidomain.DomainInfo, error)
}
// 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)
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.
@ -75,11 +81,6 @@ func (c *GandiApi) createZone(name string) (*gandizone.ZoneInfo, error) {
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) {
var zone_id int64
if zoneinfo.Domains < 2 {
@ -169,3 +170,13 @@ func (c *GandiApi) createGandiZone(domainname string, zone_id int64, records []g
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
}
//convert to dnscontrol RecordConfig format
existingRecords := []diff.Record{}
existingRecords := []*models.RecordConfig{}
oldRRs := map[key]*dns.ResourceRecordSet{}
for _, set := range rrs {
nameWithoutDot := set.Name
@ -123,11 +123,7 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
}
}
w := []diff.Record{}
for _, want := range dc.Records {
if want.TTL == 0 {
want.TTL = 300
}
if want.Type == "MX" {
want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target)
want.Priority = 0
@ -135,24 +131,24 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
//add quotes to txts
want.Target = fmt.Sprintf(`"%s"`, want.Target)
}
w = append(w, want)
}
// 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{}
desc := ""
for _, c := range create {
desc += fmt.Sprintln(c)
changedKeys[keyForRec(c.Desired.(*models.RecordConfig))] = true
changedKeys[keyForRec(c.Desired)] = true
}
for _, d := range delete {
desc += fmt.Sprintln(d)
changedKeys[keyForRec(d.Existing.(*models.RecordConfig))] = true
changedKeys[keyForRec(d.Existing)] = true
}
for _, m := range modify {
desc += fmt.Sprintln(m)
changedKeys[keyForRec(m.Existing.(*models.RecordConfig))] = true
changedKeys[keyForRec(m.Existing)] = true
}
if len(changedKeys) == 0 {
return nil, nil

View file

@ -175,7 +175,11 @@ func TestGetNameservers(t *testing.T) {
t.Errorf("Test %d: %s", i, err)
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)
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff"
"strconv"
)
var defaultNameservers = []*models.Nameserver{
@ -23,42 +24,30 @@ func (n *nameDotCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Co
if err != nil {
return nil, err
}
actual := make([]diff.Record, len(records))
for i := range records {
actual[i] = records[i]
actual := make([]*models.RecordConfig, len(records))
for i, r := range records {
actual[i] = r.toRecord()
}
desired := make([]diff.Record, 0, len(dc.Records))
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)
}
checkNSModifications(dc)
_, create, del, mod := diff.IncrementalDiff(actual, desired)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(actual)
corrections := []*models.Correction{}
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) }}
corrections = append(corrections, c)
}
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) }}
corrections = append(corrections, c)
}
for _, chng := range mod {
old := chng.Existing.(*nameComRecord)
new := chng.Desired.(*models.RecordConfig)
old := chng.Existing.Original.(*nameComRecord)
new := chng.Desired
c := &models.Correction{Msg: chng.String(), F: func() error {
err := n.deleteRecord(old.RecordID, dc.Name)
if err != nil {
@ -90,21 +79,32 @@ type nameComRecord struct {
Priority string `json:"priority"`
}
func (r *nameComRecord) GetName() string {
return r.Name
}
func (r *nameComRecord) GetType() string {
return r.Type
}
func (r *nameComRecord) GetContent() string {
return r.Content
}
func (r *nameComRecord) GetComparisionData() string {
mxPrio := ""
if r.Type == "MX" {
mxPrio = fmt.Sprintf(" %s ", r.Priority)
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 {
// 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
}
newList = append(newList, rec)
}
dc.Records = newList
}
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 {

View file

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