Skip to content

Commit fea7edc

Browse files
authored
feat: add support for standard compose ports directive (#95)
1 parent a54555c commit fea7edc

File tree

3 files changed

+518
-9
lines changed

3 files changed

+518
-9
lines changed

pkg/api/port.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ type PortSpec struct {
3434
Mode string
3535
}
3636

37+
func (p *PortSpec) isHTTP() bool {
38+
return p.Protocol == ProtocolHTTP || p.Protocol == ProtocolHTTPS
39+
}
40+
41+
// AdjustUncloudMode makes adjustments for uncloud compatibility
42+
func (p *PortSpec) AdjustUncloudMode() {
43+
if p.Protocol == "" {
44+
p.Protocol = "tcp"
45+
}
46+
if p.Mode == "" {
47+
p.Mode = PortModeIngress
48+
}
49+
if p.Mode == PortModeIngress && !p.isHTTP() && p.PublishedPort != 0 {
50+
p.Mode = PortModeHost
51+
}
52+
}
53+
3754
func (p *PortSpec) Validate() error {
3855
if p.ContainerPort == 0 {
3956
return fmt.Errorf("container port must be non-zero")
@@ -56,7 +73,7 @@ func (p *PortSpec) Validate() error {
5673
return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress)
5774
}
5875
if p.Hostname != "" {
59-
if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
76+
if !p.isHTTP() {
6077
return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
6178
}
6279
if err := validateHostname(p.Hostname); err != nil {

pkg/client/compose/port.go

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,59 @@ import (
44
"fmt"
55
"github.com/compose-spec/compose-go/v2/types"
66
"github.com/psviderski/uncloud/pkg/api"
7+
"net/netip"
8+
"strconv"
79
)
810

911
const PortsExtensionKey = "x-ports"
1012

1113
type PortsSource []string
1214

13-
// TransformServicesPortsExtension transforms the ports extension of all services in the project by replacing a string
14-
// representation of each port with a parsed PortSpec.
15+
// transformServicesPortsExtension transforms both standard 'ports' and 'x-ports' to PortSpecs.
1516
func transformServicesPortsExtension(project *types.Project) (*types.Project, error) {
1617
return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
17-
ports, ok := service.Extensions[PortsExtensionKey].(PortsSource)
18-
if !ok {
19-
return service, nil
18+
// Check for mutual exclusivity
19+
hasStandardPorts := len(service.Ports) > 0
20+
hasXPorts := service.Extensions[PortsExtensionKey] != nil
21+
22+
if hasStandardPorts && hasXPorts {
23+
return service, fmt.Errorf("service %q cannot specify both 'ports' and 'x-ports' directives, use only one", name)
2024
}
2125

22-
specs, err := transformPortsExtension(ports)
23-
if err != nil {
24-
return service, err
26+
var (
27+
specs []api.PortSpec
28+
err error
29+
)
30+
31+
if hasStandardPorts {
32+
// Convert standard ports directly to api.PortSpec
33+
specs, err = convertStandardPortsToPortSpecs(service.Ports)
34+
if err != nil {
35+
return service, fmt.Errorf("convert standard ports for service %q: %w", name, err)
36+
}
37+
} else if hasXPorts {
38+
// Use existing x-ports string-based processing for backward compatibility
39+
var portsSource PortsSource
40+
var ok bool
41+
portsSource, ok = service.Extensions[PortsExtensionKey].(PortsSource)
42+
if !ok {
43+
return service, nil
44+
}
45+
46+
// Parse the port strings using existing logic
47+
specs, err = transformPortsExtension(portsSource)
48+
if err != nil {
49+
return service, err
50+
}
51+
} else {
52+
// No ports specified
53+
return service, nil
2554
}
2655

56+
// Ensure extensions map exists before setting the port specs
57+
if service.Extensions == nil {
58+
service.Extensions = make(types.Extensions)
59+
}
2760
service.Extensions[PortsExtensionKey] = specs
2861
return service, nil
2962
})
@@ -41,3 +74,54 @@ func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) {
4174

4275
return specs, nil
4376
}
77+
78+
// convertServicePortConfigToPortSpec converts types.ServicePortConfig directly to api.PortSpec
79+
func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortSpec, error) {
80+
spec := api.PortSpec{
81+
ContainerPort: uint16(port.Target),
82+
Protocol: port.Protocol,
83+
Mode: port.Mode,
84+
}
85+
// Set published port if specified
86+
if port.Published != "" {
87+
publishedPort, err := strconv.ParseUint(port.Published, 10, 16)
88+
if err != nil {
89+
return spec, fmt.Errorf("invalid published port %q: %w", port.Published, err)
90+
}
91+
spec.PublishedPort = uint16(publishedPort)
92+
}
93+
94+
// Set host IP if specified
95+
if port.HostIP != "" {
96+
hostIP, err := netip.ParseAddr(port.HostIP)
97+
if err != nil {
98+
return spec, fmt.Errorf("invalid host IP %q: %w", port.HostIP, err)
99+
}
100+
spec.HostIP = hostIP
101+
}
102+
103+
// Apply defaults according to uncloud
104+
spec.AdjustUncloudMode()
105+
106+
// Validate the resulting spec
107+
if err := spec.Validate(); err != nil {
108+
return spec, fmt.Errorf("invalid port configuration: %w", err)
109+
}
110+
111+
return spec, nil
112+
}
113+
114+
// convertStandardPortsToPortSpecs converts []types.ServicePortConfig directly to api.PortSpecs.
115+
func convertStandardPortsToPortSpecs(ports []types.ServicePortConfig) ([]api.PortSpec, error) {
116+
var specs = make([]api.PortSpec, 0, len(ports))
117+
118+
for _, port := range ports {
119+
spec, err := convertServicePortConfigToPortSpec(port)
120+
if err != nil {
121+
return nil, err
122+
}
123+
specs = append(specs, spec)
124+
}
125+
126+
return specs, nil
127+
}

0 commit comments

Comments
 (0)