mirror of
https://github.com/containerd/containerd.git
synced 2026-06-30 19:58:29 +00:00
Merge pull request #12433 from halaney/ahalaney/erofs-idmap-latest
Add erofs idmap support
This commit is contained in:
@@ -129,13 +129,15 @@ func readonlyMounts(mounts []Mount) []Mount {
|
||||
// readonlyOverlay takes mount options for overlay mounts and makes them readonly by
|
||||
// removing workdir and upperdir (and appending the upperdir layer to lowerdir) - see:
|
||||
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#multiple-lower-layers
|
||||
// It also strips the uidmap/gidmap options to avoid needlessly doing an idmap of this
|
||||
// temporary mount
|
||||
func readonlyOverlay(opt []string) []string {
|
||||
out := make([]string, 0, len(opt))
|
||||
upper := ""
|
||||
for _, o := range opt {
|
||||
if strings.HasPrefix(o, "upperdir=") {
|
||||
upper = strings.TrimPrefix(o, "upperdir=")
|
||||
} else if !strings.HasPrefix(o, "workdir=") {
|
||||
} else if !isSkippedReadonlyOption(o) {
|
||||
out = append(out, o)
|
||||
}
|
||||
}
|
||||
@@ -149,6 +151,15 @@ func readonlyOverlay(opt []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// isSkippedReadonlyOption takes an overlayfs option string and returns
|
||||
// true if such an option should be skipped when converting the mount
|
||||
// to a readonly mount
|
||||
func isSkippedReadonlyOption(o string) bool {
|
||||
return strings.HasPrefix(o, "workdir=") ||
|
||||
strings.HasPrefix(o, "uidmap=") ||
|
||||
strings.HasPrefix(o, "gidmap=")
|
||||
}
|
||||
|
||||
// ToProto converts from [Mount] to the containerd
|
||||
// APIs protobuf definition of a Mount.
|
||||
func ToProto(mounts []Mount) []*types.Mount {
|
||||
|
||||
@@ -90,16 +90,16 @@ func IDMapMountWithAttrs(source, target string, usernsFd int, attrSet uint64, at
|
||||
|
||||
attr.Attr_set = unix.MOUNT_ATTR_IDMAP | attrSet
|
||||
attr.Attr_clr = attrClr
|
||||
attr.Propagation = 0
|
||||
attr.Propagation = unix.MS_PRIVATE
|
||||
attr.Userns_fd = uint64(usernsFd)
|
||||
|
||||
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH))
|
||||
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH|unix.AT_RECURSIVE))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open tree for %s: %w", target, err)
|
||||
}
|
||||
|
||||
defer unix.Close(dFd)
|
||||
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH, &attr); err != nil {
|
||||
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH|unix.AT_RECURSIVE, &attr); err != nil {
|
||||
return fmt.Errorf("unable to shift GID/UID or set mount attrs for %s: %w", target, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -265,11 +265,12 @@ func doPrepareIDMappedOverlay(tmpDir string, lowerDirs []string, usernsFd int) (
|
||||
if err := IDMapMountWithAttrs(commonDir, tempRemountsLocation, usernsFd, unix.MOUNT_ATTR_RDONLY, 0); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cleanMount := func() {
|
||||
// Use the Unmount helper that does retries because there can be easily an open fd
|
||||
// to the idmapped directory and when containerd forks to create a userns fd (maybe
|
||||
// for another container), it will make the mount busy for a few ms.
|
||||
err := Unmount(tempRemountsLocation, 0)
|
||||
err := UnmountRecursive(tempRemountsLocation, 0)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Warnf("failed to unmount idmapped directory %s: %v", tempRemountsLocation, err)
|
||||
}
|
||||
|
||||
@@ -239,6 +239,5 @@ For the EROFS differ:
|
||||
|
||||
## TODO
|
||||
|
||||
- ID-mapped mount spport;
|
||||
|
||||
- DMVerity support.
|
||||
- DMVerity support.
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/containerd/containerd/v2/core/snapshots"
|
||||
"github.com/containerd/containerd/v2/core/snapshots/storage"
|
||||
"github.com/containerd/containerd/v2/internal/fsverity"
|
||||
"github.com/containerd/containerd/v2/internal/userns"
|
||||
)
|
||||
|
||||
// SnapshotterConfig is used to configure the erofs snapshotter instance
|
||||
@@ -48,6 +49,7 @@ type SnapshotterConfig struct {
|
||||
defaultSize int64
|
||||
// fsMergeThreshold (>0) enables fsmerge when the number of image layers exceeds this value
|
||||
fsMergeThreshold uint
|
||||
remapIDs bool
|
||||
}
|
||||
|
||||
// Opt is an option to configure the erofs snapshotter
|
||||
@@ -88,6 +90,13 @@ func WithFsMergeThreshold(v uint) Opt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemapIDs enables kernel ID-mapped mounts for user namespace support
|
||||
func WithRemapIDs() Opt {
|
||||
return func(config *SnapshotterConfig) {
|
||||
config.remapIDs = 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
|
||||
@@ -103,6 +112,7 @@ type snapshotter struct {
|
||||
defaultWritable int64
|
||||
blockMode bool
|
||||
fsMergeThreshold uint
|
||||
remapIDs bool
|
||||
}
|
||||
|
||||
// NewSnapshotter returns a Snapshotter which uses EROFS+OverlayFS. The layers
|
||||
@@ -160,6 +170,7 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
|
||||
defaultWritable: config.defaultSize,
|
||||
blockMode: config.defaultSize > 0,
|
||||
fsMergeThreshold: config.fsMergeThreshold,
|
||||
remapIDs: config.remapIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -241,7 +252,7 @@ func (s *snapshotter) mountFsMeta(snap storage.Snapshot, id int) (mount.Mount, b
|
||||
return m, true
|
||||
}
|
||||
|
||||
func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.Mount, error) {
|
||||
func (s *snapshotter) mounts(snap storage.Snapshot, info snapshots.Info) ([]mount.Mount, error) {
|
||||
var options []string
|
||||
|
||||
if len(snap.ParentIDs) == 0 {
|
||||
@@ -377,6 +388,16 @@ func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.M
|
||||
} else {
|
||||
options = append(options, fmt.Sprintf("lowerdir={{ overlay %d %d }}", first, len(mounts)-1))
|
||||
}
|
||||
|
||||
if s.remapIDs {
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
|
||||
options = append(options, fmt.Sprintf("uidmap=%s", v))
|
||||
}
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
|
||||
options = append(options, fmt.Sprintf("gidmap=%s", v))
|
||||
}
|
||||
}
|
||||
|
||||
options = append(options, s.ovlOptions...)
|
||||
|
||||
return append(mounts, mount.Mount{
|
||||
@@ -426,9 +447,48 @@ func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
|
||||
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
|
||||
var (
|
||||
mappedUID, mappedGID = -1, -1
|
||||
uidmapLabel, gidmapLabel string
|
||||
needsRemap = false
|
||||
)
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
|
||||
uidmapLabel = v
|
||||
needsRemap = true
|
||||
}
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
|
||||
gidmapLabel = v
|
||||
needsRemap = true
|
||||
}
|
||||
|
||||
if needsRemap {
|
||||
var idMap userns.IDMap
|
||||
if err = idMap.Unmarshal(uidmapLabel, gidmapLabel); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal snapshot ID mapped labels: %w", err)
|
||||
}
|
||||
root, err := idMap.RootPair()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find root pair: %w", err)
|
||||
}
|
||||
mappedUID, mappedGID = int(root.Uid), int(root.Gid)
|
||||
}
|
||||
|
||||
// Fall back to copying ownership from parent if no ID mapping labels
|
||||
if mappedUID == -1 || mappedGID == -1 {
|
||||
if len(snap.ParentIDs) > 0 {
|
||||
uid, gid, err := getParentOwnership(s.upperPath(snap.ParentIDs[0]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get parent ownership: %w", err)
|
||||
}
|
||||
mappedUID = uid
|
||||
mappedGID = gid
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the ownership if we have valid UID/GID
|
||||
if mappedUID != -1 && mappedGID != -1 {
|
||||
if err := os.Lchown(filepath.Join(td, "fs"), mappedUID, mappedGID); err != nil {
|
||||
return fmt.Errorf("failed to chown: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,16 +122,12 @@ func convertDirToErofs(ctx context.Context, layerBlob, upperDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func upperDirectoryPermission(p, parent string) error {
|
||||
st, err := os.Stat(parent)
|
||||
func getParentOwnership(parentPath string) (uid, gid int, err error) {
|
||||
st, err := os.Stat(parentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat parent: %w", err)
|
||||
return -1, -1, 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
|
||||
return int(stat.Uid), int(stat.Gid), nil
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ func cleanupUpper(upper string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func upperDirectoryPermission(p, parent string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertDirToErofs(ctx context.Context, layerBlob, upperDir string) error {
|
||||
return errdefs.ErrNotImplemented
|
||||
}
|
||||
|
||||
func getParentOwnership(parentPath string) (uid, gid int, err error) {
|
||||
return -1, -1, nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ import (
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
capaRemapIDs = "remap-ids"
|
||||
capaOnlyRemapIDs = "only-remap-ids"
|
||||
)
|
||||
|
||||
// Config represents configuration for the native plugin.
|
||||
type Config struct {
|
||||
// Root directory for the plugin
|
||||
@@ -94,6 +99,13 @@ func init() {
|
||||
opts = append(opts, erofs.WithFsMergeThreshold(config.MaxUnmergedLayers))
|
||||
}
|
||||
|
||||
// Don't bother supporting overlay's slow_chown, only RemapIDs
|
||||
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaOnlyRemapIDs)
|
||||
if ok, err := supportsIDMappedMounts(); err == nil && ok {
|
||||
opts = append(opts, erofs.WithRemapIDs())
|
||||
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaRemapIDs)
|
||||
}
|
||||
|
||||
ic.Meta.Exports[plugins.SnapshotterRootDir] = root
|
||||
ic.Meta.Capabilities = append(ic.Meta.Capabilities, "rebase")
|
||||
return erofs.NewSnapshotter(root, opts...)
|
||||
|
||||
27
plugins/snapshots/erofs/plugin/plugin_linux.go
Normal file
27
plugins/snapshots/erofs/plugin/plugin_linux.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//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 plugin
|
||||
|
||||
import (
|
||||
overlayutils "github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
|
||||
)
|
||||
|
||||
func supportsIDMappedMounts() (bool, error) {
|
||||
return overlayutils.SupportsIDMappedMounts()
|
||||
}
|
||||
23
plugins/snapshots/erofs/plugin/plugin_other.go
Normal file
23
plugins/snapshots/erofs/plugin/plugin_other.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//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 plugin
|
||||
|
||||
func supportsIDMappedMounts() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user