From f07a28a54165b6a2e49e9d0e6d3a111252095cb5 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 31 Jan 2017 12:45:45 -0800 Subject: [PATCH] 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 --- cli/compose/convert/service.go | 22 ++- cli/compose/convert/service_test.go | 34 ++++ cli/compose/loader/loader.go | 78 +++++++++- cli/compose/loader/loader_test.go | 232 +++++++++++++++++++++++++++- 4 files changed, 342 insertions(+), 24 deletions(-) diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index a8613c0878..ef6a04ebcf 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -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)) diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index 2e614d730c..64ccfd038e 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -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{} diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 2c92666c51..2ccef7198d 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -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 { diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 3a2f27204d..53f4280b64 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -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) +}