feat: follow-up compose 'ports' support: use ingress mode by default (closes #81)

This commit is contained in:
Pasha Sviderski
2025-07-21 16:56:52 +10:00
parent fea7edcbc5
commit 6fb07db4b2
4 changed files with 45 additions and 39 deletions
+1 -1
View File
@@ -102,5 +102,5 @@ _lint:
golangci-lint run $(ARGS) golangci-lint run $(ARGS)
.PHONY: docs-image-push .PHONY: docs-image-push
docs-image: docs-image-push:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs
+1 -18
View File
@@ -34,23 +34,6 @@ type PortSpec struct {
Mode string Mode string
} }
func (p *PortSpec) isHTTP() bool {
return p.Protocol == ProtocolHTTP || p.Protocol == ProtocolHTTPS
}
// AdjustUncloudMode makes adjustments for uncloud compatibility
func (p *PortSpec) AdjustUncloudMode() {
if p.Protocol == "" {
p.Protocol = "tcp"
}
if p.Mode == "" {
p.Mode = PortModeIngress
}
if p.Mode == PortModeIngress && !p.isHTTP() && p.PublishedPort != 0 {
p.Mode = PortModeHost
}
}
func (p *PortSpec) Validate() error { func (p *PortSpec) Validate() error {
if p.ContainerPort == 0 { if p.ContainerPort == 0 {
return fmt.Errorf("container port must be non-zero") return fmt.Errorf("container port must be non-zero")
@@ -73,7 +56,7 @@ func (p *PortSpec) Validate() error {
return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress) return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress)
} }
if p.Hostname != "" { if p.Hostname != "" {
if !p.isHTTP() { if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS) return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
} }
if err := validateHostname(p.Hostname); err != nil { if err := validateHostname(p.Hostname); err != nil {
+22 -7
View File
@@ -2,10 +2,12 @@ package compose
import ( import (
"fmt" "fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
"net/netip" "net/netip"
"strconv" "strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
) )
const PortsExtensionKey = "x-ports" const PortsExtensionKey = "x-ports"
@@ -20,7 +22,8 @@ func transformServicesPortsExtension(project *types.Project) (*types.Project, er
hasXPorts := service.Extensions[PortsExtensionKey] != nil hasXPorts := service.Extensions[PortsExtensionKey] != nil
if hasStandardPorts && hasXPorts { if hasStandardPorts && hasXPorts {
return service, fmt.Errorf("service %q cannot specify both 'ports' and 'x-ports' directives, use only one", name) return service, fmt.Errorf("service %q cannot specify both 'ports' and 'x-ports' directives, use only one",
name)
} }
var ( var (
@@ -32,7 +35,7 @@ func transformServicesPortsExtension(project *types.Project) (*types.Project, er
// Convert standard ports directly to api.PortSpec // Convert standard ports directly to api.PortSpec
specs, err = convertStandardPortsToPortSpecs(service.Ports) specs, err = convertStandardPortsToPortSpecs(service.Ports)
if err != nil { if err != nil {
return service, fmt.Errorf("convert standard ports for service %q: %w", name, err) return service, fmt.Errorf("convert standard 'ports' for service '%s': %w", name, err)
} }
} else if hasXPorts { } else if hasXPorts {
// Use existing x-ports string-based processing for backward compatibility // Use existing x-ports string-based processing for backward compatibility
@@ -82,8 +85,23 @@ func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortS
Protocol: port.Protocol, Protocol: port.Protocol,
Mode: port.Mode, Mode: port.Mode,
} }
// Compose parser sets the default protocol to "tcp" and mode to "ingress". We still explicitly set these values
// to avoid relying on implicit behavior and improve code robustness.
if spec.Protocol == "" {
spec.Protocol = api.ProtocolTCP
}
if spec.Mode == "" {
spec.Mode = api.PortModeIngress
}
// Set published port if specified // Set published port if specified
if port.Published != "" { if port.Published != "" {
if strings.Contains(port.Published, "-") {
// 'a-b:x' format is not automatically expanded by the compose parser and our PortSpec does not support port
// ranges for now.
return spec, fmt.Errorf("port range '%s' for published port is not supported, use a single port",
port.Published)
}
publishedPort, err := strconv.ParseUint(port.Published, 10, 16) publishedPort, err := strconv.ParseUint(port.Published, 10, 16)
if err != nil { if err != nil {
return spec, fmt.Errorf("invalid published port %q: %w", port.Published, err) return spec, fmt.Errorf("invalid published port %q: %w", port.Published, err)
@@ -100,9 +118,6 @@ func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortS
spec.HostIP = hostIP spec.HostIP = hostIP
} }
// Apply defaults according to uncloud
spec.AdjustUncloudMode()
// Validate the resulting spec // Validate the resulting spec
if err := spec.Validate(); err != nil { if err := spec.Validate(); err != nil {
return spec, fmt.Errorf("invalid port configuration: %w", err) return spec, fmt.Errorf("invalid port configuration: %w", err)
+21 -13
View File
@@ -22,14 +22,16 @@ func TestConvertStandardPortsToPortSpecs(t *testing.T) {
{ {
name: "multiple ports", name: "multiple ports",
ports: []types.ServicePortConfig{ ports: []types.ServicePortConfig{
{Target: 8080, Published: "80", Protocol: "tcp"}, {Target: 8080, Published: "80", Protocol: "tcp", Mode: "ingress"},
{Target: 8443, Published: "443", Protocol: "tcp", Mode: "host"}, {Target: 8443, Published: "443", Protocol: "tcp", Mode: "host"},
{Target: 5353, Published: "53", Protocol: "udp"}, {Target: 5353, Published: "53", Protocol: "udp"},
{Target: 2222, Published: "22"},
}, },
expected: []api.PortSpec{ expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"},
{ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "host"}, {ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "ingress"},
{ContainerPort: 2222, PublishedPort: 22, Protocol: "tcp", Mode: "ingress"},
}, },
}, },
{ {
@@ -103,7 +105,7 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
ContainerPort: 8080, ContainerPort: 8080,
PublishedPort: 80, PublishedPort: 80,
Protocol: "tcp", Protocol: "tcp",
Mode: "host", Mode: "ingress",
}, },
}, },
{ {
@@ -146,7 +148,7 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
ContainerPort: 5353, ContainerPort: 5353,
PublishedPort: 53, PublishedPort: 53,
Protocol: "udp", Protocol: "udp",
Mode: "host", Mode: "ingress",
}, },
}, },
{ {
@@ -167,7 +169,7 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
}, },
}, },
{ {
name: "HTTP protocol stays in ingress mode", name: "HTTP protocol",
port: types.ServicePortConfig{ port: types.ServicePortConfig{
Target: 8080, Target: 8080,
Published: "80", Published: "80",
@@ -181,7 +183,7 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
}, },
}, },
{ {
name: "HTTPS protocol stays in ingress mode", name: "HTTPS protocol",
port: types.ServicePortConfig{ port: types.ServicePortConfig{
Target: 8080, Target: 8080,
Published: "443", Published: "443",
@@ -219,6 +221,13 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
}, },
wantErr: "container port must be non-zero", wantErr: "container port must be non-zero",
}, },
{
name: "missing container port",
port: types.ServicePortConfig{
Published: "8000-9000",
},
wantErr: "port range '8000-9000' for published port is not supported",
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -227,8 +236,7 @@ func TestConvertServicePortConfigToPortSpec(t *testing.T) {
result, err := convertServicePortConfigToPortSpec(tt.port) result, err := convertServicePortConfigToPortSpec(tt.port)
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) assert.ErrorContains(t, err, tt.wantErr)
assert.Contains(t, err.Error(), tt.wantErr)
return return
} }
@@ -335,9 +343,9 @@ services:
- "53:5353/udp" - "53:5353/udp"
`, `,
expected: []api.PortSpec{ expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "host"}, {ContainerPort: 5353, PublishedPort: 53, Protocol: "udp", Mode: "ingress"},
}, },
}, },
{ {
@@ -357,7 +365,7 @@ services:
mode: host mode: host
`, `,
expected: []api.PortSpec{ expected: []api.PortSpec{
{ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "ingress"},
{ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"}, {ContainerPort: 8443, PublishedPort: 443, Protocol: "tcp", Mode: "host"},
}, },
}, },