220 lines
7.4 KiB
Go
220 lines
7.4 KiB
Go
package snapshot
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
|
|
"github.com/containerd/containerd/leases"
|
|
"github.com/containerd/containerd/pkg/userns"
|
|
"github.com/containerd/containerd/snapshots"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/util/bklog"
|
|
"github.com/moby/buildkit/util/leaseutil"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// hardlinkMergeSnapshotters are the names of snapshotters that support merges implemented by
|
|
// creating "hardlink farms" where non-directory objects are hard-linked into the merged tree
|
|
// from their parent snapshots.
|
|
var hardlinkMergeSnapshotters = map[string]struct{}{
|
|
"native": {},
|
|
"overlayfs": {},
|
|
"stargz": {},
|
|
}
|
|
|
|
// overlayBasedSnapshotters are the names of snapshotter that use overlay mounts, which
|
|
// enables optimizations such as skipping the base layer when doing a hardlink merge.
|
|
var overlayBasedSnapshotters = map[string]struct{}{
|
|
"overlayfs": {},
|
|
"stargz": {},
|
|
}
|
|
|
|
type Diff struct {
|
|
Lower string
|
|
Upper string
|
|
}
|
|
|
|
type MergeSnapshotter interface {
|
|
Snapshotter
|
|
// Merge creates a snapshot whose contents are the provided diffs applied onto one
|
|
// another in the provided order, starting from scratch. The diffs are calculated
|
|
// the same way that diffs are calculated during exports, which ensures that the
|
|
// result of merging these diffs looks the same as exporting the diffs as layer
|
|
// blobs and unpacking them as an image.
|
|
//
|
|
// Each key in the provided diffs is expected to be a committed snapshot. The
|
|
// snapshot created by Merge is also committed.
|
|
//
|
|
// The size of a merged snapshot (as returned by the Usage method) depends on the merge
|
|
// implementation. Implementations using hardlinks to create merged views will take up
|
|
// less space than those that use copies, for example.
|
|
Merge(ctx context.Context, key string, diffs []Diff, opts ...snapshots.Opt) error
|
|
}
|
|
|
|
type mergeSnapshotter struct {
|
|
Snapshotter
|
|
lm leases.Manager
|
|
|
|
// Whether we should try to implement merges by hardlinking between underlying directories
|
|
useHardlinks bool
|
|
|
|
// Whether the snapshotter is overlay-based, which enables some required behavior like
|
|
// creation of whiteout devices to represent deletes in addition to some optimizations
|
|
// like using the first merge input as the parent snapshot.
|
|
overlayBased bool
|
|
}
|
|
|
|
func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager) MergeSnapshotter {
|
|
name := sn.Name()
|
|
_, useHardlinks := hardlinkMergeSnapshotters[name]
|
|
_, overlayBased := overlayBasedSnapshotters[name]
|
|
|
|
if overlayBased && userns.RunningInUserNS() {
|
|
// When using an overlay-based snapshotter, if we are running rootless on a pre-5.11
|
|
// kernel, we will not have userxattr. This results in opaque xattrs not being visible
|
|
// to us and thus breaking the overlay-optimized differ. This also means that there are
|
|
// cases where in order to apply a deletion, we'd need to create a whiteout device but
|
|
// may not have access to one to hardlink, so we just fall back to not using hardlinks
|
|
// at all in this case.
|
|
userxattr, err := needsUserXAttr(ctx, sn, lm)
|
|
if err != nil {
|
|
bklog.G(ctx).Debugf("failed to check user xattr: %v", err)
|
|
useHardlinks = false
|
|
} else {
|
|
useHardlinks = userxattr
|
|
}
|
|
}
|
|
|
|
return &mergeSnapshotter{
|
|
Snapshotter: sn,
|
|
lm: lm,
|
|
useHardlinks: useHardlinks,
|
|
overlayBased: overlayBased,
|
|
}
|
|
}
|
|
|
|
func (sn *mergeSnapshotter) Merge(ctx context.Context, key string, diffs []Diff, opts ...snapshots.Opt) error {
|
|
var baseKey string
|
|
if sn.overlayBased {
|
|
// Overlay-based snapshotters can skip the base snapshot of the merge (if one exists) and just use it as the
|
|
// parent of the merge snapshot. Other snapshotters will start empty (with baseKey set to "").
|
|
// Find the baseKey by following the chain of diffs for as long as it follows the pattern of the current lower
|
|
// being the parent of the current upper and equal to the previous upper.
|
|
var baseIndex int
|
|
for i, diff := range diffs {
|
|
info, err := sn.Stat(ctx, diff.Upper)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Parent != diff.Lower {
|
|
break
|
|
}
|
|
if diff.Lower != baseKey {
|
|
break
|
|
}
|
|
baseKey = diff.Upper
|
|
baseIndex = i + 1
|
|
}
|
|
diffs = diffs[baseIndex:]
|
|
}
|
|
|
|
tempLeaseCtx, done, err := leaseutil.WithLease(ctx, sn.lm, leaseutil.MakeTemporary)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create temporary lease for view mounts during merge")
|
|
}
|
|
defer done(context.TODO())
|
|
|
|
// Make the snapshot that will be merged into
|
|
prepareKey := identity.NewID()
|
|
if err := sn.Prepare(tempLeaseCtx, prepareKey, baseKey); err != nil {
|
|
return errors.Wrapf(err, "failed to prepare %q", key)
|
|
}
|
|
applyMounts, err := sn.Mounts(ctx, prepareKey)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get mounts of %q", key)
|
|
}
|
|
|
|
// externalHardlinks keeps track of which inodes have been hard-linked between snapshots (which is
|
|
// enabled when sn.useHardlinks is set to true)
|
|
externalHardlinks := make(map[uint64]struct{})
|
|
|
|
for _, diff := range diffs {
|
|
var lowerMounts Mountable
|
|
if diff.Lower != "" {
|
|
viewID := identity.NewID()
|
|
var err error
|
|
lowerMounts, err = sn.View(tempLeaseCtx, viewID, diff.Lower)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get mounts of lower %q", diff.Lower)
|
|
}
|
|
}
|
|
|
|
viewID := identity.NewID()
|
|
upperMounts, err := sn.View(tempLeaseCtx, viewID, diff.Upper)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get mounts of upper %q", diff.Upper)
|
|
}
|
|
|
|
err = diffApply(tempLeaseCtx, lowerMounts, upperMounts, applyMounts, sn.useHardlinks, externalHardlinks, sn.overlayBased)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// save the correctly calculated usage as a label on the committed key
|
|
usage, err := diskUsage(ctx, applyMounts, externalHardlinks)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get disk usage of diff apply merge")
|
|
}
|
|
if err := sn.Commit(ctx, key, prepareKey, withMergeUsage(usage)); err != nil {
|
|
return errors.Wrapf(err, "failed to commit %q", key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sn *mergeSnapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
|
|
// If key was created by Merge, we may need to use the annotated mergeUsage key as
|
|
// the snapshotter's usage method is wrong when hardlinks are used to create the merge.
|
|
if info, err := sn.Stat(ctx, key); err != nil {
|
|
return snapshots.Usage{}, err
|
|
} else if usage, ok, err := mergeUsageOf(info); err != nil {
|
|
return snapshots.Usage{}, err
|
|
} else if ok {
|
|
return usage, nil
|
|
}
|
|
return sn.Snapshotter.Usage(ctx, key)
|
|
}
|
|
|
|
// mergeUsage{Size,Inodes}Label hold the correct usage calculations for diffApplyMerges, for which the builtin usage
|
|
// is wrong because it can't account for hardlinks made across immutable snapshots
|
|
const mergeUsageSizeLabel = "buildkit.mergeUsageSize"
|
|
const mergeUsageInodesLabel = "buildkit.mergeUsageInodes"
|
|
|
|
func withMergeUsage(usage snapshots.Usage) snapshots.Opt {
|
|
return snapshots.WithLabels(map[string]string{
|
|
mergeUsageSizeLabel: strconv.Itoa(int(usage.Size)),
|
|
mergeUsageInodesLabel: strconv.Itoa(int(usage.Inodes)),
|
|
})
|
|
}
|
|
|
|
func mergeUsageOf(info snapshots.Info) (usage snapshots.Usage, ok bool, rerr error) {
|
|
if info.Labels == nil {
|
|
return snapshots.Usage{}, false, nil
|
|
}
|
|
if str, ok := info.Labels[mergeUsageSizeLabel]; ok {
|
|
i, err := strconv.Atoi(str)
|
|
if err != nil {
|
|
return snapshots.Usage{}, false, err
|
|
}
|
|
usage.Size = int64(i)
|
|
}
|
|
if str, ok := info.Labels[mergeUsageInodesLabel]; ok {
|
|
i, err := strconv.Atoi(str)
|
|
if err != nil {
|
|
return snapshots.Usage{}, false, err
|
|
}
|
|
usage.Inodes = int64(i)
|
|
}
|
|
return usage, true, nil
|
|
}
|