Repository: compose-spec/compose-go
Pull request #758 (2025-03-27 15:11:55)

introduce external services
https://docker.atlassian.net/browse/APCLI-1065


Patch:
diff --git a/loader/validate.go b/loader/validate.go
index aa57088..90d64ea 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -29,7 +29,7 @@ import (
 // checkConsistency validate a compose model is consistent
 func checkConsistency(project *types.Project) error { //nolint:gocyclo
 	for name, s := range project.Services {
-		if s.Build == nil && s.Image == "" {
+		if s.Build == nil && s.Image == "" && s.External == nil {
 			return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
 		}
 
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 4b6df9a..0e622c7 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -259,6 +259,20 @@
             }
           ]
         },
+        "external": {
+          "type": "object",
+          "properties": {
+            "type": {"type": "string"},
+            "options": {
+              "type": "object",
+              "patternProperties": {
+                "^.+$": {"type": ["string", "number", "null"]}
+              }
+            }
+          },
+          "additionalProperties": false,
+          "patternProperties": {"^x-": {}}
+        },
         "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
         "extra_hosts": {"$ref": "#/definitions/extra_hosts"},
         "gpus": {"$ref": "#/definitions/gpus"},
@@ -279,7 +293,6 @@
         "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
         "logging": {
           "type": "object",
-
           "properties": {
             "driver": {"type": "string"},
             "options": {
diff --git a/types/derived.gen.go b/types/derived.gen.go
index fd8e059..9e32e90 100644
--- a/types/derived.gen.go
+++ b/types/derived.gen.go
@@ -355,9 +355,15 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		}
 		copy(dst.Entrypoint, src.Entrypoint)
 	}
+	if src.External == nil {
+		dst.External = nil
+	} else {
+		dst.External = new(ExternalServiceConfig)
+		deriveDeepCopy_13(dst.External, src.External)
+	}
 	if src.Environment != nil {
 		dst.Environment = make(map[string]*string, len(src.Environment))
-		deriveDeepCopy_13(dst.Environment, src.Environment)
+		deriveDeepCopy_14(dst.Environment, src.Environment)
 	} else {
 		dst.Environment = nil
 	}
@@ -423,7 +429,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 	}
 	if src.ExtraHosts != nil {
 		dst.ExtraHosts = make(map[string][]string, len(src.ExtraHosts))
-		deriveDeepCopy_14(dst.ExtraHosts, src.ExtraHosts)
+		deriveDeepCopy_15(dst.ExtraHosts, src.ExtraHosts)
 	} else {
 		dst.ExtraHosts = nil
 	}
@@ -461,14 +467,14 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Gpus = make([]DeviceRequest, len(src.Gpus))
 		}
-		deriveDeepCopy_15(dst.Gpus, src.Gpus)
+		deriveDeepCopy_16(dst.Gpus, src.Gpus)
 	}
 	dst.Hostname = src.Hostname
 	if src.HealthCheck == nil {
 		dst.HealthCheck = nil
 	} else {
 		dst.HealthCheck = new(HealthCheckConfig)
-		deriveDeepCopy_16(dst.HealthCheck, src.HealthCheck)
+		deriveDeepCopy_17(dst.HealthCheck, src.HealthCheck)
 	}
 	dst.Image = src.Image
 	if src.Init == nil {
@@ -531,7 +537,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		dst.Logging = nil
 	} else {
 		dst.Logging = new(LoggingConfig)
-		deriveDeepCopy_17(dst.Logging, src.Logging)
+		deriveDeepCopy_18(dst.Logging, src.Logging)
 	}
 	dst.LogDriver = src.LogDriver
 	if src.LogOpt != nil {
@@ -549,7 +555,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 	dst.NetworkMode = src.NetworkMode
 	if src.Networks != nil {
 		dst.Networks = make(map[string]*ServiceNetworkConfig, len(src.Networks))
-		deriveDeepCopy_18(dst.Networks, src.Networks)
+		deriveDeepCopy_19(dst.Networks, src.Networks)
 	} else {
 		dst.Networks = nil
 	}
@@ -574,7 +580,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Ports = make([]ServicePortConfig, len(src.Ports))
 		}
-		deriveDeepCopy_19(dst.Ports, src.Ports)
+		deriveDeepCopy_20(dst.Ports, src.Ports)
 	}
 	dst.Privileged = src.Privileged
 	dst.PullPolicy = src.PullPolicy
@@ -603,7 +609,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets))
 		}
-		deriveDeepCopy_20(dst.Secrets, src.Secrets)
+		deriveDeepCopy_21(dst.Secrets, src.Secrets)
 	}
 	if src.SecurityOpt == nil {
 		dst.SecurityOpt = nil
@@ -665,7 +671,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 	dst.Tty = src.Tty
 	if src.Ulimits != nil {
 		dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits))
-		deriveDeepCopy_21(dst.Ulimits, src.Ulimits)
+		deriveDeepCopy_22(dst.Ulimits, src.Ulimits)
 	} else {
 		dst.Ulimits = nil
 	}
@@ -689,7 +695,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Volumes = make([]ServiceVolumeConfig, len(src.Volumes))
 		}
-		deriveDeepCopy_22(dst.Volumes, src.Volumes)
+		deriveDeepCopy_23(dst.Volumes, src.Volumes)
 	}
 	if src.VolumesFrom == nil {
 		dst.VolumesFrom = nil
@@ -726,7 +732,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.PostStart = make([]ServiceHook, len(src.PostStart))
 		}
