mirror of https://github.com/docker/cli.git
Merge pull request #30476 from yongtang/30447-port-config-long-syntax
Support expanded syntax of ports in `docker stack deploy`
This commit is contained in:
commit
ad5f8089c2
|
@ -3,6 +3,7 @@ package convert
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
@ -13,8 +14,6 @@ import (
|
|||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Services from compose-file types to engine API types
|
||||
|
@ -367,19 +366,16 @@ func (a byPublishedPort) Len() int { return len(a) }
|
|||
func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
|
||||
|
||||
func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
|
||||
func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
|
||||
portConfigs := []swarm.PortConfig{}
|
||||
ports, portBindings, err := nat.ParsePortSpecs(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for port := range ports {
|
||||
portConfig, err := opts.ConvertPortToPortConfig(port, portBindings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, port := range source {
|
||||
portConfig := swarm.PortConfig{
|
||||
Protocol: swarm.PortConfigProtocol(port.Protocol),
|
||||
TargetPort: port.Target,
|
||||
PublishedPort: port.Published,
|
||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||
}
|
||||
portConfigs = append(portConfigs, portConfig...)
|
||||
portConfigs = append(portConfigs, portConfig)
|
||||
}
|
||||
|
||||
sort.Sort(byPublishedPort(portConfigs))
|
||||
|
|
|
@ -143,6 +143,40 @@ func TestConvertHealthcheckDisableWithTest(t *testing.T) {
|
|||
assert.Error(t, err, "test and disable can't be set")
|
||||
}
|
||||
|
||||
func TestConvertEndpointSpec(t *testing.T) {
|
||||
source := []composetypes.ServicePortConfig{
|
||||
{
|
||||
Protocol: "udp",
|
||||
Target: 53,
|
||||
Published: 1053,
|
||||
Mode: "host",
|
||||
},
|
||||
{
|
||||
Target: 8080,
|
||||
Published: 80,
|
||||
},
|
||||
}
|
||||
endpoint, err := convertEndpointSpec(source)
|
||||
|
||||
expected := swarm.EndpointSpec{
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 80,
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 53,
|
||||
PublishedPort: 1053,
|
||||
PublishMode: "host",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, *endpoint, expected)
|
||||
}
|
||||
|
||||
func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
|
||||
networkConfigs := networkMap{}
|
||||
networks := map[string]*composetypes.ServiceNetworkConfig{}
|
||||
|
|
|
@ -12,7 +12,9 @@ import (
|
|||
"github.com/docker/docker/cli/compose/interpolation"
|
||||
"github.com/docker/docker/cli/compose/schema"
|
||||
"github.com/docker/docker/cli/compose/types"
|
||||
"github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/go-connections/nat"
|
||||
units "github.com/docker/go-units"
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
@ -237,6 +239,8 @@ func transformHook(
|
|||
return transformUlimits(data)
|
||||
case reflect.TypeOf(types.UnitBytes(0)):
|
||||
return transformSize(data)
|
||||
case reflect.TypeOf([]types.ServicePortConfig{}):
|
||||
return transformServicePort(data)
|
||||
case reflect.TypeOf(types.ServiceSecretConfig{}):
|
||||
return transformServiceSecret(data)
|
||||
case reflect.TypeOf(types.StringOrNumberList{}):
|
||||
|
@ -340,14 +344,14 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e
|
|||
|
||||
for _, file := range serviceConfig.EnvFile {
|
||||
filePath := absPath(workingDir, file)
|
||||
fileVars, err := opts.ParseEnvFile(filePath)
|
||||
fileVars, err := runconfigopts.ParseEnvFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envVars = append(envVars, fileVars...)
|
||||
}
|
||||
|
||||
for k, v := range opts.ConvertKVStringsToMap(envVars) {
|
||||
for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) {
|
||||
environment[k] = v
|
||||
}
|
||||
}
|
||||
|
@ -481,6 +485,41 @@ func transformExternal(data interface{}) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func transformServicePort(data interface{}) (interface{}, error) {
|
||||
switch entries := data.(type) {
|
||||
case []interface{}:
|
||||
// We process the list instead of individual items here.
|
||||
// The reason is that one entry might be mapped to multiple ServicePortConfig.
|
||||
// Therefore we take an input of a list and return an output of a list.
|
||||
ports := []interface{}{}
|
||||
for _, entry := range entries {
|
||||
switch value := entry.(type) {
|
||||
case int:
|
||||
v, err := toServicePortConfigs(fmt.Sprint(value))
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
ports = append(ports, v...)
|
||||
case string:
|
||||
v, err := toServicePortConfigs(value)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
ports = append(ports, v...)
|
||||
case types.Dict:
|
||||
ports = append(ports, value)
|
||||
case map[string]interface{}:
|
||||
ports = append(ports, value)
|
||||
default:
|
||||
return data, fmt.Errorf("invalid type %T for port", value)
|
||||
}
|
||||
}
|
||||
return ports, nil
|
||||
default:
|
||||
return data, fmt.Errorf("invalid type %T for port", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func transformServiceSecret(data interface{}) (interface{}, error) {
|
||||
switch value := data.(type) {
|
||||
case string:
|
||||
|
@ -572,6 +611,39 @@ func transformSize(value interface{}) (int64, error) {
|
|||
panic(fmt.Errorf("invalid type for size %T", value))
|
||||
}
|
||||
|
||||
func toServicePortConfigs(value string) ([]interface{}, error) {
|
||||
var portConfigs []interface{}
|
||||
|
||||
ports, portBindings, err := nat.ParsePortSpecs([]string{value})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We need to sort the key of the ports to make sure it is consistent
|
||||
keys := []string{}
|
||||
for port := range ports {
|
||||
keys = append(keys, string(port))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
// Reuse ConvertPortToPortConfig so that it is consistent
|
||||
portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range portConfig {
|
||||
portConfigs = append(portConfigs, types.ServicePortConfig{
|
||||
Protocol: string(p.Protocol),
|
||||
Target: p.TargetPort,
|
||||
Published: p.PublishedPort,
|
||||
Mode: string(p.PublishMode),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return portConfigs, nil
|
||||
}
|
||||
|
||||
func toMapStringString(value map[string]interface{}) map[string]string {
|
||||
output := make(map[string]string)
|
||||
for key, value := range value {
|
||||
|
|
|
@ -675,14 +675,145 @@ func TestFullExample(t *testing.T) {
|
|||
"other-other-network": nil,
|
||||
},
|
||||
Pid: "host",
|
||||
Ports: []string{
|
||||
"3000",
|
||||
"3000-3005",
|
||||
"8000:8000",
|
||||
"9090-9091:8080-8081",
|
||||
"49100:22",
|
||||
"127.0.0.1:8001:8001",
|
||||
"127.0.0.1:5000-5010:5000-5010",
|
||||
Ports: []types.ServicePortConfig{
|
||||
//"3000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"3000-3005",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"8000:8000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8000,
|
||||
Published: 8000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"9090-9091:8080-8081",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8080,
|
||||
Published: 9090,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8081,
|
||||
Published: 9091,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"49100:22",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 22,
|
||||
Published: 49100,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:8001:8001",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8001,
|
||||
Published: 8001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:5000-5010:5000-5010",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5000,
|
||||
Published: 5000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5001,
|
||||
Published: 5001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5002,
|
||||
Published: 5002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5003,
|
||||
Published: 5003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5004,
|
||||
Published: 5004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5005,
|
||||
Published: 5005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5006,
|
||||
Published: 5006,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5007,
|
||||
Published: 5007,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5008,
|
||||
Published: 5008,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5009,
|
||||
Published: 5009,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5010,
|
||||
Published: 5010,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
Privileged: true,
|
||||
ReadOnly: true,
|
||||
|
@ -825,3 +956,88 @@ networks:
|
|||
|
||||
assert.Equal(t, expected, config.Networks)
|
||||
}
|
||||
|
||||
func TestLoadExpandedPortFormat(t *testing.T) {
|
||||
config, err := loadYAML(`
|
||||
version: "3.1"
|
||||
services:
|
||||
web:
|
||||
image: busybox
|
||||
ports:
|
||||
- "80-82:8080-8082"
|
||||
- "90-92:8090-8092/udp"
|
||||
- "85:8500"
|
||||
- 8600
|
||||
- protocol: udp
|
||||
target: 53
|
||||
published: 10053
|
||||
- mode: host
|
||||
target: 22
|
||||
published: 10022
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := []types.ServicePortConfig{
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8080,
|
||||
Published: 80,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8081,
|
||||
Published: 81,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8082,
|
||||
Published: 82,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8090,
|
||||
Published: 90,
|
||||
Protocol: "udp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8091,
|
||||
Published: 91,
|
||||
Protocol: "udp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8092,
|
||||
Published: 92,
|
||||
Protocol: "udp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8500,
|
||||
Published: 85,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8600,
|
||||
Published: 0,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Target: 53,
|
||||
Published: 10053,
|
||||
Protocol: "udp",
|
||||
},
|
||||
{
|
||||
Mode: "host",
|
||||
Target: 22,
|
||||
Published: 10022,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(config.Services))
|
||||
assert.Equal(t, expected, config.Services[0].Ports)
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -167,8 +167,20 @@
|
|||
"ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["string", "number"],
|
||||
"format": "ports"
|
||||
"oneOf": [
|
||||
{"type": "number", "format": "ports"},
|
||||
{"type": "string", "format": "ports"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"target": {"type": "integer"},
|
||||
"published": {"type": "integer"},
|
||||
"protocol": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
|
|
@ -106,7 +106,7 @@ type ServiceConfig struct {
|
|||
NetworkMode string `mapstructure:"network_mode"`
|
||||
Networks map[string]*ServiceNetworkConfig
|
||||
Pid string
|
||||
Ports StringOrNumberList
|
||||
Ports []ServicePortConfig
|
||||
Privileged bool
|
||||
ReadOnly bool `mapstructure:"read_only"`
|
||||
Restart string
|
||||
|
@ -215,6 +215,14 @@ type ServiceNetworkConfig struct {
|
|||
Ipv6Address string `mapstructure:"ipv6_address"`
|
||||
}
|
||||
|
||||
// ServicePortConfig is the port configuration for a service
|
||||
type ServicePortConfig struct {
|
||||
Mode string
|
||||
Target uint32
|
||||
Published uint32
|
||||
Protocol string
|
||||
}
|
||||
|
||||
// ServiceSecretConfig is the secret configuration for a service
|
||||
type ServiceSecretConfig struct {
|
||||
Source string
|
||||
|
|
Loading…
Reference in New Issue