From d9ecf068c9ea9119c4c242c9259aafecbe7166f8 Mon Sep 17 00:00:00 2001 From: Markus Pesch Date: Sun, 27 Aug 2023 21:10:13 +0200 Subject: [PATCH] fix(dockerCompose): add Service.MergeFirstWin() --- pkg/domain/dockerCompose/config.go | 298 ++++++++ pkg/domain/dockerCompose/config_test.go | 901 ++++++++++++++++++++++++ 2 files changed, 1199 insertions(+) diff --git a/pkg/domain/dockerCompose/config.go b/pkg/domain/dockerCompose/config.go index 2cca964..4eb932a 100644 --- a/pkg/domain/dockerCompose/config.go +++ b/pkg/domain/dockerCompose/config.go @@ -381,6 +381,103 @@ type Service struct { Volumes []string `json:"volumes,omitempty" yaml:"volumes,omitempty"` } +// ExistsEnvironment returns true if the passed name of environment variable is +// already present. +func (s *Service) ExistsEnvironment(name string) bool { + for _, environment := range s.Environments { + key, _ := splitStringInKeyValue(environment, environmentDelimiter) + if key == name { + return true + } + } + + return false +} + +// ExistsLabel returns true if the passed label name is already present. +func (s *Service) ExistsLabel(name string) bool { + for _, label := range s.Labels { + key, _ := splitStringInKeyValue(label, labelDelimiter) + if key == name { + return true + } + } + + return false +} + +// ExistsPort returns true if the port definition is already present. +func (s *Service) ExistsPort(src string, dest string, protocol string) bool { + for _, port := range s.Ports { + s, d, p := splitStringInPort(port) + if s == src && d == dest && p == protocol { + return true + } + } + + return false +} + +// ExistsDestinationPort returns true if the destination port is already used. +func (s *Service) ExistsDestinationPort(dest string) bool { + for _, port := range s.Ports { + _, d, _ := splitStringInPort(port) + if d == dest { + return true + } + } + + return false +} + +// ExistsSourcePort returns true if the source port is already used. +func (s *Service) ExistsSourcePort(src string) bool { + for _, port := range s.Ports { + s, _, _ := splitStringInPort(port) + if s == src { + return true + } + } + + return false +} + +// ExistsVolume returns true if the volume definition is already present. +func (s *Service) ExistsVolume(src string, dest string, perm string) bool { + for _, volume := range s.Volumes { + s, d, p := splitStringInVolume(volume) + if s == src && d == dest && p == perm { + return true + } + } + + return false +} + +// ExistsDestinationVolume returns true if the volume definition is already present. +func (s *Service) ExistsDestinationVolume(dest string) bool { + for _, volume := range s.Volumes { + _, d, _ := splitStringInVolume(volume) + if d == dest { + return true + } + } + + return false +} + +// ExistsSourceVolume returns true if the volume definition is already present. +func (s *Service) ExistsSourceVolume(src string) bool { + for _, volume := range s.Volumes { + s, _, _ := splitStringInVolume(volume) + if s == src { + return true + } + } + + return false +} + // Equal returns true if the passed equalable is equal func (s *Service) Equal(equalable Equalable) bool { service, ok := equalable.(*Service) @@ -411,6 +508,37 @@ func (s *Service) Equal(equalable Equalable) bool { } } +func (s *Service) MergeFirstWin(service *Service) { + switch { + case s == nil && service == nil: + fallthrough + case s != nil && service == nil: + return + + // WARN: It's not possible to change the memory pointer s *Service + // to a new initialized service without returning the Service + // it self. + // + // case s == nil && service != nil: + // s = NewService() + // fallthrough + + default: + s.mergeFirstWinCapabilitiesAdd(service.CapabilitiesAdd) + s.mergeFirstWinCapabilitiesDrop(service.CapabilitiesDrop) + s.mergeFirstWinDeploy(service.Deploy) + s.mergeFirstWinEnvironments(service.Environments) + s.mergeFirstWinExtraHosts(service.ExtraHosts) + s.mergeFirstWinImage(service.Image) + s.mergeFirstWinLabels(service.Labels) + s.mergeFirstWinNetworks(service.Networks) + s.mergeFirstWinPorts(service.Ports) + s.mergeFirstWinSecrets(service.Secrets) + s.mergeFirstWinULimits(service.ULimits) + s.mergeFirstWinVolumes(service.Volumes) + } +} + // MergeLastWin merges adds or overwrite the attributes of the passed secret // with the existing one. func (s *Service) MergeLastWin(service *Service) { @@ -444,6 +572,176 @@ func (s *Service) MergeLastWin(service *Service) { } } +func (s *Service) mergeFirstWinCapabilitiesAdd(capabilitiesAdd []string) { + for _, capabilityAdd := range capabilitiesAdd { + if !existsInSlice(s.CapabilitiesAdd, capabilityAdd) && len(capabilityAdd) > 0 { + s.CapabilitiesAdd = append(s.CapabilitiesAdd, capabilityAdd) + } + } +} + +func (s *Service) mergeFirstWinCapabilitiesDrop(capabilitiesDrop []string) { + for _, capabilityDrop := range capabilitiesDrop { + if !existsInSlice(s.CapabilitiesAdd, capabilityDrop) && len(capabilityDrop) > 0 { + s.CapabilitiesDrop = append(s.CapabilitiesDrop, capabilityDrop) + } + } +} + +func (s *Service) mergeFirstWinDeploy(deploy *ServiceDeploy) { + switch { + case s.Deploy == nil && deploy != nil: + s.Deploy = deploy + case s.Deploy != nil && deploy == nil: + fallthrough + case s.Deploy == nil && deploy == nil: + return + default: + s.Deploy.MergeFirstWin(deploy) + } +} + +func (s *Service) mergeFirstWinEnvironments(environments []string) { + switch { + case s.Environments == nil && environments != nil: + s.Environments = environments + case s.Environments != nil && environments == nil: + fallthrough + case s.Environments == nil && environments == nil: + return + default: + for _, environment := range environments { + if len(environment) <= 0 { + continue + } + + key, value := splitStringInKeyValue(environment, environmentDelimiter) + if !s.ExistsEnvironment(key) { + s.SetEnvironment(key, value) + } + } + } +} + +func (s *Service) mergeFirstWinImage(image string) { + switch { + case len(s.Image) == 0 && len(image) != 0: + s.Image = image + case len(s.Image) != 0 && len(image) == 0: + fallthrough + case len(s.Image) == 0 && len(image) == 0: + fallthrough + default: + return + } +} + +func (s *Service) mergeFirstWinExtraHosts(extraHosts []string) { + for _, extraHost := range extraHosts { + if !existsInSlice(s.ExtraHosts, extraHost) && len(extraHost) > 0 { + s.ExtraHosts = append(s.ExtraHosts, extraHost) + } + } +} + +func (s *Service) mergeFirstWinLabels(labels []string) { + switch { + case s.Labels == nil && labels != nil: + s.Labels = labels + case s.Labels != nil && labels == nil: + fallthrough + case s.Labels == nil && labels == nil: + return + default: + for _, label := range labels { + if len(label) <= 0 { + continue + } + + key, value := splitStringInKeyValue(label, labelDelimiter) + if !s.ExistsLabel(key) { + s.SetLabel(key, value) + } + } + } +} + +func (s *Service) mergeFirstWinNetworks(networks map[string]*ServiceNetwork) { + switch { + case s.Networks == nil && networks != nil: + s.Networks = networks + case s.Networks != nil && networks == nil: + fallthrough + case s.Networks == nil && networks == nil: + return + default: + for name, network := range networks { + if _, exists := s.Networks[name]; exists { + s.Networks[name].MergeFirstWin(network) + } else { + s.Networks[name] = network + } + } + } +} + +func (s *Service) mergeFirstWinPorts(ports []string) { + switch { + case s.Ports == nil && ports != nil: + s.Ports = ports + case s.Ports != nil && ports == nil: + fallthrough + case s.Ports == nil && ports == nil: + return + default: + for _, port := range ports { + src, dest, protocol := splitStringInPort(port) + if !s.ExistsDestinationPort(dest) { + s.SetPort(src, dest, protocol) + } + } + } +} + +func (s *Service) mergeFirstWinSecrets(secrets []string) { + for _, secret := range secrets { + if !existsInSlice(s.Secrets, secret) && len(secret) > 0 { + s.Secrets = append(s.Secrets, secret) + } + } +} + +func (s *Service) mergeFirstWinULimits(uLimits *ServiceULimits) { + switch { + case s.ULimits == nil && uLimits != nil: + s.ULimits = uLimits + case s.ULimits != nil && uLimits == nil: + fallthrough + case s.ULimits == nil && uLimits == nil: + return + default: + s.ULimits.MergeFirstWin(uLimits) + } +} + +func (s *Service) mergeFirstWinVolumes(volumes []string) { + switch { + case s.Volumes == nil && volumes != nil: + s.Volumes = volumes + case s.Volumes != nil && volumes == nil: + fallthrough + case s.Volumes == nil && volumes == nil: + return + default: + for _, volume := range volumes { + src, dest, perm := splitStringInVolume(volume) + if !s.ExistsDestinationVolume(dest) { + s.SetVolume(src, dest, perm) + } + } + } +} + func (s *Service) mergeLastWinCapabilitiesAdd(capabilitiesAdd []string) { for _, capabilityAdd := range capabilitiesAdd { if !existsInSlice(s.CapabilitiesAdd, capabilityAdd) { diff --git a/pkg/domain/dockerCompose/config_test.go b/pkg/domain/dockerCompose/config_test.go index ef6e7a3..837738b 100644 --- a/pkg/domain/dockerCompose/config_test.go +++ b/pkg/domain/dockerCompose/config_test.go @@ -464,6 +464,907 @@ func TestService_Equal(t *testing.T) { } } +func TestService_MergeFirstWin(t *testing.T) { + require := require.New(t) + + testCases := []struct { + serviceDeploymentA *dockerCompose.Service + serviceDeploymentB *dockerCompose.Service + expectedService *dockerCompose.Service + }{ + { + serviceDeploymentA: nil, + serviceDeploymentB: nil, + expectedService: nil, + }, + { + serviceDeploymentA: &dockerCompose.Service{}, + serviceDeploymentB: &dockerCompose.Service{}, + expectedService: &dockerCompose.Service{}, + }, + + // CapabilitiesAdd + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesAdd: []string{}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesAdd: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesAdd: []string{""}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesAdd: []string{"NET_RAW"}, + }, + }, + + // CapabilitiesDrop + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesDrop: []string{}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesDrop: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + CapabilitiesDrop: []string{""}, + }, + expectedService: &dockerCompose.Service{ + CapabilitiesDrop: []string{"NET_RAW"}, + }, + }, + + // Deploy + { + serviceDeploymentA: &dockerCompose.Service{ + Deploy: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Deploy: nil, + }, + expectedService: &dockerCompose.Service{ + Deploy: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + serviceDeploymentB: &dockerCompose.Service{ + Deploy: nil, + }, + expectedService: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Deploy: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + expectedService: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + serviceDeploymentB: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + expectedService: &dockerCompose.Service{ + Deploy: dockerCompose.NewServiceDeploy(), + }, + }, + + // Environments + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: nil, + }, + expectedService: &dockerCompose.Service{ + Environments: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: nil, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.local"}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com", "PROXY_HOST=u.example.de"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com", "PROXY_HOST=u.example.de"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=a.example.local"}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Environments: []string{""}, + }, + expectedService: &dockerCompose.Service{ + Environments: []string{"PROXY_HOST=u.example.com"}, + }, + }, + + // ExtraHosts + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: nil, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: nil, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.com"}, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.com", "extra.host.local"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + ExtraHosts: []string{""}, + }, + expectedService: &dockerCompose.Service{ + ExtraHosts: []string{"extra.host.local"}, + }, + }, + + // Image + { + serviceDeploymentA: &dockerCompose.Service{ + Image: "", + }, + serviceDeploymentB: &dockerCompose.Service{ + Image: "", + }, + expectedService: &dockerCompose.Service{ + Image: "", + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Image: "HelloWorld", + }, + serviceDeploymentB: &dockerCompose.Service{ + Image: "FooBar", + }, + expectedService: &dockerCompose.Service{ + Image: "HelloWorld", + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Image: "HelloWorld", + }, + serviceDeploymentB: &dockerCompose.Service{ + Image: "", + }, + expectedService: &dockerCompose.Service{ + Image: "HelloWorld", + }, + }, + + // Labels + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: nil, + }, + expectedService: &dockerCompose.Service{ + Labels: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: nil, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true", "prometheus.io/scrape=false"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true", "prometheus.io/scrape=false"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=false"}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Labels: []string{""}, + }, + expectedService: &dockerCompose.Service{ + Labels: []string{"prometheus.io/scrape=true"}, + }, + }, + + // Networks + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: nil, + }, + expectedService: &dockerCompose.Service{ + Networks: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: nil, + }, + expectedService: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + expectedService: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + expectedService: &dockerCompose.Service{ + Networks: make(map[string]*dockerCompose.ServiceNetwork), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: nil, + }, + expectedService: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "db": {Aliases: []string{"app.db.network"}}, + }, + }, + expectedService: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "db": {Aliases: []string{"app.db.network"}}, + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network", ""}}, + }, + }, + expectedService: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network"}}, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"vpn.network"}}, + }, + }, + expectedService: &dockerCompose.Service{ + Networks: map[string]*dockerCompose.ServiceNetwork{ + "proxy": {Aliases: []string{"app.proxy.network", "vpn.network"}}, + }, + }, + }, + + // Ports + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: nil, + }, + expectedService: &dockerCompose.Service{ + Ports: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: nil, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{"10080:80"}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{"80:80/tcp"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{"80:80/tcp"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Ports: []string{"10080:80/udp"}, + }, + expectedService: &dockerCompose.Service{ + Ports: []string{"80:80"}, + }, + }, + + // Secrets + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: nil, + }, + expectedService: &dockerCompose.Service{ + Secrets: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: nil, + }, + expectedService: &dockerCompose.Service{ + Secrets: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: []string{}, + }, + expectedService: &dockerCompose.Service{ + Secrets: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: []string{}, + }, + expectedService: &dockerCompose.Service{ + Secrets: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: []string{"db_pass_credentials"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: []string{"db_pass_credentials"}, + }, + expectedService: &dockerCompose.Service{ + Secrets: []string{"db_pass_credentials"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Secrets: []string{"db_pass_credentials"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Secrets: []string{"oauth2_pass_credentials"}, + }, + expectedService: &dockerCompose.Service{ + Secrets: []string{"db_pass_credentials", "oauth2_pass_credentials"}, + }, + }, + + // ULimits + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: nil, + }, + expectedService: &dockerCompose.Service{ + ULimits: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: nil, + }, + expectedService: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + expectedService: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + expectedService: &dockerCompose.Service{ + ULimits: dockerCompose.NewServiceULimits(), + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 10, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 10, + Soft: 10, + }, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 10, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 10, + Soft: 10, + }, + }, + }, + expectedService: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 10, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 10, + Soft: 10, + }, + }, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 10, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 10, + Soft: 10, + }, + }, + }, + serviceDeploymentB: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 15, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 25, + Soft: 20, + }, + }, + }, + expectedService: &dockerCompose.Service{ + ULimits: &dockerCompose.ServiceULimits{ + NProc: 10, + NoFile: &dockerCompose.ServiceULimitsNoFile{ + Hard: 10, + Soft: 10, + }, + }, + }, + }, + + // Volumes + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: nil, + }, + expectedService: &dockerCompose.Service{ + Volumes: nil, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: nil, + }, + expectedService: &dockerCompose.Service{ + Volumes: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: nil, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: []string{}, + }, + expectedService: &dockerCompose.Service{ + Volumes: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: []string{}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: []string{}, + }, + expectedService: &dockerCompose.Service{ + Volumes: []string{}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: []string{"/etc/localtime:/etc/localtime"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: []string{"/etc/localtime:/etc/localtime"}, + }, + expectedService: &dockerCompose.Service{ + Volumes: []string{"/etc/localtime:/etc/localtime"}, + }, + }, + { + serviceDeploymentA: &dockerCompose.Service{ + Volumes: []string{"/etc/localtime:/etc/localtime"}, + }, + serviceDeploymentB: &dockerCompose.Service{ + Volumes: []string{"/usr/share/zoneinfo/Europe/Berlin:/etc/localtime"}, + }, + expectedService: &dockerCompose.Service{ + Volumes: []string{"/etc/localtime:/etc/localtime"}, + }, + }, + } + + for i, testCase := range testCases { + testCase.serviceDeploymentA.MergeFirstWin(testCase.serviceDeploymentB) + require.True(testCase.expectedService.Equal(testCase.serviceDeploymentA), "Failed test case %v", i) + } +} + func TestService_MergeLastWin(t *testing.T) { require := require.New(t)