diff --git a/cmd/containerd/builtins/builtins_unix.go b/cmd/containerd/builtins/builtins_unix.go index d2f6a4d85..f378a6b84 100644 --- a/cmd/containerd/builtins/builtins_unix.go +++ b/cmd/containerd/builtins/builtins_unix.go @@ -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" ) diff --git a/internal/erofsutils/mount_linux.go b/internal/erofsutils/mount.go similarity index 99% rename from internal/erofsutils/mount_linux.go rename to internal/erofsutils/mount.go index 6e35a0de7..16b1f347d 100644 --- a/internal/erofsutils/mount_linux.go +++ b/internal/erofsutils/mount.go @@ -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 { diff --git a/plugins/diff/erofs/differ_linux.go b/plugins/diff/erofs/compare_linux.go similarity index 56% rename from plugins/diff/erofs/differ_linux.go rename to plugins/diff/erofs/compare_linux.go index ba80794e2..a3b322c89 100644 --- a/plugins/diff/erofs/differ_linux.go +++ b/plugins/diff/erofs/compare_linux.go @@ -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 diff --git a/plugins/diff/erofs/compare_other.go b/plugins/diff/erofs/compare_other.go new file mode 100644 index 000000000..d9123da83 --- /dev/null +++ b/plugins/diff/erofs/compare_other.go @@ -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 +} diff --git a/plugins/diff/erofs/differ.go b/plugins/diff/erofs/differ.go new file mode 100644 index 000000000..c89ad2ddf --- /dev/null +++ b/plugins/diff/erofs/differ.go @@ -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 +} diff --git a/plugins/diff/erofs/plugin/plugin_linux.go b/plugins/diff/erofs/plugin/plugin.go similarity index 95% rename from plugins/diff/erofs/plugin/plugin_linux.go rename to plugins/diff/erofs/plugin/plugin.go index 89ba98866..c051ec9f8 100644 --- a/plugins/diff/erofs/plugin/plugin_linux.go +++ b/plugins/diff/erofs/plugin/plugin.go @@ -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) diff --git a/plugins/snapshots/erofs/erofs.go b/plugins/snapshots/erofs/erofs.go new file mode 100644 index 000000000..a69d10c51 --- /dev/null +++ b/plugins/snapshots/erofs/erofs.go @@ -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 +} diff --git a/plugins/snapshots/erofs/erofs_linux.go b/plugins/snapshots/erofs/erofs_linux.go index 7a9409c4c..16e0a4870 100644 --- a/plugins/snapshots/erofs/erofs_linux.go +++ b/plugins/snapshots/erofs/erofs_linux.go @@ -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 +} diff --git a/plugins/snapshots/erofs/erofs_other.go b/plugins/snapshots/erofs/erofs_other.go new file mode 100644 index 000000000..5c5b37b31 --- /dev/null +++ b/plugins/snapshots/erofs/erofs_other.go @@ -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 +} diff --git a/plugins/snapshots/erofs/plugin/plugin_linux.go b/plugins/snapshots/erofs/plugin/plugin.go similarity index 99% rename from plugins/snapshots/erofs/plugin/plugin_linux.go rename to plugins/snapshots/erofs/plugin/plugin.go index ff64f2d0e..03dd38191 100644 --- a/plugins/snapshots/erofs/plugin/plugin_linux.go +++ b/plugins/snapshots/erofs/plugin/plugin.go @@ -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.