Skip to content

#919 smoke: claim 18 step 7, a failed destroy leg writes no tombstone - #942

Merged
lex00 merged 1 commit into
mainfrom
live/smoke-failed-destroy-919
Sep 7, 2026
Merged

#919 smoke: claim 18 step 7, a failed destroy leg writes no tombstone#942
lex00 merged 1 commit into
mainfrom
live/smoke-failed-destroy-919

Conversation

@lex00

@lex00 lex00 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #919.

What this adds

A step on a-shadow-is-not-a-claimant (claim 18) that reads the record back off the emulator after a create_before_destroy replace whose destroy leg failed, the write-side change #901 (PR #920) landed with fake-backed tests only. Step 4 is the control: it already shows an ordinary replace DOES write a tombstone, so an empty list here cannot pass vacuously.

The mechanism, confirmed with the plain CLI first

The issue named two candidates. Only one works on the pinned image:

candidate what floci does
disable_api_termination accepts the ModifyInstanceAttribute, reads the attribute back as false, terminates anyway. Dead on this image. (The provider at v6.59.0 would not have cleared it either: resourceInstanceDelete only does that under force_destroy.)
IAM deny of ec2:TerminateInstances with enforcement on, a role granted Allow * + Deny ec2:TerminateInstances can RunInstances and CreateTags, and TerminateInstances answers AccessDenied ... User is not authorized to perform: ec2:TerminateInstances with the instance still running.

