diff --git a/images/default/Dockerfile b/images/default/Dockerfile index 18eb8bde5..128847421 100644 --- a/images/default/Dockerfile +++ b/images/default/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y curl gnupg2 git \ apt-transport-https ca-certificates gnupg-agent \ software-properties-common \ pkg-config libffi-dev patch diffutils libssl-dev iptables kmod \ - clang crossbuild-essential-amd64 + clang crossbuild-essential-amd64 erofs-utils # Install Docker client for the website build. RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 6a9f4d777..606297438 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "math" "os" + "os/exec" "path" "path/filepath" "reflect" @@ -3135,3 +3136,135 @@ func TestOverlayByMountAnnotation(t *testing.T) { t.Fatalf("overlay filestore at %q was not deleted after container.Destroy()", filestoreFile) } } + +// TestMountEROFS checks that the checksums from the target directory in container +// are identical with the ones from the source directory on host. +func TestMountEROFS(t *testing.T) { + // Skip this test if mkfs.erofs is not available. + mkfs, err := exec.LookPath("mkfs.erofs") + if err != nil { + t.Skipf("mkfs.erofs is not available: %v", err) + } + + // Create a temporary directory to save the test files. + assetsDir, err := ioutil.TempDir(testutil.TmpDir(), "erofs-assets") + if err != nil { + t.Fatalf("ioutil.TempDir() failed: %v", err) + } + + // Create a temporary directory with some random files in it, which will + // be used as the source directory to create the EROFS images. + sourceDir := filepath.Join(assetsDir, "source") + if err := os.Mkdir(sourceDir, 0755); err != nil { + t.Fatalf("os.Mkdir() failed: %v", err) + } + testApp, err := testutil.FindFile("test/cmd/test_app/test_app") + if err != nil { + t.Fatalf("error finding test_app: %v", err) + } + // Source directory is a small directory. Let's create a big directory in it. + // So we can cover both cases. + cmd := fmt.Sprintf("%s fsTreeCreate --target-dir=%s --create-symlink --depth=1 --file-per-level=500 --file-size=5000", testApp, filepath.Join(sourceDir, "big-directory")) + if out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput(); err != nil { + t.Fatalf("exec: sh -c %q, err: %v, out: %s", cmd, err, out) + } + + // Create a test script which can be used to get the checksums + // from a specified directory. + scriptFile := filepath.Join(assetsDir, "test-script") + if err := os.WriteFile(scriptFile, []byte(`#!/bin/bash +set -u -e -o pipefail +dir=$1 +find $dir -printf "%P\n" | sort | md5sum +find $dir -type l | sort | xargs -L 1 readlink | md5sum +find $dir -type l -o -type f | sort | xargs cat | md5sum`), 0755); err != nil { + t.Fatalf("os.WriteFile() failed: %v", err) + } + + // Get the checksums from the source directory on host. + var checksums string + if out, err := exec.Command(scriptFile, sourceDir).CombinedOutput(); err != nil { + t.Fatalf("exec: %s %s, err: %v, out: %s", scriptFile, sourceDir, err, out) + } else { + checksums = string(out) + } + + images := []struct { + name string + options string + }{ + { + // Generate extended inodes. Inline regular files if possible. + name: "image1", + options: "-E force-inode-extended", + }, + { + // Generate extended inodes. Do not inline regular files. + name: "image2", + options: "-E force-inode-extended -E noinline_data", + }, + { + // Generate compact inodes. Inline regular files if possible. + name: "image3", + options: "-E force-inode-compact", + }, + { + // Generate compact inodes. Do not inline regular files. + name: "image4", + options: "-E force-inode-compact -E noinline_data", + }, + } + + // Create the EROFS images. + for _, i := range images { + cmd := fmt.Sprintf("%s %s %s %s", mkfs, i.options, filepath.Join(assetsDir, i.name), sourceDir) + if out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput(); err != nil { + t.Fatalf("exec: sh -c %q, err: %v, out: %s", cmd, err, out) + } + } + + spec, _ := sleepSpecConf(t) + conf := testutil.TestConfig(t) + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Create and start the container. + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + c, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer c.Destroy() + if err := c.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + targetDir := "/mnt" + for _, i := range images { + // Mount the EROFS image in container. + imageFile := filepath.Join(assetsDir, i.name) + if err := c.Sandbox.Mount(c.ID, "erofs", imageFile, targetDir); err != nil { + t.Fatalf("error mounting EROFS image %q to %q, err: %v", imageFile, targetDir, err) + } + + // Get the checksums from the target directory in container, and check if they are + // identical with the ones got from the source directory on host. + if out, err := executeCombinedOutput(conf, c, nil, scriptFile, targetDir); err != nil { + t.Fatalf("exec: %s %s, err: %v, out: %s", scriptFile, targetDir, err, out) + } else if checksums != string(out) { + t.Errorf("checksums do not match, got: %s from %s, expected: %s", out, imageFile, checksums) + } + + // Unmount the EROFS image in container. + if out, err := executeCombinedOutput(conf, c, nil, "/bin/umount", targetDir); err != nil { + t.Fatalf("exec: umount %q, err: %v, out: %s", targetDir, err, out) + } + } +} diff --git a/test/cmd/test_app/main.go b/test/cmd/test_app/main.go index fc91aebee..141bc214d 100644 --- a/test/cmd/test_app/main.go +++ b/test/cmd/test_app/main.go @@ -64,6 +64,8 @@ type fsTreeCreator struct { depth uint numFilesPerLevel uint fileSize uint + targetDir string + createSymlink bool } // Name implements subcommands.Command.Name. @@ -73,7 +75,7 @@ func (*fsTreeCreator) Name() string { // Synopsis implements subcommands.Command.Synopsys. func (*fsTreeCreator) Synopsis() string { - return "creates a filesystem tree of a certain depth, with a certain number of files on each level and each file with a certain size. Some randomization is added on top of this" + return "creates a filesystem tree of a certain depth, with a certain number of files on each level and each file with a certain size and type, under a certain directory. Some randomization is added on top of this" } // Usage implements subcommands.Command.Usage. @@ -86,6 +88,8 @@ func (c *fsTreeCreator) SetFlags(f *flag.FlagSet) { f.UintVar(&c.depth, "depth", 10, "number of levels to create") f.UintVar(&c.numFilesPerLevel, "file-per-level", 10, "number of files to create per level") f.UintVar(&c.fileSize, "file-size", 4096, "size of each file") + f.StringVar(&c.targetDir, "target-dir", "/", "directory under which to create the filesystem tree") + f.BoolVar(&c.createSymlink, "create-symlink", false, "create symlinks other than the first file per level") } // Execute implements subcommands.Command.Execute. @@ -93,15 +97,26 @@ func (c *fsTreeCreator) Execute(ctx context.Context, f *flag.FlagSet, args ...an depth := c.depth + uint(rand.Uint32())%c.depth numFilesPerLevel := c.numFilesPerLevel + uint(rand.Uint32())%c.numFilesPerLevel fileSize := c.fileSize + uint(rand.Uint32())%c.fileSize + curDir := c.targetDir + if _, err := os.Stat(curDir); os.IsNotExist(err) { + if err := os.MkdirAll(curDir, 0777); err != nil { + log.Fatalf("error creating directory %q: %v", curDir, err) + } + } - curDir := "/" data := make([]byte, fileSize) rand.Read(data) for i := uint(0); i < depth; i++ { for j := uint(0); j < numFilesPerLevel; j++ { filePath := filepath.Join(curDir, fmt.Sprintf("file%d", j)) - if err := os.WriteFile(filePath, data, 0666); err != nil { - log.Fatalf("error writing file %q: %v", filePath, err) + if c.createSymlink && j > 0 { + if err := os.Symlink("file0", filePath); err != nil { + log.Fatalf("error creating symlink %q: %v", filePath, err) + } + } else { + if err := os.WriteFile(filePath, data, 0666); err != nil { + log.Fatalf("error writing file %q: %v", filePath, err) + } } } nextDir := filepath.Join(curDir, "dir")