From e30c7c007f47389eaf1e922ce6b1b4084f19e448 Mon Sep 17 00:00:00 2001
From: Markus Pesch <markus.pesch@cryptic.systems>
Date: Thu, 22 May 2025 09:49:43 +0200
Subject: [PATCH] feat: support service.command

This patch extends dcmerge to support the command attribut of a defined service.
For example:

```yaml
services:
  busybox
    command: [ "/usr/bin/cp", "--recursive", "--force", "/tmp/bar.txt", "/tmp/foo.txt"]
    image: library/busybox:latest
```

The command attribute is interpreted as a whole. This means that individual
arguments are not merged as a comparison, as this would change the meaning of
the command attribute.
---
 pkg/domain/dockerCompose/config.go            |  19 +++-
 pkg/domain/dockerCompose/config_test.go       | 103 ++++++++++++++++++
 .../merge/testcase003/docker-compose-000.yml  |   4 +
 .../merge/testcase003/docker-compose-001.yml  |   4 +
 .../merge/testcase003/expectedResult.yml      |   8 ++
 5 files changed, 137 insertions(+), 1 deletion(-)
 create mode 100644 pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-000.yml
 create mode 100644 pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-001.yml
 create mode 100644 pkg/domain/dockerCompose/test/assets/merge/testcase003/expectedResult.yml

