Move snapshots to core/snapshots
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
19
core/snapshots/benchsuite/benchmark.go
Normal file
19
core/snapshots/benchsuite/benchmark.go
Normal 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
|
||||
316
core/snapshots/benchsuite/benchmark_test.go
Normal file
316
core/snapshots/benchsuite/benchmark_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
191
core/snapshots/proxy/proxy.go
Normal file
191
core/snapshots/proxy/proxy.go
Normal 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)
|
||||
}
|
||||
464
core/snapshots/snapshotter.go
Normal file
464
core/snapshots/snapshotter.go
Normal 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
|
||||
}
|
||||
649
core/snapshots/storage/bolt.go
Normal file
649
core/snapshots/storage/bolt.go
Normal 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
|
||||
})
|
||||
}
|
||||
38
core/snapshots/storage/bolt_test.go
Normal file
38
core/snapshots/storage/bolt_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
157
core/snapshots/storage/metastore.go
Normal file
157
core/snapshots/storage/metastore.go
Normal 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()
|
||||
}
|
||||
220
core/snapshots/storage/metastore_bench_test.go
Normal file
220
core/snapshots/storage/metastore_bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
642
core/snapshots/storage/metastore_test.go
Normal file
642
core/snapshots/storage/metastore_test.go
Normal 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:]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
170
core/snapshots/testsuite/helpers.go
Normal file
170
core/snapshots/testsuite/helpers.go
Normal 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
|
||||
}
|
||||
265
core/snapshots/testsuite/issues.go
Normal file
265
core/snapshots/testsuite/issues.go
Normal 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
|
||||
1122
core/snapshots/testsuite/testsuite.go
Normal file
1122
core/snapshots/testsuite/testsuite.go
Normal file
File diff suppressed because it is too large
Load Diff
30
core/snapshots/testsuite/testsuite_unix.go
Normal file
30
core/snapshots/testsuite/testsuite_unix.go
Normal 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)
|
||||
}
|
||||
}
|
||||
21
core/snapshots/testsuite/testsuite_windows.go
Normal file
21
core/snapshots/testsuite/testsuite_windows.go
Normal 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() {}
|
||||
}
|
||||
Reference in New Issue
Block a user