Files
uncloud/pkg/client/compose/port.go
T
2025-07-21 19:30:02 +02:00

143 lines
4.1 KiB
Go

package compose
import (
"fmt"
"net/netip"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
)
const PortsExtensionKey = "x-ports"
type PortsSource []string
// transformServicesPortsExtension transforms both standard 'ports' and 'x-ports' to PortSpecs.
func transformServicesPortsExtension(project *types.Project) (*types.Project, error) {
return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
// Check for mutual exclusivity
hasStandardPorts := len(service.Ports) > 0
hasXPorts := service.Extensions[PortsExtensionKey] != nil
if hasStandardPorts && hasXPorts {
return service, fmt.Errorf("service %q cannot specify both 'ports' and 'x-ports' directives, use only one",
name)
}
var (
specs []api.PortSpec
err error
)
if hasStandardPorts {
// Convert standard ports directly to api.PortSpec
specs, err = convertStandardPortsToPortSpecs(service.Ports)
if err != nil {
return service, fmt.Errorf("convert standard 'ports' for service '%s': %w", name, err)
}
} else if hasXPorts {
// Use existing x-ports string-based processing for backward compatibility
var portsSource PortsSource
var ok bool
portsSource, ok = service.Extensions[PortsExtensionKey].(PortsSource)
if !ok {
return service, nil
}
// Parse the port strings using existing logic
specs, err = transformPortsExtension(portsSource)
if err != nil {
return service, err
}
} else {
// No ports specified
return service, nil
}
// Ensure extensions map exists before setting the port specs
if service.Extensions == nil {
service.Extensions = make(types.Extensions)
}
service.Extensions[PortsExtensionKey] = specs
return service, nil
})
}
func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) {
var specs []api.PortSpec
for _, port := range ports {
spec, err := api.ParsePortSpec(port)
if err != nil {
return specs, fmt.Errorf("parse port %q: %w", port, err)
}
specs = append(specs, spec)
}
return specs, nil
}
// convertServicePortConfigToPortSpec converts types.ServicePortConfig directly to api.PortSpec
func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortSpec, error) {
spec := api.PortSpec{
ContainerPort: uint16(port.Target),
Protocol: port.Protocol,
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
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)
if err != nil {
return spec, fmt.Errorf("invalid published port %q: %w", port.Published, err)
}
spec.PublishedPort = uint16(publishedPort)
}
// Set host IP if specified
if port.HostIP != "" {
hostIP, err := netip.ParseAddr(port.HostIP)
if err != nil {
return spec, fmt.Errorf("invalid host IP %q: %w", port.HostIP, err)
}
spec.HostIP = hostIP
}
// Validate the resulting spec
if err := spec.Validate(); err != nil {
return spec, fmt.Errorf("invalid port configuration: %w", err)
}
return spec, nil
}
// convertStandardPortsToPortSpecs converts []types.ServicePortConfig directly to api.PortSpecs.
func convertStandardPortsToPortSpecs(ports []types.ServicePortConfig) ([]api.PortSpec, error) {
specs := make([]api.PortSpec, 0, len(ports))
for _, port := range ports {
spec, err := convertServicePortConfigToPortSpec(port)
if err != nil {
return nil, err
}
specs = append(specs, spec)
}
return specs, nil
}