diff --git a/docs/api/meta_v1alpha1.md b/docs/api/meta_v1alpha1.md index 1fb844517..2f021acaa 100644 --- a/docs/api/meta_v1alpha1.md +++ b/docs/api/meta_v1alpha1.md @@ -64,6 +64,10 @@ - [func ParsePortMappings(input \[\]string) (PortMappings, error)](#ParsePortMappings) - [func (p PortMappings) String() string](#PortMappings.String) + - [type Protocol](#Protocol) + - [func (p Protocol) String() string](#Protocol.String) + - [func (p \*Protocol) UnmarshalJSON(b \[\]byte) (err + error)](#Protocol.UnmarshalJSON) - [type Size](#Size) - [func NewSizeFromBytes(bytes uint64) Size](#NewSizeFromBytes) - [func NewSizeFromSectors(sectors uint64) @@ -192,7 +196,7 @@ func (d *DMID) Pool() bool func (d DMID) String() string ``` -## type [IPAddresses](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=1541:1566#L78) +## type [IPAddresses](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=3422:3447#L155) ``` go type IPAddresses []net.IP @@ -200,7 +204,7 @@ type IPAddresses []net.IP IPAddresses represents a list of VM IP addresses -### func (IPAddresses) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=1604:1640#L82) +### func (IPAddresses) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=3485:3521#L159) ``` go func (i IPAddresses) String() string @@ -412,24 +416,26 @@ func (o *ObjectMeta) SetUID(uid UID) SetUID sets the UID of the Object -## type [PortMapping](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=132:227#L11) +## type [PortMapping](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=190:398#L14) ``` go type PortMapping struct { - HostPort uint64 `json:"hostPort"` - VMPort uint64 `json:"vmPort"` + BindAddress net.IP `json:"bindAddress,omitempty"` + HostPort uint64 `json:"hostPort"` + VMPort uint64 `json:"vmPort"` + Protocol Protocol `json:"protocol,omitempty"` } ``` PortMapping defines a port mapping between the VM and the host -### func (PortMapping) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=265:301#L18) +### func (PortMapping) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=436:472#L23) ``` go func (p PortMapping) String() string ``` -## type [PortMappings](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=418:449#L23) +## type [PortMappings](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=826:857#L42) ``` go type PortMappings []PortMapping @@ -437,18 +443,45 @@ type PortMappings []PortMapping PortMappings represents a list of port mappings -### func [ParsePortMappings](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=488:548#L27) +### func [ParsePortMappings](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=896:956#L46) ``` go func ParsePortMappings(input []string) (PortMappings, error) ``` -### func (PortMappings) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=1249:1286#L61) +### func (PortMappings) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=2475:2512#L104) ``` go func (p PortMappings) String() string ``` +## type [Protocol](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=2761:2781#L121) + +``` go +type Protocol string +``` + +Protocol specifies a network port protocol + +``` go +const ( + ProtocolTCP Protocol = "tcp" + ProtocolUDP Protocol = "udp" +) +``` + +### func (Protocol) [String](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=3135:3168#L140) + +``` go +func (p Protocol) String() string +``` + +### func (\*Protocol) [UnmarshalJSON](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/net.go?s=3192:3246#L144) + +``` go +func (p *Protocol) UnmarshalJSON(b []byte) (err error) +``` + ## type [Size](https://github.com/weaveworks/ignite/tree/master/pkg/apis/meta/v1alpha1/size.go?s=132:171#L11) ``` go diff --git a/pkg/apis/ignite/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/ignite/v1alpha1/zz_generated.deepcopy.go index 16bd93897..d30019f70 100644 --- a/pkg/apis/ignite/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/ignite/v1alpha1/zz_generated.deepcopy.go @@ -364,7 +364,9 @@ func (in *VMNetworkSpec) DeepCopyInto(out *VMNetworkSpec) { if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make(metav1alpha1.PortMappings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/pkg/apis/ignite/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/ignite/v1alpha2/zz_generated.deepcopy.go index aff460033..ba143a5eb 100644 --- a/pkg/apis/ignite/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/ignite/v1alpha2/zz_generated.deepcopy.go @@ -396,7 +396,9 @@ func (in *VMNetworkSpec) DeepCopyInto(out *VMNetworkSpec) { if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make(v1alpha1.PortMappings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/pkg/apis/ignite/validation/validation.go b/pkg/apis/ignite/validation/validation.go index 5ea530f4d..1cfb1b332 100644 --- a/pkg/apis/ignite/validation/validation.go +++ b/pkg/apis/ignite/validation/validation.go @@ -18,6 +18,7 @@ func ValidateVM(obj *api.VM) (allErrs field.ErrorList) { allErrs = append(allErrs, ValidateFileMappings(&obj.Spec.CopyFiles, field.NewPath(".spec.copyFiles"))...) allErrs = append(allErrs, ValidateVMStorage(&obj.Spec.Storage, field.NewPath(".spec.storage"))...) // TODO: Add vCPU, memory, disk max and min sizes + // TODO: Add port mapping validation return } diff --git a/pkg/apis/ignite/zz_generated.deepcopy.go b/pkg/apis/ignite/zz_generated.deepcopy.go index 7d95e39ff..e332d1204 100644 --- a/pkg/apis/ignite/zz_generated.deepcopy.go +++ b/pkg/apis/ignite/zz_generated.deepcopy.go @@ -396,7 +396,9 @@ func (in *VMNetworkSpec) DeepCopyInto(out *VMNetworkSpec) { if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make(v1alpha1.PortMappings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/pkg/apis/meta/v1alpha1/net.go b/pkg/apis/meta/v1alpha1/net.go index bf51f0af7..44c9885ae 100644 --- a/pkg/apis/meta/v1alpha1/net.go +++ b/pkg/apis/meta/v1alpha1/net.go @@ -1,22 +1,41 @@ package v1alpha1 import ( + "encoding/json" "fmt" "net" "strconv" "strings" + + "github.com/docker/go-connections/nat" ) // PortMapping defines a port mapping between the VM and the host type PortMapping struct { - HostPort uint64 `json:"hostPort"` - VMPort uint64 `json:"vmPort"` + BindAddress net.IP `json:"bindAddress,omitempty"` + HostPort uint64 `json:"hostPort"` + VMPort uint64 `json:"vmPort"` + Protocol Protocol `json:"protocol,omitempty"` } var _ fmt.Stringer = PortMapping{} func (p PortMapping) String() string { - return fmt.Sprintf("0.0.0.0:%d->%d", p.HostPort, p.VMPort) + var sb strings.Builder + + if p.BindAddress != nil { + sb.WriteString(p.BindAddress.String()) + } else { + sb.WriteString("0.0.0.0") + } + + sb.WriteString(fmt.Sprintf(":%d->%d", p.HostPort, p.VMPort)) + + if len(p.Protocol) > 0 { + sb.WriteString(fmt.Sprintf("/%s", p.Protocol)) + } + + return sb.String() } // PortMappings represents a list of port mappings @@ -27,32 +46,56 @@ var _ fmt.Stringer = PortMappings{} func ParsePortMappings(input []string) (PortMappings, error) { result := make(PortMappings, 0, len(input)) - for _, portMapping := range input { - ports := strings.Split(portMapping, ":") - if len(ports) != 2 { - return nil, fmt.Errorf("port mappings must be of form :") + _, bindings, err := nat.ParsePortSpecs(input) + if err != nil { + return nil, err + } + + for port, bindings := range bindings { + if len(bindings) > 1 { + // TODO: For now only support mapping a VM port to a single host IP/port + return nil, fmt.Errorf("only one host binding per VM binding supported for now, received %d", len(bindings)) } - hostPort, err := strconv.ParseUint(ports[0], 10, 64) - if err != nil { - return nil, err + binding := bindings[0] + var err error + var bindAddress net.IP + var hostPort uint64 + var vmPort uint64 + var protocol Protocol + + if len(binding.HostIP) > 0 { + if bindAddress = net.ParseIP(binding.HostIP); bindAddress == nil { + return nil, fmt.Errorf("invalid bind address: %q", binding.HostIP) + } + } + + if hostPort, err = strconv.ParseUint(binding.HostPort, 10, 64); err != nil { + return nil, fmt.Errorf("invalid host port: %q", binding.HostPort) + } + + if vmPort, err = strconv.ParseUint(port.Port(), 10, 64); err != nil { + return nil, fmt.Errorf("invalid VM port: %q", port.Port()) } - vmPort, err := strconv.ParseUint(ports[1], 10, 64) - if err != nil { + if protocol, err = protocolFromString(port.Proto()); err != nil { return nil, err } + mapping := PortMapping{ + BindAddress: bindAddress, + HostPort: hostPort, + VMPort: vmPort, + Protocol: protocol, + } + for _, portMapping := range result { - if portMapping.HostPort == hostPort { - return nil, fmt.Errorf("cannot use a port on the host twice") + if portMapping.HostPort == mapping.HostPort && portMapping.Protocol == mapping.Protocol { + return nil, fmt.Errorf("cannot use a port/protocol combination on the host twice") } } - result = append(result, PortMapping{ - HostPort: hostPort, - VMPort: vmPort, - }) + result = append(result, mapping) } return result, nil @@ -74,6 +117,40 @@ func (p PortMappings) String() string { return sb.String() } +// Protocol specifies a network port protocol +type Protocol string + +const ( + ProtocolTCP Protocol = "tcp" + ProtocolUDP Protocol = "udp" +) + +var _ fmt.Stringer = Protocol("") + +func protocolFromString(input string) (Protocol, error) { + for _, protocol := range []Protocol{ProtocolTCP, ProtocolUDP} { + if protocol.String() == input { + return protocol, nil + } + } + + return "", fmt.Errorf("invalid protocol: %q", input) +} + +func (p Protocol) String() string { + return string(p) +} + +func (p *Protocol) UnmarshalJSON(b []byte) (err error) { + var s string + if err = json.Unmarshal(b, &s); err != nil { + return err + } + + *p, err = protocolFromString(s) + return +} + // IPAddresses represents a list of VM IP addresses type IPAddresses []net.IP diff --git a/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go index fdf44d19a..e17ed7317 100644 --- a/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go @@ -146,6 +146,11 @@ func (in *ObjectMeta) DeepCopy() *ObjectMeta { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PortMapping) DeepCopyInto(out *PortMapping) { *out = *in + if in.BindAddress != nil { + in, out := &in.BindAddress, &out.BindAddress + *out = make(net.IP, len(*in)) + copy(*out, *in) + } return } @@ -164,7 +169,9 @@ func (in PortMappings) DeepCopyInto(out *PortMappings) { { in := &in *out = make(PortMappings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } return } } diff --git a/pkg/openapi/openapi_generated.go b/pkg/openapi/openapi_generated.go index f4c6b0111..0dbf86a1c 100644 --- a/pkg/openapi/openapi_generated.go +++ b/pkg/openapi/openapi_generated.go @@ -1620,6 +1620,12 @@ func schema_pkg_apis_meta_v1alpha1_PortMapping(ref common.ReferenceCallback) com Description: "PortMapping defines a port mapping between the VM and the host", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "bindAddress": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "byte", + }, + }, "hostPort": { SchemaProps: spec.SchemaProps{ Type: []string{"integer"}, @@ -1632,6 +1638,12 @@ func schema_pkg_apis_meta_v1alpha1_PortMapping(ref common.ReferenceCallback) com Format: "int64", }, }, + "protocol": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"hostPort", "vmPort"}, }, diff --git a/pkg/runtime/docker/client.go b/pkg/runtime/docker/client.go index 57aa6de5c..319fc97b1 100644 --- a/pkg/runtime/docker/client.go +++ b/pkg/runtime/docker/client.go @@ -10,14 +10,12 @@ import ( "github.com/docker/docker/api/types/container" cont "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" "github.com/weaveworks/ignite/pkg/runtime" "github.com/weaveworks/ignite/pkg/util" ) const ( dockerNetNSFmt = "/proc/%v/ns/net" - portFormat = "%d/tcp" // TODO: Support protocols other than TCP ) // dockerClient is a runtime.Interface @@ -103,15 +101,6 @@ func (dc *dockerClient) AttachContainer(container string) (err error) { } func (dc *dockerClient) RunContainer(image string, config *runtime.ContainerConfig, name string) (string, error) { - portBindings := make(nat.PortMap) - for _, portMapping := range config.PortBindings { - portBindings[nat.Port(fmt.Sprintf(portFormat, portMapping.VMPort))] = []nat.PortBinding{ - { - HostPort: fmt.Sprintf(portFormat, portMapping.HostPort), - }, - } - } - binds := make([]string, 0, len(config.Binds)) for _, bind := range config.Binds { binds = append(binds, fmt.Sprintf("%s:%s", bind.HostPath, bind.ContainerPath)) @@ -139,7 +128,7 @@ func (dc *dockerClient) RunContainer(image string, config *runtime.ContainerConf }, &container.HostConfig{ Binds: binds, NetworkMode: container.NetworkMode(config.NetworkMode), - PortBindings: portBindings, + PortBindings: portBindingsToPortMap(config.PortBindings), AutoRemove: config.AutoRemove, CapAdd: config.CapAdds, Resources: container.Resources{ diff --git a/pkg/runtime/docker/port.go b/pkg/runtime/docker/port.go new file mode 100644 index 000000000..8522701c7 --- /dev/null +++ b/pkg/runtime/docker/port.go @@ -0,0 +1,35 @@ +package docker + +import ( + "fmt" + "strconv" + + "github.com/docker/go-connections/nat" + meta "github.com/weaveworks/ignite/pkg/apis/meta/v1alpha1" +) + +func portBindingsToPortMap(portMappings meta.PortMappings) nat.PortMap { + portMap := make(nat.PortMap) + + for _, portMapping := range portMappings { + var hostIP string + if portMapping.BindAddress != nil { + hostIP = portMapping.BindAddress.String() + } + + protocol := portMapping.Protocol + if len(protocol) == 0 { + // Docker uses TCP by default + protocol = meta.ProtocolTCP + } + + portMap[nat.Port(fmt.Sprintf("%d/%s", portMapping.VMPort, protocol.String()))] = []nat.PortBinding{ + { + HostIP: hostIP, + HostPort: strconv.FormatUint(portMapping.HostPort, 10), + }, + } + } + + return portMap +}