Browse Source

introduce volume.type=image

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 8 months ago
parent
commit
01e83defc2
7 changed files with 99 additions and 40 deletions
  1. 1 3
      go.mod
  2. 2 2
      go.sum
  3. 7 4
      pkg/compose/build.go
  4. 41 25
      pkg/compose/create.go
  5. 19 6
      pkg/compose/pull.go
  6. 10 0
      pkg/e2e/fixtures/volumes/compose.yaml
  7. 19 0
      pkg/e2e/volumes_test.go

+ 1 - 3
go.mod

@@ -8,7 +8,7 @@ require (
 	github.com/Microsoft/go-winio v0.6.2
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
 	github.com/buger/goterm v1.0.4
-	github.com/compose-spec/compose-go/v2 v2.5.0
+	github.com/compose-spec/compose-go/v2 v2.5.1-0.20250409070949-8e1a035095ca
 	github.com/containerd/containerd/v2 v2.0.4
 	github.com/containerd/platforms v1.0.0-rc.1
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
@@ -206,5 +206,3 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
-
-replace github.com/compose-spec/compose-go/v2 => github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535

+ 2 - 2
go.sum

@@ -83,6 +83,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
 github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
+github.com/compose-spec/compose-go/v2 v2.5.1-0.20250409070949-8e1a035095ca h1:4dH4DudeDunWTYetcJxQE65osreQKvaLtFLdl9CcqME=
+github.com/compose-spec/compose-go/v2 v2.5.1-0.20250409070949-8e1a035095ca/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
 github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
 github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
@@ -167,8 +169,6 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
 github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
-github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535 h1:S/P6v3QxsMpkKn+2OSMPNkfSkadSjSHoMGAc/eBZgMU=
-github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=

+ 7 - 4
pkg/compose/build.go

@@ -318,14 +318,17 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
 }
 
 func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
-	var imageNames []string
+	imageNames := utils.Set[string]{}
 	for _, s := range project.Services {
 		imgName := api.GetImageNameOrDefault(s, project.Name)
-		if !utils.StringContains(imageNames, imgName) {
-			imageNames = append(imageNames, imgName)
+		imageNames.Add(imgName)
+		for _, volume := range s.Volumes {
+			if volume.Type == types.VolumeTypeImage {
+				imageNames.Add(volume.Source)
+			}
 		}
 	}
-	imgs, err := s.getImageSummaries(ctx, imageNames)
+	imgs, err := s.getImageSummaries(ctx, imageNames.Elements())
 	if err != nil {
 		return nil, err
 	}

+ 41 - 25
pkg/compose/create.go

@@ -871,6 +871,15 @@ MOUNTS:
 				}
 			}
 		}
+		if m.Type == mount.TypeImage {
+			version, err := s.RuntimeVersion(ctx)
+			if err != nil {
+				return nil, nil, err
+			}
+			if versions.LessThan(version, "1.48") {
+				return nil, nil, fmt.Errorf("volume with type=image require Docker Engine v28 or later")
+			}
+		}
 		mounts = append(mounts, m)
 	}
 	return binds, mounts, nil
@@ -1125,7 +1134,7 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
 		}
 	}
 
-	bind, vol, tmpfs := buildMountOptions(volume)
+	bind, vol, tmpfs, img := buildMountOptions(volume)
 
 	if bind != nil {
 		volume.Type = types.VolumeTypeBind
@@ -1140,37 +1149,35 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
 		BindOptions:   bind,
 		VolumeOptions: vol,
 		TmpfsOptions:  tmpfs,
+		ImageOptions:  img,
 	}, nil
 }
 
-func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) {
+func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, *mount.ImageOptions) {
+	if volume.Type != types.VolumeTypeBind && volume.Bind != nil {
+		logrus.Warnf("mount of type `%s` should not define `bind` option", volume.Type)
+	}
+	if volume.Type != types.VolumeTypeVolume && volume.Volume != nil {
+		logrus.Warnf("mount of type `%s` should not define `volume` option", volume.Type)
+	}
+	if volume.Type != types.VolumeTypeTmpfs && volume.Tmpfs != nil {
+		logrus.Warnf("mount of type `%s` should not define `tmpfs` option", volume.Type)
+	}
+	if volume.Type != types.VolumeTypeImage && volume.Image != nil {
+		logrus.Warnf("mount of type `%s` should not define `image` option", volume.Type)
+	}
+
 	switch volume.Type {
 	case "bind":
-		if volume.Volume != nil {
-			logrus.Warnf("mount of type `bind` should not define `volume` option")
-		}
-		if volume.Tmpfs != nil {
-			logrus.Warnf("mount of type `bind` should not define `tmpfs` option")
-		}
-		return buildBindOption(volume.Bind), nil, nil
+		return buildBindOption(volume.Bind), nil, nil, nil
 	case "volume":
-		if volume.Bind != nil {
-			logrus.Warnf("mount of type `volume` should not define `bind` option")
-		}
-		if volume.Tmpfs != nil {
-			logrus.Warnf("mount of type `volume` should not define `tmpfs` option")
-		}
-		return nil, buildVolumeOptions(volume.Volume), nil
+		return nil, buildVolumeOptions(volume.Volume), nil, nil
 	case "tmpfs":
-		if volume.Bind != nil {
-			logrus.Warnf("mount of type `tmpfs` should not define `bind` option")
-		}
-		if volume.Volume != nil {
-			logrus.Warnf("mount of type `tmpfs` should not define `volume` option")
-		}
-		return nil, nil, buildTmpfsOptions(volume.Tmpfs)
+		return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil
+	case "image":
+		return nil, nil, nil, buildImageOptions(volume.Image)
 	}
-	return nil, nil, nil
+	return nil, nil, nil, nil
 }
 
 func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
