diff --git a/frontend/dockerfile/dockerfile_mount_test.go b/frontend/dockerfile/dockerfile_mount_test.go index bab8ba7891be..5a940c2a2f7d 100644 --- a/frontend/dockerfile/dockerfile_mount_test.go +++ b/frontend/dockerfile/dockerfile_mount_test.go @@ -644,14 +644,22 @@ COPY --from=base /combined.txt / test("updated\n") } -// moby/buildkit#5566 +// testCacheMountParallel checks that two build stages sharing nested cache +// mounts (target=/foo/bar and target=/foo/bar/baz) can run side-by-side. It +// re-runs the build 20 times to surface the runc mkdir bug on nested cache +// mounts fixed in runc 1.2.3. Regression test for moby/buildkit#5566. func testCacheMountParallel(t *testing.T, sb integration.Sandbox) { - // flaky on WS2025 - integration.SkipOnPlatform(t, "windows") + // Skipped on Windows (seen on Windows Server 2025). Starting many containers + // in quick succession overloads the host, which fails with "Insufficient + // system resources". That's transient host overload, not the #5566 bug this + // test guards against (a deterministic mkdir failure setting up the nested + // cache mounts). The WCOW daemon should throttle here but doesn't yet, so we + // skip rather than mask the gap with in-test retries. + integration.SkipOnPlatform(t, "windows", "Insufficient system resources error from creating many containers in quick succession") + f := getFrontend(t, sb) - dockerfile := []byte(integration.UnixOrWindows( - ` + dockerfile := []byte(` FROM alpine AS b1 RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 1 @@ -661,21 +669,7 @@ RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz ec FROM scratch COPY --from=b1 /etc/passwd p1 COPY --from=b2 /etc/passwd p2 -`, - ` -FROM nanoserver AS b1 -USER ContainerAdministrator -RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 1 - -FROM nanoserver AS b2 -USER ContainerAdministrator -RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 2 - -FROM nanoserver -COPY --from=b1 /License.txt p1 -COPY --from=b2 /License.txt p2 -`, - )) +`) dir := integration.Tmpdir( t, diff --git a/frontend/dockerfile/dockerfile_outline_test.go b/frontend/dockerfile/dockerfile_outline_test.go index fb87a07b7d15..1a0b8a63fdab 100644 --- a/frontend/dockerfile/dockerfile_outline_test.go +++ b/frontend/dockerfile/dockerfile_outline_test.go @@ -191,15 +191,28 @@ FROM second require.True(t, called) } +// testOutlineSecrets checks that BuildKit can look at a Dockerfile and correctly +// list the secrets and SSH mounts that a given build target needs. +// +// The outline feature only reads the Dockerfile — it never actually runs the build. +// So this test makes sure that: +// - Only secrets/SSH used by the target stage (and its dependencies) are listed. +// Secrets and SSH declared in unrelated stages are ignored. +// - Each secret and SSH entry has the right name, required flag, and line number. +// - Build args inside mount ids (like id=second${BAR}) get expanded correctly. +// +// Because nothing is executed, the Linux and Windows versions of the Dockerfile +// only differ in cosmetic ways (base images and the dummy command). The line +// numbers are kept identical so the same assertions work on both platforms. func testOutlineSecrets(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") workers.CheckFeatureCompat(t, sb, workers.FeatureFrontendOutline) f := getFrontend(t, sb) if _, ok := f.(*clientFrontend); !ok { t.Skip("only test with client frontend") } - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox AS first RUN --mount=type=secret,target=/etc/passwd,required=true --mount=type=ssh true @@ -215,7 +228,25 @@ COPY --from=first /foo / RUN --mount=type=ssh,id=ssh3,required true FROM second -`) +`, + ` +FROM nanoserver AS first +RUN --mount=type=secret,target=/etc/passwd,required=true --mount=type=ssh exit 0 + +FROM nanoserver AS second +RUN --mount=type=secret,id=unused --mount=type=ssh,id=ssh2 exit 0 + +FROM nanoserver AS third +ARG BAR +RUN --mount=type=secret,id=second${BAR} exit 0 + +FROM third AS target +COPY --from=first /License.txt / +RUN --mount=type=ssh,id=ssh3,required exit 0 + +FROM second +`, + )) dir := integration.Tmpdir( t, diff --git a/frontend/dockerfile/dockerfile_provenance_test.go b/frontend/dockerfile/dockerfile_provenance_test.go index 179c32f4a777..000353200595 100644 --- a/frontend/dockerfile/dockerfile_provenance_test.go +++ b/frontend/dockerfile/dockerfile_provenance_test.go @@ -438,8 +438,17 @@ func testGitProvenanceAttestationSHA256(t *testing.T, sb integration.Sandbox) { testGitProvenanceAttestation(t, sb, "sha256") } +// testGitProvenanceAttestation checks that when a build is sourced from a Git +// repository, the resulting provenance attestation records the Git URL, branch, +// and commit SHA correctly, masks any credentials in the URL, and includes the +// base image and Git source in the resolved dependencies. func testGitProvenanceAttestation(t *testing.T, sb integration.Sandbox, format string) { - integration.SkipOnPlatform(t, "windows") + // Skipped on Windows: + // BuildKit's Git source handler fails to update submodules on Windows even when + // the repo has no submodules. The error "failed to update submodules ... git stderr:" + // occurs with empty stderr, indicating the submodule update command cannot execute + // properly on Windows. Same root cause as testDockerfileFromGit. + integration.SkipOnPlatform(t, "windows", "Git source handler submodule update not supported on Windows") workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) ctx := sb.Context() @@ -786,13 +795,12 @@ RUN echo "ok-$TARGETARCH" > /foo } } +// testClientFrontendProvenance verifies that frontend provenance metadata is +// correctly captured when a Dockerfile is built inside a client-side frontend. +// Normally client frontends skip provenance because they run in the client, not +// in BuildKit; this test ensures provenance is still recorded in that scenario. func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) - // Building with client frontend does not capture frontend provenance - // because frontend runs in client, not in BuildKit. - // This test builds Dockerfile inside a client frontend ensuring that - // in that case frontend provenance is captured. ctx := sb.Context() c, err := client.New(ctx, sb.Address()) @@ -800,6 +808,7 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { defer c.Close() registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } @@ -807,6 +816,12 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { target := registry + "/buildkit/clientprovenance:latest" + // On Windows, use windows/{arm64,amd64} platform IDs; the worker only + // executes one platform natively but the IDs flow through provenance. + armPlatformID := integration.UnixOrWindows("linux/arm64", "windows/arm64") + amdPlatformID := integration.UnixOrWindows("linux/amd64", "windows/amd64") + platformOS := integration.UnixOrWindows("linux", "windows") + f := getFrontend(t, sb) _, isClient := f.(*clientFrontend) @@ -814,13 +829,26 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { t.Skip("not a client frontend") } - dockerfile := []byte(` + // Windows uses nanoserver for both stages (no busybox/alpine equivalents); + // USER ContainerAdministrator is required so cmd.exe can write to C:\. + dockerfile := []byte(integration.UnixOrWindows( + ` FROM alpine as x86target RUN echo "alpine" > /foo FROM busybox:latest AS armtarget RUN --network=none echo "bbox" > /foo - `) + `, + ` + FROM nanoserver AS x86target + USER ContainerAdministrator + RUN echo alpine> /foo + + FROM nanoserver AS armtarget + USER ContainerAdministrator + RUN --network=none echo bbox> /foo + `, + )) dir := integration.Tmpdir( t, fstest.CreateFile("Dockerfile", dockerfile, 0600), @@ -868,19 +896,22 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { return nil, err } + // Platform IDs (armPlatformID, amdPlatformID, platformOS) are captured + // from the enclosing testClientFrontendProvenance scope so the result + // assembly and the later assertions stay in sync. res := gateway.NewResult() - res.AddRef("linux/arm64", res1.Ref) - res.AddRef("linux/amd64", res2.Ref) + res.AddRef(armPlatformID, res1.Ref) + res.AddRef(amdPlatformID, res2.Ref) pl, err := json.Marshal(exptypes.Platforms{ Platforms: []exptypes.Platform{ { - ID: "linux/arm64", - Platform: ocispecs.Platform{OS: "linux", Architecture: "arm64"}, + ID: armPlatformID, + Platform: ocispecs.Platform{OS: platformOS, Architecture: "arm64"}, }, { - ID: "linux/amd64", - Platform: ocispecs.Platform{OS: "linux", Architecture: "amd64"}, + ID: amdPlatformID, + Platform: ocispecs.Platform{OS: platformOS, Architecture: "amd64"}, }, }, }) @@ -918,11 +949,20 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { require.NoError(t, err) require.Equal(t, 4, len(imgs.Images)) - img := imgs.Find("linux/arm64") + // nanoserver layer entries are under Files/; cmd echo writes CRLF. + outFile := integration.UnixOrWindows("foo", "Files/foo") + armData := integration.UnixOrWindows([]byte("bbox\n"), []byte("bbox\r\n")) + amdData := integration.UnixOrWindows([]byte("alpine\n"), []byte("alpine\r\n")) + // Both Windows stages are based on nanoserver, so the resolved base URI is + // nanoserver for both platforms; Linux uses the original busybox/alpine. + armBase := integration.UnixOrWindows("docker/busybox", "docker/nanoserver") + amdBase := integration.UnixOrWindows("docker/alpine", "docker/nanoserver") + + img := imgs.Find(armPlatformID) require.NotNil(t, img) - require.Equal(t, []byte("bbox\n"), img.Layers[1]["foo"].Data) + require.Equal(t, armData, img.Layers[1][outFile].Data) - att := imgs.FindAttestation("linux/arm64") + att := imgs.FindAttestation(armPlatformID) require.NotNil(t, att) require.Equal(t, "attestation-manifest", att.Desc.Annotations["vnd.docker.reference.type"]) var attest intoto.Statement @@ -947,14 +987,14 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { require.Equal(t, 2, len(pred.BuildDefinition.ExternalParameters.Request.Locals)) require.Equal(t, 1, len(pred.BuildDefinition.ResolvedDependencies)) - require.Contains(t, pred.BuildDefinition.ResolvedDependencies[0].URI, "docker/busybox") + require.Contains(t, pred.BuildDefinition.ResolvedDependencies[0].URI, armBase) // amd64 - img = imgs.Find("linux/amd64") + img = imgs.Find(amdPlatformID) require.NotNil(t, img) - require.Equal(t, []byte("alpine\n"), img.Layers[1]["foo"].Data) + require.Equal(t, amdData, img.Layers[1][outFile].Data) - att = imgs.FindAttestation("linux/amd64") + att = imgs.FindAttestation(amdPlatformID) require.NotNil(t, att) require.Equal(t, "attestation-manifest", att.Desc.Annotations["vnd.docker.reference.type"]) attest = intoto.Statement{} @@ -976,7 +1016,7 @@ func testClientFrontendProvenance(t *testing.T, sb integration.Sandbox) { require.Equal(t, 2, len(pred.BuildDefinition.ExternalParameters.Request.Locals)) require.Equal(t, 1, len(pred.BuildDefinition.ResolvedDependencies)) - require.Contains(t, pred.BuildDefinition.ResolvedDependencies[0].URI, "docker/alpine") + require.Contains(t, pred.BuildDefinition.ResolvedDependencies[0].URI, amdBase) } func testGatewayBuiltinSyntaxSourceProvenance(t *testing.T, sb integration.Sandbox) { diff --git a/frontend/dockerfile/exclude_patterns_test.go b/frontend/dockerfile/exclude_patterns_test.go index 81170407f5a9..12a407a4eafe 100644 --- a/frontend/dockerfile/exclude_patterns_test.go +++ b/frontend/dockerfile/exclude_patterns_test.go @@ -25,9 +25,19 @@ func init() { allTests = append(allTests, excludedFilesTests...) } +// testExcludedFilesOnCopy verifies that COPY --exclude and ADD --exclude glob +// patterns correctly filter out matching files during a Dockerfile build, while +// preserving non-matching files with their expected content. // See #4439 func testExcludedFilesOnCopy(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") + // Skipped on Windows: + // This test builds 8 stages at once (1 base + 6 builders + 1 final), and + // every builder copies from the same base stage. On Windows, mounting that + // shared parent layer from many stages in parallel hits a known container + // bug (hcsshim::ActivateLayer fails with "file is being used by another + // process"). That's unrelated to --exclude, so we skip the whole test rather + // than weaken it. + integration.SkipOnPlatform(t, "windows", "Windows snapshotter cannot mount the same parent layer from many stages in parallel (hcsshim::ActivateLayer race)") ctx := sb.Context() c, err := client.New(ctx, sb.Address()) diff --git a/solver/jobs_test.go b/solver/jobs_test.go index ed3b06474369..0987f6535a74 100644 --- a/solver/jobs_test.go +++ b/solver/jobs_test.go @@ -1,6 +1,7 @@ package solver import ( + "fmt" "testing" "time" @@ -19,7 +20,9 @@ func init() { } func TestJobsIntegration(t *testing.T) { - mirrors := integration.WithMirroredImages(integration.OfficialImages("busybox:latest")) + mirrors := integration.WithMirroredImages(integration.OfficialImages( + integration.UnixOrWindows("busybox:latest", "nanoserver:latest"), + )) integration.Run(t, integration.TestFuncs( testParallelism, ), @@ -31,31 +34,52 @@ func TestJobsIntegration(t *testing.T) { ) } +// testParallelism verifies that two independent builds can run concurrently by +// using a shared cache mount for signaling. Each build creates a signal file and +// polls for the other's signal. With unlimited parallelism both finish under 10s; +// with single parallelism they run sequentially and exceed 10s. func testParallelism(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") ctx := sb.Context() c, err := client.New(ctx, sb.Address()) require.NoError(t, err) defer c.Close() + imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest") + cacheMountSrc := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) + // sharedDir is the single source of truth for the cache mount target. + // Using it for both llb.AddMount's destination and the command strings + // avoids relying on any implicit "/shared" -> "C:\shared" mapping in WCOW. + sharedDir := integration.UnixOrWindows("/shared", `C:\shared`) cacheMount := llb.AddMount( - "/shared", llb.Scratch(), + sharedDir, cacheMountSrc, llb.AsPersistentCacheDir("shared", llb.CacheMountShared)) - run1 := llb.Image("busybox:latest").Run( - llb.Args([]string{ - "/bin/sh", "-c", - "touch /shared/signal1 && i=0; while [ ! -f /shared/signal2 ] && [ $i -lt 10 ]; do i=$((i+1)); sleep 1; done", - }), + + // On Linux, use sh to touch a signal file and poll for the peer's signal. + // On Windows (nanoserver), use cmd with copy+ping to achieve the same + // signaling: "copy nul" creates an empty file, and "ping -n 2 127.0.0.1" + // provides a ~1-second delay per iteration. + cmd1 := integration.UnixOrWindows( + []string{"/bin/sh", "-c", + fmt.Sprintf("touch %[1]s/signal1 && i=0; while [ ! -f %[1]s/signal2 ] && [ $i -lt 10 ]; do i=$((i+1)); sleep 1; done", sharedDir)}, + []string{"cmd", "/S", "/C", + fmt.Sprintf(`copy nul %[1]s\signal1 >nul & for /L %%i in (1,1,10) do @if not exist %[1]s\signal2 ping -n 2 127.0.0.1 >nul`, sharedDir)}, + ) + run1 := llb.Image(imgName).Run( + llb.Args(cmd1), cacheMount, ).Root() d1, err := run1.Marshal(ctx) require.NoError(t, err) - run2 := llb.Image("busybox:latest").Run( - llb.Args([]string{ - "/bin/sh", "-c", - "touch /shared/signal2 && i=0; while [ ! -f /shared/signal1 ] && [ $i -lt 10 ]; do i=$((i+1)); sleep 1; done", - }), + + cmd2 := integration.UnixOrWindows( + []string{"/bin/sh", "-c", + fmt.Sprintf("touch %[1]s/signal2 && i=0; while [ ! -f %[1]s/signal1 ] && [ $i -lt 10 ]; do i=$((i+1)); sleep 1; done", sharedDir)}, + []string{"cmd", "/S", "/C", + fmt.Sprintf(`copy nul %[1]s\signal2 >nul & for /L %%i in (1,1,10) do @if not exist %[1]s\signal1 ping -n 2 127.0.0.1 >nul`, sharedDir)}, + ) + run2 := llb.Image(imgName).Run( + llb.Args(cmd2), cacheMount, ).Root() d2, err := run2.Marshal(ctx)