Repository: compose-spec/compose-go
Pull request #769 (2025-04-09 08:08:45)

introduce volume.type=image
introduce volume type "image"

see https://github.com/docker/cli/pull/5943


Patch:
diff --git a/override/merge.go b/override/merge.go
index 8baade9..2ddd786 100644
--- a/override/merge.go
+++ b/override/merge.go
@@ -61,6 +61,7 @@ func init() {
 	mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
 	mergeSpecials["services.*.healthcheck.test"] = override
 	mergeSpecials["services.*.labels"] = mergeToSequence
+	mergeSpecials["services.*.volumes.*.volume.labels"] = mergeToSequence
 	mergeSpecials["services.*.logging"] = mergeLogging
 	mergeSpecials["services.*.networks"] = mergeNetworks
 	mergeSpecials["services.*.sysctls"] = mergeToSequence
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index d77b09a..ee1137f 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -418,7 +418,9 @@
                 "type": "object",
                 "required": ["type"],
                 "properties": {
-                  "type": {"type": "string"},
+                  "type": {"type": "string",
+                    "enum": ["bind", "volume", "tmpfs", "cluster", "image"]
+                  },
                   "source": {"type": "string"},
                   "target": {"type": "string"},
                   "read_only": {"type": ["boolean", "string"]},
@@ -437,6 +439,7 @@
                   "volume": {
                     "type": "object",
                     "properties": {
+                      "labels": {"$ref": "#/definitions/list_or_dict"},
                       "nocopy": {"type": ["boolean", "string"]},
                       "subpath": {"type": "string"}
                     },
@@ -456,6 +459,14 @@
                     },
                     "additionalProperties": false,
                     "patternProperties": {"^x-": {}}
+                  },
+                  "image": {
+                    "type": "object",
+                    "properties": {
+                      "subpath": {"type": "string"}
+                    },
+                    "additionalProperties": false,
+                    "patternProperties": {"^x-": {}}
                   }
                 },
                 "additionalProperties": false,
diff --git a/types/derived.gen.go b/types/derived.gen.go
index e540c53..2d60dae 100644
--- a/types/derived.gen.go
+++ b/types/derived.gen.go
@@ -1964,6 +1964,12 @@ func deriveDeepCopy_44(dst, src *ServiceVolumeConfig) {
 		dst.Tmpfs = new(ServiceVolumeTmpfs)
 		deriveDeepCopy_54(dst.Tmpfs, src.Tmpfs)
 	}
