Skip to content

feat(ec2): capture the instance file system in CreateImage, and implement Deregister/CopyImage - #3306

Open
allensanborn wants to merge 7 commits into
floci-io:mainfrom
allensanborn:pr/ec2-image-capture-stack-v2
Open

feat(ec2): capture the instance file system in CreateImage, and implement Deregister/CopyImage#3306
allensanborn wants to merge 7 commits into
floci-io:mainfrom
allensanborn:pr/ec2-image-capture-stack-v2

Conversation

@allensanborn

Copy link
Copy Markdown
Contributor

Summary

CreateImage records metadata only. It registers a new AMI id and copies the block device mappings, but nothing captures the source instance's file system, so an AMI created from a provisioned instance launches the base image with everything provisioned on it discarded.

This makes the capture real, and adds the two operations that manage the result.

  • CreateImage commits the running container to a Docker image and stores it on the AMI, so RunInstances from that AMI starts from the captured file system.
  • DeregisterImage and CopyImage are implemented; RunInstances from a deregistered AMI is rejected.
  • Deregistering releases the captured Docker image rather than leaking it on the daemon.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

AWS Compatibility

Incorrect behavior: an AMI created by CreateImage was not a snapshot of anything. Real EC2 captures the instance's volumes, so a launch from that AMI reproduces the source instance. Floci returned a plausible AMI id whose launches silently reverted to the base image, which is the failure mode that looks like success.

Upstream has since landed the metadata half of this area independently (the reboot, findImageForCapture, captureBlockDeviceMappings, and the resolveLaunchableImageId ancestry walk). This PR is the complementary half and builds on it: the file-system capture itself, plus Deregister and Copy. Verified before submitting that commitInstance, removeCommittedImage, Image.dockerImage, case "DeregisterImage" and case "CopyImage" are all still absent from main.

CopyImage copies within the emulator; it does not model cross-region semantics beyond the id and metadata, and the docs say so rather than implying parity.

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • Commit messages follow Conventional Commits

Coverage: Ec2CreateImageCaptureTest 8, Ec2DeregisterAndCopyImageTest 19, Ec2CapturedImageReclaimTest 4. Load-bearing check: reverting src/main to main makes all three classes fail to compile, since the symbols they exercise do not exist there.

One cross-PR note for whoever merges second

Ec2CreateImageCaptureTest constructs Ec2ContainerManager with the current 12-argument constructor. My other open PR #3272 replaces that constructor with a 13-argument form (adding VpcNetworkManager) without retaining a 12-arg overload. Each branch is correct against main today, and git merges them textually clean, so only testCompile catches the interaction.

Whichever of the two lands second will need a one-line adjustment to that test, passing the extra argument. I have deliberately not pre-adapted it here, because that would not compile against main as it stands. Happy to push the fix to whichever PR is second as soon as the first merges.

CreateImage recorded only metadata. It registered a new id and set

    image.setSourceImageId(resolveLaunchableImageId(region, source.getImageId()));

so the AMI pointed at the *base* image the source instance was launched
from. Launching it started that base image, and everything provisioned on
the source instance was silently discarded.

This was worse than an unimplemented operation. A full Packer amazon-ebs
build ran green against Floci -- keypair, security group, launch, SSH,
provisioner, stop, CreateImage, cleanup -- and produced an AMI reporting
state "available" that contained nothing:

    ==> amazon-ebs.floci: PROVISIONED_BY_PACKER
    ==> amazon-ebs.floci: AMI: ami-42ba1c3d9f8023703
    Build finished after 1 minute 41 seconds.

    $ # launch that AMI, then:
    $ cat /etc/floci-probe
    cat: /etc/floci-probe: No such file or directory

Anyone building a golden AMI locally would have believed it worked.

Per API_CreateImage, CreateImage "creates an Amazon EBS-backed AMI from an
Amazon EBS-backed instance that is either running or stopped", and the
resulting AMI is a point-in-time capture of the instance's volumes. The
contract is that state at capture time is preserved.

