Support expanded ports in Compose loader

This commit adds support for expanded ports in Compose loader,
and add several unit tests for loading expanded port format.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
Yong Tang 2017-01-31 12:45:45 -08:00
parent 60c1eaf8f0
commit f07a28a541
4 changed files with 342 additions and 24 deletions

View file

@ -3,6 +3,7 @@ package convert
import ( import (
"fmt" "fmt"
"os" "os"
"sort"
"time" "time"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -13,8 +14,6 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts" runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
"sort"
) )
// Services from compose-file types to engine API types // 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) 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 (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{} portConfigs := []swarm.PortConfig{}
ports, portBindings, err := nat.ParsePortSpecs(source) for _, port := range source {
if err != nil { portConfig := swarm.PortConfig{
return nil, err Protocol: swarm.PortConfigProtocol(port.Protocol),
} TargetPort: port.Target,
PublishedPort: port.Published,
for port := range ports { PublishMode: swarm.PortConfigPublishMode(port.Mode),
portConfig, err := opts.ConvertPortToPortConfig(port, portBindings)
if err != nil {
return nil, err
} }
portConfigs = append(portConfigs, portConfig...) portConfigs = append(portConfigs, portConfig)
} }
sort.Sort(byPublishedPort(portConfigs)) sort.Sort(byPublishedPort(portConfigs))

View file

@ -143,6 +143,40 @@ func TestConvertHealthcheckDisableWithTest(t *testing.T) {
assert.Error(t, err, "test and disable can't be set") 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) { func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
networkConfigs := networkMap{} networkConfigs := networkMap{}
networks := map[string]*composetypes.ServiceNetworkConfig{} networks := map[string]*composetypes.ServiceNetworkConfig{}

View file

@ -12,7 +12,9 @@ import (
"github.com/docker/docker/cli/compose/interpolation" "github.com/docker/docker/cli/compose/interpolation"
"github.com/docker/docker/cli/compose/schema" "github.com/docker/docker/cli/compose/schema"
"github.com/docker/docker/cli/compose/types" "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" units "github.com/docker/go-units"
shellwords "github.com/mattn/go-shellwords" shellwords "github.com/mattn/go-shellwords"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
@ -237,6 +239,8 @@ func transformHook(
return transformUlimits(data) return transformUlimits(data)
case reflect.TypeOf(types.UnitBytes(0)): case reflect.TypeOf(types.UnitBytes(0)):
return transformSize(data) return transformSize(data)
case reflect.TypeOf([]types.ServicePortConfig{}):
return transformServicePort(data)
case reflect.TypeOf(types.ServiceSecretConfig{}): case reflect.TypeOf(types.ServiceSecretConfig{}):
return transformServiceSecret(data) return transformServiceSecret(data)
case reflect.TypeOf(types.StringOrNumberList{}): case reflect.TypeOf(types.StringOrNumberList{}):
@ -340,14 +344,14 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e
for _, file := range serviceConfig.EnvFile { for _, file := range serviceConfig.EnvFile {
filePath := absPath(workingDir, file) filePath := absPath(workingDir, file)
fileVars, err := opts.ParseEnvFile(filePath) fileVars, err := runconfigopts.ParseEnvFile(filePath)
if err != nil { if err != nil {
return err return err
} }
envVars = append(envVars, fileVars...) envVars = append(envVars, fileVars...)
} }
for k, v := range opts.ConvertKVStringsToMap(envVars) { for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) {
environment[k] = v 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) { func transformServiceSecret(data interface{}) (interface{}, error) {
switch value := data.(type) { switch value := data.(type) {
case string: case string:
@ -572,6 +611,39 @@ func transformSize(value interface{}) (int64, error) {
panic(fmt.Errorf("invalid type for size %T", value)) 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 { func toMapStringString(value map[string]interface{}) map[string]string {
output := make(map[string]string) output := make(map[string]string)
for key, value := range value { for key, value := range value {

View file

@ -675,14 +675,145 @@ func TestFullExample(t *testing.T) {
"other-other-network": nil, "other-other-network": nil,
}, },
Pid: "host", Pid: "host",
Ports: []string{ Ports: []types.ServicePortConfig{
"3000", //"3000",
"3000-3005", {
"8000:8000", Mode: "ingress",
"9090-9091:8080-8081", Target: 3000,
"49100:22", Protocol: "tcp",
"127.0.0.1:8001:8001", },
"127.0.0.1:5000-5010:5000-5010", //"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, Privileged: true,
ReadOnly: true, ReadOnly: true,
@ -825,3 +956,88 @@ networks:
assert.Equal(t, expected, config.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)
}