+	if src.Image == nil {
+		dst.Image = nil
+	} else {
+		dst.Image = new(ServiceVolumeImage)
+		deriveDeepCopy_55(dst.Image, src.Image)
+	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
 		src.Extensions.DeepCopy(dst.Extensions)
@@ -2028,7 +2034,7 @@ func deriveDeepCopy_46(dst, src *IPAMConfig) {
 		} else {
 			dst.Config = make([]*IPAMPool, len(src.Config))
 		}
-		deriveDeepCopy_55(dst.Config, src.Config)
+		deriveDeepCopy_56(dst.Config, src.Config)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2155,7 +2161,7 @@ func deriveDeepCopy_50(dst, src *Resource) {
 		} else {
 			dst.GenericResources = make([]GenericResource, len(src.GenericResources))
 		}
-		deriveDeepCopy_56(dst.GenericResources, src.GenericResources)
+		deriveDeepCopy_57(dst.GenericResources, src.GenericResources)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2170,7 +2176,7 @@ func deriveDeepCopy_51(dst, src []PlacementPreferences) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(PlacementPreferences)
-			deriveDeepCopy_57(field, &src_value)
+			deriveDeepCopy_58(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
@@ -2192,6 +2198,12 @@ func deriveDeepCopy_52(dst, src *ServiceVolumeBind) {
 
 // deriveDeepCopy_53 recursively copies the contents of src into dst.
 func deriveDeepCopy_53(dst, src *ServiceVolumeVolume) {
+	if src.Labels != nil {
+		dst.Labels = make(map[string]string, len(src.Labels))
+		deriveDeepCopy_4(dst.Labels, src.Labels)
+	} else {
+		dst.Labels = nil
+	}
 	dst.NoCopy = src.NoCopy
 	dst.Subpath = src.Subpath
 	if src.Extensions != nil {
@@ -2215,30 +2227,41 @@ func deriveDeepCopy_54(dst, src *ServiceVolumeTmpfs) {
 }
 
 // deriveDeepCopy_55 recursively copies the contents of src into dst.
-func deriveDeepCopy_55(dst, src []*IPAMPool) {
+func deriveDeepCopy_55(dst, src *ServiceVolumeImage) {
+	dst.SubPath = src.SubPath
+	if src.Extensions != nil {
+		dst.Extensions = make(map[string]any, len(src.Extensions))
+		src.Extensions.DeepCopy(dst.Extensions)
+	} else {
+		dst.Extensions = nil
+	}
+}
+
+// deriveDeepCopy_56 recursively copies the contents of src into dst.
+func deriveDeepCopy_56(dst, src []*IPAMPool) {
 	for src_i, src_value := range src {
 		if src_value == nil {
 			dst[src_i] = nil
 		} else {
 			dst[src_i] = new(IPAMPool)
-			deriveDeepCopy_58(dst[src_i], src_value)
+			deriveDeepCopy_59(dst[src_i], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_56 recursively copies the contents of src into dst.
-func deriveDeepCopy_56(dst, src []GenericResource) {
+// deriveDeepCopy_57 recursively copies the contents of src into dst.
+func deriveDeepCopy_57(dst, src []GenericResource) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(GenericResource)
-			deriveDeepCopy_59(field, &src_value)
+			deriveDeepCopy_60(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_57 recursively copies the contents of src into dst.
-func deriveDeepCopy_57(dst, src *PlacementPreferences) {
+// deriveDeepCopy_58 recursively copies the contents of src into dst.
+func deriveDeepCopy_58(dst, src *PlacementPreferences) {
 	dst.Spread = src.Spread
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2248,8 +2271,8 @@ func deriveDeepCopy_57(dst, src *PlacementPreferences) {
 	}
 }
 
-// deriveDeepCopy_58 recursively copies the contents of src into dst.
-func deriveDeepCopy_58(dst, src *IPAMPool) {
+// deriveDeepCopy_59 recursively copies the contents of src into dst.
+func deriveDeepCopy_59(dst, src *IPAMPool) {
 	dst.Subnet = src.Subnet
 	dst.Gateway = src.Gateway
 	dst.IPRange = src.IPRange
@@ -2267,13 +2290,13 @@ func deriveDeepCopy_58(dst, src *IPAMPool) {
 	}
 }
 
-// deriveDeepCopy_59 recursively copies the contents of src into dst.
-func deriveDeepCopy_59(dst, src *GenericResource) {
+// deriveDeepCopy_60 recursively copies the contents of src into dst.
+func deriveDeepCopy_60(dst, src *GenericResource) {
 	if src.DiscreteResourceSpec == nil {
 		dst.DiscreteResourceSpec = nil
 	} else {
 		dst.DiscreteResourceSpec = new(DiscreteGenericResource)
-		deriveDeepCopy_60(dst.DiscreteResourceSpec, src.DiscreteResourceSpec)
+		deriveDeepCopy_61(dst.DiscreteResourceSpec, src.DiscreteResourceSpec)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2283,8 +2306,8 @@ func deriveDeepCopy_59(dst, src *GenericResource) {
 	}
 }
 
-// deriveDeepCopy_60 recursively copies the contents of src into dst.
-func deriveDeepCopy_60(dst, src *DiscreteGenericResource) {
+// deriveDeepCopy_61 recursively copies the contents of src into dst.
+func deriveDeepCopy_61(dst, src *DiscreteGenericResource) {
 	dst.Kind = src.Kind
 	dst.Value = src.Value
 	if src.Extensions != nil {
diff --git a/types/types.go b/types/types.go
index 0144b70..acfb355 100644
--- a/types/types.go
+++ b/types/types.go
@@ -541,6 +541,7 @@ type ServiceVolumeConfig struct {
 	Bind        *ServiceVolumeBind   `yaml:"bind,omitempty" json:"bind,omitempty"`
 	Volume      *ServiceVolumeVolume `yaml:"volume,omitempty" json:"volume,omitempty"`
 	Tmpfs       *ServiceVolumeTmpfs  `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"`
+	Image       *ServiceVolumeImage  `yaml:"image,omitempty" json:"image,omitempty"`
 
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }
@@ -575,6 +576,8 @@ const (
 	VolumeTypeNamedPipe = "npipe"
 	// VolumeTypeCluster is the type for mounting container storage interface (CSI) volumes
 	VolumeTypeCluster = "cluster"
+	// VolumeTypeImage is the tpe for mounting an image
+	VolumeTypeImage = "image"
 
 	// SElinuxShared share the volume content
 	SElinuxShared = "z"
@@ -618,8 +621,9 @@ const (
 
 // ServiceVolumeVolume are options for a service volume of type volume
 type ServiceVolumeVolume struct {
-	NoCopy  bool   `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
-	Subpath string `yaml:"subpath,omitempty" json:"subpath,omitempty"`
+	Labels  Mapping `yaml:"labels,omitempty" json:"labels,omitempty"`
+	NoCopy  bool    `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
+	Subpath string  `yaml:"subpath,omitempty" json:"subpath,omitempty"`
 
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }
@@ -633,6 +637,11 @@ type ServiceVolumeTmpfs struct {
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }
 
+type ServiceVolumeImage struct {
+	SubPath    string     `yaml:"subpath,omitempty" json:"subpath,omitempty"`
+	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
+}
+
 type FileMode int64
 
 // FileReferenceConfig for a reference to a swarm file object


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index bde742f..6b07c37 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3778,3 +3778,24 @@ services:
 		Options: map[string]string{"bar": "zot"},
 	})
 }
+
+func TestImageVolume(t *testing.T) {
+	p, err := loadYAML(`
+name: imageVolume
+services:
+  test:
+    volumes:
+      - type: image
+        source: app/image
+        target: /mnt/image
+        image:
+          subpath: /foo
+`)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services["test"].Volumes[0], types.ServiceVolumeConfig{
+		Type:   "image",
+		Source: "app/image",
+		Target: "/mnt/image",
+		Image:  &types.ServiceVolumeImage{SubPath: "/foo"},
+	})
+}