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:
parent
60c1eaf8f0
commit
f07a28a541
4 changed files with 342 additions and 24 deletions
|
@ -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,
|
||||||
|
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||||
}
|
}
|
||||||
|
portConfigs = append(portConfigs, portConfig)
|
||||||
for port := range ports {
|
|
||||||
portConfig, err := opts.ConvertPortToPortConfig(port, portBindings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
portConfigs = append(portConfigs, portConfig...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(byPublishedPort(portConfigs))
|
sort.Sort(byPublishedPort(portConfigs))
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue