mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 05:17:02 +00:00
A layer served from the layer content cache was symlinked into the snapshot as its layer.erofs. That put a path inside the snapshot directory aliasing a blob owned by the operator and shared with every other snapshot of that layer. Everything written to that path went straight into the cache entry. Each write site then needed its own guard: the differ had to refuse to apply, Commit had to refuse to convert through a dangling link, and Remove had to avoid clearing IMMUTABLE_FL on a file it did not own. A missed guard corrupted content shared across images. A pruned entry left a dangling link that read as an absent blob on one path and as populated content on another. Record where the blob is on the snapshot and mount it from there. Nothing in the snapshot directory refers to content it does not own. A stray write creates a local file, conversion cannot reach the cache, and removal has nothing to follow. The guards Commit and Remove needed for a symlink are gone. The differ still refuses, on the read-only mounts the snapshotter hands out. resolveBlob is the one place a blob is located. It reports whether the blob is already populated, meaning there is nothing to apply or convert, and whether it belongs to this snapshot, meaning it may be written. Mounts are then assembled without consulting the filesystem to find out what a layer is. A source that does not resolve is an error on every path, including the parentless path in mounts(). A flattened parent's device= is resolved the same way. fsverity is only measured for a blob the snapshot owns. Measuring a cache entry would require the operator to enable fsverity on content this snapshotter never wrote. NewSnapshotter already rejects fsverity together with the layer content cache. The record is kept in labels outside the containerd.io/snapshot/ namespace. Labels in that namespace are inherited from image annotations, which would let an image name any path on the host. The same filter runs on everything the metadata snapshotter passes down. A client cannot set these labels, and cannot send them back after reading them through Stat. Commit and Update carry them across. The dm-verity sidecar is read from beside the cache entry. Nothing is copied into the snapshot. The sidecar cannot fall out of step with the blob it pins. An entry required to have one is rejected at Prepare. Signed-off-by: Derek McGowan <derek@mcg.dev>
146 lines
4.8 KiB
Go
146 lines
4.8 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/containerd/platforms"
|
|
"github.com/containerd/plugin"
|
|
"github.com/containerd/plugin/registry"
|
|
|
|
"github.com/containerd/containerd/v2/core/snapshots"
|
|
"github.com/containerd/containerd/v2/plugins"
|
|
"github.com/containerd/containerd/v2/plugins/snapshots/erofs"
|
|
"github.com/docker/go-units"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
const (
|
|
capaRemapIDs = "remap-ids"
|
|
capaOnlyRemapIDs = "only-remap-ids"
|
|
)
|
|
|
|
// Config represents configuration for the native plugin.
|
|
type Config struct {
|
|
// Root directory for the plugin
|
|
RootPath string `toml:"root_path"`
|
|
|
|
// MountOptions are options used for the EROFS overlayfs mount
|
|
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.
|
|
SetImmutable bool `toml:"set_immutable"`
|
|
|
|
// DefaultSize is the default size of a writable layer in string
|
|
DefaultSize string `toml:"default_size"`
|
|
|
|
// DmverityMode controls dm-verity behavior: "auto" (use if available), "on" (require), "off" (disable)
|
|
// Linux only
|
|
DmverityMode string `toml:"dmverity_mode"`
|
|
|
|
// LayerContentCaches lists directories of pre-converted, diffID-keyed erofs
|
|
// layer blobs. A layer found in one of them is mounted from that directory,
|
|
// in both sequential and parallel unpack modes. The directories are
|
|
// searched in the order given and the first one holding the layer wins; one
|
|
// that doesn't exist is treated as a miss. A layer found in none of them is
|
|
// converted normally.
|
|
//
|
|
// The blobs are read but never written and are expected to outlive the
|
|
// snapshots using them. Removing one while an image still refers to it
|
|
// leaves that image unusable.
|
|
LayerContentCaches []string `toml:"layer_content_caches"`
|
|
}
|
|
|
|
// snapshotterPlatforms returns the platforms this snapshotter advertises: the
|
|
// default platform, plus the "erofs" OS feature platform (see the EROFS image
|
|
// layer format specification, https://github.com/erofs/erofs-image-spec) so
|
|
// that clients pulling with --snapshotter erofs prefer the native EROFS image
|
|
// variant when one is available in a multi-platform index.
|
|
func snapshotterPlatforms() []ocispec.Platform {
|
|
erofsPlatform := platforms.DefaultSpec()
|
|
result := []ocispec.Platform{erofsPlatform}
|
|
erofsPlatform.OSFeatures = []string{"erofs"}
|
|
return append(result, erofsPlatform)
|
|
}
|
|
|
|
func init() {
|
|
registry.Register(&plugin.Registration{
|
|
Type: plugins.SnapshotPlugin,
|
|
ID: "erofs",
|
|
Config: &Config{},
|
|
InitFn: func(ic *plugin.InitContext) (any, error) {
|
|
ic.Meta.Platforms = append(ic.Meta.Platforms, snapshotterPlatforms()...)
|
|
|
|
config, ok := ic.Config.(*Config)
|
|
if !ok {
|
|
return nil, errors.New("invalid erofs configuration")
|
|
}
|
|
|
|
var opts []erofs.Opt
|
|
root := ic.Properties[plugins.PropertyRootDir]
|
|
if len(config.RootPath) != 0 {
|
|
root = config.RootPath
|
|
}
|
|
|
|
if len(config.OvlOptions) > 0 {
|
|
opts = append(opts, erofs.WithOvlOptions(config.OvlOptions))
|
|
}
|
|
|
|
if config.EnableFsverity {
|
|
opts = append(opts, erofs.WithFsverity())
|
|
}
|
|
|
|
if config.SetImmutable {
|
|
opts = append(opts, erofs.WithImmutable())
|
|
}
|
|
|
|
if config.DefaultSize != "" {
|
|
size, err := units.RAMInBytes(config.DefaultSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse default_size '%v': %w", config.DefaultSize, err)
|
|
}
|
|
opts = append(opts, erofs.WithDefaultSize(size))
|
|
}
|
|
|
|
if config.DmverityMode != "" {
|
|
opts = append(opts, erofs.WithDmverityMode(config.DmverityMode))
|
|
}
|
|
|
|
if len(config.LayerContentCaches) > 0 {
|
|
opts = append(opts, erofs.WithLayerContentCaches(config.LayerContentCaches...))
|
|
}
|
|
|
|
// 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, snapshots.RebaseCap)
|
|
return erofs.NewSnapshotter(root, opts...)
|
|
},
|
|
})
|
|
}
|