buildkit/snapshot/merge.go

205 lines
6.9 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
tryCrossSnapshotLink bool
// Whether the optimization of preparing on top of base layers is supported (see Merge method).
skipBaseLayers bool
// Whether we should use the "user.*" namespace when writing overlay xattrs. If false,
// "trusted.*" is used instead.
userxattr bool
}
func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager) MergeSnapshotter {
name := sn.Name()
_, tryCrossSnapshotLink := hardlinkMergeSnapshotters[name]
_, overlayBased := overlayBasedSnapshotters[name]
skipBaseLayers := overlayBased // default to skipping base layer for overlay-based snapshotters
var userxattr bool
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.
var err error
userxattr, err = needsUserXAttr(ctx, sn, lm)
if err != nil {
bklog.G(ctx).Debugf("failed to check user xattr: %v", err)
tryCrossSnapshotLink = false
skipBaseLayers = false
} else {
tryCrossSnapshotLink = userxattr
// Disable skipping base layers when in pre-5.11 rootless mode. Skipping the base layers
// necessitates the ability to set opaque xattrs sometimes, which only works in 5.11+
// kernels that support userxattr.
skipBaseLayers = userxattr
}
}
return &mergeSnapshotter{
Snapshotter: sn,
lm: lm,
tryCrossSnapshotLink: tryCrossSnapshotLink,
skipBaseLayers: skipBaseLayers,
userxattr: userxattr,
}
}
func (sn *mergeSnapshotter) Merge(ctx context.Context, key string, diffs []Diff, opts ...snapshots.Opt) error {
var baseKey string
if sn.skipBaseLayers {
// 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, i.e.:
// Diff("", A) -> Diff(A, B) -> Diff(B, C), etc.
var baseIndex int
for i, diff := range diffs {
var parentKey string
if diff.Upper != "" {
info, err := sn.Stat(ctx, diff.Upper)
if err != nil {
return err
}
parentKey = info.Parent
}
if parentKey != 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)
}
usage, err := sn.diffApply(ctx, applyMounts, diffs...)
if err != nil {
return errors.Wrap(err, "failed to apply diffs")
}
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
}