Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .github/workflows/runtime-proof.yml
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,25 @@ jobs:
if: always()
run: sudo ./feint stop --addr 127.0.0.1:4599 || true

# The closing doorstep (#521), which this workflow did not have and paid
# for on 2026-08-27 and 2026-08-28: the leg's own suites left four objects
# on the host, the next step asked the doorstep question at its start, and
# the answer came back as "a previous run left …" on a runner nothing had
# ever touched. Both halves were wrong to leave alone — a leak reported by
# the *next* thing to run, and a sentence naming a run that never existed.
#
# So the same form `mise run conformance` and conformance:leg.sh already
# carry: stop, then ask, in the closing spelling. Whatever the suites left
# reddens the leg that left it, and the report says "this run".
#
# Not `if: always()`, deliberately. A leg whose suite already failed left
# its machines by accident and not by leak; a second red there would say
# "this run leaked" about a run that simply died, and the finding this
# gate exists for would be buried under it. On the failure path the state
# is printed by the step above instead.
- name: What this leg left on the host
run: sudo tools/conformance/guard.sh leftovers-after "${{ matrix.mode }}"

# The dataplane witness gate (#486) needs Terraform: it applies the
# example stacks through `feint up`. Same pinned version and upstream
# checksums as conformance.yml's TERRAFORM_VERSION — one client, two
Expand Down Expand Up @@ -504,11 +523,14 @@ jobs:
# 2026-08-28, reproducing the leg: the three ssh suites each exit 0 and
# each leave something standing — `scw-…` and `exo-…` rule sets no client
# can delete, `fnt-default` and `feint-uplink` no resource owns — and
# `feint stop` sweeps none of them. That is why the witness gate below
# failed at its own doorstep on the one night it has been reached since it
# landed — 2026-08-27, "this host still holds what an earlier run left",
# naming feint-uplink and fnt-default; the night after never got that far.
# A job of its own starts on a runner nothing has touched.
# `feint stop` swept none of them. That is why the witness gate below
# failed at its own doorstep on the two nights it has been reached since
# it landed, "this host still holds what an earlier run left", naming
# objects the same job had made minutes earlier. The four are released by
# the graceful exit now (machine.PlumbingReleaser) and the leg asks the
# closing doorstep after its stop, so the leg reddens itself if that ever
# stops being true — but a job of its own still starts on a runner nothing
# has touched, which is one fewer thing to be wrong about.
# - Three passes is half an hour. Added to that leg it would push a 45-minute
# job onto its own timeout, and a job that times out is a verdict nobody
# wrote.
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ change ni l'un ni l'autre a sa place dans `git log`.

### Corrigé

- **Un arrêt propre rend toute la plomberie qu'aucun client ne peut supprimer,
et l'exécution qui fuit est celle qui rougit (#521).** La jambe incus-ovn de
`runtime-proof.yml` a échoué au portillon de l'étape suivante sur un runner
neuf : *a previous run left 0 machine(s) and 2 network(s) on this host*, en
nommant `feint-uplink`, `fnt-default` et les jeux de règles des groupes de
sécurité par défaut de deux providers. Il n'y avait pas d'exécution
précédente : les trois suites ssh de la jambe avaient créé les quatre, en
sortant chacune à 0.

Quatre objets, une propriété : **aucun appel client ne peut en retirer un
seul**, donc les laisser ne mesurait rien sur les suites. `fnt-default` est
le réseau sur lequel démarre une machine sans attachement à elle, créé ici
par une Vm Outscale hors Net et possédé par aucune ressource ;
`feint-uplink` était relâché depuis #521 et restait parce que `fnt-default`
y puisait encore ; les jeux `scw-*` et `exo-*` appartiennent à des groupes
par défaut qu'un client ne peut pas supprimer, et celui de Scaleway est tiré
à chaque exécution, donc une ACL s'accumulait par session. L'arrêt rend
désormais les quatre, dans le seul ordre que le runtime accepte, et chaque
restitution pose les deux questions — est-ce à nous, et quelque chose y
puise-t-il encore — de sorte qu'un `feint stop` sans `--cleanup` laisse leur
pare-feu aux machines qu'il laisse délibérément tourner.

La seconde moitié est la phrase. `feint clean --check --closing` et
`tools/conformance/guard.sh leftovers-after` posent la même question et
nomment **cette** exécution, `runtime-proof.yml` la pose après son propre
arrêt au lieu de laisser l'étape suivante rencontrer le reste, et le
portillon nomme désormais les jeux de règles sur lesquels il ne refuse *pas*
au lieu de passer dessus en silence. La colonne d'attribution du registre a
été corrigée avec eux : un réseau est trouvé par l'étiquette que lit
`Survey`, pas par le préfixe `fnt-` que la colonne annonçait — et le premier
réseau nommé par la jambe en échec, `feint-uplink`, ne porte pas ce préfixe.

