buildkit/client/mergediff_test.go

1408 lines
42 KiB
Go

package client
import (
"context"
"fmt"
"os"
"strings"
"testing"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/continuity/fs/fstest"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/testutil/integration"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
func diffOpTestCases() (tests []integration.Test) {
alpine := func() llb.State { return llb.Image("alpine:latest", llb.ResolveDigest(true)) }
// busybox doesn't have /proc or /sys in its base image, add them
// so they don't show up in every diff of an exec on it
busybox := func() llb.State {
return llb.Image("busybox:latest", llb.ResolveDigest(true)).
File(llb.Mkdir("/proc", 0755)).
File(llb.Mkdir("/sys", 0755))
}
// Diffs of identical states are empty
tests = append(tests,
verifyContents{
name: "TestDiffScratch",
state: llb.Diff(llb.Scratch(), llb.Scratch()),
contents: empty,
},
verifyContents{
name: "TestDiffSelf",
state: llb.Diff(alpine(), alpine()),
contents: empty,
},
verifyContents{
name: "TestDiffSelfDeletes",
state: llb.Merge([]llb.State{alpine(), llb.Diff(alpine(), alpine())}),
contents: contentsOf(alpine()),
},
)
// Diff of state with scratch has same contents as the state
tests = append(tests,
verifyContents{
name: "TestDiffLowerScratch",
state: llb.Diff(llb.Scratch(), alpine()),
contents: contentsOf(alpine()),
},
verifyContents{
name: "TestDiffLowerScratchDeletes",
state: llb.Merge([]llb.State{
llb.Scratch().
File(llb.Mkfile("/foo", 0755, []byte("A"))),
llb.Diff(llb.Scratch(), llb.Scratch().
File(llb.Mkfile("/foo", 0644, []byte("B"))).
File(llb.Rm("/foo")).
File(llb.Mkfile("/bar", 0644, nil))),
}),
contents: apply(
fstest.CreateFile("/bar", nil, 0644),
),
},
)
// Diff from a state to scratch is just a deletion of the contents of that state
tests = append(tests,
verifyContents{
name: "TestDiffUpperScratch",
state: llb.Merge([]llb.State{
alpine(),
llb.Scratch().File(llb.Mkfile("/foo", 0644, []byte("foo"))),
llb.Diff(alpine(), llb.Scratch()),
}),
contents: apply(
fstest.CreateFile("/foo", []byte("foo"), 0644),
),
},
)
// Basic diff slices
tests = append(tests, func() (tests []integration.Test) {
base := func() llb.State {
return alpine().
File(llb.Mkfile("/shuffleFile1", 0644, []byte("shuffleFile1"))).
File(llb.Mkdir("/shuffleDir1", 0755)).
File(llb.Mkdir("/shuffleDir1/shuffleSubdir1", 0755)).
File(llb.Mkfile("/shuffleDir1/shuffleSubfile1", 0644, nil)).
File(llb.Mkfile("/shuffleDir1/shuffleSubdir1/shuffleSubfile2", 0644, nil)).
File(llb.Mkdir("/unmodifiedDir", 0755)).
File(llb.Mkdir("/unmodifiedDir/chmodSubdir1", 0755)).
File(llb.Mkdir("/unmodifiedDir/deleteSubdir1", 0755)).
File(llb.Mkdir("/unmodifiedDir/opaqueDir1", 0755)).
File(llb.Mkdir("/unmodifiedDir/opaqueDir1/opaqueSubdir1", 0755)).
File(llb.Mkdir("/unmodifiedDir/overrideSubdir1", 0755)).
File(llb.Mkdir("/unmodifiedDir/shuffleDir2", 0755)).
File(llb.Mkdir("/unmodifiedDir/shuffleDir2/shuffleSubdir2", 0755)).
File(llb.Mkfile("/unmodifiedDir/chmodFile1", 0644, []byte("chmodFile1"))).
File(llb.Mkfile("/unmodifiedDir/modifyContentFile1", 0644, []byte("modifyContentFile1"))).
File(llb.Mkfile("/unmodifiedDir/deleteFile1", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/opaqueDir1/opaqueFile1", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/opaqueDir1/opaqueSubdir1/opaqueFile2", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/overrideFile1", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/overrideFile2", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/shuffleFile2", 0644, []byte("shuffleFile2"))).
File(llb.Mkfile("/unmodifiedDir/shuffleDir2/shuffleSubfile3", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/shuffleDir2/shuffleSubdir2/shuffleSubfile4", 0644, nil)).
File(llb.Mkdir("/modifyDir", 0755)).
File(llb.Mkdir("/modifyDir/chmodSubdir2", 0755)).
File(llb.Mkdir("/modifyDir/deleteSubdir2", 0755)).
File(llb.Mkdir("/modifyDir/opaqueDir2", 0755)).
File(llb.Mkdir("/modifyDir/opaqueDir2/opaqueSubdir2", 0755)).
File(llb.Mkdir("/modifyDir/overrideSubdir2", 0755)).
File(llb.Mkdir("/modifyDir/shuffleDir3", 0755)).
File(llb.Mkdir("/modifyDir/shuffleDir3/shuffleSubdir3", 0755)).
File(llb.Mkfile("/modifyDir/chmodFile2", 0644, []byte("chmodFile2"))).
File(llb.Mkfile("/modifyDir/modifyContentFile2", 0644, []byte("modifyContentFile2"))).
File(llb.Mkfile("/modifyDir/deleteFile2", 0644, nil)).
File(llb.Mkfile("/modifyDir/opaqueDir2/opaqueFile3", 0644, nil)).
File(llb.Mkfile("/modifyDir/opaqueDir2/opaqueSubdir2/opaqueFile4", 0644, nil)).
File(llb.Mkfile("/modifyDir/overrideFile3", 0644, nil)).
File(llb.Mkfile("/modifyDir/overrideFile4", 0644, nil)).
File(llb.Mkfile("/modifyDir/shuffleFile3", 0644, []byte("shuffleFile3"))).
File(llb.Mkfile("/modifyDir/shuffleDir3/shuffleSubfile4", 0644, nil)).
File(llb.Mkfile("/modifyDir/shuffleDir3/shuffleSubdir3/shuffleSubfile6", 0644, nil))
}
joinCmds := func(cmds ...[]string) []string {
var all []string
for _, cmd := range cmds {
all = append(all, cmd...)
}
return all
}
var allCmds [][]string
var allContents []contents
baseDiffCmds := []string{
"chmod 0700 /modifyDir",
}
baseDiffContents := apply(
fstest.CreateDir("/unmodifiedDir", 0755),
fstest.CreateDir("/modifyDir", 0700),
)
allCmds = append(allCmds, baseDiffCmds)
allContents = append(allContents, baseDiffContents)
newFileCmds := []string{
// Create a new file under a new dir
"mkdir /newdir1",
"touch /newdir1/newfile1",
// Create a new file under an unmodified existing dir
"touch /unmodifiedDir/newfile2",
// Create a new file under a modified existing dir
"touch /modifyDir/newfile3",
}
newFileContents := apply(
fstest.CreateDir("/newdir1", 0755),
fstest.CreateFile("/newdir1/newfile1", nil, 0644),
fstest.CreateFile("/unmodifiedDir/newfile2", nil, 0644),
fstest.CreateFile("/modifyDir/newfile3", nil, 0644),
)
allCmds = append(allCmds, newFileCmds)
allContents = append(allContents, newFileContents)
tests = append(tests, verifyContents{
name: "TestDiffNewFiles",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
newFileCmds,
)...)),
contents: mergeContents(
baseDiffContents,
newFileContents,
),
})
modifyFileCmds := []string{
// Modify an existing file under an unmodified existing dir
"chmod 0444 /unmodifiedDir/chmodFile1",
"echo -n modifyContentFile0 > /unmodifiedDir/modifyContentFile1",
// Modify an existing file under a modified existing dir
"chmod 0440 /modifyDir/chmodFile2",
"echo -n modifyContentFile0 > /modifyDir/modifyContentFile2",
}
modifyFileContents := apply(
fstest.CreateFile("/unmodifiedDir/chmodFile1", []byte("chmodFile1"), 0444),
fstest.CreateFile("/unmodifiedDir/modifyContentFile1", []byte("modifyContentFile0"), 0644),
fstest.CreateFile("/modifyDir/chmodFile2", []byte("chmodFile2"), 0440),
fstest.CreateFile("/modifyDir/modifyContentFile2", []byte("modifyContentFile0"), 0644),
)
allCmds = append(allCmds, modifyFileCmds)
allContents = append(allContents, modifyFileContents)
tests = append(tests, verifyContents{
name: "TestDiffModifiedFiles",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
modifyFileCmds,
)...)),
contents: mergeContents(
baseDiffContents,
modifyFileContents,
),
})
createNewDirCmds := []string{
// Create a new dir under a new dir
"mkdir -p /newdir2/newsubdir1",
// Create a new dir under an unmodified existing dir
"mkdir /unmodifiedDir/newsubdir2",
// Create a new dir under a modified existing dir
"mkdir /modifyDir/newsubdir3",
}
createNewDirContents := apply(
fstest.CreateDir("/newdir2", 0755),
fstest.CreateDir("/newdir2/newsubdir1", 0755),
fstest.CreateDir("/unmodifiedDir/newsubdir2", 0755),
fstest.CreateDir("/modifyDir/newsubdir3", 0755),
)
allCmds = append(allCmds, createNewDirCmds)
allContents = append(allContents, createNewDirContents)
tests = append(tests, verifyContents{
name: "TestDiffNewDirs",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
createNewDirCmds,
)...)),
contents: mergeContents(
baseDiffContents,
createNewDirContents,
),
})
modifyDirCmds := []string{
// Modify a dir under an unmodified existing dir
"chmod 0700 /unmodifiedDir/chmodSubdir1",
// Modify a dir under an modified existing dir
"chmod 0770 /modifyDir/chmodSubdir2",
}
modifyDirContents := apply(
fstest.CreateDir("/unmodifiedDir/chmodSubdir1", 0700),
fstest.CreateDir("/modifyDir/chmodSubdir2", 0770),
)
allCmds = append(allCmds, modifyDirCmds)
allContents = append(allContents, modifyDirContents)
tests = append(tests, verifyContents{
name: "TestDiffModifiedDirs",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
modifyDirCmds,
)...)),
contents: mergeContents(
baseDiffContents,
modifyDirContents,
),
})
overrideDirCmds := []string{
"rm -rf /unmodifiedDir/overrideSubdir1",
"echo -n overrideSubdir1 > /unmodifiedDir/overrideSubdir1",
"rm -rf /modifyDir/overrideSubdir2",
"echo -n overrideSubdir2 > /modifyDir/overrideSubdir2",
}
overrideDirContents := apply(
fstest.CreateFile("/unmodifiedDir/overrideSubdir1", []byte("overrideSubdir1"), 0644),
fstest.CreateFile("/modifyDir/overrideSubdir2", []byte("overrideSubdir2"), 0644),
)
allCmds = append(allCmds, overrideDirCmds)
allContents = append(allContents, overrideDirContents)
tests = append(tests, verifyContents{
name: "TestDiffOverrideDirs",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
overrideDirCmds,
)...)),
contents: mergeContents(
baseDiffContents,
overrideDirContents,
),
})
overrideFileCmds := []string{
"rm /unmodifiedDir/overrideFile1",
"mkdir -m 0700 /unmodifiedDir/overrideFile1",
"rm /unmodifiedDir/overrideFile2",
"mkdir -m 0750 /unmodifiedDir/overrideFile2",
"mkdir -m 0770 /unmodifiedDir/overrideFile2/newsubdir4",
"touch /unmodifiedDir/overrideFile2/newfile4",
"touch /unmodifiedDir/overrideFile2/newsubdir4/newfile5",
"rm /modifyDir/overrideFile3",
"mkdir -m 0700 /modifyDir/overrideFile3",
"rm /modifyDir/overrideFile4",
"mkdir -m 0750 /modifyDir/overrideFile4",
"mkdir -m 0770 /modifyDir/overrideFile4/newsubdir5",
"touch /modifyDir/overrideFile4/newfile6",
"touch /modifyDir/overrideFile4/newsubdir5/newfile7",
}
overrideFileContents := apply(
fstest.CreateDir("/unmodifiedDir/overrideFile1", 0700),
fstest.CreateDir("/unmodifiedDir/overrideFile2", 0750),
fstest.CreateDir("/unmodifiedDir/overrideFile2/newsubdir4", 0770),
fstest.CreateFile("/unmodifiedDir/overrideFile2/newfile4", nil, 0644),
fstest.CreateFile("/unmodifiedDir/overrideFile2/newsubdir4/newfile5", nil, 0644),
fstest.CreateDir("/modifyDir/overrideFile3", 0700),
fstest.CreateDir("/modifyDir/overrideFile4", 0750),
fstest.CreateDir("/modifyDir/overrideFile4/newsubdir5", 0770),
fstest.CreateFile("/modifyDir/overrideFile4/newfile6", nil, 0644),
fstest.CreateFile("/modifyDir/overrideFile4/newsubdir5/newfile7", nil, 0644),
)
allCmds = append(allCmds, overrideFileCmds)
allContents = append(allContents, overrideFileContents)
tests = append(tests, verifyContents{
name: "TestDiffOverrideFiles",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
overrideFileCmds,
)...)),
contents: mergeContents(
baseDiffContents,
overrideFileContents,
),
})
deleteFileCmds := []string{
"rm /unmodifiedDir/deleteFile1",
"rm /modifyDir/deleteFile2",
}
allCmds = append(allCmds, deleteFileCmds)
tests = append(tests, verifyContents{
name: "TestDiffDeleteFiles",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
deleteFileCmds,
)...)),
contents: mergeContents(
baseDiffContents,
),
})
tests = append(tests, verifyContents{
name: "TestDiffDeleteFilesMerge",
state: llb.Merge([]llb.State{
base(),
llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
deleteFileCmds,
)...)),
}),
contents: mergeContents(
contentsOf(base()),
baseDiffContents,
apply(
fstest.Remove("/unmodifiedDir/deleteFile1"),
fstest.Remove("/modifyDir/deleteFile2"),
),
),
})
deleteDirCmds := []string{
"rm -rf /unmodifiedDir/deleteSubdir1",
"rm -rf /modifyDir/deleteSubdir2",
}
allCmds = append(allCmds, deleteDirCmds)
tests = append(tests, verifyContents{
name: "TestDiffDeleteDirs",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
deleteDirCmds,
)...)),
contents: mergeContents(
baseDiffContents,
),
})
tests = append(tests, verifyContents{
name: "TestDiffDeleteDirsMerge",
state: llb.Merge([]llb.State{
base(),
llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
deleteDirCmds,
)...)),
}),
contents: mergeContents(
contentsOf(base()),
baseDiffContents,
apply(
fstest.RemoveAll("/unmodifiedDir/deleteSubdir1"),
fstest.RemoveAll("/modifyDir/deleteSubdir2"),
),
),
})
basePlusExtra := func() llb.State {
return base().
File(llb.Mkdir("/extradir", 0755)).
File(llb.Mkfile("/extradir/extrafile", 0755, nil))
}
tests = append(tests, verifyContents{
name: "TestDiffUnmatchedDelete",
state: llb.Merge([]llb.State{
base(),
llb.Diff(basePlusExtra(), basePlusExtra().File(llb.Rm("/extradir/extrafile"))),
}),
contents: mergeContents(
contentsOf(base()),
apply(
// Surprisingly, it's expected that /extradir shows up in the diff.
// This is the behavior of the exporter, so we have to enforce
// consistency with it.
// https://github.com/containerd/containerd/pull/2095
fstest.CreateDir("/extradir", 0755),
),
),
})
// Opaque dirs should be converted to the "explicit whiteout" format, as described in the OCI image spec:
// https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout
opaqueDirCmds := []string{
"rm -rf /unmodifiedDir/opaqueDir1",
"mkdir -p /unmodifiedDir/opaqueDir1/newOpaqueSubdir1",
"touch /unmodifiedDir/opaqueDir1/newOpaqueFile1",
"touch /unmodifiedDir/opaqueDir1/newOpaqueSubdir1/newOpaqueFile2",
"rm -rf /modifyDir/opaqueDir2",
"mkdir -p /modifyDir/opaqueDir2/newOpaqueSubdir2",
"touch /modifyDir/opaqueDir2/newOpaqueFile3",
"touch /modifyDir/opaqueDir2/newOpaqueSubdir2/newOpaqueFile4",
}
opaqueDirContents := apply(
fstest.CreateDir("/unmodifiedDir/opaqueDir1", 0755),
fstest.CreateDir("/unmodifiedDir/opaqueDir1/newOpaqueSubdir1", 0755),
fstest.CreateFile("/unmodifiedDir/opaqueDir1/newOpaqueFile1", nil, 0644),
fstest.CreateFile("/unmodifiedDir/opaqueDir1/newOpaqueSubdir1/newOpaqueFile2", nil, 0644),
fstest.CreateDir("/modifyDir/opaqueDir2", 0755),
fstest.CreateDir("/modifyDir/opaqueDir2/newOpaqueSubdir2", 0755),
fstest.CreateFile("/modifyDir/opaqueDir2/newOpaqueFile3", nil, 0644),
fstest.CreateFile("/modifyDir/opaqueDir2/newOpaqueSubdir2/newOpaqueFile4", nil, 0644),
)
allCmds = append(allCmds, opaqueDirCmds)
allContents = append(allContents, opaqueDirContents)
tests = append(tests, verifyContents{
name: "TestDiffOpaqueDirs",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
opaqueDirCmds,
)...)),
contents: mergeContents(
baseDiffContents,
opaqueDirContents,
),
})
tests = append(tests, verifyContents{
name: "TestDiffOpaqueDirsMerge",
state: llb.Merge([]llb.State{
base().
File(llb.Mkfile("/unmodifiedDir/opaqueDir1/rebaseFile1", 0644, nil)).
File(llb.Mkfile("/unmodifiedDir/opaqueDir1/opaqueSubdir1/rebaseFile2", 0644, nil)).
File(llb.Mkfile("/modifyDir/opaqueDir2/rebaseFile3", 0644, nil)).
File(llb.Mkfile("/modifyDir/opaqueDir2/opaqueSubdir2/rebaseFile4", 0644, nil)),
llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
opaqueDirCmds,
)...)),
}),
contents: mergeContents(
contentsOf(base()),
baseDiffContents,
apply(
fstest.RemoveAll("/unmodifiedDir/opaqueDir1"),
fstest.RemoveAll("/modifyDir/opaqueDir2"),
),
opaqueDirContents,
apply(
fstest.CreateFile("/unmodifiedDir/opaqueDir1/rebaseFile1", nil, 0644),
fstest.CreateFile("/modifyDir/opaqueDir2/rebaseFile3", nil, 0644),
),
),
})
// Test that shuffling files back and forth without making any actual changes results
// in no diff. This requires careful handling in the overlay differ, which can be easily
// tricked into thinking this is a diff because the shuffled files will show up in the
// upper dir.
// Note that this is only tested on files and not directories because overlay mounts
// that don't have redirect_dir=on will fail any attempt to make the rename syscall
// on a directory with EXDEV. In theory, busybox's mv command is supposed to handle
// this transparently, but it doesn't support setting nanosecond timestamps when falling
// back to a copy, so the mv'd files have truncated timestamps that cause the differ
// to think there is a diff.
shuffleFileCmds := []string{
// Shuffle a file under root
"mv /shuffleFile1 /shuffleFile1tmp",
"mv /shuffleFile1tmp /shuffleFile1",
// Shuffle a file under an unmodified existing dir
"mv /unmodifiedDir/shuffleFile2 /unmodifiedDir/shuffleFile2tmp",
"mv /unmodifiedDir/shuffleFile2tmp /unmodifiedDir/shuffleFile2",
// Shuffle a file under a modified existing dir
"mv /modifyDir/shuffleFile3 /modifyDir/shuffleFile3tmp",
"mv /modifyDir/shuffleFile3tmp /modifyDir/shuffleFile3",
}
allCmds = append(allCmds, shuffleFileCmds)
tests = append(tests, verifyContents{
name: "TestDiffShuffleFiles",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
shuffleFileCmds,
)...)),
contents: mergeContents(
baseDiffContents,
apply(fstest.RemoveAll("/unmodifiedDir")),
),
})
tests = append(tests, verifyContents{
name: "TestDiffShuffleFilesMerge",
state: llb.Merge([]llb.State{
base(),
llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
shuffleFileCmds,
)...)),
}),
contents: mergeContents(
contentsOf(base()),
baseDiffContents,
),
})
// verify that fifos and devices are handled
// TODO: test sockets. Not straightforward because there's no builtin commands in alpine or busybox
// that create sockets except for daemons like udhcpd... Maybe you could mount a socket and then copy
// from within the container?
mknodFifosCmds := []string{
"mknod /unmodifiedDir/fifo1 p",
"mknod /modifyDir/fifo2 p",
}
mknodFifosContents := apply(
mkfifo("/unmodifiedDir/fifo1", 0644),
mkfifo("/modifyDir/fifo2", 0644),
)
allCmds = append(allCmds, mknodFifosCmds)
allContents = append(allContents, mknodFifosContents)
tests = append(tests, verifyContents{
name: "TestDiffFifos",
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
mknodFifosCmds,
)...)),
contents: mergeContents(
baseDiffContents,
mknodFifosContents,
),
})
mknodChardevCmds := []string{
"mknod /unmodifiedDir/null1 c 1 3",
"mknod /modifyDir/null2 c 1 3",
}
mknodChardevContents := apply(
mkchardev("/unmodifiedDir/null1", 0644, 1, 3),
mkchardev("/modifyDir/null2", 0644, 1, 3),
)
tests = append(tests, verifyContents{
name: "TestDiffChardevs",
skipOnRootless: true, // you need root user namespace privilege to create device nodes
state: llb.Diff(base(), runShell(base(), joinCmds(
baseDiffCmds,
mknodChardevCmds,
)...)),
contents: mergeContents(
baseDiffContents,
mknodChardevContents,
),
})
// combine all the previous tests into one to make sure the diffs can be calculated
// together equivalently
var flattenedCmds []string
for _, cmds := range allCmds {
flattenedCmds = append(flattenedCmds, cmds...)
}
tests = append(tests, verifyContents{
name: "TestDiffCombinedSingleLayer",
state: llb.Diff(base(), runShell(base(), flattenedCmds...)),
contents: mergeContents(allContents...),
})
tests = append(tests, verifyContents{
name: "TestDiffCombinedMultiLayer",
state: llb.Diff(base(), chainRunShells(base(), allCmds...)),
contents: mergeContents(allContents...),
})
return tests
}()...)
tests = append(tests, func() []integration.Test {
base := func() llb.State {
return runShell(alpine(),
"mkdir -p /parentdir/dir/subdir",
"touch /parentdir/dir/A /parentdir/dir/B /parentdir/dir/subdir/C",
)
}
return []integration.Test{
verifyContents{
name: "TestDiffOpaqueDirs",
state: llb.Merge([]llb.State{
runShell(busybox(),
"mkdir -p /parentdir/dir/subdir",
"touch /parentdir/dir/A /parentdir/dir/B /parentdir/dir/D",
),
llb.Diff(base(), runShell(base(),
"rm -rf /parentdir/dir",
"mkdir /parentdir/dir",
"touch /parentdir/dir/E",
)),
}),
contents: contentsOf(runShell(busybox(),
"mkdir -p /parentdir/dir",
"touch /parentdir/dir/D",
"touch /parentdir/dir/E",
)),
},
}
}()...)
// Symlink handling tests
tests = append(tests, func() []integration.Test {
linkFooToBar := func() llb.State {
return llb.Diff(alpine(), runShell(alpine(), "mkdir -p /bar", "ln -s /bar /foo"))
}
alpinePlusFoo := func() llb.State {
return runShell(alpine(), "mkdir /foo")
}
deleteFoo := func() llb.State {
return llb.Diff(alpinePlusFoo(), runShell(alpinePlusFoo(), "rm -rf /foo"))
}
createFooFile := func() llb.State {
return llb.Diff(alpinePlusFoo(), runShell(alpinePlusFoo(), "touch /foo/file"))
}
return []integration.Test{
verifyContents{
name: "TestDiffDirOverridesSymlink",
state: llb.Merge([]llb.State{
busybox(),
linkFooToBar(),
createFooFile(),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /bar",
"mkdir /foo",
"touch /foo/file",
)),
},
verifyContents{
name: "TestDiffSymlinkOverridesDir",
state: llb.Merge([]llb.State{
busybox(),
createFooFile(),
linkFooToBar(),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /bar",
"ln -s /bar /foo",
)),
},
verifyContents{
name: "TestDiffSymlinkOverridesSymlink",
state: llb.Merge([]llb.State{
busybox(),
llb.Diff(alpine(), runShell(alpine(),
"mkdir /1 /2",
"ln -s /1 /a",
"ln -s /2 /a/b",
)),
llb.Diff(alpine(), runShell(alpine(),
"mkdir /3",
"ln -s /3 /a",
)),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /1 /2 /3",
"ln -s /3 /a",
"ln -s /2 /1/b",
)),
},
verifyContents{
name: "TestDiffDeleteDoesNotFollowSymlink",
state: llb.Merge([]llb.State{
busybox(),
linkFooToBar(),
deleteFoo(),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /bar",
)),
},
verifyContents{
name: "TestDiffDeleteDoesNotFollowParentSymlink",
state: llb.Merge([]llb.State{
busybox(),
linkFooToBar().File(llb.Mkfile("/bar/file", 0644, nil)),
llb.Diff(createFooFile(), createFooFile().File(llb.Rm("/foo/file"))),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /bar",
"touch /bar/file",
"mkdir /foo",
)),
},
verifyContents{
name: "TestDiffCircularSymlinks",
state: llb.Merge([]llb.State{
busybox(),
llb.Diff(alpine(), runShell(alpine(), "ln -s /2 /1", "ln -s /1 /2")),
llb.Scratch().
File(llb.Mkfile("/1", 0644, []byte("foo"))),
}),
contents: contentsOf(runShell(busybox(),
"echo -n foo > /1",
"ln -s /1 /2",
)),
},
verifyContents{
name: "TestDiffCircularDirSymlink",
state: llb.Merge([]llb.State{
busybox(),
llb.Diff(alpine(), runShell(alpine(), "mkdir /foo", "ln -s ../foo /foo/link")),
llb.Diff(alpine(), runShell(alpine(), "mkdir -p /foo/link", "touch /foo/link/file")),
}),
contents: contentsOf(runShell(busybox(),
"mkdir -p /foo/link",
"touch /foo/link/file",
)),
},
verifyContents{
name: "TestDiffCircularParentDirSymlinks",
state: llb.Merge([]llb.State{
busybox(),
llb.Diff(alpine(), runShell(alpine(), "ln -s /2 /1", "ln -s /1 /2")),
llb.Diff(alpine(), runShell(alpine(), "mkdir /1", "echo -n foo > /1/file")),
}),
contents: contentsOf(runShell(busybox(),
"mkdir /1",
"echo -n foo > /1/file",
"ln -s /1 /2",
)),
},
}
}()...)
// Test hardlinks
// NOTE: There are long-standing inconsistencies in hardlink handling between overlay snapshotters
// and the native snapshotter that have to be avoided here. Namely, when a hardlink is copied-up
// by overlay, the link will be broken unless the inodes index feature is enabled (which we can't
// enabled because it disallows you from moving overlay layers ontop of different ones from which
// they were originally created). See the overlay docs for more details:
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html?highlight=overlayfs#non-standard-behavior
tests = append(tests, func() []integration.Test {
linkedFiles := func() llb.State {
return llb.Diff(alpine(), runShell(alpine(),
"mkdir /dir",
"touch /dir/1",
"ln /dir/1 /dir/2",
"chmod 0600 /dir/2",
))
}
mntB := func() llb.State {
chmodExecState := runShellExecState(busybox(), "chmod 0777 /A/dir/1")
chmodExecState.AddMount("/A", linkedFiles())
return chmodExecState.AddMount("/B", linkedFiles())
}
return []integration.Test{
verifyContents{
name: "TestDiffHardlinks",
state: linkedFiles(),
contents: apply(
fstest.CreateDir("/dir", 0755),
fstest.CreateFile("/dir/1", nil, 0600),
fstest.Link("/dir/1", "/dir/2"),
),
},
verifyContents{
name: "TestDiffHardlinkChangesDoNotPropagateBetweenSnapshots",
state: mntB(),
contents: apply(
fstest.CreateDir("/dir", 0755),
fstest.CreateFile("/dir/1", nil, 0600),
fstest.Link("/dir/1", "/dir/2"),
),
},
}
}()...)
// Diffs of lazy blobs should work.
tests = append(tests,
verifyContents{
name: "TestDiffLazyBlobMerge",
// Merge(A, Diff(A,B)) == B
state: llb.Merge([]llb.State{busybox(), llb.Diff(busybox(), alpine())}),
contents: contentsOf(alpine()),
},
)
// Diffs of exec mounts should only include the changes made under the mount
tests = append(tests, func() []integration.Test {
splitDiffExecState := func() llb.ExecState {
execState := runShellExecState(alpine(), "touch /root/A", "touch /mnt/B")
execState.AddMount("/mnt", busybox())
return execState
}
return []integration.Test{
verifyContents{
name: "TestDiffExecRoot",
state: llb.Diff(alpine(), splitDiffExecState().Root()),
contents: apply(
fstest.CreateDir("/root", 0700),
fstest.CreateFile("/root/A", nil, 0644),
),
},
verifyContents{
name: "TestDiffExecMount",
state: llb.Diff(busybox(), splitDiffExecState().GetMount("/mnt")),
contents: apply(
fstest.CreateFile("/B", nil, 0644),
),
},
}
}()...)
// Diff+Merge combinations
tests = append(tests, func() []integration.Test {
a := func() llb.State {
return llb.Scratch().File(llb.Mkfile("A", 0644, []byte("A")))
}
b := func() llb.State {
return llb.Scratch().File(llb.Mkfile("B", 0644, []byte("B")))
}
c := func() llb.State {
return llb.Scratch().File(llb.Mkfile("C", 0644, []byte("C")))
}
deleteC := func() llb.State {
return c().File(llb.Rm("C"))
}
ab := func() llb.State {
return llb.Merge([]llb.State{a(), b()})
}
abc := func() llb.State {
return llb.Merge([]llb.State{a(), b(), c()})
}
abDeleteC := func() llb.State {
return llb.Merge([]llb.State{a(), b(), deleteC()})
}
// nested is abcdae-a
nested := func() llb.State {
return llb.Merge([]llb.State{
abc().File(llb.Mkfile("D", 0644, []byte("D"))),
llb.Merge([]llb.State{
a(),
llb.Scratch().File(llb.Mkfile("E", 0644, []byte("E"))),
}).File(llb.Rm("A")),
})
}
return []integration.Test{
verifyContents{
name: "TestDiffOnlyMerge",
state: llb.Merge([]llb.State{
llb.Diff(a(), b()),
llb.Diff(b(), a()),
}),
contents: contentsOf(a()),
},
verifyContents{
name: "TestDiffOfMerges",
state: llb.Diff(ab(), abc()),
contents: apply(
fstest.CreateFile("/C", []byte("C"), 0644),
),
},
verifyContents{
name: "TestDiffOfMergesWithDeletes",
state: llb.Merge([]llb.State{abc(), llb.Diff(abc(), abDeleteC())}),
contents: apply(
fstest.CreateFile("/A", []byte("A"), 0644),
fstest.CreateFile("/B", []byte("B"), 0644),
),
},
verifyContents{
name: "TestDiffSingleLayerOnMerge",
state: llb.Diff(abDeleteC(), abDeleteC().File(llb.Mkfile("D", 0644, []byte("D")))),
contents: apply(
fstest.CreateFile("/D", []byte("D"), 0644),
),
},
verifyContents{
name: "TestDiffSingleDeleteLayerOnMerge",
state: llb.Merge([]llb.State{
abDeleteC(),
llb.Diff(abc(), abc().File(llb.Rm("A"))),
}),
contents: apply(
fstest.CreateFile("/B", []byte("B"), 0644),
),
},
verifyContents{
name: "TestDiffMultipleLayerOnMerge",
state: llb.Merge([]llb.State{
abDeleteC(),
llb.Diff(abc(), abc().
File(llb.Mkfile("D", 0644, []byte("D"))).
File(llb.Rm("A")),
),
}),
contents: apply(
fstest.CreateFile("/B", []byte("B"), 0644),
fstest.CreateFile("/D", []byte("D"), 0644),
),
},
verifyContents{
name: "TestDiffNestedLayeredMerges",
state: llb.Diff(abc(), nested().File(llb.Mkfile("F", 0644, []byte("F")))),
contents: apply(
fstest.CreateFile("/D", []byte("D"), 0644),
fstest.CreateFile("/E", []byte("E"), 0644),
fstest.CreateFile("/F", []byte("F"), 0644),
),
},
verifyContents{
name: "TestDiffNestedLayeredMergeDeletes",
// this is "ab" + "d" + Diff("abc", "abcdae-a"+"-d") == "abd" + "dae-a-d" == abddae-a-d
state: llb.Merge([]llb.State{
ab().File(llb.Mkfile("D", 0644, []byte("D"))),
llb.Diff(abc(), nested().File(llb.Rm("D"))),
}),
contents: apply(
fstest.CreateFile("/B", []byte("B"), 0644),
fstest.CreateFile("/E", []byte("E"), 0644),
),
},
}
}()...)
tests = append(tests, func() []integration.Test {
a := func() llb.State {
return llb.Scratch().File(llb.Mkfile("A", 0644, []byte("A")))
}
ab := func() llb.State {
return a().File(llb.Mkfile("B", 0644, []byte("B")))
}
abc := func() llb.State {
return ab().File(llb.Mkfile("C", 0644, []byte("C")))
}
return []integration.Test{
// Diffs of diffs
verifyContents{
name: "TestDiffOfDiffs",
state: llb.Diff(
llb.Diff(a(), ab()),
llb.Diff(a(), abc()),
),
contents: apply(
fstest.CreateFile("/C", []byte("C"), 0644),
),
},
verifyContents{
name: "TestDiffOfDiffsWithDeletes",
state: llb.Merge([]llb.State{
abc(),
llb.Diff(
llb.Diff(a(), abc()),
llb.Diff(a(), ab()),
),
}),
contents: apply(
fstest.CreateFile("/A", []byte("A"), 0644),
fstest.CreateFile("/B", []byte("B"), 0644),
),
},
// Diffs can be used as layer parents
verifyContents{
name: "TestDiffAsParentSingleLayer",
state: llb.Diff(a(), ab()).File(llb.Mkfile("D", 0644, []byte("D"))),
contents: apply(
fstest.CreateFile("B", []byte("B"), 0644),
fstest.CreateFile("D", []byte("D"), 0644),
),
},
verifyContents{
name: "TestDiffAsParentSingleLayerDelete",
state: llb.Merge([]llb.State{
ab(),
llb.Diff(a(), ab()).File(llb.Rm("B")),
}),
contents: apply(
fstest.CreateFile("A", []byte("A"), 0644),
),
},
verifyContents{
name: "TestDiffAsParentMultipleLayers",
state: llb.Diff(a(), abc()).File(llb.Mkfile("D", 0644, []byte("D"))),
contents: apply(
fstest.CreateFile("B", []byte("B"), 0644),
fstest.CreateFile("C", []byte("C"), 0644),
fstest.CreateFile("D", []byte("D"), 0644),
),
},
verifyContents{
name: "TestDiffAsParentMultipleLayerDelete",
state: llb.Merge([]llb.State{
ab(),
llb.Diff(a(), abc()).File(llb.Rm("B")),
}),
contents: apply(
fstest.CreateFile("A", []byte("A"), 0644),
fstest.CreateFile("C", []byte("C"), 0644),
),
},
}
}()...)
// Single layer diffs should reuse blobs
tests = append(tests, func() []integration.Test {
mergeBase := func() llb.State {
return llb.Merge([]llb.State{
alpine(),
llb.Scratch().File(llb.Mkfile("/foo", 0644, []byte("/foo"))),
})
}
return []integration.Test{
verifyBlobReuse{
name: "TestDiffSingleLayerBlobReuse",
base: alpine(),
upper: runShell(alpine(),
"cat /dev/urandom | head -c 100 | sha256sum > /randomfile",
),
},
verifyBlobReuse{
name: "TestDiffSingleLayerOnMergeBlobReuse",
base: mergeBase(),
upper: runShell(mergeBase(),
"cat /dev/urandom | head -c 100 | sha256sum > /randomfile",
),
},
}
}()...)
// Regression tests
tests = append(tests, func() []integration.Test {
base := func() llb.State {
return llb.Scratch().File(llb.Mkdir("/dir", 0755))
}
return []integration.Test{
verifyContents{
// Verifies that when a directory with contents is used a a base layer
// in a merge, subsequent merges that first delete the dir (resulting in
// a whiteout device w/ overlay snapshotters) and then recreate the dir
// correctly set it as opaque.
name: "TestDiffMergeOpaqueRegression",
state: llb.Merge([]llb.State{
base().File(llb.Mkfile("/dir/a", 0644, nil)),
base().File(llb.Rm("/dir")),
base().File(llb.Mkfile("/dir/b", 0644, nil)),
}),
contents: apply(
fstest.CreateDir("/dir", 0755),
fstest.CreateFile("/dir/b", nil, 0644),
),
},
}
}()...)
return tests
}
// contents lets you create fstest.Appliers using the sandbox, which
// enables i.e. using an llb.State or fstest.Applier interchangeably
// during test assertions on the contents of a given dir.
type contents func(sb integration.Sandbox) fstest.Applier
// implements fstest.Applier
type applyFn func(root string) error
func (a applyFn) Apply(root string) error {
return a(root)
}
func contentsOf(state llb.State) contents {
return func(sb integration.Sandbox) fstest.Applier {
return applyFn(func(root string) error {
ctx := sb.Context()
c, err := New(ctx, sb.Address())
if err != nil {
return err
}
defer c.Close()
def, err := state.Marshal(ctx)
if err != nil {
return err
}
_, err = c.Solve(ctx, def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: root,
},
},
}, nil)
if err != nil {
return err
}
return nil
})
}
}
func apply(appliers ...fstest.Applier) contents {
return func(sb integration.Sandbox) fstest.Applier {
return fstest.Apply(appliers...)
}
}
func mergeContents(subContents ...contents) contents {
return func(sb integration.Sandbox) fstest.Applier {
var appliers []fstest.Applier
for _, sub := range subContents {
appliers = append(appliers, sub(sb))
}
return fstest.Apply(appliers...)
}
}
func empty(sb integration.Sandbox) fstest.Applier {
return applyFn(func(root string) error {
return nil
})
}
type verifyContents struct {
name string
state llb.State
contents contents
skipOnRootless bool
}
func (tc verifyContents) Name() string {
return tc.name
}
func (tc verifyContents) Run(t *testing.T, sb integration.Sandbox) {
if tc.skipOnRootless && sb.Rootless() {
t.Skip("rootless")
}
requiresLinux(t)
cdAddress := sb.ContainerdAddress()
ctx := sb.Context()
ctdCtx := namespaces.WithNamespace(ctx, "buildkit")
c, err := 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)
// verify the build has the expected contents
imageName := fmt.Sprintf("buildkit/%s", strings.ToLower(tc.name))
imageTarget := fmt.Sprintf("%s/%s:latest", registry, imageName)
cacheName := fmt.Sprintf("buildkit/%s-cache", imageName)
cacheTarget := fmt.Sprintf("%s/%s:latest", registry, cacheName)
var importInlineCacheOpts []CacheOptionsEntry
var exportInlineCacheOpts []CacheOptionsEntry
var importRegistryCacheOpts []CacheOptionsEntry
var exportRegistryCacheOpts []CacheOptionsEntry
if os.Getenv("TEST_DOCKERD") != "1" {
importInlineCacheOpts = []CacheOptionsEntry{{
Type: "registry",
Attrs: map[string]string{
"ref": imageTarget,
},
}}
exportInlineCacheOpts = []CacheOptionsEntry{{
Type: "inline",
}}
importRegistryCacheOpts = []CacheOptionsEntry{{
Type: "registry",
Attrs: map[string]string{
"ref": cacheTarget,
},
}}
exportRegistryCacheOpts = []CacheOptionsEntry{{
Type: "registry",
Attrs: map[string]string{
"ref": cacheTarget,
},
}}
}
resetState(t, c, sb)
requireContents(ctx, t, c, sb, tc.state, nil, exportInlineCacheOpts, imageTarget, tc.contents(sb))
if os.Getenv("TEST_DOCKERD") == "1" {
return
}
for _, importCacheOpts := range [][]CacheOptionsEntry{importInlineCacheOpts, importRegistryCacheOpts} {
// Check that the cache is actually used. This can only be asserted on
// in containerd-based tests because it needs access to the image+content store
if cdAddress != "" {
client, err := newContainerd(cdAddress)
require.NoError(t, err)
defer client.Close()
def, err := tc.state.Marshal(sb.Context())
require.NoError(t, err)
resetState(t, c, sb)
_, err = c.Solve(ctx, def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterImage,
Attrs: map[string]string{
"name": imageTarget,
"push": "true",
},
},
},
CacheImports: importCacheOpts,
}, nil)
require.NoError(t, err)
img, err := client.GetImage(ctdCtx, imageTarget)
require.NoError(t, err)
var unexpectedLayers []ocispecs.Descriptor
require.NoError(t, images.Walk(ctdCtx, images.HandlerFunc(func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
if images.IsLayerType(desc.MediaType) {
_, err := client.ContentStore().Info(ctdCtx, desc.Digest)
if err == nil {
unexpectedLayers = append(unexpectedLayers, desc)
} else {
require.True(t, errdefs.IsNotFound(err))
}
}
return images.Children(ctx, client.ContentStore(), desc)
}), img.Target()))
require.Empty(t, unexpectedLayers)
}
// verify that builds using cache reimport the same contents
resetState(t, c, sb)
requireContents(ctx, t, c, sb, tc.state, importCacheOpts, exportRegistryCacheOpts, imageTarget, tc.contents(sb))
}
}
type verifyBlobReuse struct {
name string
base llb.State
upper llb.State
}
func (tc verifyBlobReuse) Name() string {
return tc.name
}
func (tc verifyBlobReuse) Run(t *testing.T, sb integration.Sandbox) {
skipDockerd(t, sb)
requiresLinux(t)
cdAddress := sb.ContainerdAddress()
if cdAddress == "" {
t.Skip("test requires containerd worker")
}
client, err := newContainerd(cdAddress)
require.NoError(t, err)
defer client.Close()
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
// export the upper, find the layers that were created for it
def, err := tc.upper.Marshal(sb.Context())
require.NoError(t, err)
imageName := fmt.Sprintf("buildkit/%s", strings.ToLower(tc.name))
imageTarget := fmt.Sprintf("%s/%s:latest", registry, imageName)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterImage,
Attrs: map[string]string{
"name": imageTarget,
"push": "true",
},
},
},
}, nil)
require.NoError(t, err)
img, err := client.GetImage(ctx, imageTarget)
require.NoError(t, err)
layerBlobs := map[digest.Digest]struct{}{}
require.NoError(t, images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
if images.IsLayerType(desc.MediaType) {
_, err := client.ContentStore().Info(ctx, desc.Digest)
require.NoError(t, err)
layerBlobs[desc.Digest] = struct{}{}
}
return images.Children(ctx, client.ContentStore(), desc)
}), img.Target()))
require.NotEmpty(t, layerBlobs)
// export the diff, verify that no new layer blobs are created
diff := llb.Diff(tc.base, tc.upper)
def, err = diff.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterImage,
Attrs: map[string]string{
"name": imageTarget,
"push": "true",
},
},
},
}, nil)
require.NoError(t, err)
img, err = client.GetImage(ctx, imageTarget)
require.NoError(t, err)
require.NoError(t, images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
if images.IsLayerType(desc.MediaType) {
_, err := client.ContentStore().Info(ctx, desc.Digest)
require.NoError(t, err)
if _, ok := layerBlobs[desc.Digest]; !ok {
return nil, errors.Errorf("unexpected layer blob %s", desc.Digest)
}
}
return images.Children(ctx, client.ContentStore(), desc)
}), img.Target()))
}
func resetState(t *testing.T, c *Client, sb integration.Sandbox) {
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
cdAddress := sb.ContainerdAddress()
if cdAddress != "" {
client, err := newContainerd(cdAddress)
require.NoError(t, err)
defer client.Close()
imageService := client.ImageService()
imageList, err := imageService.List(ctx)
require.NoError(t, err)
for _, img := range imageList {
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
require.NoError(t, err)
}
}
checkAllReleasable(t, c, sb, true)
}