Repository navigation
feat(toolchain): split the toolchain image into traust-toolchain and traust-runtime (XWING-2440) - #277
Conversation
3bfc5d7 to
f3b8764
Compare
|
Reviewed — the split is sound and this is a net supply-chain improvement. What I checked:
One non-blocking nit, and it is pre-existing rather than introduced here: release assets are checked against their reported version, not against a published digest. A tampered asset that lies about |
|
/lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: gurnben, JGiola The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
Heads-up: this went Also worth knowing for the rebase: #279 moved |
…traust-runtime (XWING-2440)
External tooling and the traust Python stack become two separate images, so a
scanner pin advance (weekly at most) never forces a rebuild of the Python
layer, and vice versa. No vulnerability database ships in either image;
scanners read theirs from a mounted feeds cache.
Containerfile.tools (three stages: two builders, one fresh final image)
- go-builder: builds govulncheck and a statically-linked skopeo (CGO_ENABLED=0
- a dynamically-linked build there links against the builder's newer glibc,
which the final UBI9 base can't satisfy) from registry.access.redhat.com/
hi/go:1.26.9-builder, pinned by digest. skopeo's module path is
go.podman.io/skopeo (the containers-org rename); 1.26 specifically because
hi/go:1.27.3's stdlib net/http2 is incompatible with skopeo's resolved
grpc v1.80.0.
- tools-builder: downloads and compiles everything else (opengrep, gitleaks,
syft, grype, cosign from pinned release assets; yara from source, built
with --disable-shared so it statically links libyara; the pinned Go
runtime; the isolated pip-audit venv) using a build toolchain (gcc,
autotools, pkgconfig) that never reaches the final image.
- tools: a FRESH final stage, not a trimmed copy of either builder. Installs
only git-core and python3.11 - the runtime dependency list is driven by
running ldd against every native binary (cosign/gitleaks/grype/syft/skopeo
are fully static; opengrep needs only libc; yara/yarac need libc/libm;
govulncheck needs libresolv/libc for its cgo DNS resolver - all already
part of base glibc). git-core is a genuine runtime dependency: gitleaks
(history mode) and yara (rule-pack clone) both declare it in
traust_engine.adapters.tools, not a build-time leftover.
- Every binary verified against its exact pin at build time; a failed
install fails the build (yara's install no longer swallows failures).
opengrep installs from its pinned release asset, not the mutable
install.sh URL. The Go pin now reads from config/external-tools.yaml
(was hardcoded to 1.23.0 regardless of the manifest).
- Base image digests are inlined directly in each FROM line, matching this
repo's existing convention - not indirected through an ARG with a
hardcoded default that nothing overrides.
Containerfile.runtime
- FROM the tools image by digest (never a floating tag). traust-core/
traust-engine/traust installed via `uv sync --frozen --no-editable` at
their pinned Git SHAs. Labels carry the toolchain hash and commit.
- No database population step (the old image's grype db update, silently
ignored on failure, is gone).
scripts/containerfile-build-args.py
- A pin-set hash over the *resolved* installed versions (sorted name=version
pairs) tags both images and the `toolchain:<hash>` runner label - stable
under comment or reordering edits to external-tools.yaml, changes only on
a real version bump. A --check flag validates every binary the tools image
installs has a real pin, so a missing one fails loudly instead of falling
back to a Containerfile-local default.
scripts/build-toolchain-image.sh: rewritten to build both images, resolving
the tools image's local digest automatically (`podman image inspect
--format '{{.Digest}}'` works without a registry push) and passing it to the
runtime build. Fixed a real shift-inside-a-for-loop argument-parsing bug
along the way (a bash gotcha: `shift` inside `for arg in "$@"` doesn't affect
the loop's iteration).
traust/context.py: register_providers() now receives feeds_cache=, resolved
lazily (same timing as the engine load) so a laptop install with no
progress_tracker configured keeps scanning tools' native caches unchanged.
Needs the engine's door for this (merged traust-engine#104, scanner-db
wiring): grype and govulncheck now read their databases from
<feeds_cache>/tool-dbs/<tool> and raise before starting when that database
is missing or unusable - confirmed necessary against the real binaries:
grype fails on its own with an empty database, but govulncheck otherwise
silently reports zero findings against an empty -db file:// path. The
traust-engine pin is bumped to 402a8a2, the merged commit.
CI (.github/workflows/container-images.yml): builds both images on every PR
touching them, and checks every property this plan's "Done when" names -
every binary at its pin, no Python stack or database in the tools image, no
build toolchain (gcc/make/automake/autoconf/libtool/pkg-config) or missing
shared library, the runtime label names the toolchain hash, and a scan
against an emptied database degrades (grype exits non-zero) rather than
reporting zero findings.
Retired: Containerfile.toolchain (replaced by the split), build-pinned-
wheels.py (superseded by `uv sync --frozen` installing directly from the
pinned Git sources).
Verified end to end with real podman builds (not just written): both images
build from a clean machine; every binary at its pin; no traust_core, no
build toolchain, no database anywhere in the tools image (1.19GB, down from
1.64GB for the equivalent single-stage build); an empty mounted
tool-dbs/grype makes grype exit non-zero with a clear error; the composed
runtime image imports the full traust stack and runs its CLI.
16 new tests (pin-set hash and inventory validation; feeds-cache wiring: no
cache configured leaves tools on native caches, FEEDS_CACHE_DIR env override,
locations.yaml resolution). Full suite: 2601 passed, 61 skipped; 14
pre-existing failures confirmed present on a clean development checkout too
(local GPG-signing configuration issue, unrelated to this change). Lint
clean.
f3b8764 to
f64edbf
Compare
|
New changes are detected. LGTM label has been removed. |
Plan:
plans/traust-standardization/11-consumer-adoption/08-jacopo-toolchain-image-split.mdContainerfile.toolchainwas one "all-in-one" image: pinned scanner binaries, the whole Python stack and skill data. This splits it intotraust-toolchain(pinned scanner binaries only) andtraust-runtime(FROMit, by digest, plus traust-core/traust-engine/traust), so a scanner pin advance (weekly at most) never forces a rebuild of the Python layer, and vice versa. No vulnerability database ships in either image.What
Containerfile.tools— three stages: two builders, one fresh final image.go-builder: buildsgovulncheckand a statically-linkedskopeo(CGO_ENABLED=0— a dynamically-linked build there links against the builder's newer glibc, which the final UBI9 base can't satisfy) fromregistry.access.redhat.com/hi/go:1.26.9-builder, pinned by digest.skopeo's module path isgo.podman.io/skopeo(the containers-org rename); 1.26 specifically becausehi/go:1.27.3's stdlibnet/http2is incompatible with skopeo's resolvedgrpc v1.80.0.tools-builder: downloads and compiles everything else (opengrep, gitleaks, syft, grype, cosign from pinned release assets; yara from source with--disable-sharedso it statically links libyara; the pinned Go runtime; the isolated pip-audit venv) using a build toolchain (gcc, autotools, pkgconfig) that never reaches the final image.tools: a fresh final stage, not a trimmed copy of either builder. Installs onlygit-coreandpython3.11— driven by runninglddagainst every native binary (cosign/gitleaks/grype/syft/skopeo are fully static; opengrep needs only libc; yara/yarac need libc/libm; govulncheck needs libresolv/libc for its cgo DNS resolver — all already part of base glibc).git-coreis a genuine runtime dependency (gitleaks history mode, yara rule-pack clone both declare it), not a build leftover.install.shURL. The Go pin now reads fromconfig/external-tools.yaml(was hardcoded to 1.23.0 regardless of the manifest). Base image digests are inlined directly in eachFROMline, matching this repo's existing convention.Containerfile.runtime—FROMthe tools image by digest (never a floating tag);traust-core/traust-engine/traustviauv sync --frozen --no-editableat their pinned Git SHAs; labels carry the toolchain hash and commit. No database population step (the old image'sgrype db update, silently ignored on failure, is gone).scripts/containerfile-build-args.py— a pin-set hash over the resolved installed versions (sortedname=versionpairs) tags both images and thetoolchain:<hash>runner label; stable under comment/reordering edits to the manifest, changes only on a real version bump.--checkvalidates every binary the tools image installs has a real pin.scripts/build-toolchain-image.sh— rewritten to build both images, resolving the tools image's local digest automatically and passing it to the runtime build. Fixed a real shift-inside-a-for-loop argument-parsing bug along the way.traust/context.py—register_providers()now receivesfeeds_cache=, resolved lazily so a laptop install with noprogress_trackerconfigured keeps scanning tools' native caches unchanged. Needs traust-engine#104 (merged, scanner-db wiring): grype and govulncheck now read their databases from<feeds_cache>/tool-dbs/<tool>and raise before starting when that database is missing or unusable — confirmed necessary against the real binaries: grype fails on its own with an empty database, but govulncheck otherwise silently reports zero findings against an empty-db file://path. The traust-engine pin is bumped to402a8a2, the merged commit.CI (
.github/workflows/container-images.yml) — builds both images on every PR touching them, and checks every property this plan's "Done when" names: every binary at its pin, no Python stack or database in the tools image, no build toolchain or missing shared library, the runtime label names the toolchain hash, and a scan against an emptied database degrades rather than reporting zero.Retired:
Containerfile.toolchain(replaced),build-pinned-wheels.py(superseded byuv sync --frozeninstalling directly from the pinned Git sources).Verification
Built and verified end to end with real
podman build(not just written): both images build from a clean machine; every binary at its pin; notraust_core, no build toolchain, no database anywhere in the tools image (1.19GB, down from 1.64GB for the equivalent single-stage build); an empty mountedtool-dbs/grypemakes grype exit non-zero with a clear error; the composed runtime image imports the full traust stack and runs its CLI.uv run pytest tests/ -n auto: 2601 passed, 61 skipped (14 pre-existing failures confirmed present on a cleandevelopmentcheckout too — a local GPG-signing config issue, unrelated to this change)uv run ruff check ./ruff format --check .: cleanReviewer checks (from the plan)
FROMthe tools image by digestQuality checklist
Legacy files
Containerfile.toolchainContainerfile.tools+Containerfile.runtimescripts/build-pinned-wheels.pyuv sync --frozen --no-editableinstalling directly from the pinned Git sources inContainerfile.runtimeChanged tests
None — no existing test's assertions were changed, only new tests added.