- **Le portillon de sortie de la porte des stacks pose enfin la question que
son propre commentaire annonçait, et la porte cesse de choisir son runtime
en silence (#504).** Elle se terminait sur
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,36 @@ what this project is judged on: **a response shape a client can observe**, and

### Fixed

- **A graceful exit gives back every piece of host plumbing no client can
delete, and a run that leaks is the run that goes red (#521).** The incus-ovn
leg of `runtime-proof.yml` failed at the doorstep of the next step on a
GitHub runner nothing had ever touched: *a previous run left 0 machine(s) and
2 network(s) on this host*, naming `feint-uplink`, `fnt-default` and the rule
sets of two providers' default security groups. There was no previous run —
the leg's own three ssh suites had made all four, each exiting 0.

Four objects, one property: **no client call can remove any of them**, so
leaving them measured nothing about the suites. `fnt-default` is the network
a machine with no attachment of its own boots on, created here by an Outscale
Vm outside a Net and owned by no resource; `feint-uplink` had been released
since #521 and stayed because `fnt-default` still drew from it; the `scw-*`
and `exo-*` rule sets belong to default security groups a client cannot
delete, and Scaleway's is minted per run, so one host ACL accumulated per
session. The exit now releases all four, in the only order the runtime
accepts, and each release answers both questions — is it ours, and is
anything still drawing from it — so a `feint stop` without `--cleanup` leaves
the firewall on the machines it deliberately leaves running.

The second half is the sentence. `feint clean --check --closing` and
`tools/conformance/guard.sh leftovers-after` ask the identical question and
name **this** run, `runtime-proof.yml` asks it after its own stop instead of
letting the next step meet the residue, and the doorstep now names the rule
sets it does *not* refuse on rather than passing over them in silence. The
ledger's attribution column was corrected with them: a network is found by
the label `Survey` reads, not by the `fnt-` prefix the column claimed — and
the first network the failing leg reported, `feint-uplink`, carries no such
prefix.

- **The stack gate's closing doorstep asks the question its own comment
claimed, and the gate stops choosing its runtime in silence (#504).** It
closed with `guard_leftovers_for "$RUNTIME" "the end of the run"` under a
Expand Down
24 changes: 20 additions & 4 deletions internal/cli/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func clean(args []string, stdout io.Writer) error {
check := fs.Bool("check", false, "report what this user cannot remove and remove nothing; exit 1 if anything is stuck")
format := fs.String("format", "text", "output format: text, or json for one aggregatable line per object found")
doorstep := fs.Bool("doorstep", false, "also refuse a machine or network of an earlier run; only true before a run starts, because a run in flight owns both")
closing := fs.Bool("closing", false, "the same refusal, asked once this run's emulator has stopped: what is found is then this run's own leak, and the report says so")
force := fs.Bool("force", false, "also clear what no ordinary command of the runtime reaches: the peering rows a deleted network left behind, named one by one before they go")
if err := fs.Parse(args); err != nil {
return err
Expand All @@ -40,9 +41,24 @@ func clean(args []string, stdout io.Writer) error {
if *check && *force {
return fmt.Errorf("--check removes nothing and --force removes what nothing else can: ask for one of the two")
}
// The two moments name opposite culprits — "a previous run" and "this run" —
// and a caller who asked for both wants a sentence that cannot be true. The
// refusal is identical either way, so guessing here would cost nothing at
// the gate and everything at the report, which is the whole subject of the
// leg that made this flag exist.
if *doorstep && *closing {
return fmt.Errorf("--doorstep asks what an earlier run left and --closing what this one did: ask for one of the two")
}
moment := momentInFlight
switch {
case *doorstep:
moment = momentDoorstep
case *closing:
moment = momentClosing
}
led := newLedger(stdout, *format == "json", time.Now())
if *check {
return reportStuckLeftovers(stdout, led, *vm, *doorstep)
return reportStuckLeftovers(stdout, led, *vm, moment)
}

// The state directories go first, and deliberately before the runtime is
Expand Down Expand Up @@ -207,7 +223,7 @@ var (
//
// TestCleanCheckRefusesAHostWhoseLeftoverThisUserCannotEnd and
// TestCleanCheckPassesWhenTheSweepItselfWouldClearThem fail without it.
func reportStuckLeftovers(stdout io.Writer, led *ledger, vm string, doorstep bool) error {
func reportStuckLeftovers(stdout io.Writer, led *ledger, vm string, moment leftoverMoment) error {
// The runtime half first, and #426 is why it exists at all.
//
// Before this, the doorstep asked one question — is there a DHCP service
Expand Down Expand Up @@ -253,8 +269,8 @@ func reportStuckLeftovers(stdout io.Writer, led *ledger, vm string, doorstep boo
return fmt.Errorf("could not ask the %s runtime what a previous run left: %w", vm, err)
}

if doorstep {
if err := refuseRuntimeLeftovers(stdout, led, vm, rt); err != nil {
if moment.asks() {
if err := refuseRuntimeLeftovers(stdout, led, vm, rt, moment); err != nil {
return err
}
}
Expand Down
Loading
Loading