123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782 |
- package loader
- import (
- "fmt"
- "io/ioutil"
- "os"
- "sort"
- "testing"
- "time"
- "github.com/docker/docker/cli/compose/types"
- "github.com/stretchr/testify/assert"
- )
- func buildConfigDetails(source types.Dict) types.ConfigDetails {
- workingDir, err := os.Getwd()
- if err != nil {
- panic(err)
- }
- return types.ConfigDetails{
- WorkingDir: workingDir,
- ConfigFiles: []types.ConfigFile{
- {Filename: "filename.yml", Config: source},
- },
- Environment: nil,
- }
- }
- var sampleYAML = `
- version: "3"
- services:
- foo:
- image: busybox
- networks:
- with_me:
- bar:
- image: busybox
- environment:
- - FOO=1
- networks:
- - with_ipam
- volumes:
- hello:
- driver: default
- driver_opts:
- beep: boop
- networks:
- default:
- driver: bridge
- driver_opts:
- beep: boop
- with_ipam:
- ipam:
- driver: default
- config:
- - subnet: 172.28.0.0/16
- `
- var sampleDict = types.Dict{
- "version": "3",
- "services": types.Dict{
- "foo": types.Dict{
- "image": "busybox",
- "networks": types.Dict{"with_me": nil},
- },
- "bar": types.Dict{
- "image": "busybox",
- "environment": []interface{}{"FOO=1"},
- "networks": []interface{}{"with_ipam"},
- },
- },
- "volumes": types.Dict{
- "hello": types.Dict{
- "driver": "default",
- "driver_opts": types.Dict{
- "beep": "boop",
- },
- },
- },
- "networks": types.Dict{
- "default": types.Dict{
- "driver": "bridge",
- "driver_opts": types.Dict{
- "beep": "boop",
- },
- },
- "with_ipam": types.Dict{
- "ipam": types.Dict{
- "driver": "default",
- "config": []interface{}{
- types.Dict{
- "subnet": "172.28.0.0/16",
- },
- },
- },
- },
- },
- }
- var sampleConfig = types.Config{
- Services: []types.ServiceConfig{
- {
- Name: "foo",
- Image: "busybox",
- Environment: map[string]string{},
- Networks: map[string]*types.ServiceNetworkConfig{
- "with_me": nil,
- },
- },
- {
- Name: "bar",
- Image: "busybox",
- Environment: map[string]string{"FOO": "1"},
- Networks: map[string]*types.ServiceNetworkConfig{
- "with_ipam": nil,
- },
- },
- },
- Networks: map[string]types.NetworkConfig{
- "default": {
- Driver: "bridge",
- DriverOpts: map[string]string{
- "beep": "boop",
- },
- },
- "with_ipam": {
- Ipam: types.IPAMConfig{
- Driver: "default",
- Config: []*types.IPAMPool{
- {
- Subnet: "172.28.0.0/16",
- },
- },
- },
- },
- },
- Volumes: map[string]types.VolumeConfig{
- "hello": {
- Driver: "default",
- DriverOpts: map[string]string{
- "beep": "boop",
- },
- },
- },
- }
- func TestParseYAML(t *testing.T) {
- dict, err := ParseYAML([]byte(sampleYAML))
- if !assert.NoError(t, err) {
- return
- }
- assert.Equal(t, sampleDict, dict)
- }
- func TestLoad(t *testing.T) {
- actual, err := Load(buildConfigDetails(sampleDict))
- if !assert.NoError(t, err) {
- return
- }
- assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
- assert.Equal(t, sampleConfig.Networks, actual.Networks)
- assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
- }
- func TestParseAndLoad(t *testing.T) {
- actual, err := loadYAML(sampleYAML)
- if !assert.NoError(t, err) {
- return
- }
- assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
- assert.Equal(t, sampleConfig.Networks, actual.Networks)
- assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
- }
- func TestInvalidTopLevelObjectType(t *testing.T) {
- _, err := loadYAML("1")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Top-level object must be a mapping")
- _, err = loadYAML("\"hello\"")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Top-level object must be a mapping")
- _, err = loadYAML("[\"hello\"]")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Top-level object must be a mapping")
- }
- func TestNonStringKeys(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- 123:
- foo:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Non-string key at top level: 123")
- _, err = loadYAML(`
- version: "3"
- services:
- foo:
- image: busybox
- 123:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Non-string key in services: 123")
- _, err = loadYAML(`
- version: "3"
- services:
- foo:
- image: busybox
- networks:
- default:
- ipam:
- config:
- - 123: oh dear
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
- _, err = loadYAML(`
- version: "3"
- services:
- dict-env:
- image: busybox
- environment:
- 1: FOO
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
- }
- func TestSupportedVersion(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- foo:
- image: busybox
- `)
- assert.NoError(t, err)
- _, err = loadYAML(`
- version: "3.0"
- services:
- foo:
- image: busybox
- `)
- assert.NoError(t, err)
- }
- func TestUnsupportedVersion(t *testing.T) {
- _, err := loadYAML(`
- version: "2"
- services:
- foo:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "version")
- _, err = loadYAML(`
- version: "2.0"
- services:
- foo:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "version")
- }
- func TestInvalidVersion(t *testing.T) {
- _, err := loadYAML(`
- version: 3
- services:
- foo:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "version must be a string")
- }
- func TestV1Unsupported(t *testing.T) {
- _, err := loadYAML(`
- foo:
- image: busybox
- `)
- assert.Error(t, err)
- }
- func TestNonMappingObject(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- - foo:
- image: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "services must be a mapping")
- _, err = loadYAML(`
- version: "3"
- services:
- foo: busybox
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "services.foo must be a mapping")
- _, err = loadYAML(`
- version: "3"
- networks:
- - default:
- driver: bridge
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "networks must be a mapping")
- _, err = loadYAML(`
- version: "3"
- networks:
- default: bridge
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "networks.default must be a mapping")
- _, err = loadYAML(`
- version: "3"
- volumes:
- - data:
- driver: local
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "volumes must be a mapping")
- _, err = loadYAML(`
- version: "3"
- volumes:
- data: local
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "volumes.data must be a mapping")
- }
- func TestNonStringImage(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- foo:
- image: ["busybox", "latest"]
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "services.foo.image must be a string")
- }
- func TestValidEnvironment(t *testing.T) {
- config, err := loadYAML(`
- version: "3"
- services:
- dict-env:
- image: busybox
- environment:
- FOO: "1"
- BAR: 2
- BAZ: 2.5
- QUUX:
- list-env:
- image: busybox
- environment:
- - FOO=1
- - BAR=2
- - BAZ=2.5
- - QUUX=
- `)
- assert.NoError(t, err)
- expected := map[string]string{
- "FOO": "1",
- "BAR": "2",
- "BAZ": "2.5",
- "QUUX": "",
- }
- assert.Equal(t, 2, len(config.Services))
- for _, service := range config.Services {
- assert.Equal(t, expected, service.Environment)
- }
- }
- func TestInvalidEnvironmentValue(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- dict-env:
- image: busybox
- environment:
- FOO: ["1"]
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
- }
- func TestInvalidEnvironmentObject(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- dict-env:
- image: busybox
- environment: "FOO=1"
- `)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
- }
- func TestEnvironmentInterpolation(t *testing.T) {
- config, err := loadYAML(`
- version: "3"
- services:
- test:
- image: busybox
- labels:
- - home1=$HOME
- - home2=${HOME}
- - nonexistent=$NONEXISTENT
- - default=${NONEXISTENT-default}
- networks:
- test:
- driver: $HOME
- volumes:
- test:
- driver: $HOME
- `)
- assert.NoError(t, err)
- home := os.Getenv("HOME")
- expectedLabels := map[string]string{
- "home1": home,
- "home2": home,
- "nonexistent": "",
- "default": "default",
- }
- assert.Equal(t, expectedLabels, config.Services[0].Labels)
- assert.Equal(t, home, config.Networks["test"].Driver)
- assert.Equal(t, home, config.Volumes["test"].Driver)
- }
- func TestUnsupportedProperties(t *testing.T) {
- dict, err := ParseYAML([]byte(`
- version: "3"
- services:
- web:
- image: web
- build: ./web
- links:
- - bar
- db:
- image: db
- build: ./db
- `))
- assert.NoError(t, err)
- configDetails := buildConfigDetails(dict)
- _, err = Load(configDetails)
- assert.NoError(t, err)
- unsupported := GetUnsupportedProperties(configDetails)
- assert.Equal(t, []string{"build", "links"}, unsupported)
- }
- func TestDeprecatedProperties(t *testing.T) {
- dict, err := ParseYAML([]byte(`
- version: "3"
- services:
- web:
- image: web
- container_name: web
- db:
- image: db
- container_name: db
- expose: ["5434"]
- `))
- assert.NoError(t, err)
- configDetails := buildConfigDetails(dict)
- _, err = Load(configDetails)
- assert.NoError(t, err)
- deprecated := GetDeprecatedProperties(configDetails)
- assert.Equal(t, 2, len(deprecated))
- assert.Contains(t, deprecated, "container_name")
- assert.Contains(t, deprecated, "expose")
- }
- func TestForbiddenProperties(t *testing.T) {
- _, err := loadYAML(`
- version: "3"
- services:
- foo:
- image: busybox
- volumes:
- - /data
- volume_driver: some-driver
- bar:
- extends:
- service: foo
- `)
- assert.Error(t, err)
- assert.IsType(t, &ForbiddenPropertiesError{}, err)
- fmt.Println(err)
- forbidden := err.(*ForbiddenPropertiesError).Properties
- assert.Equal(t, 2, len(forbidden))
- assert.Contains(t, forbidden, "volume_driver")
- assert.Contains(t, forbidden, "extends")
- }
- func durationPtr(value time.Duration) *time.Duration {
- return &value
- }
- func int64Ptr(value int64) *int64 {
- return &value
- }
- func uint64Ptr(value uint64) *uint64 {
- return &value
- }
- func TestFullExample(t *testing.T) {
- bytes, err := ioutil.ReadFile("full-example.yml")
- assert.NoError(t, err)
- config, err := loadYAML(string(bytes))
- if !assert.NoError(t, err) {
- return
- }
- workingDir, err := os.Getwd()
- assert.NoError(t, err)
- homeDir := os.Getenv("HOME")
- stopGracePeriod := time.Duration(20 * time.Second)
- expectedServiceConfig := types.ServiceConfig{
- Name: "foo",
- CapAdd: []string{"ALL"},
- CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
- CgroupParent: "m-executor-abcd",
- Command: []string{"bundle", "exec", "thin", "-p", "3000"},
- ContainerName: "my-web-container",
- DependsOn: []string{"db", "redis"},
- Deploy: types.DeployConfig{
- Mode: "replicated",
- Replicas: uint64Ptr(6),
- Labels: map[string]string{"FOO": "BAR"},
- UpdateConfig: &types.UpdateConfig{
- Parallelism: uint64Ptr(3),
- Delay: time.Duration(10 * time.Second),
- FailureAction: "continue",
- Monitor: time.Duration(60 * time.Second),
- MaxFailureRatio: 0.3,
- },
- Resources: types.Resources{
- Limits: &types.Resource{
- NanoCPUs: "0.001",
- MemoryBytes: 50 * 1024 * 1024,
- },
- Reservations: &types.Resource{
- NanoCPUs: "0.0001",
- MemoryBytes: 20 * 1024 * 1024,
- },
- },
- RestartPolicy: &types.RestartPolicy{
- Condition: "on_failure",
- Delay: durationPtr(5 * time.Second),
- MaxAttempts: uint64Ptr(3),
- Window: durationPtr(2 * time.Minute),
- },
- Placement: types.Placement{
- Constraints: []string{"node=foo"},
- },
- },
- Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
- DNS: []string{"8.8.8.8", "9.9.9.9"},
- DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
- DomainName: "foo.com",
- Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
- Environment: map[string]string{
- "RACK_ENV": "development",
- "SHOW": "true",
- "SESSION_SECRET": "",
- "FOO": "1",
- "BAR": "2",
- "BAZ": "3",
- },
- Expose: []string{"3000", "8000"},
- ExternalLinks: []string{
- "redis_1",
- "project_db_1:mysql",
- "project_db_1:postgresql",
- },
- ExtraHosts: map[string]string{
- "otherhost": "50.31.209.229",
- "somehost": "162.242.195.82",
- },
- HealthCheck: &types.HealthCheckConfig{
- Test: []string{
- "CMD-SHELL",
- "echo \"hello world\"",
- },
- Interval: "10s",
- Timeout: "1s",
- Retries: uint64Ptr(5),
- },
- Hostname: "foo",
- Image: "redis",
- Ipc: "host",
- Labels: map[string]string{
- "com.example.description": "Accounting webapp",
- "com.example.number": "42",
- "com.example.empty-label": "",
- },
- Links: []string{
- "db",
- "db:database",
- "redis",
- },
- Logging: &types.LoggingConfig{
- Driver: "syslog",
- Options: map[string]string{
- "syslog-address": "tcp://192.168.0.42:123",
- },
- },
- MacAddress: "02:42:ac:11:65:43",
- NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
- Networks: map[string]*types.ServiceNetworkConfig{
- "some-network": {
- Aliases: []string{"alias1", "alias3"},
- Ipv4Address: "",
- Ipv6Address: "",
- },
- "other-network": {
- Ipv4Address: "172.16.238.10",
- Ipv6Address: "2001:3984:3989::10",
- },
- "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",
- },
- Privileged: true,
- ReadOnly: true,
- Restart: "always",
- SecurityOpt: []string{
- "label=level:s0:c100,c200",
- "label=type:svirt_apache_t",
- },
- StdinOpen: true,
- StopSignal: "SIGUSR1",
- StopGracePeriod: &stopGracePeriod,
- Tmpfs: []string{"/run", "/tmp"},
- Tty: true,
- Ulimits: map[string]*types.UlimitsConfig{
- "nproc": {
- Single: 65535,
- },
- "nofile": {
- Soft: 20000,
- Hard: 40000,
- },
- },
- User: "someone",
- Volumes: []string{
- "/var/lib/mysql",
- "/opt/data:/var/lib/mysql",
- fmt.Sprintf("%s:/code", workingDir),
- fmt.Sprintf("%s/static:/var/www/html", workingDir),
- fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
- "datavolume:/var/lib/mysql",
- },
- WorkingDir: "/code",
- }
- assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
- expectedNetworkConfig := map[string]types.NetworkConfig{
- "some-network": {},
- "other-network": {
- Driver: "overlay",
- DriverOpts: map[string]string{
- "foo": "bar",
- "baz": "1",
- },
- Ipam: types.IPAMConfig{
- Driver: "overlay",
- Config: []*types.IPAMPool{
- {Subnet: "172.16.238.0/24"},
- {Subnet: "2001:3984:3989::/64"},
- },
- },
- },
- "external-network": {
- External: types.External{
- Name: "external-network",
- External: true,
- },
- },
- "other-external-network": {
- External: types.External{
- Name: "my-cool-network",
- External: true,
- },
- },
- }
- assert.Equal(t, expectedNetworkConfig, config.Networks)
- expectedVolumeConfig := map[string]types.VolumeConfig{
- "some-volume": {},
- "other-volume": {
- Driver: "flocker",
- DriverOpts: map[string]string{
- "foo": "bar",
- "baz": "1",
- },
- },
- "external-volume": {
- External: types.External{
- Name: "external-volume",
- External: true,
- },
- },
- "other-external-volume": {
- External: types.External{
- Name: "my-cool-volume",
- External: true,
- },
- },
- }
- assert.Equal(t, expectedVolumeConfig, config.Volumes)
- }
- func loadYAML(yaml string) (*types.Config, error) {
- dict, err := ParseYAML([]byte(yaml))
- if err != nil {
- return nil, err
- }
- return Load(buildConfigDetails(dict))
- }
- func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
- sort.Sort(servicesByName(services))
- return services
- }
- type servicesByName []types.ServiceConfig
- func (sbn servicesByName) Len() int { return len(sbn) }
- func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] }
- func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }
|