package walking import ( "context" "crypto/rand" "encoding/base64" "fmt" "io" "time" "github.com/containerd/containerd/archive" "github.com/containerd/containerd/archive/compression" "github.com/containerd/containerd/content" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) type walkingDiff struct { store content.Store } var emptyDesc = ocispec.Descriptor{} // NewWalkingDiff is a generic implementation of diff.Comparer. The diff is // calculated by mounting both the upper and lower mount sets and walking the // mounted directories concurrently. Changes are calculated by comparing files // against each other or by comparing file existence between directories. // NewWalkingDiff uses no special characteristics of the mount sets and is // expected to work with any filesystem. func NewWalkingDiff(store content.Store) diff.Comparer { return &walkingDiff{ store: store, } } // Compare creates a diff between the given mounts and uploads the result // to the content store. func (s *walkingDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) { var config diff.Config for _, opt := range opts { if err := opt(&config); err != nil { return emptyDesc, err } } if config.MediaType == "" { config.MediaType = ocispec.MediaTypeImageLayerGzip } var isCompressed bool switch config.MediaType { case ocispec.MediaTypeImageLayer: case ocispec.MediaTypeImageLayerGzip: isCompressed = true default: return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", config.MediaType) } var ocidesc ocispec.Descriptor if err := mount.WithTempMount(ctx, lower, func(lowerRoot string) error { return mount.WithTempMount(ctx, upper, func(upperRoot string) error { var newReference bool if config.Reference == "" { newReference = true config.Reference = uniqueRef() } cw, err := s.store.Writer(ctx, config.Reference, 0, "") if err != nil { return errors.Wrap(err, "failed to open writer") } defer func() { if err != nil { cw.Close() if newReference { if err := s.store.Abort(ctx, config.Reference); err != nil { log.G(ctx).WithField("ref", config.Reference).Warnf("failed to delete diff upload") } } } }() if !newReference { if err := cw.Truncate(0); err != nil { return err } } if isCompressed { dgstr := digest.SHA256.Digester() compressed, err := compression.CompressStream(cw, compression.Gzip) if err != nil { return errors.Wrap(err, "failed to get compressed stream") } err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash()), lowerRoot, upperRoot) compressed.Close() if err != nil { return errors.Wrap(err, "failed to write compressed diff") } if config.Labels == nil { config.Labels = map[string]string{} } config.Labels["containerd.io/uncompressed"] = dgstr.Digest().String() } else { if err = archive.WriteDiff(ctx, cw, lowerRoot, upperRoot); err != nil { return errors.Wrap(err, "failed to write diff") } } var commitopts []content.Opt if config.Labels != nil { commitopts = append(commitopts, content.WithLabels(config.Labels)) } dgst := cw.Digest() if err := cw.Commit(ctx, 0, dgst, commitopts...); err != nil { return errors.Wrap(err, "failed to commit") } info, err := s.store.Info(ctx, dgst) if err != nil { return errors.Wrap(err, "failed to get info from content store") } ocidesc = ocispec.Descriptor{ MediaType: config.MediaType, Size: info.Size, Digest: info.Digest, } return nil }) }); err != nil { return emptyDesc, err } return ocidesc, nil } func uniqueRef() string { t := time.Now() var b [3]byte // Ignore read failures, just decreases uniqueness rand.Read(b[:]) return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:])) }