Skip to content

runtime: select BuildKit explicitly + fix tar symlink encoding#47

Merged
bilby91 merged 3 commits into
mainfrom
fix/buildkit-builder
May 12, 2026
Merged

runtime: select BuildKit explicitly + fix tar symlink encoding#47
bilby91 merged 3 commits into
mainfrom
fix/buildkit-builder

Conversation

@bilby91
Copy link
Copy Markdown
Member

@bilby91 bilby91 commented May 12, 2026

Summary

  • runtime/docker/build.go was calling ImageBuild without setting ImageBuildOptions.Version, so dockerd routed every build through its classic builder. Classic synthesizes one intermediate container per Dockerfile step; each container API call is authz-checked. Behind an authz plugin this turns a sub-second build into a multi-minute one (~140× slowdown observed on a 7-step Dockerfile against a 7.7GB base — production uid_reconcile builds were taking ~200s where BuildKit takes ~1.5s on the same host).
  • Setting Version: build.BuilderBuildKit is sufficient — the moby /build endpoint accepts a tar request body in BuildKit mode, so no session / filesync wiring is required (verified empirically against a real dockerd before writing the patch). This avoids pulling in github.com/moby/buildkit as a direct dep.
  • Per-step progress events are dropped under BuildKit (aux records are protobuf-encoded and decoding requires the buildkit module). BuildStart / BuildCompleted still fire and errors still propagate; per-vertex progress is deferred to a future PR if needed.
  • Fix a long-standing bug in tarDirectory: symlinks were emitted as TypeSymlink with an empty Linkname, which some tar readers reject as malformed. Common in compose contexts containing node_modules/.bin/* or similar bin-symlinks. Now os.Readlink(path) is passed to tar.FileInfoHeader.
  • Drop the # syntax=docker/dockerfile:1.4 directive from feature/dockerfile.go: classic ignored it, but BuildKit treats it as a hard frontend-pull requirement that needs a session for credential forwarding (we don't open one) and hangs behind broken registry mirrors. Emitted Dockerfile uses no 1.4-only syntax. The identical fix in useruid.go is on branch fix/uid-reconcile-syntax-directive.

BuildKit is now a hard requirement. Docker Engine has shipped with BuildKit enabled by default since 23.0 (Feb 2023); this is in line with the lib's modern-spec stance.

Test plan

  • Unit tests pass (go test ./...)
  • New integration test TestBuildContext_SurvivesSymlinks builds a workspace whose context contains an unreferenced symlink — regression guard for the tar encoding bug
  • Existing integration tests pass (go test -tags integration ./test/integration/, ~52s)
  • Frontend-directive regression guard added to feature/dockerfile_test.go
  • Verified against local dockerd: Version=BuilderBuildKit + tar request body builds successfully without a session

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Build contexts preserve symlinks so unreferenced symlinked files won’t break builds.
    • Docker builds now enforce BuildKit for consistent build behavior.
    • Generated Dockerfiles no longer include the syntax directive comment.
  • Tests

    • Added integration test validating symlink preservation in build contexts.
    • Added unit tests covering Dockerfile base-image extraction and build-arg substitution.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c618ab3-c757-4cbf-946e-9626722a901d

📥 Commits

Reviewing files that changed from the base of the PR and between 7f942c8 and 9d3dcc1.

📒 Files selected for processing (1)
  • runtime/docker/build_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • runtime/docker/build_test.go

📝 Walkthrough

Walkthrough

Removes the generated Dockerfile # syntax= directive, enforces BuildKit for image builds, pre-pulls resolved base images, preserves symlinks in build-context tar archives, and adds unit and integration tests for parsing, substitution, and symlink survival.

Changes

Docker BuildKit and symlink handling

Layer / File(s) Summary
Dockerfile syntax directive removal
feature/dockerfile.go, feature/dockerfile_test.go
generateDockerfile no longer emits a # syntax= frontend directive; tests updated to assert no # syntax= lines appear.
BuildKit enforcement, pre-pull, and helpers
runtime/docker/build.go
Import BuildKit build types, set client.ImageBuildOptions.Version = build.BuilderBuildKit, add prePullBaseImages, extractBaseImages, and substitution helpers, and expand streamBuildOutput docs.
Build context symlink preservation
runtime/docker/build.go
tarDirectory preserves symlinks as tar.TypeSymlink using os.Readlink for linkname and skips reading symlink target contents when archiving.
Integration test for symlink handling
test/integration/build_context_symlink_test.go
New integration test (build-tagged integration) creates a temp workspace with a real file and a relative symlink, runs eng.Up, and asserts the built image produced the expected marker.
Unit tests for Dockerfile parsing and substitution
runtime/docker/build_test.go
New unit tests for extractBaseImages and substituteArgs covering multiple Dockerfile scenarios and variable forms.

Sequence Diagram(s)

sequenceDiagram
  participant PrePull as prePullBaseImages
  participant Tar as tarDirectory
  participant DockerClient as ImageBuild
  participant Stream as streamBuildOutput

  PrePull->>DockerClient: pull resolved base images (best-effort)
  Tar->>DockerClient: stream build context tar (symlinks preserved)
  DockerClient->>Stream: emit build JSON-line records (BuildKit/classic)
  Stream->>Client: forward parsed output lines
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • crunchloop/devcontainer#48: Also removes a # syntax= Dockerfile frontend directive from generated Dockerfiles and adds tests to prevent reintroduction.

"🐰
I skip the Dockerfile cue tonight,
BuildKit hums and builds just right,
Symlinks tucked into the tar so neat,
Tests confirm our build's heartbeat,
Hops of joy for builds complete!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: BuildKit selection and tar symlink encoding fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/buildkit-builder

Comment @coderabbitai help to get the list of available commands and usage tips.

ImageBuild was called without setting ImageBuildOptions.Version, so
dockerd routed every build through its classic builder. Classic
synthesizes one intermediate container per Dockerfile step and
routes every container API through the daemon's authorization
pipeline. Behind an authz plugin (DAP's dap-authz, any equivalent
on consumer hosts) the per-step round-trip storm turns a sub-second
build into a multi-minute one — observed ~140× slowdown on a 7-step
Dockerfile against a 7.7GB base. BuildKit uses a single streaming
session and is unaffected.

Set Version: build.BuilderBuildKit. The moby /build endpoint accepts
a tar request body in BuildKit mode (verified empirically), so no
session / filesync wiring is required for our usage. Per-step
progress events are dropped under BuildKit because the aux trace
records are protobuf-encoded and decoding them would require pulling
in github.com/moby/buildkit; BuildStart / BuildCompleted still fire
and errors still propagate.

Also fix a long-standing bug in tarDirectory: symlinks were emitted
as TypeSymlink with an empty Linkname (the call to
tar.FileInfoHeader passed "" instead of os.Readlink(path)). Some tar
readers reject those as malformed and abort the build mid-stream —
common in compose-primary contexts containing node_modules/.bin/* or
similar bin-symlinks. Now readlink the target and pass it through.

Drop the `# syntax=docker/dockerfile:1.4` directive from the
features Dockerfile: classic ignored it, but BuildKit treats it as a
hard frontend-pull requirement that needs a session for credential
forwarding (which we don't open) and hangs behind broken registry
mirrors. The emitted Dockerfile uses no 1.4-only syntax. Same
directive in useruid.go is addressed in PR fix/uid-reconcile-syntax-directive.

Integration coverage: test/integration/build_context_symlink_test.go
builds a workspace whose context contains an unreferenced symlink
and asserts the build completes — regression guard for the tar
encoding bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bilby91 bilby91 force-pushed the fix/buildkit-builder branch from 59e248d to 7873300 Compare May 12, 2026 22:22
bilby91 and others added 2 commits May 12, 2026 19:28
BuildKit refuses remote image-metadata resolution without an active
session ("no active sessions"), even for fully anonymous public-image
pulls — the session is the mandatory callback channel for registry
credentials. The previous commit set Version: BuilderBuildKit and
worked locally only because images were already in dockerd's local
store; CI (cold cache) hit the session error.

Side-step the session entirely by pre-pulling each FROM image via
the classic ImagePull API (which doesn't require a session). Once
the image is in the local store, BuildKit picks up the cached
manifest and skips remote resolution.

Implementation:
  - extractBaseImages does a naive Dockerfile parse: literal FROMs,
    $VAR / ${VAR} substitution against ARG defaults and BuildSpec
    Args, --platform=... and AS <stage> stripping, stage-name
    detection to avoid pulling intermediate stages.
  - prePullBaseImages calls ImageInspect first (skip if local) then
    PullImage best-effort (silently ignore errors so locally-built
    images like dc-go-base-* / dc-go-final-* don't fail when there's
    no remote to pull from; if the image really can't be obtained,
    the subsequent ImageBuild surfaces a clearer error).

Avoids pulling in github.com/moby/buildkit as a direct dep (~100+
transitive packages). The parser is intentionally minimal and drops
FROMs with unresolved vars rather than guessing — a missed pre-pull
just falls back to BuildKit's native error path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@runtime/docker/build_test.go`:
- Around line 8-14: The TestExtractBaseImages test struct has inconsistent field
alignment causing gofmt to fail; open the struct in TestExtractBaseImages and
reformat the fields (name, dockerfile, buildArgs, want) to have consistent
spacing/indentation, then run gofmt -w runtime/docker/build_test.go (or run your
editor's gofmt) to apply the canonical formatting so CI passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 51eb8f8e-cb43-4c1d-b036-07011f5104e1

📥 Commits

Reviewing files that changed from the base of the PR and between 7873300 and 7f942c8.

📒 Files selected for processing (2)
  • runtime/docker/build.go
  • runtime/docker/build_test.go

Comment thread runtime/docker/build_test.go
@bilby91 bilby91 merged commit 40bdaa2 into main May 12, 2026
5 checks passed
bilby91 added a commit that referenced this pull request May 12, 2026
Documents the BuildKit-switch / pre-pull bundle (#47), the
useruid frontend-directive drop (#48), the BuildCompletedEvent
duration fix (#46), and the dependabot CI bumps (#40-#42) under
their respective Keep a Changelog sections.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bilby91 bilby91 deleted the fix/buildkit-builder branch May 14, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant