
From golangci-lint: > SA1019: rand.Read has been deprecated since Go 1.20 because it >shouldn't be used: For almost all use cases, crypto/rand.Read is more >appropriate. (staticcheck) > SA1019: rand.Seed has been deprecated since Go 1.20 and an alternative >has been available since Go 1.0: Programs that call Seed and then expect >a specific sequence of results from the global random source (using >functions such as Int) can be broken when a dependency changes how >much it consumes from the global random source. To avoid such breakages, >programs that need a specific result sequence should use >NewRand(NewSource(seed)) to obtain a random generator that other >packages cannot access. (staticcheck) See also: - https://pkg.go.dev/math/rand@go1.20#Read - https://pkg.go.dev/math/rand@go1.20#Seed Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1113 lines
32 KiB
Go
1113 lines
32 KiB
Go
/*
|
|
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 testsuite
|
|
|
|
import (
|
|
"context"
|
|
//nolint:revive // go-digest needs the blank import. See https://github.com/opencontainers/go-digest#usage.
|
|
_ "crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/log/logtest"
|
|
"github.com/containerd/containerd/mount"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/containerd/pkg/randutil"
|
|
"github.com/containerd/containerd/pkg/testutil"
|
|
"github.com/containerd/containerd/snapshots"
|
|
"github.com/containerd/continuity/fs/fstest"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// SnapshotterFunc is used in SnapshotterSuite
|
|
type SnapshotterFunc func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error)
|
|
|
|
// SnapshotterSuite runs a test suite on the snapshotter given a factory function.
|
|
func SnapshotterSuite(t *testing.T, name string, snapshotterFn SnapshotterFunc) {
|
|
restoreMask := clearMask()
|
|
defer restoreMask()
|
|
|
|
t.Run("Basic", makeTest(name, snapshotterFn, checkSnapshotterBasic))
|
|
t.Run("StatActive", makeTest(name, snapshotterFn, checkSnapshotterStatActive))
|
|
t.Run("StatComitted", makeTest(name, snapshotterFn, checkSnapshotterStatCommitted))
|
|
t.Run("TransitivityTest", makeTest(name, snapshotterFn, checkSnapshotterTransitivity))
|
|
t.Run("PreareViewFailingtest", makeTest(name, snapshotterFn, checkSnapshotterPrepareView))
|
|
t.Run("Update", makeTest(name, snapshotterFn, checkUpdate))
|
|
t.Run("Remove", makeTest(name, snapshotterFn, checkRemove))
|
|
t.Run("Walk", makeTest(name, snapshotterFn, checkWalk))
|
|
|
|
t.Run("LayerFileupdate", makeTest(name, snapshotterFn, checkLayerFileUpdate))
|
|
t.Run("RemoveDirectoryInLowerLayer", makeTest(name, snapshotterFn, checkRemoveDirectoryInLowerLayer))
|
|
t.Run("Chown", makeTest(name, snapshotterFn, checkChown))
|
|
t.Run("DirectoryPermissionOnCommit", makeTest(name, snapshotterFn, checkDirectoryPermissionOnCommit))
|
|
t.Run("RemoveIntermediateSnapshot", makeTest(name, snapshotterFn, checkRemoveIntermediateSnapshot))
|
|
t.Run("DeletedFilesInChildSnapshot", makeTest(name, snapshotterFn, checkDeletedFilesInChildSnapshot))
|
|
t.Run("MoveFileFromLowerLayer", makeTest(name, snapshotterFn, checkFileFromLowerLayer))
|
|
|
|
t.Run("ViewReadonly", makeTest(name, snapshotterFn, checkSnapshotterViewReadonly))
|
|
|
|
t.Run("StatInWalk", makeTest(name, snapshotterFn, checkStatInWalk))
|
|
t.Run("CloseTwice", makeTest(name, snapshotterFn, closeTwice))
|
|
t.Run("RootPermission", makeTest(name, snapshotterFn, checkRootPermission))
|
|
|
|
// Different snapshotters behave slightly differently in the tests below.
|
|
t.Run("Rename", makeTest(name, snapshotterFn, checkRename(name)))
|
|
t.Run("128LayersMount", makeTest(name, snapshotterFn, check128LayersMount(name)))
|
|
}
|
|
|
|
func makeTest(
|
|
snapshotter string,
|
|
snapshotterFn func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error),
|
|
fn func(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string),
|
|
) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := logtest.WithT(context.Background(), t)
|
|
ctx = namespaces.WithNamespace(ctx, "testsuite")
|
|
// Make two directories: a snapshotter root and a play area for the tests:
|
|
//
|
|
// /tmp
|
|
// work/ -> passed to test functions
|
|
// root/ -> passed to snapshotter
|
|
//
|
|
tmpDir, err := os.MkdirTemp("", "snapshot-suite-"+snapshotter+"-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
root := filepath.Join(tmpDir, "root")
|
|
if err := os.MkdirAll(root, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
snapshotter, cleanup, err := snapshotterFn(ctx, root)
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize snapshotter: %+v", err)
|
|
}
|
|
defer func() {
|
|
if cleanup != nil {
|
|
if err := cleanup(); err != nil {
|
|
t.Errorf("Cleanup failed: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
work := filepath.Join(tmpDir, "work")
|
|
if err := os.MkdirAll(work, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer testutil.DumpDirOnFailure(t, tmpDir)
|
|
fn(ctx, t, snapshotter, work)
|
|
}
|
|
}
|
|
|
|
var opt = snapshots.WithLabels(map[string]string{
|
|
"containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
|
|
// checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter.
|
|
func checkSnapshotterBasic(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
initialApplier := fstest.Apply(
|
|
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
|
|
fstest.CreateDir("/a", 0755),
|
|
fstest.CreateDir("/a/b", 0755),
|
|
fstest.CreateDir("/a/b/c", 0755),
|
|
)
|
|
|
|
diffApplier := fstest.Apply(
|
|
fstest.CreateFile("/bar", []byte("bar\n"), 0777),
|
|
// also, change content of foo to bar
|
|
fstest.CreateFile("/foo", []byte("bar\n"), 0777),
|
|
fstest.RemoveAll("/a/b"),
|
|
)
|
|
|
|
preparing := filepath.Join(work, "preparing")
|
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
|
|
if err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
if len(mounts) < 1 {
|
|
t.Fatal("expected mounts to have entries")
|
|
}
|
|
|
|
if err := mount.All(mounts, preparing); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
if err := initialApplier.Apply(preparing); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
// unmount before commit
|
|
testutil.Unmount(t, preparing)
|
|
|
|
committed := filepath.Join(work, "committed")
|
|
if err := snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
si, err := snapshotter.Stat(ctx, committed)
|
|
if err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
assert.Empty(t, si.Parent)
|
|
assert.Equal(t, snapshots.KindCommitted, si.Kind)
|
|
|
|
_, err = snapshotter.Stat(ctx, preparing)
|
|
if err == nil {
|
|
t.Fatalf("%s should no longer be available after Commit", preparing)
|
|
}
|
|
|
|
next := filepath.Join(work, "nextlayer")
|
|
if err := os.MkdirAll(next, 0777); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
mounts, err = snapshotter.Prepare(ctx, next, committed, opt)
|
|
if err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
if err := mount.All(mounts, next); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
if err := fstest.CheckDirectoryEqualWithApplier(next, initialApplier); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
if err := diffApplier.Apply(next); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
// unmount before commit
|
|
testutil.Unmount(t, next)
|
|
|
|
ni, err := snapshotter.Stat(ctx, next)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, committed, ni.Parent)
|
|
assert.Equal(t, snapshots.KindActive, ni.Kind)
|
|
|
|
nextCommitted := filepath.Join(work, "committed-next")
|
|
if err := snapshotter.Commit(ctx, nextCommitted, next, opt); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
si2, err := snapshotter.Stat(ctx, nextCommitted)
|
|
if err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
assert.Equal(t, committed, si2.Parent)
|
|
assert.Equal(t, snapshots.KindCommitted, si2.Kind)
|
|
|
|
_, err = snapshotter.Stat(ctx, next)
|
|
if err == nil {
|
|
t.Fatalf("%s should no longer be available after Commit", next)
|
|
}
|
|
|
|
expected := map[string]snapshots.Info{
|
|
si.Name: si,
|
|
si2.Name: si2,
|
|
}
|
|
walked := map[string]snapshots.Info{} // walk is not ordered
|
|
assert.Nil(t, snapshotter.Walk(ctx, func(ctx context.Context, si snapshots.Info) error {
|
|
walked[si.Name] = si
|
|
return nil
|
|
}))
|
|
|
|
for ek, ev := range expected {
|
|
av, ok := walked[ek]
|
|
if !ok {
|
|
t.Errorf("Missing stat for %v", ek)
|
|
continue
|
|
}
|
|
assert.Equal(t, ev, av)
|
|
}
|
|
|
|
nextnext := filepath.Join(work, "nextnextlayer")
|
|
if err := os.MkdirAll(nextnext, 0777); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
mounts, err = snapshotter.View(ctx, nextnext, nextCommitted, opt)
|
|
if err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
if err := mount.All(mounts, nextnext); err != nil {
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
if err := fstest.CheckDirectoryEqualWithApplier(nextnext,
|
|
fstest.Apply(initialApplier, diffApplier)); err != nil {
|
|
testutil.Unmount(t, nextnext)
|
|
t.Fatalf("failure reason: %+v", err)
|
|
}
|
|
|
|
testutil.Unmount(t, nextnext)
|
|
assert.Nil(t, snapshotter.Remove(ctx, nextnext))
|
|
|
|
err = snapshotter.Remove(ctx, committed)
|
|
assert.NotNil(t, err)
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "remove")
|
|
}
|
|
|
|
assert.Nil(t, snapshotter.Remove(ctx, nextCommitted))
|
|
assert.Nil(t, snapshotter.Remove(ctx, committed))
|
|
}
|
|
|
|
// Create a New Layer on top of base layer with Prepare, Stat on new layer, should return Active layer.
|
|
func checkSnapshotterStatActive(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing := filepath.Join(work, "preparing")
|
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(mounts) < 1 {
|
|
t.Fatal("expected mounts to have entries")
|
|
}
|
|
|
|
if err = mount.All(mounts, preparing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, preparing)
|
|
|
|
if err = os.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
si, err := snapshotter.Stat(ctx, preparing)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, si.Name, preparing)
|
|
assert.Equal(t, snapshots.KindActive, si.Kind)
|
|
assert.Equal(t, "", si.Parent)
|
|
}
|
|
|
|
// Commit a New Layer on top of base layer with Prepare & Commit , Stat on new layer, should return Committed layer.
|
|
func checkSnapshotterStatCommitted(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing := filepath.Join(work, "preparing")
|
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(mounts) < 1 {
|
|
t.Fatal("expected mounts to have entries")
|
|
}
|
|
|
|
if err = mount.All(mounts, preparing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, preparing)
|
|
|
|
if err = os.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
committed := filepath.Join(work, "committed")
|
|
if err = snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
si, err := snapshotter.Stat(ctx, committed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, si.Name, committed)
|
|
assert.Equal(t, snapshots.KindCommitted, si.Kind)
|
|
assert.Equal(t, "", si.Parent)
|
|
|
|
}
|
|
|
|
func snapshotterPrepareMount(ctx context.Context, snapshotter snapshots.Snapshotter, diffPathName string, parent string, work string) (string, error) {
|
|
preparing := filepath.Join(work, diffPathName)
|
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
mounts, err := snapshotter.Prepare(ctx, preparing, parent, opt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(mounts) < 1 {
|
|
return "", fmt.Errorf("expected mounts to have entries")
|
|
}
|
|
|
|
if err = mount.All(mounts, preparing); err != nil {
|
|
return "", err
|
|
}
|
|
return preparing, nil
|
|
}
|
|
|
|
// Given A <- B <- C, B is the parent of C and A is a transitive parent of C (in this case, a "grandparent")
|
|
func checkSnapshotterTransitivity(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, preparing)
|
|
|
|
if err = os.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
snapA := filepath.Join(work, "snapA")
|
|
if err = snapshotter.Commit(ctx, snapA, preparing, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
next, err := snapshotterPrepareMount(ctx, snapshotter, "next", snapA, work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, next)
|
|
|
|
if err = os.WriteFile(filepath.Join(next, "foo"), []byte("foo bar\n"), 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
snapB := filepath.Join(work, "snapB")
|
|
if err = snapshotter.Commit(ctx, snapB, next, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
siA, err := snapshotter.Stat(ctx, snapA)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
siB, err := snapshotter.Stat(ctx, snapB)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
siParentB, err := snapshotter.Stat(ctx, siB.Parent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test the transivity
|
|
assert.Equal(t, "", siA.Parent)
|
|
assert.Equal(t, snapA, siB.Parent)
|
|
assert.Equal(t, "", siParentB.Parent)
|
|
|
|
}
|
|
|
|
// Creating two layers with Prepare or View with same key must fail.
|
|
func checkSnapshotterPrepareView(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, preparing)
|
|
|
|
snapA := filepath.Join(work, "snapA")
|
|
if err = snapshotter.Commit(ctx, snapA, preparing, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Prepare & View with same key
|
|
newLayer := filepath.Join(work, "newlayer")
|
|
if err = os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Prepare & View with same key
|
|
_, err = snapshotter.Prepare(ctx, newLayer, snapA, opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = snapshotter.View(ctx, newLayer, snapA, opt)
|
|
assert.True(t, err != nil)
|
|
|
|
// Two Prepare with same key
|
|
prepLayer := filepath.Join(work, "prepLayer")
|
|
if err = os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = snapshotter.Prepare(ctx, prepLayer, snapA, opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = snapshotter.Prepare(ctx, prepLayer, snapA, opt)
|
|
assert.True(t, err != nil)
|
|
|
|
// Two View with same key
|
|
viewLayer := filepath.Join(work, "viewLayer")
|
|
if err = os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = snapshotter.View(ctx, viewLayer, snapA, opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = snapshotter.View(ctx, viewLayer, snapA, opt)
|
|
assert.True(t, err != nil)
|
|
|
|
}
|
|
|
|
// Deletion of files/folder of base layer in new layer, On Commit, those files should not be visible.
|
|
func checkDeletedFilesInChildSnapshot(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
|
|
l1Init := fstest.Apply(
|
|
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
|
|
fstest.CreateFile("/foobar", []byte("foobar\n"), 0777),
|
|
)
|
|
l2Init := fstest.Apply(
|
|
fstest.RemoveAll("/foobar"),
|
|
)
|
|
l3Init := fstest.Apply()
|
|
|
|
if err := checkSnapshots(ctx, snapshotter, work, l1Init, l2Init, l3Init); err != nil {
|
|
t.Fatalf("Check snapshots failed: %+v", err)
|
|
}
|
|
|
|
}
|
|
|
|
// Create three layers. Deleting intermediate layer must fail.
|
|
func checkRemoveIntermediateSnapshot(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
|
|
base, err := snapshotterPrepareMount(ctx, snapshotter, "base", "", work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
committedBase := filepath.Join(work, "committed-base")
|
|
if err = snapshotter.Commit(ctx, committedBase, base, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create intermediate layer
|
|
intermediate := filepath.Join(work, "intermediate")
|
|
if _, err = snapshotter.Prepare(ctx, intermediate, committedBase, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
committedInter := filepath.Join(work, "committed-inter")
|
|
if err = snapshotter.Commit(ctx, committedInter, intermediate, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create top layer
|
|
topLayer := filepath.Join(work, "toplayer")
|
|
if _, err = snapshotter.Prepare(ctx, topLayer, committedInter, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Deletion of intermediate layer must fail.
|
|
err = snapshotter.Remove(ctx, committedInter)
|
|
if err == nil {
|
|
t.Fatal("intermediate layer removal should fail.")
|
|
}
|
|
|
|
//Removal from toplayer to base should not fail.
|
|
err = snapshotter.Remove(ctx, topLayer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = snapshotter.Remove(ctx, committedInter)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testutil.Unmount(t, base)
|
|
err = snapshotter.Remove(ctx, committedBase)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// baseTestSnapshots creates a base set of snapshots for tests, each snapshot is empty
|
|
// Tests snapshots:
|
|
//
|
|
// c1 - committed snapshot, no parent
|
|
// c2 - committed snapshot, c1 is parent
|
|
// a1 - active snapshot, c2 is parent
|
|
// a1 - active snapshot, no parent
|
|
// v1 - view snapshot, v1 is parent
|
|
// v2 - view snapshot, no parent
|
|
func baseTestSnapshots(ctx context.Context, snapshotter snapshots.Snapshotter) error {
|
|
if _, err := snapshotter.Prepare(ctx, "c1-a", "", opt); err != nil {
|
|
return err
|
|
}
|
|
if err := snapshotter.Commit(ctx, "c1", "c1-a", opt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := snapshotter.Prepare(ctx, "c2-a", "c1", opt); err != nil {
|
|
return err
|
|
}
|
|
if err := snapshotter.Commit(ctx, "c2", "c2-a", opt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := snapshotter.Prepare(ctx, "a1", "c2", opt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := snapshotter.Prepare(ctx, "a2", "", opt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := snapshotter.View(ctx, "v1", "c2", opt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := snapshotter.View(ctx, "v2", "", opt); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkUpdate(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
t1 := time.Now().UTC()
|
|
if err := baseTestSnapshots(ctx, snapshotter); err != nil {
|
|
t.Fatalf("Failed to create base snapshots: %v", err)
|
|
}
|
|
t2 := time.Now().UTC()
|
|
testcases := []struct {
|
|
name string
|
|
kind snapshots.Kind
|
|
parent string
|
|
}{
|
|
{
|
|
name: "c1",
|
|
kind: snapshots.KindCommitted,
|
|
},
|
|
{
|
|
name: "c2",
|
|
kind: snapshots.KindCommitted,
|
|
parent: "c1",
|
|
},
|
|
{
|
|
name: "a1",
|
|
kind: snapshots.KindActive,
|
|
parent: "c2",
|
|
},
|
|
{
|
|
name: "a2",
|
|
kind: snapshots.KindActive,
|
|
},
|
|
{
|
|
name: "v1",
|
|
kind: snapshots.KindView,
|
|
parent: "c2",
|
|
},
|
|
{
|
|
name: "v2",
|
|
kind: snapshots.KindView,
|
|
},
|
|
}
|
|
for _, tc := range testcases {
|
|
st, err := snapshotter.Stat(ctx, tc.name)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat %s: %v", tc.name, err)
|
|
}
|
|
if st.Created.Before(t1) || st.Created.After(t2) {
|
|
t.Errorf("(%s) wrong created time %s: expected between %s and %s", tc.name, st.Created, t1, t2)
|
|
continue
|
|
}
|
|
if st.Created != st.Updated {
|
|
t.Errorf("(%s) unexpected updated time %s: expected %s", tc.name, st.Updated, st.Created)
|
|
continue
|
|
}
|
|
if st.Kind != tc.kind {
|
|
t.Errorf("(%s) unexpected kind %s, expected %s", tc.name, st.Kind, tc.kind)
|
|
continue
|
|
}
|
|
if st.Parent != tc.parent {
|
|
t.Errorf("(%s) unexpected parent %q, expected %q", tc.name, st.Parent, tc.parent)
|
|
continue
|
|
}
|
|
if st.Name != tc.name {
|
|
t.Errorf("(%s) unexpected name %q, expected %q", tc.name, st.Name, tc.name)
|
|
continue
|
|
}
|
|
|
|
createdAt := st.Created
|
|
rootTime := time.Now().UTC().Format(time.RFC3339)
|
|
expected := map[string]string{
|
|
"l1": "v1",
|
|
"l2": "v2",
|
|
"l3": "v3",
|
|
// Keep root label
|
|
"containerd.io/gc.root": rootTime,
|
|
}
|
|
st.Parent = "doesnotexist"
|
|
st.Labels = expected
|
|
u1 := time.Now().UTC()
|
|
st, err = snapshotter.Update(ctx, st)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update %s: %v", tc.name, err)
|
|
}
|
|
u2 := time.Now().UTC()
|
|
|
|
if st.Created != createdAt {
|
|
t.Errorf("(%s) wrong created time %s: expected %s", tc.name, st.Created, createdAt)
|
|
continue
|
|
}
|
|
if st.Updated.Before(u1) || st.Updated.After(u2) {
|
|
t.Errorf("(%s) wrong updated time %s: expected between %s and %s", tc.name, st.Updated, u1, u2)
|
|
continue
|
|
}
|
|
if st.Kind != tc.kind {
|
|
t.Errorf("(%s) unexpected kind %s, expected %s", tc.name, st.Kind, tc.kind)
|
|
continue
|
|
}
|
|
if st.Parent != tc.parent {
|
|
t.Errorf("(%s) unexpected parent %q, expected %q", tc.name, st.Parent, tc.parent)
|
|
continue
|
|
}
|
|
if st.Name != tc.name {
|
|
t.Errorf("(%s) unexpected name %q, expected %q", tc.name, st.Name, tc.name)
|
|
continue
|
|
}
|
|
assertLabels(t, st.Labels, expected)
|
|
|
|
expected = map[string]string{
|
|
"l1": "updated",
|
|
"l3": "v3",
|
|
|
|
"containerd.io/gc.root": rootTime,
|
|
}
|
|
st.Labels = map[string]string{
|
|
"l1": "updated",
|
|
"l4": "v4",
|
|
}
|
|
st, err = snapshotter.Update(ctx, st, "labels.l1", "labels.l2")
|
|
if err != nil {
|
|
t.Fatalf("Failed to update %s: %v", tc.name, err)
|
|
}
|
|
assertLabels(t, st.Labels, expected)
|
|
|
|
expected = map[string]string{
|
|
"l4": "v4",
|
|
|
|
"containerd.io/gc.root": rootTime,
|
|
}
|
|
st.Labels = expected
|
|
st, err = snapshotter.Update(ctx, st, "labels")
|
|
if err != nil {
|
|
t.Fatalf("Failed to update %s: %v", tc.name, err)
|
|
}
|
|
assertLabels(t, st.Labels, expected)
|
|
|
|
// Test failure received when providing immutable field path
|
|
st.Parent = "doesnotexist"
|
|
st, err = snapshotter.Update(ctx, st, "parent")
|
|
if err == nil {
|
|
t.Errorf("Expected error updating with immutable field path")
|
|
} else if !errdefs.IsInvalidArgument(err) {
|
|
t.Fatalf("Unexpected error updating %s: %+v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertLabels(t *testing.T, actual, expected map[string]string) {
|
|
if len(actual) != len(expected) {
|
|
t.Fatalf("Label size mismatch: %d vs %d\n\tActual: %#v\n\tExpected: %#v", len(actual), len(expected), actual, expected)
|
|
}
|
|
for k, v := range expected {
|
|
if a := actual[k]; v != a {
|
|
t.Errorf("Wrong label value for %s, got %q, expected %q", k, a, v)
|
|
}
|
|
}
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
func checkRemove(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
if _, err := snapshotter.Prepare(ctx, "committed-a", "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Commit(ctx, "committed-1", "committed-a", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := snapshotter.Prepare(ctx, "reuse-1", "committed-1", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Remove(ctx, "reuse-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := snapshotter.View(ctx, "reuse-1", "committed-1", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Remove(ctx, "reuse-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := snapshotter.Prepare(ctx, "reuse-1", "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Remove(ctx, "committed-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Commit(ctx, "committed-1", "reuse-1", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// checkSnapshotterViewReadonly ensures a KindView snapshot to be mounted as a read-only filesystem.
|
|
// This function is called only when WithTestViewReadonly is true.
|
|
func checkSnapshotterViewReadonly(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing := filepath.Join(work, "preparing")
|
|
if _, err := snapshotter.Prepare(ctx, preparing, "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
committed := filepath.Join(work, "committed")
|
|
if err := snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
view := filepath.Join(work, "view")
|
|
m, err := snapshotter.View(ctx, view, committed, opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
viewMountPoint := filepath.Join(work, "view-mount")
|
|
if err := os.MkdirAll(viewMountPoint, 0777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Just checking the option string of m is not enough, we need to test real mount. (#1368)
|
|
if err := mount.All(m, viewMountPoint); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testfile := filepath.Join(viewMountPoint, "testfile")
|
|
if err := os.WriteFile(testfile, []byte("testcontent"), 0777); err != nil {
|
|
t.Logf("write to %q failed with %v (EROFS is expected but can be other error code)", testfile, err)
|
|
} else {
|
|
t.Fatalf("write to %q should fail (EROFS) but did not fail", testfile)
|
|
}
|
|
testutil.Unmount(t, viewMountPoint)
|
|
assert.Nil(t, snapshotter.Remove(ctx, view))
|
|
assert.Nil(t, snapshotter.Remove(ctx, committed))
|
|
}
|
|
|
|
// Move files from base layer to new location in intermediate layer.
|
|
// Verify if the file at source is deleted and copied to new location.
|
|
func checkFileFromLowerLayer(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
l1Init := fstest.Apply(
|
|
fstest.CreateDir("/dir1", 0700),
|
|
fstest.CreateFile("/dir1/f1", []byte("Hello"), 0644),
|
|
fstest.CreateDir("dir2", 0700),
|
|
fstest.CreateFile("dir2/f2", []byte("..."), 0644),
|
|
)
|
|
l2Init := fstest.Apply(
|
|
fstest.CreateDir("/dir3", 0700),
|
|
fstest.CreateFile("/dir3/f1", []byte("Hello"), 0644),
|
|
fstest.RemoveAll("/dir1"),
|
|
fstest.Link("dir2/f2", "dir3/f2"),
|
|
fstest.RemoveAll("dir2/f2"),
|
|
)
|
|
|
|
if err := checkSnapshots(ctx, snapshotter, work, l1Init, l2Init); err != nil {
|
|
t.Fatalf("Check snapshots failed: %+v", err)
|
|
}
|
|
}
|
|
|
|
func closeTwice(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
n := fmt.Sprintf("closeTwice-%d", randutil.Int())
|
|
prepare := fmt.Sprintf("%s-prepare", n)
|
|
|
|
// do some dummy ops to modify the snapshotter internal state
|
|
if _, err := snapshotter.Prepare(ctx, prepare, "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Commit(ctx, n, prepare, opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Remove(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Close(); err != nil {
|
|
t.Fatalf("The first close failed: %+v", err)
|
|
}
|
|
if err := snapshotter.Close(); err != nil {
|
|
t.Fatalf("The second close failed: %+v", err)
|
|
}
|
|
}
|
|
|
|
func checkRootPermission(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer testutil.Unmount(t, preparing)
|
|
st, err := os.Stat(preparing)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mode := st.Mode() & 0777; mode != 0755 {
|
|
t.Fatalf("expected 0755, got 0%o", mode)
|
|
}
|
|
}
|
|
|
|
func check128LayersMount(name string) func(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
return func(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
if name == "Aufs" {
|
|
t.Skip("aufs tests have issues with whiteouts here on some CI kernels")
|
|
}
|
|
lowestApply := fstest.Apply(
|
|
fstest.CreateFile("/bottom", []byte("way at the bottom\n"), 0777),
|
|
fstest.CreateFile("/overwriteme", []byte("FIRST!\n"), 0777),
|
|
fstest.CreateDir("/ADDHERE", 0755),
|
|
fstest.CreateDir("/ONLYME", 0755),
|
|
fstest.CreateFile("/ONLYME/bottom", []byte("bye!\n"), 0777),
|
|
)
|
|
|
|
appliers := []fstest.Applier{lowestApply}
|
|
for i := 1; i <= 127; i++ {
|
|
appliers = append(appliers, fstest.Apply(
|
|
fstest.CreateFile("/overwriteme", []byte(fmt.Sprintf("%d WAS HERE!\n", i)), 0777),
|
|
fstest.CreateFile(fmt.Sprintf("/ADDHERE/file-%d", i), []byte("same\n"), 0755),
|
|
fstest.RemoveAll("/ONLYME"),
|
|
fstest.CreateDir("/ONLYME", 0755),
|
|
fstest.CreateFile(fmt.Sprintf("/ONLYME/file-%d", i), []byte("only me!\n"), 0777),
|
|
))
|
|
}
|
|
|
|
flat := filepath.Join(work, "flat")
|
|
if err := os.MkdirAll(flat, 0777); err != nil {
|
|
t.Fatalf("failed to create flat dir(%s): %+v", flat, err)
|
|
}
|
|
|
|
// NOTE: add gc labels to avoid snapshots get removed by gc...
|
|
parent := ""
|
|
for i, applier := range appliers {
|
|
preparing := filepath.Join(work, fmt.Sprintf("prepare-layer-%d", i))
|
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
t.Fatalf("[layer %d] failed to create preparing dir(%s): %+v", i, preparing, err)
|
|
}
|
|
|
|
mounts, err := snapshotter.Prepare(ctx, preparing, parent, opt)
|
|
if err != nil {
|
|
t.Fatalf("[layer %d] failed to get mount info: %+v", i, err)
|
|
}
|
|
|
|
if err := mount.All(mounts, preparing); err != nil {
|
|
t.Fatalf("[layer %d] failed to mount on the target(%s): %+v", i, preparing, err)
|
|
}
|
|
|
|
if err := fstest.CheckDirectoryEqual(preparing, flat); err != nil {
|
|
testutil.Unmount(t, preparing)
|
|
t.Fatalf("[layer %d] preparing doesn't equal to flat before apply: %+v", i, err)
|
|
}
|
|
|
|
if err := applier.Apply(flat); err != nil {
|
|
testutil.Unmount(t, preparing)
|
|
t.Fatalf("[layer %d] failed to apply on flat dir: %+v", i, err)
|
|
}
|
|
|
|
if err = applier.Apply(preparing); err != nil {
|
|
testutil.Unmount(t, preparing)
|
|
t.Fatalf("[layer %d] failed to apply on preparing dir: %+v", i, err)
|
|
}
|
|
|
|
if err := fstest.CheckDirectoryEqual(preparing, flat); err != nil {
|
|
testutil.Unmount(t, preparing)
|
|
t.Fatalf("[layer %d] preparing doesn't equal to flat after apply: %+v", i, err)
|
|
}
|
|
|
|
testutil.Unmount(t, preparing)
|
|
|
|
parent = filepath.Join(work, fmt.Sprintf("committed-%d", i))
|
|
if err := snapshotter.Commit(ctx, parent, preparing, opt); err != nil {
|
|
t.Fatalf("[layer %d] failed to commit the preparing: %+v", i, err)
|
|
}
|
|
|
|
}
|
|
|
|
view := filepath.Join(work, "fullview")
|
|
if err := os.MkdirAll(view, 0777); err != nil {
|
|
t.Fatalf("failed to create fullview dir(%s): %+v", view, err)
|
|
}
|
|
|
|
mounts, err := snapshotter.View(ctx, view, parent, opt)
|
|
if err != nil {
|
|
t.Fatalf("failed to get view's mount info: %+v", err)
|
|
}
|
|
|
|
if err := mount.All(mounts, view); err != nil {
|
|
t.Fatalf("failed to mount on the target(%s): %+v", view, err)
|
|
}
|
|
defer testutil.Unmount(t, view)
|
|
|
|
if err := fstest.CheckDirectoryEqual(view, flat); err != nil {
|
|
t.Fatalf("fullview should equal to flat: %+v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkWalk(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
|
|
opt := snapshots.WithLabels(map[string]string{
|
|
"containerd.io/gc.root": "check-walk",
|
|
})
|
|
|
|
// No parent active
|
|
if _, err := snapshotter.Prepare(ctx, "a-np", "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Base parent
|
|
if _, err := snapshotter.Prepare(ctx, "p-tmp", "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Commit(ctx, "p", "p-tmp", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Active
|
|
if _, err := snapshotter.Prepare(ctx, "a", "p", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// View
|
|
if _, err := snapshotter.View(ctx, "v", "p", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Base parent with label=1
|
|
if _, err := snapshotter.Prepare(ctx, "p-wl-tmp", "", opt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := snapshotter.Commit(ctx, "p-wl", "p-wl-tmp", snapshots.WithLabels(map[string]string{
|
|
"l": "1",
|
|
"containerd.io/gc.root": "check-walk",
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// active with label=2
|
|
if _, err := snapshotter.Prepare(ctx, "a-wl", "p-wl", snapshots.WithLabels(map[string]string{
|
|
"l": "2",
|
|
"containerd.io/gc.root": "check-walk",
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// view with label=3
|
|
if _, err := snapshotter.View(ctx, "v-wl", "p-wl", snapshots.WithLabels(map[string]string{
|
|
"l": "3",
|
|
"containerd.io/gc.root": "check-walk",
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// no parent active with label=2
|
|
if _, err := snapshotter.Prepare(ctx, "a-np-wl", "", snapshots.WithLabels(map[string]string{
|
|
"l": "2",
|
|
"containerd.io/gc.root": "check-walk",
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for i, tc := range []struct {
|
|
matches []string
|
|
filters []string
|
|
}{
|
|
{
|
|
matches: []string{"a-np", "p", "a", "v", "p-wl", "a-wl", "v-wl", "a-np-wl"},
|
|
filters: []string{"labels.\"containerd.io/gc.root\"==check-walk"},
|
|
},
|
|
{
|
|
matches: []string{"a-np", "a", "a-wl", "a-np-wl"},
|
|
filters: []string{"kind==active,labels.\"containerd.io/gc.root\"==check-walk"},
|
|
},
|
|
{
|
|
matches: []string{"v", "v-wl"},
|
|
filters: []string{"kind==view,labels.\"containerd.io/gc.root\"==check-walk"},
|
|
},
|
|
{
|
|
matches: []string{"p", "p-wl"},
|
|
filters: []string{"kind==committed,labels.\"containerd.io/gc.root\"==check-walk"},
|
|
},
|
|
{
|
|
matches: []string{"p", "a-np-wl"},
|
|
filters: []string{"name==p", "name==a-np-wl"},
|
|
},
|
|
{
|
|
matches: []string{"a-wl"},
|
|
filters: []string{"name==a-wl,labels.l"},
|
|
},
|
|
{
|
|
matches: []string{"a", "v"},
|
|
filters: []string{"parent==p"},
|
|
},
|
|
{
|
|
matches: []string{"a", "v", "a-wl", "v-wl"},
|
|
filters: []string{"parent!=\"\",labels.\"containerd.io/gc.root\"==check-walk"},
|
|
},
|
|
{
|
|
matches: []string{"p-wl", "a-wl", "v-wl", "a-np-wl"},
|
|
filters: []string{"labels.l"},
|
|
},
|
|
{
|
|
matches: []string{"a-wl", "a-np-wl"},
|
|
filters: []string{"labels.l==2"},
|
|
},
|
|
} {
|
|
actual := []string{}
|
|
err := snapshotter.Walk(ctx, func(ctx context.Context, si snapshots.Info) error {
|
|
actual = append(actual, si.Name)
|
|
return nil
|
|
}, tc.filters...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sort.Strings(tc.matches)
|
|
sort.Strings(actual)
|
|
if len(actual) != len(tc.matches) {
|
|
t.Errorf("[%d] Unexpected result (size):\nActual:\n\t%#v\nExpected:\n\t%#v", i, actual, tc.matches)
|
|
continue
|
|
}
|
|
for j := range actual {
|
|
if actual[j] != tc.matches[j] {
|
|
t.Errorf("[%d] Unexpected result @%d:\nActual:\n\t%#vExpected:\n\t%#v", i, j, actual, tc.matches)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|