Ship the Cloud Native Buildpacks lifecycle in the builder image, and refuse legibly without it - #591
Merged
Merged
Conversation
A build of a repository with no Dockerfile invoked /cnb/lifecycle/creator inside a builder image that carried no lifecycle, so the entire explanation a 27-second failed build gave was `sh: line 30: /cnb/lifecycle/creator: No such file or directory`. The image's own header said a missing lifecycle would fail fast with a missing-lifecycle error; there was no such error, only the shell's. Rebase the builder image onto heroku/builder:26, a published Cloud Native Buildpacks builder for Ubuntu 26.04, and install buildah into it rather than copying a lifecycle into the previous buildah base. A buildpacks build runs the buildpacks inside the builder image's own root filesystem and exports the result onto the run image that builder declares: the two are a published, tested pair, and splicing /cnb into an unrelated base unpairs them, so anything a buildpack compiles rather than downloads links against one distribution's C library and then has to load on another's. The buildpacks side therefore chooses the base, and buildah, which has no such pairing, is installed on top. Heroku's builder over Paketo's on size (~473 MB compressed against ~780 MB for ubuntu-resolute-builder and ~1.5 GB for ubuntu-noble-builder, on an image pulled per build), on UID (1000, already the UID the Job pins, where Paketo's current builders are 1001), and on coverage (.NET, Go, Java, Node.js, PHP, Python, Ruby and Scala, where Paketo's small builders carry only Go and Java). Ubuntu 26.04 carries buildah 1.42, the same series the previous base carried, so the Dockerfile path is not moved onto an older builder; it does need the helpers that base bundled and a distribution package does not pull in — uidmap, and netavark/aardvark-dns/slirp4netns for the container network buildah configures for each RUN step, without which it refuses before the first instruction. Test for the lifecycle before running it, whatever image is wired. The builder image bundles one, so this is not the expected path, but the branch invokes an absolute path inside an image the install can override, and the shell's answer for a missing file is not one a user can act on. The refusal names the cause and the ways out, in the voice the insecure-registry refusal beside it already uses. Mount the layers directory the lifecycle writes into — 208 MB for a trivial Node application, and previously not mounted at all — as a writable emptyDir, for both branches, because the branch is chosen inside the pod from the cloned tree. Point DOCKER_CONFIG at the already-mounted registry auth: the lifecycle does not read REGISTRY_AUTH_FILE but resolves credentials through the docker keychain, so without it a no-Dockerfile build would authenticate as nobody. buildah prefers REGISTRY_AUTH_FILE, so the Dockerfile path is unchanged. The buildpacks path still cannot push to the plain-HTTP in-cluster registry and still refuses when the target is one (ADR-0054 §5). Signed-off-by: Nicholas Phillips <nsphilli@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #590.
A build of a repository with no Dockerfile ran
/cnb/lifecycle/creatorinside an image thatcarried no lifecycle, so the whole explanation a failed build gave was
sh: line 30: /cnb/lifecycle/creator: No such file or directory. Both halves of that are fixedhere: the lifecycle is in the builder image, and the branch refuses in a sentence when it is not.
The lifecycle
images/burrow-builder/Dockerfileis rebased ontoheroku/builder:26— a published CloudNative Buildpacks builder for Ubuntu 26.04 — and installs buildah on top of it, rather than
copying a lifecycle into the previous buildah base.
The direction matters. A buildpacks build runs the buildpacks inside the builder image's own root
filesystem and exports what they produce onto the run image that builder declares; the two are
published and tested as a pair. Splicing
/cnbinto an unrelated base does run — the lifecyclereads
/cnband never inspects the image it is running in — but it unpairs them, and anything abuildpack compiles rather than downloads (node-gyp, a Python C extension, cgo) then links against
one distribution's C library and has to load on another's. That failure surfaces at runtime, in
someone else's application, long after the build went green. So the buildpacks side chooses the
base and buildah, which has no such pairing, is installed into it.
Heroku's builder over Paketo's, on three counts:
paketobuildpacks/ubuntu-resolute-builderand~1.5 GB for
ubuntu-noble-builder. This image is pulled per build./etc/passwdentry the lifecycle needs for the user it drops to. Paketo's current builders are1001 and would require changing the Job's fixed UID to match.
small builders are not the cheaper answer — they carry only Go and Java, so the interpreted
languages this path exists for would not detect at all.
Ubuntu 26.04 carries buildah 1.42, the same series the previous
quay.io/buildah/stablebasecarried, so the working Dockerfile path is not moved onto an older builder by the change. It does
need the helpers that base bundled and a distribution package does not pull in:
uidmapfor thesetuid uid-map helpers, and
netavark/aardvark-dns/slirp4netnsfor the container networkbuildah configures for each
RUNstep — without netavark it refuses before the first instruction.Size delta: 261 MB → 533 MB compressed (1.48 GB unpacked), measured by pushing both to a
registry and summing layers. That is the price of a language-detecting builder in the same image
as a container builder, and it is paid on the pull, which is per node rather than per build.
The refusal, which stands on its own
The buildpacks branch now tests for the lifecycle before running it and fails with a sentence
naming the cause and the ways out, in the voice the insecure-registry refusal beside it already
uses. The builder image bundles a lifecycle, so this is not the expected path — but the branch
invokes an absolute path inside an image the install can override (
BURROW_BUILD_IMAGE), and theshell's answer for a missing file is not an answer a user can act on.
What the Job had to grow
The lifecycle writes into
/layers— 208 MB for a trivial Node application — and the Job mountedno such path, so it gets a writable emptyDir, mounted for both branches because the branch is
chosen inside the pod from the cloned tree (ADR-0053 §3).
/workspace,$HOMEand/tmpwerealready mounted and are the only other paths the lifecycle writes; the platform directory is
created in the image.
DOCKER_CONFIGnow names the already-mounted registry-auth directory. The lifecycle does not read$REGISTRY_AUTH_FILE— it resolves credentials through the docker keychain — so without this ano-Dockerfile build would authenticate as nobody and its push to a private registry would be
refused. buildah prefers
$REGISTRY_AUTH_FILE, so naming the same file twice leaves the Dockerfilepath unchanged.
Verified
Neither path can be exercised by the test suite —
TestBuildIntegrationis still a skippedtracker, and the k3d suite never runs a build — so both were run locally against the built image,
as the pod runs them (privileged, UID 1000, 2 GiB, the emptyDirs mounted, the recipe taken verbatim
from
buildScript):--tls-verify=false, andwrites the digest to the termination log. Unchanged behaviour on the new base.
seconds; the digest read out of
/layers/report.tomlmatches the pushed manifest. The producedimage's entrypoint is the lifecycle's
webprocess and it recordsCNB_LAYERS_DIR=/layers.exits 1.
Still true, and out of scope here
The buildpacks path still cannot push to the plain-HTTP in-cluster registry and still refuses when
the target is one (ADR-0054 §5). The lifecycle does support it —
-insecure-registry/CNB_INSECURE_REGISTRIES— so lifting that limit is now a small change, but it is a decision withits own record rather than a rider on this one.