Disable records instead of deleting them
A disabled record stays in Jarl with its ID, TTL and comment but is left out of the zone; the zone file lists it as a comment. Web UI toggle, jarl record disable|enable, record.disable|enable operations and API endpoints. Database schema 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f04ab523b
commit
295c549479
29 files changed
+815
-60
No files matched your search
@@ -88,7 +88,7 @@ func DiffOps(from, to *model.State) []model.Op {
|
||||
}
|
||||
for _, k := range sortedKeys(to.Records) {
|
||||
n := to.Records[k]
|
||||
in := RecordInput{ZoneID: n.ZoneID, Name: &n.Name, Type: &n.Type, Data: &n.Data, Comment: &n.Comment}
|
||||
in := RecordInput{ZoneID: n.ZoneID, Name: &n.Name, Type: &n.Type, Data: &n.Data, Comment: &n.Comment, Disabled: &n.Disabled}
|
||||
if n.TTL != nil {
|
||||
in.TTL = model.Some(*n.TTL)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
func disableReq(id int64, ptr bool) Request {
|
||||
return NewRequest(OpRecordDisable, id, RecordInput{PTR: ptr})
|
||||
}
|
||||
|
||||
func enableReq(id int64, ptr bool) Request {
|
||||
return NewRequest(OpRecordEnable, id, RecordInput{PTR: ptr})
|
||||
}
|
||||
|
||||
func (env *testEnv) zoneFile(name string) string {
|
||||
env.t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join(env.dir, "zones/_default", name+".zone"))
|
||||
if err != nil {
|
||||
env.t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func serialOf(env *testEnv, zone string) uint32 {
|
||||
return env.state().ZonesNamed(zone)[0].SOA.Serial
|
||||
}
|
||||
|
||||
func TestDisableRecord(t *testing.T) {
|
||||
env := newEnv(t)
|
||||
setupZone(env)
|
||||
env.submit(env.admin, rec("example.com", "www", "A", "192.0.2.10"))
|
||||
www := findRec(env.state(), "www.example.com.", "A")
|
||||
serial := serialOf(env, "example.com")
|
||||
|
||||
res := env.submit(env.admin, disableReq(www.ID, false))
|
||||
if len(res.Ops) != 1 || res.Ops[0].Summary != "disable www.example.com. A 192.0.2.10" {
|
||||
t.Fatalf("ops = %+v", res.Ops)
|
||||
}
|
||||
if r := env.state().Records[www.ID]; r == nil || !r.Disabled {
|
||||
t.Fatalf("record = %+v", r)
|
||||
}
|
||||
zf := env.zoneFile("example.com")
|
||||
if !strings.Contains(zf, "; disabled: www\t\tIN\tA\t192.0.2.10\n") || strings.Contains(zf, "\nwww\t") {
|
||||
t.Errorf("zone file:\n%s", zf)
|
||||
}
|
||||
if s := serialOf(env, "example.com"); s != serial+1 {
|
||||
t.Errorf("serial after disable = %d, want %d", s, serial+1)
|
||||
}
|
||||
// Editing a disabled record changes nothing that is served.
|
||||
env.submit(env.admin, NewRequest(OpRecordUpdate, www.ID, RecordInput{Comment: model.Ptr("old web server"), Data: model.Ptr("192.0.2.11")}))
|
||||
if s := serialOf(env, "example.com"); s != serial+1 {
|
||||
t.Errorf("serial after editing a disabled record = %d", s)
|
||||
}
|
||||
// Disabling it again says so.
|
||||
if e := env.submitErr(env.admin, disableReq(www.ID, false)); e.Kind != KindConflict || !strings.Contains(e.Message, "already disabled") {
|
||||
t.Errorf("second disable: %+v", e)
|
||||
}
|
||||
|
||||
env.submit(env.admin, enableReq(www.ID, false))
|
||||
r := env.state().Records[www.ID]
|
||||
if r.Disabled || r.Data != "192.0.2.11" || r.Comment != "old web server" {
|
||||
t.Errorf("enabled record = %+v", r)
|
||||
}
|
||||
if zf := env.zoneFile("example.com"); !strings.Contains(zf, "\nwww\t\tIN\tA\t192.0.2.11\t; old web server\n") || strings.Contains(zf, "disabled") {
|
||||
t.Errorf("zone file after enable:\n%s", zf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValidation(t *testing.T) {
|
||||
env := newEnv(t)
|
||||
setupZone(env)
|
||||
// The zone must keep a name server.
|
||||
ns := findRec(env.state(), "example.com.", "NS")
|
||||
if e := env.submitErr(env.admin, disableReq(ns.ID, false)); e.Kind != KindValidation {
|
||||
t.Errorf("disabling the only NS: %+v", e)
|
||||
}
|
||||
|
||||
// A disabled record doesn't count for the CNAME rule...
|
||||
env.submit(env.admin, rec("example.com", "www", "A", "192.0.2.10"))
|
||||
www := findRec(env.state(), "www.example.com.", "A")
|
||||
env.submit(env.admin, disableReq(www.ID, false))
|
||||
res := env.submit(env.admin, rec("example.com", "www", "CNAME", "ns1"))
|
||||
// ...but there's a warning that it can't come back,
|
||||
if len(res.Problems) != 1 || res.Problems[0].Code != "disabled_conflict" || res.Problems[0].ObjectID != www.ID {
|
||||
t.Errorf("problems = %+v", res.Problems)
|
||||
}
|
||||
// and enabling it is checked like adding it.
|
||||
if e := env.submitErr(env.admin, enableReq(www.ID, false)); e.Kind != KindValidation || !strings.Contains(e.Message, "CNAME") {
|
||||
t.Errorf("enabling next to a CNAME: %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableByMatchAndPTR(t *testing.T) {
|
||||
env := newEnv(t)
|
||||
setupZone(env)
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.0/24")}))
|
||||
env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", Name: model.Ptr("nas"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.20"), PTR: true}))
|
||||
ptrDisabled := func() bool {
|
||||
return findRec(env.state(), "20.2.0.192.in-addr.arpa.", "PTR").Disabled
|
||||
}
|
||||
|
||||
// By match, with the PTR record.
|
||||
match := RecordInput{Zone: "example.com", Name: model.Ptr("nas"), Type: model.Ptr("A"), PTR: true}
|
||||
res := env.submit(env.admin, NewRequest(OpRecordDisable, 0, match))
|
||||
if len(res.Ops) != 2 || res.Ops[1].Summary != "disable 20.2.0.192.in-addr.arpa. PTR nas.example.com." {
|
||||
t.Fatalf("ops = %+v", res.Ops)
|
||||
}
|
||||
if !findRec(env.state(), "nas.example.com.", "A").Disabled || !ptrDisabled() {
|
||||
t.Fatal("A or PTR not disabled")
|
||||
}
|
||||
env.submit(env.admin, NewRequest(OpRecordEnable, 0, match))
|
||||
if findRec(env.state(), "nas.example.com.", "A").Disabled || ptrDisabled() {
|
||||
t.Fatal("A or PTR not enabled")
|
||||
}
|
||||
// Without ptr, the PTR record stays as it is.
|
||||
a := findRec(env.state(), "nas.example.com.", "A")
|
||||
env.submit(env.admin, disableReq(a.ID, false))
|
||||
if ptrDisabled() {
|
||||
t.Error("PTR disabled without ptr")
|
||||
}
|
||||
|
||||
// Data narrows the match; nothing matching is an error unless missing_ok.
|
||||
env.submit(env.admin, rec("example.com", "api", "A", "192.0.2.30"), rec("example.com", "api", "A", "192.0.2.31"))
|
||||
one := RecordInput{Zone: "example.com", Name: model.Ptr("api"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.31")}
|
||||
env.submit(env.admin, NewRequest(OpRecordDisable, 0, one))
|
||||
if findRecData(env.state(), "api.example.com.", "192.0.2.30").Disabled || !findRecData(env.state(), "api.example.com.", "192.0.2.31").Disabled {
|
||||
t.Error("data match wrong")
|
||||
}
|
||||
none := RecordInput{Zone: "example.com", Name: model.Ptr("nope"), Type: model.Ptr("A")}
|
||||
if e := env.submitErr(env.admin, NewRequest(OpRecordDisable, 0, none)); e.Kind != KindNotFound {
|
||||
t.Errorf("no match: %+v", e)
|
||||
}
|
||||
// missing_ok: nothing to do (an error the CLI's --missing-ok accepts).
|
||||
none.MissingOK = true
|
||||
if e := env.submitErr(env.admin, NewRequest(OpRecordDisable, 0, none)); e.Message != "nothing to do" {
|
||||
t.Errorf("missing_ok: %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableRRSetAndRevert(t *testing.T) {
|
||||
env := newEnv(t)
|
||||
setupZone(env)
|
||||
env.submit(env.admin, rec("example.com", "api", "A", "192.0.2.30"), rec("example.com", "api", "A", "192.0.2.31"))
|
||||
r30 := findRecData(env.state(), "api.example.com.", "192.0.2.30")
|
||||
r31 := findRecData(env.state(), "api.example.com.", "192.0.2.31")
|
||||
env.submit(env.admin, disableReq(r30.ID, false), disableReq(r31.ID, false))
|
||||
|
||||
// Setting the RRset enables a disabled record with a given value and
|
||||
// leaves the other disabled one alone.
|
||||
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", Name: "api", Type: "A", Data: []string{"192.0.2.30", "192.0.2.40"}}))
|
||||
s := env.state()
|
||||
if r := s.Records[r30.ID]; r == nil || r.Disabled {
|
||||
t.Errorf(".30 = %+v, want kept and enabled", r)
|
||||
}
|
||||
if r := s.Records[r31.ID]; r == nil || !r.Disabled {
|
||||
t.Errorf(".31 = %+v, want kept and disabled", r)
|
||||
}
|
||||
|
||||
// Reverting a disabling changeset enables the record again.
|
||||
res := env.submit(env.admin, disableReq(r30.ID, false))
|
||||
rev, err := env.e.Revert(env.ctx, res.ChangesetID, env.admin)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := env.e.Apply(env.ctx, rev.ID, env.admin, ApplyOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if env.state().Records[r30.ID].Disabled {
|
||||
t.Error("revert did not enable the record")
|
||||
}
|
||||
}
|
||||
+139
-6
@@ -155,6 +155,8 @@ func (x *expander) expand(req Request) ([]model.Op, error) {
|
||||
return x.recordDelete(req)
|
||||
case OpRRSetReplace:
|
||||
return x.rrsetReplace(req)
|
||||
case OpRecordDisable, OpRecordEnable:
|
||||
return x.recordSetDisabled(req, req.Op == OpRecordDisable)
|
||||
case OpViewCreate:
|
||||
return x.viewCreate(req)
|
||||
case OpViewUpdate, OpViewDelete:
|
||||
@@ -413,6 +415,9 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
if src != nil {
|
||||
for _, r := range x.st.ZoneRecords(src.ID) {
|
||||
ri := RecordInput{Name: model.Ptr(r.Name), Type: model.Ptr(r.Type), Data: model.Ptr(r.Data), Comment: model.Ptr(r.Comment)}
|
||||
if r.Disabled {
|
||||
ri.Disabled = model.Ptr(true)
|
||||
}
|
||||
if r.TTL != nil {
|
||||
ri.TTL = model.Some(*r.TTL)
|
||||
} else {
|
||||
@@ -502,6 +507,10 @@ func (x *expander) ptrCreate(z *model.Zone, owner string, p *rr.Parsed) ([]model
|
||||
for _, r := range x.st.ZoneRecords(rz.ID) {
|
||||
if r.Name == ptrName && r.Type == "PTR" {
|
||||
if strings.EqualFold(r.Data, owner) {
|
||||
if r.Disabled {
|
||||
in := RecordInput{Disabled: model.Ptr(false)}
|
||||
return []model.Op{newOp(OpRecordUpdate, r.ID, r.Version, in, fmt.Sprintf("enable %s PTR %s", r.Name, r.Data))}, nil
|
||||
}
|
||||
return nil, nil // already there
|
||||
}
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("%s already has a PTR record (%s); adding another one", ptrName, r.Data))
|
||||
@@ -515,6 +524,34 @@ func (x *expander) ptrCreate(z *model.Zone, owner string, p *rr.Parsed) ([]model
|
||||
return []model.Op{newOp(OpRecordCreate, ids[0], 0, in, fmt.Sprintf("add %s IN PTR %s", ptrName, owner))}, nil
|
||||
}
|
||||
|
||||
// ptrSetDisabled returns operations disabling or enabling the PTR record
|
||||
// of an address record.
|
||||
func (x *expander) ptrSetDisabled(z *model.Zone, owner string, p *rr.Parsed, disabled bool) []model.Op {
|
||||
addr, ok := addrOf(p)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rz, ptrName := reverseZoneFor(x.st, z.ViewID, addr)
|
||||
if rz == nil {
|
||||
return nil
|
||||
}
|
||||
var ops []model.Op
|
||||
for _, r := range x.st.ZoneRecords(rz.ID) {
|
||||
if r.Name == ptrName && r.Type == "PTR" && strings.EqualFold(r.Data, owner) && r.Disabled != disabled {
|
||||
in := RecordInput{Disabled: model.Ptr(disabled)}
|
||||
ops = append(ops, newOp(OpRecordUpdate, r.ID, r.Version, in, fmt.Sprintf("%s %s PTR %s", enableWord(disabled), r.Name, r.Data)))
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
func enableWord(disabled bool) string {
|
||||
if disabled {
|
||||
return "disable"
|
||||
}
|
||||
return "enable"
|
||||
}
|
||||
|
||||
func (x *expander) ptrDelete(z *model.Zone, owner string, p *rr.Parsed) []model.Op {
|
||||
addr, ok := addrOf(p)
|
||||
if !ok {
|
||||
@@ -635,8 +672,15 @@ func (x *expander) recordUpdate(req Request) ([]model.Op, error) {
|
||||
if in.Comment != nil && *in.Comment != r.Comment {
|
||||
changes = append(changes, "comment")
|
||||
}
|
||||
toggled := in.Disabled != nil && *in.Disabled != r.Disabled
|
||||
summary := fmt.Sprintf("change %s %s %s", r.Name, r.Type, r.Data)
|
||||
if len(changes) > 0 {
|
||||
switch {
|
||||
case toggled && len(changes) == 0:
|
||||
summary = fmt.Sprintf("%s %s %s %s", enableWord(*in.Disabled), r.Name, r.Type, r.Data)
|
||||
case toggled:
|
||||
changes = append(changes, enableWord(*in.Disabled)+"d")
|
||||
fallthrough
|
||||
case len(changes) > 0:
|
||||
summary += ": " + strings.Join(changes, ", ")
|
||||
}
|
||||
base := req.BaseVersion
|
||||
@@ -644,15 +688,28 @@ func (x *expander) recordUpdate(req Request) ([]model.Op, error) {
|
||||
base = r.Version
|
||||
}
|
||||
ops := []model.Op{newOp(OpRecordUpdate, r.ID, base, in, summary)}
|
||||
if ptr && (p.Data != r.Data || owner != r.Name) {
|
||||
disabled := r.Disabled
|
||||
if in.Disabled != nil {
|
||||
disabled = *in.Disabled
|
||||
}
|
||||
moved := p.Data != r.Data || owner != r.Name
|
||||
if ptr && moved {
|
||||
if old, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, old)...)
|
||||
}
|
||||
more, err := x.ptrCreate(z, owner, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !disabled {
|
||||
more, err := x.ptrCreate(z, owner, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
if ptr && !moved && toggled {
|
||||
if _, ok := addrOf(p); !ok {
|
||||
return nil, errf(KindBadRequest, "ptr only applies to A and AAAA records")
|
||||
}
|
||||
ops = append(ops, x.ptrSetDisabled(z, owner, p, disabled)...)
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
@@ -725,6 +782,82 @@ func (x *expander) recordDelete(req Request) ([]model.Op, error) {
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
// recordSetDisabled expands record.disable and record.enable: the record
|
||||
// with the request's ID, or the records matching zone, name, type and
|
||||
// (optionally) data.
|
||||
func (x *expander) recordSetDisabled(req Request, disabled bool) ([]model.Op, error) {
|
||||
var in RecordInput
|
||||
if err := decode(req.Data, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var recs []*model.Record
|
||||
if req.ID != 0 {
|
||||
r := x.st.Records[req.ID]
|
||||
if r == nil {
|
||||
return nil, errf(KindNotFound, "record %d does not exist", req.ID)
|
||||
}
|
||||
recs = append(recs, r)
|
||||
} else {
|
||||
z, err := x.resolveZone(in.ZoneID, in.Zone, in.View)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Name == nil || in.Type == nil {
|
||||
return nil, errf(KindBadRequest, "finding records needs a name and a type (or give the record id)")
|
||||
}
|
||||
owner, err := dnsname.Owner(*in.Name, z.Name)
|
||||
if err != nil {
|
||||
return nil, errf(KindBadRequest, "%v", err)
|
||||
}
|
||||
typ, _, err := rr.NormalizeType(*in.Type)
|
||||
if err != nil {
|
||||
return nil, errf(KindBadRequest, "%v", err)
|
||||
}
|
||||
data, what := "", owner+" "+typ
|
||||
if in.Data != nil && strings.TrimSpace(*in.Data) != "" {
|
||||
_, p, err := parseRecord(z, owner, typ, *in.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, what = p.Data, what+" "+p.Data
|
||||
}
|
||||
for _, r := range x.st.ZoneRecords(z.ID) {
|
||||
if r.Name == owner && r.Type == typ && (data == "" || strings.EqualFold(r.Data, data)) {
|
||||
recs = append(recs, r)
|
||||
}
|
||||
}
|
||||
if len(recs) == 0 {
|
||||
if in.MissingOK {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errf(KindNotFound, "no record %s in zone %s", dnsname.Display(what), dnsname.Display(z.Name))
|
||||
}
|
||||
}
|
||||
var ops []model.Op
|
||||
for _, r := range recs {
|
||||
if r.Disabled == disabled {
|
||||
continue // already so
|
||||
}
|
||||
base := 0
|
||||
if req.ID != 0 {
|
||||
base = req.BaseVersion
|
||||
}
|
||||
more, err := x.recordUpdate(Request{Op: OpRecordUpdate, ID: r.ID, BaseVersion: base, Data: mustJSON(RecordInput{Disabled: model.Ptr(disabled), PTR: in.PTR})})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
if len(ops) == 0 && !in.MissingOK {
|
||||
what := fmt.Sprintf("%s %s %s is", recs[0].Name, recs[0].Type, recs[0].Data)
|
||||
if len(recs) > 1 {
|
||||
what = fmt.Sprintf("all %d %s %s records are", len(recs), recs[0].Name, recs[0].Type)
|
||||
}
|
||||
return nil, errf(KindConflict, "%s already %sd", what, enableWord(disabled))
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
func (x *expander) rrsetReplace(req Request) ([]model.Op, error) {
|
||||
var in RRSetInput
|
||||
if err := decode(req.Data, &in); err != nil {
|
||||
|
||||
@@ -29,6 +29,12 @@ const (
|
||||
OpOptionUpdate = "option.update"
|
||||
OpOptionDelete = "option.delete"
|
||||
OpSettingsUpdate = "settings.update"
|
||||
|
||||
// Requests only: disabling and enabling records, by ID or by match
|
||||
// (zone, name, type and optionally data), become record.update
|
||||
// operations.
|
||||
OpRecordDisable = "record.disable"
|
||||
OpRecordEnable = "record.enable"
|
||||
)
|
||||
|
||||
// OpKinds lists all operation kinds.
|
||||
@@ -121,8 +127,10 @@ type RecordInput struct {
|
||||
TTL model.Opt[uint32] `json:"ttl,omitzero"` // null: use the zone default
|
||||
Data *string `json:"data,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
// PTR also creates (or deletes) the matching PTR record in the
|
||||
// reverse zone, for A and AAAA records.
|
||||
// Disabled keeps the record in Jarl but out of the zone.
|
||||
Disabled *bool `json:"disabled,omitempty"`
|
||||
// PTR also creates (or deletes, disables or enables) the matching PTR
|
||||
// record in the reverse zone, for A and AAAA records.
|
||||
PTR bool `json:"ptr,omitempty"`
|
||||
// MissingOK makes a delete-by-match succeed when nothing matches.
|
||||
MissingOK bool `json:"missing_ok,omitempty"`
|
||||
|
||||
+22
-4
@@ -373,7 +373,7 @@ func parseRecord(z *model.Zone, name, typ, data string) (string, *rr.Parsed, err
|
||||
|
||||
func rrsetTTL(st *model.State, zoneID int64, name, typ string, exceptID int64) (*uint32, bool) {
|
||||
for _, r := range st.Records {
|
||||
if r.ZoneID == zoneID && r.Name == name && r.Type == typ && r.ID != exceptID {
|
||||
if r.ZoneID == zoneID && r.Name == name && r.Type == typ && r.ID != exceptID && !r.Disabled {
|
||||
if r.TTL == nil {
|
||||
return nil, true
|
||||
}
|
||||
@@ -422,6 +422,9 @@ func applyRecordCreate(st *model.State, op model.Op, c *applyCtx) error {
|
||||
if in.Comment != nil {
|
||||
r.Comment = *in.Comment
|
||||
}
|
||||
if in.Disabled != nil {
|
||||
r.Disabled = *in.Disabled
|
||||
}
|
||||
for _, w := range p.Warnings {
|
||||
c.warn("%s %s: %s", dnsname.Display(owner), p.Type, w)
|
||||
}
|
||||
@@ -498,6 +501,9 @@ func applyRecordUpdate(st *model.State, op model.Op, c *applyCtx) error {
|
||||
if in.Comment != nil {
|
||||
r.Comment = *in.Comment
|
||||
}
|
||||
if in.Disabled != nil {
|
||||
r.Disabled = *in.Disabled
|
||||
}
|
||||
r.Touch(c.user(), c.now)
|
||||
return nil
|
||||
}
|
||||
@@ -617,20 +623,32 @@ func applyRRSetReplace(st *model.State, op model.Op, c *applyCtx) error {
|
||||
} else if t, ok := rrsetTTL(st, z.ID, owner, typ, 0); ok {
|
||||
ttl, ttlKnown = t, true
|
||||
}
|
||||
// The RRset becomes exactly the given data. A disabled record with
|
||||
// one of the values is enabled again; other disabled records are left
|
||||
// alone.
|
||||
keep := map[string]bool{}
|
||||
for id, r := range st.Records {
|
||||
// (In ID order, so which of two identical records is kept doesn't
|
||||
// depend on map order.)
|
||||
for _, id := range sortedKeys(st.Records) {
|
||||
r := st.Records[id]
|
||||
if r.ZoneID != z.ID || r.Name != owner || r.Type != typ {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(r.Data)
|
||||
if seen[k] && !keep[k] {
|
||||
keep[k] = true
|
||||
if ttlKnown && !sameTTL(r.TTL, ttl) {
|
||||
r.TTL = copyTTL(ttl)
|
||||
if (ttlKnown && !sameTTL(r.TTL, ttl)) || r.Disabled {
|
||||
if ttlKnown {
|
||||
r.TTL = copyTTL(ttl)
|
||||
}
|
||||
r.Disabled = false
|
||||
r.Touch(c.user(), c.now)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r.Disabled {
|
||||
continue
|
||||
}
|
||||
delete(st.Records, id)
|
||||
}
|
||||
ids := in.NewIDs
|
||||
|
||||
Reference in new issue
Block a user