Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/containerd/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package builtins

// register containerd builtins here
import (
_ "github.com/containerd/containerd/v2/plugins/mount/fsview/erofs"

_ "github.com/containerd/containerd/v2/core/runtime/v2"
_ "github.com/containerd/containerd/v2/plugins/content/local/plugin"
_ "github.com/containerd/containerd/v2/plugins/events"
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ go 1.26.2
require (
dario.cat/mergo v1.0.2
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6
github.com/Microsoft/go-winio v0.6.2
github.com/Microsoft/hcsshim v0.14.0-rc.1
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29
github.com/Microsoft/hcsshim v0.15.0-rc.1
github.com/checkpoint-restore/checkpointctl v1.5.0
github.com/checkpoint-restore/go-criu/v7 v7.2.0
github.com/containerd/btrfs/v2 v2.0.0
Expand Down Expand Up @@ -37,6 +37,7 @@ require (
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/docker/go-metrics v0.0.1
github.com/docker/go-units v0.5.0
github.com/erofs/go-erofs v0.2.0
github.com/fsnotify/fsnotify v1.9.0
github.com/google/certtostore v1.0.6
github.com/google/go-cmp v0.7.0
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=
github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw=
github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
Expand Down Expand Up @@ -105,6 +105,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erofs/go-erofs v0.2.0 h1:LoqBN0t85zH74ozeSS6eyw6sRGRjYdR5T0Z2LweoXvo=
github.com/erofs/go-erofs v0.2.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
Expand Down
327 changes: 327 additions & 0 deletions internal/fsview/mount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
/*
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 fsview

import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"strings"
"text/template"

"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/errdefs"
)

// View is an interface for temporarily viewing a filesystem,
// implementing the fs.FS interface with a close method.
type View interface {
fs.FS
Close() error
}

type view struct {
fs.FS
cleanup func() error
}

func (v view) Close() error {
if v.cleanup != nil {
return v.cleanup()
}
return nil
}

// readLinkView wraps an fs.ReadLinkFS with a cleanup function,
// implementing both View and fs.ReadLinkFS.
type readLinkView struct {
fs.ReadLinkFS
cleanup func() error
}

func (v readLinkView) Close() error {
if v.cleanup != nil {
return v.cleanup()
}
return nil
}

// newView creates a View that preserves fs.ReadLinkFS if the underlying
// fs.FS implements it.
func newView(fsys fs.FS, cleanup func() error) View {
if rl, ok := fsys.(fs.ReadLinkFS); ok {
return readLinkView{ReadLinkFS: rl, cleanup: cleanup}
}
return view{FS: fsys, cleanup: cleanup}
}

// FSMounts returns a View for the provided mounts if possible to open
// the mounts directly without mounting.
//
// If not supported, a nil View and an error will be returned.
func FSMounts(m []mount.Mount) (View, error) {
if len(m) == 0 {
return nil, nil
}
return resolveMount(m[len(m)-1], m[:len(m)-1])
}

// resolveMount tries registered handlers first, then built-in handlers.
func resolveMount(m mount.Mount, preceding []mount.Mount) (View, error) {
for _, h := range registered {
if h.HandleMount == nil {
continue
}
v, err := h.HandleMount(m)
if errors.Is(err, errdefs.ErrNotImplemented) {
continue
}
return v, err
}

switch {
case m.Type == "bind" || m.Type == "rbind":
return openBind(m)
case m.Type == "overlay":
return openOverlay(m)
case strings.HasPrefix(m.Type, "format/"):
return openFormatMount(m, preceding)
}

return nil, fmt.Errorf("mount type %s cannot be directly viewed: %w", m.Type, errdefs.ErrNotImplemented)
}

func openBind(m mount.Mount) (View, error) {
r, err := os.OpenRoot(m.Source)
if err != nil {
return nil, err
}
return newView(r.FS(), r.Close), nil
}

func openOverlay(m mount.Mount) (View, error) {
layers, err := openOverlayPaths(m.Options)
if err != nil {
return nil, err
}
return newOverlayView(layers)
}

func openFormatMount(m mount.Mount, preceding []mount.Mount) (View, error) {
types := strings.Split(m.Type, "/")
if len(types) < 2 || types[0] != "format" || types[len(types)-1] != "overlay" {
return nil, errdefs.ErrNotImplemented
}

var layers []View
closeLayers := func() {
for _, l := range layers {
l.Close()
}
}
for _, opt := range m.Options {
if val, ok := strings.CutPrefix(opt, "upperdir="); ok {
upper, err := resolveOverlayValue(val, preceding)
if err != nil {
if errors.Is(err, errdefs.ErrNotImplemented) {
continue
}
closeLayers()
return nil, fmt.Errorf("failed to handle upperdir option: %w", err)
}
if len(layers) > 0 {
layers = append(upper, layers...)
} else {
layers = upper
}
}
if val, ok := strings.CutPrefix(opt, "lowerdir="); ok {
for l := range strings.SplitSeq(val, ":") {
lowers, err := resolveOverlayValue(l, preceding)
if err != nil {
closeLayers()
return nil, fmt.Errorf("failed to handle lowerdir option: %w", err)
}
layers = append(layers, lowers...)
}
}
}

return newOverlayView(layers)
}

func newOverlayView(layers []View) (View, error) {
var fsList []fs.FS
for _, layer := range layers {
fsList = append(fsList, layer)
}

ofs, err := NewOverlayFS(fsList)
if err != nil {
for _, layer := range layers {
layer.Close()
}
return nil, err
}

return newView(ofs, func() error {
var errs []error
for _, layer := range layers {
errs = append(errs, layer.Close())
}
return errors.Join(errs...)
}), nil
}

// resolveOverlayValue resolves a single overlay option value, which may be
// a plain directory path or a Go template expression like "{{ mount 0 }}".
func resolveOverlayValue(s string, preceding []mount.Mount) ([]View, error) {
if !strings.Contains(s, "{{") {
r, err := os.OpenRoot(s)
if err != nil {
return nil, err
}
return []View{newView(r.FS(), r.Close)}, nil
}

tmplExpr, suffix := splitTemplateSuffix(s)

var layers []View
addLayer := func(v View) { layers = append(layers, v) }
boundsCheck := func(i int) error {
if i < 0 || i >= len(preceding) {
return fmt.Errorf("index out of bounds: %d, has %d preceding mounts", i, len(preceding))
}
return nil
}

fm := template.FuncMap{
"source": func(i int) (string, error) {
if err := boundsCheck(i); err != nil {
return "", err
}
r, err := os.OpenRoot(preceding[i].Source)
if err != nil {
return "", fmt.Errorf("failed to open source of mount %d: %w", i, err)
}
addLayer(newView(r.FS(), r.Close))
return "", nil
},
"mount": func(i int) (string, error) {
if err := boundsCheck(i); err != nil {
return "", err
}
v, err := resolveMount(preceding[i], preceding[:i])
if err != nil {
return "", fmt.Errorf("failed to resolve mount %d: %w", i, err)
}
addLayer(v)
return "", nil
},
"overlay": func(start, end int) (string, error) {
i := start
for {
if err := boundsCheck(i); err != nil {
return "", err
}
v, err := resolveMount(preceding[i], preceding[:i])
if err != nil {
return "", fmt.Errorf("failed to resolve mount %d: %w", i, err)
}
addLayer(v)
if i == end {
break
}
if start > end {
i--
} else {
i++
}
}
return "", nil
},
}

t, err := template.New("").Funcs(fm).Parse(tmplExpr)
if err != nil {
return nil, err
}

if err := t.Execute(io.Discard, nil); err != nil {
for _, l := range layers {
l.Close()
}
return nil, err
}

if suffix != "" {
for i, l := range layers {
subFS, err := fs.Sub(l, suffix)
if err != nil {
for _, l := range layers {
l.Close()
}
return nil, fmt.Errorf("failed to create sub view for path %q: %w", suffix, err)
}
layers[i] = newView(subFS, l.Close)
}
}

return layers, nil
}

func splitTemplateSuffix(s string) (string, string) {
i := strings.LastIndex(s, "}}")
if i < 0 {
return s, ""
}
tmpl := s[:i+2]
suffix := strings.TrimPrefix(s[i+2:], "/")
return tmpl, suffix
}

func openOverlayPaths(options []string) ([]View, error) {
var (
lower string
paths []string
)
for _, o := range options {
if val, ok := strings.CutPrefix(o, "lowerdir="); ok {
lower = val
} else if val, ok := strings.CutPrefix(o, "upperdir="); ok {
paths = append(paths, val)
}
}
if lower != "" {
paths = append(paths, strings.Split(lower, ":")...)
}

var layers []View
for _, p := range paths {
r, err := os.OpenRoot(p)
if err != nil {
for _, l := range layers {
l.Close()
}
return nil, err
}
layers = append(layers, newView(r.FS(), r.Close))
}
return layers, nil
}
Loading
Loading