diff --git a/pkg/domain/dockerCompose/config.go b/pkg/domain/dockerCompose/config.go
index 706942f..179976d 100644
--- a/pkg/domain/dockerCompose/config.go
+++ b/pkg/domain/dockerCompose/config.go
@@ -531,6 +531,7 @@ func NewSecret() *Secret {
 }
 
 type Service struct {
+	Command            []string                   `json:"command,omitempty" yaml:"command,omitempty"`
 	CapabilitiesAdd    []string                   `json:"cap_add,omitempty" yaml:"cap_add,omitempty"`
 	CapabilitiesDrop   []string                   `json:"cap_drop,omitempty" yaml:"cap_drop,omitempty"`
 	DependsOnContainer *DependsOnContainer        `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
@@ -641,7 +642,8 @@ func (s *Service) Equal(equalable Equalable) bool {
 	case s == nil && service != nil:
 		return false
 	default:
-		return equalSlice(s.CapabilitiesAdd, service.CapabilitiesAdd) &&
+		return equalSlice(s.Command, service.Command) &&
+			equalSlice(s.CapabilitiesAdd, service.CapabilitiesAdd) &&
 			equalSlice(s.CapabilitiesDrop, service.CapabilitiesDrop) &&
 			s.DependsOnContainer.Equal(service.DependsOnContainer) &&
 			s.Deploy.Equal(service.Deploy) &&
@@ -673,6 +675,7 @@ func (s *Service) MergeExistingWin(service *Service) {
 	// 	fallthrough
 
 	default:
+		s.mergeExistingWinCommand(service.Command)
 		s.mergeExistingWinCapabilitiesAdd(service.CapabilitiesAdd)
 		s.mergeExistingWinCapabilitiesDrop(service.CapabilitiesDrop)
 		s.mergeExistingWinDependsOnContainer(service.DependsOnContainer)
@@ -707,6 +710,7 @@ func (s *Service) MergeLastWin(service *Service) {
 	// 	fallthrough
 
 	default:
+		s.mergeLastWinCommand(service.Command)
 		s.mergeLastWinCapabilitiesAdd(service.CapabilitiesAdd)
 		s.mergeLastWinCapabilitiesDrop(service.CapabilitiesDrop)
 		s.mergeLastWinDependsOnContainer(service.DependsOnContainer)
@@ -723,6 +727,13 @@ func (s *Service) MergeLastWin(service *Service) {
 	}
 }
 
+func (s *Service) mergeExistingWinCommand(command []string) {
+	if len(s.Command) > 0 {
+		return
+	}
+	s.Command = command
+}
+
 func (s *Service) mergeExistingWinCapabilitiesAdd(capabilitiesAdd []string) {
 	for _, capabilityAdd := range capabilitiesAdd {
 		if !existsInSlice(s.CapabilitiesAdd, capabilityAdd) && len(capabilityAdd) > 0 {
@@ -935,6 +946,12 @@ func (s *Service) mergeExistingWinVolumes(volumes []string) {
 	}
 }
 
+func (s *Service) mergeLastWinCommand(command []string) {
+	if len(command) > 0 {
+		s.Command = command
+	}
+}
+
 func (s *Service) mergeLastWinCapabilitiesAdd(capabilitiesAdd []string) {
 	for _, capabilityAdd := range capabilitiesAdd {
 		if len(capabilityAdd) <= 0 {
diff --git a/pkg/domain/dockerCompose/config_test.go b/pkg/domain/dockerCompose/config_test.go
index 3f0e210..996b359 100644
--- a/pkg/domain/dockerCompose/config_test.go
+++ b/pkg/domain/dockerCompose/config_test.go
@@ -301,6 +301,7 @@ func TestService_Equal(t *testing.T) {
 		},
 		{
 			equalableA: &dockerCompose.Service{
+				Command:            []string{},
 				CapabilitiesAdd:    []string{},
 				CapabilitiesDrop:   []string{},
 				DependsOnContainer: &dockerCompose.DependsOnContainer{},
@@ -316,6 +317,7 @@ func TestService_Equal(t *testing.T) {
 				Volumes:            []string{},
 			},
 			equalableB: &dockerCompose.Service{
+				Command:            []string{},
 				CapabilitiesAdd:    []string{},
 				CapabilitiesDrop:   []string{},
 				DependsOnContainer: &dockerCompose.DependsOnContainer{},
@@ -332,6 +334,15 @@ func TestService_Equal(t *testing.T) {
 			},
 			expectedResult: true,
 		},
+		{
+			equalableA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			equalableB: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			expectedResult: true,
+		},
 		{
 			equalableA: &dockerCompose.Service{
 				CapabilitiesAdd: []string{"NET_ADMIN"},
@@ -636,6 +647,52 @@ func TestService_MergeExistingWin(t *testing.T) {
 			expectedService:    &dockerCompose.Service{},
 		},
 
+		// Command
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{""},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+
 		// CapabilitiesAdd
 		{
 			serviceDeploymentA: &dockerCompose.Service{
@@ -1620,6 +1677,52 @@ func TestService_MergeLastWin(t *testing.T) {
 			expectedService:    &dockerCompose.Service{},
 		},
 
+		// Command
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+		},
+		{
+			serviceDeploymentA: &dockerCompose.Service{
+				Command: []string{"/usr/bin/cp", "--recursive", "/tmp/foo.txt", "/tmp/bar.txt"},
+			},
+			serviceDeploymentB: &dockerCompose.Service{
+				Command: []string{""},
+			},
+			expectedService: &dockerCompose.Service{
+				Command: []string{""},
+			},
+		},
+
 		// CapabilitiesAdd
 		{
 			serviceDeploymentA: &dockerCompose.Service{
diff --git a/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-000.yml b/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-000.yml
new file mode 100644
index 0000000..6cabc83
--- /dev/null
+++ b/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-000.yml
@@ -0,0 +1,4 @@
+services:
+  frontend:
+    command: [ "/usr/bin/cp", "--recursive", "--force", "/tmp/foo.txt", "/tmp/bar.txt" ]
+    image: library/frontend:latest
\ No newline at end of file
diff --git a/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-001.yml b/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-001.yml
new file mode 100644
index 0000000..6b0bdd0
--- /dev/null
+++ b/pkg/domain/dockerCompose/test/assets/merge/testcase003/docker-compose-001.yml
@@ -0,0 +1,4 @@
+services:
+  frontend:
+    command: [ "/usr/bin/cp", "--recursive", "--force", "/tmp/bar.txt", "/tmp/foo.txt"]
+    image: library/frontend:latest
\ No newline at end of file
diff --git a/pkg/domain/dockerCompose/test/assets/merge/testcase003/expectedResult.yml b/pkg/domain/dockerCompose/test/assets/merge/testcase003/expectedResult.yml
new file mode 100644
index 0000000..72499ac
--- /dev/null
+++ b/pkg/domain/dockerCompose/test/assets/merge/testcase003/expectedResult.yml
@@ -0,0 +1,8 @@
+services:
+  backend:
+    image: library/backend:latest
+  frontend:
+    depends_on:
+      backend:
+        condition: service_completed_successfully
+    image: library/frontend:latest