feat(server): build via BuildKit client when buildkitd address is set - #408
feat(server): build via BuildKit client when buildkitd address is set#408vmrm wants to merge 4 commits into
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 (werf#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>
5a4f1b3 to
a392942
Compare
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 werf#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>
Dependency delta, re-measuredSince "this module had zero docker/moby/buildkit dependencies before" is the main thing to weigh here, the exact numbers, measured on
The two numbers differ because Direct additions — two: The other 45 are indirect, and they are not 45 independent decisions — they are four clusters plus a tail:
Nothing was dropped from None of this is reachable when On the two red Snyk checks
For precedent on shipping with these two checks red: #389 was merged with the same pair failing. CIThe The only |
|
@alexey-igrychev could you take a look when you get a chance, or point me at whoever should? Two concrete asks:
On the design itself, the one question genuinely worth your call is whether |
|
Superseded by #409, which carries these four commits unchanged ( Nothing from this PR is lost: the branch |
…#409) ## Summary Adds an alternative release-build path that talks to an already running `buildkitd` through the BuildKit Go client instead of shelling out to `docker buildx`. The address is set per project via `configure` (`buildkitd_address`) or process-wide via `TRDL_BUILDKITD_ADDRESS`; with no address set, the `docker buildx` path 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 `docker` binary and no docker socket. There `exec.CommandContext(ctx, "docker", …)` fails with `executable file not found` before the buildx driver choice from #398 can matter at all. Pointing the plugin at an external buildkitd removes the CLI dependency for `unix://`/`tcp://` while keeping the docker CLI as the default for every existing installation. The address lives in per-project `configure` rather than only in the environment because module-based deployments have no way to inject env vars into the Vault pod, and `configure` is 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 single `Solve` against an external buildkitd: frontend `dockerfile.v0`, in-context service Dockerfile via `filename`, `no-cache`, `image-resolve-mode=pull`, the context tar streamed through the session upload provider, build secrets and mac-signing credentials served by `secretsprovider.FromMap` under 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` — `NewBuilder` returns a BuildKit-mode builder when an address resolves: no `buildx create`, and `Remove` is a no-op since nothing is provisioned per build. - `server/path_configure.go` — optional `configure` field `buildkitd_address`, validated at configure time against a scheme allowlist (`unix`, `tcp`, `docker-container`, `kube-pod`), fail-closed like the #398 driver allowlist. Per-project config wins over the env fallback. - Dependencies: `github.com/moby/buildkit v0.31.2` direct, `golang.org/x/sync` promoted from indirect, 46 new modules overall, plugin binary 42.6 MB → 51.5 MB per the measurements in #408 (not re-measured here). `containerd/v2` pinned to v2.2.5 and `klauspost/compress` to v1.18.7 so the module-level govulncheck advisory set matches `main`. 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 with `image-resolve-mode=pull` and no cache a private base image could not be resolved at all, while the CLI path gets those credentials through buildx. `Builder.Build` closes 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. `logWriter` returns 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:ai` runs everything behind the `ai_tests` tag, which no task ran before. - `ci`: job `ai_server` starts `moby/buildkit:v0.31.2` and 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 encrypts `tcp://` nor authenticates the daemon, that the address is a trust boundary for whoever can write `configure`, and that one daemon is shared by every project pointed at it — all of it the administrator's responsibility. `github.com/docker/cli` becomes a direct dependency (already in the graph via the connhelpers) and adds `docker/docker-credential-helpers` as an indirect one; both were already in `go.sum`. ## Verification - Live run against buildkitd v0.31.2 over `tcp://`, using the same recipe the new CI job uses: streamed tar context in, `.trdl/Dockerfile` picked by `filename`, a secret mount read back out of the exported artifacts tar (`TestAI_BuildkitSmoke`, 2s). - Mutations run against the new tests, each failing only the test that covers it: dropping the auth provider from the session; removing `defer contextReader.Close()` (the producer test then reports the producer still blocked); removing `<-done` from 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. - Not run: the quill-stub mac-signing e2e from #398 against a buildkitd address; the `docker-container://` and `kube-pod://` transports live (only `tcp://` was exercised); binary size after the `docker/cli/cli/config` addition. ## 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. - Behaviour differences when, and only when, an address is set: no per-build builder lifecycle, so concurrent releases share one buildkitd and its gc/parallelism limits; secrets are no longer exported into the plugin process environment. - `go.sum` churn: MVS bumps of existing indirect deps alongside the new modules. - Pre-existing and untouched here: the generated Dockerfile mounts `certificate_password` unconditionally, so a passwordless mac-signing certificate fails in the signing stage on both paths; a build log line above 64 KB kills the `bufio.Scanner` in `logWriter` and blocks the release. ## After merge - [ ] Close #408 with a link to this PR — required, the contributor is waiting on a maintainer response there. - [ ] Triage the `license/snyk` and `security/snyk` checks; their findings are only visible inside the werf Snyk org. - [ ] File follow-ups for the two pre-existing defects listed above (passwordless mac-signing mount, oversized log line). - [ ] Regenerate `docs/_includes/reference/cli/trdl_use.md`, stale on `main` since #391 and deliberately left out of this PR. --------- Signed-off-by: Vasily Marmer <vasily.marmer@flant.com> Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com> Co-authored-by: Vasily Marmer <vasily.marmer@flant.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Merged as #409 — thank you, this is the shape it landed in. Your four commits are the base of that PR. The squash merge collapsed them into What changed on top of your work:
On your two asks: both Snyk checks came out green on #409 ( Two pre-existing defects surfaced while reviewing this and are fixed in #416: #410 (a passwordless mac signing certificate takes the signer stage down) and #411 (a build log line above 64 KB hangs the release task). #413 proposes dropping the #412 covers the "no environment variables in the pod" half of the motivation without any of this dependency cost, and is being handled separately — it will need a rebase now that #409 has landed. |
Summary
Add an alternative build path that talks to an already running
buildkitddirectly through the BuildKit client (github.com/moby/buildkit/client), instead of shelling out to thedockerCLI. The address is set per project viaconfigure(buildkitd_address) or process-wide viaTRDL_BUILDKITD_ADDRESS; with no address set, the existingdocker buildxpath is byte-for-byte unchanged. Follow-up to #398.Key changes
server/pkg/docker/buildkit.go(new): the release build mapped onto a singleSolveagainst an external buildkitd:dockerfile.v0, in-context service Dockerfile via thefilenameattr,no-cache,image-resolve-mode=pull(the equivalents of--no-cache/--pull);docker buildx buildstdin) is streamed through the session upload provider;secretsprovider.FromMapunder the same ids the generated Dockerfile mounts (--mount=type=secret,id=…), so the mac-signing signer stage keeps working; unlike the CLI path, they are no longer exported into the plugin's process environment;PlainModeprogress display), including the existing error recommendations.unix://andtcp://are direct gRPC (no external binaries),docker-container://andkube-pod://go throughdocker exec/kubectl exec(connhelpers registered via blank imports).server/pkg/docker/builder.go:NewBuilderreturns a BuildKit-mode builder when an address is resolved — nobuildx create, andRemoveis a no-op (nothing is provisioned per build). Otherwise the previous exec path runs unchanged.server/path_configure.go: new optionalconfigurefieldbuildkitd_address, validated at configure time (scheme allowlist, fail-closed like the feat(server): make buildx driver configurable via env #398 driver allowlist). Per-project config wins over theTRDL_BUILDKITD_ADDRESSenv fallback.TestSetCliArgs_ExecPathUnchanged,TestResolveBuildkitdAddress_EmptyKeepsExecPath, plus the pre-existingTestBuildxCreateArgs_*suite);TestAI_BuildkitSmoke(opt-in,ai_teststag, skips withoutTRDL_SMOKE_BUILDKITD_ADDRESS): end-to-end against a real buildkitd — tar context in, secret mount readable in aRUN, artifacts tar out.configureAPI partial. Also corrects thekubernetesdriver note added in feat(server): make buildx driver configurable via env #398, which said rootless BuildKit requires PodSecuritybaseline— buildx's rootless pod spec setsseccompProfile: Unconfinedplus theunconfinedAppArmor annotation (driver/kubernetes/manifest/manifest.go), andUnconfinedis rejected by both the Seccomp and the AppArmor control atbaseline, so that namespace has to beprivilegedor exempt from PodSecurity admission.Why
The trdl secret engine is also compiled into host processes and shipped in distroless images that contain no
dockerbinary (Deckhouse Stronghold ships/usr/bin/strongholdalone, read-only root fs, no docker socket). There,exec.CommandContext(ctx, "docker", …)fails withexecutable file not foundbefore the buildx driver choice from #398 can even matter. Pointing the plugin at an external buildkitd removes the CLI dependency entirely forunix:///tcp://, while keeping the docker CLI as the default for every existing installation.The address lives in per-project
configure(not only env) because in module-based deployments there is no way to inject env vars into the Vault pod, whileconfigureis already the per-project channel for s3/git/quorum settings.Dependencies
Honest numbers, since the server module previously had zero docker/moby/buildkit dependencies:
github.com/moby/buildkit v0.31.2,golang.org/x/syncpromoted from indirect;go.modoverall, notablycontainerd/*(API types),docker/cli(connhelper/commandconn only),tonistiigi/fsutil,opentelemetry-*(buildkit client instrumentation),in-toto,grpc-gateway;godirective 1.25.0 → 1.25.9 (required by buildkit);toolchain go1.25.12unchanged. buildkit v0.32.x was deliberately not taken — it requires go 1.26.3, which would break the Go 1.25.12 builder image pinned intrdl.yaml;containerd/v2pinned to v2.2.5 instead of the v2.2.4 buildkit requires: govulncheck reports five advisories against v2.2.4 (GO-2026-5064/5338/5475/5622/5758), all fixed in v2.2.5;hashicorp/go-retryablehttpv0.7.7 → v0.7.8 (required by every buildkit release since v0.28); every newly added module is Apache-2.0/MIT/BSD (checked with go-licenses);klauspost/compressv1.18.6 → v1.18.7 (buildkit pulls in v1.18.6, which carries GO-2026-5841; the symbol is unreachable from trdl), the module-level advisory set is identical tomain— 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 two symbol-level findings it also reports are stdlib on the scanning host's go1.26.4 toolchain, not properties of this module;Review focus / risks
server/pkg/docker/buildkit.go— the Solve mapping. Verified live against buildkitd v0.31.2 over bothdocker-container://(connhelper) andtcp://(direct gRPC): streamed tar context,.trdl/Dockerfilepicked byfilename, a secret mount read back out of the exported artifacts tar. The smoke test in the PR reproduces exactly that; run it withTRDL_SMOKE_BUILDKITD_ADDRESS=docker-container://<buildkitd-container> go test -tags ai_tests -run TestAI_BuildkitSmoke ./pkg/docker/.privilegedis claimed anywhere in these docs).go.sumchurn: MVS bumps of a handful of existing indirect deps alongside the new modules.🤖 Generated with Claude Code