docs(24-02): create plan summary
Route 53 sync + drift detection plan complete: 3 tasks, 4 files, all verification passing (22 new tests, tsc clean, full suite green aside from pre-existing unrelated itglue-search failures).
This commit is contained in:
parent
0acf1fa24f
commit
6df084cb98
1 changed files with 92 additions and 0 deletions
|
|
@ -0,0 +1,92 @@
|
||||||
|
---
|
||||||
|
phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud
|
||||||
|
plan: 02
|
||||||
|
subsystem: aws-route53
|
||||||
|
tags: [route53, sync, drift-detection, soft-delete, sync_history]
|
||||||
|
dependency-graph:
|
||||||
|
requires:
|
||||||
|
- "lib/services/route53-factory.ts (getRoute53Client / isRoute53Configured)"
|
||||||
|
- "lib/types/route53.ts (Route53Record, Route53SyncResult, Route53HistorySource)"
|
||||||
|
- "migrations/102_route53_tables.sql (route53_zones / route53_records / route53_record_history)"
|
||||||
|
provides:
|
||||||
|
- "lib/services/route53-record-key.ts (buildRecordKey, normalizeRecordSet, recordSetsEqual, classifyDrift, toHistoryPayload)"
|
||||||
|
- "lib/services/route53-sync-service.ts (Route53SyncService, getRoute53SyncService(), buildDriftHistoryRows)"
|
||||||
|
affects:
|
||||||
|
- "plan 24-05 (CRUD routes import getRoute53SyncService for post-write history/audit patterns)"
|
||||||
|
- "plan 24-06 (scheduler wiring calls fullSync()/incrementalSync())"
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Sync-service class shape mirrors lib/services/veeam-sync-service.ts: constructor(client?), isSyncing guard, executeSync() step loop with per-step error isolation, sync_history bookkeeping"
|
||||||
|
- "Pure drift-classification logic extracted into an injectable, unit-testable function (buildDriftHistoryRows) rather than inlined in the DB-touching sync method"
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- lib/services/route53-record-key.ts
|
||||||
|
- lib/services/route53-record-key.test.ts
|
||||||
|
- lib/services/route53-sync-service.ts
|
||||||
|
- lib/services/route53-sync-service.test.ts
|
||||||
|
modified: []
|
||||||
|
decisions:
|
||||||
|
- "Combined Task 2 (sync mirror) and Task 3 (drift detection) into a single write of route53-sync-service.ts rather than writing sync-only first and retrofitting drift after — the plan's own Task 2 action already anticipated buildDriftHistoryRows()'s existence (referencing it in Task 3's read_first), so building both together avoided a throwaway intermediate version of syncRecords()."
|
||||||
|
- "Symlinked node_modules from the main repo checkout into this worktree (git-ignored, not committed) — the worktree had no node_modules of its own and @aws-sdk/client-route-53 types were required for both writing and type-checking this plan's code."
|
||||||
|
metrics:
|
||||||
|
duration: "~45 minutes, 3 tasks, 4 files"
|
||||||
|
completed: "2026-08-05"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 24 Plan 2: Route 53 Sync + Drift Detection Summary
|
||||||
|
|
||||||
|
Built the Route 53 → Postgres mirror sync (`Route53SyncService`) with paginated
|
||||||
|
zone/record ingestion, soft-delete reconciliation, and whole-recordset drift
|
||||||
|
detection that writes `sync_detected_drift` history rows whenever a mirrored
|
||||||
|
record's live AWS value no longer matches Postgres.
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
**`lib/services/route53-record-key.ts`** — pure, dependency-free helpers:
|
||||||
|
- `buildRecordKey({ zoneId, name, type, setIdentifier })` → `${zoneId}:${name}:${type}:${setIdentifier ?? ''}`, the `route53_records.record_key` primary key
|
||||||
|
- `normalizeRecordSet(rs, zoneId)` — lowercases name (preserving trailing dot), uppercases type, coerces missing TTL to `null`, sorts `resourceRecords` by value (so AWS's unordered list never produces phantom drift), passes through `aliasTarget`
|
||||||
|
- `recordSetsEqual(a, b)` — TTL + serialized resourceRecords + serialized aliasTarget comparison; both-null is equal, one-null is not
|
||||||
|
- `classifyDrift(prev, next)` → `'create' | 'update' | 'delete' | null`, returning `null` for no material difference (no-op history row avoided)
|
||||||
|
- `toHistoryPayload(ns)` — whole-recordset JSONB snapshot for `before_value`/`after_value`
|
||||||
|
|
||||||
|
**`lib/services/route53-sync-service.ts`** — `Route53SyncService` class:
|
||||||
|
- `fullSync()` / `incrementalSync()` both delegate to `executeSync()`, which runs the same two steps in both cases (Route 53 exposes no modification cursor, so "incremental" is the same full diff run more often — documented in a code comment per D-11)
|
||||||
|
- `syncZones()` — paginates `ListHostedZonesCommand` (Marker/IsTruncated/NextMarker), strips the `/hostedzone/` prefix from each zone id, fetches `GetHostedZoneCommand` per zone for `DelegationSet.NameServers` (backs plan 24-04's D-12 NS-delegation check), upserts into `route53_zones`, then soft-deletes zones no longer returned by AWS
|
||||||
|
- `syncRecords()` — for each live zone, paginates `ListResourceRecordSetsCommand` (StartRecordName/Type/Identifier), normalizes each recordset, loads the zone's current mirror rows, computes drift via `buildDriftHistoryRows()` **before** upserting (so the pre-sync state is still available), inserts drift history rows in a transaction, then upserts records and soft-deletes anything no longer present
|
||||||
|
- `buildDriftHistoryRows(prevByKey, nextByKey, zoneId)` — exported pure function; unions both keyed maps, classifies drift per key via `classifyDrift`, skips `null` (no-change) keys, builds `DriftHistoryRow[]` with whole-recordset before/after payloads
|
||||||
|
- Initial-import guard: if a zone's mirror had zero rows before this sync, drift history generation is skipped entirely for that zone (logged once) — prevents flooding the append-only ledger with meaningless `create` rows on first import
|
||||||
|
- `sync_history` bookkeeping: `entity_type='route53'`, `sync_type` written as the literal `'full'`/`'incremental'` (matching the table's CHECK constraint), catastrophic-failure catch marks the row `status='failed'`
|
||||||
|
- All error logging uses `error instanceof Error ? error.message : String(error)` — never the raw AWS SDK error object (T-24-03: avoids leaking `$metadata`/request headers into logs)
|
||||||
|
- No `integration_settings` check anywhere in the file (D-10: Route 53's disable toggle is display-only, must never gate sync)
|
||||||
|
- `getRoute53SyncService()` module-level singleton, following `pax8-sync-service.ts`'s lazy-instance pattern, exported for plans 24-05/24-06
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed as written. Tasks 2 and 3 were combined into a single write of `route53-sync-service.ts` (see Decisions above) since the file only exists once either way; both tasks' acceptance criteria are independently verified below.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `npx vitest run lib/services/route53-record-key.test.ts` — 16 tests passed
|
||||||
|
- `npx vitest run lib/services/route53-sync-service.test.ts` — 6 tests passed
|
||||||
|
- `npx tsc --noEmit --pretty` — exits 0
|
||||||
|
- `npm test` (full suite) — 489 passed, 2 pre-existing failures in `lib/services/analyzer/itglue-search.test.ts`, unrelated to this plan's files and already logged in `deferred-items.md` from plan 24-01 (neither `itglue-search.ts` nor its test were touched by this plan)
|
||||||
|
- Acceptance-criteria greps all confirmed: `integration_settings` count 0, `entity_type` present, both pagination loops present (`IsTruncated` count 2), both soft-delete statements present (`is_deleted = true` count 2), `sync_detected_drift` literal present, `changed_by_user_id` NULL for drift rows, initial-import guard present (`prevByKey.size === 0`), `buildDriftHistoryRows` call precedes the `INSERT INTO route53_records` statement in source order
|
||||||
|
|
||||||
|
## Environment Note (not a code deviation)
|
||||||
|
|
||||||
|
This worktree had no `node_modules` directory. Symlinked it from the main
|
||||||
|
repo checkout (`ln -s /opt/stacks/pulse/node_modules ./node_modules`) so
|
||||||
|
`@aws-sdk/client-route-53` types and `vitest`/`tsc` were available. This is a
|
||||||
|
local filesystem convenience, not a git-tracked change — `node_modules` is
|
||||||
|
`.gitignore`d and no commit references it.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `lib/services/route53-record-key.ts` — FOUND
|
||||||
|
- `lib/services/route53-record-key.test.ts` — FOUND
|
||||||
|
- `lib/services/route53-sync-service.ts` — FOUND
|
||||||
|
- `lib/services/route53-sync-service.test.ts` — FOUND
|
||||||
|
- Commit `c18271d` (Task 1) — FOUND in `git log --oneline --all`
|
||||||
|
- Commit `d8c0912` (Task 2) — FOUND in `git log --oneline --all`
|
||||||
|
- Commit `0acf1fa` (Task 3) — FOUND in `git log --oneline --all`
|
||||||
Loading…
Add table
Add a link
Reference in a new issue