Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 14 additions & 20 deletions frontend/dockerfile/dockerfile_mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test already contains windows logic. Should it be removed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The test initially included the Windows logic, even when it was first skipped. I have now removed it to avoid any confusion.


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

Expand All @@ -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,
Expand Down
37 changes: 34 additions & 3 deletions frontend/dockerfile/dockerfile_outline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
84 changes: 62 additions & 22 deletions frontend/dockerfile/dockerfile_provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -786,41 +795,60 @@ 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())
require.NoError(t, err)
defer c.Close()

registry, err := sb.NewRegistry()

if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)

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)
if !isClient {
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),
Expand Down Expand Up @@ -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"},
},
},
})
Expand Down Expand Up @@ -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
Expand All @@ -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{}
Expand All @@ -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) {
Expand Down
12 changes: 11 additions & 1 deletion frontend/dockerfile/exclude_patterns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
ngebremariam-msft marked this conversation as resolved.
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())
Expand Down
50 changes: 37 additions & 13 deletions solver/jobs_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package solver

import (
"fmt"
"testing"
"time"

Expand All @@ -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,
),
Expand All @@ -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")
Comment thread
ngebremariam-msft marked this conversation as resolved.
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))
Comment thread
ngebremariam-msft marked this conversation as resolved.
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)},
)
Comment thread
ngebremariam-msft marked this conversation as resolved.
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)
Expand Down
Loading