feat: support depends_on
All checks were successful
continuous-integration/drone/push Build is passing

This PR supports the extended pattern of `depends_on`. If the short version of
`depends_on` is defined, it will be migrated to the extended version like the
example below:

```yaml
services:
  web:
    depends_on
    - database

services:
  web:
    depends_on:
      database:
        condition: service_started
```

All three types of merging strategies are supported.
This commit is contained in:
Markus Pesch 2025-02-24 22:44:11 +01:00
parent 003db26fe5
commit ab282e5173
Signed by: volker.raschek
GPG Key ID: 852BCC170D81A982
7 changed files with 211 additions and 90 deletions

View File

@ -8,7 +8,7 @@ import (
"git.cryptic.systems/volker.raschek/dcmerge/pkg/domain/dockerCompose"
"git.cryptic.systems/volker.raschek/dcmerge/pkg/fetcher"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func Execute(version string) error {
@ -100,10 +100,12 @@ func run(cmd *cobra.Command, args []string) error {
defer f.Close()
yamlEncoder := yaml.NewEncoder(f)
yamlEncoder.SetIndent(0)
return yamlEncoder.Encode(dockerComposeConfig)
default:
yamlEncoder := yaml.NewEncoder(os.Stdout)
yamlEncoder.SetIndent(0)
return yamlEncoder.Encode(dockerComposeConfig)
}

3
go.mod
View File

@ -5,7 +5,7 @@ go 1.20
require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@ -13,5 +13,4 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

2
go.sum
View File

@ -14,7 +14,5 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -4,6 +4,8 @@ import (
"fmt"
"regexp"
"strings"
"gopkg.in/yaml.v3"
)
const (
@ -517,19 +519,19 @@ func NewSecret() *Secret {
}
type Service struct {
CapabilitiesAdd []string `json:"cap_add,omitempty" yaml:"cap_add,omitempty"`
CapabilitiesDrop []string `json:"cap_drop,omitempty" yaml:"cap_drop,omitempty"`
DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
Deploy *ServiceDeploy `json:"deploy,omitempty" yaml:"deploy,omitempty"`
Environments []string `json:"environment,omitempty" yaml:"environment,omitempty"`
ExtraHosts []string `json:"extra_hosts,omitempty" yaml:"extra_hosts,omitempty"`
Image string `json:"image,omitempty" yaml:"image,omitempty"`
Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"`
Networks map[string]*ServiceNetwork `json:"networks,omitempty" yaml:"networks,omitempty"`
Ports []Port `json:"ports,omitempty" yaml:"ports,omitempty"`
Secrets []string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
ULimits *ServiceULimits `json:"ulimits,omitempty" yaml:"ulimits,omitempty"`
Volumes []string `json:"volumes,omitempty" yaml:"volumes,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"`
Deploy *ServiceDeploy `json:"deploy,omitempty" yaml:"deploy,omitempty"`
Environments []string `json:"environment,omitempty" yaml:"environment,omitempty"`
ExtraHosts []string `json:"extra_hosts,omitempty" yaml:"extra_hosts,omitempty"`
Image string `json:"image,omitempty" yaml:"image,omitempty"`
Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"`
Networks map[string]*ServiceNetwork `json:"networks,omitempty" yaml:"networks,omitempty"`
Ports []Port `json:"ports,omitempty" yaml:"ports,omitempty"`
Secrets []string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
ULimits *ServiceULimits `json:"ulimits,omitempty" yaml:"ulimits,omitempty"`
Volumes []string `json:"volumes,omitempty" yaml:"volumes,omitempty"`
}
// ExistsEnvironment returns true if the passed name of environment variable is
@ -629,7 +631,7 @@ func (s *Service) Equal(equalable Equalable) bool {
default:
return equalSlice(s.CapabilitiesAdd, service.CapabilitiesAdd) &&
equalSlice(s.CapabilitiesDrop, service.CapabilitiesDrop) &&
equalSlice(s.DependsOn, service.DependsOn) &&
s.DependsOnContainer.Equal(service.DependsOnContainer) &&
s.Deploy.Equal(service.Deploy) &&
equalSlice(s.Environments, service.Environments) &&
equalSlice(s.ExtraHosts, service.ExtraHosts) &&
@ -661,7 +663,7 @@ func (s *Service) MergeExistingWin(service *Service) {
default:
s.mergeExistingWinCapabilitiesAdd(service.CapabilitiesAdd)
s.mergeExistingWinCapabilitiesDrop(service.CapabilitiesDrop)
s.mergeExistingWinDependsOn(service.DependsOn)
s.mergeExistingWinDependsOnContainer(service.DependsOnContainer)
s.mergeExistingWinDeploy(service.Deploy)
s.mergeExistingWinEnvironments(service.Environments)
s.mergeExistingWinExtraHosts(service.ExtraHosts)
@ -695,7 +697,7 @@ func (s *Service) MergeLastWin(service *Service) {
default:
s.mergeLastWinCapabilitiesAdd(service.CapabilitiesAdd)
s.mergeLastWinCapabilitiesDrop(service.CapabilitiesDrop)
s.mergeLastWinDependsOn(service.DependsOn)
s.mergeLastWinDependsOnContainer(service.DependsOnContainer)
s.mergeLastWinDeploy(service.Deploy)
s.mergeLastWinEnvironments(service.Environments)
s.mergeLastWinExtraHosts(service.ExtraHosts)
@ -725,10 +727,22 @@ func (s *Service) mergeExistingWinCapabilitiesDrop(capabilitiesDrop []string) {
}
}
func (s *Service) mergeExistingWinDependsOn(dependsOn []string) {
for _, depOn := range dependsOn {
if !existsInSlice(s.DependsOn, depOn) && len(depOn) > 0 {
s.DependsOn = append(s.DependsOn, depOn)
func (s *Service) mergeExistingWinDependsOnContainer(dependsOnContainer *DependsOnContainer) {
switch {
case s.DependsOnContainer != nil && dependsOnContainer == nil:
fallthrough
case s.DependsOnContainer == nil && dependsOnContainer == nil:
return
case s.DependsOnContainer == nil && dependsOnContainer != nil:
s.DependsOnContainer = dependsOnContainer
default:
for name, depOn := range dependsOnContainer.DependsOn {
if !ExistsInMap(s.DependsOnContainer.DependsOn, name) && depOn != nil {
if s.DependsOnContainer.DependsOn == nil {
s.DependsOnContainer.DependsOn = make(map[string]*ServiceDependsOn)
}
s.DependsOnContainer.DependsOn[name] = depOn
}
}
}
}
@ -933,14 +947,20 @@ func (s *Service) mergeLastWinCapabilitiesDrop(capabilitiesDrop []string) {
}
}
func (s *Service) mergeLastWinDependsOn(dependsOn []string) {
for _, dep := range dependsOn {
if len(dep) <= 0 {
continue
}
if !existsInSlice(s.DependsOn, dep) {
s.DependsOn = append(s.DependsOn, dep)
func (s *Service) mergeLastWinDependsOnContainer(dependsOnContainer *DependsOnContainer) {
switch {
case s.DependsOnContainer != nil && dependsOnContainer == nil:
fallthrough
case s.DependsOnContainer == nil && dependsOnContainer == nil:
return
case s.DependsOnContainer == nil && dependsOnContainer != nil:
s.DependsOnContainer = dependsOnContainer
default:
for name, depOn := range dependsOnContainer.DependsOn {
if s.DependsOnContainer.DependsOn == nil {
s.DependsOnContainer.DependsOn = make(map[string]*ServiceDependsOn)
}
s.DependsOnContainer.DependsOn[name] = depOn
}
}
}
@ -1222,6 +1242,66 @@ func (s *Service) SetVolume(src string, dest string, perm string) {
}
}
const ServiceDependsOnConditionServiceStarted string = "service_started"
// DependsOnContainer is a wrapper to handle different YAML type formats of DependsOn.
type DependsOnContainer struct {
Slice []string
DependsOn map[string]*ServiceDependsOn
}
// Equal returns true if the passed equalable is equal
func (sdoc *DependsOnContainer) Equal(equalable Equalable) bool {
serviceDependsOnContainer, ok := equalable.(*DependsOnContainer)
if !ok {
return false
}
switch {
case sdoc == nil && serviceDependsOnContainer == nil:
return true
case sdoc != nil && serviceDependsOnContainer == nil:
fallthrough
case sdoc == nil && serviceDependsOnContainer != nil:
return false
default:
return equalSlice(sdoc.Slice, serviceDependsOnContainer.Slice) &&
EqualStringMap(sdoc.DependsOn, serviceDependsOnContainer.DependsOn)
}
}
// MarshalYAML implements the MarshalYAML interface to customize the behavior when being marshaled into a YAML document.
func (sdoc *DependsOnContainer) MarshalYAML() (interface{}, error) {
return sdoc.DependsOn, nil
}
// UnmarshalYAML implements the UnmarshalYAML interface to customize the behavior when being unmarshaled into a YAML
// document.
func (sdoc *DependsOnContainer) UnmarshalYAML(value *yaml.Node) error {
if sdoc.DependsOn == nil {
sdoc.DependsOn = make(map[string]*ServiceDependsOn)
}
if sdoc.Slice == nil {
sdoc.Slice = make([]string, 0)
}
if err := value.Decode(&sdoc.Slice); err == nil {
for _, s := range sdoc.Slice {
sdoc.DependsOn[s] = &ServiceDependsOn{
Condition: ServiceDependsOnConditionServiceStarted,
}
}
return nil
}
if err := value.Decode(sdoc.DependsOn); err != nil {
return err
}
return nil
}
// NewService returns an empty initialized Service.
func NewService() *Service {
return &Service{
@ -1239,6 +1319,31 @@ func NewService() *Service {
}
}
type ServiceDependsOn struct {
Condition string `yaml:"condition,omitempty"`
Restart string `yaml:"restart,omitempty"`
}
// Equal returns true if the passed equalable is equal
func (sdo *ServiceDependsOn) Equal(equalable Equalable) bool {
serviceDependsOn, ok := equalable.(*ServiceDependsOn)
if !ok {
return false
}
switch {
case sdo == nil && serviceDependsOn == nil:
return true
case sdo != nil && serviceDependsOn == nil:
fallthrough
case sdo == nil && serviceDependsOn != nil:
return false
default:
return sdo.Condition == serviceDependsOn.Condition &&
sdo.Restart == serviceDependsOn.Restart
}
}
type ServiceDeploy struct {
Resources *ServiceDeployResources `json:"resources" yaml:"resources"`
}

View File

@ -223,34 +223,34 @@ func TestService_Equal(t *testing.T) {
},
{
equalableA: &dockerCompose.Service{
CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{},
DependsOn: []string{},
Deploy: nil,
Environments: []string{},
ExtraHosts: []string{},
Image: "",
Labels: []string{},
Networks: map[string]*dockerCompose.ServiceNetwork{},
Ports: []dockerCompose.Port{},
Secrets: []string{},
ULimits: nil,
Volumes: []string{},
CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
Deploy: nil,
Environments: []string{},
ExtraHosts: []string{},
Image: "",
Labels: []string{},
Networks: map[string]*dockerCompose.ServiceNetwork{},
Ports: []dockerCompose.Port{},
Secrets: []string{},
ULimits: nil,
Volumes: []string{},
},
equalableB: &dockerCompose.Service{
CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{},
DependsOn: []string{},
Deploy: nil,
Environments: []string{},
ExtraHosts: []string{},
Image: "",
Labels: []string{},
Networks: map[string]*dockerCompose.ServiceNetwork{},
Ports: []dockerCompose.Port{},
Secrets: []string{},
ULimits: nil,
Volumes: []string{},
CapabilitiesAdd: []string{},
CapabilitiesDrop: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
Deploy: nil,
Environments: []string{},
ExtraHosts: []string{},
Image: "",
Labels: []string{},
Networks: map[string]*dockerCompose.ServiceNetwork{},
Ports: []dockerCompose.Port{},
Secrets: []string{},
ULimits: nil,
Volumes: []string{},
},
expectedResult: true,
},
@ -292,19 +292,37 @@ func TestService_Equal(t *testing.T) {
},
{
equalableA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
equalableB: &dockerCompose.Service{
DependsOn: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
expectedResult: false,
},
{
equalableA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
equalableB: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
expectedResult: false,
},
{
equalableA: &dockerCompose.Service{
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
equalableB: &dockerCompose.Service{
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{}},
},
expectedResult: false,
},
{
equalableA: &dockerCompose.Service{
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
equalableB: &dockerCompose.Service{
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
expectedResult: true,
},
@ -598,46 +616,46 @@ func TestService_MergeExistingWin(t *testing.T) {
// DependsOn
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{""},
DependsOnContainer: nil,
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
@ -1582,46 +1600,46 @@ func TestService_MergeLastWin(t *testing.T) {
// DependsOn
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{},
DependsOnContainer: &dockerCompose.DependsOnContainer{},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},
{
serviceDeploymentA: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
serviceDeploymentB: &dockerCompose.Service{
DependsOn: []string{""},
DependsOnContainer: nil,
},
expectedService: &dockerCompose.Service{
DependsOn: []string{"app"},
DependsOnContainer: &dockerCompose.DependsOnContainer{DependsOn: map[string]*dockerCompose.ServiceDependsOn{"app": {Condition: "service_started"}}},
},
},

View File

@ -8,7 +8,7 @@ import (
"os"
"git.cryptic.systems/volker.raschek/dcmerge/pkg/domain/dockerCompose"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func Fetch(urls ...string) ([]*dockerCompose.Config, error) {

View File

@ -30,8 +30,7 @@
"matchManagers": [ "gomod" ],
"matchPackageNames": [
"gopkg.in/yaml.v2"
],
"matchUpdateTypes": [ "major" ]
]
}
],
"postUpdateOptions": [