mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
543 lines
12 KiB
Go
543 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"net/netip"
|
|
"testing"
|
|
)
|
|
|
|
func TestParsePortSpec(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
port string
|
|
expected PortSpec
|
|
wantErr string
|
|
}{
|
|
// Ingress mode (default).
|
|
{
|
|
name: "container port only",
|
|
port: "8080",
|
|
expected: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "published and container port",
|
|
port: "8000:8080",
|
|
expected: PortSpec{
|
|
PublishedPort: 8000,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "published port zero",
|
|
port: "0:8080",
|
|
expected: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "tcp protocol explicit",
|
|
port: "8080/tcp",
|
|
expected: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "udp protocol",
|
|
port: "8080/udp",
|
|
expected: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "published port udp",
|
|
port: "8000:8080/udp",
|
|
expected: PortSpec{
|
|
PublishedPort: 8000,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "hostname and container port",
|
|
port: "app.example.com:8080",
|
|
expected: PortSpec{
|
|
Hostname: "app.example.com",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "hostname and container port http",
|
|
port: "app.example.com:8080/http",
|
|
expected: PortSpec{
|
|
Hostname: "app.example.com",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "hostname and published port",
|
|
port: "app.example.com:6443:8080",
|
|
expected: PortSpec{
|
|
Hostname: "app.example.com",
|
|
PublishedPort: 6443,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "hostname and published port http",
|
|
port: "app.example.com:8000:8080/http",
|
|
expected: PortSpec{
|
|
Hostname: "app.example.com",
|
|
PublishedPort: 8000,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
|
|
// Host mode.
|
|
{
|
|
name: "host mode published with protocol",
|
|
port: "80:8080/udp@host",
|
|
expected: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
{
|
|
name: "host mode with IPv4",
|
|
port: "127.0.0.1:80:8080@host",
|
|
expected: PortSpec{
|
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
{
|
|
name: "host mode with IPv6",
|
|
port: "[2001:db8::1234:5678]:80:8080@host",
|
|
expected: PortSpec{
|
|
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
{
|
|
name: "host mode with IP and protocol",
|
|
port: "127.0.0.1:80:8080/udp@host",
|
|
expected: PortSpec{
|
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
|
|
// Error cases.
|
|
{
|
|
name: "empty",
|
|
port: "",
|
|
wantErr: "invalid container port",
|
|
},
|
|
{
|
|
name: "invalid container port",
|
|
port: "invalid",
|
|
wantErr: "invalid container port",
|
|
},
|
|
{
|
|
name: "container port zero",
|
|
port: "0",
|
|
wantErr: "container port must be non-zero",
|
|
},
|
|
{
|
|
name: "out of range container port",
|
|
port: "100500",
|
|
wantErr: "invalid container port",
|
|
},
|
|
{
|
|
name: "just protocol",
|
|
port: "/tcp",
|
|
wantErr: "invalid container port",
|
|
},
|
|
{
|
|
name: "just mode",
|
|
port: "@host",
|
|
wantErr: "invalid container port",
|
|
},
|
|
{
|
|
name: "multiple @ symbols",
|
|
port: "8080@host@host",
|
|
wantErr: "too many '@' symbols",
|
|
},
|
|
{
|
|
name: "invalid mode",
|
|
port: "8080@invalid",
|
|
wantErr: "invalid mode: 'invalid'",
|
|
},
|
|
{
|
|
name: "multiple protocols",
|
|
port: "8080/tcp/udp",
|
|
wantErr: "too many '/' symbols",
|
|
},
|
|
{
|
|
name: "invalid protocol",
|
|
port: "8080/invalid",
|
|
wantErr: "unsupported protocol: 'invalid'",
|
|
},
|
|
{
|
|
name: "invalid published port",
|
|
port: "app.example.com:invalid:8080",
|
|
wantErr: "invalid published port",
|
|
},
|
|
{
|
|
name: "missing hostname with http",
|
|
port: "8080/http",
|
|
wantErr: "hostname is required",
|
|
},
|
|
{
|
|
name: "missing hostname with http",
|
|
port: "8080/https",
|
|
wantErr: "hostname is required",
|
|
},
|
|
{
|
|
name: "missing hostname with published https port",
|
|
port: "8000:8080/https",
|
|
wantErr: "hostname is required",
|
|
},
|
|
{
|
|
name: "invalid hostname",
|
|
port: "app:8080/http",
|
|
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
|
|
},
|
|
{
|
|
name: "hostname with tcp protocol",
|
|
port: "app.example.com:8080/tcp",
|
|
wantErr: "hostname is only valid with 'http' or 'https' protocols",
|
|
},
|
|
|
|
{
|
|
name: "missing published port in host mode",
|
|
port: "8080@host",
|
|
wantErr: "published port is required in host mode",
|
|
},
|
|
{
|
|
name: "missing published port with protocol in host mode",
|
|
port: "8080/udp@host",
|
|
wantErr: "published port is required in host mode",
|
|
},
|
|
{
|
|
name: "invalid host IPv4",
|
|
port: "300.0.0.1:80:8080@host",
|
|
wantErr: "invalid host IP",
|
|
},
|
|
{
|
|
name: "invalid host IPv6",
|
|
port: "[:::1]:80:8080@host",
|
|
wantErr: "invalid host IP",
|
|
},
|
|
{
|
|
name: "missing closing bracket in IPv6",
|
|
port: "[::1:80:8080@host",
|
|
wantErr: "invalid host IP",
|
|
},
|
|
{
|
|
name: "missing brackets in IPv6",
|
|
port: "2001:db8::1234:5678:80:8080@host",
|
|
wantErr: "invalid host IP",
|
|
},
|
|
{
|
|
name: "http in host mode",
|
|
port: "80:8080/http@host",
|
|
wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported",
|
|
},
|
|
{
|
|
name: "https in host mode",
|
|
port: "80:8080/https@host",
|
|
wantErr: "unsupported protocol 'https' in host mode, only 'tcp' and 'udp' are supported",
|
|
},
|
|
{
|
|
name: "hostname in host mode",
|
|
port: "app.example.com:8080@host",
|
|
wantErr: "hostname cannot be specified in host mode",
|
|
},
|
|
{
|
|
name: "hostname with invalid published port",
|
|
port: "app.example.com:invalid:8080@host",
|
|
wantErr: "invalid published port",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := ParsePortSpec(tt.port)
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err, "Expected error: %s, got nil, spec: %+v", tt.wantErr, spec)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, spec)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPortSpec_Validate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
spec PortSpec
|
|
wantErr string
|
|
}{
|
|
// Valid ingress mode.
|
|
{
|
|
name: "ingress mode tcp",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeIngress,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode tcp",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode with published tcp port",
|
|
spec: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode udp",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode with published udp port",
|
|
spec: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode with hostname and http",
|
|
spec: PortSpec{
|
|
Hostname: "app.example.com",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTP,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode with hostname and https",
|
|
spec: PortSpec{
|
|
Hostname: "app.example.com",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
},
|
|
},
|
|
{
|
|
name: "ingress mode with hostname and published port",
|
|
spec: PortSpec{
|
|
Hostname: "app.example.com",
|
|
PublishedPort: 6443,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
},
|
|
},
|
|
|
|
// Valid host mode.
|
|
{
|
|
name: "host mode",
|
|
spec: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
{
|
|
name: "host mode with IPv4",
|
|
spec: PortSpec{
|
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
{
|
|
name: "host mode with IPv6",
|
|
spec: PortSpec{
|
|
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolUDP,
|
|
Mode: PortModeHost,
|
|
},
|
|
},
|
|
|
|
// Error cases.
|
|
{
|
|
name: "missing container port",
|
|
spec: PortSpec{
|
|
Protocol: ProtocolTCP,
|
|
},
|
|
wantErr: "container port must be non-zero",
|
|
},
|
|
{
|
|
name: "invalid protocol",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: "invalid",
|
|
},
|
|
wantErr: "invalid protocol: 'invalid'",
|
|
},
|
|
{
|
|
name: "invalid mode",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: "invalid",
|
|
},
|
|
wantErr: "invalid mode: 'invalid'",
|
|
},
|
|
{
|
|
name: "hostname with non-http protocol",
|
|
spec: PortSpec{
|
|
Hostname: "app.example.com",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
},
|
|
wantErr: "hostname is only valid with 'http' or 'https' protocols",
|
|
},
|
|
{
|
|
name: "invalid hostname",
|
|
spec: PortSpec{
|
|
Hostname: "app",
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
},
|
|
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
|
|
},
|
|
{
|
|
name: "missing hostname with http",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTP,
|
|
},
|
|
wantErr: "hostname is required with 'http' or 'https' protocols",
|
|
},
|
|
{
|
|
name: "missing hostname with https",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
},
|
|
wantErr: "hostname is required with 'http' or 'https' protocols",
|
|
},
|
|
{
|
|
name: "host IP in ingress mode",
|
|
spec: PortSpec{
|
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
},
|
|
wantErr: "host IP cannot be specified in ingress mode",
|
|
},
|
|
{
|
|
name: "zero published port in host mode",
|
|
spec: PortSpec{
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
wantErr: "published port is required in host mode",
|
|
},
|
|
{
|
|
name: "hostname in host mode",
|
|
spec: PortSpec{
|
|
Hostname: "app.example.com",
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolTCP,
|
|
Mode: PortModeHost,
|
|
},
|
|
wantErr: "hostname cannot be specified in host mode",
|
|
},
|
|
{
|
|
name: "http in host mode",
|
|
spec: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTP,
|
|
Mode: PortModeHost,
|
|
},
|
|
wantErr: "unsupported protocol 'http' in host mode",
|
|
},
|
|
{
|
|
name: "https in host mode",
|
|
spec: PortSpec{
|
|
PublishedPort: 80,
|
|
ContainerPort: 8080,
|
|
Protocol: ProtocolHTTPS,
|
|
Mode: PortModeHost,
|
|
},
|
|
wantErr: "unsupported protocol 'https' in host mode",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := tt.spec.Validate()
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err, tt.wantErr)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|