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 Nov 11, 2024
1 parent 6ac445c commit 4ef1a7f
Show file tree
Hide file tree
Showing 10 changed files with 241 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
5 changes: 5 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 Down
1 change: 1 addition & 0 deletions daemon/image_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ 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
GetLayerMountID(cid string) (string, error)
ReleaseLayer(rwlayer layer.RWLayer) error
Expand Down
19 changes: 15 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)
containerImg, err := i.imageStore.Get(container.ImageID)
if err != nil {
return nil, err
}
layerID = img.RootFS.ChainID()
img = containerImg
}

rwLayerOpts := &layer.CreateRWLayerOpts{
Expand All @@ -133,7 +133,18 @@ 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) {
var layerID layer.ChainID
if img != nil {
layerID = img.RootFS.ChainID()
}

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

// GetLayerByID returns a layer by ID
Expand Down
61 changes: 42 additions & 19 deletions daemon/mounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,51 @@ 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)
if accessor, ok := daemon.imageService.(layerAccessor); ok {
layer, err := accessor.GetLayerByID(layerName)
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
err = daemon.imageService.ReleaseLayer(layer)
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
err = layer.Unmount()
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
}
}
}

Expand Down
29 changes: 29 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,32 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
}
}

if mp.Type == mounttypes.TypeImage {
if !daemon.Config().CommonConfig.Features["image-mount"] {
return fmt.Errorf("Feature 'image-mount' is not enabled")
}
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
2 changes: 2 additions & 0 deletions hack/make/.integration-daemon-start
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ if [ "$DOCKER_EXPERIMENTAL" ]; then
extra_params="$extra_params --experimental"
fi

extra_params="$extra_params --feature=image-mount"

dockerd="dockerd"
if [ -f "/sys/fs/cgroup/cgroup.controllers" ]; then
if [ -z "$TEST_IGNORE_CGROUP_CHECK" ] && [ -z "$TEST_SKIP_INTEGRATION_CLI" ]; then
Expand Down
118 changes: 118 additions & 0 deletions integration/volume/mount_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package volume

import (
"bytes"
"context"
"io"
"path/filepath"
"strings"
"testing"

"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
Expand All @@ -14,6 +17,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/internal/safepath"
"github.com/docker/docker/testutil/fakecontext"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
Expand Down Expand Up @@ -112,6 +116,82 @@ func TestRunMountVolumeSubdir(t *testing.T) {
}
}

func TestRunMountImage(t *testing.T) {
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.45"), "skip test from new feature")

ctx := setupTest(t)
apiClient := testEnv.APIClient()

testImage := setupTestImage(t, ctx, apiClient)

for _, tc := range []struct {
name string
cmd []string
volumeTarget string
createErr string
startErr string
expected string
skipPlatform string
}{
{name: "image", cmd: []string{"cat", "/image/foo"}, expected: "bar", skipPlatform: "windows:image mounts not supported on Windows"},
} {
t.Run(tc.name, func(t *testing.T) {
if tc.skipPlatform != "" {
platform, reason, _ := strings.Cut(tc.skipPlatform, ":")
if testEnv.DaemonInfo.OSType == platform {
t.Skip(reason)
}
}

cfg := containertypes.Config{
Image: "busybox",
Cmd: tc.cmd,
}

hostCfg := containertypes.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeImage,
Source: testImage,
Target: "/image",
},
},
}

ctrName := strings.ReplaceAll(t.Name(), "/", "_")
create, creatErr := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, &network.NetworkingConfig{}, nil, ctrName)
id := create.ID
if id != "" {
defer apiClient.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
}

if tc.createErr != "" {
assert.ErrorContains(t, creatErr, tc.createErr)
return
}
assert.NilError(t, creatErr, "container creation failed")

startErr := apiClient.ContainerStart(ctx, id, containertypes.StartOptions{})
if tc.startErr != "" {
assert.ErrorContains(t, startErr, tc.startErr)
return
}
assert.NilError(t, startErr)

output, err := container.Output(ctx, apiClient, id)
assert.Check(t, err)

inspect, err := apiClient.ContainerInspect(ctx, id)
if assert.Check(t, err) {
assert.Check(t, is.Equal(inspect.State.ExitCode, 0))
}

assert.Check(t, is.Equal(strings.TrimSpace(output.Stderr), ""))
assert.Check(t, is.Equal(strings.TrimSpace(output.Stdout), tc.expected))
})
}
}

// setupTestVolume sets up a volume with:
// .
// |-- bar.txt (file with "foo")
Expand Down Expand Up @@ -183,3 +263,41 @@ func setupTestVolume(t *testing.T, client client.APIClient) string {

return volumeName
}

func setupTestImage(t *testing.T, ctx context.Context, client client.APIClient) string {
imgName := "test-image"
dockerfile := `
FROM scratch
ADD foo /
`

source := fakecontext.New(
t,
"",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("foo", "bar"),
)
defer source.Close()

resp, err := client.ImageBuild(ctx,
source.AsTarReader(t),
types.ImageBuildOptions{
Remove: false,
ForceRemove: false,
Tags: []string{imgName},
})
assert.NilError(t, err)

out := bytes.NewBuffer(nil)
_, err = io.Copy(out, resp.Body)
assert.Check(t, resp.Body.Close())
assert.NilError(t, err)

_, _, err = client.ImageInspectWithRaw(ctx, imgName)
if err != nil {
t.Log(out)
}
assert.NilError(t, err)

return imgName
}
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
Loading

0 comments on commit 4ef1a7f

Please sign in to comment.