fix(diff): merge template records sharing name+type into one RRset
Two template rows at the same name and type (e.g. two MX) produced two diffs with one key; each PATCH replaced the whole provider RRset, so only the last value survived. Merge values by key before diffing. docs: add lint/single-test commands and compose files to CLAUDE.md Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e12c4729b
commit
c5f1e64873
3 files changed
+63
-2
No files matched your search
@@ -15,11 +15,13 @@ make build-all # web + build
|
||||
make docker-up # docker compose: app + postgres (собирает образ)
|
||||
|
||||
cd web && npm run test -- --run # фронт-тесты (Vitest)
|
||||
cd web && npx vitest run src/path/File.test.tsx # один фронт-тест
|
||||
cd web && npm run lint # oxlint
|
||||
cd web && npx tsc --noEmit # проверка типов
|
||||
cd web && npm run build # прод-сборка фронта
|
||||
cd web && npm run build # прод-сборка фронта (tsc -b && vite build)
|
||||
```
|
||||
|
||||
Конфигурация только через env: `DNS_AR_DB_DSN`, `DNS_AR_ENC_KEY` (base64, декодит в **ровно 32 байта**; `openssl rand -base64 32`), `DNS_AR_LISTEN` (default `:8080`). Миграции (goose) гоняются приложением на старте.
|
||||
Конфигурация только через env: `DNS_AR_DB_DSN`, `DNS_AR_ENC_KEY` (base64, декодит в **ровно 32 байта**; `openssl rand -base64 32`), `DNS_AR_LISTEN` (default `:8080`). Миграции (goose) гоняются приложением на старте. Compose-файлы: `docker-compose.yml` (локально), `docker-compose-prod.yml` (прод). `AGENTS.md` — копия этого файла для Codex (untracked): при правках синхронизировать или не коммитить.
|
||||
|
||||
## Критичные подводные камни
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ func (c Changeset) Prunes() []RecordDiff {
|
||||
// Diff compares a template against the actual zone records.
|
||||
// Records present in the zone but absent from the template yield Delete.
|
||||
func Diff(template, actual []model.Record) Changeset {
|
||||
template = mergeByKey(template)
|
||||
current := index(actual)
|
||||
seen := make(map[string]bool, len(template))
|
||||
var diffs []RecordDiff
|
||||
@@ -120,6 +121,43 @@ func Diff(template, actual []model.Record) Changeset {
|
||||
return Changeset{Diffs: diffs}
|
||||
}
|
||||
|
||||
// mergeByKey folds template records sharing a Key() into one RRset whose
|
||||
// Values are the union (deduplicated by normalized value) in first-seen order.
|
||||
// A template may list several records at the same name+type (two MX rows);
|
||||
// the provider models them as ONE RRset, so applying them separately would
|
||||
// make each replace the previous. TTL of the first record wins. Order of
|
||||
// first occurrences is preserved.
|
||||
func mergeByKey(recs []model.Record) []model.Record {
|
||||
out := make([]model.Record, 0, len(recs))
|
||||
pos := make(map[string]int, len(recs))
|
||||
for _, r := range recs {
|
||||
k := r.Key()
|
||||
i, ok := pos[k]
|
||||
if !ok {
|
||||
r.Values = append([]string(nil), r.Values...)
|
||||
pos[k] = len(out)
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
have := make(map[string]bool, len(out[i].Values))
|
||||
for _, v := range out[i].Values {
|
||||
have[normalizeValueOf(out[i].Type, v)] = true
|
||||
}
|
||||
for _, v := range r.Values {
|
||||
nv := normalizeValueOf(r.Type, v)
|
||||
if !have[nv] {
|
||||
have[nv] = true
|
||||
out[i].Values = append(out[i].Values, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeValueOf(t model.RecordType, v string) string {
|
||||
return model.Record{Type: t, Values: []string{v}}.NormalizedValues()[0]
|
||||
}
|
||||
|
||||
// index builds a lookup of records by Key(). If two records in recs share
|
||||
// the same Key() (e.g. a provider returning a duplicate RRset entry), this
|
||||
// is a deliberate, documented choice, not an oversight: the LAST record with
|
||||
|
||||
@@ -301,3 +301,24 @@ func TestMarkCustomNeverMarksReadOnly(t *testing.T) {
|
||||
t.Fatalf("read-only diffs must never become custom, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Two template rows at the same name+type are one RRset: values are merged.
|
||||
func TestDiffMergesTemplateRecordsWithSameKey(t *testing.T) {
|
||||
tpl := []model.Record{
|
||||
{Type: model.MX, Name: "example.com.", TTL: 600, Values: []string{"0 pmg2-mail.example.com."}},
|
||||
{Type: model.MX, Name: "example.com.", TTL: 600, Values: []string{"10 pmg1-mail.example.com."}},
|
||||
{Type: model.MX, Name: "Example.com.", TTL: 600, Values: []string{"10 pmg1-mail.example.com"}}, // dup value
|
||||
}
|
||||
cs := Diff(tpl, nil)
|
||||
if len(cs.Diffs) != 1 {
|
||||
t.Fatalf("want 1 diff, got %d", len(cs.Diffs))
|
||||
}
|
||||
d := cs.Diffs[0]
|
||||
if d.Kind != Add || len(d.Desired.Values) != 2 {
|
||||
t.Fatalf("want Add with 2 values, got %v %v", d.Kind, d.Desired.Values)
|
||||
}
|
||||
actual := []model.Record{{Type: model.MX, Name: "example.com.", TTL: 600, Values: []string{"10 pmg1-mail.example.com.", "0 pmg2-mail.example.com."}}}
|
||||
if got := Diff(tpl, actual).Diffs[0].Kind; got != InSync {
|
||||
t.Fatalf("want InSync, got %v", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user