feat: cache the trivy db and skip duplicate image scans#41
Conversation
Two efficiency improvements for pipelines that scan several images per build: - Mount a named docker volume (default: jpipe-trivy-cache) at trivy's cache path, so the vulnerability DB is downloaded once per agent instead of once per scan. The DB download typically costs more time than the scan itself. Opt out with cacheVolume: ''. - Skip scanning an image whose docker image ID was already scanned in the same build (e.g. one built image published under several names) and reuse the first report for publishing. Opt out with skipDuplicates: false. Behaviour is otherwise unchanged: same flags, same report publishing, same allowFailure semantics.
…d gates Review follow-ups on the trivy efficiency changes: - The duplicate marker now includes the scan-relevant configuration (severity, ignoreUnfixed, extraFlags, report, trivyVersion), so a second instance with a stricter or differently-formatted scan of the same image still runs instead of silently inheriting the first verdict. - Markers record the scan outcome; a skipped duplicate of a failed scan now re-signals the failure through its own allowFailure semantics instead of showing clean. - The cache volume defaults to a per-job name instead of one shared volume: trivy's cache DB takes an exclusive file lock, so a cluster-wide volume could make concurrent builds of unrelated jobs contend. Explicit names and '' (disable) behave as before. - Quote the report dirs in the copy command; document that the stale- marker cleanup is hygiene only and that trivy's exit code cannot distinguish vulnerabilities from other scan errors.
…up keys - Run trivy with --exit-code=2 and returnStatus: vulnerabilities exit 2 and stay subject to allowFailure, while any other non-zero exit (cache DB lock, image pull failure, ...) now fails the build outright instead of masquerading as a benign UNSTABLE. Operational failures write no marker, so a retry re-scans from scratch instead of inheriting a transient error as a verdict. - Key the duplicate marker on a SHA-256 digest of the scan config instead of String.hashCode(), whose 32-bit collisions could silently skip a stricter second scan. - Key the report directory on the full sanitized image path so distinct images sharing a basename no longer overwrite each other's reports. - Document that the per-job cache volume does not isolate concurrent builds of the same job, and that such contention now surfaces loudly; note why no marker is written on a hard findings failure.
|
Review follow-ups, all pushed:
On the per-job vs per-build volume question: kept per-job. A per-build volume would eliminate same-job contention but also the cache hit itself — every build would re-download the DB, which is the cost this feature removes. Same-job concurrent builds can still contend on the lock, but with the exit-code split that contention now surfaces as a loud operational failure rather than a silent UNSTABLE, which makes the trade-off observable. If it bites in practice, switching the default to include the executor/build number is a one-liner. |
A stray shell metacharacter in the tag (versions are often derived from git describe) would otherwise break the inspect and be masked by the "could not resolve" fallback. BREAKING CHANGE: with allowFailure: true, a trivy operational error (image pull failure, cache DB lock — any non-zero exit other than the findings code) now fails the build instead of being swallowed as an UNSTABLE stage. allowFailure only tolerates vulnerability findings; a scan that never ran can no longer pass a gate. Pipelines that relied on allowFailure to ride out transient scan errors will now fail loudly and should retry instead.
|
Final review round, addressed:
|
Product decision: a broken scan (cache DB lock, image pull failure, DB mirror down — any non-zero trivy exit other than the findings code) must not hold a developer's build hostage to scan-infrastructure flakiness. Such errors are now downgraded to an UNSTABLE stage with a distinct message instead of failing the build, independent of allowFailure (which governs vulnerability findings only). No dedup marker is written for a broken scan, so a retry re-scans and duplicates never inherit a transient error. This supersedes the earlier "fail hard on operational errors" behaviour; there is no longer a breaking change to allowFailure semantics.
|
Update on the operational-error handling: reverted the hard-fail from the previous round. The earlier revision failed the build on any non-vulnerability trivy exit (cache lock, DB mirror down, pull failure). On reflection that's the wrong trade-off for us — it would let trivy infrastructure flakiness block developers' builds. New behaviour:
PR title/body updated (dropped the BREAKING CHANGE note), and there's an explicit reviewer note documenting the decision so it isn't "corrected" back to hard-fail later. |
There was a problem hiding this comment.
Pull request overview
This PR enhances the TrivyPlugin to reduce CI time spent on repeated Trivy work by introducing a persistent vulnerability DB cache (via a Docker volume) and by skipping duplicate scans of identical images within the same build while preserving the correct pass/fail/unstable semantics.
Changes:
- Add a configurable Docker volume mount to persist Trivy’s DB/cache across scans (defaulting to a per-job volume; opt-out supported).
- Add duplicate-image detection (by Docker image ID + scan-config digest) to reuse prior scan report/verdict within the same build (opt-out supported).
- Refine exit-code handling to distinguish vulnerability findings (
--exit-code=2) from operational errors, treating operational errors as UNSTABLE/non-blocking by design.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/io/stenic/jpipe/plugin/TrivyPluginSpec.groovy | Adds comprehensive Spock coverage for cache volume behavior, duplicate-scan skipping, report dir naming, and findings vs operational-error behavior. |
| src/io/stenic/jpipe/plugin/TrivyPlugin.groovy | Implements cache volume mounting, duplicate-scan markers keyed by image ID + config digest, distinct report directories, and exit-code based error handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Two efficiency improvements to
TrivyPlugin, both on by default and individually opt-out:jpipe-trivy-cache-<job>): trivy's cache DB takes an exclusive file lock, so a single cluster-wide volume could make concurrent builds of unrelated jobs contend. Pass a fixedcacheVolumename to deliberately share, or''to disable the mount.allowFailure, so a strict gate never silently inherits a relaxed instance's pass. Detection degrades to a normal scan if the image ID can't be resolved;skipDuplicates: falsedisables it.Findings vs. operational errors — and why operational errors never block
The scan runs with
--exit-code=2viareturnStatus, which lets us tell two things apart that trivy's default--exit-code=1conflates:allowFailure(UNSTABLE when tolerated, failing when not). Unchanged from today.Deliberate product decision: operational errors are downgraded to an UNSTABLE stage and never block the build, independent of
allowFailure. Rationale: a developer's build must not be held hostage to trivy scan-infrastructure flakiness — a down DB mirror or a transient lock should not turn into a red, blocking pipeline. It is not swallowed silently either (the old behaviour, where a broken scan looked identical to a clean one): the stage goes UNSTABLE with a distinct message, and no dedup marker is written, so a retry re-scans from scratch and duplicates never inherit a transient error.Also: report directories are keyed on the full sanitized image path, so distinct images sharing a basename (
repo/appvsother/app) no longer overwrite each other's reports.Testing
TrivyPluginSpec(19 cases): per-job/explicit/disabled cache volume, single scan + outcome marker, duplicate skip + report reuse, config-mismatch and report-format-mismatch rescans, distinct image IDs, basename collision,skipDuplicates: false, image-ID fallback, findings with/withoutallowFailure, failed-verdict re-signaling (lenient + strict duplicates),--exit-code=2wiring, and operational errors going UNSTABLE + non-blocking (with and withoutallowFailure).mvn clean testviamake test: the new spec passes; the one pre-existingbasePipelineSpecfailure also fails on a cleanmaincheckout and is unrelated.