builds: address build-system review follow-ups - #384
Conversation
- Bound POST /builds multipart reads: 512 MiB source tarball and 1 MiB per form field, returning 400 on overflow instead of unbounded reads - Fail builds hard when required secrets cannot be provided: provider errors and missing or malformed secret IDs now produce clear build or request failures, and the builder agent fails instead of proceeding without secrets on timeout - Cancel and await in-flight builds on service shutdown via queue lifecycle tracking; builds still outlive request cancellation through context.WithoutCancel so trace values propagate - Recover interrupted build attempts by removing leftover source/config volumes, deleting only this build's stale builder instance when a leftover volume is still attached - Remove the fixed pre-dial sleep in waitForResult in favor of context-aware readiness retries - Drop the unimplemented AllowedDomains build policy field - Log corrupt build metadata entries instead of silently skipping them - Correct ErrBuildInProgress documentation for the queued-to-running cancel race
-->
✱ stlc build✅ go code · compare
✅ typescript code · compare
Diagnostics: 💡 0 new / 5 total note
Build metadata
This comment is auto-generated by stlc and is kept up to date as you push. |
hiroTamada
left a comment
There was a problem hiding this comment.
Fable 5 review — round 1
Reviewed head 0a18d88 against #54 and #337 (including the Bugbot finding on #337). Read the full diff plus surrounding code (queue.go, manager.go, builder_agent/main.go, cmd/api/main.go shutdown handler, lib/volumes/manager.go, lib/instances/manager.go). Verified locally:
go build -tags containers_image_openpgp ./...,go vet,gofmtcleango test -race -count=1 -tags containers_image_openpgp ./lib/builds/...— passgo test -race -count=1 -tags containers_image_openpgp -run 'Build' ./cmd/api/api/— passmake oapi-generatereproduceslib/oapi/oapi.goexactly (no drift; spec change is doc-only)
What I checked strictly
- Bounded memory / multipart: every part now goes through
readLimitedPart(limit+1LimitReader), including unknown field names;part.Close()on all paths; boundary tests at/over limit for both source and small fields. No bypass found — a part is capped regardless of itsContent-Dispositionname, and onlysourcegets the 512 MiB limit. - Shutdown / cancellation ordering:
wg.Addhappens under the queue mutex andcanStartLockedchecksshutdown, so noAdd-after-Waitrace; pending builds are cancelled and left on disk;Shutdownis idempotent;MarkComplete/cancel/wg.Donedefer order is correct.main.godrains HTTP beforeBuildManager.Shutdown, so no new enqueues race shutdown. Goroutine accounting verified by the shutdown/await tests and the-racestress test. - Context-value propagation:
CreateBuildenqueues withcontext.WithoutCancel(ctx);TestManagerShutdown_InFlightBuildpins both value propagation and detachment from request cancellation. Dedup paths inEnqueueSerialcreate no leaked cancels (theWithCancelis created after the dedup checks). - Hard-fail secret protocol: both sides fail closed — host validates IDs (
ValidateSecretIDat API boundary and inFileSecretProvider), reports provider errors and missing values viasecrets_response.error; guest fails on host error, on missing IDs in the response, on the 30s timeout, and backstops with a per-secretos.Stat(which also covers the "config not loaded yet, skipped secrets" path inhandleSecretsRequest).NoOpSecretProvider's empty map is caught bymissingSecretsErroron the host. - Crash recovery conservatism:
deleteLeftoverBuildVolumeonly touchesbuilder-<buildID>by exact name and refuses to force-delete a volume attached to an unknown holder;CreateVolumeFromArchivereturnsErrAlreadyExistsbefore consuming the reader (verified inlib/volumes/manager.go), so the source-reader reuse on retry is sound. The #337 Bugbot finding (config copy-over fallback masking a failed recreate) is fixed here and covered byTestRegisterBuildConfigVolume_RecreateFailure. - Corruption observability / API compat / scope:
listAllBuildslogging tested;AllowedDomainswas never exposed in the OpenAPI spec (Go-only field, stale JSON keys unmarshal harmlessly); diff stays in scope.
Non-blocking notes (low)
lib/builds/manager.gorunBuild+TestManagerShutdown_InFlightBuild: a build cancelled by graceful shutdown is persisted asfailedwith a rawcontext canceled-style error. Deliberate and pinned by test, but the user-facing failure reason for "server restarted" is opaque; consider mapping shutdown cancellation to a clearer error message (or leaving such builds inbuildingfor startup recovery, which the leftover-volume handling now supports).cmd/api/api/builds.goreadLimitedPart: the underlyingio.ReadAllerror is dropped ("failed to read %s field"); logging the cause would help debug malformed multipart bodies. Matches prior behavior, so purely a nice-to-have.lib/builds/manager.gowaitForResult: after the final failed dial attempt the loop still runs the instance check and one full 2s retry sleep before exiting with the "after retries" error — one wasted interval, cosmetic.
No blocking, high, or medium correctness or test findings remain. Fable 5 is satisfied.
A build whose run is cancelled by service shutdown kept no useful terminal state: it was marked failed with an opaque "context canceled" error and, unlike a crash, was never recovered on the next start. Keep such builds in their non-terminal status so RecoverPendingBuilds re-runs them, matching crash semantics now that leftover source/config volume recovery makes re-runs safe. Also surface multipart part read errors in the 400 message and skip the trailing retry sleep after the final builder-agent dial attempt.
|
Follow-up on the round-1 Fable notes, addressed in 18fafa4:
|
hiroTamada
left a comment
There was a problem hiding this comment.
Fable 5 final review — post-Ralph pass
Re-reviewed the new head 18fafa4 ("builds: leave shutdown-interrupted builds for startup recovery") on top of the round-1 head, plus all prior comments.
Ralph-pass changes verified
- Shutdown-interrupted builds now recover instead of failing opaquely (
lib/builds/manager.gorunBuild): thectx.Err() != nilgate is precise — the run context is only ever cancelled by queueShutdownonce a build has started (pending-cancel removes the build beforeStartFnruns; the goroutine's deferredcancelfires afterrunBuildreturns). Crucially, the check is on the outer run ctx, notbuildCtx(WithTimeout), so genuine build timeouts still mark the build failed rather than being mistaken for shutdown. UserCancelBuildon a running build works via instance deletion, not context cancellation, so the terminal-state protection path is unchanged (still pinned byTestUpdateBuildComplete_PreservesTerminalStatus). The same gate on thewaitForImageReadyfailure branch is consistent.TestManagerShutdown_InFlightBuildupdated to pinStatusBuilding+ nil error, matching crash semantics thatRecoverPendingBuilds+ the leftover-volume recovery from this PR make safe to re-run. readLimitedPartnow wraps the underlying read error — clearer 400s, nothing sensitive leaked.waitForResultskips the trailing retry sleep after the final dial attempt;conn == nilerror path still carries the last dial error.cmd/api/main.go/Manager.Shutdowndoc comments updated to match the new semantics.
Validation on 18fafa4
go build -tags containers_image_openpgp ./...,go vet,gofmt— cleango test -race -count=1 -tags containers_image_openpgp ./lib/builds/...— passgo test -race -count=1 -tags containers_image_openpgp -run 'Build' ./cmd/api/api/— passmake oapi-generate— zero drift inlib/oapi/oapi.go
All round-1 notes are resolved and no new issues were introduced. No blocking, high, or medium findings remain. Fable 5 is satisfied after the Ralph pass.
Summary
Implements the still-actionable follow-up items from the build-system review tracked in #54, against current main:
sourcetarball (consistent with the 10 GB source volume) and 1 MiB for each small form field. Over-limit uploads get a clear 400 (invalid_source/invalid_request) instead of an unboundedio.ReadAll; multipart read failures surface their cause in the 400 message.FileSecretProvidernow errors on missing secrets and invalid IDs (path-traversal validation preserved via exportedValidateSecretID, also enforced at the API boundary with a 400). The host reports provider errors / missing values to the builder agent over vsock (secrets_response.error), and the agent fails the build with a clear message instead of proceeding with empty secrets — including the guest-side 30s wait timeout, which previously proceeded anyway.BuildQueuestart functions now take a context; each run derives a cancellable context tracked by the queue, and newManager.Shutdown/BuildQueue.Shutdown(wired into the API shutdown handler) cancel in-flight builds and wait for their goroutines.CreateBuildenqueues withcontext.WithoutCancel(ctx)so builds survive request cancellation while keeping trace/log values. Pending builds are never started after shutdown — they stay queued on disk for startup recovery. Cancel of a pending build also releases its context. A run cancelled by shutdown is left in its non-terminal status (never a spurious "context canceled" failure) so startup recovery re-runs it, matching crash semantics.executeBuildnow tolerates leftoverbuild-source-<id>/build-config-<id>volumes from an interrupted attempt — delete and recreate once; if a leftover volume is still attached, only the stalebuilder-<id>instance is deleted (detaching it) before the volume delete, and a volume attached to an unknown instance is never force-deleted (clear error instead). The config-volume copy-over fallback no longer masks a failed recreate (addresses the known review finding on builds: recover crashed builds by cleaning up leftover source/config volumes #337, which this supersedes on current main).waitForResultdrops the unconditional 3s sleep; it dials immediately and retries with a context-aware 2s interval (30 attempts, no trailing sleep after the last one), failing fast if the builder instance stops.AllowedDomainsremoved: the build-policy field was never wired to anything; removed fromBuildPolicyalong with the stale "domain allowlist" claim in the builds README. Egress stays all-or-nothing vianetwork_mode.listAllBuilds/listPendingBuildstake a logger and warn on unreadable/unparseable entries while still returning valid builds.ErrBuildInProgressdoc corrected: the sentinel covers the queued-to-running cancel race; comment now says so.Also pinned existing guarantees with tests: terminal-state protection (a cancelled build whose run later fails stays cancelled), duplicate builder-instance deletion tolerance on cancel, and the queued-race sentinel.
Test plan
go test -tags containers_image_openpgp ./lib/builds/...— passgo test -race -tags containers_image_openpgp ./lib/builds/...— passgo test -race -tags containers_image_openpgp -run 'TestCreateBuild_' ./cmd/api/api/— passbuildingfor recovery (not a spurious failure), multipart limit boundaries (at/over limit for source and form fields, secret-ID rejection), provider hard-fail cases, queue shutdown cancellation/await/no-start-after-shutdown plus a race stress test, manager shutdown detaching request cancellation while preserving context values, leftover-volume recovery (already-exists, in-use with/without stale builder, config recreate failure), corrupt-metadata logging.go vetandgofmtclean.lib/oapi/oapi.goregenerated viamake oapi-generate(spec comment only; thesourcefield now documents the size limits).cmd/api/apisuite: VM-backed tests fail in this environment (no KVM / no net-admin) identically onorigin/main; all non-VM tests pass.Note
Medium Risk
Touches build queue shutdown, secret handling, and volume recovery on retry—important for reliability and security, but well-tested and scoped to the build path.
Overview
Follow-up hardening for the build system: upload bounds, required secrets, graceful shutdown, and retry after interruption.
POST /builds now reads every multipart part through size limits (512 MiB for
source, 1 MiB per other field) and returns clear 400s instead of unbounded reads. Secret IDs are validated at the API (ValidateSecretID) before a job is accepted.Secrets are required end-to-end: the file provider errors on missing or traversal-prone IDs; the host sends
secrets_response.errorover vsock when fetch fails; the builder agent fails the build on host errors, timeouts, or missing values (no more proceeding with empty secrets after a 30s wait).Lifecycle:
BuildQueueruns use a cancellable context;Manager.Shutdown(API shutdown) cancels in-flight work and waits for goroutines. Enqueue usescontext.WithoutCancelso builds outlive request cancellation but still carry trace context. Shutdown-interrupted runs stay in a non-terminal status for startup recovery. Crash retries delete and recreate leftoverbuild-source-*/build-config-*volumes, optionally removing a stalebuilder-<id>instance when a volume is still attached.Smaller changes: drop unused
AllowedDomainsfromBuildPolicy;waitForResultdials immediately with retries (no fixed 3s sleep); corrupt build metadata is logged and skipped when listing.Reviewed by Cursor Bugbot for commit 18fafa4. Bugbot is set up for automated code reviews on this repo. Configure here.