Skip to content

Commit

Permalink
Implement mount from image
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
  • Loading branch information
LaurentGoderre committed Oct 30, 2024
1 parent 8eba9bf commit fd65589
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 23 deletions.
2 changes: 2 additions & 0 deletions api/types/mount/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const (
TypeNamedPipe Type = "npipe"
// TypeCluster is the type for Swarm Cluster Volumes.
TypeCluster Type = "cluster"
// TypeImage is the type for mounting another image's filesystem
TypeImage Type = "image"
)

// Mount represents a mount (volume).
Expand Down
11 changes: 11 additions & 0 deletions daemon/containerd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
dimages "github.com/docker/docker/daemon/images"
"github.com/docker/docker/daemon/snapshotter"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/registry"
Expand Down Expand Up @@ -115,6 +116,10 @@ func (i *ImageService) CreateLayer(container *container.Container, initFunc laye
return nil, errdefs.NotImplemented(errdefs.NotImplemented(errors.New("not implemented")))
}

func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (layer.RWLayer, error) {
return nil, errdefs.NotImplemented(errdefs.NotImplemented(errors.New("not implemented")))
}

// LayerStoreStatus returns the status for each layer store
// called from info.go
func (i *ImageService) LayerStoreStatus() [][2]string {
Expand All @@ -124,6 +129,12 @@ func (i *ImageService) LayerStoreStatus() [][2]string {
}
}

// GetLayerByID returns a layer by ID
// called from daemon.go Daemon.restore().
func (i *ImageService) GetLayerByID(cid string) (layer.RWLayer, error) {
return nil, errdefs.NotImplemented(errors.New("not implemented"))
}

// GetLayerMountID returns the mount ID for a layer
// called from daemon.go Daemon.Shutdown(), and Daemon.Cleanup() (cleanup is actually containerCleanup)
// TODO: needs to be refactored to Unmount (see callers), or removed and replaced with GetLayerByID
Expand Down
2 changes: 2 additions & 0 deletions daemon/image_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ type ImageService interface {

GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error)
CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error)
CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (layer.RWLayer, error)
LayerStoreStatus() [][2]string
GetLayerByID(cid string) (layer.RWLayer, error)
GetLayerMountID(cid string) (string, error)
ReleaseLayer(rwlayer layer.RWLayer) error
LayerDiskUsage(ctx context.Context) (int64, error)
Expand Down
16 changes: 12 additions & 4 deletions daemon/images/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ func (i *ImageService) Children(_ context.Context, id image.ID) ([]image.ID, err
// called from create.go
// TODO: accept an opt struct instead of container?
func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
var layerID layer.ChainID
var img *image.Image
if container.ImageID != "" {
img, err := i.imageStore.Get(container.ImageID)
i, err := i.imageStore.Get(container.ImageID)
if err != nil {
return nil, err
}
layerID = img.RootFS.ChainID()
img = i
}

rwLayerOpts := &layer.CreateRWLayerOpts{
Expand All @@ -133,7 +133,15 @@ func (i *ImageService) CreateLayer(container *container.Container, initFunc laye
StorageOpt: container.HostConfig.StorageOpt,
}

return i.layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts)
return i.CreateLayerFromImage(img, container.ID, rwLayerOpts)
}

// CreateLayerFromImage creates a file system from an arbitrary image
// Used to mount an image inside another
func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (layer.RWLayer, error) {
layerID := img.RootFS.ChainID()

return i.layerStore.CreateRWLayer(layerName, layerID, rwLayerOpts)
}

