Support image resolution¶
Target customization lives in small overlay container images rather than in contemper's own code.
The base case is a plain root filesystem. A support image can be nothing but a filesystem: its layers merge into the authored image and that's the entire mechanism. Everything below is additive. An image that needs no conditions and declares no requirements needs no labels.
One entry point per target. contemper's configuration maps a target
to a single support image reference, its default (see Default support
images). That image
declares its own variants in labels on its own image config, rather
than contemper keeping a list of every candidate. Adding a variant means
republishing one image, not cutting a contemper release. --support
replaces a target's default rather than stacking with it, so there is
still exactly one entry point per build.
Labels declare two separate things. Requirements state what the authored image must provide, and can be unconditional: a target whose agent needs a first-boot configuration mechanism declares that flatly. Variants declare alternative versions of the support image plus the conditions selecting each. A support image may use either, both, or neither.
Declarations are image config labels. A support image is described
by an ordinary Containerfile and built with the same tooling as the VM
image itself (podman, buildah, docker buildx). A Containerfile can set
config labels with LABEL, but manifest annotations need engine-specific
build flags, so labels are the engine-neutral place for the declarations.
A support image is extended with FROM. Because the declarations are
labels, an image built FROM a published support image inherits them,
and a provider can add files to it, or override one variant's .image
label, without republishing the variants or touching contemper.
Variants outside the support image's namespace are pulled anonymously. The labels are content the image controls. If they could name any image and have it pulled with the user's credentials, a third-party support image could make contemper pull a private image the user has access to into the disk. A variant in the same registry and namespace as the support image keeps the user's credentials, so a private support image can have private variants. Any other variant is allowed, to let a derived support image keep the published variants, but every request for it is made without credentials: a private image there simply fails. A variant in a different registry that is a local or private address (localhost, loopback, link-local, private ranges, hosts contacted over plain HTTP) is refused outright, since otherwise a published support image could make contemper send requests to services on the build host's network; variants in the support image's own registry are unaffected.
Branch names are encoded in label keys, for example
io.contemper.branch.init-system.openrc.requires.files. The alternative,
branch as a label value, forces parallel arrays that nothing
keeps index-aligned once a support image belongs to more than one
branch.
Branches are orthogonal axes, not a tree. The entry point's own root
filesystem merges unconditionally and carries what every variant shares
(the agent binary, common boot configuration), with winning variant
layers stacking above it, so a variant contributes only its differences.
Two axes are real today: init-system, selecting the agent's service
definition, and first-boot, selecting configuration for whichever
day-1 provisioning mechanism is present. Any init system can pair with
any first-boot mechanism, which is exactly what makes them axes. A tree
would need one leaf image per combination, 2×2 now and growing
multiplicatively; orthogonal axes need one image per value, 2+2.
Architecture is deliberately not an axis. Support images resolve through standard OCI image indexes exactly as source images do, so a variant that differs only by architecture is a multi-arch index rather than a branch.
Support images drop in files; they never install. A support image may contribute config files and static binaries, and nothing else. It cannot run a package manager, because contemper never executes image content. That constraint is why the requirements mechanism exists: an image that needs cloud-init present has no way to install it, so it declares the requirement. Requirements state what must already be present; variants adapt to which of several possible things is present.
Static linking keeps the axis count down. Contributed binaries carry
no libc or distro dependency, so distribution never becomes an axis of
its own, and contributed config files target locations the init system
or tool standardizes (/etc/init.d, /etc/systemd/system,
/etc/cloud/cloud.cfg.d) rather than distro-specific paths.
Optional axes need declared defaults. Since zero matches aborts the build, a no-op default variant is how an axis expresses that it is optional.
Resolution is exactly one level deep, deliberately. Only the target's own support image is examined for labels. An image pulled in because it won a branch is merged as-is; its labels are never read. Transitive resolution would mean handling cycles, mutual references, depth limits, and diagnostics for chains that fail several levels down. A support image that seems to need a second level is usually better modeled as another branch on the entry point.
Predicates are file-existence checks only. No expression language, no interpreter. AND is "list several required paths in one variant"; OR is "define two variants in the same branch".
Resolution is deterministic and fails loudly. Exactly one variant per branch must match, or its declared default applies. Zero matches or several both abort the build, naming the branch and the paths checked. No priority ordering and no tie-breaking: if two variants can both match, their predicates are wrong.
Manifest reads come before layer pulls. Candidate evaluation only needs manifests, so variants that lose are never downloaded. The merged-view path index predicates are checked against is built during the layer walk contemper does anyway.
Pick load-bearing markers
Predicates should target the actual init binary, not incidental markers such as a systemd library directory that some minimal images carry without systemd being PID 1. A false positive silently installs the wrong service integration and is only discovered at boot.