C.W.K.
Stream
Lesson 02 of 04 · published

plan 과 apply 는 하나의 plan 이다

~15 min · firelink, plan-apply, immutable, toctou

Level 0식은 재
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"미리 본 걸 정확히 승인해 — 아니면 승인 자체를 못 해."

하나의 불변 plan, 두 state

Firelink 의 모든 mutation 은 같은 lifecycle 을 돌아. 요청이 capability 를 resolve 하고, preflight 를 통과하고, 불변 operation plan 이 되고, armed confirmation 뒤에서 기다리고, 배타적으로 실행되고, postcondition 을 검증하고, durable·audit 된 결과로 안착해. plan 이 그 심장이고, dry-run 과 apply 는 두 operation 이 아냐 — 그 하나의 plan 의 두 state 야. dry-run 이 정확한 plan 을 만들고 persist 해; UI 가 그 target·warning·기대 효과를 보여줘; apply 가 그 같은 plan 을, id 로, armed two-click 뒤에서 실행해. typed plan 은 canonical id 와 adapter-resolve 된 인자를 지녀 — 절대 raw command token 이 아니라.

apply 는 plan 을 실행해 — 절대 재구성 안 해

preview 를 신뢰할 수 있게 만드는 규칙이 여기 있어. backend 는 새 raw 값으로 operation 을 독립적으로 재구성하는 apply 요청을 절대 안 받아. apply 는 새 인자 집합이 아니라 plan id 를 받아. 부드러운 operation 을 preview 하고 payload 를 바꿔 다르고 거친 걸 apply 할 수 없어 — apply 가 payload 를 안 읽고, 저장된 plan 을 실행하니까. dry-run 에서 본 게 정확히, byte 단위로, 돌아가는 거야. preview 는 추정이 아냐; 실행되는 artifact 야.

바뀐 세계는 plan 을 무효화한다

preview 와 apply 사이에 세계가 움직이면? source state, registry revision, target 선택이 바뀌면, plan 이 무효화돼서 재생성·재프리뷰 되어야 해. 이게 전형적 time-of-check-to-time-of-use 간격을 일부러 닫은 거야. 한 현실에 대해 preview 하고 다른 현실에 대해 apply 할 수 없어. plan 은 지어진 세계에 핀으로 고정되고, 그 세계가 바뀌면 네 승인은 더는 적용 안 돼 — 다시 봐야 해. plan id 의 만료와 합쳐지면, apply 는 늘 신선하고, 안 바뀌고, 명시적으로 승인된 현실에 대한 거야.

apply 가 저장된 불변 plan 을 id 로 실행하게 만들어 — apply 시점에 클라이언트가 보내는 뭐로든 operation 을 재구성하지 말고. 승인한 것과 돌아가는 게 동일함이 보장되고, 세계의 어떤 변경도 승인을 무효화하면, 'preview' 는 추측이 아니라 약속이 돼.

Code

dry-run 이 plan 을 persist; apply 가 id 로 실행·python
def dry_run(request) -> Plan:
    cap = resolve_capability(request)          # typed, from the registry
    preflight(cap)
    plan = Plan(
        id = new_plan_id(),
        capability = cap,                       # canonical ids + resolved args
        world_fingerprint = fingerprint(cap),   # source state + registry revision
        expires_at = now() + TTL,
    )
    store.save(plan)                            # the EXACT artifact the UI previews
    return plan

def apply(plan_id, armed: bool):
    plan = store.get(plan_id)                    # by id — NOT a fresh payload
    if plan is None or plan.expired(): raise PlanInvalid
    if not armed: raise NeedsArmedConfirmation
    if fingerprint(plan.capability) != plan.world_fingerprint:
        store.invalidate(plan_id)                # world moved -> re-plan, re-preview
        raise PlanStale
    return execute(plan)                          # runs the stored plan, verbatim

# There is no apply(plan_id, new_service=..., new_target=...). Apply reads no
# parameters. You cannot preview one thing and apply another.

External links

Exercise

네가 아는 '하기 전에 확인' flow 를 잡아 (checkout, 대량 삭제, preview 있는 인프라 변경). 물어봐. '확인'이 인자를 다시 보내, 아니면 preview 한 걸 정확히 commit 해? preview 와 commit 된 액션이 다를 수 있는 경우를 찾아 (카트가 바뀜, 가격 갱신, target 추가). 고침을 설계해. 저장된 id-참조 plan 더하기 변경-시-무효화 규칙. 세계가 발밑에서 움직였을 때 유저는 뭘 봐?
Hint
'확인'이 같은 form 필드를 다시 post 하면, preview 와 액션이 drift 할 수 있어. 견고한 모양은 이거야. preview 가 id 를 만들고, 확인이 그 id 만 보내고, 밑 state 가 바뀌면 서버가 거부해 — stale 승인에 조용히 행동하는 대신 재프리뷰를 강제해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.