Update erofs to compile on Darwin

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2025-09-02 15:15:34 -07:00
parent ee8ae9d569
commit 1adaf27c1e
10 changed files with 904 additions and 714 deletions

View File

@@ -19,7 +19,9 @@
package builtins
import (
_ "github.com/containerd/containerd/v2/plugins/diff/erofs/plugin"
_ "github.com/containerd/containerd/v2/plugins/diff/walking/plugin"
_ "github.com/containerd/containerd/v2/plugins/snapshots/blockfile/plugin"
_ "github.com/containerd/containerd/v2/plugins/snapshots/erofs/plugin"
_ "github.com/containerd/containerd/v2/plugins/snapshots/native/plugin"
)

View File

@@ -26,9 +26,10 @@ import (
"path/filepath"
"strings"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/containerd/v2/core/mount"
)
func ConvertTarErofs(ctx context.Context, r io.Reader, layerPath, uuid string, mkfsExtraOpts []string) error {

View File

@@ -22,10 +22,7 @@ import (
"encoding/base64"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/containerd/continuity/fs"
@@ -36,82 +33,14 @@ import (
"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/containerd/containerd/v2/pkg/archive"
"github.com/containerd/containerd/v2/pkg/archive/compression"
"github.com/containerd/containerd/v2/pkg/epoch"
"github.com/containerd/containerd/v2/pkg/labels"
"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)
}
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 writeDiff(ctx context.Context, w io.Writer, lower []mount.Mount, upperRoot string) error {
var opts []archive.ChangeWriterOpt
@@ -260,115 +189,6 @@ func (s erofsDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts
}, nil
}
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
}
func uniqueRef() string {
t := time.Now()
var b [3]byte

View File

@@ -0,0 +1,35 @@
//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 erofs
import (
"context"
"github.com/containerd/errdefs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/containerd/containerd/v2/core/diff"
"github.com/containerd/containerd/v2/core/mount"
)
// Compare creates a diff between the given mounts and uploads the result
// to the content store.
func (s erofsDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) {
return emptyDesc, errdefs.ErrNotImplemented
}

View File

@@ -0,0 +1,213 @@
/*
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"
"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)
}
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
}

View File

@@ -19,13 +19,14 @@ package plugin
import (
"fmt"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/internal/erofsutils"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/diff/erofs"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
// Config represents configuration for the erofs plugin.
@@ -60,7 +61,9 @@ func init() {
return nil, err
}
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
p := platforms.DefaultSpec()
p.OS = "linux"
ic.Meta.Platforms = append(ic.Meta.Platforms, p)
cs := md.(*metadata.DB).ContentStore()
config := ic.Config.(*Config)

View File

@@ -0,0 +1,561 @@
/*
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"
"os"
"path/filepath"
"runtime"
"github.com/containerd/continuity/fs"
"github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/storage"
"github.com/containerd/containerd/v2/internal/fsverity"
)
// SnapshotterConfig is used to configure the erofs snapshotter instance
type SnapshotterConfig struct {
// ovlOptions are the base options added to the overlayfs mount (defaults to [""])
ovlOptions []string
// enableFsverity enables fsverity for EROFS layers
enableFsverity bool
// setImmutable enables IMMUTABLE_FL file attribute for EROFS layers
setImmutable bool
}
// Opt is an option to configure the erofs snapshotter
type Opt func(config *SnapshotterConfig)
// WithOvlOptions defines the extra mount options for overlayfs
func WithOvlOptions(options []string) Opt {
return func(config *SnapshotterConfig) {
config.ovlOptions = options
}
}
// WithFsverity enables fsverity for EROFS layers
func WithFsverity() Opt {
return func(config *SnapshotterConfig) {
config.enableFsverity = true
}
}
// WithImmutable enables IMMUTABLE_FL file attribute for EROFS layers
func WithImmutable() Opt {
return func(config *SnapshotterConfig) {
config.setImmutable = true
}
}
type MetaStore interface {
TransactionContext(ctx context.Context, writable bool) (context.Context, storage.Transactor, error)
WithTransaction(ctx context.Context, writable bool, fn storage.TransactionCallback) error
Close() error
}
type snapshotter struct {
root string
ms *storage.MetaStore
ovlOptions []string
enableFsverity bool
setImmutable bool
}
// NewSnapshotter returns a Snapshotter which uses EROFS+OverlayFS. The layers
// are stored under the provided root. A metadata file is stored under the root.
func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
var config SnapshotterConfig
for _, opt := range opts {
opt(&config)
}
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
if err := checkCompatibility(root); err != nil {
return nil, err
}
// Check fsverity support if enabled
if config.enableFsverity {
// TODO: Call specific function here
supported, err := fsverity.IsSupported(root)
if err != nil {
return nil, fmt.Errorf("failed to check fsverity support on %q: %w", root, err)
}
if !supported {
return nil, fmt.Errorf("fsverity is not supported on the filesystem of %q", root)
}
}
if config.setImmutable && runtime.GOOS != "linux" {
return nil, fmt.Errorf("setting IMMUTABLE_FL is only supported on Linux")
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
return nil, err
}
return &snapshotter{
root: root,
ms: ms,
ovlOptions: config.ovlOptions,
enableFsverity: config.enableFsverity,
setImmutable: config.setImmutable,
}, nil
}
// Close closes the snapshotter
func (s *snapshotter) Close() error {
return s.ms.Close()
}
func (s *snapshotter) upperPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "fs")
}
func (s *snapshotter) workPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "work")
}
// A committed layer blob generated by the EROFS differ
func (s *snapshotter) layerBlobPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "layer.erofs")
}
func (s *snapshotter) lowerPath(id string) (string, error) {
layerBlob := s.layerBlobPath(id)
if _, err := os.Stat(layerBlob); err != nil {
return "", fmt.Errorf("failed to find valid erofs layer blob: %w", err)
}
return layerBlob, nil
}
func (s *snapshotter) prepareDirectory(ctx context.Context, snapshotDir string, kind snapshots.Kind) (string, error) {
td, err := os.MkdirTemp(snapshotDir, "new-")
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %w", err)
}
if err := os.Mkdir(filepath.Join(td, "fs"), 0755); err != nil {
return td, err
}
if kind == snapshots.KindActive {
if err := os.Mkdir(filepath.Join(td, "work"), 0711); err != nil {
return td, err
}
}
// Create a special file for the EROFS differ to indicate it will be
// prepared as an EROFS layer by the EROFS snapshotter.
if err := os.WriteFile(filepath.Join(td, ".erofslayer"), []byte{}, 0644); err != nil {
return td, err
}
return td, nil
}
func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.Mount, error) {
var options []string
if len(snap.ParentIDs) == 0 {
if layerBlob, err := s.lowerPath(snap.ID); err == nil {
if snap.Kind != snapshots.KindView {
return nil, fmt.Errorf("only works for snapshots.KindView on a committed snapshot: %w", err)
}
if s.enableFsverity {
if err := s.verifyFsverity(layerBlob); err != nil {
return nil, err
}
}
return []mount.Mount{
{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
},
}, nil
}
// if we only have one layer/no parents then just return a bind mount as overlay
// will not work
roFlag := "rw"
if snap.Kind == snapshots.KindView {
roFlag = "ro"
}
return []mount.Mount{
{
Source: s.upperPath(snap.ID),
Type: "bind",
Options: append(options,
roFlag,
"rbind",
),
},
}, nil
}
if snap.Kind == snapshots.KindActive {
options = append(options,
fmt.Sprintf("workdir=%s", s.workPath(snap.ID)),
fmt.Sprintf("upperdir=%s", s.upperPath(snap.ID)),
)
} else if len(snap.ParentIDs) == 1 {
layerBlob, err := s.lowerPath(snap.ParentIDs[0])
if err != nil {
return nil, err
}
return []mount.Mount{
{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
},
}, nil
}
var mounts []mount.Mount
for i := range snap.ParentIDs {
layerBlob, err := s.lowerPath(snap.ParentIDs[i])
if err != nil {
return nil, err
}
m := mount.Mount{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
}
mounts = append(mounts, m)
}
options = append(options, fmt.Sprintf("lowerdir={{ overlay 0 %d }}", len(mounts)-1))
options = append(options, s.ovlOptions...)
return append(mounts, mount.Mount{
Type: "format/overlay",
Source: "overlay",
Options: options,
}), nil
}
func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var (
snap storage.Snapshot
td, path string
info snapshots.Info
)
defer func() {
if err != nil {
if td != "" {
if err1 := os.RemoveAll(td); err1 != nil {
log.G(ctx).WithError(err1).Warn("failed to cleanup temp snapshot directory")
}
}
if path != "" {
if err1 := os.RemoveAll(path); err1 != nil {
log.G(ctx).WithError(err1).WithField("path", path).Error("failed to reclaim snapshot directory, directory may need removal")
err = fmt.Errorf("failed to remove path: %v: %w", err1, err)
}
}
}
}()
if err := s.ms.WithTransaction(ctx, true, func(ctx context.Context) (err error) {
snapshotDir := filepath.Join(s.root, "snapshots")
td, err = s.prepareDirectory(ctx, snapshotDir, kind)
if err != nil {
return fmt.Errorf("failed to create prepare snapshot dir: %w", err)
}
snap, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
_, info, _, err = storage.GetInfo(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot info: %w", err)
}
if len(snap.ParentIDs) > 0 {
if err := upperDirectoryPermission(filepath.Join(td, "fs"), s.upperPath(snap.ParentIDs[0])); err != nil {
return err
}
}
path = filepath.Join(snapshotDir, snap.ID)
if err = os.Rename(td, path); err != nil {
return fmt.Errorf("failed to rename: %w", err)
}
td = ""
return nil
}); err != nil {
return nil, err
}
return s.mounts(snap, info)
}
func (s *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (s *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
var layerBlob string
// Apply the overlayfs upperdir (generated by non-EROFS differs) into a EROFS blob
// in a read transaction first since conversion could be slow.
err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, _, _, err := storage.GetInfo(ctx, key)
if err != nil {
return err
}
// If the layer blob doesn't exist, which means this layer wasn't applied by
// the EROFS differ (possibly the walking differ), convert the upperdir instead.
layerBlob = s.layerBlobPath(id)
if _, err := os.Stat(layerBlob); err != nil {
if cerr := convertDirToErof(ctx, layerBlob, s.upperPath(id)); cerr != nil {
if errdefs.IsNotImplemented(cerr) {
return err
}
return fmt.Errorf("failed to convert upper to erofs layer: %w", cerr)
}
}
// Enable fsverity on the EROFS layer if configured
if s.enableFsverity {
if err := fsverity.Enable(layerBlob); err != nil {
return fmt.Errorf("failed to enable fsverity: %w", err)
}
}
// Set IMMUTABLE_FL on the EROFS layer to avoid artificial data loss
if s.setImmutable {
if err := setImmutable(layerBlob, true); err != nil {
log.G(ctx).WithError(err).Warnf("failed to set IMMUTABLE_FL for %s", layerBlob)
}
}
return nil
})
if err != nil {
return err
}
return s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
if _, err := os.Stat(layerBlob); err != nil {
return fmt.Errorf("failed to get the converted erofs blob: %w", err)
}
usage, err := fs.DiskUsage(ctx, layerBlob)
if err != nil {
return err
}
if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
return fmt.Errorf("failed to commit snapshot %s: %w", key, err)
}
return nil
})
}
func (s *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var snap storage.Snapshot
var info snapshots.Info
if err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
snap, err = storage.GetSnapshot(ctx, key)
if err != nil {
return fmt.Errorf("failed to get active mount: %w", err)
}
_, info, _, err = storage.GetInfo(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot info: %w", err)
}
return nil
}); err != nil {
return nil, err
}
return s.mounts(snap, info)
}
func (s *snapshotter) getCleanupDirectories(ctx context.Context) ([]string, error) {
ids, err := storage.IDMap(ctx)
if err != nil {
return nil, err
}
snapshotDir := filepath.Join(s.root, "snapshots")
fd, err := os.Open(snapshotDir)
if err != nil {
return nil, err
}
defer fd.Close()
dirs, err := fd.Readdirnames(0)
if err != nil {
return nil, err
}
cleanup := []string{}
for _, d := range dirs {
if _, ok := ids[d]; ok {
continue
}
cleanup = append(cleanup, filepath.Join(snapshotDir, d))
}
return cleanup, nil
}
// Remove abandons the snapshot identified by key. The snapshot will
// immediately become unavailable and unrecoverable. Disk space will
// be freed up on the next call to `Cleanup`.
func (s *snapshotter) Remove(ctx context.Context, key string) (err error) {
var removals []string
var id string
// Remove directories after the transaction is closed, failures must not
// return error since the transaction is committed with the removal
// key no longer available.
defer func() {
if err == nil {
if err := cleanupUpper(s.upperPath(id)); err != nil {
log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to cleanup upperdir")
}
for _, dir := range removals {
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
}
}
}()
return s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
var k snapshots.Kind
id, k, err = storage.Remove(ctx, key)
if err != nil {
return fmt.Errorf("failed to remove snapshot %s: %w", key, err)
}
removals, err = s.getCleanupDirectories(ctx)
if err != nil {
return fmt.Errorf("unable to get directories for removal: %w", err)
}
// The layer blob is only persisted for committed snapshots.
if k == snapshots.KindCommitted {
// Clear IMMUTABLE_FL before removal, since this flag avoids it.
err = setImmutable(s.layerBlobPath(id), false)
if err != nil && !errdefs.IsNotImplemented(err) {
return fmt.Errorf("failed to clear IMMUTABLE_FL: %w", err)
}
}
return nil
})
}
func (s *snapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
err = s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
_, info, _, err = storage.GetInfo(ctx, key)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (s *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (_ snapshots.Info, err error) {
err = s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (s *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
return s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
return storage.WalkInfo(ctx, fn, fs...)
})
}
// Usage returns the resources taken by the snapshot identified by key.
//
// For active snapshots, this will scan the usage of the overlay "diff" (aka
// "upper") directory and may take some time.
//
// For committed snapshots, the value is returned from the metadata database.
func (s *snapshotter) Usage(ctx context.Context, key string) (_ snapshots.Usage, err error) {
var (
usage snapshots.Usage
info snapshots.Info
id string
)
if err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, info, usage, err = storage.GetInfo(ctx, key)
return err
}); err != nil {
return usage, err
}
if info.Kind == snapshots.KindActive {
upperPath := s.upperPath(id)
du, err := fs.DiskUsage(ctx, upperPath)
if err != nil {
// TODO(stevvooe): Consider not reporting an error in this case.
return snapshots.Usage{}, err
}
usage = snapshots.Usage(du)
}
return usage, nil
}
// Add a method to verify fsverity
func (s *snapshotter) verifyFsverity(path string) error {
if !s.enableFsverity {
return nil
}
enabled, err := fsverity.IsEnabled(path)
if err != nil {
return fmt.Errorf("failed to check fsverity status: %w", err)
}
if !enabled {
return fmt.Errorf("fsverity is not enabled on %s", path)
}
return nil
}

View File

@@ -30,60 +30,9 @@ import (
"golang.org/x/sys/unix"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/storage"
"github.com/containerd/containerd/v2/internal/erofsutils"
"github.com/containerd/containerd/v2/internal/fsverity"
)
// SnapshotterConfig is used to configure the erofs snapshotter instance
type SnapshotterConfig struct {
// ovlOptions are the base options added to the overlayfs mount (defaults to [""])
ovlOptions []string
// enableFsverity enables fsverity for EROFS layers
enableFsverity bool
// setImmutable enables IMMUTABLE_FL file attribute for EROFS layers
setImmutable bool
}
// Opt is an option to configure the erofs snapshotter
type Opt func(config *SnapshotterConfig)
// WithOvlOptions defines the extra mount options for overlayfs
func WithOvlOptions(options []string) Opt {
return func(config *SnapshotterConfig) {
config.ovlOptions = options
}
}
// WithFsverity enables fsverity for EROFS layers
func WithFsverity() Opt {
return func(config *SnapshotterConfig) {
config.enableFsverity = true
}
}
// WithImmutable enables IMMUTABLE_FL file attribute for EROFS layers
func WithImmutable() Opt {
return func(config *SnapshotterConfig) {
config.setImmutable = true
}
}
type MetaStore interface {
TransactionContext(ctx context.Context, writable bool) (context.Context, storage.Transactor, error)
WithTransaction(ctx context.Context, writable bool, fn storage.TransactionCallback) error
Close() error
}
type snapshotter struct {
root string
ms *storage.MetaStore
ovlOptions []string
enableFsverity bool
setImmutable bool
}
// check if EROFS kernel filesystem is registered or not
func findErofs() bool {
fs, err := os.ReadFile("/proc/filesystems")
@@ -93,262 +42,20 @@ func findErofs() bool {
return bytes.Contains(fs, []byte("\terofs\n"))
}
// NewSnapshotter returns a Snapshotter which uses EROFS+OverlayFS. The layers
// are stored under the provided root. A metadata file is stored under the root.
func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
var config SnapshotterConfig
for _, opt := range opts {
opt(&config)
}
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
func checkCompatibility(root string) error {
supportsDType, err := fs.SupportsDType(root)
if err != nil {
return nil, err
return err
}
if !supportsDType {
return nil, fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
return fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
}
if !findErofs() {
return nil, fmt.Errorf("EROFS unsupported, please `modprobe erofs`: %w", plugin.ErrSkipPlugin)
return fmt.Errorf("EROFS unsupported, please `modprobe erofs`: %w", plugin.ErrSkipPlugin)
}
// Check fsverity support if enabled
if config.enableFsverity {
supported, err := fsverity.IsSupported(root)
if err != nil {
return nil, fmt.Errorf("failed to check fsverity support on %q: %w", root, err)
}
if !supported {
return nil, fmt.Errorf("fsverity is not supported on the filesystem of %q", root)
}
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
return nil, err
}
return &snapshotter{
root: root,
ms: ms,
ovlOptions: config.ovlOptions,
enableFsverity: config.enableFsverity,
setImmutable: config.setImmutable,
}, nil
}
// Close closes the snapshotter
func (s *snapshotter) Close() error {
return s.ms.Close()
}
func (s *snapshotter) upperPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "fs")
}
func (s *snapshotter) workPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "work")
}
// A committed layer blob generated by the EROFS differ
func (s *snapshotter) layerBlobPath(id string) string {
return filepath.Join(s.root, "snapshots", id, "layer.erofs")
}
func (s *snapshotter) lowerPath(id string) (string, error) {
layerBlob := s.layerBlobPath(id)
if _, err := os.Stat(layerBlob); err != nil {
return "", fmt.Errorf("failed to find valid erofs layer blob: %w", err)
}
return layerBlob, nil
}
func (s *snapshotter) prepareDirectory(ctx context.Context, snapshotDir string, kind snapshots.Kind) (string, error) {
td, err := os.MkdirTemp(snapshotDir, "new-")
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %w", err)
}
if err := os.Mkdir(filepath.Join(td, "fs"), 0755); err != nil {
return td, err
}
if kind == snapshots.KindActive {
if err := os.Mkdir(filepath.Join(td, "work"), 0711); err != nil {
return td, err
}
}
// Create a special file for the EROFS differ to indicate it will be
// prepared as an EROFS layer by the EROFS snapshotter.
if err := os.WriteFile(filepath.Join(td, ".erofslayer"), []byte{}, 0644); err != nil {
return td, err
}
return td, nil
}
func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.Mount, error) {
var options []string
if len(snap.ParentIDs) == 0 {
if layerBlob, err := s.lowerPath(snap.ID); err == nil {
if snap.Kind != snapshots.KindView {
return nil, fmt.Errorf("only works for snapshots.KindView on a committed snapshot: %w", err)
}
if s.enableFsverity {
if err := s.verifyFsverity(layerBlob); err != nil {
return nil, err
}
}
return []mount.Mount{
{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
},
}, nil
}
// if we only have one layer/no parents then just return a bind mount as overlay
// will not work
roFlag := "rw"
if snap.Kind == snapshots.KindView {
roFlag = "ro"
}
return []mount.Mount{
{
Source: s.upperPath(snap.ID),
Type: "bind",
Options: append(options,
roFlag,
"rbind",
),
},
}, nil
}
if snap.Kind == snapshots.KindActive {
options = append(options,
fmt.Sprintf("workdir=%s", s.workPath(snap.ID)),
fmt.Sprintf("upperdir=%s", s.upperPath(snap.ID)),
)
} else if len(snap.ParentIDs) == 1 {
layerBlob, err := s.lowerPath(snap.ParentIDs[0])
if err != nil {
return nil, err
}
return []mount.Mount{
{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
},
}, nil
}
var mounts []mount.Mount
for i := range snap.ParentIDs {
layerBlob, err := s.lowerPath(snap.ParentIDs[i])
if err != nil {
return nil, err
}
m := mount.Mount{
Source: layerBlob,
Type: "erofs",
Options: []string{"ro", "loop"},
}
mounts = append(mounts, m)
}
options = append(options, fmt.Sprintf("lowerdir={{ overlay 0 %d }}", len(mounts)-1))
options = append(options, s.ovlOptions...)
return append(mounts, mount.Mount{
Type: "format/overlay",
Source: "overlay",
Options: options,
}), nil
}
func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var (
snap storage.Snapshot
td, path string
info snapshots.Info
)
defer func() {
if err != nil {
if td != "" {
if err1 := os.RemoveAll(td); err1 != nil {
log.G(ctx).WithError(err1).Warn("failed to cleanup temp snapshot directory")
}
}
if path != "" {
if err1 := os.RemoveAll(path); err1 != nil {
log.G(ctx).WithError(err1).WithField("path", path).Error("failed to reclaim snapshot directory, directory may need removal")
err = fmt.Errorf("failed to remove path: %v: %w", err1, err)
}
}
}
}()
if err := s.ms.WithTransaction(ctx, true, func(ctx context.Context) (err error) {
snapshotDir := filepath.Join(s.root, "snapshots")
td, err = s.prepareDirectory(ctx, snapshotDir, kind)
if err != nil {
return fmt.Errorf("failed to create prepare snapshot dir: %w", err)
}
snap, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
_, info, _, err = storage.GetInfo(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot info: %w", err)
}
if len(snap.ParentIDs) > 0 {
st, err := os.Stat(s.upperPath(snap.ParentIDs[0]))
if err != nil {
return fmt.Errorf("failed to stat parent: %w", err)
}
stat := st.Sys().(*syscall.Stat_t)
if err := os.Lchown(filepath.Join(td, "fs"), int(stat.Uid), int(stat.Gid)); err != nil {
return fmt.Errorf("failed to chown: %w", err)
}
}
path = filepath.Join(snapshotDir, snap.ID)
if err = os.Rename(td, path); err != nil {
return fmt.Errorf("failed to rename: %w", err)
}
td = ""
return nil
}); err != nil {
return nil, err
}
return s.mounts(snap, info)
}
func (s *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (s *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
return nil
}
func setImmutable(path string, enable bool) error {
@@ -376,250 +83,51 @@ func setImmutable(path string, enable bool) error {
return unix.IoctlSetPointerInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, newattr)
}
func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
var layerBlob, upperDir string
// Apply the overlayfs upperdir (generated by non-EROFS differs) into a EROFS blob
// in a read transaction first since conversion could be slow.
err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, _, _, err := storage.GetInfo(ctx, key)
if err != nil {
return err
}
// If the layer blob doesn't exist, which means this layer wasn't applied by
// the EROFS differ (possibly the walking differ), convert the upperdir instead.
layerBlob = s.layerBlobPath(id)
if _, err := os.Stat(layerBlob); err != nil {
upperDir = s.upperPath(id)
err = erofsutils.ConvertErofs(ctx, layerBlob, upperDir, nil)
if err != nil {
return err
}
// Remove all sub-directories in the overlayfs upperdir. Leave the
// overlayfs upperdir itself since it's used for Lchown.
fd, err := os.Open(upperDir)
if err != nil {
return err
}
defer fd.Close()
dirs, err := fd.Readdirnames(0)
if err != nil {
return err
}
for _, d := range dirs {
dir := filepath.Join(upperDir, d)
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
}
}
// Enable fsverity on the EROFS layer if configured
if s.enableFsverity {
if err := fsverity.Enable(layerBlob); err != nil {
return fmt.Errorf("failed to enable fsverity: %w", err)
}
}
// Set IMMUTABLE_FL on the EROFS layer to avoid artificial data loss
if s.setImmutable {
if err := setImmutable(layerBlob, true); err != nil {
log.G(ctx).WithError(err).Warnf("failed to set IMMUTABLE_FL for %s", layerBlob)
}
}
return nil
})
func cleanupUpper(upper string) error {
if err := mount.UnmountAll(upper, 0); err != nil {
return fmt.Errorf("failed to unmount EROFS mount on %v: %w", upper, err)
}
return nil
}
func convertDirToErof(ctx context.Context, layerBlob, upperDir string) error {
err := erofsutils.ConvertErofs(ctx, layerBlob, upperDir, nil)
if err != nil {
return err
}
return s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
if _, err := os.Stat(layerBlob); err != nil {
return fmt.Errorf("failed to get the converted erofs blob: %w", err)
}
usage, err := fs.DiskUsage(ctx, layerBlob)
if err != nil {
return err
}
if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
return fmt.Errorf("failed to commit snapshot %s: %w", key, err)
}
return nil
})
}
func (s *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var snap storage.Snapshot
var info snapshots.Info
if err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
snap, err = storage.GetSnapshot(ctx, key)
if err != nil {
return fmt.Errorf("failed to get active mount: %w", err)
}
_, info, _, err = storage.GetInfo(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot info: %w", err)
}
return nil
}); err != nil {
return nil, err
}
return s.mounts(snap, info)
}
func (s *snapshotter) getCleanupDirectories(ctx context.Context) ([]string, error) {
ids, err := storage.IDMap(ctx)
// Remove all sub-directories in the overlayfs upperdir. Leave the
// overlayfs upperdir itself since it's used for Lchown.
fd, err := os.Open(upperDir)
if err != nil {
return nil, err
}
snapshotDir := filepath.Join(s.root, "snapshots")
fd, err := os.Open(snapshotDir)
if err != nil {
return nil, err
return err
}
defer fd.Close()
dirs, err := fd.Readdirnames(0)
if err != nil {
return nil, err
return err
}
cleanup := []string{}
for _, d := range dirs {
if _, ok := ids[d]; ok {
continue
dir := filepath.Join(upperDir, d)
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
cleanup = append(cleanup, filepath.Join(snapshotDir, d))
}
return cleanup, nil
}
// Remove abandons the snapshot identified by key. The snapshot will
// immediately become unavailable and unrecoverable. Disk space will
// be freed up on the next call to `Cleanup`.
func (s *snapshotter) Remove(ctx context.Context, key string) (err error) {
var removals []string
var id string
// Remove directories after the transaction is closed, failures must not
// return error since the transaction is committed with the removal
// key no longer available.
defer func() {
if err == nil {
if err := mount.UnmountAll(s.upperPath(id), 0); err != nil {
log.G(ctx).Warnf("failed to unmount EROFS mount for %v", id)
}
for _, dir := range removals {
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
}
}
}()
return s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
var k snapshots.Kind
id, k, err = storage.Remove(ctx, key)
if err != nil {
return fmt.Errorf("failed to remove snapshot %s: %w", key, err)
}
removals, err = s.getCleanupDirectories(ctx)
if err != nil {
return fmt.Errorf("unable to get directories for removal: %w", err)
}
// The layer blob is only persisted for committed snapshots.
if k == snapshots.KindCommitted {
// Clear IMMUTABLE_FL before removal, since this flag avoids it.
err = setImmutable(s.layerBlobPath(id), false)
if err != nil {
return fmt.Errorf("failed to clear IMMUTABLE_FL: %w", err)
}
}
return nil
})
}
func (s *snapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
err = s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
_, info, _, err = storage.GetInfo(ctx, key)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (s *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (_ snapshots.Info, err error) {
err = s.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (s *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
return s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
return storage.WalkInfo(ctx, fn, fs...)
})
}
// Usage returns the resources taken by the snapshot identified by key.
//
// For active snapshots, this will scan the usage of the overlay "diff" (aka
// "upper") directory and may take some time.
//
// For committed snapshots, the value is returned from the metadata database.
func (s *snapshotter) Usage(ctx context.Context, key string) (_ snapshots.Usage, err error) {
var (
usage snapshots.Usage
info snapshots.Info
id string
)
if err := s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, info, usage, err = storage.GetInfo(ctx, key)
return err
}); err != nil {
return usage, err
}
if info.Kind == snapshots.KindActive {
upperPath := s.upperPath(id)
du, err := fs.DiskUsage(ctx, upperPath)
if err != nil {
// TODO(stevvooe): Consider not reporting an error in this case.
return snapshots.Usage{}, err
}
usage = snapshots.Usage(du)
}
return usage, nil
}
// Add a method to verify fsverity
func (s *snapshotter) verifyFsverity(path string) error {
if !s.enableFsverity {
return nil
}
enabled, err := fsverity.IsEnabled(path)
if err != nil {
return fmt.Errorf("failed to check fsverity status: %w", err)
}
if !enabled {
return fmt.Errorf("fsverity is not enabled on %s", path)
}
return nil
}
func upperDirectoryPermission(p, parent string) error {
st, err := os.Stat(parent)
if err != nil {
return fmt.Errorf("failed to stat parent: %w", err)
}
stat := st.Sys().(*syscall.Stat_t)
if err := os.Lchown(p, int(stat.Uid), int(stat.Gid)); err != nil {
return fmt.Errorf("failed to chown: %w", err)
}
return nil
}

View File

@@ -0,0 +1,45 @@
//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 erofs
import (
"context"
"github.com/containerd/errdefs"
)
func checkCompatibility(root string) error {
return nil
}
func setImmutable(path string, enable bool) error {
return errdefs.ErrNotImplemented
}
func cleanupUpper(upper string) error {
return nil
}
func upperDirectoryPermission(p, parent string) error {
return nil
}
func convertDirToErof(ctx context.Context, layerBlob, upperDir string) error {
return errdefs.ErrNotImplemented
}

View File

@@ -19,11 +19,12 @@ package plugin
import (
"errors"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/snapshots/erofs"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/snapshots/erofs"
)
// Config represents configuration for the native plugin.
@@ -35,6 +36,7 @@ type Config struct {
OvlOptions []string `toml:"ovl_mount_options"`
// EnableFsverity enables fsverity for EROFS layers
// Linux only
EnableFsverity bool `toml:"enable_fsverity"`
// If `SetImmutable` is enabled, IMMUTABLE_FL will be set on layer blobs.