Skip to content

Commit

Permalink
Add a TTL model
Browse files Browse the repository at this point in the history
This unifies the logic for handling the dnscontrol-default TTL, and
frees up the TTL value 0 for use by providers (e.g. Linode, which uses
it as its sentinel for its default TTL).

Closes StackExchange#2444.
  • Loading branch information
str4d committed Jul 10, 2023
1 parent 31bf652 commit 9197a19
Show file tree
Hide file tree
Showing 78 changed files with 371 additions and 297 deletions.
31 changes: 17 additions & 14 deletions commands/getZones.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,12 @@ func GetZone(args GetZoneArgs) error {
for i, recs := range zoneRecs {
zoneName := zones[i]

z := prettyzone.PrettySort(recs, zoneName, 0, nil)
z := prettyzone.PrettySort(recs, zoneName, models.EmptyTTL(), nil)
switch args.OutputFormat {

case "zone":
fmt.Fprintf(w, "$ORIGIN %s.\n", zoneName)
prettyzone.WriteZoneFileRC(w, z.Records, zoneName, uint32(args.DefaultTTL), nil)
prettyzone.WriteZoneFileRC(w, z.Records, zoneName, models.NewTTL(uint32(args.DefaultTTL)), nil)
fmt.Fprintln(w)

case "js", "djs":
Expand All @@ -243,12 +243,15 @@ func GetZone(args GetZoneArgs) error {
fmt.Fprintf(w, `D("%s", REG_CHANGEME%s`, zoneName, sep)
var o []string
o = append(o, fmt.Sprintf("DnsProvider(%s)", dspVariableName))
defaultTTL := uint32(args.DefaultTTL)
if defaultTTL == 0 {
defaultTTL := models.EmptyTTL()
if args.DefaultTTL != 0 {
defaultTTL = models.NewTTL(uint32(args.DefaultTTL))
}
if !defaultTTL.IsSet() {
defaultTTL = prettyzone.MostCommonTTL(recs)
}
if defaultTTL != models.DefaultTTL && defaultTTL != 0 {
o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL))
if defaultTTL.IsSet() {
o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL.Value()))
}
for _, rec := range recs {
if (rec.Type == "CNAME") && (rec.Name == "@") {
Expand Down Expand Up @@ -287,7 +290,7 @@ func GetZone(args GetZoneArgs) error {
}

fmt.Fprintf(w, "%s\t%s\t%d\tIN\t%s\t%s%s\n",
rec.NameFQDN, rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined(), cfproxy)
rec.NameFQDN, rec.Name, rec.TTL.Value(), rec.Type, rec.GetTargetCombined(), cfproxy)
}

default:
Expand All @@ -307,15 +310,15 @@ func jsonQuoted(i string) string {
return string(b)
}

func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL models.TTL) string {

target := rec.GetTargetCombined()

ttl := uint32(0)
ttl := models.EmptyTTL()
ttlop := ""
if rec.TTL != defaultTTL && rec.TTL != 0 {
if rec.TTL != defaultTTL && rec.TTL.IsSet() {
ttl = rec.TTL
ttlop = fmt.Sprintf(", TTL(%d)", ttl)
ttlop = fmt.Sprintf(", TTL(%d)", ttl.Value())
}

cfproxy := ""
Expand Down Expand Up @@ -386,7 +389,7 @@ func makeCaa(rec *models.RecordConfig, ttlop string) string {
// TODO(tlim): Generate a CAA_BUILDER() instead?
}

func makeR53alias(rec *models.RecordConfig, ttl uint32) string {
func makeR53alias(rec *models.RecordConfig, ttl models.TTL) string {
items := []string{
"'" + rec.Name + "'",
"'" + rec.R53Alias["type"] + "'",
Expand All @@ -395,8 +398,8 @@ func makeR53alias(rec *models.RecordConfig, ttl uint32) string {
if z, ok := rec.R53Alias["zone_id"]; ok {
items = append(items, "R53_ZONE('"+z+"')")
}
if ttl != 0 {
items = append(items, fmt.Sprintf("TTL(%d)", ttl))
if ttl.IsSet() {
items = append(items, fmt.Sprintf("TTL(%d)", ttl.Value()))
}
return rec.Type + "(" + strings.Join(items, ", ") + ")"
}
8 changes: 4 additions & 4 deletions commands/r53_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestR53Test_1(t *testing.T) {
rec.R53Alias = make(map[string]string)
rec.R53Alias["type"] = "A"
w := `R53_ALIAS('foo', 'A', 'bar')`
if g := makeR53alias(&rec, 0); g != w {
if g := makeR53alias(&rec, models.EmptyTTL()); g != w {
t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
}
}
Expand All @@ -32,7 +32,7 @@ func TestR53Test_1ttl(t *testing.T) {
rec.R53Alias = make(map[string]string)
rec.R53Alias["type"] = "A"
w := `R53_ALIAS('foo', 'A', 'bar', TTL(321))`
if g := makeR53alias(&rec, 321); g != w {
if g := makeR53alias(&rec, models.NewTTL(321)); g != w {
t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
}
}
Expand All @@ -48,7 +48,7 @@ func TestR53Test_2(t *testing.T) {
rec.R53Alias["type"] = "A"
rec.R53Alias["zone_id"] = "blarg"
w := `R53_ALIAS('foo', 'A', 'bar', R53_ZONE('blarg'))`
if g := makeR53alias(&rec, 0); g != w {
if g := makeR53alias(&rec, models.EmptyTTL()); g != w {
t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
}
}
Expand All @@ -64,7 +64,7 @@ func TestR53Test_2ttl(t *testing.T) {
rec.R53Alias["type"] = "A"
rec.R53Alias["zone_id"] = "blarg"
w := `R53_ALIAS('foo', 'A', 'bar', R53_ZONE('blarg'), TTL(123))`
if g := makeR53alias(&rec, 123); g != w {
if g := makeR53alias(&rec, models.NewTTL(123)); g != w {
t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
}
}
4 changes: 2 additions & 2 deletions integrationTest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ func loc(name string, d1 uint8, m1 uint8, s1 float32, ns string,
func makeRec(name, target, typ string) *models.RecordConfig {
r := &models.RecordConfig{
Type: typ,
TTL: 300,
TTL: models.NewTTL(300),
}
SetLabel(r, name, "**current-domain**")
r.SetTarget(target)
Expand Down Expand Up @@ -734,7 +734,7 @@ func txt(name, target string) *models.RecordConfig {

// func (r *models.RecordConfig) ttl(t uint32) *models.RecordConfig {
func ttl(r *models.RecordConfig, t uint32) *models.RecordConfig {
r.TTL = t
r.TTL = models.NewTTL(t)
return r
}

Expand Down
42 changes: 42 additions & 0 deletions models/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,48 @@ import (
// DefaultTTL is applied to any DNS record without an explicit TTL.
const DefaultTTL = uint32(300)

// TTL implements an optional TTL field, with a default of DefaultTTL.
type TTL struct {
value *uint32
}

func NewTTL(ttl uint32) TTL {
value := new(uint32)
*value = ttl
return TTL{
value,
}
}

// EmptyTTL returns a new TTL without an explicit value.
func EmptyTTL() TTL {
return TTL{
value: nil,
}
}

func (ttl TTL) IsSet() bool {
return ttl.value != nil
}

func (ttl TTL) Value() uint32 {
if ttl.IsSet() {
return *ttl.value
} else {
return DefaultTTL
}
}

func (ttl *TTL) ValueRef() *uint32 {
if ttl.IsSet() {
return ttl.value
} else {
defaultTTL := new(uint32)
*defaultTTL = DefaultTTL
return defaultTTL
}
}

// DNSConfig describes the desired DNS configuration, usually loaded from dnsconfig.js.
type DNSConfig struct {
Registrars []*RegistrarConfig `json:"registrars"`
Expand Down
10 changes: 6 additions & 4 deletions models/dns_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package models

import "testing"
import (
"testing"
)

func TestRR(t *testing.T) {
experiment := RecordConfig{
Type: "A",
Name: "foo",
NameFQDN: "foo.example.com",
target: "1.2.3.4",
TTL: 0,
TTL: EmptyTTL(),
MxPreference: 0,
}
expected := "foo.example.com.\t300\tIN\tA\t1.2.3.4"
Expand All @@ -22,7 +24,7 @@ func TestRR(t *testing.T) {
Name: "@",
NameFQDN: "example.com",
target: "mailto:[email protected]",
TTL: 300,
TTL: NewTTL(300),
CaaTag: "iodef",
CaaFlag: 1,
}
Expand All @@ -37,7 +39,7 @@ func TestRR(t *testing.T) {
Name: "@",
NameFQDN: "_443._tcp.example.com",
target: "abcdef0123456789",
TTL: 300,
TTL: NewTTL(300),
TlsaUsage: 0,
TlsaSelector: 0,
TlsaMatchingType: 1,
Expand Down
2 changes: 1 addition & 1 deletion models/dnsrr.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
header := rr.Header()
rc := new(RecordConfig)
rc.Type = dns.TypeToString[header.Rrtype]
rc.TTL = header.Ttl
rc.TTL = NewTTL(header.Ttl)
rc.Original = rr
rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin)
var err error
Expand Down
20 changes: 12 additions & 8 deletions models/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ type RecordConfig struct {
SubDomain string `json:"subdomain,omitempty"`
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
target string // If a name, must end with "."
TTL uint32 `json:"ttl,omitempty"`
TTL TTL `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.

Expand Down Expand Up @@ -158,7 +158,7 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
SubDomain string `json:"subdomain,omitempty"`
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
target string // If a name, must end with "."
TTL uint32 `json:"ttl,omitempty"`
TTL *uint32 `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.

Expand Down Expand Up @@ -212,6 +212,9 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
copier.CopyWithOption(&rc, &recj, copier.Option{IgnoreEmpty: true, DeepCopy: true})
// Set each unexported field.
rc.SetTarget(recj.Target)
if recj.TTL != nil {
rc.TTL = NewTTL(*recj.TTL)
}

// Some sanity checks:
if recj.Type != rc.Type {
Expand All @@ -223,6 +226,10 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
if recj.Name != rc.Name {
panic("DEBUG: NAME NOT COPIED\n")
}
if !((recj.TTL == nil && !rc.TTL.IsSet()) ||
(recj.TTL != nil && rc.TTL.IsSet() && *recj.TTL == *rc.TTL.value)) {
panic("DEBUG: TTL NOT COPIED\n")
}

return nil
}
Expand Down Expand Up @@ -321,10 +328,10 @@ func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string {
var content string
switch rc.Type {
case "SOA":
content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL)
content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL.Value())
// SoaSerial is not used in comparison
default:
content = fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL)
content = fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL.Value())
}
for _, valueMap := range extraMaps {
// sort the extra values map keys to perform a deterministic
Expand Down Expand Up @@ -373,10 +380,7 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.Header().Name = rc.NameFQDN + "."
rr.Header().Rrtype = rdtype
rr.Header().Class = dns.ClassINET
rr.Header().Ttl = rc.TTL
if rc.TTL == 0 {
rr.Header().Ttl = DefaultTTL
}
rr.Header().Ttl = rc.TTL.Value()

// Fill in the data.
switch rdtype { // #rtype_variations
Expand Down
4 changes: 2 additions & 2 deletions models/record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func TestRecordConfig_Copy(t *testing.T) {
SubDomain: "sub",
NameFQDN: "namef",
target: "targette",
TTL: 12345,
TTL: NewTTL(12345),
Metadata: map[string]string{"me": "ah", "da": "ta"},
MxPreference: 123,
SrvPriority: 223,
Expand Down Expand Up @@ -189,7 +189,7 @@ func TestRecordConfig_Copy(t *testing.T) {
SubDomain: tt.fields.SubDomain,
NameFQDN: tt.fields.NameFQDN,
target: tt.fields.target,
TTL: tt.fields.TTL,
TTL: NewTTL(tt.fields.TTL),
Metadata: tt.fields.Metadata,
MxPreference: tt.fields.MxPreference,
SrvPriority: tt.fields.SrvPriority,
Expand Down
2 changes: 1 addition & 1 deletion models/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (rc *RecordConfig) GetTargetSortable() string {

// GetTargetDebug returns a string with the various fields spelled out.
func (rc *RecordConfig) GetTargetDebug() string {
content := fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, rc.target, rc.TTL)
content := fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, rc.target, rc.TTL.Value())
switch rc.Type { // #rtype_variations
case "A", "AAAA", "CNAME", "NS", "PTR", "TXT", "AKAMAICDN":
// Nothing special.
Expand Down
4 changes: 2 additions & 2 deletions pkg/diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func myRecord(s string) *models.RecordConfig {
ttl, _ := strconv.ParseUint(parts[2], 10, 32)
r := &models.RecordConfig{
Type: parts[1],
TTL: uint32(ttl),
TTL: models.NewTTL(uint32(ttl)),
Metadata: map[string]string{},
}
r.SetLabel(parts[0], "example.com")
Expand Down Expand Up @@ -72,7 +72,7 @@ func TestUnchangedWithAddition(t *testing.T) {

// s stringifies a RecordConfig for testing purposes.
func s(rc *models.RecordConfig) string {
return fmt.Sprintf("%s %s %d %s", rc.GetLabel(), rc.Type, rc.TTL, rc.GetTargetCombined())
return fmt.Sprintf("%s %s %d %s", rc.GetLabel(), rc.Type, rc.TTL.Value(), rc.GetTargetCombined())
}

func TestOutOfOrderRecords(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/diff2/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func humanDiff(a, b targetConfig) string {
}

// Just the TTLs are different:
return fmt.Sprintf("%s ttl=(%d->%d)", a.comparableNoTTL, a.rec.TTL, b.rec.TTL)
return fmt.Sprintf("%s ttl=(%d->%d)", a.comparableNoTTL, a.rec.TTL.Value(), b.rec.TTL.Value())
}

func diffTargets(existing, desired []targetConfig) ChangeList {
Expand Down
32 changes: 16 additions & 16 deletions pkg/diff2/analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ func init() {
color.NoColor = true
}

var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [0]
var testDataAA5678 = makeRec("laba", "A", "5.6.7.8") //
var testDataAA1234ttl700 = makeRecTTL("laba", "A", "1.2.3.4", 700) //
var testDataAA5678ttl700 = makeRecTTL("laba", "A", "5.6.7.8", 700) //
var testDataAMX10a = makeRec("laba", "MX", "10 laba") // [1]
var testDataCCa = makeRec("labc", "CNAME", "laba") // [2]
var testDataEA15 = makeRec("labe", "A", "10.10.10.15") // [3]
var e4 = makeRec("labe", "A", "10.10.10.16") // [4]
var e5 = makeRec("labe", "A", "10.10.10.17") // [5]
var e6 = makeRec("labe", "A", "10.10.10.18") // [6]
var e7 = makeRec("labg", "NS", "10.10.10.15") // [7]
var e8 = makeRec("labg", "NS", "10.10.10.16") // [8]
var e9 = makeRec("labg", "NS", "10.10.10.17") // [9]
var e10 = makeRec("labg", "NS", "10.10.10.18") // [10]
var e11mx = makeRec("labh", "MX", "22 ttt") // [11]
var e11 = makeRec("labh", "CNAME", "labd") // [11]
var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [0]
var testDataAA5678 = makeRec("laba", "A", "5.6.7.8") //
var testDataAA1234ttl700 = makeRecTTL("laba", "A", "1.2.3.4", models.NewTTL(700)) //
var testDataAA5678ttl700 = makeRecTTL("laba", "A", "5.6.7.8", models.NewTTL(700)) //
var testDataAMX10a = makeRec("laba", "MX", "10 laba") // [1]
var testDataCCa = makeRec("labc", "CNAME", "laba") // [2]
var testDataEA15 = makeRec("labe", "A", "10.10.10.15") // [3]
var e4 = makeRec("labe", "A", "10.10.10.16") // [4]
var e5 = makeRec("labe", "A", "10.10.10.17") // [5]
var e6 = makeRec("labe", "A", "10.10.10.18") // [6]
var e7 = makeRec("labg", "NS", "10.10.10.15") // [7]
var e8 = makeRec("labg", "NS", "10.10.10.16") // [8]
var e9 = makeRec("labg", "NS", "10.10.10.17") // [9]
var e10 = makeRec("labg", "NS", "10.10.10.18") // [10]
var e11mx = makeRec("labh", "MX", "22 ttt") // [11]
var e11 = makeRec("labh", "CNAME", "labd") // [11]
var testDataApexMX1aaa = makeRec("", "MX", "1 aaa")

var testDataAA1234clone = makeRec("laba", "A", "1.2.3.4") // [0']
Expand Down
Loading

0 comments on commit 9197a19

Please sign in to comment.