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 (собирает образ)
|
make docker-up # docker compose: app + postgres (собирает образ)
|
||||||
|
|
||||||
cd web && npm run test -- --run # фронт-тесты (Vitest)
|
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 && 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.
|
// Diff compares a template against the actual zone records.
|
||||||
// Records present in the zone but absent from the template yield Delete.
|
// Records present in the zone but absent from the template yield Delete.
|
||||||
func Diff(template, actual []model.Record) Changeset {
|
func Diff(template, actual []model.Record) Changeset {
|
||||||
|
template = mergeByKey(template)
|
||||||
current := index(actual)
|
current := index(actual)
|
||||||
seen := make(map[string]bool, len(template))
|
seen := make(map[string]bool, len(template))
|
||||||
var diffs []RecordDiff
|
var diffs []RecordDiff
|
||||||
@@ -120,6 +121,43 @@ func Diff(template, actual []model.Record) Changeset {
|
|||||||
return Changeset{Diffs: diffs}
|
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
|
// 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
|
// 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
|
// 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)
|
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