Move snapshots to core/snapshots

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:54:09 -08:00
parent e0fe656daf
commit fcd39ccc53
72 changed files with 86 additions and 86 deletions

View File

@@ -23,9 +23,9 @@ import (
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/platforms"
"github.com/containerd/containerd/v2/snapshots"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/semaphore"

View File

@@ -24,8 +24,8 @@ import (
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/leases"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/snapshots"
)
func adaptImage(o interface{}) filters.Adaptor {

View File

@@ -28,11 +28,11 @@ import (
eventstypes "github.com/containerd/containerd/v2/api/events"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/gc"
"github.com/containerd/containerd/v2/namespaces"
"github.com/containerd/containerd/v2/pkg/cleanup"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/log"
bolt "go.etcd.io/bbolt"
)

View File

@@ -33,13 +33,13 @@ import (
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/leases"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/gc"
"github.com/containerd/containerd/v2/namespaces"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/containerd/containerd/v2/plugins/snapshots/native"
"github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/log/logtest"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"

View File

@@ -27,11 +27,11 @@ import (
eventstypes "github.com/containerd/containerd/v2/api/events"
"github.com/containerd/containerd/v2/core/metadata/boltutil"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/labels"
"github.com/containerd/containerd/v2/namespaces"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/log"
bolt "go.etcd.io/bbolt"
)

View File

@@ -28,13 +28,13 @@ import (
"time"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/testsuite"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/namespaces"
"github.com/containerd/containerd/v2/pkg/testutil"
"github.com/containerd/containerd/v2/plugins/snapshots/native"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/containerd/v2/snapshots/testsuite"
bolt "go.etcd.io/bbolt"
)

View File

@@ -0,0 +1,19 @@
//go:build linux
/*
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 benchsuite

View File

@@ -0,0 +1,316 @@
//go:build linux
/*
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 benchsuite
import (
"context"
"crypto/rand"
"flag"
"fmt"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/containerd/continuity/fs/fstest"
"github.com/stretchr/testify/assert"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/plugins/snapshots/devmapper"
"github.com/containerd/containerd/v2/plugins/snapshots/native"
"github.com/containerd/containerd/v2/plugins/snapshots/overlay"
"github.com/containerd/log"
)
var (
dmPoolDev string
dmRootPath string
overlayRootPath string
nativeRootPath string
)
func init() {
flag.StringVar(&dmPoolDev, "dm.thinPoolDev", "", "Pool device to run benchmark on")
flag.StringVar(&dmRootPath, "dm.rootPath", "", "Root dir for devmapper snapshotter")
flag.StringVar(&overlayRootPath, "overlay.rootPath", "", "Root dir for overlay snapshotter")
flag.StringVar(&nativeRootPath, "native.rootPath", "", "Root dir for native snapshotter")
// Avoid mixing benchmark output and INFO messages
if err := log.SetLevel("error"); err != nil {
panic(fmt.Sprintf("failed to set up log level: %v", err))
}
}
func BenchmarkNative(b *testing.B) {
if nativeRootPath == "" {
b.Skip("native root dir must be provided")
}
snapshotter, err := native.NewSnapshotter(nativeRootPath)
assert.Nil(b, err)
defer func() {
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(nativeRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
func BenchmarkOverlay(b *testing.B) {
if overlayRootPath == "" {
b.Skip("overlay root dir must be provided")
}
snapshotter, err := overlay.NewSnapshotter(overlayRootPath)
assert.Nil(b, err, "failed to create overlay snapshotter")
defer func() {
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(overlayRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
func BenchmarkDeviceMapper(b *testing.B) {
if dmPoolDev == "" {
b.Skip("devmapper benchmark requires thin-pool device to be prepared in advance and provided")
}
if dmRootPath == "" {
b.Skip("devmapper snapshotter root dir must be provided")
}
config := &devmapper.Config{
PoolName: dmPoolDev,
RootPath: dmRootPath,
BaseImageSize: "16Mb",
}
ctx := context.Background()
snapshotter, err := devmapper.NewSnapshotter(ctx, config)
assert.Nil(b, err)
defer func() {
err := snapshotter.ResetPool(ctx)
assert.Nil(b, err)
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(dmRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
// benchmarkSnapshotter tests snapshotter performance.
// It writes 16 layers with randomly created, modified, or removed files.
// Depending on layer index different sets of files are modified.
// In addition to total snapshotter execution time, benchmark outputs a few additional
// details - time taken to Prepare layer, mount, write data and unmount time,
// and Commit snapshot time.
func benchmarkSnapshotter(b *testing.B, snapshotter snapshots.Snapshotter) {
const (
layerCount = 16
fileSizeBytes = int64(1 * 1024 * 1024) // 1 MB
)
var (
total = 0
layers = make([]fstest.Applier, 0, layerCount)
layerIndex = int64(0)
)
for i := 1; i <= layerCount; i++ {
appliers := makeApplier(i, fileSizeBytes)
layers = append(layers, fstest.Apply(appliers...))
total += len(appliers)
}
var (
benchN int
prepareDuration time.Duration
writeDuration time.Duration
commitDuration time.Duration
)
// Wrap test with Run so additional details output will be added right below the benchmark result
b.Run("run", func(b *testing.B) {
var (
ctx = context.Background()
parent string
current string
)
// Reset durations since test might be ran multiple times
prepareDuration = 0
writeDuration = 0
commitDuration = 0
benchN = b.N
b.SetBytes(int64(total) * fileSizeBytes)
var timer time.Time
for i := 0; i < b.N; i++ {
for l := 0; l < layerCount; l++ {
current = fmt.Sprintf("prepare-layer-%d", atomic.AddInt64(&layerIndex, 1))
timer = time.Now()
mounts, err := snapshotter.Prepare(ctx, current, parent)
assert.Nil(b, err)
prepareDuration += time.Since(timer)
timer = time.Now()
err = mount.WithTempMount(ctx, mounts, layers[l].Apply)
assert.Nil(b, err)
writeDuration += time.Since(timer)
parent = fmt.Sprintf("committed-%d", atomic.AddInt64(&layerIndex, 1))
timer = time.Now()
err = snapshotter.Commit(ctx, parent, current)
assert.Nil(b, err)
commitDuration += time.Since(timer)
}
}
})
// Output extra measurements - total time taken to Prepare, mount and write data, and Commit
const outputFormat = "%-25s\t%s\n"
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/prepare",
testing.BenchmarkResult{N: benchN, T: prepareDuration})
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/write",
testing.BenchmarkResult{N: benchN, T: writeDuration})
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/commit",
testing.BenchmarkResult{N: benchN, T: commitDuration})
fmt.Fprintln(os.Stdout)
}
// makeApplier returns a slice of fstest.Applier where files are written randomly.
// Depending on layer index, the returned layers will overwrite some files with the
// same generated names with new contents or deletions.
func makeApplier(layerIndex int, fileSizeBytes int64) []fstest.Applier {
seed := time.Now().UnixNano()
switch {
case layerIndex%3 == 0:
return []fstest.Applier{
updateFile("/a"),
updateFile("/b"),
fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
updateFile("/d"),
fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
updateFile("/e"),
fstest.RemoveAll("/g"),
fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
updateFile("/i"),
fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
}
case layerIndex%2 == 0:
return []fstest.Applier{
updateFile("/a"),
fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
fstest.RemoveAll("/c"),
fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
updateFile("/e"),
fstest.RemoveAll("/f"),
fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
updateFile("/h"),
fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
updateFile("/j"),
}
default:
return []fstest.Applier{
fstest.CreateRandomFile("/a", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/e", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
}
}
}
// applierFn represents helper func that implements fstest.Applier
type applierFn func(root string) error
func (fn applierFn) Apply(root string) error {
return fn(root)
}
// updateFile modifies a few bytes in the middle in order to demonstrate the difference in performance
// for block-based snapshotters (like devicemapper) against file-based snapshotters (like overlay, which need to
// perform a copy-up of the full file any time a single bit is modified).
func updateFile(name string) applierFn {
return func(root string) error {
path := filepath.Join(root, name)
file, err := os.OpenFile(path, os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open %q: %w", path, err)
}
info, err := file.Stat()
if err != nil {
file.Close()
return err
}
var (
offset = info.Size() / 2
buf = make([]byte, 4)
)
if _, err := rand.Read(buf); err != nil {
file.Close()
return err
}
if _, err := file.WriteAt(buf, offset); err != nil {
file.Close()
return fmt.Errorf("failed to write %q at offset %d: %w", path, offset, err)
}
return file.Close()
}
}

View File

@@ -0,0 +1,191 @@
/*
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 proxy
import (
"context"
"io"
snapshotsapi "github.com/containerd/containerd/v2/api/services/snapshots/v1"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
protobuftypes "github.com/containerd/containerd/v2/protobuf/types"
)
// NewSnapshotter returns a new Snapshotter which communicates over a GRPC
// connection using the containerd snapshot GRPC API.
func NewSnapshotter(client snapshotsapi.SnapshotsClient, snapshotterName string) snapshots.Snapshotter {
return &proxySnapshotter{
client: client,
snapshotterName: snapshotterName,
}
}
type proxySnapshotter struct {
client snapshotsapi.SnapshotsClient
snapshotterName string
}
func (p *proxySnapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
resp, err := p.client.Stat(ctx,
&snapshotsapi.StatSnapshotRequest{
Snapshotter: p.snapshotterName,
Key: key,
})
if err != nil {
return snapshots.Info{}, errdefs.FromGRPC(err)
}
return snapshots.InfoFromProto(resp.Info), nil
}
func (p *proxySnapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
resp, err := p.client.Update(ctx,
&snapshotsapi.UpdateSnapshotRequest{
Snapshotter: p.snapshotterName,
Info: snapshots.InfoToProto(info),
UpdateMask: &protobuftypes.FieldMask{
Paths: fieldpaths,
},
})
if err != nil {
return snapshots.Info{}, errdefs.FromGRPC(err)
}
return snapshots.InfoFromProto(resp.Info), nil
}
func (p *proxySnapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
resp, err := p.client.Usage(ctx, &snapshotsapi.UsageRequest{
Snapshotter: p.snapshotterName,
Key: key,
})
if err != nil {
return snapshots.Usage{}, errdefs.FromGRPC(err)
}
return snapshots.UsageFromProto(resp), nil
}
func (p *proxySnapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
resp, err := p.client.Mounts(ctx, &snapshotsapi.MountsRequest{
Snapshotter: p.snapshotterName,
Key: key,
})
if err != nil {
return nil, errdefs.FromGRPC(err)
}
return mount.FromProto(resp.Mounts), nil
}
func (p *proxySnapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
var local snapshots.Info
for _, opt := range opts {
if err := opt(&local); err != nil {
return nil, err
}
}
resp, err := p.client.Prepare(ctx, &snapshotsapi.PrepareSnapshotRequest{
Snapshotter: p.snapshotterName,
Key: key,
Parent: parent,
Labels: local.Labels,
})
if err != nil {
return nil, errdefs.FromGRPC(err)
}
return mount.FromProto(resp.Mounts), nil
}
func (p *proxySnapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
var local snapshots.Info
for _, opt := range opts {
if err := opt(&local); err != nil {
return nil, err
}
}
resp, err := p.client.View(ctx, &snapshotsapi.ViewSnapshotRequest{
Snapshotter: p.snapshotterName,
Key: key,
Parent: parent,
Labels: local.Labels,
})
if err != nil {
return nil, errdefs.FromGRPC(err)
}
return mount.FromProto(resp.Mounts), nil
}
func (p *proxySnapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
var local snapshots.Info
for _, opt := range opts {
if err := opt(&local); err != nil {
return err
}
}
_, err := p.client.Commit(ctx, &snapshotsapi.CommitSnapshotRequest{
Snapshotter: p.snapshotterName,
Name: name,
Key: key,
Labels: local.Labels,
})
return errdefs.FromGRPC(err)
}
func (p *proxySnapshotter) Remove(ctx context.Context, key string) error {
_, err := p.client.Remove(ctx, &snapshotsapi.RemoveSnapshotRequest{
Snapshotter: p.snapshotterName,
Key: key,
})
return errdefs.FromGRPC(err)
}
func (p *proxySnapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
sc, err := p.client.List(ctx, &snapshotsapi.ListSnapshotsRequest{
Snapshotter: p.snapshotterName,
Filters: fs,
})
if err != nil {
return errdefs.FromGRPC(err)
}
for {
resp, err := sc.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return errdefs.FromGRPC(err)
}
if resp == nil {
return nil
}
for _, info := range resp.Info {
if err := fn(ctx, snapshots.InfoFromProto(info)); err != nil {
return err
}
}
}
}
func (p *proxySnapshotter) Close() error {
return nil
}
func (p *proxySnapshotter) Cleanup(ctx context.Context) error {
_, err := p.client.Cleanup(ctx, &snapshotsapi.CleanupRequest{
Snapshotter: p.snapshotterName,
})
return errdefs.FromGRPC(err)
}

View File

@@ -0,0 +1,464 @@
/*
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 snapshots
import (
"context"
"encoding/json"
"strings"
"time"
snapshotsapi "github.com/containerd/containerd/v2/api/services/snapshots/v1"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/protobuf"
)
const (
// UnpackKeyPrefix is the beginning of the key format used for snapshots that will have
// image content unpacked into them.
UnpackKeyPrefix = "extract"
// UnpackKeyFormat is the format for the snapshotter keys used for extraction
UnpackKeyFormat = UnpackKeyPrefix + "-%s %s"
inheritedLabelsPrefix = "containerd.io/snapshot/"
labelSnapshotRef = "containerd.io/snapshot.ref"
// LabelSnapshotUIDMapping is the label used for UID mappings
LabelSnapshotUIDMapping = "containerd.io/snapshot/uidmapping"
// LabelSnapshotGIDMapping is the label used for GID mappings
LabelSnapshotGIDMapping = "containerd.io/snapshot/gidmapping"
)
// Kind identifies the kind of snapshot.
type Kind uint8
// definitions of snapshot kinds
const (
KindUnknown Kind = iota
KindView
KindActive
KindCommitted
)
// ParseKind parses the provided string into a Kind
//
// If the string cannot be parsed KindUnknown is returned
func ParseKind(s string) Kind {
s = strings.ToLower(s)
switch s {
case "view":
return KindView
case "active":
return KindActive
case "committed":
return KindCommitted
}
return KindUnknown
}
// String returns the string representation of the Kind
func (k Kind) String() string {
switch k {
case KindView:
return "View"
case KindActive:
return "Active"
case KindCommitted:
return "Committed"
}
return "Unknown"
}
// MarshalJSON the Kind to JSON
func (k Kind) MarshalJSON() ([]byte, error) {
return json.Marshal(k.String())
}
// UnmarshalJSON the Kind from JSON
func (k *Kind) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
*k = ParseKind(s)
return nil
}
// KindToProto converts from [Kind] to the protobuf definition [snapshots.Kind].
func KindToProto(kind Kind) snapshotsapi.Kind {
if kind == KindActive {
return snapshotsapi.Kind_ACTIVE
}
if kind == KindView {
return snapshotsapi.Kind_VIEW
}
return snapshotsapi.Kind_COMMITTED
}
// KindFromProto converts from the protobuf definition [snapshots.Kind] to
// [Kind].
func KindFromProto(kind snapshotsapi.Kind) Kind {
if kind == snapshotsapi.Kind_ACTIVE {
return KindActive
}
if kind == snapshotsapi.Kind_VIEW {
return KindView
}
return KindCommitted
}
// Info provides information about a particular snapshot.
// JSON marshalling is supported for interacting with tools like ctr,
type Info struct {
Kind Kind // active or committed snapshot
Name string // name or key of snapshot
Parent string `json:",omitempty"` // name of parent snapshot
// Labels for a snapshot.
//
// Note: only labels prefixed with `containerd.io/snapshot/` will be inherited
// by the snapshotter's `Prepare`, `View`, or `Commit` calls.
Labels map[string]string `json:",omitempty"`
Created time.Time `json:",omitempty"` // Created time
Updated time.Time `json:",omitempty"` // Last update time
}
// InfoToProto converts from [Info] to the protobuf definition [snapshots.Info].
func InfoToProto(info Info) *snapshotsapi.Info {
return &snapshotsapi.Info{
Name: info.Name,
Parent: info.Parent,
Kind: KindToProto(info.Kind),
CreatedAt: protobuf.ToTimestamp(info.Created),
UpdatedAt: protobuf.ToTimestamp(info.Updated),
Labels: info.Labels,
}
}
// InfoFromProto converts from the protobuf definition [snapshots.Info] to
// [Info].
func InfoFromProto(info *snapshotsapi.Info) Info {
return Info{
Name: info.Name,
Parent: info.Parent,
Kind: KindFromProto(info.Kind),
Created: protobuf.FromTimestamp(info.CreatedAt),
Updated: protobuf.FromTimestamp(info.UpdatedAt),
Labels: info.Labels,
}
}
// Usage defines statistics for disk resources consumed by the snapshot.
//
// These resources only include the resources consumed by the snapshot itself
// and does not include resources usage by the parent.
type Usage struct {
Inodes int64 // number of inodes in use.
Size int64 // provides usage, in bytes, of snapshot
}
// Add the provided usage to the current usage
func (u *Usage) Add(other Usage) {
u.Size += other.Size
// TODO(stevvooe): assumes independent inodes, but provides an upper
// bound. This should be pretty close, assuming the inodes for a
// snapshot are roughly unique to it. Don't trust this assumption.
u.Inodes += other.Inodes
}
// UsageFromProto converts from the protobuf definition [snapshots.Usage] to
// [Usage].
func UsageFromProto(resp *snapshotsapi.UsageResponse) Usage {
return Usage{
Inodes: resp.Inodes,
Size: resp.Size,
}
}
// UsageToProto converts from [Usage] to the protobuf definition [snapshots.Usage].
func UsageToProto(usage Usage) *snapshotsapi.UsageResponse {
return &snapshotsapi.UsageResponse{
Inodes: usage.Inodes,
Size: usage.Size,
}
}
// WalkFunc defines the callback for a snapshot walk.
type WalkFunc func(context.Context, Info) error
// Snapshotter defines the methods required to implement a snapshot snapshotter for
// allocating, snapshotting and mounting filesystem changesets. The model works
// by building up sets of changes with parent-child relationships.
//
// A snapshot represents a filesystem state. Every snapshot has a parent, where
// the empty parent is represented by the empty string. A diff can be taken
// between a parent and its snapshot to generate a classic layer.
//
// An active snapshot is created by calling `Prepare`. After mounting, changes
// can be made to the snapshot. The act of committing creates a committed
// snapshot. The committed snapshot will get the parent of active snapshot. The
// committed snapshot can then be used as a parent. Active snapshots can never
// act as a parent.
//
// Snapshots are best understood by their lifecycle. Active snapshots are
// always created with Prepare or View. Committed snapshots are always created
// with Commit. Active snapshots never become committed snapshots and vice
// versa. All snapshots may be removed.
//
// For consistency, we define the following terms to be used throughout this
// interface for snapshotter implementations:
//
// `ctx` - refers to a context.Context
// `key` - refers to an active snapshot
// `name` - refers to a committed snapshot
// `parent` - refers to the parent in relation
//
// Most methods take various combinations of these identifiers. Typically,
// `name` and `parent` will be used in cases where a method *only* takes
// committed snapshots. `key` will be used to refer to active snapshots in most
// cases, except where noted. All variables used to access snapshots use the
// same key space. For example, an active snapshot may not share the same key
// with a committed snapshot.
//
// We cover several examples below to demonstrate the utility of the snapshotter.
//
// # Importing a Layer
//
// To import a layer, we simply have the snapshotter provide a list of
// mounts to be applied such that our dst will capture a changeset. We start
// out by getting a path to the layer tar file and creating a temp location to
// unpack it to:
//
// layerPath, tmpDir := getLayerPath(), mkTmpDir() // just a path to layer tar file.
//
// We start by using the snapshotter to Prepare a new snapshot transaction, using a
// key and descending from the empty parent "". To prevent our layer from being
// garbage collected during unpacking, we add the `containerd.io/gc.root` label:
//
// noGcOpt := snapshots.WithLabels(map[string]string{
// "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
// })
// mounts, err := snapshotter.Prepare(ctx, key, "", noGcOpt)
// if err != nil { ... }
//
// We get back a list of mounts from snapshotter.Prepare(), with the key identifying
// the active snapshot. Mount this to the temporary location with the
// following:
//
// if err := mount.All(mounts, tmpDir); err != nil { ... }
//
// Once the mounts are performed, our temporary location is ready to capture
// a diff. In practice, this works similar to a filesystem transaction. The
// next step is to unpack the layer. We have a special function unpackLayer
// that applies the contents of the layer to target location and calculates the
// DiffID of the unpacked layer (this is a requirement for docker
// implementation):
//
// layer, err := os.Open(layerPath)
// if err != nil { ... }
// digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location
// if err != nil { ... }
//
// When the above completes, we should have a filesystem that represents the
// contents of the layer. Careful implementations should verify that digest
// matches the expected DiffID. When completed, we unmount the mounts:
//
// unmount(mounts) // optional, for now
//
// Now that we've verified and unpacked our layer, we commit the active
// snapshot to a name. For this example, we are just going to use the layer
// digest, but in practice, this will probably be the ChainID. This also removes
// the active snapshot:
//
// if err := snapshotter.Commit(ctx, digest.String(), key, noGcOpt); err != nil { ... }
//
// Now, we have a layer in the snapshotter that can be accessed with the digest
// provided during commit.
//
// # Importing the Next Layer
//
// Making a layer depend on the above is identical to the process described
// above except that the parent is provided as parent when calling
// snapshotter.Prepare(), assuming a clean, unique key identifier:
//
// mounts, err := snapshotter.Prepare(ctx, key, parentDigest, noGcOpt)
//
// We then mount, apply and commit, as we did above. The new snapshot will be
// based on the content of the previous one.
//
// # Running a Container
//
// To run a container, we simply provide snapshotter.Prepare() the committed image
// snapshot as the parent. After mounting, the prepared path can
// be used directly as the container's filesystem:
//
// mounts, err := snapshotter.Prepare(ctx, containerKey, imageRootFSChainID)
//
// The returned mounts can then be passed directly to the container runtime. If
// one would like to create a new image from the filesystem, snapshotter.Commit() is
// called:
//
// if err := snapshotter.Commit(ctx, newImageSnapshot, containerKey); err != nil { ... }
//
// Alternatively, for most container runs, snapshotter.Remove() will be called to
// signal the snapshotter to abandon the changes.
type Snapshotter interface {
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
Stat(ctx context.Context, key string) (Info, error)
// Update updates the info for a snapshot.
//
// Only mutable properties of a snapshot may be updated.
Update(ctx context.Context, info Info, fieldpaths ...string) (Info, error)
// Usage returns the resource usage of an active or committed snapshot
// excluding the usage of parent snapshots.
//
// The running time of this call for active snapshots is dependent on
// implementation, but may be proportional to the size of the resource.
// Callers should take this into consideration. Implementations should
// attempt to honor context cancellation and avoid taking locks when making
// the calculation.
Usage(ctx context.Context, key string) (Usage, error)
// Mounts returns the mounts for the active snapshot transaction identified
// by key. Can be called on a read-write or readonly transaction. This is
// available only for active snapshots.
//
// This can be used to recover mounts after calling View or Prepare.
Mounts(ctx context.Context, key string) ([]mount.Mount, error)
// Prepare creates an active snapshot identified by key descending from the
// provided parent. The returned mounts can be used to mount the snapshot
// to capture changes.
//
// If a parent is provided, after performing the mounts, the destination
// will start with the content of the parent. The parent must be a
// committed snapshot. Changes to the mounted destination will be captured
// in relation to the parent. The default parent, "", is an empty
// directory.
//
// The changes may be saved to a committed snapshot by calling Commit. When
// one is done with the transaction, Remove should be called on the key.
//
// Multiple calls to Prepare or View with the same key should fail.
Prepare(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)
// View behaves identically to Prepare except the result may not be
// committed back to the snapshot snapshotter. View returns a readonly view on
// the parent, with the active snapshot being tracked by the given key.
//
// This method operates identically to Prepare, except the mounts returned
// may have the readonly flag set. Any modifications to the underlying
// filesystem will be ignored. Implementations may perform this in a more
// efficient manner that differs from what would be attempted with
// `Prepare`.
//
// Commit may not be called on the provided key and will return an error.
// To collect the resources associated with key, Remove must be called with
// key as the argument.
View(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)
// Commit captures the changes between key and its parent into a snapshot
// identified by name. The name can then be used with the snapshotter's other
// methods to create subsequent snapshots.
//
// A committed snapshot will be created under name with the parent of the
// active snapshot.
//
// After commit, the snapshot identified by key is removed.
Commit(ctx context.Context, name, key string, opts ...Opt) error
// Remove the committed or active snapshot by the provided key.
//
// All resources associated with the key will be removed.
//
// If the snapshot is a parent of another snapshot, its children must be
// removed before proceeding.
Remove(ctx context.Context, key string) error
// Walk will call the provided function for each snapshot in the
// snapshotter which match the provided filters. If no filters are
// given all items will be walked.
// Filters:
// name
// parent
// kind (active,view,committed)
// labels.(label)
Walk(ctx context.Context, fn WalkFunc, filters ...string) error
// Close releases the internal resources.
//
// Close is expected to be called on the end of the lifecycle of the snapshotter,
// but not mandatory.
//
// Close returns nil when it is already closed.
Close() error
}
// Cleaner defines a type capable of performing asynchronous resource cleanup.
// The Cleaner interface should be used by snapshotters which implement fast
// removal and deferred resource cleanup. This prevents snapshots from needing
// to perform lengthy resource cleanup before acknowledging a snapshot key
// has been removed and available for re-use. This is also useful when
// performing multi-key removal with the intent of cleaning up all the
// resources after each snapshot key has been removed.
type Cleaner interface {
Cleanup(ctx context.Context) error
}
// Opt allows setting mutable snapshot properties on creation
type Opt func(info *Info) error
// WithLabels appends labels to a created snapshot
func WithLabels(labels map[string]string) Opt {
return func(info *Info) error {
if info.Labels == nil {
info.Labels = make(map[string]string)
}
for k, v := range labels {
info.Labels[k] = v
}
return nil
}
}
// FilterInheritedLabels filters the provided labels by removing any key which
// isn't a snapshot label. Snapshot labels have a prefix of "containerd.io/snapshot/"
// or are the "containerd.io/snapshot.ref" label.
func FilterInheritedLabels(labels map[string]string) map[string]string {
if labels == nil {
return nil
}
filtered := make(map[string]string)
for k, v := range labels {
if k == labelSnapshotRef || strings.HasPrefix(k, inheritedLabelsPrefix) {
filtered[k] = v
}
}
return filtered
}

View File

@@ -0,0 +1,649 @@
/*
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 storage
import (
"context"
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/containerd/containerd/v2/core/metadata/boltutil"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/filters"
bolt "go.etcd.io/bbolt"
)
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeySnapshot = []byte("snapshots")
bucketKeyParents = []byte("parents")
bucketKeyID = []byte("id")
bucketKeyParent = []byte("parent")
bucketKeyKind = []byte("kind")
bucketKeyInodes = []byte("inodes")
bucketKeySize = []byte("size")
// ErrNoTransaction is returned when an operation is attempted with
// a context which is not inside of a transaction.
ErrNoTransaction = errors.New("no transaction in context")
)
// parentKey returns a composite key of the parent and child identifiers. The
// parts of the key are separated by a zero byte.
func parentKey(parent, child uint64) []byte {
b := make([]byte, binary.Size([]uint64{parent, child})+1)
i := binary.PutUvarint(b, parent)
j := binary.PutUvarint(b[i+1:], child)
return b[0 : i+j+1]
}
// parentPrefixKey returns the parent part of the composite key with the
// zero byte separator.
func parentPrefixKey(parent uint64) []byte {
b := make([]byte, binary.Size(parent)+1)
i := binary.PutUvarint(b, parent)
return b[0 : i+1]
}
// getParentPrefix returns the first part of the composite key which
// represents the parent identifier.
func getParentPrefix(b []byte) uint64 {
parent, _ := binary.Uvarint(b)
return parent
}
// GetInfo returns the snapshot Info directly from the metadata. Requires a
// context with a storage transaction.
func GetInfo(ctx context.Context, key string) (string, snapshots.Info, snapshots.Usage, error) {
var (
id uint64
su snapshots.Usage
si = snapshots.Info{
Name: key,
}
)
err := withSnapshotBucket(ctx, key, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
getUsage(bkt, &su)
return readSnapshot(bkt, &id, &si)
})
if err != nil {
return "", snapshots.Info{}, snapshots.Usage{}, err
}
return strconv.FormatUint(id, 10), si, su, nil
}
// UpdateInfo updates an existing snapshot info's data
func UpdateInfo(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
updated := snapshots.Info{
Name: info.Name,
}
err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(info.Name))
if sbkt == nil {
return fmt.Errorf("snapshot does not exist: %w", errdefs.ErrNotFound)
}
if err := readSnapshot(sbkt, nil, &updated); err != nil {
return err
}
if len(fieldpaths) > 0 {
for _, path := range fieldpaths {
if strings.HasPrefix(path, "labels.") {
if updated.Labels == nil {
updated.Labels = map[string]string{}
}
key := strings.TrimPrefix(path, "labels.")
updated.Labels[key] = info.Labels[key]
continue
}
switch path {
case "labels":
updated.Labels = info.Labels
default:
return fmt.Errorf("cannot update %q field on snapshot %q: %w", path, info.Name, errdefs.ErrInvalidArgument)
}
}
} else {
// Set mutable fields
updated.Labels = info.Labels
}
updated.Updated = time.Now().UTC()
if err := boltutil.WriteTimestamps(sbkt, updated.Created, updated.Updated); err != nil {
return err
}
return boltutil.WriteLabels(sbkt, updated.Labels)
})
if err != nil {
return snapshots.Info{}, err
}
return updated, nil
}
// WalkInfo iterates through all metadata Info for the stored snapshots and
// calls the provided function for each. Requires a context with a storage
// transaction.
func WalkInfo(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
filter, err := filters.ParseAll(fs...)
if err != nil {
return err
}
// TODO: allow indexes (name, parent, specific labels)
return withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
// skip non buckets
if v != nil {
return nil
}
var (
sbkt = bkt.Bucket(k)
si = snapshots.Info{
Name: string(k),
}
)
if err := readSnapshot(sbkt, nil, &si); err != nil {
return err
}
if !filter.Match(adaptSnapshot(si)) {
return nil
}
return fn(ctx, si)
})
})
}
// GetSnapshot returns the metadata for the active or view snapshot transaction
// referenced by the given key. Requires a context with a storage transaction.
func GetSnapshot(ctx context.Context, key string) (s Snapshot, err error) {
err = withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return fmt.Errorf("snapshot does not exist: %w", errdefs.ErrNotFound)
}
s.ID = strconv.FormatUint(readID(sbkt), 10)
s.Kind = readKind(sbkt)
if s.Kind != snapshots.KindActive && s.Kind != snapshots.KindView {
return fmt.Errorf("requested snapshot %v not active or view: %w", key, errdefs.ErrFailedPrecondition)
}
if parentKey := sbkt.Get(bucketKeyParent); len(parentKey) > 0 {
spbkt := bkt.Bucket(parentKey)
if spbkt == nil {
return fmt.Errorf("parent does not exist: %w", errdefs.ErrNotFound)
}
s.ParentIDs, err = parents(bkt, spbkt, readID(spbkt))
if err != nil {
return fmt.Errorf("failed to get parent chain: %w", err)
}
}
return nil
})
if err != nil {
return Snapshot{}, err
}
return
}
// CreateSnapshot inserts a record for an active or view snapshot with the provided parent.
func CreateSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) (s Snapshot, err error) {
switch kind {
case snapshots.KindActive, snapshots.KindView:
default:
return Snapshot{}, fmt.Errorf("snapshot type %v invalid; only snapshots of type Active or View can be created: %w", kind, errdefs.ErrInvalidArgument)
}
var base snapshots.Info
for _, opt := range opts {
if err := opt(&base); err != nil {
return Snapshot{}, err
}
}
err = createBucketIfNotExists(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
var (
spbkt *bolt.Bucket
)
if parent != "" {
spbkt = bkt.Bucket([]byte(parent))
if spbkt == nil {
return fmt.Errorf("missing parent %q bucket: %w", parent, errdefs.ErrNotFound)
}
if readKind(spbkt) != snapshots.KindCommitted {
return fmt.Errorf("parent %q is not committed snapshot: %w", parent, errdefs.ErrInvalidArgument)
}
}
sbkt, err := bkt.CreateBucket([]byte(key))
if err != nil {
if err == bolt.ErrBucketExists {
err = fmt.Errorf("snapshot %v: %w", key, errdefs.ErrAlreadyExists)
}
return err
}
id, err := bkt.NextSequence()
if err != nil {
return fmt.Errorf("unable to get identifier for snapshot %q: %w", key, err)
}
t := time.Now().UTC()
si := snapshots.Info{
Parent: parent,
Kind: kind,
Labels: base.Labels,
Created: t,
Updated: t,
}
if err := putSnapshot(sbkt, id, si); err != nil {
return err
}
if spbkt != nil {
pid := readID(spbkt)
// Store a backlink from the key to the parent. Store the snapshot name
// as the value to allow following the backlink to the snapshot value.
if err := pbkt.Put(parentKey(pid, id), []byte(key)); err != nil {
return fmt.Errorf("failed to write parent link for snapshot %q: %w", key, err)
}
s.ParentIDs, err = parents(bkt, spbkt, pid)
if err != nil {
return fmt.Errorf("failed to get parent chain for snapshot %q: %w", key, err)
}
}
s.ID = strconv.FormatUint(id, 10)
s.Kind = kind
return nil
})
if err != nil {
return Snapshot{}, err
}
return
}
// Remove removes a snapshot from the metastore. The string identifier for the
// snapshot is returned as well as the kind. The provided context must contain a
// writable transaction.
func Remove(ctx context.Context, key string) (string, snapshots.Kind, error) {
var (
id uint64
si snapshots.Info
)
if err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return fmt.Errorf("snapshot %v: %w", key, errdefs.ErrNotFound)
}
if err := readSnapshot(sbkt, &id, &si); err != nil {
return fmt.Errorf("failed to read snapshot %s: %w", key, err)
}
if pbkt != nil {
k, _ := pbkt.Cursor().Seek(parentPrefixKey(id))
if getParentPrefix(k) == id {
return fmt.Errorf("cannot remove snapshot with child: %w", errdefs.ErrFailedPrecondition)
}
if si.Parent != "" {
spbkt := bkt.Bucket([]byte(si.Parent))
if spbkt == nil {
return fmt.Errorf("snapshot %v: %w", key, errdefs.ErrNotFound)
}
if err := pbkt.Delete(parentKey(readID(spbkt), id)); err != nil {
return fmt.Errorf("failed to delete parent link: %w", err)
}
}
}
if err := bkt.DeleteBucket([]byte(key)); err != nil {
return fmt.Errorf("failed to delete snapshot: %w", err)
}
return nil
}); err != nil {
return "", 0, err
}
return strconv.FormatUint(id, 10), si.Kind, nil
}
// CommitActive renames the active snapshot transaction referenced by `key`
// as a committed snapshot referenced by `Name`. The resulting snapshot will be
// committed and readonly. The `key` reference will no longer be available for
// lookup or removal. The returned string identifier for the committed snapshot
// is the same identifier of the original active snapshot. The provided context
// must contain a writable transaction.
func CommitActive(ctx context.Context, key, name string, usage snapshots.Usage, opts ...snapshots.Opt) (string, error) {
var (
id uint64
base snapshots.Info
)
for _, opt := range opts {
if err := opt(&base); err != nil {
return "", err
}
}
if err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
dbkt, err := bkt.CreateBucket([]byte(name))
if err != nil {
if err == bolt.ErrBucketExists {
err = errdefs.ErrAlreadyExists
}
return fmt.Errorf("committed snapshot %v: %w", name, err)
}
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return fmt.Errorf("failed to get active snapshot %q: %w", key, errdefs.ErrNotFound)
}
var si snapshots.Info
if err := readSnapshot(sbkt, &id, &si); err != nil {
return fmt.Errorf("failed to read active snapshot %q: %w", key, err)
}
if si.Kind != snapshots.KindActive {
return fmt.Errorf("snapshot %q is not active: %w", key, errdefs.ErrFailedPrecondition)
}
si.Kind = snapshots.KindCommitted
si.Created = time.Now().UTC()
si.Updated = si.Created
// Replace labels, do not inherit
si.Labels = base.Labels
if err := putSnapshot(dbkt, id, si); err != nil {
return err
}
if err := putUsage(dbkt, usage); err != nil {
return err
}
if err := bkt.DeleteBucket([]byte(key)); err != nil {
return fmt.Errorf("failed to delete active snapshot %q: %w", key, err)
}
if si.Parent != "" {
spbkt := bkt.Bucket([]byte(si.Parent))
if spbkt == nil {
return fmt.Errorf("missing parent %q of snapshot %q: %w", si.Parent, key, errdefs.ErrNotFound)
}
pid := readID(spbkt)
// Updates parent back link to use new key
if err := pbkt.Put(parentKey(pid, id), []byte(name)); err != nil {
return fmt.Errorf("failed to update parent link %q from %q to %q: %w", pid, key, name, err)
}
}
return nil
}); err != nil {
return "", err
}
return strconv.FormatUint(id, 10), nil
}
// IDMap returns all the IDs mapped to their key
func IDMap(ctx context.Context) (map[string]string, error) {
m := map[string]string{}
if err := withBucket(ctx, func(ctx context.Context, bkt, _ *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
// skip non buckets
if v != nil {
return nil
}
id := readID(bkt.Bucket(k))
m[strconv.FormatUint(id, 10)] = string(k)
return nil
})
}); err != nil {
return nil, err
}
return m, nil
}
func withSnapshotBucket(ctx context.Context, key string, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
vbkt := tx.Bucket(bucketKeyStorageVersion)
if vbkt == nil {
return fmt.Errorf("bucket does not exist: %w", errdefs.ErrNotFound)
}
bkt := vbkt.Bucket(bucketKeySnapshot)
if bkt == nil {
return fmt.Errorf("snapshots bucket does not exist: %w", errdefs.ErrNotFound)
}
bkt = bkt.Bucket([]byte(key))
if bkt == nil {
return fmt.Errorf("snapshot does not exist: %w", errdefs.ErrNotFound)
}
return fn(ctx, bkt, vbkt.Bucket(bucketKeyParents))
}
func withBucket(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
bkt := tx.Bucket(bucketKeyStorageVersion)
if bkt == nil {
return fmt.Errorf("bucket does not exist: %w", errdefs.ErrNotFound)
}
return fn(ctx, bkt.Bucket(bucketKeySnapshot), bkt.Bucket(bucketKeyParents))
}
func createBucketIfNotExists(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
bkt, err := tx.CreateBucketIfNotExists(bucketKeyStorageVersion)
if err != nil {
return fmt.Errorf("failed to create version bucket: %w", err)
}
sbkt, err := bkt.CreateBucketIfNotExists(bucketKeySnapshot)
if err != nil {
return fmt.Errorf("failed to create snapshots bucket: %w", err)
}
pbkt, err := bkt.CreateBucketIfNotExists(bucketKeyParents)
if err != nil {
return fmt.Errorf("failed to create parents bucket: %w", err)
}
return fn(ctx, sbkt, pbkt)
}
func parents(bkt, pbkt *bolt.Bucket, parent uint64) (parents []string, err error) {
for {
parents = append(parents, strconv.FormatUint(parent, 10))
parentKey := pbkt.Get(bucketKeyParent)
if len(parentKey) == 0 {
return
}
pbkt = bkt.Bucket(parentKey)
if pbkt == nil {
return nil, fmt.Errorf("missing parent: %w", errdefs.ErrNotFound)
}
parent = readID(pbkt)
}
}
func readKind(bkt *bolt.Bucket) (k snapshots.Kind) {
kind := bkt.Get(bucketKeyKind)
if len(kind) == 1 {
k = snapshots.Kind(kind[0])
}
return
}
func readID(bkt *bolt.Bucket) uint64 {
id, _ := binary.Uvarint(bkt.Get(bucketKeyID))
return id
}
func readSnapshot(bkt *bolt.Bucket, id *uint64, si *snapshots.Info) error {
if id != nil {
*id = readID(bkt)
}
if si != nil {
si.Kind = readKind(bkt)
si.Parent = string(bkt.Get(bucketKeyParent))
if err := boltutil.ReadTimestamps(bkt, &si.Created, &si.Updated); err != nil {
return err
}
labels, err := boltutil.ReadLabels(bkt)
if err != nil {
return err
}
si.Labels = labels
}
return nil
}
func putSnapshot(bkt *bolt.Bucket, id uint64, si snapshots.Info) error {
idEncoded, err := encodeID(id)
if err != nil {
return err
}
updates := [][2][]byte{
{bucketKeyID, idEncoded},
{bucketKeyKind, []byte{byte(si.Kind)}},
}
if si.Parent != "" {
updates = append(updates, [2][]byte{bucketKeyParent, []byte(si.Parent)})
}
for _, v := range updates {
if err := bkt.Put(v[0], v[1]); err != nil {
return err
}
}
if err := boltutil.WriteTimestamps(bkt, si.Created, si.Updated); err != nil {
return err
}
return boltutil.WriteLabels(bkt, si.Labels)
}
func getUsage(bkt *bolt.Bucket, usage *snapshots.Usage) {
usage.Inodes, _ = binary.Varint(bkt.Get(bucketKeyInodes))
usage.Size, _ = binary.Varint(bkt.Get(bucketKeySize))
}
func putUsage(bkt *bolt.Bucket, usage snapshots.Usage) error {
for _, v := range []struct {
key []byte
value int64
}{
{bucketKeyInodes, usage.Inodes},
{bucketKeySize, usage.Size},
} {
e, err := encodeSize(v.value)
if err != nil {
return err
}
if err := bkt.Put(v.key, e); err != nil {
return err
}
}
return nil
}
func encodeSize(size int64) ([]byte, error) {
var (
buf [binary.MaxVarintLen64]byte
sizeEncoded = buf[:]
)
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, size)]
if len(sizeEncoded) == 0 {
return nil, fmt.Errorf("failed encoding size = %v", size)
}
return sizeEncoded, nil
}
func encodeID(id uint64) ([]byte, error) {
var (
buf [binary.MaxVarintLen64]byte
idEncoded = buf[:]
)
idEncoded = idEncoded[:binary.PutUvarint(idEncoded, id)]
if len(idEncoded) == 0 {
return nil, fmt.Errorf("failed encoding id = %v", id)
}
return idEncoded, nil
}
func adaptSnapshot(info snapshots.Info) filters.Adaptor {
return filters.AdapterFunc(func(fieldpath []string) (string, bool) {
if len(fieldpath) == 0 {
return "", false
}
switch fieldpath[0] {
case "kind":
switch info.Kind {
case snapshots.KindActive:
return "active", true
case snapshots.KindView:
return "view", true
case snapshots.KindCommitted:
return "committed", true
}
case "name":
return info.Name, true
case "parent":
return info.Parent, true
case "labels":
if len(info.Labels) == 0 {
return "", false
}
v, ok := info.Labels[strings.Join(fieldpath[1:], ".")]
return v, ok
}
return "", false
})
}

View File

@@ -0,0 +1,38 @@
/*
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 storage
import (
"path/filepath"
"testing"
// Does not require root but flag must be defined for snapshot tests
_ "github.com/containerd/containerd/v2/pkg/testutil"
)
func TestMetastore(t *testing.T) {
MetaStoreSuite(t, "Metastore", func(root string) (*MetaStore, error) {
return NewMetaStore(filepath.Join(root, "metadata.db"))
})
}
func BenchmarkSuite(b *testing.B) {
Benchmarks(b, "BoltDBBench", func(root string) (*MetaStore, error) {
return NewMetaStore(filepath.Join(root, "metadata.db"))
})
}

View File

@@ -0,0 +1,157 @@
/*
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 storage provides a metadata storage implementation for snapshot
// drivers. Drive implementations are responsible for starting and managing
// transactions using the defined context creator. This storage package uses
// BoltDB for storing metadata. Access to the raw boltdb transaction is not
// provided, but the stored object is provided by the proto subpackage.
package storage
import (
"context"
"errors"
"fmt"
"sync"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/log"
bolt "go.etcd.io/bbolt"
)
// Transactor is used to finalize an active transaction.
type Transactor interface {
// Commit commits any changes made during the transaction. On error a
// caller is expected to clean up any resources which would have relied
// on data mutated as part of this transaction. Only writable
// transactions can commit, non-writable must call Rollback.
Commit() error
// Rollback rolls back any changes made during the transaction. This
// must be called on all non-writable transactions and aborted writable
// transaction.
Rollback() error
}
// Snapshot hold the metadata for an active or view snapshot transaction. The
// ParentIDs hold the snapshot identifiers for the committed snapshots this
// active or view is based on. The ParentIDs are ordered from the lowest base
// to highest, meaning they should be applied in order from the first index to
// the last index. The last index should always be considered the active
// snapshots immediate parent.
type Snapshot struct {
Kind snapshots.Kind
ID string
ParentIDs []string
}
// MetaStore is used to store metadata related to a snapshot driver. The
// MetaStore is intended to store metadata related to name, state and
// parentage. Using the MetaStore is not required to implement a snapshot
// driver but can be used to handle the persistence and transactional
// complexities of a driver implementation.
type MetaStore struct {
dbfile string
dbL sync.Mutex
db *bolt.DB
}
// NewMetaStore returns a snapshot MetaStore for storage of metadata related to
// a snapshot driver backed by a bolt file database. This implementation is
// strongly consistent and does all metadata changes in a transaction to prevent
// against process crashes causing inconsistent metadata state.
func NewMetaStore(dbfile string) (*MetaStore, error) {
return &MetaStore{
dbfile: dbfile,
}, nil
}
type transactionKey struct{}
// TransactionContext creates a new transaction context. The writable value
// should be set to true for transactions which are expected to mutate data.
func (ms *MetaStore) TransactionContext(ctx context.Context, writable bool) (context.Context, Transactor, error) {
ms.dbL.Lock()
if ms.db == nil {
db, err := bolt.Open(ms.dbfile, 0600, nil)
if err != nil {
ms.dbL.Unlock()
return ctx, nil, fmt.Errorf("failed to open database file: %w", err)
}
ms.db = db
}
ms.dbL.Unlock()
tx, err := ms.db.Begin(writable)
if err != nil {
return ctx, nil, fmt.Errorf("failed to start transaction: %w", err)
}
ctx = context.WithValue(ctx, transactionKey{}, tx)
return ctx, tx, nil
}
// TransactionCallback represents a callback to be invoked while under a metastore transaction.
type TransactionCallback func(ctx context.Context) error
// WithTransaction is a convenience method to run a function `fn` while holding a meta store transaction.
// If the callback `fn` returns an error or the transaction is not writable, the database transaction will be discarded.
func (ms *MetaStore) WithTransaction(ctx context.Context, writable bool, fn TransactionCallback) error {
ctx, trans, err := ms.TransactionContext(ctx, writable)
if err != nil {
return err
}
var result []error
err = fn(ctx)
if err != nil {
result = append(result, err)
}
// Always rollback if transaction is not writable
if err != nil || !writable {
if terr := trans.Rollback(); terr != nil {
log.G(ctx).WithError(terr).Error("failed to rollback transaction")
result = append(result, fmt.Errorf("rollback failed: %w", terr))
}
} else {
if terr := trans.Commit(); terr != nil {
log.G(ctx).WithError(terr).Error("failed to commit transaction")
result = append(result, fmt.Errorf("commit failed: %w", terr))
}
}
if err := errors.Join(result...); err != nil {
log.G(ctx).WithError(err).Debug("snapshotter error")
return err
}
return nil
}
// Close closes the metastore and any underlying database connections
func (ms *MetaStore) Close() error {
ms.dbL.Lock()
defer ms.dbL.Unlock()
if ms.db == nil {
return nil
}
return ms.db.Close()
}

View File

@@ -0,0 +1,220 @@
/*
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 storage
import (
"context"
"fmt"
"testing"
"github.com/containerd/containerd/v2/core/snapshots"
)
// Benchmarks returns a benchmark suite using the provided metadata store
// creation method
func Benchmarks(b *testing.B, name string, metaFn metaFactory) {
b.Run("StatActive", makeBench(b, name, metaFn, statActiveBenchmark))
b.Run("StatCommitted", makeBench(b, name, metaFn, statCommittedBenchmark))
b.Run("CreateActive", makeBench(b, name, metaFn, createActiveBenchmark))
b.Run("Remove", makeBench(b, name, metaFn, removeBenchmark))
b.Run("Commit", makeBench(b, name, metaFn, commitBenchmark))
b.Run("GetActive", makeBench(b, name, metaFn, getActiveBenchmark))
b.Run("WriteTransaction", openCloseWritable(b, name, metaFn))
b.Run("ReadTransaction", openCloseReadonly(b, name, metaFn))
}
// makeBench creates a benchmark with a writable transaction
func makeBench(b *testing.B, name string, metaFn metaFactory, fn func(context.Context, *testing.B, *MetaStore)) func(b *testing.B) {
return func(b *testing.B) {
ctx := context.Background()
ms, err := metaFn(b.TempDir())
if err != nil {
b.Fatal(err)
}
b.Cleanup(func() {
ms.Close()
})
ctx, t, err := ms.TransactionContext(ctx, true)
if err != nil {
b.Fatal(err)
}
defer t.Commit()
b.ResetTimer()
fn(ctx, b, ms)
}
}
func openCloseWritable(b *testing.B, name string, metaFn metaFactory) func(b *testing.B) {
return func(b *testing.B) {
ctx := context.Background()
ms, err := metaFn(b.TempDir())
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, t, err := ms.TransactionContext(ctx, true)
if err != nil {
b.Fatal(err)
}
if err := t.Commit(); err != nil {
b.Fatal(err)
}
}
}
}
func openCloseReadonly(b *testing.B, name string, metaFn metaFactory) func(b *testing.B) {
return func(b *testing.B) {
ctx := context.Background()
ms, err := metaFn(b.TempDir())
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, t, err := ms.TransactionContext(ctx, false)
if err != nil {
b.Fatal(err)
}
if err := t.Rollback(); err != nil {
b.Fatal(err)
}
}
}
}
func createActiveFromBase(ctx context.Context, ms *MetaStore, active, base string) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "bottom", ""); err != nil {
return err
}
if _, err := CommitActive(ctx, "bottom", base, snapshots.Usage{}); err != nil {
return err
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, active, base)
return err
}
func statActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, err := GetInfo(ctx, "active")
if err != nil {
b.Fatal(err)
}
}
}
func statCommittedBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil {
b.Fatal(err)
}
if _, err := CommitActive(ctx, "active", "committed", snapshots.Usage{}); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, err := GetInfo(ctx, "committed")
if err != nil {
b.Fatal(err)
}
}
}
func createActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
for i := 0; i < b.N; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StopTimer()
if _, _, err := Remove(ctx, "active"); err != nil {
b.Fatal(err)
}
b.StartTimer()
}
}
func removeBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
for i := 0; i < b.N; i++ {
b.StopTimer()
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StartTimer()
if _, _, err := Remove(ctx, "active"); err != nil {
b.Fatal(err)
}
}
}
func commitBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
b.StopTimer()
for i := 0; i < b.N; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StartTimer()
if _, err := CommitActive(ctx, "active", "committed", snapshots.Usage{}); err != nil {
b.Fatal(err)
}
b.StopTimer()
if _, _, err := Remove(ctx, "committed"); err != nil {
b.Fatal(err)
}
}
}
func getActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
var base string
for i := 1; i <= 10; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "tmp", base); err != nil {
b.Fatalf("create active failed: %+v", err)
}
base = fmt.Sprintf("base-%d", i)
if _, err := CommitActive(ctx, "tmp", base, snapshots.Usage{}); err != nil {
b.Fatalf("commit failed: %+v", err)
}
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", base); err != nil {
b.Fatalf("create active failed: %+v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := GetSnapshot(ctx, "active"); err != nil {
b.Fatal(err)
}
}
}

View File

@@ -0,0 +1,642 @@
/*
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 storage
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/errdefs"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
type testFunc func(context.Context, *testing.T, *MetaStore)
type metaFactory func(string) (*MetaStore, error)
type populateFunc func(context.Context, *MetaStore) error
// MetaStoreSuite runs a test suite on the metastore given a factory function.
func MetaStoreSuite(t *testing.T, name string, meta func(root string) (*MetaStore, error)) {
t.Run("GetInfo", makeTest(t, name, meta, inReadTransaction(testGetInfo, basePopulate)))
t.Run("GetInfoNotExist", makeTest(t, name, meta, inReadTransaction(testGetInfoNotExist, basePopulate)))
t.Run("GetInfoEmptyDB", makeTest(t, name, meta, inReadTransaction(testGetInfoNotExist, nil)))
t.Run("Walk", makeTest(t, name, meta, inReadTransaction(testWalk, basePopulate)))
t.Run("GetSnapshot", makeTest(t, name, meta, testGetSnapshot))
t.Run("GetSnapshotNotExist", makeTest(t, name, meta, inReadTransaction(testGetSnapshotNotExist, basePopulate)))
t.Run("GetSnapshotCommitted", makeTest(t, name, meta, inReadTransaction(testGetSnapshotCommitted, basePopulate)))
t.Run("GetSnapshotEmptyDB", makeTest(t, name, meta, inReadTransaction(testGetSnapshotNotExist, basePopulate)))
t.Run("CreateActive", makeTest(t, name, meta, inWriteTransaction(testCreateActive)))
t.Run("CreateActiveNotExist", makeTest(t, name, meta, inWriteTransaction(testCreateActiveNotExist)))
t.Run("CreateActiveExist", makeTest(t, name, meta, inWriteTransaction(testCreateActiveExist)))
t.Run("CreateActiveFromActive", makeTest(t, name, meta, inWriteTransaction(testCreateActiveFromActive)))
t.Run("Commit", makeTest(t, name, meta, inWriteTransaction(testCommit)))
t.Run("CommitNotExist", makeTest(t, name, meta, inWriteTransaction(testCommitExist)))
t.Run("CommitExist", makeTest(t, name, meta, inWriteTransaction(testCommitExist)))
t.Run("CommitCommitted", makeTest(t, name, meta, inWriteTransaction(testCommitCommitted)))
t.Run("CommitViewFails", makeTest(t, name, meta, inWriteTransaction(testCommitViewFails)))
t.Run("Remove", makeTest(t, name, meta, inWriteTransaction(testRemove)))
t.Run("RemoveNotExist", makeTest(t, name, meta, inWriteTransaction(testRemoveNotExist)))
t.Run("RemoveWithChildren", makeTest(t, name, meta, inWriteTransaction(testRemoveWithChildren)))
t.Run("ParentIDs", makeTest(t, name, meta, inWriteTransaction(testParents)))
}
// makeTest creates a testsuite with a writable transaction
func makeTest(t *testing.T, name string, metaFn metaFactory, fn testFunc) func(t *testing.T) {
return func(t *testing.T) {
ctx := context.Background()
ms, err := metaFn(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ms.Close()
})
fn(ctx, t, ms)
}
}
func inReadTransaction(fn testFunc, pf populateFunc) testFunc {
return func(ctx context.Context, t *testing.T, ms *MetaStore) {
if pf != nil {
ctx, tx, err := ms.TransactionContext(ctx, true)
if err != nil {
t.Fatal(err)
}
if err := pf(ctx, ms); err != nil {
if rerr := tx.Rollback(); rerr != nil {
t.Logf("Rollback failed: %+v", rerr)
}
t.Fatalf("Populate failed: %+v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("Populate commit failed: %+v", err)
}
}
ctx, tx, err := ms.TransactionContext(ctx, false)
if err != nil {
t.Fatalf("Failed start transaction: %+v", err)
}
defer func() {
if err := tx.Rollback(); err != nil {
t.Logf("Rollback failed: %+v", err)
if !t.Failed() {
t.FailNow()
}
}
}()
fn(ctx, t, ms)
}
}
func inWriteTransaction(fn testFunc) testFunc {
return func(ctx context.Context, t *testing.T, ms *MetaStore) {
ctx, tx, err := ms.TransactionContext(ctx, true)
if err != nil {
t.Fatalf("Failed to start transaction: %+v", err)
}
defer func() {
if t.Failed() {
if err := tx.Rollback(); err != nil {
t.Logf("Rollback failed: %+v", err)
}
} else {
if err := tx.Commit(); err != nil {
t.Fatalf("Commit failed: %+v", err)
}
}
}()
fn(ctx, t, ms)
}
}
// basePopulate creates 7 snapshots
// - "committed-1": committed without parent
// - "committed-2": committed with parent "committed-1"
// - "active-1": active without parent
// - "active-2": active with parent "committed-1"
// - "active-3": active with parent "committed-2"
// - "active-4": readonly active without parent"
// - "active-5": readonly active with parent "committed-2"
func basePopulate(ctx context.Context, ms *MetaStore) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-1", ""); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CommitActive(ctx, "committed-tmp-1", "committed-1", snapshots.Usage{Size: 1}); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-2", "committed-1"); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CommitActive(ctx, "committed-tmp-2", "committed-2", snapshots.Usage{Size: 2}); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", ""); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-2", "committed-1"); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-3", "committed-2"); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", ""); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-2"); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
return nil
}
var baseInfo = map[string]snapshots.Info{
"committed-1": {
Name: "committed-1",
Parent: "",
Kind: snapshots.KindCommitted,
},
"committed-2": {
Name: "committed-2",
Parent: "committed-1",
Kind: snapshots.KindCommitted,
},
"active-1": {
Name: "active-1",
Parent: "",
Kind: snapshots.KindActive,
},
"active-2": {
Name: "active-2",
Parent: "committed-1",
Kind: snapshots.KindActive,
},
"active-3": {
Name: "active-3",
Parent: "committed-2",
Kind: snapshots.KindActive,
},
"view-1": {
Name: "view-1",
Parent: "",
Kind: snapshots.KindView,
},
"view-2": {
Name: "view-2",
Parent: "committed-2",
Kind: snapshots.KindView,
},
}
func assertNotExist(t *testing.T, err error) {
t.Helper()
assert.True(t, errdefs.IsNotFound(err), "got %+v", err)
}
func assertNotActive(t *testing.T, err error) {
t.Helper()
assert.True(t, errdefs.IsFailedPrecondition(err), "got %+v", err)
}
func assertNotCommitted(t *testing.T, err error) {
t.Helper()
assert.True(t, errdefs.IsInvalidArgument(err), "got %+v", err)
}
func assertExist(t *testing.T, err error) {
t.Helper()
assert.True(t, errdefs.IsAlreadyExists(err), "got %+v", err)
}
func testGetInfo(ctx context.Context, t *testing.T, _ *MetaStore) {
for key, expected := range baseInfo {
_, info, _, err := GetInfo(ctx, key)
assert.Nil(t, err, "on key %v", key)
assert.Truef(t, cmp.Equal(expected, info, cmpSnapshotInfo), "on key %v", key)
}
}
// compare snapshot.Info Updated and Created fields by checking they are
// within a threshold of time.Now()
var cmpSnapshotInfo = cmp.FilterPath(
func(path cmp.Path) bool {
field := path.Last().String()
return field == ".Created" || field == ".Updated"
},
cmp.Comparer(func(expected, actual time.Time) bool {
// cmp.Options must be symmetric, so swap the args
if actual.IsZero() {
actual, expected = expected, actual
}
if !expected.IsZero() {
return false
}
// actual value should be within a few seconds of now
now := time.Now()
delta := now.Sub(actual)
threshold := 30 * time.Second
return delta > -threshold && delta < threshold
}))
func testGetInfoNotExist(ctx context.Context, t *testing.T, _ *MetaStore) {
_, _, _, err := GetInfo(ctx, "active-not-exist")
assertNotExist(t, err)
}
func testWalk(ctx context.Context, t *testing.T, _ *MetaStore) {
found := map[string]snapshots.Info{}
err := WalkInfo(ctx, func(ctx context.Context, info snapshots.Info) error {
if _, ok := found[info.Name]; ok {
return errors.New("entry already encountered")
}
found[info.Name] = info
return nil
})
assert.NoError(t, err)
assert.True(t, cmp.Equal(baseInfo, found, cmpSnapshotInfo))
}
func testGetSnapshot(ctx context.Context, t *testing.T, ms *MetaStore) {
snapshotMap := map[string]Snapshot{}
populate := func(ctx context.Context, ms *MetaStore) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-1", ""); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
if _, err := CommitActive(ctx, "committed-tmp-1", "committed-1", snapshots.Usage{}); err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
for _, opts := range []struct {
Kind snapshots.Kind
Name string
Parent string
}{
{
Name: "active-1",
Kind: snapshots.KindActive,
},
{
Name: "active-2",
Parent: "committed-1",
Kind: snapshots.KindActive,
},
{
Name: "view-1",
Kind: snapshots.KindView,
},
{
Name: "view-2",
Parent: "committed-1",
Kind: snapshots.KindView,
},
} {
active, err := CreateSnapshot(ctx, opts.Kind, opts.Name, opts.Parent)
if err != nil {
return fmt.Errorf("failed to create active: %w", err)
}
snapshotMap[opts.Name] = active
}
return nil
}
test := func(ctx context.Context, t *testing.T, ms *MetaStore) {
for key, expected := range snapshotMap {
s, err := GetSnapshot(ctx, key)
assert.Nil(t, err, "failed to get snapshot %s", key)
assert.Equalf(t, expected, s, "on key %s", key)
}
}
inReadTransaction(test, populate)(ctx, t, ms)
}
func testGetSnapshotCommitted(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := GetSnapshot(ctx, "committed-1")
assertNotActive(t, err)
}
func testGetSnapshotNotExist(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := GetSnapshot(ctx, "active-not-exist")
assertNotExist(t, err)
}
func testCreateActive(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
if a1.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
a2, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", "")
if err != nil {
t.Fatal(err)
}
if a2.ID == a1.ID {
t.Fatal("Returned active identifiers must be unique")
}
if a2.Kind != snapshots.KindView {
t.Fatal("Expected a view")
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
a3, err := CreateSnapshot(ctx, snapshots.KindActive, "active-3", "committed-1")
if err != nil {
t.Fatal(err)
}
if a3.ID == a1.ID {
t.Fatal("Returned active identifiers must be unique")
}
if len(a3.ParentIDs) != 1 {
t.Fatalf("Expected 1 parent, got %d", len(a3.ParentIDs))
}
if a3.ParentIDs[0] != commitID {
t.Fatal("Expected active parent to be same as commit ID")
}
if a3.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
a4, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-1")
if err != nil {
t.Fatal(err)
}
if a4.ID == a1.ID {
t.Fatal("Returned active identifiers must be unique")
}
if len(a3.ParentIDs) != 1 {
t.Fatalf("Expected 1 parent, got %d", len(a3.ParentIDs))
}
if a3.ParentIDs[0] != commitID {
t.Fatal("Expected active parent to be same as commit ID")
}
if a4.Kind != snapshots.KindView {
t.Fatal("Expected a view")
}
}
func testCreateActiveExist(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
assertExist(t, err)
_, err = CreateSnapshot(ctx, snapshots.KindActive, "committed-1", "")
assertExist(t, err)
}
func testCreateActiveNotExist(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "does-not-exist")
assertNotExist(t, err)
}
func testCreateActiveFromActive(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-new", "active-1")
assertNotCommitted(t, err)
}
func testCommit(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
if a1.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
_, err = GetSnapshot(ctx, "active-1")
assertNotExist(t, err)
_, err = GetSnapshot(ctx, "committed-1")
assertNotActive(t, err)
}
func testCommitExist(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
assertExist(t, err)
}
func testCommitCommitted(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "committed-1", "committed-3", snapshots.Usage{})
assertNotActive(t, err)
}
func testCommitViewFails(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "view-1", "committed-3", snapshots.Usage{})
if err == nil {
t.Fatal("Expected error committing readonly active")
}
}
func testRemove(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
a2, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", "committed-1")
if err != nil {
t.Fatal(err)
}
a3, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-1")
if err != nil {
t.Fatal(err)
}
_, _, err = Remove(ctx, "active-1")
assertNotExist(t, err)
r3, k3, err := Remove(ctx, "view-2")
if err != nil {
t.Fatal(err)
}
if r3 != a3.ID {
t.Fatal("Expected remove ID to match create ID")
}
if k3 != snapshots.KindView {
t.Fatalf("Expected view kind, got %v", k3)
}
r2, k2, err := Remove(ctx, "view-1")
if err != nil {
t.Fatal(err)
}
if r2 != a2.ID {
t.Fatal("Expected remove ID to match create ID")
}
if k2 != snapshots.KindView {
t.Fatalf("Expected view kind, got %v", k2)
}
r1, k1, err := Remove(ctx, "committed-1")
if err != nil {
t.Fatal(err)
}
if r1 != commitID {
t.Fatal("Expected remove ID to match commit ID")
}
if k1 != snapshots.KindCommitted {
t.Fatalf("Expected committed kind, got %v", k1)
}
}
func testRemoveWithChildren(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, _, err := Remove(ctx, "committed-1")
if err == nil {
t.Fatalf("Expected removal of snapshot with children to error")
}
_, _, err = Remove(ctx, "committed-1")
if err == nil {
t.Fatalf("Expected removal of snapshot with children to error")
}
}
func testRemoveNotExist(ctx context.Context, t *testing.T, _ *MetaStore) {
_, _, err := Remove(ctx, "does-not-exist")
assertNotExist(t, err)
}
func testParents(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
testcases := []struct {
Name string
Parents int
}{
{"committed-1", 0},
{"committed-2", 1},
{"active-1", 0},
{"active-2", 1},
{"active-3", 2},
{"view-1", 0},
{"view-2", 2},
}
for _, tc := range testcases {
name := tc.Name
expectedID := ""
expectedParents := []string{}
for i := tc.Parents; i >= 0; i-- {
sid, info, _, err := GetInfo(ctx, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
var (
id string
parents []string
)
if info.Kind == snapshots.KindCommitted {
// When committed, create view and resolve from view
nid := fmt.Sprintf("test-%s-%d", tc.Name, i)
s, err := CreateSnapshot(ctx, snapshots.KindView, nid, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
if len(s.ParentIDs) != i+1 {
t.Fatalf("Unexpected number of parents for view of %s: %d, expected %d", name, len(s.ParentIDs), i+1)
}
id = s.ParentIDs[0]
parents = s.ParentIDs[1:]
} else {
s, err := GetSnapshot(ctx, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
if len(s.ParentIDs) != i {
t.Fatalf("Unexpected number of parents for %s: %d, expected %d", name, len(s.ParentIDs), i)
}
id = s.ID
parents = s.ParentIDs
}
if sid != id {
t.Fatalf("Info ID mismatched resolved snapshot ID for %s, %s vs %s", name, sid, id)
}
if expectedID != "" {
if id != expectedID {
t.Errorf("Unexpected ID of parent: %s, expected %s", id, expectedID)
}
}
if len(expectedParents) > 0 {
for j := range expectedParents {
if parents[j] != expectedParents[j] {
t.Errorf("Unexpected ID in parent array at %d: %s, expected %s", j, parents[j], expectedParents[j])
}
}
}
if i > 0 {
name = info.Parent
expectedID = parents[0]
expectedParents = parents[1:]
}
}
}
}

View File

@@ -0,0 +1,170 @@
/*
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"
"fmt"
"os"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/pkg/randutil"
"github.com/containerd/continuity/fs/fstest"
)
const umountflags int = 0
func applyToMounts(m []mount.Mount, work string, a fstest.Applier) (err error) {
td, err := os.MkdirTemp(work, "prepare")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(td)
if err := mount.All(m, td); err != nil {
return fmt.Errorf("failed to mount: %w", err)
}
defer func() {
if err1 := mount.UnmountAll(td, umountflags); err1 != nil && err == nil {
err = fmt.Errorf("failed to unmount: %w", err1)
}
}()
return a.Apply(td)
}
// createSnapshot creates a new snapshot in the snapshotter
// given an applier to run on top of the given parent.
func createSnapshot(ctx context.Context, sn snapshots.Snapshotter, parent, work string, a fstest.Applier) (string, error) {
n := fmt.Sprintf("%p-%d", a, randutil.Int())
prepare := fmt.Sprintf("%s-prepare", n)
m, err := sn.Prepare(ctx, prepare, parent, opt)
if err != nil {
return "", fmt.Errorf("failed to prepare snapshot: %w", err)
}
if err := applyToMounts(m, work, a); err != nil {
return "", fmt.Errorf("failed to apply: %w", err)
}
if err := sn.Commit(ctx, n, prepare, opt); err != nil {
return "", fmt.Errorf("failed to commit: %w", err)
}
return n, nil
}
func checkSnapshot(ctx context.Context, sn snapshots.Snapshotter, work, name, check string) (err error) {
td, err := os.MkdirTemp(work, "check")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer func() {
if err1 := os.RemoveAll(td); err1 != nil && err == nil {
err = fmt.Errorf("failed to remove temporary directory %s: %w", td, err1)
}
}()
view := fmt.Sprintf("%s-view", name)
m, err := sn.View(ctx, view, name, opt)
if err != nil {
return fmt.Errorf("failed to create view: %w", err)
}
defer func() {
if err1 := sn.Remove(ctx, view); err1 != nil && err == nil {
err = fmt.Errorf("failed to remove view: %w", err1)
}
}()
if err := mount.All(m, td); err != nil {
return fmt.Errorf("failed to mount: %w", err)
}
defer func() {
if err1 := mount.UnmountAll(td, umountflags); err1 != nil && err == nil {
err = fmt.Errorf("failed to unmount view: %w", err1)
}
}()
if err := fstest.CheckDirectoryEqual(check, td); err != nil {
return fmt.Errorf("check directory failed: %w", err)
}
return nil
}
// checkSnapshots creates a new chain of snapshots in the given snapshotter
// using the provided appliers, checking each snapshot created in a view
// against the changes applied to a single directory.
func checkSnapshots(ctx context.Context, sn snapshots.Snapshotter, work string, as ...fstest.Applier) error {
td, err := os.MkdirTemp(work, "flat")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(td)
var parentID string
for i, a := range as {
s, err := createSnapshot(ctx, sn, parentID, work, a)
if err != nil {
return fmt.Errorf("failed to create snapshot %d: %w", i+1, err)
}
if err := a.Apply(td); err != nil {
return fmt.Errorf("failed to apply to check directory on %d: %w", i+1, err)
}
if err := checkSnapshot(ctx, sn, work, s, td); err != nil {
return fmt.Errorf("snapshot check failed on snapshot %d: %w", i+1, err)
}
parentID = s
}
return nil
}
// checkInfo checks that the infos are the same
func checkInfo(si1, si2 snapshots.Info) error {
if si1.Kind != si2.Kind {
return fmt.Errorf("expected kind %v, got %v", si1.Kind, si2.Kind)
}
if si1.Name != si2.Name {
return fmt.Errorf("expected name %v, got %v", si1.Name, si2.Name)
}
if si1.Parent != si2.Parent {
return fmt.Errorf("expected Parent %v, got %v", si1.Parent, si2.Parent)
}
if len(si1.Labels) != len(si2.Labels) {
return fmt.Errorf("expected %d labels, got %d", len(si1.Labels), len(si2.Labels))
}
for k, l1 := range si1.Labels {
l2 := si2.Labels[k]
if l1 != l2 {
return fmt.Errorf("expected label %v, got %v", l1, l2)
}
}
if si1.Created != si2.Created {
return fmt.Errorf("expected Created %v, got %v", si1.Created, si2.Created)
}
if si1.Updated != si2.Updated {
return fmt.Errorf("expected Updated %v, got %v", si1.Updated, si2.Updated)
}
return nil
}

View File

@@ -0,0 +1,265 @@
/*
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"
"fmt"
"runtime"
"strings"
"testing"
"time"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/continuity/fs/fstest"
)
// Checks which cover former issues found in older layering models.
//
// NOTE: In older models, applying with tar was used to create read only layers,
// however with the snapshot model read only layers are created just using
// mounts and commits. Read write layers are a separate type of snapshot which
// is not committed, avoiding any confusion in the snapshotter about whether
// a snapshot will be mutated in the future.
// checkLayerFileUpdate tests the update of a single file in an upper layer
// Cause of issue was originally related to tar, snapshot should be able to
// avoid such issues by not relying on tar to create layers.
// See https://github.com/docker/docker/issues/21555
func checkLayerFileUpdate(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/etc", 0700),
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
)
l2Init := fstest.Apply(
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.2"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0666),
fstest.CreateDir("/root", 0700),
fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
)
var sleepTime time.Duration
// run 5 times to account for sporadic failure
for i := 0; i < 5; i++ {
time.Sleep(sleepTime)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
// Sleep until next second boundary before running again
nextTime := time.Now()
sleepTime = time.Unix(nextTime.Unix()+1, 0).Sub(nextTime)
}
}
// checkRemoveDirectoryInLowerLayer
// See https://github.com/docker/docker/issues/25244
func checkRemoveDirectoryInLowerLayer(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/lib", 0700),
fstest.CreateFile("/lib/hidden", []byte{}, 0644),
)
l2Init := fstest.Apply(
fstest.RemoveAll("/lib"),
fstest.CreateDir("/lib", 0700),
fstest.CreateFile("/lib/not-hidden", []byte{}, 0644),
)
l3Init := fstest.Apply(
fstest.CreateFile("/lib/newfile", []byte{}, 0644),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init, l3Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkChown
// See https://github.com/docker/docker/issues/20240 aufs
// See https://github.com/docker/docker/issues/24913 overlay
// see https://github.com/docker/docker/issues/28391 overlay2
func checkChown(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
if runtime.GOOS == "windows" {
t.Skip("Chown is not supported on Windows")
}
l1Init := fstest.Apply(
fstest.CreateDir("/opt", 0700),
fstest.CreateDir("/opt/a", 0700),
fstest.CreateDir("/opt/a/b", 0700),
fstest.CreateFile("/opt/a/b/file.txt", []byte("hello"), 0644),
)
l2Init := fstest.Apply(
fstest.Chown("/opt", 1, 1),
fstest.Chown("/opt/a", 1, 1),
fstest.Chown("/opt/a/b", 1, 1),
fstest.Chown("/opt/a/b/file.txt", 1, 1),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkRename
// https://github.com/docker/docker/issues/25409
func checkRename(ss string) func(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
return func(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateDir("/somefiles", 0700),
fstest.CreateFile("/somefiles/f1", []byte("was here first!"), 0644),
fstest.CreateFile("/somefiles/f2", []byte("nothing interesting"), 0644),
)
var applier []fstest.Applier
switch ss {
// With neither OVERLAY_FS_REDIRECT_DIR nor redirect_dir,
// renaming the directory on the lower directory doesn't work on overlayfs.
// https://github.com/torvalds/linux/blob/v5.18/Documentation/filesystems/overlayfs.rst#renaming-directories
//
// It doesn't work on fuse-overlayfs either.
// https://github.com/containerd/fuse-overlayfs-snapshotter/pull/53#issuecomment-1543442048
case "overlayfs", "fuse-overlayfs":
// NOP
default:
applier = append(applier, fstest.Rename("/dir1", "/dir2"))
}
applier = append(
applier,
fstest.CreateFile("/somefiles/f1-overwrite", []byte("new content 1"), 0644),
fstest.Rename("/somefiles/f1-overwrite", "/somefiles/f1"),
fstest.Rename("/somefiles/f2", "/somefiles/f3"),
)
l2Init := fstest.Apply(applier...)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
}
// checkDirectoryPermissionOnCommit
// https://github.com/docker/docker/issues/27298
func checkDirectoryPermissionOnCommit(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
if runtime.GOOS == "windows" {
t.Skip("Chown is not supported on WCOW")
}
l1Init := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateDir("/dir2", 0700),
fstest.CreateDir("/dir3", 0700),
fstest.CreateDir("/dir4", 0700),
fstest.CreateFile("/dir4/f1", []byte("..."), 0644),
fstest.CreateDir("/dir5", 0700),
fstest.CreateFile("/dir5/f1", []byte("..."), 0644),
fstest.Chown("/dir1", 1, 1),
fstest.Chown("/dir2", 1, 1),
fstest.Chown("/dir3", 1, 1),
fstest.Chown("/dir5", 1, 1),
fstest.Chown("/dir5/f1", 1, 1),
)
l2Init := fstest.Apply(
fstest.Chown("/dir2", 0, 0),
fstest.RemoveAll("/dir3"),
fstest.Chown("/dir4", 1, 1),
fstest.Chown("/dir4/f1", 1, 1),
)
l3Init := fstest.Apply(
fstest.CreateDir("/dir3", 0700),
fstest.Chown("/dir3", 1, 1),
fstest.RemoveAll("/dir5"),
fstest.CreateDir("/dir5", 0700),
fstest.Chown("/dir5", 1, 1),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init, l3Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkStatInWalk ensures that a stat can be called during a walk
func checkStatInWalk(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
prefix := "stats-in-walk-"
if err := createNamedSnapshots(ctx, sn, prefix); err != nil {
t.Fatal(err)
}
err := sn.Walk(ctx, func(ctx context.Context, si snapshots.Info) error {
if !strings.HasPrefix(si.Name, prefix) {
// Only stat snapshots from this test
return nil
}
si2, err := sn.Stat(ctx, si.Name)
if err != nil {
return err
}
return checkInfo(si, si2)
})
if err != nil {
t.Fatal(err)
}
}
func createNamedSnapshots(ctx context.Context, snapshotter snapshots.Snapshotter, ns string) error {
c1 := fmt.Sprintf("%sc1", ns)
c2 := fmt.Sprintf("%sc2", ns)
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, fmt.Sprintf("%sa1", ns), c2, opt); err != nil {
return err
}
if _, err := snapshotter.View(ctx, fmt.Sprintf("%sv1", ns), c2, opt); err != nil {
return err
}
return nil
}
// More issues to test
//
// checkRemoveAfterCommit
// See https://github.com/docker/docker/issues/24309
//
// checkUnixDomainSockets
// See https://github.com/docker/docker/issues/12080
//
// checkDirectoryInodeStability
// See https://github.com/docker/docker/issues/19647
//
// checkOpenFileInodeStability
// See https://github.com/docker/docker/issues/12327
//
// checkGetCWD
// See https://github.com/docker/docker/issues/19082
//
// checkChmod
// See https://github.com/docker/machine/issues/3327
//
// checkRemoveInWalk
// Allow mutations during walk without deadlocking

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
//go:build !windows
/*
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 (
"syscall"
)
func clearMask() func() {
oldumask := syscall.Umask(0)
return func() {
syscall.Umask(oldumask)
}
}

View File

@@ -0,0 +1,21 @@
/*
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
func clearMask() func() {
return func() {}
}