So the scenario exports FLOCI_IAM_ENFORCEMENT=true before stack_up (the harness's test key bypasses the filter, so steps 1 to 6 run exactly as before) and step 7 creates a no-terminate role, proves its fence on a throwaway instance with the CLI, then applies a third replace under the role.

What step 7 asserts

  • the apply exits non-zero, the replacement's Creation complete line is present, and the error is the platform's denial of the destroy leg
  • the CLI lists exactly two running instances marked for the address: the deposed one and its replacement, and the deposed one reads running
  • the record file: identity.import_id is the replacement, deposed[] is the old instance, tombstone[] still carries step 4's two entries and does not carry the old instance
  • the next plan proposes aws_instance.web (deposed object ...) will be destroyed rather than pruning it; a plan that exits 0 without mentioning the deposed instance fails the step

BREAK=1 now runs two arms on one estate

The step-5 duplicate arm used to tear down and exit 0. It now terminates the duplicate and deletes its markers (confirmed with the CLI that an untagged terminated instance leaves describe-instances, describe-tags and the tagging API), so step 7's arm can run. Step 7's arm patches the record to list the running, deposed instance under tombstone, the entry the write side produced before #901, and the read must catch it. The patch is reverted before teardown.

Placement

Step 7 sits before teardown rather than directly after step 4. Converging the estate for step 5 would need a recovery apply that destroys the deposed object, and on today's main that apply records no tombstone (#938), so the shadow-arm plan would refuse on the lingering tag. With the deposed-alive state last, only the teardown has to deal with it, and it does.

Measured

Both arms, FLOCI_PORT=4750, pinned image sha256:a39185cc:

$ bash live/smoke/smoke.sh a-shadow-is-not-a-claimant
=== 7. a failed destroy leg writes no tombstone ===
    aws: [ERROR]: An error occurred (AccessDenied) when calling the TerminateInstances operation: User is not authorized to perform: ec2:TerminateInstances
    aws_instance.web: Creation complete after 10s [id=i-8bdd7bc5f639c6f07]
    Error: terminating EC2 Instance (i-ab39d21e298740497): operation error EC2: TerminateInstances, https response error StatusCode: 403, ...
    i-ab39d21e298740497 is running (the deposed object, the destroy the platform refused); i-8bdd7bc5f639c6f07 is its replacement
    identity.import_id = i-8bdd7bc5f639c6f07   (the replacement, live)
    deposed[]          = i-ab39d21e298740497   (the old object, live, the destroy that was refused)
    tombstone[]        = i-2272f959adbca418b i-67c9f4ac99301f1d8
      # aws_instance.web (deposed object d6d3e90b) will be destroyed
PASS: smoke scenario 'a-shadow-is-not-a-claimant' - every claim held (smoke v1.0.0)

$ BREAK=1 bash live/smoke/smoke.sh a-shadow-is-not-a-claimant
-> caught - the plan refused with "Two live resources claiming one address", naming both i-46adb6f07004367d2 and i-7a5e2cf4e369816a4. ...
-> caught - the record named the running, deposed i-46adb6f07004367d2 as destroyed, and the read refused it. ...
PASS: smoke scenario 'a-shadow-is-not-a-claimant' - every claim held (smoke v1.0.0)

Docs updated: claim 18's step list and BREAK paragraph in site/content/docs/claims.md, the table's minute count, and the entry in live/smoke/README.md.

Refs #901, #920, #900, #854, #670, #938.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KXQqcqYiLU513jwXziuQnh

#901 (PR #920) changed what an apply WRITES: a create_before_destroy replace
whose destroy leg fails no longer records the deposed, still-running object
as destroyed. It landed with fake-backed tests only. This is the emulator
step that reads the record back off disk, after step 4's control has already
shown that an ordinary replace DOES write a tombstone, so an empty list here
cannot pass by never having replaced anything.

The mechanism, confirmed against the pinned floci image with the plain CLI
and no tofu in the loop before it was written into the step:

  - disable_api_termination does not work here. floci accepts the
    ModifyInstanceAttribute call, reads the attribute back as false, and
    terminates the instance anyway. (The provider at v6.59.0 would not have
    cleared it either - resourceInstanceDelete only does that under
    force_destroy - but the emulator never enforces it, so the leg would
    not fail.)
  - An IAM deny does. With FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true, a
    role granted Allow * plus Deny ec2:TerminateInstances can RunInstances
    and CreateTags, and TerminateInstances answers
    "AccessDenied ... User is not authorized to perform:
    ec2:TerminateInstances" with the instance still running.

So the scenario exports FLOCI_IAM_ENFORCEMENT=true before stack_up (the
harness's "test" key keeps bypassing the filter, so steps 1-6 run exactly as
before), and step 7 creates the no-terminate role, proves its fence on a
throwaway instance with the CLI, then applies a third replace under the role
with create_before_destroy on. The apply exits non-zero; the record's
identity.import_id is the replacement, deposed[] is the old instance, and
tombstone[] carries step 4's two entries and not the old instance; the CLI
reads the old instance as running; and the next plan proposes
"aws_instance.web (deposed object ...) will be destroyed" rather than pruning
it.

BREAK=1 now has two arms in one run. The step-5 duplicate arm no longer
tears down and exits: it terminates the duplicate and deletes its markers
(confirmed with the CLI that an untagged terminated instance leaves
describe-instances, describe-tags and the tagging API), so step 7's arm runs
on the same estate. Step 7's arm patches the record to list the running,
deposed instance under tombstone - the entry the write side produced before
#901 - and the read must catch it; then the patch is reverted before
teardown.

Step 7 sits before teardown rather than directly after step 4 because the
recovery apply that would be needed to converge the estate for step 5
destroys a deposed object and, on today's main, records no tombstone for it
(#938), so the shadow-arm plan that follows would refuse on the lingering
tag. Placing the deposed-alive state last means only the teardown has to
deal with it, and the teardown handles deposed objects.

Measured on FLOCI_PORT=4750 against sha256:a39185cc:

  $ bash live/smoke/smoke.sh a-shadow-is-not-a-claimant
  === 7. a failed destroy leg writes no tombstone ===
      aws: [ERROR]: An error occurred (AccessDenied) when calling the TerminateInstances operation: User is not authorized to perform: ec2:TerminateInstances
      aws_instance.web: Creation complete after 10s [id=i-8bdd7bc5f639c6f07]
      Error: terminating EC2 Instance (i-ab39d21e298740497): operation error EC2: TerminateInstances, https response error StatusCode: 403, ...
      i-ab39d21e298740497 is running (the deposed object, the destroy the platform refused); i-8bdd7bc5f639c6f07 is its replacement
      identity.import_id = i-8bdd7bc5f639c6f07   (the replacement, live)
      deposed[]          = i-ab39d21e298740497   (the old object, live, the destroy that was refused)
      tombstone[]        = i-2272f959adbca418b i-67c9f4ac99301f1d8
        # aws_instance.web (deposed object d6d3e90b) will be destroyed
  PASS: smoke scenario 'a-shadow-is-not-a-claimant' - every claim held (smoke v1.0.0)

  $ BREAK=1 bash live/smoke/smoke.sh a-shadow-is-not-a-claimant
  -> caught - the plan refused with "Two live resources claiming one address", naming both i-46adb6f07004367d2 and i-7a5e2cf4e369816a4. ...
  -> caught - the record named the running, deposed i-46adb6f07004367d2 as destroyed, and the read refused it. ...
  PASS: smoke scenario 'a-shadow-is-not-a-claimant' - every claim held (smoke v1.0.0)

Docs: claims.md's claim 18 steps and BREAK paragraph, the table's minute
count, and live/smoke/README.md's entry.

Refs #901, #920, #900, #854, #670, #938.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXQqcqYiLU513jwXziuQnh
@lex00
lex00 merged commit 6e24aa9 into main Sep 7, 2026
3 checks passed
@lex00
lex00 deleted the live/smoke-failed-destroy-919 branch September 7, 2026 04:44
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.

smoke: a claim step for #901 - a failed destroy leg writes no tombstone

1 participant