-		deriveDeepCopy_23(dst.PostStart, src.PostStart)
+		deriveDeepCopy_24(dst.PostStart, src.PostStart)
 	}
 	if src.PreStop == nil {
 		dst.PreStop = nil
@@ -744,7 +750,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.PreStop = make([]ServiceHook, len(src.PreStop))
 		}
-		deriveDeepCopy_23(dst.PreStop, src.PreStop)
+		deriveDeepCopy_24(dst.PreStop, src.PreStop)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -770,7 +776,7 @@ func deriveDeepCopy_(dst, src map[string]NetworkConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(NetworkConfig)
-			deriveDeepCopy_24(field, &src_value)
+			deriveDeepCopy_25(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -781,7 +787,7 @@ func deriveDeepCopy_1(dst, src map[string]VolumeConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(VolumeConfig)
-			deriveDeepCopy_25(field, &src_value)
+			deriveDeepCopy_26(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -792,7 +798,7 @@ func deriveDeepCopy_2(dst, src map[string]SecretConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(SecretConfig)
-			deriveDeepCopy_26(field, &src_value)
+			deriveDeepCopy_27(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -803,7 +809,7 @@ func deriveDeepCopy_3(dst, src map[string]ConfigObjConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(ConfigObjConfig)
-			deriveDeepCopy_27(field, &src_value)
+			deriveDeepCopy_28(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -841,7 +847,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 	}
 	if src.Args != nil {
 		dst.Args = make(map[string]*string, len(src.Args))
-		deriveDeepCopy_13(dst.Args, src.Args)
+		deriveDeepCopy_14(dst.Args, src.Args)
 	} else {
 		dst.Args = nil
 	}
@@ -915,7 +921,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 	dst.Pull = src.Pull
 	if src.ExtraHosts != nil {
 		dst.ExtraHosts = make(map[string][]string, len(src.ExtraHosts))
-		deriveDeepCopy_14(dst.ExtraHosts, src.ExtraHosts)
+		deriveDeepCopy_15(dst.ExtraHosts, src.ExtraHosts)
 	} else {
 		dst.ExtraHosts = nil
 	}
@@ -938,7 +944,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 		} else {
 			dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets))
 		}
-		deriveDeepCopy_20(dst.Secrets, src.Secrets)
+		deriveDeepCopy_21(dst.Secrets, src.Secrets)
 	}
 	dst.ShmSize = src.ShmSize
 	if src.Tags == nil {
@@ -961,7 +967,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 	}
 	if src.Ulimits != nil {
 		dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits))
-		deriveDeepCopy_21(dst.Ulimits, src.Ulimits)
+		deriveDeepCopy_22(dst.Ulimits, src.Ulimits)
 	} else {
 		dst.Ulimits = nil
 	}
@@ -1010,7 +1016,7 @@ func deriveDeepCopy_6(dst, src *DevelopConfig) {
 		} else {
 			dst.Watch = make([]Trigger, len(src.Watch))
 		}
-		deriveDeepCopy_28(dst.Watch, src.Watch)
+		deriveDeepCopy_29(dst.Watch, src.Watch)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1039,7 +1045,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.WeightDevice = make([]WeightDevice, len(src.WeightDevice))
 		}
-		deriveDeepCopy_29(dst.WeightDevice, src.WeightDevice)
+		deriveDeepCopy_30(dst.WeightDevice, src.WeightDevice)
 	}
 	if src.DeviceReadBps == nil {
 		dst.DeviceReadBps = nil
@@ -1057,7 +1063,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceReadBps = make([]ThrottleDevice, len(src.DeviceReadBps))
 		}
-		deriveDeepCopy_30(dst.DeviceReadBps, src.DeviceReadBps)
+		deriveDeepCopy_31(dst.DeviceReadBps, src.DeviceReadBps)
 	}
 	if src.DeviceReadIOps == nil {
 		dst.DeviceReadIOps = nil
@@ -1075,7 +1081,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceReadIOps = make([]ThrottleDevice, len(src.DeviceReadIOps))
 		}
-		deriveDeepCopy_30(dst.DeviceReadIOps, src.DeviceReadIOps)
+		deriveDeepCopy_31(dst.DeviceReadIOps, src.DeviceReadIOps)
 	}
 	if src.DeviceWriteBps == nil {
 		dst.DeviceWriteBps = nil
@@ -1093,7 +1099,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceWriteBps = make([]ThrottleDevice, len(src.DeviceWriteBps))
 		}
-		deriveDeepCopy_30(dst.DeviceWriteBps, src.DeviceWriteBps)
+		deriveDeepCopy_31(dst.DeviceWriteBps, src.DeviceWriteBps)
 	}
 	if src.DeviceWriteIOps == nil {
 		dst.DeviceWriteIOps = nil
@@ -1111,7 +1117,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceWriteIOps = make([]ThrottleDevice, len(src.DeviceWriteIOps))
 		}