CreateImage now commits the source container to a Docker image
(floci-ami/<ami-id>:latest) and stores that reference on the Image.
RunInstances prefers it, walking the CreateImage chain so an AMI captured
from an instance that was itself launched from a capture still resolves.
The guest runtime (systemd vs minimal, cloud-init) still comes from the
ancestor, since committing a layer does not change how the guest boots.

Capture is best-effort by design: if the daemon refuses the commit, the
AMI keeps its sourceImageId and behaves exactly as before rather than
failing the API call. Skipped entirely in mock mode, where there is no
container.

Ec2ContainerManager gains commitInstance() and removeCommittedImage(); the
latter exists so captures can be reclaimed when the owning AMI is
deregistered, rather than accumulating on disk.

Verified end to end against a real Packer 1.16.0 build on this branch: the
build succeeds, and launching the resulting AMI and reading the file the
provisioner wrote returns PROVISIONED_BY_PACKER. The committed image is
visible as floci-ami/ami-295129250f0c23320:latest.

Tests cover the capture itself with a mocked Docker client: that the
reference is split into repository and tag (passing it whole would create
a repository literally named "floci-ami/ami-123:latest"), that a missing
container captures nothing, that a daemon failure yields null rather than
propagating, and that removing an absent or never-captured image is not an
error. 8 tests, all green.

Not addressed here: reclaiming the committed image on DeregisterImage,
which needs the DeregisterImage work to land first.
Both actions returned UnsupportedOperation:

    $ aws ec2 deregister-image --image-id ami-... --endpoint-url http://localhost:4577
    An error occurred (UnsupportedOperation) ... Operation DeregisterImage is not supported.
    $ aws ec2 copy-image --source-region us-east-1 ...
    An error occurred (UnsupportedOperation) ... Operation CopyImage is not supported.

The rest of the AMI lifecycle already worked -- RegisterImage, CreateImage,
DescribeImages, and RunInstances from a created AMI -- so the gap sat at the
end of it. DeregisterImage is what Packer calls under force_deregister to
clear the AMI holding the name it is about to reuse; without it the second
run of any iterative build dies on InvalidAMIName.Duplicate and the build is
one-shot. CopyImage is the build-in-one-region, promote-to-the-others
pattern that several Gruntwork AMI modules use.

The cause was only the missing `case` arms in Ec2QueryHandler's dispatch
switch, whose default is UnsupportedOperation. EC2_ACTIONS in
AwsQueryController is not on this path for a signed request: resolveService()
takes the service from the SigV4 credential scope and only falls back to
inferServiceFromAction() when that fails. CreateImage is likewise absent from
EC2_ACTIONS and works fine. All three are added anyway so the unsigned/raw
client fallback agrees with the dispatcher.

DeregisterImage. "Deregisters the specified AMI. A deregistered AMI can't be
used to launch new instances", and it does not delete "Instances already
launched from the AMI" (API_DeregisterImage). The image is therefore
tombstoned with the documented AMI state `deregistered` rather than dropped
from the store: DescribeImages stops reporting it, its name is released so
the rebuild can claim it, RunInstances rejects it with
InvalidAMIID.Unavailable, but an instance launched from it keeps resolving
its ancestry to a guest image, so a stop/start still comes back on the right
one. Keeping the tombstone is also what lets a repeat call answer
InvalidAMIID.Unavailable ("has been deregistered and is no longer available")
instead of InvalidAMIID.NotFound ("The specified AMI doesn't exist") -- a
retry can tell the two apart. Deregistering a catalog AMI, which is owned by
amazon rather than by the caller, is AuthFailure: "trying to use an AMI for
which you do not have permissions". Snapshots are kept unless
DeleteAssociatedSnapshots is set ("Default: The snapshots are not deleted"),
and one shared with another AMI is reported `skipped` rather than deleted,
since "if a snapshot is associated with multiple AMIs, it won't be deleted
even if specified for deletion, although the AMI will still be deregistered."

