Skip to content

fix(orphans): stop false-positives + surface EIP leaks; doc idle-stop cost (#262) - #273

Merged
scttfrdmn merged 2 commits into
mainfrom
fix/262-stopped-instance-billing-leaks
Jul 7, 2026
Merged

fix(orphans): stop false-positives + surface EIP leaks; doc idle-stop cost (#262)#273
scttfrdmn merged 2 commits into
mainfrom
fix/262-stopped-instance-billing-leaks

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Closes #262. A stopped instance keeps billing for its EBS volumes and any attached Elastic IP, and spawn orphans — the "at minimum, warn" safety net — both over-reported (listed already-deleted volumes) and under-reported (never surfaced EIPs). Confirmed live on v0.68.1.

Core invariant

spawn releases only what it alone creates. An exhaustive repo sweep confirmed spawn never allocates an Elastic IP (it uses auto-assigned ephemeral public IPs, freed by EC2 on stop/terminate). So every EIP attached to a spawn instance is a user-allocated static address, and spawn must never release it (release is irreversible). EIP handling here is therefore reporting-only.

Part 1 — orphans false-positives (bug)

enrichVolumeState/enrichInstanceState issued one batched Describe call; EC2 fails the whole call with InvalidVolume.NotFound/InvalidInstanceID.NotFound if any single id is already gone, which blanked every resource's state — and a blank volume state was classed as an orphan. So one deleted volume made orphans report every volume (incl. deleted ones).

  • New isVolumeNotFound (mirrors isInstanceNotFound); on a NotFound batch, fall back to a per-id sweep so survivors keep real state and gone ones are marked deleted.
  • IsLikelyOrphan: only a genuinely available volume is an orphan (blank/deleted is not).

Part 2 — EIP visibility (report-only)

  • scanAddresses (separate DescribeAddresses pass) surfaces unassociated EIPs and EIPs attached to a stopped spawn instance as address rows in orphans/resources. An EIP on a running spawn instance, or on a non-spawn instance, is not flagged.
  • spawn status <instance> reports any attached EIP (per user request): informational while running, a billing warning while stopped.
  • RemoveResource refuses to release an address (defense in depth); cleanup filters addresses out and prints an aws ec2 release-address hint instead. spawn never destroys a user EIP.

Part 3 — docs (no runtime default change)

--on-complete/--hibernate-on-idle help note stop's ongoing EBS/EIP cost and recommend terminate for batch/headless; README gains a "Bounding cost" section and lists resources/orphans/cleanup.

Tests

Per-id fallback (deleted volume doesn't blank survivors), address classifier, RemoveResource-refuses-address, and a substrate-backed GetInstanceElasticIP end-to-end. go build/vet/gofmt clean; pkg/aws green.

Note: the pre-existing cmd TestCatalogValid failure only reproduces locally (maintainer ~/.spawn/catalog.yaml overlay binds recipe apps to private ECR images) — CI has no overlay and is unaffected by this PR.

MINOR release.

… cost (#262)

Stopped instances silently keep billing (EBS + any attached Elastic IP), and
'spawn orphans' — the safety net for that — both under- and over-reported.

Part 1 — orphans false-positives: state enrichment issued one batched
DescribeVolumes/DescribeInstances; EC2 fails the whole call if any id is already
gone, blanking every state, and a blank volume state was treated as an orphan
(so one deleted volume made orphans list them all). Add per-id NotFound fallback
(survivors keep real state; gone ones marked 'deleted') via new isVolumeNotFound,
and only 'available' volumes are orphans now.

Part 2 — EIP visibility (report-only): scanAddresses surfaces unassociated EIPs
and EIPs on stopped spawn instances as 'address' orphans; 'spawn status' reports
an instance's attached EIP (info while running, billing warning while stopped).
Core invariant: spawn allocates no EIP so it NEVER releases one — RemoveResource
refuses 'address', cleanup filters them out with a release-address hint.

Part 3 — docs: --on-complete/--hibernate-on-idle help note stop's ongoing cost
and recommend terminate for batch/headless; README 'Bounding cost' section +
resources/orphans/cleanup commands.

Tests: per-id fallback, classifier, RemoveResource-refuses-address, and a
substrate-backed GetInstanceElasticIP. MINOR release.

Fixes #262
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.64103% with 77 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/aws/cleanup.go 59.01% 49 Missing and 1 partial ⚠️
cmd/status.go 0.00% 12 Missing ⚠️
cmd/cleanup.go 0.00% 11 Missing ⚠️
pkg/aws/client.go 55.55% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…6-5764)

Newly-published advisory GO-2026-5764 in aws/protocol/eventstream (transitive
via service/s3 and service/lambda) failed govulncheck on these two Lambda
submodules. Bump eventstream->v1.7.8, s3->v1.97.3, lambda->v1.88.5; go mod tidy.
No code change. Unrelated to the #262 fix but required to green CI.
@scttfrdmn
scttfrdmn merged commit b0f85f6 into main Jul 7, 2026
7 checks passed
@scttfrdmn
scttfrdmn deleted the fix/262-stopped-instance-billing-leaks branch July 7, 2026 21:10
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.

TTL/idle 'stop' leaves the Elastic IP attached → silent indefinite billing on a stopped instance

1 participant