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.
This commit is contained in:
Markus Pesch 2025-05-22 09:49:43 +02:00
parent 973e90986c
commit cf5b389bbc
Signed by: volker.raschek
GPG Key ID: 852BCC170D81A982
5 changed files with 137 additions and 1 deletions

View File

@ -531,6 +531,7 @@ func NewSecret() *Secret {
} }
type Service struct { type Service struct {
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
CapabilitiesAdd []string `json:"cap_add,omitempty" yaml:"cap_add,omitempty"` CapabilitiesAdd []string `json:"cap_add,omitempty" yaml:"cap_add,omitempty"`
CapabilitiesDrop []string `json:"cap_drop,omitempty" yaml:"cap_drop,omitempty"` CapabilitiesDrop []string `json:"cap_drop,omitempty" yaml:"cap_drop,omitempty"`
DependsOnContainer *DependsOnContainer `json:"depends_on,omitempty" yaml:"depends_on,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: case s == nil && service != nil:
return false return false
default: default:
return equalSlice(s.CapabilitiesAdd, service.CapabilitiesAdd) && return equalSlice(s.Command, service.Command) &&
equalSlice(s.CapabilitiesAdd, service.CapabilitiesAdd) &&
equalSlice(s.CapabilitiesDrop, service.CapabilitiesDrop) && equalSlice(s.CapabilitiesDrop, service.CapabilitiesDrop) &&
s.DependsOnContainer.Equal(service.DependsOnContainer) && s.DependsOnContainer.Equal(service.DependsOnContainer) &&
s.Deploy.Equal(service.Deploy) && s.Deploy.Equal(service.Deploy) &&
@ -673,6 +675,7 @@ func (s *Service) MergeExistingWin(service *Service) {
// fallthrough // fallthrough
default: default:
s.mergeExistingWinCommand(service.Command)
s.mergeExistingWinCapabilitiesAdd(service.CapabilitiesAdd) s.mergeExistingWinCapabilitiesAdd(service.CapabilitiesAdd)
s.mergeExistingWinCapabilitiesDrop(service.CapabilitiesDrop) s.mergeExistingWinCapabilitiesDrop(service.CapabilitiesDrop)
s.mergeExistingWinDependsOnContainer(service.DependsOnContainer) s.mergeExistingWinDependsOnContainer(service.DependsOnContainer)
@ -707,6 +710,7 @@ func (s *Service) MergeLastWin(service *Service) {
// fallthrough // fallthrough
default: default:
s.mergeLastWinCommand(service.Command)
s.mergeLastWinCapabilitiesAdd(service.CapabilitiesAdd) s.mergeLastWinCapabilitiesAdd(service.CapabilitiesAdd)
s.mergeLastWinCapabilitiesDrop(service.CapabilitiesDrop) s.mergeLastWinCapabilitiesDrop(service.CapabilitiesDrop)
s.mergeLastWinDependsOnContainer(service.DependsOnContainer) 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) { func (s *Service) mergeExistingWinCapabilitiesAdd(capabilitiesAdd []string) {
for _, capabilityAdd := range capabilitiesAdd { for _, capabilityAdd := range capabilitiesAdd {
if !existsInSlice(s.CapabilitiesAdd, capabilityAdd) && len(capabilityAdd) > 0 { 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) { func (s *Service) mergeLastWinCapabilitiesAdd(capabilitiesAdd []string) {
for _, capabilityAdd := range capabilitiesAdd { for _, capabilityAdd := range capabilitiesAdd {
if len(capabilityAdd) <= 0 { if len(capabilityAdd) <= 0 {

View File

@ -301,6 +301,7 @@ func TestService_Equal(t *testing.T) {
}, },
{ {
equalableA: &dockerCompose.Service{ equalableA: &dockerCompose.Service{
Command: []string{},
CapabilitiesAdd: []string{}, CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{}, CapabilitiesDrop: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{}, DependsOnContainer: &dockerCompose.DependsOnContainer{},
@ -316,6 +317,7 @@ func TestService_Equal(t *testing.T) {
Volumes: []string{}, Volumes: []string{},
}, },
equalableB: &dockerCompose.Service{ equalableB: &dockerCompose.Service{
Command: []string{},
CapabilitiesAdd: []string{}, CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{}, CapabilitiesDrop: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{}, DependsOnContainer: &dockerCompose.DependsOnContainer{},
@ -332,6 +334,15 @@ func TestService_Equal(t *testing.T) {
}, },
expectedResult: true, 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{ equalableA: &dockerCompose.Service{
CapabilitiesAdd: []string{"NET_ADMIN"}, CapabilitiesAdd: []string{"NET_ADMIN"},
@ -636,6 +647,52 @@ func TestService_MergeExistingWin(t *testing.T) {
expectedService: &dockerCompose.Service{}, 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 // CapabilitiesAdd
{ {
serviceDeploymentA: &dockerCompose.Service{ serviceDeploymentA: &dockerCompose.Service{
@ -1620,6 +1677,52 @@ func TestService_MergeLastWin(t *testing.T) {
expectedService: &dockerCompose.Service{}, 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 // CapabilitiesAdd
{ {
serviceDeploymentA: &dockerCompose.Service{ serviceDeploymentA: &dockerCompose.Service{

View File

@ -0,0 +1,4 @@
services:
frontend:
command: [ "/usr/bin/cp", "--recursive", "--force", "/tmp/foo.txt", "/tmp/bar.txt" ]
image: library/frontend:latest

View File

@ -0,0 +1,4 @@
services:
frontend:
command: [ "/usr/bin/cp", "--recursive", "--force", "/tmp/bar.txt", "/tmp/foo.txt"]
image: library/frontend:latest

View File

@ -0,0 +1,8 @@
services:
backend:
image: library/backend:latest
frontend:
depends_on:
backend:
condition: service_completed_successfully
image: library/frontend:latest