CopyImage. "The copy operation must be initiated in the destination Region",
and for a Region-to-Region copy "the destination Region is the Region in
which you initiate the copy operation" (API_CopyImage) -- so the request's
own region is the destination and only the source is looked up under
SourceRegion. That matters here because Floci keys registered AMIs by
(region, id) and describeImages filters on the stored region, so a source id
that exists in us-east-1 is genuinely absent when SourceRegion names another
region; that case is InvalidAMIID.NotFound. The copy is an independent image:
its own id, its own snapshot ids (sharing the source's would make deleting
either look like it took the other's backing with it), owned by the caller.

State: AWS reports the copy as `pending` until the backing snapshots finish.
Floci's store is in-memory and the copy completes inside the call, so it is
`available` immediately -- matching what CreateImage and RegisterImage
already return rather than inventing a state machine that nothing would ever
advance. A caller polling for `available` returns on its first poll.

Concurrency. registerImage's duplicate-name check is a read-modify-write over
the whole image store, and CopyImage now reaches it too. The stripe is taken
on (region, name), not on an image id -- the id does not exist yet and is
unique by construction, whereas the name is the invariant. deregisterImage's
check-then-tombstone takes the (region, id) stripe. Both are the striped-lock
pattern already used by modifyVpcEndpoint.

Tests are driven through the service, not over HTTP, for the concurrency
cases: requests through the test server serialize enough to hide a lost
update, the same trap called out in
Ec2ModifyVpcEndpointTest.concurrentAssociationsAllSurvive. Both race tests
were confirmed to fail with their lock removed -- 6 of 24 concurrent copies
claimed one name, and one AMI was deregistered twice -- which is also why the
name race pads the store first: the window that has to be hit is the scan,
and against an empty store the test passes with or without the lock.

The rest of the coverage is over HTTP: that DeregisterImage answers
return=true and the AMI then vanishes from DescribeImages, that unknown,
already-deregistered and not-owned ids get the three distinct documented
errors, that the freed name can be registered again (the force_deregister
loop), that an instance launched from the AMI keeps running while a new
launch is refused, snapshot retention and the shared-snapshot skip, and for
CopyImage that the copy lands in the caller's region and not the source's,
is available and owned by the caller, gets its own snapshots, is launchable
where it landed, and rejects a missing or deregistered source.

Not implemented: the Encrypted/KmsKeyId, CopyImageTags, TagSpecification and
ClientToken parameters of CopyImage, and Outpost/Local Zone destinations.
CreateImage captures the source instance's file system as a committed
Docker image (floci-ami/<ami-id>:latest). Those are real disk. Nothing
released them, so an iterative build -- Packer with force_deregister, or
any loop that rebuilds the same AMI -- left one committed layer behind per
iteration, indefinitely.

DeregisterImage now releases the capture. The AMI record itself is still
kept as a tombstone, as the DeregisterImage implementation requires for
ancestry resolution; only the Docker image is reclaimed.

The exception is the one that matters for correctness. AWS keeps instances
launched from a deregistered AMI running, and DeregisterImage's own
documentation notes that already-launched instances are unaffected -- they
can still be stopped and started. Removing the layer such an instance
boots from would break exactly that, so reclamation is skipped while any
non-terminated instance still resolves to the capture, and the tombstone
keeps its reference. Correctness before disk: the worst case is a layer
that outlives its AMI, not an instance that cannot start.

Terminated instances deliberately do not pin a capture. They can never
boot again, and treating them as live would mean nothing is ever reclaimed
in a long-running emulator.

Tests drive the real CreateImage path rather than building a fixture by
hand -- the image store returns detached copies, so setting the captured
reference on a returned Image never reaches the store and the test would
silently assert nothing. They needed two further pieces of care worth
noting:

  - The shared test configuration sets floci.services.ec2.mock=true, which
    skips all container work, so both capture and reclamation would be
    dead code under test. A QuarkusTestProfile turns it off; the container
    manager is still mocked, so no Docker is involved.
  - Outside mock mode, termination is delegated to the container manager.
    With that mocked, an instance stays "running" forever, which would
    have made the terminated-instance assertion vacuous. The mock now
    performs the same state transition the real path does.
@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR captures an EC2 instance container into a Docker-backed AMI, adds CopyImage and DeregisterImage support, prevents launches from deregistered images, and manages captured-image reclamation.

  • CreateImage now fails rather than silently publishing an AMI when filesystem capture fails.
  • CopyImage preserves captured filesystem references across regions.
  • DeregisterImage tombstones AMIs and optionally deletes unshared snapshots.
  • Launch and registration operations coordinate through an image-registry lock.
  • Tests cover capture, copying, deregistration, race handling, and reclamation.

Confidence Score: 5/5

The PR appears safe to merge, with a non-blocking captured-image cleanup leak when multiple dependent instances are terminated together.

The previous blocking findings are resolved in the current code, including capture failure propagation, copy preservation, and registry race handling. The remaining issue affects eventual Docker-layer reclamation rather than AMI correctness or launch safety.

Files Needing Attention: src/main/java/io/github/hectorvent/floci/services/ec2/Ec2Service.java

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/ec2/Ec2Service.java Implements captured-AMI launch, copy, deregistration, locking, and reclamation; multi-instance termination can leave a retained capture unreclaimed.
src/main/java/io/github/hectorvent/floci/services/ec2/Ec2ContainerManager.java Adds Docker commit and removal operations with explicit capture failure handling.
src/main/java/io/github/hectorvent/floci/services/ec2/Ec2QueryHandler.java Adds AWS Query handlers and XML responses for DeregisterImage and CopyImage.
src/main/java/io/github/hectorvent/floci/services/ec2/model/Image.java Persists the Docker image reference associated with a captured AMI.
src/test/java/io/github/hectorvent/floci/services/ec2/Ec2CapturedImageReclaimIntegrationTest.java Covers capture retention and reclamation with the required integration-test naming convention.
src/test/java/io/github/hectorvent/floci/services/ec2/Ec2DeregisterAndCopyImageIntegrationTest.java Exercises deregistration, copying, snapshot handling, and launch behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[CreateImage] --> B[Commit instance container]
    B --> C[Store Docker image reference on AMI]
    C --> D[RunInstances resolves captured image]
    C --> E[CopyImage shares captured reference]
    C --> F[DeregisterImage tombstones AMI]
    F --> G{Live instance or registered copy still depends on capture?}
    G -- Yes --> H[Retain Docker image]
    G -- No --> I[Remove Docker image]
    H --> J[Terminate dependent instance]
    J --> G
Loading

Reviews (4): Last reviewed commit: "fix(test): pass the VpcNetworkManager th..." | Re-trigger Greptile

Comment thread src/main/java/io/github/hectorvent/floci/services/ec2/Ec2Service.java Outdated
Comment thread src/main/java/io/github/hectorvent/floci/services/ec2/Ec2ContainerManager.java Outdated
Comment thread src/main/java/io/github/hectorvent/floci/services/ec2/Ec2Service.java Outdated
Comment thread src/main/java/io/github/hectorvent/floci/services/ec2/Ec2Service.java Outdated
…nnot

Six review findings on the image-capture stack, all of which end in the same
place: an AMI that reports itself available while its file system is not what
the caller asked for.

CopyImage copied the flattened ancestry but not the capture, so launching a
copy silently started the base image. The copy now carries the reference, and
because a Docker reference is global to the daemon, reclamation counts another
AMI carrying the same tag as a live user.

A capture that could not be made was reported the same way as "there was
nothing to capture", so a refused commit produced an immediately-available AMI
that booted the ancestor. commitInstance now throws, and CreateImage drops the
half-built AMI and fails: InternalError when the commit was refused,
IncorrectInstanceState when the instance has no container to capture. Floci
commits inline, so there is no later state transition a poller could observe
and no honest way to report this asynchronously.

Two races shared one cause: the invariants over the image registry are whole
set scan-then-mutate sequences, and the three operations that hold them ran
under three different monitors. RegisterImage locked a name stripe while
DeregisterImage locked an id stripe, so a registration could reuse a snapshot
between the shared-reference scan and the delete. RunInstances took no image
lock at all, so a launch could resolve a captured tag, pause before storing its
instance, and have deregistration remove the tag underneath it. Both now run
under one imageRegistryLock; RunInstances holds it only to resolve the AMI and
publish its instances, with the container launches outside it. The lock is
single rather than striped because the capture invariant is not region-scoped:
a copy shares its source's layer across regions.

removeCommittedImage suppressed docker errors while the caller cleared the
reference regardless, so a transient failure leaked the layer permanently.
It now reports whether the layer is gone, and the reference survives a refusal.

Finally, a capture retained for a live instance was never reclaimed, because
reclamation only ran during deregistration and a second deregistration is
rejected. Termination now reclaims what deregistration had to keep.

Ec2ServiceTest builds a container-backed service with a bare container-manager
mock, so its instances have no container and every CreateImage there would now
be rejected for a reason those tests are not about; they get a commit stub.
@allensanborn

Copy link
Copy Markdown
Contributor Author

All six addressed in 07401c7. Four were correctness bugs, and one of them made the feature's headline promise unreliable, so thank you for the catch.

Copy drops the captured filesystem (P1) - CopyImage carried the catalog ancestor but not the dockerImage reference, so a copied AMI launched the base image and silently lost everything provisioned. That is the exact failure this PR exists to remove, reintroduced on the copy path. The capture now survives the copy, and deregistering the source keeps a capture its copy still carries.

Capture failure reported success (P1) - a failed commit still produced an immediately-available AMI, so an image build could report success and quietly hand back the ancestor. It now fails loudly rather than presenting a ready AMI with nothing captured. Accepted-then-silently-wrong is the one option that cannot be right here.

Two lock-ordering races (P1) - both real. RunInstances now shares the registry lock for the resolve-and-publish step only, so a launch cannot resolve a captured tag and then have deregistration remove it before the instance is stored; and registration and deregistration are on the same monitor, so a snapshot cannot be reused between the shared-reference scan and the delete. The container launches themselves stay outside the lock.

One design note worth surfacing: that monitor is single rather than striped per region, deliberately. Docker image references are global to the daemon, and a CopyImage shares a layer with its source across regions, so a per-region lock would be unsound for the capture-release check specifically. The other two invariants would be fine striped; the layer one is what forces a single monitor.

Failed cleanup forgot the capture (P2) - dockerImage was cleared regardless of whether removal succeeded, so a transient daemon error leaked the layer permanently, with no path to rediscover the tag. The reference is now kept when removal did not succeed, and removal reports whether it actually happened.

Retained captures were never reclaimed (P2) - taken rather than deferred, since it turned out small: terminate now releases a capture once the last dependent instance goes away, instead of relying on a second deregistration that is rejected anyway.

Collateral worth flagging, because it changed test fixtures rather than production code. The wider -Dtest=Ec2* sweep surfaced 5 errors the three named test classes did not: several Ec2ServiceTest cases build a container-backed service with a bare mock(Ec2ContainerManager), so their instances have no container and the new "nothing to capture" rejection fired. That is a fixture artifact - a running instance with no container cannot occur in container-backed mode - so the fix is a helper that stubs the commit, plus one resolver stub in runInstancesOnACreatedImageResolvesTheSourceGuest that would otherwise NPE now that a created AMI really does carry a capture. Calling it out explicitly so it does not read as loosening a test to make a change fit.

875 tests green across the full Ec2* sweep. Every one of the seven production changes was reverted in isolation and the matching test confirmed red before restoring; the mapping is in the commit body.

@allensanborn

Copy link
Copy Markdown
Contributor Author

Housekeeping so the thread list reflects reality: I have resolved the two threads at Ec2Service.java:2335 ("Deregistration races with launch") and :4581 ("Snapshot deletion races registration"). Both were fixed in 07401c7 and described in the comment above; the threads stayed open because a fix does not auto-resolve them, and four of the six from that round were marked outdated while these two were not, so they still read as live. Reopen either if you disagree with the fix rather than the bookkeeping.

The two genuinely new items from the 15:21 re-review are being worked and I have not resolved those:

  • P1 Ec2Service.java:2517, launch race orphans resources. This one is distinct from the two above rather than a restatement: they were about the lock, this is about what happens when the second requireNotDeregistered check throws after root volumes, tags, the subnet IP allocation and possibly a caller-supplied ENI attachment have already been persisted. Nothing rolls those back.
  • P2 Ec2CapturedImageReclaimTest.java:52, integration test naming. Fair, and the convention is real here rather than merely documented: 607 of the 736 @QuarkusTest classes on main are named *IntegrationTest. Renaming.

…ts anything

RunInstances checks the AMI tombstone twice: once on entry, and once under the image
registry lock, because DeregisterImage releases the captured layer as soon as no live
instance resolves to it, and a launch that had resolved the layer but not yet stored
its instance would lose it underneath.

The second check fired too late. By the time it ran, the launch had already written a
root volume per instance, the instance tags, a subnet IP, and the attachment on a
caller-supplied ENI. The reservation was never returned, so no caller had an id to
clean any of that up with, and the interface stayed in-use for good.

Move the build loop inside the same locked section, after the check, so the launch
persists nothing until it knows it is allowed to proceed. Rollback was the other
option and is worse: the subnet IP counter has no free list to return an offset to,
and a rollback path only runs once something has already gone wrong. The lock is not
meaningfully wider, since what moved inside it is in-memory record construction; the
slow part, the container launch, still runs outside.

The regression test holds the registry lock itself, so the launch parks at a known
point and the tombstone lands there on purpose rather than racing for the window.
Its file is renamed to *IntegrationTest, matching what the rest of the repo calls a
@QuarkusTest.
…Test

607 of the 736 @QuarkusTest classes on main end in IntegrationTest, and this one
starts a full application like the rest of them. Ec2CreateImageCaptureTest is left
alone: it drives a plain Ec2Service with Mockito and is not an integration test.
@allensanborn

Copy link
Copy Markdown
Contributor Author

Both new items done in c5aecfd and 2d9b867.

Launch race (P1). Confirmed before changing anything, and the enumeration matters because it shows why a rollback would have been the wrong shape. Between the early requireNotDeregistered and the second one, runInstances persists four things per instance: the root volume, the instance tags, a caller-supplied ENI written back with its attachment and status in-use, and a bump of the subnet IP counter, which is monotonic with no free list. So a late tombstone left an orphan volume, orphan tags, an ENI stuck in-use, and a permanently consumed address.

Rather than unwinding those on the failure path, the build loop now sits inside synchronized (imageRegistryLock) after the check, so nothing is persisted before the AMI is known good. Nothing to roll back is a better invariant than rolling back correctly, particularly for the IP counter, which has no release path to roll back into.

The critical section did not grow to cover the slow part: containerManager.launch stays outside the lock, with a comment saying why. The lock protects the registry and a reference into it, not the containers.

Coverage: aTombstoneLandingMidLaunchLeavesNoOrphanedResources. Load-bearing checked by reverting the production change, which fails it on a rejected launch must not leave its root volume behind ==> expected: <0> but was: <1>.

Test naming (P2). Renamed Ec2CapturedImageReclaimTest and Ec2DeregisterAndCopyImageTest to *IntegrationTest. I left Ec2CreateImageCaptureTest alone deliberately: it is a plain Mockito unit test with no @QuarkusTest, so the integration-test convention does not apply to it and renaming it would misdescribe what it is.

876 tests green across the full Ec2* sweep, up one from the added case.

Also worth flagging since it came up in the earlier round: I resolved the two threads at Ec2Service.java:2335 and :4581 after confirming both fixes are in the head. They had stayed open because a fix does not auto-resolve a thread, not because anything was outstanding.

@hectorvent hectorvent added ec2 Amazon Elastic Compute Cloud (EC2) enhancement New feature or request labels Sep 9, 2026
@hectorvent

Copy link
Copy Markdown
Collaborator

Thank you, and sorry this sat: you closed eight findings across two rounds without anyone replying, and the two CopyImage ones were the sort that would have quietly undone the point of the PR.

I found nothing to fix in the code. The shapes check out member by member against the model, including the nested deleteSnapshotResultSet wrapper, and every error code is a real EC2 one used in its documented sense.

(blocking) It needs a rebase, and there is a trap in it that is our fault. The only textual conflict is a one-line ArgumentMatchers.any import in Ec2ServiceTest; keep your side. But #3272 merged an hour ago and added a 13th constructor parameter to Ec2ContainerManager, and git auto-merges Ec2CreateImageCaptureTest cleanly because neither side touched those lines. So the break only appears at test-compile, and ./mvnw compile still passes. Adding mock(VpcNetworkManager.class) at line 45 fixes it; I tried it and 193 tests pass afterwards, including #3272's and #3319's own.

@hectorvent hectorvent added the waiting-author Review posted; waiting on the PR author to respond label Sep 10, 2026
…quires

Merges current main and repairs the one thing the merge could not see. floci-io#3272 added
a 13th constructor parameter to Ec2ContainerManager; Ec2CreateImageCaptureTest builds
that constructor but neither side edited those lines, so git auto-merged cleanly and
./mvnw compile still passes. Only test-compile catches it, which is the whole reason
that is the command worth running.

Reproduced before fixing: compile exit 0, test-compile exit 1 on "no suitable
constructor found". Adds mock(VpcNetworkManager.class) as the 13th argument.

Also keeps our side of the only textual conflict, the ArgumentMatchers.any import in
Ec2ServiceTest, which this branch's tests use and main no longer does.
@allensanborn

Copy link
Copy Markdown
Contributor Author

Rebased in d6ff91d, and your diagnosis was exact.

Merged current main. The only textual conflict was the ArgumentMatchers.any import in Ec2ServiceTest and I kept our side, since this branch's tests use it and main no longer does.

The constructor trap reproduces precisely as you described, and I checked it in both directions before touching anything rather than just applying the fix:

./mvnw compile       exit 0
./mvnw test-compile   exit 1   no suitable constructor found for Ec2ContainerManager(...)

mock(VpcNetworkManager.class) as the 13th argument fixes it. The full Ec2* sweep is 911 green, up from 876 as main's own tests came across in the merge.

Worth saying that this one was foreseeable and I flagged it in the PR description when I opened this: #3272 replaced the 12-argument constructor without keeping an overload, git merges the test cleanly because neither side touches those lines, and only test-compile sees it. I offered then to push the fix to whichever PR landed second. That should have happened when #3272 merged rather than waiting for you to hit it, and I am sorry you spent time reproducing something already predicted in the thread.

Thank you for reviewing it shape by shape against the model. On the two CopyImage findings you singled out: the first was that CopyImage carried the catalog ancestor but not the dockerImage reference, so a copied AMI silently launched the base image, which is exactly the failure this PR exists to remove, reintroduced on the copy path.

Comment on lines +4699 to +4702
boolean stillLaunchable = instances.scan(i -> true).stream()
.filter(i -> i.getRegion() != null && !i.getInstanceId().equals(excludedInstanceId))
.filter(i -> i.getState() != null && !"terminated".equals(i.getState().getName()))
.anyMatch(i -> captured.equals(capturedImageFor(i.getRegion(), i.getImageId())));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Batch Termination Leaks Capture

When multiple instances that depend on the same deregistered AMI are terminated in one request, reclamation excludes only the instance currently being processed. The other instances remain recorded as shutting-down, which this scan treats as live. Each reclamation attempt therefore retains the captured Docker image, and no later attempt runs after asynchronous teardown finishes, so the image remains on disk indefinitely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ec2 Amazon Elastic Compute Cloud (EC2) enhancement New feature or request waiting-author Review posted; waiting on the PR author to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants