contain
v0.7.4
Published
Simple declarative container builds from local artifacts
Readme
contain
Simple declarative container builds from local artifacts.
contain is a unix philosophy CLI that does the following thing well:
Produces a container image from a local directory structure and a base image.
It runs nicely with Skaffold as custom buildCommand, as it picks up the IMAGE and PLATFORMS envs.
commands
The CLI now has sub-commands (migrated to cobra):
contain build– existing build/append functionality (also the default when you invoke justcontain ...for backwards compatibility).contain sbom– experimental stub that will generate a Software Bill of Materials from a build metadata file in future versions. Currently it just echoes the provided--build-metadatapath.
Examples (old style still works):
contain -x -b busybox:latest --file-output out.json
contain build -x -b busybox:latest --file-output out.json
contain sbom --build-metadata out/localdir.buildctl.jsonsbom subcommand
contain sbom is a CLI to produces an application SBOM in SPDX format from:
- an SBOM file from CI such as
- npm sbom output
- mvn spdx:createSPDX output
- a container image build result such as
- skaffold build
--file-output - buildctl build
--metadata-file - contain build
--file-outputor--metadata-file
- and optionally a base image ref
- otherwise autodetected from the
org.opencontainers.image.base.nameannotation (also used by for example go-containeregistry)
The resulting SPDX fully describes your deliverabe, if your image builds only deal with appending artifacts to base images. That's the principle of contain.
SBOM output and provenance
contain focuses on builds that append local artifacts to existing multi-architecture base images. This shapes our SPDX output and relationships so that downstream tooling understands the provenance correctly while remaining compatible with a variety of input SBOMs (npm, Maven, etc.).
What we emit when you run contain sbom:
- Preserve the input SBOM verbatim (application packages, dependencies, relationships) and add a creator entry:
Tool: contain-<version>. - Add two container packages under
packageswithprimaryPackagePurpose: CONTAINER:- Base image package: name equals the base ref without digest (e.g.
example.net/misc/base-image:abc). If the base digest is known, we add a SHA256 checksum to the package. We discover this primarily from the built image’s OCI annotationsorg.opencontainers.image.base.nameandorg.opencontainers.image.base.digestand allow env overrides (CONTAIN_SPDX_BASE_NAME,CONTAIN_SPDX_BASE_DIGEST) to avoid registry access in CI/tests. - Result image package: name equals
imageName@sha256:<digest>using the pushed digest from the build metadata. This is the immutable deliverable that identifies the published container image.
- Base image package: name equals the base ref without digest (e.g.
- Set
documentDescribesto include the result image package’s SPDXID so that the top-level described artifact is your deliverable image, not only the application package from the tool SBOM. - Add a provenance relationship:
RESULT DESCENDANT_OF BASE. This expresses that the result container is derived from the base image, aligning with SPDX 2.3 relationship semantics for lineage.
Why these choices matter for append-style builds:
- Multi-arch parity: The same application file tree is appended to each platform manifest; there is a single top-level deliverable (the manifest list digest). Using the index digest as the result package name communicates this cross-platform deliverable clearly.
- Minimal coupling: We do not rewrite or duplicate application dependencies from the input SBOM. contain only adds the container provenance (base/result) that traditional language-package SBOMs lack.
- Compatibility: We operate on generic SPDX JSON fields (
packages,relationships,documentDescribes) and avoid tool-specific extensions so inputs from npm/Maven/other producers continue to validate. If a container package already exists, we update it in-place (e.g., add a SHA256 checksum) without reordering non-container packages. New container packages are appended.
Notes and fallbacks:
- Base autodiscovery is best-effort. If the registry is unreachable, set
CONTAIN_SPDX_BASE_NAMEandCONTAIN_SPDX_BASE_DIGESTto ensure deterministic enrichment. - We only add platform-agnostic provenance. contain builds are intentionally platform-agnostic at the layer level; provenance is expressed once at the result (manifest list) level.
- Output stays pretty-printed JSON with two-space indentation for readability and easy diffing in CI.
libs
https://pkg.go.dev/github.com/spdx/tools-golang https://pkg.go.dev/sigs.k8s.io/bom https://pkg.go.dev/github.com/ibm/sbom-utility
basics
Contain is designed to take platform-agnostic layers and append to multi-platform bases. Nodejs and Java are examples of runtime environments that work well with such images.
Future versions might add support for:
- Single platform base images (can be auto detected)
- Configuring platform per layer, i.e. omit a layer on non-matching platforms
To leave room for single platform images, Contain requires that you set platforms to all,
the same value you'd use for ko multi-platform images.
There are many image manifests formats but Contain supports only OCI. By validating manifest types Contain helps keeping your images consistent. Future versions could add support for other formats, but that would be opt-in by config.
execution
Here's an example of a base image manifest, with optional attestation:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:2e2643ac2745067b637a4e1c4d5a3936b27a430cf0d989562c04fb7d7c53e69c",
"size": 475,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:e34c5ca17d295d5873f451ab094fb5b5515a0a5ec433d8613276baeb8f1c7741",
"size": 475,
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:897bf5e232d9c5a72947462cc13072e988428e0ff80f4441c7a238e4892afc00",
"size": 566,
"annotations": {
"vnd.docker.reference.digest": "sha256:2e2643ac2745067b637a4e1c4d5a3936b27a430cf0d989562c04fb7d7c53e69c",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:a7f5930278c418d53dc56bfcd22f7332fbda225006a1875fbc673df454929a49",
"size": 566,
"annotations": {
"vnd.docker.reference.digest": "sha256:e34c5ca17d295d5873f451ab094fb5b5515a0a5ec433d8613276baeb8f1c7741",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}This means that to get the actual base layer references, Contain will have to pull both of the application/vnd.oci.image.manifest.v1+json manifests. They're also pretty-printed json, like
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:0d0715737c21d4dc2a49af26ef780241ad5d6ab1a0e1133364e40d002ca16722",
"size": 575
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:c61587a79a418fb6188de8add2e9f694b012acde27abefd27dedaff5f02de71e",
"size": 93
}
]
}Running sha256sum on the above you get 2e2643ac2745067b637a4e1c4d5a3936b27a430cf0d989562c04fb7d7c53e69c which is the digest in the index manifest.
Using this manifest you can retrieve the layers,
but because Contain is agnostic to what the base image contains there's no need to spend the bandwidth of pulling them.
Later on if the resulting image is pushed to a different registry than where the base image lives, go-containerregistry will handle the copying of all layers.
Contain does not support nested indexes. It will bail if any manifest in the index has mediaType application/vnd.oci.image.index.v1+json.
Upon successful retrieval of indexes, contain can start the actual build.
Layers are just tarballs. The task for Contain is to produce these tar+gzips from your config,
hash them and append each layer in order to each platform's index.
That creates a new index per platform, each one having a new digest (checksum).
With those checksums Contain can produce a new index.
In practice Contain will run append and push to the first platform, then derive the layers to append from that one.
Currently Contain can't update attestations. Those index entries are therefore dropped.
Configuration
Contain supports template variables in config yaml using the framework from Skaffold.
Reproducible Builds
Contain implements reproducible builds using deterministic layer creation:
- Timestamps: All files and directories in layers use
SOURCE_DATE_EPOCH(1970-01-01T00:00:00Z) for reproducible timestamps - File Modes: File permissions are normalized to 0644 for regular files and 0755 for directories by default
- Executable Preservation: The executable bit (0111) is preserved from source files when present
- Mode Override: Layer attributes can override the default file and directory modes
- Symlink Support: Symbolic links pointing within the source tree are preserved with their target paths
- Directory Inclusion: Directory entries are explicitly included in layers for complete filesystem representation
Mode Configuration
You can override the default file and directory modes using layer attributes:
layers:
- localDir:
path: ./build
containerPath: /app
layerAttributes:
mode: 0600 # Override file mode
dirMode: 0700 # Override directory mode
uid: 1000
gid: 1000Reproducible Layer Content
The reproducible build implementation ensures that:
- Identical source produces identical layers regardless of build time or environment
- File metadata is normalized to prevent variations from filesystem differences
- Directory structure is completely captured including empty directories
- Symlinks are preserved when they point within the source tree
- Build timestamps do not affect layer checksums
This enables reliable caching, content-addressable storage, and deterministic container image builds.
Migration to Reproducible Builds
This version introduces major changes for reproducible builds that affect layer digests:
- Breaking Change: All layer digests will change due to normalized timestamps and file modes
- Test Updates Required: ExpectDigest values in integration tests need updating
- Backward Compatibility: Configuration remains compatible, only layer content changes
- Benefits: Builds are now deterministic across environments and time
To update existing configurations, simply rebuild your images - the functionality remains the same but with improved reproducibility guarantees.
