mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat: detect conflicing service port on a container
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,6 +13,14 @@ const (
|
|||||||
LabelServiceName = "uncloud.service.name"
|
LabelServiceName = "uncloud.service.name"
|
||||||
LabelServiceMode = "uncloud.service.mode"
|
LabelServiceMode = "uncloud.service.mode"
|
||||||
LabelServicePorts = "uncloud.service.ports"
|
LabelServicePorts = "uncloud.service.ports"
|
||||||
|
|
||||||
|
StateCreated = "created"
|
||||||
|
StateDead = "dead"
|
||||||
|
StateExited = "exited"
|
||||||
|
StatePaused = "paused"
|
||||||
|
StateRemoving = "removing"
|
||||||
|
StateRestarting = "restarting"
|
||||||
|
StateRunning = "running"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
@@ -43,6 +52,9 @@ func (c *Container) ServicePorts() ([]PortSpec, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(encoded) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
publishPorts := strings.Split(encoded, ",")
|
publishPorts := strings.Split(encoded, ",")
|
||||||
ports := make([]PortSpec, len(publishPorts))
|
ports := make([]PortSpec, len(publishPorts))
|
||||||
@@ -70,7 +82,7 @@ var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
|
|||||||
// Healthy determines if the container is running and healthy based on its status string.
|
// Healthy determines if the container is running and healthy based on its status string.
|
||||||
// A running container with no health check configured is considered healthy.
|
// A running container with no health check configured is considered healthy.
|
||||||
func (c *Container) Healthy() bool {
|
func (c *Container) Healthy() bool {
|
||||||
if c.State != "running" {
|
if c.State != StateRunning {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,3 +100,40 @@ func (c *Container) Healthy() bool {
|
|||||||
// If the health status in parentheses is "healthy", the container is considered healthy.
|
// If the health status in parentheses is "healthy", the container is considered healthy.
|
||||||
return matches[1] == types.Healthy
|
return matches[1] == types.Healthy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stopped determines if the container is stopped and doesn't try to restart.
|
||||||
|
func (c *Container) Stopped() bool {
|
||||||
|
return c.State == StateCreated || c.State == StateDead || c.State == StateExited
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
||||||
|
func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
|
||||||
|
svcPorts, err := c.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get service ports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conflicting []PortSpec
|
||||||
|
for _, p := range ports {
|
||||||
|
if p.Mode != PortModeHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two host ports conflict if they have the same published port number and protocol, and either:
|
||||||
|
// * At least one host IP is not set (meaning it uses all interfaces)
|
||||||
|
// * Both host IPs are identical
|
||||||
|
for _, svcPort := range svcPorts {
|
||||||
|
if svcPort.Mode != PortModeHost ||
|
||||||
|
svcPort.PublishedPort != p.PublishedPort ||
|
||||||
|
svcPort.Protocol != p.Protocol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !svcPort.HostIP.IsValid() || !p.HostIP.IsValid() || svcPort.HostIP.Compare(p.HostIP) == 0 {
|
||||||
|
conflicting = append(conflicting, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicting, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,3 +92,178 @@ func TestContainer_Healthy(t *testing.T) {
|
|||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainer_ConflictingServicePorts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containerPorts string
|
||||||
|
checkPorts []PortSpec
|
||||||
|
want []PortSpec
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no conflicts when container has no ports",
|
||||||
|
containerPorts: "",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port and protocol conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same port but different protocols don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple protocols on same port don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host,8080:80/udp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with different published ports don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8081, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port but different host IPs don't conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.2"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port, protocol, and host IP conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP conflicts with specific host IP on same port and protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP doesn't conflict with different protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode ports don't conflict with host mode ports",
|
||||||
|
containerPorts: "8080:80/tcp",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container with invalid port spec returns error",
|
||||||
|
containerPorts: "invalid:port:spec",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
container := &Container{
|
||||||
|
Container: types.Container{
|
||||||
|
Labels: map[string]string{
|
||||||
|
LabelServicePorts: tt.containerPorts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := container.ConflictingServicePorts(tt.checkPorts)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user