mirror of
https://github.com/containerd/containerd.git
synced 2026-11-07 13:56:44 +00:00
Use the Linux default rather than the block size from the local macOS system. The local macOS block size is not relevant as the erofs file will not be mounted directly on macOS. Signed-off-by: Derek McGowan <derek@mcg.dev>
236 lines
6.3 KiB
Go
236 lines
6.3 KiB
Go
/*
|
|
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 erofs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/log"
|
|
digest "github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/diff"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/mount"
|
|
"github.com/containerd/containerd/v2/internal/erofsutils"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var emptyDesc = ocispec.Descriptor{}
|
|
|
|
type differ interface {
|
|
diff.Applier
|
|
diff.Comparer
|
|
}
|
|
|
|
// erofsDiff does erofs comparison and application
|
|
type erofsDiff struct {
|
|
store content.Store
|
|
mkfsExtraOpts []string
|
|
// enableTarIndex enables generating tar index for tar content
|
|
// instead of fully converting the tar to EROFS format
|
|
enableTarIndex bool
|
|
}
|
|
|
|
// DifferOpt is an option for configuring the erofs differ
|
|
type DifferOpt func(d *erofsDiff)
|
|
|
|
// WithMkfsOptions sets extra options for mkfs.erofs
|
|
func WithMkfsOptions(opts []string) DifferOpt {
|
|
return func(d *erofsDiff) {
|
|
d.mkfsExtraOpts = opts
|
|
}
|
|
}
|
|
|
|
// WithTarIndexMode enables tar index mode for EROFS layers
|
|
func WithTarIndexMode() DifferOpt {
|
|
return func(d *erofsDiff) {
|
|
d.enableTarIndex = true
|
|
}
|
|
}
|
|
|
|
// NewErofsDiffer creates a new EROFS differ with the provided options
|
|
func NewErofsDiffer(store content.Store, opts ...DifferOpt) differ {
|
|
d := &erofsDiff{
|
|
store: store,
|
|
}
|
|
|
|
// Apply all options
|
|
for _, opt := range opts {
|
|
opt(d)
|
|
}
|
|
|
|
// Add default block size on darwin if not already specified
|
|
d.mkfsExtraOpts = addDefaultMkfsOpts(d.mkfsExtraOpts)
|
|
|
|
return d
|
|
}
|
|
|
|
// A valid EROFS native layer media type should end with ".erofs".
|
|
//
|
|
// Please avoid using any +suffix to list the algorithms used inside EROFS
|
|
// blobs, since:
|
|
// - Each EROFS layer can use multiple compression algorithms;
|
|
// - The suffixes should only indicate the corresponding preprocessor for
|
|
// `images.DiffCompression`.
|
|
//
|
|
// Since `images.DiffCompression` doesn't support arbitrary media types,
|
|
// disallow non-empty suffixes for now.
|
|
func isErofsMediaType(mt string) bool {
|
|
mediaType, _, hasExt := strings.Cut(mt, "+")
|
|
if hasExt {
|
|
return false
|
|
}
|
|
return strings.HasSuffix(mediaType, ".erofs")
|
|
}
|
|
|
|
func (s erofsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts []mount.Mount, opts ...diff.ApplyOpt) (d ocispec.Descriptor, err error) {
|
|
t1 := time.Now()
|
|
defer func() {
|
|
if err == nil {
|
|
log.G(ctx).WithFields(log.Fields{
|
|
"d": time.Since(t1),
|
|
"digest": desc.Digest,
|
|
"size": desc.Size,
|
|
"media": desc.MediaType,
|
|
}).Debugf("diff applied")
|
|
}
|
|
}()
|
|
|
|
native := false
|
|
if isErofsMediaType(desc.MediaType) {
|
|
native = true
|
|
} else if _, err := images.DiffCompression(ctx, desc.MediaType); err != nil {
|
|
return emptyDesc, fmt.Errorf("currently unsupported media type: %s", desc.MediaType)
|
|
}
|
|
|
|
var config diff.ApplyConfig
|
|
for _, o := range opts {
|
|
if err := o(ctx, desc, &config); err != nil {
|
|
return emptyDesc, fmt.Errorf("failed to apply config opt: %w", err)
|
|
}
|
|
}
|
|
|
|
layer, err := erofsutils.MountsToLayer(mounts)
|
|
if err != nil {
|
|
return emptyDesc, err
|
|
}
|
|
|
|
ra, err := s.store.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
return emptyDesc, fmt.Errorf("failed to get reader from content store: %w", err)
|
|
}
|
|
defer ra.Close()
|
|
|
|
layerBlobPath := path.Join(layer, "layer.erofs")
|
|
if native {
|
|
f, err := os.Create(layerBlobPath)
|
|
if err != nil {
|
|
return emptyDesc, err
|
|
}
|
|
_, err = io.Copy(f, content.NewReader(ra))
|
|
f.Close()
|
|
if err != nil {
|
|
return emptyDesc, err
|
|
}
|
|
return desc, nil
|
|
}
|
|
|
|
processor := diff.NewProcessorChain(desc.MediaType, content.NewReader(ra))
|
|
for {
|
|
if processor, err = diff.GetProcessor(ctx, processor, config.ProcessorPayloads); err != nil {
|
|
return emptyDesc, fmt.Errorf("failed to get stream processor for %s: %w", desc.MediaType, err)
|
|
}
|
|
if processor.MediaType() == ocispec.MediaTypeImageLayer {
|
|
break
|
|
}
|
|
}
|
|
defer processor.Close()
|
|
|
|
digester := digest.Canonical.Digester()
|
|
rc := &readCounter{
|
|
r: io.TeeReader(processor, digester.Hash()),
|
|
}
|
|
|
|
// Choose between tar index or tar conversion mode
|
|
if s.enableTarIndex {
|
|
// Use the tar index method: generate tar index and append tar
|
|
err = erofsutils.GenerateTarIndexAndAppendTar(ctx, rc, layerBlobPath, s.mkfsExtraOpts)
|
|
if err != nil {
|
|
return emptyDesc, fmt.Errorf("failed to generate tar index: %w", err)
|
|
}
|
|
log.G(ctx).WithField("path", layerBlobPath).Debug("Applied layer using tar index mode")
|
|
} else {
|
|
// Use the tar method: fully convert tar to EROFS
|
|
u := uuid.NewSHA1(uuid.NameSpaceURL, []byte("erofs:blobs/"+desc.Digest))
|
|
err = erofsutils.ConvertTarErofs(ctx, rc, layerBlobPath, u.String(), s.mkfsExtraOpts)
|
|
if err != nil {
|
|
return emptyDesc, fmt.Errorf("failed to convert tar to erofs: %w", err)
|
|
}
|
|
log.G(ctx).WithField("path", layerBlobPath).Debug("Applied layer using tar conversion mode")
|
|
}
|
|
|
|
// Read any trailing data
|
|
if _, err := io.Copy(io.Discard, rc); err != nil {
|
|
return emptyDesc, err
|
|
}
|
|
|
|
return ocispec.Descriptor{
|
|
MediaType: ocispec.MediaTypeImageLayer,
|
|
Size: rc.c,
|
|
Digest: digester.Digest(),
|
|
}, nil
|
|
}
|
|
|
|
type readCounter struct {
|
|
r io.Reader
|
|
c int64
|
|
}
|
|
|
|
func (rc *readCounter) Read(p []byte) (n int, err error) {
|
|
n, err = rc.r.Read(p)
|
|
rc.c += int64(n)
|
|
return
|
|
}
|
|
|
|
// addDefaultMkfsOpts adds default options for mkfs.erofs
|
|
func addDefaultMkfsOpts(mkfsExtraOpts []string) []string {
|
|
if runtime.GOOS != "darwin" {
|
|
return mkfsExtraOpts
|
|
}
|
|
|
|
// Check if -b argument is already present
|
|
for _, opt := range mkfsExtraOpts {
|
|
if strings.HasPrefix(opt, "-b") {
|
|
return mkfsExtraOpts
|
|
}
|
|
}
|
|
|
|
// Add -b4096 as the first option to prevent unusable block
|
|
// size from being used on macOS.
|
|
return append([]string{"-b4096"}, mkfsExtraOpts...)
|
|
}
|