diff --git a/config/convert.go b/config/convert.go index 9cb8c5b10..dacee03b5 100644 --- a/config/convert.go +++ b/config/convert.go @@ -1,6 +1,9 @@ package config -import "github.com/docker/libcompose/utils" +import ( + "github.com/docker/libcompose/utils" + "github.com/docker/libcompose/yaml" +) // ConvertServices converts a set of v1 service configs to v2 service configs func ConvertServices(v1Services map[string]*ServiceConfigV1) (map[string]*ServiceConfig, error) { @@ -9,7 +12,7 @@ func ConvertServices(v1Services map[string]*ServiceConfigV1) (map[string]*Servic for name, service := range v1Services { replacementFields[name] = &ServiceConfig{ - Build: Build{ + Build: yaml.Build{ Context: service.Build, Dockerfile: service.Dockerfile, }, diff --git a/config/convert_test.go b/config/convert_test.go index 565e66688..ddd563ed0 100644 --- a/config/convert_test.go +++ b/config/convert_test.go @@ -3,6 +3,8 @@ package config import ( "reflect" "testing" + + "github.com/docker/libcompose/yaml" ) func TestBuild(t *testing.T) { @@ -18,7 +20,7 @@ func TestBuild(t *testing.T) { v2Config := v2Services["test"] - expectedBuild := Build{ + expectedBuild := yaml.Build{ Context: ".", Dockerfile: "Dockerfile", } diff --git a/config/merge_fixtures_test.go b/config/merge_fixtures_test.go new file mode 100644 index 000000000..3910da9a6 --- /dev/null +++ b/config/merge_fixtures_test.go @@ -0,0 +1,27 @@ +package config + +import ( + "io/ioutil" + "path/filepath" + "testing" +) + +func TestMergeOnValidFixtures(t *testing.T) { + files, err := ioutil.ReadDir("testdata/") + if err != nil { + t.Fatal(err) + } + for _, file := range files { + if file.IsDir() { + continue + } + data, err := ioutil.ReadFile(filepath.Join("testdata", file.Name())) + if err != nil { + t.Fatalf("error reading %q: %v", file.Name(), err) + } + _, _, _, _, err = Merge(NewServiceConfigs(), nil, nil, file.Name(), data, nil) + if err != nil { + t.Errorf("error loading %q: %v\n %v", file.Name(), string(data), err) + } + } +} diff --git a/config/merge_test.go b/config/merge_test.go index 32450e126..ba324d8e0 100644 --- a/config/merge_test.go +++ b/config/merge_test.go @@ -1,6 +1,8 @@ package config -import "testing" +import ( + "testing" +) type NullLookup struct { } diff --git a/config/merge_v2.go b/config/merge_v2.go index a2fe4daa2..919bd8eab 100644 --- a/config/merge_v2.go +++ b/config/merge_v2.go @@ -186,7 +186,14 @@ func resolveContextV2(inFile string, serviceData RawService) RawService { if _, ok := serviceData["build"]; !ok { return serviceData } - build := serviceData["build"].(map[interface{}]interface{}) + var build map[interface{}]interface{} + if buildAsString, ok := serviceData["build"].(string); ok { + build = map[interface{}]interface{}{ + "context": buildAsString, + } + } else { + build = serviceData["build"].(map[interface{}]interface{}) + } context := asString(build["context"]) if context == "" { return serviceData diff --git a/config/testdata/build-image.v2.yml b/config/testdata/build-image.v2.yml new file mode 100644 index 000000000..9a8e2ace5 --- /dev/null +++ b/config/testdata/build-image.v2.yml @@ -0,0 +1,6 @@ +version: "2" + +services: + simple: + build: . + image: myimage diff --git a/config/testdata/build.v1.yml b/config/testdata/build.v1.yml new file mode 100644 index 000000000..10b1a9b71 --- /dev/null +++ b/config/testdata/build.v1.yml @@ -0,0 +1,5 @@ +simple1: + build: . +simple2: + build: . + dockerfile: alternate diff --git a/config/testdata/build.v2.yml b/config/testdata/build.v2.yml new file mode 100644 index 000000000..0d5e18581 --- /dev/null +++ b/config/testdata/build.v2.yml @@ -0,0 +1,18 @@ +version: "2" + +services: + simple1: + build: . + simple2: + context: ./dir + simple3: + context: ./another + dockerfile: alternate + args: + buildno: 1 + user: vincent + simple4: + context: ./another + args: + buildno: 2 + user: josh diff --git a/config/testdata/command.v1.yml b/config/testdata/command.v1.yml new file mode 100644 index 000000000..f4ed2ae24 --- /dev/null +++ b/config/testdata/command.v1.yml @@ -0,0 +1,6 @@ +simple1: + image: myimage + command: bundle exec thi-p 3000 +simple2: + image: myimage + command: [bundle, exec, thin, -p, "3000"] diff --git a/config/testdata/depends-on.v2.yml b/config/testdata/depends-on.v2.yml new file mode 100644 index 000000000..d0e76ef08 --- /dev/null +++ b/config/testdata/depends-on.v2.yml @@ -0,0 +1,11 @@ +version: '2' +services: + web: + build: . + depends_on: + - db + - redis + redis: + image: redis + db: + image: postgres diff --git a/config/testdata/dns.v1.yml b/config/testdata/dns.v1.yml new file mode 100644 index 000000000..b29cf1e01 --- /dev/null +++ b/config/testdata/dns.v1.yml @@ -0,0 +1,6 @@ +simple: + build: . + dns: 8.8.8.8 + dns: + - 8.8.8.8 + - 9.9.9.9 diff --git a/config/testdata/entrypoint.v1.yml b/config/testdata/entrypoint.v1.yml new file mode 100644 index 000000000..0f23ce6f4 --- /dev/null +++ b/config/testdata/entrypoint.v1.yml @@ -0,0 +1,12 @@ +simple1: + build: . + entrypoint: + - php + - -d + - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so + - -d + - memory_limit=-1 + - vendor/bin/phpunit +simple2: + build: . + entrypoint: /code/entrypoint.sh diff --git a/config/testdata/entrypoint.v2.yml b/config/testdata/entrypoint.v2.yml new file mode 100644 index 000000000..7764cd750 --- /dev/null +++ b/config/testdata/entrypoint.v2.yml @@ -0,0 +1,14 @@ +version: "2" +services: + simple1: + build: . + entrypoint: + - php + - -d + - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so + - -d + - memory_limit=-1 + - vendor/bin/phpunit + simple2: + build: . + entrypoint: /code/entrypoint.sh diff --git a/config/testdata/logging.v1.yml b/config/testdata/logging.v1.yml new file mode 100644 index 000000000..ebcc95967 --- /dev/null +++ b/config/testdata/logging.v1.yml @@ -0,0 +1,8 @@ +simple1: + image: myimage + log_driver: syslog + log_opt: + syslog-address: "tcp://192.168.0.42:123" +simple2: + image: myimage + log_driver: "none" diff --git a/config/testdata/logging.v2.yml b/config/testdata/logging.v2.yml new file mode 100644 index 000000000..067405f5e --- /dev/null +++ b/config/testdata/logging.v2.yml @@ -0,0 +1,12 @@ +version: "2" +services: + simple1: + image: myimage + logging: + driver: syslog + options: + syslog-address: "tcp://192.168.0.42:123" + simple2: + image: myimage + logging: + driver: "none" diff --git a/config/testdata/network-mode.v2.yml b/config/testdata/network-mode.v2.yml new file mode 100644 index 000000000..11f692f08 --- /dev/null +++ b/config/testdata/network-mode.v2.yml @@ -0,0 +1,17 @@ +version: "2" +services: + simple1: + image: myimage + network_mode: bridge + simple2: + image: myimage + network_mode: "service:bridge" + simple3: + image: myimage + network_mode: "container:test_container" + simple4: + image: myimage + network_mode: host + simple5: + image: myimage + network_mode: none diff --git a/config/testdata/networks-definition.v2.yml b/config/testdata/networks-definition.v2.yml new file mode 100644 index 000000000..eb39bb9b5 --- /dev/null +++ b/config/testdata/networks-definition.v2.yml @@ -0,0 +1,21 @@ +version: "2" +networks: + default: + driver: custom-driver + network1: + driver: bridge + driver_opts: + com.docker.network.enable_ipv6: "true" + ipam: + driver: default + config: + - subnet: 172.16.238.0/24 + gateway: 172.16.238.1 + - subnet: 2001:3984:3989::/64 + gateway: 2001:3984:3989::1 + network2: + external: true + network3: + external: + name: name-of-network + network3: {} diff --git a/config/testdata/networks.v2.yml b/config/testdata/networks.v2.yml new file mode 100644 index 000000000..acd32e82e --- /dev/null +++ b/config/testdata/networks.v2.yml @@ -0,0 +1,29 @@ +version: "2" +services: + simple1: + image: myimage + networks: + - network1 + - network2 + simple2: + image: myimage + networks: + network1: + aliases: + - alias1 + - alias3 + network2: + aliases: + - alias2 + simple3: + image: myimage + networks: + network1: + ipv4_address: 172.16.238.10 + ipv6_address: 2001:3984:3989::10 + simple4: + image: myimage + networks: ["network1"] + simple5: + image: myimage + networks: ["network1", "network2"] diff --git a/config/testdata/ulimits.v1.yml b/config/testdata/ulimits.v1.yml new file mode 100644 index 000000000..c4505d3da --- /dev/null +++ b/config/testdata/ulimits.v1.yml @@ -0,0 +1,7 @@ +simple1: + image: myimage + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 diff --git a/config/testdata/ulimits.v2.yml b/config/testdata/ulimits.v2.yml new file mode 100644 index 000000000..41991f2e7 --- /dev/null +++ b/config/testdata/ulimits.v2.yml @@ -0,0 +1,9 @@ +version: "2" +services: + simple1: + image: myimage + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 diff --git a/config/testdata/volumes-definition.v2.yml b/config/testdata/volumes-definition.v2.yml new file mode 100644 index 000000000..b1d78ad1b --- /dev/null +++ b/config/testdata/volumes-definition.v2.yml @@ -0,0 +1,14 @@ +version: "2" +volumes: + volume1: + driver: foo + volume2: + driver: bar + driver_opts: + foo: "bar" + baz: "" + volume3: + external: true + volume4: + external: + name: name-of-volume diff --git a/config/testdata/volumes.v1.yml b/config/testdata/volumes.v1.yml new file mode 100644 index 000000000..b45875a1a --- /dev/null +++ b/config/testdata/volumes.v1.yml @@ -0,0 +1,14 @@ +simple1: + image: myimage + volumes: + - /var/lib/mysql + - /opt/data:/var/lib/mysql + - ./cache:/tmp/cache + - ~configs:/etc/configs/:ro + - datavolume:/var/lib/mysql + volume_driver: mydriver + volumes_from: + - service_name + - service_name:ro + - container_name + - container_name:rw diff --git a/config/testdata/volumes.v2.yml b/config/testdata/volumes.v2.yml new file mode 100644 index 000000000..c40c921f7 --- /dev/null +++ b/config/testdata/volumes.v2.yml @@ -0,0 +1,11 @@ +version: "2" +services: + simple1: + image: myimage + volumes: + - /var/lib/mysql + - /opt/data:/var/lib/mysql + - ./cache:/tmp/cache + - ~configs:/etc/configs/:ro + - datavolume:/var/lib/mysql + volume_driver: mydriver diff --git a/config/types.go b/config/types.go index c8a305d31..ecb2d736e 100644 --- a/config/types.go +++ b/config/types.go @@ -68,13 +68,6 @@ type ServiceConfigV1 struct { Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"` } -// Build holds v2 build information -type Build struct { - Context string `yaml:"context,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty"` - Args yaml.MaporEqualSlice `yaml:"args,omitempty"` -} - // Log holds v2 logging information type Log struct { Driver string `yaml:"driver,omitempty"` @@ -83,14 +76,14 @@ type Log struct { // ServiceConfig holds version 2 of libcompose service configuration type ServiceConfig struct { - Build Build `yaml:"build,omitempty"` + Build yaml.Build `yaml:"build,omitempty"` CapAdd []string `yaml:"cap_add,omitempty"` CapDrop []string `yaml:"cap_drop,omitempty"` CPUSet string `yaml:"cpuset,omitempty"` CPUShares int64 `yaml:"cpu_shares,omitempty"` CPUQuota int64 `yaml:"cpu_quota,omitempty"` Command yaml.Command `yaml:"command,flow,omitempty"` - CgroupParent string `yaml:"cgroup_parrent,omitempty"` + CgroupParent string `yaml:"cgroup_parent,omitempty"` ContainerName string `yaml:"container_name,omitempty"` Devices []string `yaml:"devices,omitempty"` DependsOn []string `yaml:"depends_on,omitempty"` @@ -114,7 +107,7 @@ type ServiceConfig struct { MemLimit int64 `yaml:"mem_limit,omitempty"` MemSwapLimit int64 `yaml:"memswap_limit,omitempty"` NetworkMode string `yaml:"network_mode,omitempty"` - Networks []string `yaml:"networks,omitempty"` + Networks yaml.Networks `yaml:"networks,omitempty"` Pid string `yaml:"pid,omitempty"` Ports []string `yaml:"ports,omitempty"` Privileged bool `yaml:"privileged,omitempty"` @@ -137,20 +130,28 @@ type ServiceConfig struct { type VolumeConfig struct { Driver string `yaml:"driver,omitempty"` DriverOpts map[string]string `yaml:"driver_opts,omitempty"` - External bool `yaml:"external,omitempty"` + External yaml.External `yaml:"external,omitempty"` } // Ipam holds v2 network IPAM information type Ipam struct { - Driver string `yaml:"driver,omitempty"` - Config []string `yaml:"config,omitempty"` + Driver string `yaml:"driver,omitempty"` + Config []IpamConfig `yaml:"config,omitempty"` +} + +// IpamConfig holds v2 network IPAM configuration information +type IpamConfig struct { + Subnet string `yaml:"subnet,omitempty"` + IPRange string `yaml:"ip_range,omitempty"` + Gateway string `yaml:"gateway,omitempty"` + AuxAddress map[string]string `yaml:"aux_addresses,omitempty"` } // NetworkConfig holds v2 network configuration type NetworkConfig struct { Driver string `yaml:"driver,omitempty"` DriverOpts map[string]string `yaml:"driver_opts,omitempty"` - External bool `yaml:"external,omitempty"` + External yaml.External `yaml:"external,omitempty"` Ipam Ipam `yaml:"ipam,omitempty"` } diff --git a/docker/service.go b/docker/service.go index ab521e7db..cbd140c9f 100644 --- a/docker/service.go +++ b/docker/service.go @@ -170,7 +170,7 @@ func (s *Service) build(ctx context.Context, buildOptions options.Build) error { Client: s.clientFactory.Create(s), ContextDirectory: s.Config().Build.Context, Dockerfile: s.Config().Build.Dockerfile, - BuildArgs: s.Config().Build.Args.ToMap(), + BuildArgs: s.Config().Build.Args, AuthConfigs: s.authLookup.All(), NoCache: buildOptions.NoCache, ForceRemove: buildOptions.ForceRemove, diff --git a/yaml/build.go b/yaml/build.go new file mode 100644 index 000000000..802ba917b --- /dev/null +++ b/yaml/build.go @@ -0,0 +1,101 @@ +package yaml + +import ( + "fmt" + "strconv" +) + +// Build represents a build element in compose file. +// It can take multiple form in the compose file, hence this special type +type Build struct { + Context string + Dockerfile string + Args map[string]string +} + +// MarshalYAML implements the Marshaller interface. +func (b Build) MarshalYAML() (tag string, value interface{}, err error) { + m := map[string]interface{}{} + if b.Context != "" { + m["context"] = b.Context + } + if b.Dockerfile != "" { + m["dockerfile"] = b.Dockerfile + } + if len(b.Args) > 0 { + m["args"] = b.Args + } + return "", m, nil +} + +// UnmarshalYAML implements the Unmarshaller interface. +func (b *Build) UnmarshalYAML(tag string, value interface{}) error { + switch v := value.(type) { + case string: + b.Context = v + case map[interface{}]interface{}: + for mapKey, mapValue := range v { + switch mapKey { + case "context": + b.Context = mapValue.(string) + case "dockerfile": + b.Dockerfile = mapValue.(string) + case "args": + args, err := handleBuildArgs(mapValue) + if err != nil { + return err + } + b.Args = args + default: + // Ignore unknown keys + continue + } + } + default: + return fmt.Errorf("Failed to unmarshal Build: %#v", value) + } + return nil +} + +func handleBuildArgs(value interface{}) (map[string]string, error) { + var args map[string]string + switch v := value.(type) { + case map[interface{}]interface{}: + return handleBuildArgMap(v) + case []interface{}: + args = map[string]string{} + for _, elt := range v { + uniqArgs, err := handleBuildArgs(elt) + if err != nil { + return args, nil + } + for mapKey, mapValue := range uniqArgs { + args[mapKey] = mapValue + } + } + return args, nil + default: + return args, fmt.Errorf("Failed to unmarshal Build args: %#v", value) + } +} + +func handleBuildArgMap(m map[interface{}]interface{}) (map[string]string, error) { + args := map[string]string{} + for mapKey, mapValue := range m { + var argValue string + name, ok := mapKey.(string) + if !ok { + return args, fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + switch a := mapValue.(type) { + case string: + argValue = a + case int64: + argValue = strconv.Itoa(int(a)) + default: + return args, fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", mapValue, mapValue) + } + args[name] = argValue + } + return args, nil +} diff --git a/yaml/build_test.go b/yaml/build_test.go new file mode 100644 index 000000000..622ac5549 --- /dev/null +++ b/yaml/build_test.go @@ -0,0 +1,120 @@ +package yaml + +import ( + "testing" + + yaml "github.com/cloudfoundry-incubator/candiedyaml" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalBuild(t *testing.T) { + builds := []struct { + build Build + expected string + }{ + { + expected: `{} +`, + }, + { + build: Build{ + Context: ".", + }, + expected: `context: . +`, + }, + { + build: Build{ + Context: ".", + Dockerfile: "alternate", + }, + expected: `context: . +dockerfile: alternate +`, + }, + { + build: Build{ + Context: ".", + Dockerfile: "alternate", + Args: map[string]string{ + "buildno": "1", + "user": "vincent", + }, + }, + expected: `args: + buildno: "1" + user: vincent +context: . +dockerfile: alternate +`, + }, + } + for _, build := range builds { + bytes, err := yaml.Marshal(build.build) + assert.Nil(t, err) + assert.Equal(t, build.expected, string(bytes), "should be equal") + } +} + +func TestUnmarshalBuild(t *testing.T) { + builds := []struct { + yaml string + expected *Build + }{ + { + yaml: `.`, + expected: &Build{ + Context: ".", + }, + }, + { + yaml: `context: .`, + expected: &Build{ + Context: ".", + }, + }, + { + yaml: `context: . +dockerfile: alternate`, + expected: &Build{ + Context: ".", + Dockerfile: "alternate", + }, + }, + { + yaml: `context: . +dockerfile: alternate +args: + buildno: 1 + user: vincent`, + expected: &Build{ + Context: ".", + Dockerfile: "alternate", + Args: map[string]string{ + "buildno": "1", + "user": "vincent", + }, + }, + }, + { + yaml: `context: . +args: + - buildno: 1 + - user: vincent`, + expected: &Build{ + Context: ".", + Args: map[string]string{ + "buildno": "1", + "user": "vincent", + }, + }, + }, + } + for _, build := range builds { + actual := &Build{} + err := yaml.Unmarshal([]byte(build.yaml), actual) + assert.Nil(t, err) + assert.Equal(t, build.expected, actual, "should be equal") + } +} diff --git a/yaml/command.go b/yaml/command.go new file mode 100644 index 000000000..1f855a32c --- /dev/null +++ b/yaml/command.go @@ -0,0 +1,32 @@ +package yaml + +import ( + "fmt" + + "github.com/docker/engine-api/types/strslice" + "github.com/flynn/go-shlex" +) + +// Command represents a docker command, can be a string or an array of strings. +type Command strslice.StrSlice + +// UnmarshalYAML implements the Unmarshaller interface. +func (s *Command) UnmarshalYAML(tag string, value interface{}) error { + switch value := value.(type) { + case []interface{}: + parts, err := toStrings(value) + if err != nil { + return err + } + *s = parts + case string: + parts, err := shlex.Split(value) + if err != nil { + return err + } + *s = parts + default: + return fmt.Errorf("Failed to unmarshal Command: %#v", value) + } + return nil +} diff --git a/yaml/command_test.go b/yaml/command_test.go new file mode 100644 index 000000000..a414b6ca5 --- /dev/null +++ b/yaml/command_test.go @@ -0,0 +1,55 @@ +package yaml + +import ( + "strings" + "testing" + + yaml "github.com/cloudfoundry-incubator/candiedyaml" + + "github.com/stretchr/testify/assert" +) + +type StructCommand struct { + Entrypoint Command `yaml:"entrypoint,flow,omitempty"` + Command Command `yaml:"command,flow,omitempty"` +} + +var sampleStructCommand = `command: bash` + +func TestUnmarshalCommand(t *testing.T) { + s := &StructCommand{} + err := yaml.Unmarshal([]byte(sampleStructCommand), s) + + assert.Nil(t, err) + assert.Equal(t, Command{"bash"}, s.Command) + assert.Nil(t, s.Entrypoint) + bytes, err := yaml.Marshal(s) + assert.Nil(t, err) + + s2 := &StructCommand{} + err = yaml.Unmarshal(bytes, s2) + + assert.Nil(t, err) + assert.Equal(t, Command{"bash"}, s2.Command) + assert.Nil(t, s2.Entrypoint) +} + +var sampleEmptyCommand = `{}` + +func TestUnmarshalEmptyCommand(t *testing.T) { + s := &StructCommand{} + err := yaml.Unmarshal([]byte(sampleEmptyCommand), s) + + assert.Nil(t, err) + assert.Nil(t, s.Command) + + bytes, err := yaml.Marshal(s) + assert.Nil(t, err) + assert.Equal(t, "{}", strings.TrimSpace(string(bytes))) + + s2 := &StructCommand{} + err = yaml.Unmarshal(bytes, s2) + + assert.Nil(t, err) + assert.Nil(t, s2.Command) +} diff --git a/yaml/external.go b/yaml/external.go new file mode 100644 index 000000000..0b5b3d6d0 --- /dev/null +++ b/yaml/external.go @@ -0,0 +1,44 @@ +package yaml + +import ( + "fmt" +) + +// External represents an external network entry in compose file. +// It can be a boolean (true|false) or have a name +type External struct { + External bool + Name string +} + +// MarshalYAML implements the Marshaller interface. +func (n External) MarshalYAML() (tag string, value interface{}, err error) { + if n.Name == "" { + return "", n.External, nil + } + return "", map[string]interface{}{ + "name": n.Name, + }, nil +} + +// UnmarshalYAML implements the Unmarshaller interface. +func (n *External) UnmarshalYAML(tag string, value interface{}) error { + switch v := value.(type) { + case bool: + n.External = v + case map[interface{}]interface{}: + for mapKey, mapValue := range v { + switch mapKey { + case "name": + n.Name = mapValue.(string) + default: + // Ignore unknown keys + continue + } + } + n.External = true + default: + return fmt.Errorf("Failed to unmarshal External: %#v", value) + } + return nil +} diff --git a/yaml/external_test.go b/yaml/external_test.go new file mode 100644 index 000000000..a6ea06b1e --- /dev/null +++ b/yaml/external_test.go @@ -0,0 +1,83 @@ +package yaml + +import ( + "testing" + + yaml "github.com/cloudfoundry-incubator/candiedyaml" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalExternal(t *testing.T) { + externals := []struct { + external External + expected string + }{ + { + external: External{}, + expected: `false +`, + }, + { + external: External{ + External: false, + }, + expected: `false +`, + }, + { + external: External{ + External: true, + }, + expected: `true +`, + }, + { + external: External{ + External: true, + Name: "network-name", + }, + expected: `name: network-name +`, + }, + } + for _, e := range externals { + bytes, err := yaml.Marshal(e.external) + assert.Nil(t, err) + assert.Equal(t, e.expected, string(bytes), "should be equal") + } +} + +func TestUnmarshalExternal(t *testing.T) { + externals := []struct { + yaml string + expected *External + }{ + { + yaml: `true`, + expected: &External{ + External: true, + }, + }, + { + yaml: `false`, + expected: &External{ + External: false, + }, + }, + { + yaml: ` +name: name-of-network`, + expected: &External{ + External: true, + Name: "name-of-network", + }, + }, + } + for _, e := range externals { + actual := &External{} + err := yaml.Unmarshal([]byte(e.yaml), actual) + assert.Nil(t, err) + assert.Equal(t, e.expected, actual, "should be equal") + } +} diff --git a/yaml/network.go b/yaml/network.go new file mode 100644 index 000000000..f5f655481 --- /dev/null +++ b/yaml/network.go @@ -0,0 +1,97 @@ +package yaml + +import ( + "fmt" +) + +// Networks represents a list of service networks in compose file. +// It has several representation, hence this specific struct. +type Networks struct { + Networks []Network +} + +// Network represents a service network in compose file. +type Network struct { + Name string `yaml:"-"` + Aliases []string `yaml:"aliases,omitempty"` + IPv4Address string `yaml:"ipv4_address,omitempty"` + IPv6Address string `yaml:"ipv6_address,omitempty"` +} + +// MarshalYAML implements the Marshaller interface. +func (n Networks) MarshalYAML() (tag string, value interface{}, err error) { + m := map[string]Network{} + for _, network := range n.Networks { + m[network.Name] = network + } + return "", m, nil +} + +// UnmarshalYAML implements the Unmarshaller interface. +func (n *Networks) UnmarshalYAML(tag string, value interface{}) error { + switch v := value.(type) { + case []interface{}: + n.Networks = []Network{} + for _, network := range v { + name, ok := network.(string) + if !ok { + return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + n.Networks = append(n.Networks, Network{ + Name: name, + }) + } + case map[interface{}]interface{}: + n.Networks = []Network{} + for mapKey, mapValue := range v { + name, ok := mapKey.(string) + if !ok { + return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + network, err := handleNetwork(name, mapValue) + if err != nil { + return err + } + n.Networks = append(n.Networks, network) + } + default: + return fmt.Errorf("Failed to unmarshal Networks: %#v", value) + } + return nil +} + +func handleNetwork(name string, value interface{}) (Network, error) { + switch v := value.(type) { + case map[interface{}]interface{}: + network := Network{ + Name: name, + } + for mapKey, mapValue := range v { + name, ok := mapKey.(string) + if !ok { + return Network{}, fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + switch name { + case "aliases": + aliases, ok := mapValue.([]interface{}) + if !ok { + return Network{}, fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", aliases, aliases) + } + network.Aliases = []string{} + for _, alias := range aliases { + network.Aliases = append(network.Aliases, alias.(string)) + } + case "ipv4_address": + network.IPv4Address = mapValue.(string) + case "ipv6_address": + network.IPv6Address = mapValue.(string) + default: + // Ignorer unknown keys ? + continue + } + } + return network, nil + default: + return Network{}, fmt.Errorf("Failed to unmarshal Network: %#v", value) + } +} diff --git a/yaml/network_test.go b/yaml/network_test.go new file mode 100644 index 000000000..b0d4c519e --- /dev/null +++ b/yaml/network_test.go @@ -0,0 +1,154 @@ +package yaml + +import ( + "testing" + + yaml "github.com/cloudfoundry-incubator/candiedyaml" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalNetworks(t *testing.T) { + networks := []struct { + networks Networks + expected string + }{ + { + networks: Networks{}, + expected: `{} +`, + }, + { + networks: Networks{ + Networks: []Network{ + { + Name: "network1", + }, + { + Name: "network2", + }, + }, + }, + expected: `network1: {} +network2: {} +`, + }, + { + networks: Networks{ + Networks: []Network{ + { + Name: "network1", + Aliases: []string{"alias1", "alias2"}, + }, + { + Name: "network2", + }, + }, + }, + expected: `network1: + aliases: + - alias1 + - alias2 +network2: {} +`, + }, + { + networks: Networks{ + Networks: []Network{ + { + Name: "network1", + Aliases: []string{"alias1", "alias2"}, + }, + { + Name: "network2", + IPv4Address: "172.16.238.10", + IPv6Address: "2001:3984:3989::10", + }, + }, + }, + expected: `network1: + aliases: + - alias1 + - alias2 +network2: + ipv4_address: 172.16.238.10 + ipv6_address: 2001:3984:3989::10 +`, + }, + } + for _, network := range networks { + bytes, err := yaml.Marshal(network.networks) + assert.Nil(t, err) + assert.Equal(t, network.expected, string(bytes), "should be equal") + } +} + +func TestUnmarshalNetworks(t *testing.T) { + networks := []struct { + yaml string + expected *Networks + }{ + { + yaml: `- network1 +- network2`, + expected: &Networks{ + Networks: []Network{ + { + Name: "network1", + }, + { + Name: "network2", + }, + }, + }, + }, + { + yaml: `network1: {}`, + expected: &Networks{ + Networks: []Network{ + { + Name: "network1", + }, + }, + }, + }, + { + yaml: `network1: + aliases: + - alias1 + - alias2`, + expected: &Networks{ + Networks: []Network{ + { + Name: "network1", + Aliases: []string{"alias1", "alias2"}, + }, + }, + }, + }, + { + yaml: `network1: + aliases: + - alias1 + - alias2 + ipv4_address: 172.16.238.10 + ipv6_address: 2001:3984:3989::10`, + expected: &Networks{ + Networks: []Network{ + { + Name: "network1", + Aliases: []string{"alias1", "alias2"}, + IPv4Address: "172.16.238.10", + IPv6Address: "2001:3984:3989::10", + }, + }, + }, + }, + } + for _, network := range networks { + actual := &Networks{} + err := yaml.Unmarshal([]byte(network.yaml), actual) + assert.Nil(t, err) + assert.Equal(t, network.expected, actual, "should be equal") + } +} diff --git a/yaml/types_yaml.go b/yaml/types_yaml.go index 3c9373568..a8add0244 100644 --- a/yaml/types_yaml.go +++ b/yaml/types_yaml.go @@ -2,13 +2,10 @@ package yaml import ( "fmt" - "reflect" - "sort" "strconv" "strings" "github.com/docker/engine-api/types/strslice" - "github.com/flynn/go-shlex" ) // Stringorslice represents a string or an array of strings. @@ -32,128 +29,6 @@ func (s *Stringorslice) UnmarshalYAML(tag string, value interface{}) error { return nil } -// Ulimits represents a list of Ulimit. -// It is, however, represented in yaml as keys (and thus map in Go) -type Ulimits struct { - Elements []Ulimit -} - -// MarshalYAML implements the Marshaller interface. -func (u Ulimits) MarshalYAML() (tag string, value interface{}, err error) { - ulimitMap := make(map[string]Ulimit) - for _, ulimit := range u.Elements { - ulimitMap[ulimit.Name] = ulimit - } - return "", ulimitMap, nil -} - -// UnmarshalYAML implements the Unmarshaller interface. -func (u *Ulimits) UnmarshalYAML(tag string, value interface{}) error { - ulimits := make(map[string]Ulimit) - yamlUlimits := reflect.ValueOf(value) - switch yamlUlimits.Kind() { - case reflect.Map: - for _, key := range yamlUlimits.MapKeys() { - var name string - var soft, hard int64 - mapValue := yamlUlimits.MapIndex(key).Elem() - name = key.Elem().String() - switch mapValue.Kind() { - case reflect.Int64: - soft = mapValue.Int() - hard = mapValue.Int() - case reflect.Map: - if len(mapValue.MapKeys()) != 2 { - return fmt.Errorf("Failed to unmarshal Ulimit: %#v", mapValue) - } - for _, subKey := range mapValue.MapKeys() { - subValue := mapValue.MapIndex(subKey).Elem() - switch subKey.Elem().String() { - case "soft": - soft = subValue.Int() - case "hard": - hard = subValue.Int() - } - } - default: - return fmt.Errorf("Failed to unmarshal Ulimit: %#v, %v", mapValue, mapValue.Kind()) - } - ulimits[name] = Ulimit{ - Name: name, - ulimitValues: ulimitValues{ - Soft: soft, - Hard: hard, - }, - } - } - keys := make([]string, 0, len(ulimits)) - for key := range ulimits { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - u.Elements = append(u.Elements, ulimits[key]) - } - default: - return fmt.Errorf("Failed to unmarshal Ulimit: %#v", value) - } - return nil -} - -// Ulimit represents ulimit information. -type Ulimit struct { - ulimitValues - Name string -} - -type ulimitValues struct { - Soft int64 `yaml:"soft"` - Hard int64 `yaml:"hard"` -} - -// MarshalYAML implements the Marshaller interface. -func (u Ulimit) MarshalYAML() (tag string, value interface{}, err error) { - if u.Soft == u.Hard { - return "", u.Soft, nil - } - return "", u.ulimitValues, err -} - -// NewUlimit creates a Ulimit based on the specified parts. -func NewUlimit(name string, soft int64, hard int64) Ulimit { - return Ulimit{ - Name: name, - ulimitValues: ulimitValues{ - Soft: soft, - Hard: hard, - }, - } -} - -// Command represents a docker command, can be a string or an array of strings. -type Command strslice.StrSlice - -// UnmarshalYAML implements the Unmarshaller interface. -func (s *Command) UnmarshalYAML(tag string, value interface{}) error { - switch value := value.(type) { - case []interface{}: - parts, err := toStrings(value) - if err != nil { - return err - } - *s = parts - case string: - parts, err := shlex.Split(value) - if err != nil { - return err - } - *s = parts - default: - return fmt.Errorf("Failed to unmarshal Command: %#v", value) - } - return nil -} - // SliceorMap represents a slice or a map of strings. type SliceorMap map[string]string diff --git a/yaml/types_yaml_test.go b/yaml/types_yaml_test.go index 439790fe8..e1fa8dfd9 100644 --- a/yaml/types_yaml_test.go +++ b/yaml/types_yaml_test.go @@ -2,7 +2,6 @@ package yaml import ( "fmt" - "strings" "testing" yaml "github.com/cloudfoundry-incubator/candiedyaml" @@ -36,11 +35,6 @@ type StructSliceorMap struct { Bars []string `yaml:"bars"` } -type StructCommand struct { - Entrypoint Command `yaml:"entrypoint,flow,omitempty"` - Command Command `yaml:"command,flow,omitempty"` -} - func TestSliceOrMapYaml(t *testing.T) { str := `{foos: [bar=baz, far=faz]}` @@ -118,161 +112,3 @@ func TestMaporsliceYaml(t *testing.T) { assert.True(t, contains(s2.Foo, "bar=baz")) assert.True(t, contains(s2.Foo, "far=1")) } - -var sampleStructCommand = `command: bash` - -func TestUnmarshalCommand(t *testing.T) { - s := &StructCommand{} - err := yaml.Unmarshal([]byte(sampleStructCommand), s) - - assert.Nil(t, err) - assert.Equal(t, Command{"bash"}, s.Command) - assert.Nil(t, s.Entrypoint) - bytes, err := yaml.Marshal(s) - assert.Nil(t, err) - - s2 := &StructCommand{} - err = yaml.Unmarshal(bytes, s2) - - assert.Nil(t, err) - assert.Equal(t, Command{"bash"}, s2.Command) - assert.Nil(t, s2.Entrypoint) -} - -var sampleEmptyCommand = `{}` - -func TestUnmarshalEmptyCommand(t *testing.T) { - s := &StructCommand{} - err := yaml.Unmarshal([]byte(sampleEmptyCommand), s) - - assert.Nil(t, err) - assert.Nil(t, s.Command) - - bytes, err := yaml.Marshal(s) - assert.Nil(t, err) - assert.Equal(t, "{}", strings.TrimSpace(string(bytes))) - - s2 := &StructCommand{} - err = yaml.Unmarshal(bytes, s2) - - assert.Nil(t, err) - assert.Nil(t, s2.Command) -} - -func TestMarshalUlimit(t *testing.T) { - ulimits := []struct { - ulimits *Ulimits - expected string - }{ - { - ulimits: &Ulimits{ - Elements: []Ulimit{ - { - ulimitValues: ulimitValues{ - Soft: 65535, - Hard: 65535, - }, - Name: "nproc", - }, - }, - }, - expected: `nproc: 65535 -`, - }, - { - ulimits: &Ulimits{ - Elements: []Ulimit{ - { - Name: "nofile", - ulimitValues: ulimitValues{ - Soft: 20000, - Hard: 40000, - }, - }, - }, - }, - expected: `nofile: - soft: 20000 - hard: 40000 -`, - }, - } - - for _, ulimit := range ulimits { - - bytes, err := yaml.Marshal(ulimit.ulimits) - - assert.Nil(t, err) - assert.Equal(t, ulimit.expected, string(bytes), "should be equal") - } -} - -func TestUnmarshalUlimits(t *testing.T) { - ulimits := []struct { - yaml string - expected *Ulimits - }{ - { - yaml: "nproc: 65535", - expected: &Ulimits{ - Elements: []Ulimit{ - { - Name: "nproc", - ulimitValues: ulimitValues{ - Soft: 65535, - Hard: 65535, - }, - }, - }, - }, - }, - { - yaml: `nofile: - soft: 20000 - hard: 40000`, - expected: &Ulimits{ - Elements: []Ulimit{ - { - Name: "nofile", - ulimitValues: ulimitValues{ - Soft: 20000, - Hard: 40000, - }, - }, - }, - }, - }, - { - yaml: `nproc: 65535 -nofile: - soft: 20000 - hard: 40000`, - expected: &Ulimits{ - Elements: []Ulimit{ - { - Name: "nofile", - ulimitValues: ulimitValues{ - Soft: 20000, - Hard: 40000, - }, - }, - { - Name: "nproc", - ulimitValues: ulimitValues{ - Soft: 65535, - Hard: 65535, - }, - }, - }, - }, - }, - } - - for _, ulimit := range ulimits { - actual := &Ulimits{} - err := yaml.Unmarshal([]byte(ulimit.yaml), actual) - - assert.Nil(t, err) - assert.Equal(t, ulimit.expected, actual, "should be equal") - } -} diff --git a/yaml/ulimit.go b/yaml/ulimit.go new file mode 100644 index 000000000..d4dd57701 --- /dev/null +++ b/yaml/ulimit.go @@ -0,0 +1,106 @@ +package yaml + +import ( + "fmt" + "sort" +) + +// Ulimits represents a list of Ulimit. +// It is, however, represented in yaml as keys (and thus map in Go) +type Ulimits struct { + Elements []Ulimit +} + +// MarshalYAML implements the Marshaller interface. +func (u Ulimits) MarshalYAML() (tag string, value interface{}, err error) { + ulimitMap := make(map[string]Ulimit) + for _, ulimit := range u.Elements { + ulimitMap[ulimit.Name] = ulimit + } + return "", ulimitMap, nil +} + +// UnmarshalYAML implements the Unmarshaller interface. +func (u *Ulimits) UnmarshalYAML(tag string, value interface{}) error { + ulimits := make(map[string]Ulimit) + switch v := value.(type) { + case map[interface{}]interface{}: + for mapKey, mapValue := range v { + name, ok := mapKey.(string) + if !ok { + return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + var soft, hard int64 + switch mv := mapValue.(type) { + case int64: + soft = mv + hard = mv + case map[interface{}]interface{}: + if len(mv) != 2 { + return fmt.Errorf("Failed to unmarshal Ulimit: %#v", mapValue) + } + for mkey, mvalue := range mv { + switch mkey { + case "soft": + soft = mvalue.(int64) + case "hard": + hard = mvalue.(int64) + default: + // FIXME(vdemeester) Should we ignore or fail ? + continue + } + } + default: + return fmt.Errorf("Failed to unmarshal Ulimit: %v, %T", mapValue, mapValue) + } + ulimits[name] = Ulimit{ + Name: name, + ulimitValues: ulimitValues{ + Soft: soft, + Hard: hard, + }, + } + } + keys := make([]string, 0, len(ulimits)) + for key := range ulimits { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + u.Elements = append(u.Elements, ulimits[key]) + } + default: + return fmt.Errorf("Failed to unmarshal Ulimit: %#v", value) + } + return nil +} + +// Ulimit represents ulimit information. +type Ulimit struct { + ulimitValues + Name string +} + +type ulimitValues struct { + Soft int64 `yaml:"soft"` + Hard int64 `yaml:"hard"` +} + +// MarshalYAML implements the Marshaller interface. +func (u Ulimit) MarshalYAML() (tag string, value interface{}, err error) { + if u.Soft == u.Hard { + return "", u.Soft, nil + } + return "", u.ulimitValues, err +} + +// NewUlimit creates a Ulimit based on the specified parts. +func NewUlimit(name string, soft int64, hard int64) Ulimit { + return Ulimit{ + Name: name, + ulimitValues: ulimitValues{ + Soft: soft, + Hard: hard, + }, + } +} diff --git a/yaml/ulimit_test.go b/yaml/ulimit_test.go new file mode 100644 index 000000000..ba3beb339 --- /dev/null +++ b/yaml/ulimit_test.go @@ -0,0 +1,127 @@ +package yaml + +import ( + "testing" + + yaml "github.com/cloudfoundry-incubator/candiedyaml" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalUlimit(t *testing.T) { + ulimits := []struct { + ulimits *Ulimits + expected string + }{ + { + ulimits: &Ulimits{ + Elements: []Ulimit{ + { + ulimitValues: ulimitValues{ + Soft: 65535, + Hard: 65535, + }, + Name: "nproc", + }, + }, + }, + expected: `nproc: 65535 +`, + }, + { + ulimits: &Ulimits{ + Elements: []Ulimit{ + { + Name: "nofile", + ulimitValues: ulimitValues{ + Soft: 20000, + Hard: 40000, + }, + }, + }, + }, + expected: `nofile: + soft: 20000 + hard: 40000 +`, + }, + } + + for _, ulimit := range ulimits { + + bytes, err := yaml.Marshal(ulimit.ulimits) + + assert.Nil(t, err) + assert.Equal(t, ulimit.expected, string(bytes), "should be equal") + } +} + +func TestUnmarshalUlimits(t *testing.T) { + ulimits := []struct { + yaml string + expected *Ulimits + }{ + { + yaml: "nproc: 65535", + expected: &Ulimits{ + Elements: []Ulimit{ + { + Name: "nproc", + ulimitValues: ulimitValues{ + Soft: 65535, + Hard: 65535, + }, + }, + }, + }, + }, + { + yaml: `nofile: + soft: 20000 + hard: 40000`, + expected: &Ulimits{ + Elements: []Ulimit{ + { + Name: "nofile", + ulimitValues: ulimitValues{ + Soft: 20000, + Hard: 40000, + }, + }, + }, + }, + }, + { + yaml: `nproc: 65535 +nofile: + soft: 20000 + hard: 40000`, + expected: &Ulimits{ + Elements: []Ulimit{ + { + Name: "nofile", + ulimitValues: ulimitValues{ + Soft: 20000, + Hard: 40000, + }, + }, + { + Name: "nproc", + ulimitValues: ulimitValues{ + Soft: 65535, + Hard: 65535, + }, + }, + }, + }, + }, + } + + for _, ulimit := range ulimits { + actual := &Ulimits{} + err := yaml.Unmarshal([]byte(ulimit.yaml), actual) + + assert.Nil(t, err) + assert.Equal(t, ulimit.expected, actual, "should be equal") + } +}