blob: b55f0b8731f38c8140aa435551c9fd76b504b6eb [file] [log] [blame] [edit]
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package erofs
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/storage"
"github.com/containerd/containerd/v2/core/snapshots/testsuite"
"github.com/containerd/containerd/v2/internal/erofsutils"
"github.com/containerd/containerd/v2/internal/fsverity"
"github.com/containerd/containerd/v2/pkg/archive/tartest"
"github.com/containerd/containerd/v2/pkg/testutil"
"github.com/containerd/containerd/v2/plugins/content/local"
erofsdiffer "github.com/containerd/containerd/v2/plugins/diff/erofs"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
testFileContent = "Hello, this is content for testing the EROFS Snapshotter!"
testNestedFileContent = "Nested file content"
)
func newSnapshotter(t *testing.T, opts ...Opt) func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
_, err := exec.LookPath("mkfs.erofs")
if err != nil {
t.Skipf("could not find mkfs.erofs: %v", err)
}
if !findErofs() {
t.Skip("check for erofs kernel support failed, skipping test")
}
return func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
snapshotter, err := NewSnapshotter(root, opts...)
if err != nil {
return nil, nil, err
}
return snapshotter, func() error { return snapshotter.Close() }, nil
}
}
func testMount(t *testing.T, scratchFile string) error {
root, err := os.MkdirTemp(t.TempDir(), "")
if err != nil {
return err
}
defer os.RemoveAll(root)
m := []mount.Mount{
{
Type: "ext4",
Source: scratchFile,
Options: []string{"loop", "direct-io", "sync"},
},
}
if err := mount.All(m, root); err != nil {
return fmt.Errorf("failed to mount device %s: %w", scratchFile, err)
}
if err := os.Remove(filepath.Join(root, "lost+found")); err != nil {
return err
}
if err := os.Mkdir(filepath.Join(root, "work"), 0755); err != nil {
return err
}
if err := os.Mkdir(filepath.Join(root, "upper"), 0755); err != nil {
return err
}
return mount.UnmountAll(root, 0)
}
func TestErofs(t *testing.T) {
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "erofs", newSnapshotter(t))
}
func TestErofsWithQuota(t *testing.T) {
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "erofs", newSnapshotter(t, WithDefaultSize(16*1024*1024)))
}
func TestErofsFsverity(t *testing.T) {
testutil.RequiresRoot(t)
ctx := context.Background()
root := t.TempDir()
// Skip if fsverity is not supported
supported, err := fsverity.IsSupported(root)
if !supported || err != nil {
t.Skip("fsverity not supported, skipping test")
}
// Create snapshotter with fsverity enabled
s, err := NewSnapshotter(root, WithFsverity())
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Create a test snapshot
key := "test-snapshot"
mounts, err := s.Prepare(ctx, key, "")
if err != nil {
t.Fatal(err)
}
target := filepath.Join(root, key)
if err := os.MkdirAll(target, 0755); err != nil {
t.Fatal(err)
}
if err := mount.All(mounts, target); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, target)
// Write test data
if err := os.WriteFile(filepath.Join(target, "foo"), []byte("test data"), 0777); err != nil {
t.Fatal(err)
}
// Commit the snapshot
commitKey := "test-commit"
if err := s.Commit(ctx, commitKey, key); err != nil {
t.Fatal(err)
}
snap := s.(*snapshotter)
// Get the internal ID from the snapshotter
var id string
if err := snap.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, _, _, err = storage.GetInfo(ctx, commitKey)
return err
}); err != nil {
t.Fatal(err)
}
// Verify fsverity is enabled on the EROFS layer
layerPath := snap.layerBlobPath(id)
enabled, err := fsverity.IsEnabled(layerPath)
if err != nil {
t.Fatalf("Failed to check fsverity status: %v", err)
}
if !enabled {
t.Fatal("Expected fsverity to be enabled on committed layer")
}
// Try to modify the layer file directly (should fail)
if err := os.WriteFile(layerPath, []byte("tampered data"), 0666); err == nil {
t.Fatal("Expected direct write to fsverity-enabled layer to fail")
}
}
func TestErofsDifferWithTarIndexMode(t *testing.T) {
testutil.RequiresRoot(t)
ctx := context.Background()
if !findErofs() {
t.Skip("check for erofs kernel support failed, skipping test")
}
// Check if mkfs.erofs supports tar index mode
supported, err := erofsutils.SupportGenerateFromTar()
if err != nil || !supported {
t.Skip("mkfs.erofs does not support tar mode, skipping tar index test")
}
tempDir := t.TempDir()
// Create content store for the differ
contentStore, err := local.NewStore(filepath.Join(tempDir, "content"))
if err != nil {
t.Fatal(err)
}
// Create EROFS differ with tar index mode enabled
differ := erofsdiffer.NewErofsDiffer(contentStore, erofsdiffer.WithTarIndexMode())
// Create EROFS snapshotter
snapshotRoot := filepath.Join(tempDir, "snapshots")
s, err := NewSnapshotter(snapshotRoot)
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Create test tar content
tarReader := createTestTarContent()
defer tarReader.Close()
// Read the tar content into a buffer for digest calculation and writing
tarContent, err := io.ReadAll(tarReader)
if err != nil {
t.Fatal(err)
}
// Write tar content to content store
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayerGzip,
Digest: digest.FromBytes(tarContent),
Size: int64(len(tarContent)),
}
writer, err := contentStore.Writer(ctx,
content.WithRef("test-layer"),
content.WithDescriptor(desc))
if err != nil {
t.Fatal(err)
}
if _, err := writer.Write(tarContent); err != nil {
writer.Close()
t.Fatal(err)
}
if err := writer.Commit(ctx, desc.Size, desc.Digest); err != nil {
writer.Close()
t.Fatal(err)
}
writer.Close()
// Prepare a snapshot using the snapshotter
snapshotKey := "test-snapshot"
mounts, err := s.Prepare(ctx, snapshotKey, "")
if err != nil {
t.Fatal(err)
}
// Apply the tar content using the EROFS differ with tar index mode
appliedDesc, err := differ.Apply(ctx, desc, mounts)
if err != nil {
t.Fatal(err)
}
t.Logf("Applied layer using EROFS differ with tar index mode:")
t.Logf(" Original: %s (%d bytes)", desc.Digest, desc.Size)
t.Logf(" Applied: %s (%d bytes)", appliedDesc.Digest, appliedDesc.Size)
t.Logf(" MediaType: %s", appliedDesc.MediaType)
// Commit the snapshot to finalize the EROFS layer creation
commitKey := "test-commit"
if err := s.Commit(ctx, commitKey, snapshotKey); err != nil {
t.Fatal(err)
}
// Get the internal snapshot ID to check the EROFS layer file
snap := s.(*snapshotter)
var id string
if err := snap.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, _, _, err = storage.GetInfo(ctx, commitKey)
return err
}); err != nil {
t.Fatal(err)
}
// Verify the EROFS layer file was created
layerPath := snap.layerBlobPath(id)
if _, err := os.Stat(layerPath); err != nil {
t.Fatalf("EROFS layer file should exist: %v", err)
}
// Verify the layer file is not empty
stat, err := os.Stat(layerPath)
if err != nil {
t.Fatal(err)
}
if stat.Size() == 0 {
t.Fatal("EROFS layer file should not be empty")
}
t.Logf("EROFS layer file created with tar index mode: %s (%d bytes)", layerPath, stat.Size())
// Create a view to verify the content
viewKey := "test-view"
viewMounts, err := s.View(ctx, viewKey, commitKey)
if err != nil {
t.Fatal(err)
}
viewTarget := filepath.Join(tempDir, viewKey)
if err := os.MkdirAll(viewTarget, 0755); err != nil {
t.Fatal(err)
}
if err := mount.All(viewMounts, viewTarget); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, viewTarget)
// Verify we can read the original test data
testData, err := os.ReadFile(filepath.Join(viewTarget, "test-file.txt"))
if err != nil {
t.Fatal(err)
}
expected := testFileContent
if string(testData) != expected {
t.Fatalf("Expected %q, got %q", expected, string(testData))
}
// Verify nested file
nestedData, err := os.ReadFile(filepath.Join(viewTarget, "testdir", "nested.txt"))
if err != nil {
t.Fatal(err)
}
expectedNested := testNestedFileContent
if string(nestedData) != expectedNested {
t.Fatalf("Expected %q, got %q", expectedNested, string(nestedData))
}
t.Logf("Successfully verified EROFS Snapshotter using the differ with tar index mode")
}
// Helper function to create test tar content using tartest
func createTestTarContent() io.ReadCloser {
// Create a tar context with current time for consistency
tc := tartest.TarContext{}.WithModTime(time.Now())
// Create the tar with our test files and directories
tarWriter := tartest.TarAll(
tc.File("test-file.txt", []byte(testFileContent), 0644),
tc.Dir("testdir", 0755),
tc.File("testdir/nested.txt", []byte(testNestedFileContent), 0644),
)
// Return the tar as a ReadCloser
return tartest.TarFromWriterTo(tarWriter)
}