feat(server): build via BuildKit client when buildkitd address is set - #409
Merged
Conversation
Release artifacts are built by shelling out to the docker CLI (buildx create/build/rm), which fails with "executable file not found" when the plugin runs in an environment without the docker binary, e.g. compiled into a process shipped in a distroless image. Follow up on the configurable buildx driver (#398), which made the builder configurable but still requires the CLI. Introduce an alternative build path that talks to an already running buildkitd directly through github.com/moby/buildkit/client: the per-build buildx builder provisioning and removal disappear, and the build maps to a single Solve with the dockerfile.v0 frontend (context tar streamed via the session upload provider, secrets via secretsprovider, tar exporter into the same pipe the buildx path writes to). The buildkitd address is set per project via the configure endpoint (buildkitd_address) or, as a process-wide fallback, via the TRDL_BUILDKITD_ADDRESS env var; the per-project value wins. Supported address schemes: unix://, tcp:// (direct gRPC), docker-container://, kube-pod:// (via docker/kubectl exec). With no address set the docker CLI path stays byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
govulncheck reports five advisories against the v2.2.4 buildkit pulls in transitively (GO-2026-5064/5338/5475/5622/5758), all fixed in v2.2.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
The kubernetes-driver notes added in #398 said rootless BuildKit requires the `baseline` PodSecurity level. It does not fit `baseline` either: buildx's rootless pod spec sets `seccompProfile: Unconfined` and the `unconfined` AppArmor annotation (driver/kubernetes/manifest/manifest.go), and Unconfined is rejected by both the Seccomp and the AppArmor control at `baseline`. The builder namespace has to be `privileged` or exempt from PodSecurity admission. This also removes the contradiction with the external-buildkitd section added by this PR, which already states that BuildKit needs a relaxed seccomp/AppArmor profile even when rootless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
v1.18.6, pulled in by buildkit, carries GO-2026-5841 (out-of-bounds read in the s2 decoder), fixed in v1.18.7. The symbol is not reachable from trdl, but it was the only module-level advisory this branch added over main. With the bump, govulncheck reports exactly the same module-level set as main -- GO-2026-5932 (x/crypto/openpgp, unmaintained) and GO-2022-0646 / GO-2022-0635 (aws-sdk-go v1 S3 crypto), none of which have a fixed version. The plugin binary is byte-identical in size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
… BuildKit mode The BuildKit client path attached only the context uploader and the secrets provider to the Solve session, so no registry credentials ever reached buildkitd: with image-resolve-mode=pull and no cache, a private base image could not be resolved at all, while the docker CLI path receives those credentials from the docker config through buildx. Attach the same docker-config auth provider. The goroutine streaming the build context blocked on write forever whenever a build stopped consuming it: the upload provider closes the reader only once buildkitd pulls the context, so a Solve failing before that leaked the goroutine and its 64 MiB buffer on every failed release. Close the reader when the build returns, which covers the exec path too. Alongside that, fail closed on an address whose scheme carries no endpoint instead of failing at build time, wait for the build log to drain instead of dropping its tail and leaking the exec path scanner goroutine, pass a context to the exported address validation, and rename ContextPath after what it actually holds. github.com/docker/cli becomes a direct dependency and adds docker/docker-credential-helpers as an indirect one; both were already in go.sum. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
The tests around the BuildKit path asserted the maps the very same helpers had just built, so removing the context uploader from the Solve session — which breaks every build — left the whole suite green. Assert instead that the session serves the build context, the secrets and the registry credentials, that a failed build unblocks the context producer, and that the log writer drains before it returns. Add server:test:ai, since tests behind the ai_tests tag were never executed by any task. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
Tests behind the ai_tests build tag never ran anywhere, which left the only test exercising the BuildKit Solve path a no-op. Start buildkitd in a job and point the smoke test at it. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
The buildkitd address example used a plaintext tcp:// endpoint without saying that the build context, the build secrets and the mac signing credentials all travel over that connection, that the client neither encrypts it nor authenticates the daemon, and that one daemon is shared by every project pointed at it. Make securing the channel and isolating the daemon the administrator's stated responsibility. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
The e2e module consumes the server module through `replace … => ../server`, so the BuildKit dependency chain added to the server module left its go.mod incomplete: both `task lint` and `task e2e:test:e2e` failed with "updates to go.mod needed" before compiling a single test. Run go mod tidy, which also carries over the go directive bump to 1.25.9 that buildkit requires. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
alexey-igrychev
force-pushed
the
feat/server/buildkit-client-builder
branch
from
August 5, 2026 15:50
f365285 to
72df4a6
Compare
Nothing exercised the chain from `release` through the BuildKit client to the published artifact: the unit smoke test covers a bare Solve, and the e2e suites all build through the docker CLI. The flow_vault suite already switches build backends by environment for the buildx driver check, so give it the same hook for a buildkitd address and run it in CI against a buildkitd container. The suite's fixture reads two build secrets inside the build, so the run also proves the secrets reach the build through the session rather than only that their ids map. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
This was referenced Aug 5, 2026
`ai_server` read as "AI server" rather than as the kind of tests the job runs, which is what `unit_server` and `unit_client` express. Rename it to `ai_tests_server`, and add the new BuildKit e2e job to the coverage job's needs so its artifact is not uploaded in a race with the job producing it. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
vmrm
approved these changes
Aug 5, 2026
vmrm
left a comment
Contributor
There was a problem hiding this comment.
Approving. I read the seven commits on top of my four from #408 (73a6cbf9..0aac05e5) — they close real gaps, and I'd rather this PR go in than mine.
What the delta fixes, and why each one matters:
- Registry credentials (
buildkitSessionAttachables). The buildx path gets them from the docker config through the CLI; the client path had no auth attachable at all, soimage-resolve-mode=pullcould only reach public registries. Every private base image would have failed at resolve time, and my e2e never covered that because it pulls public images. - Draining the build context (
defer contextReader.Close()inBuilder.Build). A build that fails before the context is fully read left the goroutine feeding the pipe blocked on write forever. This is a leak on the error path of both builders, not just the BuildKit one. - Log tail (
logWriterreturning a wait function). Closing the pipe writer did not guarantee the scanner goroutine had drained it, so the last lines of a build log — including the interesting ones on failure — could be dropped. Both call sites now wait. - Empty endpoint after the scheme.
tcp://alone passed the scheme allowlist and then failed deep inside the client. Rejecting it atconfiguretime is the right place, consistent with the #398 driver allowlist. - CI against a real buildkitd —
End-to-end tests (BuildKit client)running the full release cycle overtcp://, plus theai_tests_serversmoke job. This is what #408 was missing: everything there was verified against unit tests and my own stand, and there was no positive marker in CI that the BuildKit path had actually built anything. WithTRDL_TEST_BUILDKITD_ADDRESSthreaded into the e2e configure, the same flow-vault suite now covers both backends. - QUICKSTART security section. Correct, and worth stating explicitly: the plugin ships the whole context and every build secret (including the mac-signing certificate, its password and the notary key) over that connection,
tcp://is plaintext and unauthenticated, and write access to<project>/configuretherefore becomes equivalent to access to the release secrets. The daemon-is-shared point is the one people will miss.
All checks are green on 0aac05e5, including the two new jobs.
One nit, not blocking: ValidateBuildkitdAddress and resolveBuildkitdAddress now take a context.Context that neither of them uses — it only ripples out into path_configure.go and the tests. Either use it or drop the parameter; if it's there for a planned reachability check against the address, a // TODO naming that would help.
#408 is closed in favour of this PR.
alexey-igrychev
marked this pull request as ready for review
August 5, 2026 18:40
This was referenced Aug 5, 2026
alexey-igrychev
added a commit
that referenced
this pull request
Aug 5, 2026
… certificate (#416) ## Summary Two release-build defects that predate #409 and affect both build paths: an oversized build log line hangs the release task, and a passwordless mac signing certificate fails the signer stage. ## Why Both turn an ordinary situation into a broken release, and neither reports a cause the operator can act on. `bufio.Scanner` stops at `bufio.ErrTooLong` on a line above 64 KB, after which the goroutine reading the pipe returns. The build keeps writing into that pipe, so the next write blocks forever — `cmd.Run` on the docker CLI path, the progress display on the BuildKit client path — and the release task hangs with no error, until the plugin process is restarted. The generated service Dockerfile mounts `certificate_password` unconditionally and reads it with `cat` under `set -e -o pipefail`, while both build paths served that secret only when the stored password was non-empty. `--mount=type=secret` defaults to `required=false`, so BuildKit mounts nothing and the `cat` takes the whole `RUN` down. A passwordless p12 is legitimate. ## Key changes - `server/pkg/docker/builder.go` — `logWriter` raises the line limit to 1 MiB and drains the pipe to `io.Discard` in a deferred call, so parsing that stops for any reason can no longer block the writer. An oversized line now costs the rest of the build log, reported through the logger, instead of the release. - `server/pkg/docker/mac_signing.go`, `server/pkg/docker/buildkit.go` — the password secret is always served, with an empty value when there is none, so quill receives an empty `QUILL_SIGN_PASSWORD`. Both paths mount the same ids and are changed together. - `server/pkg/docker/buildkit_test.go` — `TestBuildkitSecretsData_NoPasswordNoCredentials` asserted the defect (`NotContains` the password id); the passwordless half is replaced by the new test below, and what remains covers the no-credentials case. ## Verification - Mutations run, each reverted after the run, each killing exactly the test that covers it: - removing the drain from `logWriter` → `TestAI_LogWriter_OversizedLineDoesNotBlockTheBuild` fails after its 30s timeout, reporting the blocked writer; - restoring `if Password != ""` in `buildkitSecretsData` → `TestAI_MacSigningSecrets_PasswordlessCertificateIsStillServed` fails; - restoring the same condition in `GetMacSigningCommandMounts` → the same test fails, so both halves of it discriminate. - Not run: a release with a passwordless certificate through the mac-signing e2e suite. Its quill stub validates the five `QUILL_*` variables, so accepting an empty password there is a change to the fixture rather than a run of the existing one. ## Review focus / risks - The 1 MiB limit is a judgement call: below it a line is buffered whole, above it the remaining log is dropped. The hang is gone either way, and the drop is logged. - An empty `QUILL_SIGN_PASSWORD` reaches quill for a passwordless certificate. This is the intended reading of "no password", but it is a behaviour change for anyone who stored an empty password expecting the release to fail. ## After merge - [ ] Close #410 and #411 (linked by `Fixes` in the commits, so a squash merge closes them automatically — verify it did). --------- Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
alexey-igrychev
added a commit
that referenced
this pull request
Aug 5, 2026
`docs/_includes/reference/cli/trdl_use.md` still described the `trdl use` help text from before #391, so `task docs:gen` produced a diff on a clean checkout of `main` and every PR touching the generator picked up this unrelated change. Regenerated on its own, no hand edits. Noticed while working on #409, where the same diff appeared and was reverted to keep that PR scoped. Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
alexey-igrychev
added a commit
that referenced
this pull request
Aug 5, 2026
## Summary A project can now choose its buildx driver and driver options through `configure`, instead of only through the environment of the Vault process. That is the only route available when the secret engine is compiled into a host process whose environment the administrator cannot set, where the `kubernetes` driver was unreachable until now. Continues #412 by @vmrm, rebased onto `main` after #409. ## What - `configure` accepts `buildx_driver` — `docker-container` or `kubernetes`, empty by default — and it wins over `TRDL_BUILDX_DRIVER`. - `configure` accepts `buildx_driver_opts` — a list, one `--driver-opt` per element, empty by default — and it wins over `TRDL_BUILDX_DRIVER_OPTS_*`. Elements are passed through verbatim, so `nodeselector=disktype=ssd,zone=a` stays one option. - Each of the two settings resolves on its own: configuration, then environment, then the previous default of `docker-container` with no options. A field omitted or set to a blank value means "not configured" and falls back to the environment instead of clearing it, so building with no options while the environment defines some requires unsetting those variables. - An unsupported driver is rejected whatever its source: writing `buildx_driver=docker` fails `configure` and stores nothing, while an unsupported `TRDL_BUILDX_DRIVER` still fails when the builder is created. The error names the setting the value came from. - `configure` rejects `buildx_driver` or `buildx_driver_opts` written together with `buildkitd_address`, and leaves an already stored configuration untouched: that address replaces the buildx path entirely, so the driver settings would have no effect. Blank values count as unset on both sides of that check. - When the address comes from `TRDL_BUILDKITD_ADDRESS` instead, the combination cannot be refused at write time — the variable is process-wide and can change after a project is configured — so the release reports `the configured buildx driver settings are not used` in both the release task log and the plugin log. - An update that omits the buildx fields clears the stored ones: `configure` replaces the whole document. - With neither field set, `docker buildx create` is invoked exactly as before, down to the argument list, and a configuration stored before these fields existed reads back with both fields empty and builds as it did. - UNVERIFIED: that `buildx_driver_opts` and `buildkitd_address` reach the release build at all — deleting their assignment in `pathRelease` leaves every suite green. The driver now has an end-to-end guard; these two would need one job each, because the value has to break the build when it is lost. ## Why Everything else a project needs already lives in `configure` — the Git repository, the S3 credentials, the signature quorum — because Vault has no other per-project channel. The build backend was the exception: it could only be set through the environment of the process that hosts the plugin, and a host process that ships the engine as a built-in plugin may expose no way to set one, which left such an installation on the default driver permanently. Environment-only was the alternative, and #409 shows why it does not hold: the same argument produced `buildkitd_address` as a `configure` field, so keeping the driver out of `configure` would have split one decision across two mechanisms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an alternative release-build path that talks to an already running
buildkitdthrough the BuildKit Go client instead of shelling out todocker buildx. The address is set per project viaconfigure(buildkitd_address) or process-wide viaTRDL_BUILDKITD_ADDRESS; with no address set, thedocker buildxpath is unchanged.Continues #408 by @vmrm — their four commits are taken as-is, with four commits on top that close the findings from reviewing them.
Why
The secret engine is also compiled into host processes shipped in distroless images that contain no
dockerbinary and no docker socket. Thereexec.CommandContext(ctx, "docker", …)fails withexecutable file not foundbefore the buildx driver choice from #398 can matter at all. Pointing the plugin at an external buildkitd removes the CLI dependency forunix:///tcp://while keeping the docker CLI as the default for every existing installation.The address lives in per-project
configurerather than only in the environment because module-based deployments have no way to inject env vars into the Vault pod, andconfigureis already the per-project channel for s3/git/quorum settings.Key changes
From #408:
server/pkg/docker/buildkit.go— the release build mapped onto a singleSolveagainst an external buildkitd: frontenddockerfile.v0, in-context service Dockerfile viafilename,no-cache,image-resolve-mode=pull, the context tar streamed through the session upload provider, build secrets and mac-signing credentials served bysecretsprovider.FromMapunder the ids the generated Dockerfile mounts, tar exporter writing into the same pipe the buildx path writes to, progress through the existing logger.server/pkg/docker/builder.go—NewBuilderreturns a BuildKit-mode builder when an address resolves: nobuildx create, andRemoveis a no-op since nothing is provisioned per build.server/path_configure.go— optionalconfigurefieldbuildkitd_address, validated at configure time against a scheme allowlist (unix,tcp,docker-container,kube-pod), fail-closed like the feat(server): make buildx driver configurable via env #398 driver allowlist. Per-project config wins over the env fallback.github.com/moby/buildkit v0.31.2direct,golang.org/x/syncpromoted from indirect, 46 new modules overall, plugin binary 42.6 MB → 51.5 MB per the measurements in feat(server): build via BuildKit client when buildkitd address is set #408 (not re-measured here).containerd/v2pinned to v2.2.5 andklauspost/compressto v1.18.7 so the module-level govulncheck advisory set matchesmain.On top of that:
fix(server): the Solve session now also attaches the docker-config auth provider — without it no registry credentials reached buildkitd, so withimage-resolve-mode=pulland no cache a private base image could not be resolved at all, while the CLI path gets those credentials through buildx.Builder.Buildcloses the context reader on return: the upload provider closes it only once buildkitd pulls the context, so a Solve failing earlier left the goroutine streaming the context blocked on write forever with its 64 MiB buffer. An address whose scheme carries no endpoint (unix://) is now rejected at configure time instead of failing on the next release.logWriterreturns a wait function, so the tail of a build log is not dropped and the exec path stops leaking its scanner goroutine.test(server): the session wiring, the context release and the log drain are asserted against behaviour rather than against the maps the same helpers build.server:test:airuns everything behind theai_teststag, which no task ran before.ci: jobai_serverstartsmoby/buildkit:v0.31.2and points the smoke test at it, so the only test that exercises the Solve path stops being a no-op.docs: the buildkitd section states that the build context, the build secrets and the mac-signing credentials travel over that connection, that the client neither encryptstcp://nor authenticates the daemon, that the address is a trust boundary for whoever can writeconfigure, and that one daemon is shared by every project pointed at it — all of it the administrator's responsibility.github.com/docker/clibecomes a direct dependency (already in the graph via the connhelpers) and addsdocker/docker-credential-helpersas an indirect one; both were already ingo.sum.Verification
tcp://, using the same recipe the new CI job uses: streamed tar context in,.trdl/Dockerfilepicked byfilename, a secret mount read back out of the exported artifacts tar (TestAI_BuildkitSmoke, 2s).defer contextReader.Close()(the producer test then reports the producer still blocked); removing<-donefrom the log wait; removing the empty-endpoint check. Before these tests existed, removing the context uploader from the session — which breaks every build — left the whole suite green.docker-container://andkube-pod://transports live (onlytcp://was exercised); binary size after thedocker/cli/cli/configaddition.Review focus / risks
server/pkg/docker/buildkit.go— the Solve mapping and the session attachables.tcp://is plaintext and unauthenticated: the client has no TLS options, so an operator who ignores the documented requirement ships build secrets and the mac-signing notary key in the clear. Adding mTLS options is deliberately left to a separate change; the current mitigation is documentation only.go.sumchurn: MVS bumps of existing indirect deps alongside the new modules.certificate_passwordunconditionally, so a passwordless mac-signing certificate fails in the signing stage on both paths; a build log line above 64 KB kills thebufio.ScannerinlogWriterand blocks the release.After merge
license/snykandsecurity/snykchecks; their findings are only visible inside the werf Snyk org.docs/_includes/reference/cli/trdl_use.md, stale onmainsince feat(client): add support for pinning explicit versions in main commands #391 and deliberately left out of this PR.