@@ -1199,7 +1206,7 @@ func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
 	return &mount.VolumeOptions{
 		NoCopy:  vol.NoCopy,
 		Subpath: vol.Subpath,
-		// Labels:       , // FIXME missing from model ?
+		Labels:  vol.Labels,
 		// DriverConfig: , // FIXME missing from model ?
 	}
 }
@@ -1214,6 +1221,15 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
 	}
 }
 
+func buildImageOptions(image *types.ServiceVolumeImage) *mount.ImageOptions {
+	if image == nil {
+		return nil
+	}
+	return &mount.ImageOptions{
+		Subpath: image.SubPath,
+	}
+}
+
 func (s *composeService) ensureNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) {
 	if n.External {
 		return s.resolveExternalNetwork(ctx, n)

+ 19 - 6
pkg/compose/pull.go

@@ -290,15 +290,28 @@ func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
 }
 
 func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]api.ImageSummary, quietPull bool) error {
-	var needPull []types.ServiceConfig
-	for _, service := range project.Services {
+	needPull := map[string]types.ServiceConfig{}
+	for name, service := range project.Services {
 		pull, err := mustPull(service, images)
 		if err != nil {
 			return err
 		}
 		if pull {
-			needPull = append(needPull, service)
+			needPull[name] = service
 		}
+		for i, vol := range service.Volumes {
+			if vol.Type == types.VolumeTypeImage {
+				if _, ok := images[vol.Source]; !ok {
+					// Hack: create a fake ServiceConfig so we pull missing volume image
+					n := fmt.Sprintf("%s:volume %d", name, i)
+					needPull[n] = types.ServiceConfig{
+						Name:  n,
+						Image: vol.Source,
+					}
+				}
+			}
+		}
+
 	}
 	if len(needPull) == 0 {
 		return nil
@@ -308,11 +321,11 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
 		w := progress.ContextWriter(ctx)
 		eg, ctx := errgroup.WithContext(ctx)
 		eg.SetLimit(s.maxConcurrency)
-		pulledImages := make([]api.ImageSummary, len(needPull))
-		for i, service := range needPull {
+		pulledImages := map[string]api.ImageSummary{}
+		for name, service := range needPull {
 			eg.Go(func() error {
 				id, err := s.pullServiceImage(ctx, service, s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
-				pulledImages[i] = api.ImageSummary{
+				pulledImages[name] = api.ImageSummary{
 					ID:          id,
 					Repository:  service.Image,
 					LastTagTime: time.Now(),

+ 10 - 0
pkg/e2e/fixtures/volumes/compose.yaml

@@ -0,0 +1,10 @@
+services:
+  with_image:
+    image: alpine
+    command: "ls -al /mnt/image"
+    volumes:
+      - type: image
+        source: nginx:alpine
+        target: /mnt/image
+        image:
+          subpath: usr/share/nginx/html/

+ 19 - 0
pkg/e2e/volumes_test.go

@@ -174,3 +174,22 @@ func TestUpRecreateVolumes_IgnoreBinds(t *testing.T) {
 	res := c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/bind.yaml", "--project-name", projectName, "up", "-d")
 	assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
 }
+
+func TestImageVolume(t *testing.T) {
+	c := NewCLI(t)
+	const projectName = "compose-e2e-image-volume"
+	t.Cleanup(func() {
+		c.cleanupWithDown(t, projectName)
+	})
+
+	version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
+	major, _, found := strings.Cut(version.Combined(), ".")
+	assert.Assert(t, found)
+	if major == "26" || major == "27" {
+		t.Skip("Skipping test due to docker version < 28")
+	}
+
+	res := c.RunDockerComposeCmd(t, "-f", "./fixtures/volumes/compose.yaml", "--project-name", projectName, "up", "with_image")
+	out := res.Combined()
+	assert.Check(t, strings.Contains(out, "index.html"))
+}