Skip to content

build: remove the partial layer file when ImageLayoutToLayer fails - #2586

Open
RaphaelFakhri wants to merge 2 commits into
chainguard-dev:mainfrom
RaphaelFakhri:fix/layer-partial-cleanup
Open

RaphaelFakhri wants to merge 2 commits into
chainguard-dev:mainfrom
RaphaelFakhri:fix/layer-partial-cleanup

Conversation

@RaphaelFakhri

Copy link
Copy Markdown

Fixes #2483

ImageLayoutToLayer leaves its output file behind on every error path, and TarballPath() keeps returning that path. The CLI hides this because it removes its temp dir, but a library caller that sets TempDirPath, or lets Options.TempDir() create one, keeps a partial layer from every failed build. With WithCompressedLayerFile(true) the leftover is a well-formed gzip of a truncated tar.

This change removes the file and clears TarballPath on failure, for both the tar and EROFS paths, but only when apko named the file. A path set through WithTarball belongs to the caller and stays in place. splitLayers and splitErofsLayers already clean up this way.

Tests

  • TestImageLayoutToLayerFailureCleanup injects an open failure for one file (reusing failOpenFS) and checks, for tar and erofs, that the temp dir is empty and TarballPath() is cleared. Both subtests fail on main (partial layer files left behind: [apko.tar.gz], [apko.erofs]) and pass with the change.
  • TestImageLayoutToLayerFailureKeepsCallerTarball checks that a WithTarball path is kept.
  • go test ./pkg/build/... ./internal/cli/... and golangci-lint run ./pkg/build/... (v2.14) pass.

ImageLayoutToLayer left its output file behind on every error path, and
TarballPath kept pointing at it. The CLI wraps builds in a temp dir it
removes, but library callers that supply TempDirPath, or let TempDir()
create one, keep a partial layer from every failed attempt.

Remove the file and clear TarballPath on failure when apko named the file
itself. A path set through WithTarball belongs to the caller and is left
in place. This matches the cleanup splitLayers and splitErofsLayers
already do.

Fixes chainguard-dev#2483
@RaphaelFakhri
RaphaelFakhri force-pushed the fix/layer-partial-cleanup branch from 7283324 to 9fb3b6b Compare October 19, 2026 15:23
smoser
smoser previously approved these changes Oct 20, 2026
Comment thread pkg/build/build.go Outdated
if bc.o.TarballPath != "" {
// A path set through WithTarball belongs to the caller; only a file apko
// named itself is removed on failure.
owned := bc.o.TarballPath == ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: owned is inferred from TarballPath being empty on entry, but this function sets bc.o.TarballPath itself on success. So if a Context is reused after a successful call, the second call treats apko's own temp file as caller-owned and won't clean it up on failure.

Nothing in-tree reuses a Context, and resetting the path to "" on failure means a retry after a failed call still behaves, so this is fine as is. Just noting that the ownership signal depends on that side effect. Tracking it explicitly, e.g. a field set only by WithTarball, would remove the dependency if it ever matters.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Both notes are addressed in 37ea80d:

  • ImageLayoutToLayer now records the file it creates itself in a new ownedTarball field on Context, and treats that path as owned on later calls. A Context reused after a successful call now removes its own file when a later call fails. TestImageLayoutToLayerReuseFailureCleanup covers this for tar and EROFS, and fails without the change.
  • The WithTarball doc comment now says the file belongs to the caller and may hold a partial layer after a failed build. TestImageLayoutToLayerFailureKeepsCallerTarball also checks that TarballPath() still returns the caller's path.

Comment thread pkg/build/layers_test.go

// A file the caller named through WithTarball is the caller's, so a failed
// build leaves it in place.
func TestImageLayoutToLayerFailureKeepsCallerTarball(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: keeping a WithTarball file after a failure seems like the right call (don't delete what you didn't create), but what's left is a truncated layer. Might be worth a line in the WithTarball doc comment saying the file may be partial on error. This test could also assert what TarballPath() returns in this case, since that is part of the contract too.

Ownership of the output file was inferred from TarballPath being empty on
entry, but a successful call sets TarballPath to apko's own temp file. A
Context reused after a success then treated that file as caller-owned and
left it behind when a later call failed.

Record the file ImageLayoutToLayer creates itself and treat it as owned
on later calls. Document that a WithTarball file may hold a partial layer
after a failed build, and assert TarballPath in the caller-tarball test.
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.

ImageLayoutToLayer leaves its partial output file behind on build failure

2 participants