-		deriveDeepCopy_30(dst.DeviceWriteIOps, src.DeviceWriteIOps)
+		deriveDeepCopy_31(dst.DeviceWriteIOps, src.DeviceWriteIOps)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1126,7 +1132,7 @@ func deriveDeepCopy_8(dst, src []ServiceConfigObjConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceConfigObjConfig)
-			deriveDeepCopy_31(field, &src_value)
+			deriveDeepCopy_32(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
@@ -1150,7 +1156,7 @@ func deriveDeepCopy_10(dst, src map[string]ServiceDependency) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(ServiceDependency)
-			deriveDeepCopy_32(field, &src_value)
+			deriveDeepCopy_33(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -1175,28 +1181,28 @@ func deriveDeepCopy_11(dst, src *DeployConfig) {
 		dst.UpdateConfig = nil
 	} else {
 		dst.UpdateConfig = new(UpdateConfig)
-		deriveDeepCopy_33(dst.UpdateConfig, src.UpdateConfig)
+		deriveDeepCopy_34(dst.UpdateConfig, src.UpdateConfig)
 	}
 	if src.RollbackConfig == nil {
 		dst.RollbackConfig = nil
 	} else {
 		dst.RollbackConfig = new(UpdateConfig)
-		deriveDeepCopy_33(dst.RollbackConfig, src.RollbackConfig)
+		deriveDeepCopy_34(dst.RollbackConfig, src.RollbackConfig)
 	}
 	func() {
 		field := new(Resources)
-		deriveDeepCopy_34(field, &src.Resources)
+		deriveDeepCopy_35(field, &src.Resources)
 		dst.Resources = *field
 	}()
 	if src.RestartPolicy == nil {
 		dst.RestartPolicy = nil
 	} else {
 		dst.RestartPolicy = new(RestartPolicy)
-		deriveDeepCopy_35(dst.RestartPolicy, src.RestartPolicy)
+		deriveDeepCopy_36(dst.RestartPolicy, src.RestartPolicy)
 	}
 	func() {
 		field := new(Placement)
-		deriveDeepCopy_36(field, &src.Placement)
+		deriveDeepCopy_37(field, &src.Placement)
 		dst.Placement = *field
 	}()
 	dst.EndpointMode = src.EndpointMode
@@ -1213,14 +1219,31 @@ func deriveDeepCopy_12(dst, src []DeviceMapping) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(DeviceMapping)
-			deriveDeepCopy_37(field, &src_value)
+			deriveDeepCopy_38(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
 // deriveDeepCopy_13 recursively copies the contents of src into dst.
-func deriveDeepCopy_13(dst, src map[string]*string) {
+func deriveDeepCopy_13(dst, src *ExternalServiceConfig) {
+	dst.Type = src.Type
+	if src.Options != nil {
+		dst.Options = make(map[string]string, len(src.Options))
+		deriveDeepCopy_4(dst.Options, src.Options)
+	} else {
+		dst.Options = nil
+	}
+	if src.Extensions != nil {
+		dst.Extensions = make(map[string]any, len(src.Extensions))
+		src.Extensions.DeepCopy(dst.Extensions)
+	} else {
+		dst.Extensions = nil
+	}
+}
+
+// deriveDeepCopy_14 recursively copies the contents of src into dst.
+func deriveDeepCopy_14(dst, src map[string]*string) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1234,8 +1257,8 @@ func deriveDeepCopy_13(dst, src map[string]*string) {
 	}
 }
 
-// deriveDeepCopy_14 recursively copies the contents of src into dst.
-func deriveDeepCopy_14(dst, src map[string][]string) {
+// deriveDeepCopy_15 recursively copies the contents of src into dst.
+func deriveDeepCopy_15(dst, src map[string][]string) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1261,19 +1284,19 @@ func deriveDeepCopy_14(dst, src map[string][]string) {
 	}
 }
 
-// deriveDeepCopy_15 recursively copies the contents of src into dst.
-func deriveDeepCopy_15(dst, src []DeviceRequest) {
+// deriveDeepCopy_16 recursively copies the contents of src into dst.
+func deriveDeepCopy_16(dst, src []DeviceRequest) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(DeviceRequest)
-			deriveDeepCopy_38(field, &src_value)
+			deriveDeepCopy_39(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_16 recursively copies the contents of src into dst.
-func deriveDeepCopy_16(dst, src *HealthCheckConfig) {
+// deriveDeepCopy_17 recursively copies the contents of src into dst.
+func deriveDeepCopy_17(dst, src *HealthCheckConfig) {
 	if src.Test == nil {
 		dst.Test = nil
 	} else {
@@ -1331,8 +1354,8 @@ func deriveDeepCopy_16(dst, src *HealthCheckConfig) {
 	}
 }
 
-// deriveDeepCopy_17 recursively copies the contents of src into dst.
-func deriveDeepCopy_17(dst, src *LoggingConfig) {
+// deriveDeepCopy_18 recursively copies the contents of src into dst.
+func deriveDeepCopy_18(dst, src *LoggingConfig) {
 	dst.Driver = src.Driver
 	if src.Options != nil {
 		dst.Options = make(map[string]string, len(src.Options))
@@ -1348,8 +1371,8 @@ func deriveDeepCopy_17(dst, src *LoggingConfig) {
 	}
 }
 
-// deriveDeepCopy_18 recursively copies the contents of src into dst.
-func deriveDeepCopy_18(dst, src map[string]*ServiceNetworkConfig) {
+// deriveDeepCopy_19 recursively copies the contents of src into dst.
+func deriveDeepCopy_19(dst, src map[string]*ServiceNetworkConfig) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1358,35 +1381,35 @@ func deriveDeepCopy_18(dst, src map[string]*ServiceNetworkConfig) {
 			dst[src_key] = nil
 		} else {
 			dst[src_key] = new(ServiceNetworkConfig)
-			deriveDeepCopy_39(dst[src_key], src_value)
+			deriveDeepCopy_40(dst[src_key], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_19 recursively copies the contents of src into dst.
-func deriveDeepCopy_19(dst, src []ServicePortConfig) {
+// deriveDeepCopy_20 recursively copies the contents of src into dst.
+func deriveDeepCopy_20(dst, src []ServicePortConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServicePortConfig)
-			deriveDeepCopy_40(field, &src_value)
+			deriveDeepCopy_41(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_20 recursively copies the contents of src into dst.
-func deriveDeepCopy_20(dst, src []ServiceSecretConfig) {
+// deriveDeepCopy_21 recursively copies the contents of src into dst.
+func deriveDeepCopy_21(dst, src []ServiceSecretConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceSecretConfig)
-			deriveDeepCopy_41(field, &src_value)
+			deriveDeepCopy_42(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_21 recursively copies the contents of src into dst.
-func deriveDeepCopy_21(dst, src map[string]*UlimitsConfig) {
+// deriveDeepCopy_22 recursively copies the contents of src into dst.
+func deriveDeepCopy_22(dst, src map[string]*UlimitsConfig) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1395,35 +1418,35 @@ func deriveDeepCopy_21(dst, src map[string]*UlimitsConfig) {
 			dst[src_key] = nil
 		} else {
 			dst[src_key] = new(UlimitsConfig)
-			deriveDeepCopy_42(dst[src_key], src_value)
+			deriveDeepCopy_43(dst[src_key], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_22 recursively copies the contents of src into dst.
-func deriveDeepCopy_22(dst, src []ServiceVolumeConfig) {
+// deriveDeepCopy_23 recursively copies the contents of src into dst.
+func deriveDeepCopy_23(dst, src []ServiceVolumeConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceVolumeConfig)
-			deriveDeepCopy_43(field, &src_value)
+			deriveDeepCopy_44(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_23 recursively copies the contents of src into dst.
-func deriveDeepCopy_23(dst, src []ServiceHook) {
+// deriveDeepCopy_24 recursively copies the contents of src into dst.
+func deriveDeepCopy_24(dst, src []ServiceHook) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceHook)
-			deriveDeepCopy_44(field, &src_value)
+			deriveDeepCopy_45(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_24 recursively copies the contents of src into dst.
-func deriveDeepCopy_24(dst, src *NetworkConfig) {
+// deriveDeepCopy_25 recursively copies the contents of src into dst.
+func deriveDeepCopy_25(dst, src *NetworkConfig) {
 	dst.Name = src.Name
 	dst.Driver = src.Driver
 	if src.DriverOpts != nil {
@@ -1434,7 +1457,7 @@ func deriveDeepCopy_24(dst, src *NetworkConfig) {
 	}
 	func() {
 		field := new(IPAMConfig)
-		deriveDeepCopy_45(field, &src.Ipam)
+		deriveDeepCopy_46(field, &src.Ipam)
 		dst.Ipam = *field
 	}()
 	dst.External = src.External
@@ -1472,8 +1495,8 @@ func deriveDeepCopy_24(dst, src *NetworkConfig) {
 	}
 }
 
-// deriveDeepCopy_25 recursively copies the contents of src into dst.
-func deriveDeepCopy_25(dst, src *VolumeConfig) {
+// deriveDeepCopy_26 recursively copies the contents of src into dst.
+func deriveDeepCopy_26(dst, src *VolumeConfig) {
 	dst.Name = src.Name
 	dst.Driver = src.Driver
 	if src.DriverOpts != nil {
@@ -1503,8 +1526,8 @@ func deriveDeepCopy_25(dst, src *VolumeConfig) {
 	}
 }
 
-// deriveDeepCopy_26 recursively copies the contents of src into dst.
-func deriveDeepCopy_26(dst, src *SecretConfig) {
+// deriveDeepCopy_27 recursively copies the contents of src into dst.
+func deriveDeepCopy_27(dst, src *SecretConfig) {
 	dst.Name = src.Name
 	dst.File = src.File
 	dst.Environment = src.Environment
@@ -1533,8 +1556,8 @@ func deriveDeepCopy_26(dst, src *SecretConfig) {
 	}
 }
 
-// deriveDeepCopy_27 recursively copies the contents of src into dst.
-func deriveDeepCopy_27(dst, src *ConfigObjConfig) {
+// deriveDeepCopy_28 recursively copies the contents of src into dst.
+func deriveDeepCopy_28(dst, src *ConfigObjConfig) {
 	dst.Name = src.Name
 	dst.File = src.File
 	dst.Environment = src.Environment
@@ -1563,41 +1586,41 @@ func deriveDeepCopy_27(dst, src *ConfigObjConfig) {
 	}
 }
 
-// deriveDeepCopy_28 recursively copies the contents of src into dst.
-func deriveDeepCopy_28(dst, src []Trigger) {
+// deriveDeepCopy_29 recursively copies the contents of src into dst.
+func deriveDeepCopy_29(dst, src []Trigger) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(Trigger)
-			deriveDeepCopy_46(field, &src_value)
+			deriveDeepCopy_47(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_29 recursively copies the contents of src into dst.
-func deriveDeepCopy_29(dst, src []WeightDevice) {
+// deriveDeepCopy_30 recursively copies the contents of src into dst.
+func deriveDeepCopy_30(dst, src []WeightDevice) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(WeightDevice)
-			deriveDeepCopy_47(field, &src_value)
+			deriveDeepCopy_48(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_30 recursively copies the contents of src into dst.
-func deriveDeepCopy_30(dst, src []ThrottleDevice) {
+// deriveDeepCopy_31 recursively copies the contents of src into dst.
+func deriveDeepCopy_31(dst, src []ThrottleDevice) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ThrottleDevice)
-			deriveDeepCopy_48(field, &src_value)
+			deriveDeepCopy_49(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_31 recursively copies the contents of src into dst.
-func deriveDeepCopy_31(dst, src *ServiceConfigObjConfig) {
+// deriveDeepCopy_32 recursively copies the contents of src into dst.
+func deriveDeepCopy_32(dst, src *ServiceConfigObjConfig) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.UID = src.UID
@@ -1616,8 +1639,8 @@ func deriveDeepCopy_31(dst, src *ServiceConfigObjConfig) {
 	}
 }
 
-// deriveDeepCopy_32 recursively copies the contents of src into dst.
-func deriveDeepCopy_32(dst, src *ServiceDependency) {
+// deriveDeepCopy_33 recursively copies the contents of src into dst.
+func deriveDeepCopy_33(dst, src *ServiceDependency) {
 	dst.Condition = src.Condition
 	dst.Restart = src.Restart
 	if src.Extensions != nil {
@@ -1629,8 +1652,8 @@ func deriveDeepCopy_32(dst, src *ServiceDependency) {
 	dst.Required = src.Required
 }
 
-// deriveDeepCopy_33 recursively copies the contents of src into dst.
-func deriveDeepCopy_33(dst, src *UpdateConfig) {
+// deriveDeepCopy_34 recursively copies the contents of src into dst.
+func deriveDeepCopy_34(dst, src *UpdateConfig) {
 	if src.Parallelism == nil {
 		dst.Parallelism = nil
 	} else {
@@ -1650,19 +1673,19 @@ func deriveDeepCopy_33(dst, src *UpdateConfig) {
 	}
 }
 
-// deriveDeepCopy_34 recursively copies the contents of src into dst.
-func deriveDeepCopy_34(dst, src *Resources) {
+// deriveDeepCopy_35 recursively copies the contents of src into dst.
+func deriveDeepCopy_35(dst, src *Resources) {
 	if src.Limits == nil {
 		dst.Limits = nil
 	} else {
 		dst.Limits = new(Resource)
-		deriveDeepCopy_49(dst.Limits, src.Limits)
+		deriveDeepCopy_50(dst.Limits, src.Limits)
 	}
 	if src.Reservations == nil {
 		dst.Reservations = nil
 	} else {
 		dst.Reservations = new(Resource)
-		deriveDeepCopy_49(dst.Reservations, src.Reservations)
+		deriveDeepCopy_50(dst.Reservations, src.Reservations)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1672,8 +1695,8 @@ func deriveDeepCopy_34(dst, src *Resources) {
 	}
 }
 
-// deriveDeepCopy_35 recursively copies the contents of src into dst.
-func deriveDeepCopy_35(dst, src *RestartPolicy) {
+// deriveDeepCopy_36 recursively copies the contents of src into dst.
+func deriveDeepCopy_36(dst, src *RestartPolicy) {
 	dst.Condition = src.Condition
 	if src.Delay == nil {
 		dst.Delay = nil
@@ -1701,8 +1724,8 @@ func deriveDeepCopy_35(dst, src *RestartPolicy) {
 	}
 }
 
-// deriveDeepCopy_36 recursively copies the contents of src into dst.
-func deriveDeepCopy_36(dst, src *Placement) {
+// deriveDeepCopy_37 recursively copies the contents of src into dst.
+func deriveDeepCopy_37(dst, src *Placement) {
 	if src.Constraints == nil {
 		dst.Constraints = nil
 	} else {
@@ -1737,7 +1760,7 @@ func deriveDeepCopy_36(dst, src *Placement) {
 		} else {
 			dst.Preferences = make([]PlacementPreferences, len(src.Preferences))
 		}
-		deriveDeepCopy_50(dst.Preferences, src.Preferences)
+		deriveDeepCopy_51(dst.Preferences, src.Preferences)
 	}
 	dst.MaxReplicas = src.MaxReplicas
 	if src.Extensions != nil {
@@ -1748,8 +1771,8 @@ func deriveDeepCopy_36(dst, src *Placement) {
 	}
 }
 
-// deriveDeepCopy_37 recursively copies the contents of src into dst.
-func deriveDeepCopy_37(dst, src *DeviceMapping) {
+// deriveDeepCopy_38 recursively copies the contents of src into dst.
+func deriveDeepCopy_38(dst, src *DeviceMapping) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.Permissions = src.Permissions
@@ -1761,8 +1784,8 @@ func deriveDeepCopy_37(dst, src *DeviceMapping) {
 	}
 }
 
-// deriveDeepCopy_38 recursively copies the contents of src into dst.
-func deriveDeepCopy_38(dst, src *DeviceRequest) {
+// deriveDeepCopy_39 recursively copies the contents of src into dst.
+func deriveDeepCopy_39(dst, src *DeviceRequest) {
 	if src.Capabilities == nil {
 		dst.Capabilities = nil
 	} else {
@@ -1809,8 +1832,8 @@ func deriveDeepCopy_38(dst, src *DeviceRequest) {
 	}
 }
 
-// deriveDeepCopy_39 recursively copies the contents of src into dst.
-func deriveDeepCopy_39(dst, src *ServiceNetworkConfig) {
+// deriveDeepCopy_40 recursively copies the contents of src into dst.
+func deriveDeepCopy_40(dst, src *ServiceNetworkConfig) {
 	dst.Priority = src.Priority
 	dst.GatewayPriority = src.GatewayPriority
 	if src.Aliases == nil {
@@ -1866,8 +1889,8 @@ func deriveDeepCopy_39(dst, src *ServiceNetworkConfig) {
 	}
 }
 
-// deriveDeepCopy_40 recursively copies the contents of src into dst.
-func deriveDeepCopy_40(dst, src *ServicePortConfig) {
+// deriveDeepCopy_41 recursively copies the contents of src into dst.
+func deriveDeepCopy_41(dst, src *ServicePortConfig) {
 	dst.Name = src.Name
 	dst.Mode = src.Mode
 	dst.HostIP = src.HostIP
@@ -1883,8 +1906,8 @@ func deriveDeepCopy_40(dst, src *ServicePortConfig) {
 	}
 }
 
-// deriveDeepCopy_41 recursively copies the contents of src into dst.
-func deriveDeepCopy_41(dst, src *ServiceSecretConfig) {
+// deriveDeepCopy_42 recursively copies the contents of src into dst.
+func deriveDeepCopy_42(dst, src *ServiceSecretConfig) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.UID = src.UID
@@ -1903,8 +1926,8 @@ func deriveDeepCopy_41(dst, src *ServiceSecretConfig) {
 	}
 }
 
-// deriveDeepCopy_42 recursively copies the contents of src into dst.
-func deriveDeepCopy_42(dst, src *UlimitsConfig) {
+// deriveDeepCopy_43 recursively copies the contents of src into dst.
+func deriveDeepCopy_43(dst, src *UlimitsConfig) {
 	dst.Single = src.Single
 	dst.Soft = src.Soft
 	dst.Hard = src.Hard
@@ -1916,8 +1939,8 @@ func deriveDeepCopy_42(dst, src *UlimitsConfig) {
 	}
 }
 
-// deriveDeepCopy_43 recursively copies the contents of src into dst.
-func deriveDeepCopy_43(dst, src *ServiceVolumeConfig) {
+// deriveDeepCopy_44 recursively copies the contents of src into dst.
+func deriveDeepCopy_44(dst, src *ServiceVolumeConfig) {
 	dst.Type = src.Type
 	dst.Source = src.Source
 	dst.Target = src.Target
@@ -1927,19 +1950,19 @@ func deriveDeepCopy_43(dst, src *ServiceVolumeConfig) {
 		dst.Bind = nil
 	} else {
 		dst.Bind = new(ServiceVolumeBind)
-		deriveDeepCopy_51(dst.Bind, src.Bind)
+		deriveDeepCopy_52(dst.Bind, src.Bind)
 	}
 	if src.Volume == nil {
 		dst.Volume = nil
 	} else {
 		dst.Volume = new(ServiceVolumeVolume)
-		deriveDeepCopy_52(dst.Volume, src.Volume)
+		deriveDeepCopy_53(dst.Volume, src.Volume)
 	}
 	if src.Tmpfs == nil {
 		dst.Tmpfs = nil
 	} else {
 		dst.Tmpfs = new(ServiceVolumeTmpfs)
-		deriveDeepCopy_53(dst.Tmpfs, src.Tmpfs)
+		deriveDeepCopy_54(dst.Tmpfs, src.Tmpfs)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1949,8 +1972,8 @@ func deriveDeepCopy_43(dst, src *ServiceVolumeConfig) {
 	}
 }
 
-// deriveDeepCopy_44 recursively copies the contents of src into dst.
-func deriveDeepCopy_44(dst, src *ServiceHook) {
+// deriveDeepCopy_45 recursively copies the contents of src into dst.
+func deriveDeepCopy_45(dst, src *ServiceHook) {
 	if src.Command == nil {
 		dst.Command = nil
 	} else {
@@ -1974,7 +1997,7 @@ func deriveDeepCopy_44(dst, src *ServiceHook) {
 	dst.WorkingDir = src.WorkingDir
 	if src.Environment != nil {
 		dst.Environment = make(map[string]*string, len(src.Environment))
-		deriveDeepCopy_13(dst.Environment, src.Environment)
+		deriveDeepCopy_14(dst.Environment, src.Environment)
 	} else {
 		dst.Environment = nil
 	}
@@ -1986,8 +2009,8 @@ func deriveDeepCopy_44(dst, src *ServiceHook) {
 	}
 }
 
-// deriveDeepCopy_45 recursively copies the contents of src into dst.
-func deriveDeepCopy_45(dst, src *IPAMConfig) {
+// deriveDeepCopy_46 recursively copies the contents of src into dst.
+func deriveDeepCopy_46(dst, src *IPAMConfig) {
 	dst.Driver = src.Driver
 	if src.Config == nil {
 		dst.Config = nil
@@ -2005,7 +2028,7 @@ func deriveDeepCopy_45(dst, src *IPAMConfig) {
 		} else {
 			dst.Config = make([]*IPAMPool, len(src.Config))
 		}
-		deriveDeepCopy_54(dst.Config, src.Config)
+		deriveDeepCopy_55(dst.Config, src.Config)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2015,14 +2038,14 @@ func deriveDeepCopy_45(dst, src *IPAMConfig) {
 	}
 }
 
-// deriveDeepCopy_46 recursively copies the contents of src into dst.
-func deriveDeepCopy_46(dst, src *Trigger) {
+// deriveDeepCopy_47 recursively copies the contents of src into dst.
+func deriveDeepCopy_47(dst, src *Trigger) {
 	dst.Path = src.Path
 	dst.Action = src.Action
 	dst.Target = src.Target
 	func() {
 		field := new(ServiceHook)
-		deriveDeepCopy_44(field, &src.Exec)
+		deriveDeepCopy_45(field, &src.Exec)
 		dst.Exec = *field
 	}()
 	if src.Include == nil {
@@ -2069,8 +2092,8 @@ func deriveDeepCopy_46(dst, src *Trigger) {
 	}
 }
 
-// deriveDeepCopy_47 recursively copies the contents of src into dst.
-func deriveDeepCopy_47(dst, src *WeightDevice) {
+// deriveDeepCopy_48 recursively copies the contents of src into dst.
+func deriveDeepCopy_48(dst, src *WeightDevice) {
 	dst.Path = src.Path
 	dst.Weight = src.Weight
 	if src.Extensions != nil {
@@ -2081,8 +2104,8 @@ func deriveDeepCopy_47(dst, src *WeightDevice) {
 	}
 }
 
-// deriveDeepCopy_48 recursively copies the contents of src into dst.
-func deriveDeepCopy_48(dst, src *ThrottleDevice) {
+// deriveDeepCopy_49 recursively copies the contents of src into dst.
+func deriveDeepCopy_49(dst, src *ThrottleDevice) {
 	dst.Path = src.Path
 	dst.Rate = src.Rate
 	if src.Extensions != nil {
@@ -2093,8 +2116,8 @@ func deriveDeepCopy_48(dst, src *ThrottleDevice) {
 	}
 }
 
-// deriveDeepCopy_49 recursively copies the contents of src into dst.
-func deriveDeepCopy_49(dst, src *Resource) {
+// deriveDeepCopy_50 recursively copies the contents of src into dst.
+func deriveDeepCopy_50(dst, src *Resource) {
 	dst.NanoCPUs = src.NanoCPUs
 	dst.MemoryBytes = src.MemoryBytes
 	dst.Pids = src.Pids
@@ -2114,7 +2137,7 @@ func deriveDeepCopy_49(dst, src *Resource) {
 		} else {
 			dst.Devices = make([]DeviceRequest, len(src.Devices))
 		}
-		deriveDeepCopy_15(dst.Devices, src.Devices)
+		deriveDeepCopy_16(dst.Devices, src.Devices)
 	}
 	if src.GenericResources == nil {
 		dst.GenericResources = nil
@@ -2132,7 +2155,7 @@ func deriveDeepCopy_49(dst, src *Resource) {
 		} else {
 			dst.GenericResources = make([]GenericResource, len(src.GenericResources))
 		}
-		deriveDeepCopy_55(dst.GenericResources, src.GenericResources)
+		deriveDeepCopy_56(dst.GenericResources, src.GenericResources)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2142,19 +2165,19 @@ func deriveDeepCopy_49(dst, src *Resource) {
 	}
 }
 
-// deriveDeepCopy_50 recursively copies the contents of src into dst.
-func deriveDeepCopy_50(dst, src []PlacementPreferences) {
+// deriveDeepCopy_51 recursively copies the contents of src into dst.
+func deriveDeepCopy_51(dst, src []PlacementPreferences) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(PlacementPreferences)
-			deriveDeepCopy_56(field, &src_value)
+			deriveDeepCopy_57(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_51 recursively copies the contents of src into dst.
-func deriveDeepCopy_51(dst, src *ServiceVolumeBind) {
+// deriveDeepCopy_52 recursively copies the contents of src into dst.
+func deriveDeepCopy_52(dst, src *ServiceVolumeBind) {
 	dst.SELinux = src.SELinux
 	dst.Propagation = src.Propagation
 	dst.CreateHostPath = src.CreateHostPath
@@ -2167,8 +2190,8 @@ func deriveDeepCopy_51(dst, src *ServiceVolumeBind) {
 	}
 }
 
-// deriveDeepCopy_52 recursively copies the contents of src into dst.
-func deriveDeepCopy_52(dst, src *ServiceVolumeVolume) {
+// deriveDeepCopy_53 recursively copies the contents of src into dst.
+func deriveDeepCopy_53(dst, src *ServiceVolumeVolume) {
 	dst.NoCopy = src.NoCopy
 	dst.Subpath = src.Subpath
 	if src.Extensions != nil {
@@ -2179,8 +2202,8 @@ func deriveDeepCopy_52(dst, src *ServiceVolumeVolume) {
 	}
 }
 
-// deriveDeepCopy_53 recursively copies the contents of src into dst.
-func deriveDeepCopy_53(dst, src *ServiceVolumeTmpfs) {
+// deriveDeepCopy_54 recursively copies the contents of src into dst.
+func deriveDeepCopy_54(dst, src *ServiceVolumeTmpfs) {
 	dst.Size = src.Size
 	dst.Mode = src.Mode
 	if src.Extensions != nil {
@@ -2191,31 +2214,31 @@ func deriveDeepCopy_53(dst, src *ServiceVolumeTmpfs) {
 	}
 }
 
-// deriveDeepCopy_54 recursively copies the contents of src into dst.
-func deriveDeepCopy_54(dst, src []*IPAMPool) {
+// deriveDeepCopy_55 recursively copies the contents of src into dst.
+func deriveDeepCopy_55(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_57(dst[src_i], src_value)
+			deriveDeepCopy_58(dst[src_i], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_55 recursively copies the contents of src into dst.
-func deriveDeepCopy_55(dst, src []GenericResource) {
+// deriveDeepCopy_56 recursively copies the contents of src into dst.
+func deriveDeepCopy_56(dst, src []GenericResource) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(GenericResource)
-			deriveDeepCopy_58(field, &src_value)
+			deriveDeepCopy_59(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_56 recursively copies the contents of src into dst.
-func deriveDeepCopy_56(dst, src *PlacementPreferences) {
+// deriveDeepCopy_57 recursively copies the contents of src into dst.
+func deriveDeepCopy_57(dst, src *PlacementPreferences) {
 	dst.Spread = src.Spread
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2225,8 +2248,8 @@ func deriveDeepCopy_56(dst, src *PlacementPreferences) {
 	}
 }
 
-// deriveDeepCopy_57 recursively copies the contents of src into dst.
-func deriveDeepCopy_57(dst, src *IPAMPool) {
+// deriveDeepCopy_58 recursively copies the contents of src into dst.
+func deriveDeepCopy_58(dst, src *IPAMPool) {
 	dst.Subnet = src.Subnet
 	dst.Gateway = src.Gateway
 	dst.IPRange = src.IPRange
@@ -2244,13 +2267,13 @@ func deriveDeepCopy_57(dst, src *IPAMPool) {
 	}
 }
 
-// deriveDeepCopy_58 recursively copies the contents of src into dst.
-func deriveDeepCopy_58(dst, src *GenericResource) {
+// deriveDeepCopy_59 recursively copies the contents of src into dst.
+func deriveDeepCopy_59(dst, src *GenericResource) {
 	if src.DiscreteResourceSpec == nil {
 		dst.DiscreteResourceSpec = nil
 	} else {
 		dst.DiscreteResourceSpec = new(DiscreteGenericResource)
-		deriveDeepCopy_59(dst.DiscreteResourceSpec, src.DiscreteResourceSpec)
+		deriveDeepCopy_60(dst.DiscreteResourceSpec, src.DiscreteResourceSpec)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2260,8 +2283,8 @@ func deriveDeepCopy_58(dst, src *GenericResource) {
 	}
 }
 
-// deriveDeepCopy_59 recursively copies the contents of src into dst.
-func deriveDeepCopy_59(dst, src *DiscreteGenericResource) {
+// deriveDeepCopy_60 recursively copies the contents of src into dst.
+func deriveDeepCopy_60(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 fe95444..189b5e7 100644
--- a/types/types.go
+++ b/types/types.go
@@ -75,8 +75,8 @@ type ServiceConfig struct {
 	// If set, overrides ENTRYPOINT from the image.
 	//
 	// Set to `[]` or an empty string to clear the entrypoint from the image.
-	Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details.
-
+	Entrypoint      ShellCommand                     `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details.
+	External        *ExternalServiceConfig           `yaml:"external,omitempty" json:"external,omitempty"`
 	Environment     MappingWithEquals                `yaml:"environment,omitempty" json:"environment,omitempty"`
 	EnvFiles        []EnvFile                        `yaml:"env_file,omitempty" json:"env_file,omitempty"`
 	Expose          StringOrNumberList               `yaml:"expose,omitempty" json:"expose,omitempty"`
@@ -142,6 +142,12 @@ type ServiceConfig struct {
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }
 
+type ExternalServiceConfig struct {
+	Type       string     `yaml:"type,omitempty" json:"driver,omitempty"`
+	Options    Options    `yaml:"options,omitempty" json:"options,omitempty"`
+	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
+}
+
 // MarshalYAML makes ServiceConfig implement yaml.Marshaller
 func (s ServiceConfig) MarshalYAML() (interface{}, error) {
 	type t ServiceConfig


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 3902888..d28cc5f 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3760,3 +3760,20 @@ services:
 	assert.Equal(t, len(p.Services["test"].Secrets), 1)
 	assert.Equal(t, *p.Services["test"].Secrets[0].Mode, types.FileMode(0o440))
 }
+
+func TestExternalService(t *testing.T) {
+	p, err := loadYAML(`
+name: external-service
+services:
+  test:
+    external:
+      type: foo
+      options:
+        bar: zot
+`)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services["test"].External, &types.ExternalServiceConfig{
+		Type:    "foo",
+		Options: map[string]string{"bar": "zot"},
+	})
+}