// GetLayerByID returns a layer by ID
Expand Down
56 changes: 37 additions & 19 deletions daemon/mounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,46 @@ func (daemon *Daemon) removeMountPoints(container *container.Container, rm bool)
var rmErrors []string
ctx := context.TODO()
for _, m := range container.MountPoints {
if m.Type != mounttypes.TypeVolume || m.Volume == nil {
continue
}
daemon.volumes.Release(ctx, m.Volume.Name(), container.ID)
if !rm {
continue
}
if m.Type == mounttypes.TypeVolume {
if m.Volume == nil {
continue
}
daemon.volumes.Release(ctx, m.Volume.Name(), container.ID)
if !rm {
continue
}

// Do not remove named mountpoints
// these are mountpoints specified like `docker run -v <name>:/foo`
if m.Spec.Source != "" {
continue
// Do not remove named mountpoints
// these are mountpoints specified like `docker run -v <name>:/foo`
if m.Spec.Source != "" {
continue
}

err := daemon.volumes.Remove(ctx, m.Volume.Name())
// Ignore volume in use errors because having this
// volume being referenced by other container is
// not an error, but an implementation detail.
// This prevents docker from logging "ERROR: Volume in use"
// where there is another container using the volume.
if err != nil && !volumesservice.IsInUse(err) {
rmErrors = append(rmErrors, err.Error())
}
}

err := daemon.volumes.Remove(ctx, m.Volume.Name())
// Ignore volume in use errors because having this
// volume being referenced by other container is
// not an error, but an implementation detail.
// This prevents docker from logging "ERROR: Volume in use"
// where there is another container using the volume.
if err != nil && !volumesservice.IsInUse(err) {
rmErrors = append(rmErrors, err.Error())
if m.Type == mounttypes.TypeImage {
layerName := fmt.Sprintf("%s-%s", container.ID, m.Spec.Source)
layer, err := daemon.imageService.GetLayerByID(layerName)
if err != nil {
rmErrors = append(rmErrors, err.Error())
}
err = daemon.imageService.ReleaseLayer(layer)
if err != nil {
rmErrors = append(rmErrors, err.Error())
}
err = layer.Unmount()
if err != nil {
rmErrors = append(rmErrors, err.Error())
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions daemon/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ package daemon // import "github.com/docker/docker/daemon"

import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"

"github.com/containerd/log"
"github.com/docker/docker/api/types/backend"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
mounttypes "github.com/docker/docker/api/types/mount"
volumetypes "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/container"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/layer"
"github.com/docker/docker/volume"
volumemounts "github.com/docker/docker/volume/mounts"
"github.com/docker/docker/volume/service"
Expand Down Expand Up @@ -245,6 +248,29 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
}
}

if mp.Type == mounttypes.TypeImage {
img, err := daemon.imageService.GetImage(ctx, mp.Source, backend.GetImageOpts{})
if err != nil {
return err
}

rwLayerOpts := &layer.CreateRWLayerOpts{
StorageOpt: container.HostConfig.StorageOpt,
}

layerName := fmt.Sprintf("%s-%s", container.ID, mp.Source)
layer, err := daemon.imageService.CreateLayerFromImage(img, layerName, rwLayerOpts)
if err != nil {
return err
}
path, err := layer.Mount("")
if err != nil {
return err
}

mp.Source = path
}

binds[mp.Destination] = true
dereferenceIfExists(mp.Destination)
mountPoints[mp.Destination] = mp
Expand Down
16 changes: 16 additions & 0 deletions volume/mounts/linux_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour
if _, err := p.ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
return &errMountConfig{mnt, err}
}
case mount.TypeImage:
if mnt.BindOptions != nil {
return &errMountConfig{mnt, errExtraField("BindOptions")}
}
if len(mnt.Source) == 0 {
return &errMountConfig{mnt, errMissingField("Source")}
}
default:
return &errMountConfig{mnt, errors.New("mount type unknown")}
}
Expand Down Expand Up @@ -353,6 +360,15 @@ func (p *linuxParser) parseMountSpec(cfg mount.Mount, validateBindSourceExists b
}
case mount.TypeTmpfs:
// NOP
case mount.TypeImage:
mp.Source = cfg.Source
if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
mp.Propagation = cfg.BindOptions.Propagation
} else {
// If user did not specify a propagation mode, get
// default propagation mode.
mp.Propagation = linuxDefaultPropagationMode
}
}
return mp, nil
}
Expand Down
4 changes: 4 additions & 0 deletions volume/mounts/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ func TestParseMountSpec(t *testing.T) {
input: mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)},
expected: MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()},
},
{
input: mount.Mount{Type: mount.TypeImage, Source: "alpine", Target: testDestinationPath},
expected: MountPoint{Type: mount.TypeImage, Source: "alpine", Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()},
},
}

for _, tc := range cases {
Expand Down

0 comments on commit fd65589

Please sign in to comment.