Implement image mount for the snapshotter

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
This commit is contained in:
Laurent Goderre
2024-12-05 15:09:34 -05:00
parent 8c58934106
commit 844797348e
4 changed files with 100 additions and 50 deletions

View File

@@ -151,7 +151,19 @@ func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force,
}
using := func(c *container.Container) bool {
return c.ImageID == imgID
if c.ImageID == imgID {
return true
}
for _, mp := range c.MountPoints {
if mp.Type == "image" {
if mp.Spec.Source == string(imgID) {
return true
}
}
}
return false
}
// TODO: Should this also check parentage here?
ctr := i.containers.First(using)

View File

@@ -16,43 +16,44 @@ import (
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/opencontainers/image-spec/identity"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// CreateLayer creates a new layer for a container.
// TODO(vvoland): Decouple from container
func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.MountInit) (container.RWLayer, error) {
ctx := context.TODO()
var parentSnapshot string
var descriptor *ocispec.Descriptor
if ctr.ImageManifest != nil {
img := c8dimages.Image{
Target: *ctr.ImageManifest,
}
platformImg, err := i.NewImageManifest(ctx, img, img.Target)
if err != nil {
return nil, err
}
unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
if err != nil {
return nil, err
}
if !unpacked {
if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
return nil, err
}
}
diffIDs, err := platformImg.RootFS(ctx)
if err != nil {
return nil, err
}
parentSnapshot = identity.ChainID(diffIDs).String()
descriptor = ctr.ImageManifest
}
id := ctr.ID
rwLayerOpts := &layer.CreateRWLayerOpts{
StorageOpt: ctr.HostConfig.StorageOpt,
}
return i.createLayer(descriptor, ctr.ID, rwLayerOpts, initFunc)
}
// CreateLayerFromImage creates a new layer from an image
func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (container.RWLayer, error) {
var descriptor *ocispec.Descriptor
if img != nil {
descriptor = img.Details.ManifestDescriptor
}
return i.createLayer(descriptor, layerName, rwLayerOpts, nil)
}
func (i *ImageService) createLayer(descriptor *ocispec.Descriptor, layerName string, rwLayerOpts *layer.CreateRWLayerOpts, initFunc layer.MountInit) (container.RWLayer, error) {
ctx := context.TODO()
var parentSnapshot string
if descriptor != nil {
snapshot, err := i.getImageSnapshot(ctx, descriptor)
if err != nil {
return nil, err
}
parentSnapshot = snapshot
}
// TODO: Consider a better way to do this. It is better to have a container directly
// reference a snapshot, however, that is not done today because a container may
@@ -60,21 +61,24 @@ func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.Moun
// removing this lease and only temporarily holding a lease on re-create, using
// non-expiring leases introduces the possibility of leaking resources.
ls := i.client.LeasesService()
lease, err := ls.Create(ctx, leases.WithID(id))
lease, err := ls.Create(ctx, leases.WithID(layerName))
if err != nil {
return nil, err
}
ctx = leases.WithLease(ctx, lease.ID)
if err := i.prepareInitLayer(ctx, id, parentSnapshot, initFunc); err != nil {
return nil, err
if initFunc != nil {
if err := i.prepareInitLayer(ctx, layerName, parentSnapshot, initFunc); err != nil {
return nil, err
}
parentSnapshot = layerName + "-init"
}
sn := i.client.SnapshotService(i.StorageDriver())
if !i.idMapping.Empty() {
err = i.remapSnapshot(ctx, sn, id, id+"-init")
err = i.remapSnapshot(ctx, sn, layerName, parentSnapshot)
} else {
_, err = sn.Prepare(ctx, id, id+"-init")
_, err = sn.Prepare(ctx, layerName, parentSnapshot)
}
if err != nil {
@@ -82,7 +86,7 @@ func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.Moun
}
return &rwLayer{
id: id,
id: layerName,
snapshotterName: i.StorageDriver(),
snapshotter: sn,
refCountMounter: i.refCountMounter,
@@ -90,8 +94,35 @@ func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.Moun
}, nil
}
func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (container.RWLayer, error) {
return nil, errdefs.NotImplemented(errdefs.NotImplemented(errors.New("not implemented")))
func (i *ImageService) getImageSnapshot(ctx context.Context, descriptor *ocispec.Descriptor) (string, error) {
c8dImg := c8dimages.Image{
Target: *descriptor,
}
platformImg, err := i.NewImageManifest(ctx, c8dImg, c8dImg.Target)
if err != nil {
return "", err
}
unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
if err != nil {
return "", err
}
if !unpacked {
if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
return "", err
}
}
diffIDs, err := platformImg.RootFS(ctx)
if err != nil {
return "", err
}
snapshot := identity.ChainID(diffIDs).String()
return snapshot, nil
}
type rwLayer struct {
@@ -193,7 +224,9 @@ func (l *rwLayer) Unmount() error {
}
func (l rwLayer) Metadata() (map[string]string, error) {
return nil, nil
return map[string]string{
"ID": l.id,
}, nil
}
// ReleaseLayer releases a layer allowing it to be removed

View File

@@ -77,15 +77,19 @@ func (daemon *Daemon) removeMountPoints(container *container.Container, rm bool)
if m.Type == mounttypes.TypeImage {
layer := m.Layer
err := layer.Unmount()
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
err = daemon.imageService.ReleaseLayer(layer)
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
if layer != nil {
err := layer.Unmount()
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
err = daemon.imageService.ReleaseLayer(layer)
if err != nil {
rmErrors = append(rmErrors, err.Error())
continue
}
} else {
rmErrors = append(rmErrors, fmt.Sprintf("layer not found for image %s", m.Name))
}
}
}

View File

@@ -120,7 +120,6 @@ func TestRunMountVolumeSubdir(t *testing.T) {
func TestRunMountImage(t *testing.T) {
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "skip test from new feature")
skip.If(t, testEnv.UsingSnapshotter(), "snapshotter not supported")
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "image mounts not supported on Windows")
ctx := setupTest(t)
@@ -187,8 +186,10 @@ func TestRunMountImage(t *testing.T) {
// Test image mounted is in use logic
if tc.name == "image_remove" {
img, _, _ := apiClient.ImageInspectWithRaw(ctx, testImage)
imgId := strings.Split(img.ID, ":")[1]
_, removeErr := apiClient.ImageRemove(ctx, testImage, image.RemoveOptions{})
assert.ErrorContains(t, removeErr, fmt.Sprintf(`unable to remove repository reference "test-image" (must force) - container %s is using its referenced image`, id[:12]))
assert.ErrorContains(t, removeErr, fmt.Sprintf(`container %s is using its referenced image %s`, id[:12], imgId[:12]))
}
// Test that the container servives a restart when mounted image is removed