mirror of https://github.com/docker/cli.git
Remove --port and update --publish for services to support syntaxes
Add support for simple and complex syntax to `--publish` through the use of `PortOpt`. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
1b400f6284
commit
016a56b064
37
opts/port.go
37
opts/port.go
|
@ -3,10 +3,12 @@ package opts
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -23,6 +25,11 @@ type PortOpt struct {
|
||||||
|
|
||||||
// Set a new port value
|
// Set a new port value
|
||||||
func (p *PortOpt) Set(value string) error {
|
func (p *PortOpt) Set(value string) error {
|
||||||
|
longSyntax, err := regexp.MatchString(`\w+=\w+(,\w+=\w+)*`, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if longSyntax {
|
||||||
csvReader := csv.NewReader(strings.NewReader(value))
|
csvReader := csv.NewReader(strings.NewReader(value))
|
||||||
fields, err := csvReader.Read()
|
fields, err := csvReader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -76,6 +83,17 @@ func (p *PortOpt) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ports = append(p.ports, pConfig)
|
p.ports = append(p.ports, pConfig)
|
||||||
|
} else {
|
||||||
|
// short syntax
|
||||||
|
portConfigs := []swarm.PortConfig{}
|
||||||
|
// We can ignore errors because the format was already validated by ValidatePort
|
||||||
|
ports, portBindings, _ := nat.ParsePortSpecs([]string{value})
|
||||||
|
|
||||||
|
for port := range ports {
|
||||||
|
portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
|
||||||
|
}
|
||||||
|
p.ports = append(p.ports, portConfigs...)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,3 +116,22 @@ func (p *PortOpt) String() string {
|
||||||
func (p *PortOpt) Value() []swarm.PortConfig {
|
func (p *PortOpt) Value() []swarm.PortConfig {
|
||||||
return p.ports
|
return p.ports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertPortToPortConfig converts ports to the swarm type
|
||||||
|
func ConvertPortToPortConfig(
|
||||||
|
port nat.Port,
|
||||||
|
portBindings map[nat.Port][]nat.PortBinding,
|
||||||
|
) []swarm.PortConfig {
|
||||||
|
ports := []swarm.PortConfig{}
|
||||||
|
|
||||||
|
for _, binding := range portBindings[port] {
|
||||||
|
hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
|
||||||
|
ports = append(ports, swarm.PortConfig{
|
||||||
|
//TODO Name: ?
|
||||||
|
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
|
||||||
|
TargetPort: uint32(port.Int()),
|
||||||
|
PublishedPort: uint32(hostPort),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,243 @@
|
||||||
|
package opts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPortOptValidSimpleSyntax(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expected []swarm.PortConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
value: "80",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "80:8080",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "8080:80/tcp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "80:8080/udp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "udp",
|
||||||
|
TargetPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "80-81:8080-8081/tcp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 8081,
|
||||||
|
PublishedPort: 81,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "80-82:8080-8082/udp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "udp",
|
||||||
|
TargetPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Protocol: "udp",
|
||||||
|
TargetPort: 8081,
|
||||||
|
PublishedPort: 81,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Protocol: "udp",
|
||||||
|
TargetPort: 8082,
|
||||||
|
PublishedPort: 82,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
var port PortOpt
|
||||||
|
assert.NilError(t, port.Set(tc.value))
|
||||||
|
assert.Equal(t, len(port.Value()), len(tc.expected))
|
||||||
|
for _, expectedPortConfig := range tc.expected {
|
||||||
|
assertContains(t, port.Value(), expectedPortConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortOptValidComplexSyntax(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expected []swarm.PortConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
value: "target=80",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
TargetPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=80,protocol=tcp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=80,published=8080,protocol=tcp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "published=80,target=8080,protocol=tcp",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=80,published=8080,protocol=tcp,mode=host",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: "tcp",
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: 8080,
|
||||||
|
PublishMode: "host",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=80,published=8080,mode=host",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: 8080,
|
||||||
|
PublishMode: "host",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=80,published=8080,mode=ingress",
|
||||||
|
expected: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: 8080,
|
||||||
|
PublishMode: "ingress",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
var port PortOpt
|
||||||
|
assert.NilError(t, port.Set(tc.value))
|
||||||
|
assert.Equal(t, len(port.Value()), len(tc.expected))
|
||||||
|
for _, expectedPortConfig := range tc.expected {
|
||||||
|
assertContains(t, port.Value(), expectedPortConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortOptInvalidComplexSyntax(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
value: "invalid,target=80",
|
||||||
|
expectedError: "invalid field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "invalid=field",
|
||||||
|
expectedError: "invalid field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "protocol=invalid",
|
||||||
|
expectedError: "invalid protocol value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "target=invalid",
|
||||||
|
expectedError: "invalid syntax",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "published=invalid",
|
||||||
|
expectedError: "invalid syntax",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "mode=invalid",
|
||||||
|
expectedError: "invalid publish mode value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "published=8080,protocol=tcp,mode=ingress",
|
||||||
|
expectedError: "missing mandatory field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `target=80,protocol="tcp,mode=ingress"`,
|
||||||
|
expectedError: "non-quoted-field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `target=80,"protocol=tcp,mode=ingress"`,
|
||||||
|
expectedError: "invalid protocol value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
var port PortOpt
|
||||||
|
assert.Error(t, port.Set(tc.value), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContains(t *testing.T, portConfigs []swarm.PortConfig, expected swarm.PortConfig) {
|
||||||
|
var contains = false
|
||||||
|
for _, portConfig := range portConfigs {
|
||||||
|
if portConfig == expected {
|
||||||
|
contains = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !contains {
|
||||||
|
t.Errorf("expected %v to contain %v, did not", portConfigs, expected)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue