Repository: compose-spec/compose-go
Pull request #164 (2021-08-06 08:50:55)

introduce _merge to merge serviceConfigs
define a single place to handle service configuration merge, managing corner-cases

see https://github.com/compose-spec/compose-go/pull/163 for context

ndeloof: @lewiscowleschipuk wdyt? 
lewiscowleschipuk: Thank you! @MrHuli, time to bug Docker to update Docker Desktop 😁 
lewiscowleschipuk: I see that this now has merge conflicts... (oh the irony).

I know it's cheeky to ask, but as this is being shipped by the docker team out to many computers, are they involved in this project at all? I'm wondering when this finally does merge, how long it will be before it is reflected in the production docker release which has this edge case bug.

Patch:
diff --git a/loader/loader.go b/loader/loader.go
index b0a478f..d868c72 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -32,14 +32,13 @@ import (
 	"github.com/compose-spec/compose-go/schema"
 	"github.com/compose-spec/compose-go/template"
 	"github.com/compose-spec/compose-go/types"
-	units "github.com/docker/go-units"
-	"github.com/imdario/mergo"
-	shellwords "github.com/mattn/go-shellwords"
+	"github.com/docker/go-units"
+	"github.com/mattn/go-shellwords"
 	"github.com/mitchellh/mapstructure"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/ulyssessouza/godotenv"
-	yaml "gopkg.in/yaml.v2"
+	"gopkg.in/yaml.v2"
 )
 
 // Options supported by Load
@@ -504,10 +503,10 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
 			}
 		}
 
-		if err := mergo.Merge(baseService, serviceConfig, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
-			return nil, errors.Wrapf(err, "cannot merge service %s", name)
+		serviceConfig, err = _merge(baseService, serviceConfig)
+		if err != nil {
+			return nil, err
 		}
-		serviceConfig = baseService
 	}
 
 	return serviceConfig, nil
diff --git a/loader/merge.go b/loader/merge.go
index b6bac39..dd4fe42 100644
--- a/loader/merge.go
+++ b/loader/merge.go
@@ -87,13 +87,11 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig,
 	for name, overrideService := range overrideServices {
 		overrideService := overrideService
 		if baseService, ok := baseServices[name]; ok {
-			if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
-				return base, errors.Wrapf(err, "cannot merge service %s", name)
+			merged, err := _merge(&baseService, &overrideService)
+			if err != nil {
+				return nil, errors.Wrapf(err, "cannot merge service %s", name)
 			}
-			if len(overrideService.Command) > 0 {
-				baseService.Command = overrideService.Command
-			}
-			baseServices[name] = baseService
+			baseServices[name] = *merged
 			continue
 		}
 		baseServices[name] = overrideService
@@ -106,6 +104,19 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig,
 	return services, nil
 }
 
+func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConfig) (*types.ServiceConfig, error) {
+	if err := mergo.Merge(baseService, overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
+		return nil, err
+	}
+	if len(overrideService.Command) > 0 {
+		baseService.Command = overrideService.Command
+	}
+	if len(overrideService.Entrypoint) > 0 {
+		baseService.Entrypoint = overrideService.Entrypoint
+	}
+	return baseService, nil
+}
+
 func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
 	secrets, ok := s.([]types.ServiceSecretConfig)
 	if !ok {


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index d79f80f..2197a1f 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -290,6 +290,25 @@ services:
 	assert.Check(t, service.Command[0] == "echo")
 }
 
+func TestLoadExtendsOverrideCommand(t *testing.T) {
+	actual, err := loadYAML(`
+services:
+  foo:
+    image: busybox
+    extends:
+      service: bar
+    command: "/bin/ash -c \"rm -rf /tmp/might-not-exist\""
+  bar:
+    image: alpine
+    command: "/bin/ash -c \"echo Oh no...\""`)
+	assert.NilError(t, err)
+	assert.Check(t, is.Len(actual.Services, 2))
+	service, err := actual.GetService("foo")
+	assert.NilError(t, err)
+	assert.Check(t, service.Image == "busybox")
+	assert.DeepEqual(t, service.Command, types.ShellCommand{"/bin/ash", "-c", "rm -rf /tmp/might-not-exist"})
+}
+
 func TestLoadCredentialSpec(t *testing.T) {
 	actual, err := loadYAML(`
 services: