Skip to content

refactor: 도메인 종류 판정을 긍정 목록으로 이관 - #120

Merged
yessjun merged 4 commits into
mainfrom
fix/domain-row-vm-independence
Sep 9, 2026
Merged

refactor: 도메인 종류 판정을 긍정 목록으로 이관#120
yessjun merged 4 commits into
mainfrom
fix/domain-row-vm-independence

Conversation

@yessjun

@yessjun yessjun commented Sep 9, 2026

Copy link
Copy Markdown
Member

📝 작업내용

  • 발행 경로의 판정 둘을 「CUSTOM이 아니면」에서 DomainKind의 긍정 목록으로 이관
  • servedByPlatformProxy()가 답하는 자리 넷: 플랫폼 A 레코드 소유, 행이 레코드를 빚졌다는 표시, 도메인을 받치는 인증서 조회, 에이전트 certRef 선택
  • reservesNameAfterRelease()가 답하는 자리 넷: 해제 시 유예 부여, 예약 만료 표시, 만료 계산, 사전 통지
  • VM당 서브도메인 상한이 세는 종류는 SubdomainPolicy.CAPPED_KINDS 열거 집합
  • certRefFor는 답이 없는 종류에 Let's Encrypt ref를 주지 않고 예외 발생. 호출부는 JobRunr 잡 둘이라 요청 경로가 아님
  • 예약 스윕과 라우트 리컨실 사이클이 후보별 예외 격리. 전체 재동기화 루프는 매니페스트가 권위이므로 격리하지 않고 중단을 유지
  • VM을 갖지 않는 행의 알림은 수신자 규칙 부재를 WARN으로 기록. requireRevivable은 비교 방향을 뒤집어 이름 충돌에 409 응답
  • 호출부 없는 findForMember 쿼리 삭제

⭐️ 검증

  • 테스트 1348개 통과, 실패 0, 오류 0
  • 스윕 격리 테스트와 종류 표 트립와이어는 각각 격리 제거와 값 추가로 빨간불 확인 후 원복
  • 동작 무변경 주장은 독립 검증에서 DomainKind 값 3개, DB enum 3개, domains.vm_id NOT NULL로 확인

💬 리뷰 포인트

  • servedByPlatformProxy()reservesNameAfterRelease(), CAPPED_KINDS 셋을 하나로 합치지 않은 것. 지금은 답이 같지만 존 안에 있으면서 프록시가 서빙하지 않는 종류에서 셋이 갈림
  • 전체 재동기화 루프만 격리하지 않은 것. 건너뛰면 서빙 중인 vhost가 프루닝되므로 중단이 안전한 방향이라고 판단함
  • certRefFor가 예외를 던지는 선택. 에이전트의 정확 일치 거부는 인식되는 ref를 틀린 이름에 주는 방향을 덮지 못함

Four places asked whether a domain was not CUSTOM to decide whether the
platform writes its A record, whether the row owes a record at all, which
certificate backs it and which certRef the agent gets. That reads as "the
platform serves every name in its own zone", which was true while it did.
A kind that sits under a platform root and points somewhere else inherits
all four answers wrongly, and the worst of them overwrites a user's own
records with the proxy address.

DomainKind now carries servedByPlatformProxy(), and the per-VM cap counts
an enumerated set rather than everything that is not CUSTOM. certRefFor
throws for a kind with no answer instead of falling through to the Let's
Encrypt ref: that ref makes the agent drive certbot for the name, which
for a name inside a platform root is the 2026-07-30 accident, and nginx -t
accepts the result.

DomainKindTest pins both answers per value and fails when a value is added
without a decision. The unused findForMember query goes with it, since it
carried the same shape of latent bug in a join nobody calls.
Two paths dereference a domain's VM without asking whether it has one, and
both fail in a way that is worse than the missing value.

The reservation sweep is the only thing that frees a reserved name, runs
with no retries, and does not isolate its candidates. A notification for a
row with no VM throws out of the loop, so one such row stops every later
candidate for good: the name space quietly stops recycling and nothing
says so. Each candidate is now contained and logged, and the notification
step says at WARN that a kind without a VM has no recipient rule here
rather than reaching a lookup that throws on a null id.

requireRevivable read the held row's VM first, so a name held by something
other than a VM answered a collision with a cross-user 500 instead of the
409 the unique index would give. Reversed, the held name simply loses.

The sweep test spies the repository to make one candidate fail and asserts
the other is still reclaimed; without the containment it fails.
Converting only the proxy-serving question left the reservation grace still
spelled "not CUSTOM" in four places, and that is worse than before: a kind
added now falls outside the per-VM cap and inside the grace, where the one
test used to keep those two answers together. reservesNameAfterRelease()
gives the second question its own home, the release path, the reserved-until
reading, the expiry and the advance notice all ask it, and DomainKindTest
pins three answers per value instead of two.

The certRef note claimed more than the record supports. The 2026-07-30
review found the same outcome on the agent's side, before deploy, and
closed it there by requiring an exact ref match; nothing shipped and
nothing went unnoticed. What that refusal does not cover is this
direction, where the ref is the recognised one and the name is wrong, and
that is the reason the throw stays.

managed() now says what it tests. It answers for the proxy's address
record, not for every record the platform writes, and those stop being the
same question the moment a name in our zone points somewhere else.
The reconcile scan is ordered and the cycle had no isolation, so a route
that throws every time sits at the head of it and blocks every later route
for good. This job is what eventually confirms a route nothing else
retried, which makes the blockage silent as well as permanent. Same defect
the reservation sweep had, and the same containment.

The transport case keeps stopping the cycle on purpose: an unreachable
agent says nothing about the route, and pushing the rest at an absent
agent only spends the window.

The resync loop is deliberately left whole and now says why. Its manifest
is authoritative, so a name skipped there has its vhost pruned; aborting
before the agent is called changes nothing, and that is the safe direction
for it.
@yessjun
yessjun merged commit 5294c4b into main Sep 9, 2026
1 check passed
@yessjun
yessjun deleted the fix/domain-row-vm-independence branch September 9, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant