github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/cli/compose/loader/loader_test.go (about)

     1  package loader
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"sort"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/docker/docker/cli/compose/types"
    12  	"github.com/stretchr/testify/assert"
    13  )
    14  
    15  func buildConfigDetails(source types.Dict) types.ConfigDetails {
    16  	workingDir, err := os.Getwd()
    17  	if err != nil {
    18  		panic(err)
    19  	}
    20  
    21  	return types.ConfigDetails{
    22  		WorkingDir: workingDir,
    23  		ConfigFiles: []types.ConfigFile{
    24  			{Filename: "filename.yml", Config: source},
    25  		},
    26  		Environment: nil,
    27  	}
    28  }
    29  
    30  var sampleYAML = `
    31  version: "3"
    32  services:
    33    foo:
    34      image: busybox
    35      networks:
    36        with_me:
    37    bar:
    38      image: busybox
    39      environment:
    40        - FOO=1
    41      networks:
    42        - with_ipam
    43  volumes:
    44    hello:
    45      driver: default
    46      driver_opts:
    47        beep: boop
    48  networks:
    49    default:
    50      driver: bridge
    51      driver_opts:
    52        beep: boop
    53    with_ipam:
    54      ipam:
    55        driver: default
    56        config:
    57          - subnet: 172.28.0.0/16
    58  `
    59  
    60  var sampleDict = types.Dict{
    61  	"version": "3",
    62  	"services": types.Dict{
    63  		"foo": types.Dict{
    64  			"image":    "busybox",
    65  			"networks": types.Dict{"with_me": nil},
    66  		},
    67  		"bar": types.Dict{
    68  			"image":       "busybox",
    69  			"environment": []interface{}{"FOO=1"},
    70  			"networks":    []interface{}{"with_ipam"},
    71  		},
    72  	},
    73  	"volumes": types.Dict{
    74  		"hello": types.Dict{
    75  			"driver": "default",
    76  			"driver_opts": types.Dict{
    77  				"beep": "boop",
    78  			},
    79  		},
    80  	},
    81  	"networks": types.Dict{
    82  		"default": types.Dict{
    83  			"driver": "bridge",
    84  			"driver_opts": types.Dict{
    85  				"beep": "boop",
    86  			},
    87  		},
    88  		"with_ipam": types.Dict{
    89  			"ipam": types.Dict{
    90  				"driver": "default",
    91  				"config": []interface{}{
    92  					types.Dict{
    93  						"subnet": "172.28.0.0/16",
    94  					},
    95  				},
    96  			},
    97  		},
    98  	},
    99  }
   100  
   101  var sampleConfig = types.Config{
   102  	Services: []types.ServiceConfig{
   103  		{
   104  			Name:        "foo",
   105  			Image:       "busybox",
   106  			Environment: map[string]string{},
   107  			Networks: map[string]*types.ServiceNetworkConfig{
   108  				"with_me": nil,
   109  			},
   110  		},
   111  		{
   112  			Name:        "bar",
   113  			Image:       "busybox",
   114  			Environment: map[string]string{"FOO": "1"},
   115  			Networks: map[string]*types.ServiceNetworkConfig{
   116  				"with_ipam": nil,
   117  			},
   118  		},
   119  	},
   120  	Networks: map[string]types.NetworkConfig{
   121  		"default": {
   122  			Driver: "bridge",
   123  			DriverOpts: map[string]string{
   124  				"beep": "boop",
   125  			},
   126  		},
   127  		"with_ipam": {
   128  			Ipam: types.IPAMConfig{
   129  				Driver: "default",
   130  				Config: []*types.IPAMPool{
   131  					{
   132  						Subnet: "172.28.0.0/16",
   133  					},
   134  				},
   135  			},
   136  		},
   137  	},
   138  	Volumes: map[string]types.VolumeConfig{
   139  		"hello": {
   140  			Driver: "default",
   141  			DriverOpts: map[string]string{
   142  				"beep": "boop",
   143  			},
   144  		},
   145  	},
   146  }
   147  
   148  func TestParseYAML(t *testing.T) {
   149  	dict, err := ParseYAML([]byte(sampleYAML))
   150  	if !assert.NoError(t, err) {
   151  		return
   152  	}
   153  	assert.Equal(t, sampleDict, dict)
   154  }
   155  
   156  func TestLoad(t *testing.T) {
   157  	actual, err := Load(buildConfigDetails(sampleDict))
   158  	if !assert.NoError(t, err) {
   159  		return
   160  	}
   161  	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
   162  	assert.Equal(t, sampleConfig.Networks, actual.Networks)
   163  	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
   164  }
   165  
   166  func TestLoadV31(t *testing.T) {
   167  	actual, err := loadYAML(`
   168  version: "3.1"
   169  services:
   170    foo:
   171      image: busybox
   172      secrets: [super]
   173  secrets:
   174    super:
   175      external: true
   176  `)
   177  	if !assert.NoError(t, err) {
   178  		return
   179  	}
   180  	assert.Equal(t, len(actual.Services), 1)
   181  	assert.Equal(t, len(actual.Secrets), 1)
   182  }
   183  
   184  func TestParseAndLoad(t *testing.T) {
   185  	actual, err := loadYAML(sampleYAML)
   186  	if !assert.NoError(t, err) {
   187  		return
   188  	}
   189  	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
   190  	assert.Equal(t, sampleConfig.Networks, actual.Networks)
   191  	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
   192  }
   193  
   194  func TestInvalidTopLevelObjectType(t *testing.T) {
   195  	_, err := loadYAML("1")
   196  	assert.Error(t, err)
   197  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   198  
   199  	_, err = loadYAML("\"hello\"")
   200  	assert.Error(t, err)
   201  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   202  
   203  	_, err = loadYAML("[\"hello\"]")
   204  	assert.Error(t, err)
   205  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   206  }
   207  
   208  func TestNonStringKeys(t *testing.T) {
   209  	_, err := loadYAML(`
   210  version: "3"
   211  123:
   212    foo:
   213      image: busybox
   214  `)
   215  	assert.Error(t, err)
   216  	assert.Contains(t, err.Error(), "Non-string key at top level: 123")
   217  
   218  	_, err = loadYAML(`
   219  version: "3"
   220  services:
   221    foo:
   222      image: busybox
   223    123:
   224      image: busybox
   225  `)
   226  	assert.Error(t, err)
   227  	assert.Contains(t, err.Error(), "Non-string key in services: 123")
   228  
   229  	_, err = loadYAML(`
   230  version: "3"
   231  services:
   232    foo:
   233      image: busybox
   234  networks:
   235    default:
   236      ipam:
   237        config:
   238          - 123: oh dear
   239  `)
   240  	assert.Error(t, err)
   241  	assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
   242  
   243  	_, err = loadYAML(`
   244  version: "3"
   245  services:
   246    dict-env:
   247      image: busybox
   248      environment:
   249        1: FOO
   250  `)
   251  	assert.Error(t, err)
   252  	assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
   253  }
   254  
   255  func TestSupportedVersion(t *testing.T) {
   256  	_, err := loadYAML(`
   257  version: "3"
   258  services:
   259    foo:
   260      image: busybox
   261  `)
   262  	assert.NoError(t, err)
   263  
   264  	_, err = loadYAML(`
   265  version: "3.0"
   266  services:
   267    foo:
   268      image: busybox
   269  `)
   270  	assert.NoError(t, err)
   271  }
   272  
   273  func TestUnsupportedVersion(t *testing.T) {
   274  	_, err := loadYAML(`
   275  version: "2"
   276  services:
   277    foo:
   278      image: busybox
   279  `)
   280  	assert.Error(t, err)
   281  	assert.Contains(t, err.Error(), "version")
   282  
   283  	_, err = loadYAML(`
   284  version: "2.0"
   285  services:
   286    foo:
   287      image: busybox
   288  `)
   289  	assert.Error(t, err)
   290  	assert.Contains(t, err.Error(), "version")
   291  }
   292  
   293  func TestInvalidVersion(t *testing.T) {
   294  	_, err := loadYAML(`
   295  version: 3
   296  services:
   297    foo:
   298      image: busybox
   299  `)
   300  	assert.Error(t, err)
   301  	assert.Contains(t, err.Error(), "version must be a string")
   302  }
   303  
   304  func TestV1Unsupported(t *testing.T) {
   305  	_, err := loadYAML(`
   306  foo:
   307    image: busybox
   308  `)
   309  	assert.Error(t, err)
   310  }
   311  
   312  func TestNonMappingObject(t *testing.T) {
   313  	_, err := loadYAML(`
   314  version: "3"
   315  services:
   316    - foo:
   317        image: busybox
   318  `)
   319  	assert.Error(t, err)
   320  	assert.Contains(t, err.Error(), "services must be a mapping")
   321  
   322  	_, err = loadYAML(`
   323  version: "3"
   324  services:
   325    foo: busybox
   326  `)
   327  	assert.Error(t, err)
   328  	assert.Contains(t, err.Error(), "services.foo must be a mapping")
   329  
   330  	_, err = loadYAML(`
   331  version: "3"
   332  networks:
   333    - default:
   334        driver: bridge
   335  `)
   336  	assert.Error(t, err)
   337  	assert.Contains(t, err.Error(), "networks must be a mapping")
   338  
   339  	_, err = loadYAML(`
   340  version: "3"
   341  networks:
   342    default: bridge
   343  `)
   344  	assert.Error(t, err)
   345  	assert.Contains(t, err.Error(), "networks.default must be a mapping")
   346  
   347  	_, err = loadYAML(`
   348  version: "3"
   349  volumes:
   350    - data:
   351        driver: local
   352  `)
   353  	assert.Error(t, err)
   354  	assert.Contains(t, err.Error(), "volumes must be a mapping")
   355  
   356  	_, err = loadYAML(`
   357  version: "3"
   358  volumes:
   359    data: local
   360  `)
   361  	assert.Error(t, err)
   362  	assert.Contains(t, err.Error(), "volumes.data must be a mapping")
   363  }
   364  
   365  func TestNonStringImage(t *testing.T) {
   366  	_, err := loadYAML(`
   367  version: "3"
   368  services:
   369    foo:
   370      image: ["busybox", "latest"]
   371  `)
   372  	assert.Error(t, err)
   373  	assert.Contains(t, err.Error(), "services.foo.image must be a string")
   374  }
   375  
   376  func TestValidEnvironment(t *testing.T) {
   377  	config, err := loadYAML(`
   378  version: "3"
   379  services:
   380    dict-env:
   381      image: busybox
   382      environment:
   383        FOO: "1"
   384        BAR: 2
   385        BAZ: 2.5
   386        QUUX:
   387    list-env:
   388      image: busybox
   389      environment:
   390        - FOO=1
   391        - BAR=2
   392        - BAZ=2.5
   393        - QUUX=
   394  `)
   395  	assert.NoError(t, err)
   396  
   397  	expected := types.MappingWithEquals{
   398  		"FOO":  "1",
   399  		"BAR":  "2",
   400  		"BAZ":  "2.5",
   401  		"QUUX": "",
   402  	}
   403  
   404  	assert.Equal(t, 2, len(config.Services))
   405  
   406  	for _, service := range config.Services {
   407  		assert.Equal(t, expected, service.Environment)
   408  	}
   409  }
   410  
   411  func TestInvalidEnvironmentValue(t *testing.T) {
   412  	_, err := loadYAML(`
   413  version: "3"
   414  services:
   415    dict-env:
   416      image: busybox
   417      environment:
   418        FOO: ["1"]
   419  `)
   420  	assert.Error(t, err)
   421  	assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
   422  }
   423  
   424  func TestInvalidEnvironmentObject(t *testing.T) {
   425  	_, err := loadYAML(`
   426  version: "3"
   427  services:
   428    dict-env:
   429      image: busybox
   430      environment: "FOO=1"
   431  `)
   432  	assert.Error(t, err)
   433  	assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
   434  }
   435  
   436  func TestEnvironmentInterpolation(t *testing.T) {
   437  	config, err := loadYAML(`
   438  version: "3"
   439  services:
   440    test:
   441      image: busybox
   442      labels:
   443        - home1=$HOME
   444        - home2=${HOME}
   445        - nonexistent=$NONEXISTENT
   446        - default=${NONEXISTENT-default}
   447  networks:
   448    test:
   449      driver: $HOME
   450  volumes:
   451    test:
   452      driver: $HOME
   453  `)
   454  
   455  	assert.NoError(t, err)
   456  
   457  	home := os.Getenv("HOME")
   458  
   459  	expectedLabels := types.MappingWithEquals{
   460  		"home1":       home,
   461  		"home2":       home,
   462  		"nonexistent": "",
   463  		"default":     "default",
   464  	}
   465  
   466  	assert.Equal(t, expectedLabels, config.Services[0].Labels)
   467  	assert.Equal(t, home, config.Networks["test"].Driver)
   468  	assert.Equal(t, home, config.Volumes["test"].Driver)
   469  }
   470  
   471  func TestUnsupportedProperties(t *testing.T) {
   472  	dict, err := ParseYAML([]byte(`
   473  version: "3"
   474  services:
   475    web:
   476      image: web
   477      build: ./web
   478      links:
   479        - bar
   480    db:
   481      image: db
   482      build: ./db
   483  `))
   484  	assert.NoError(t, err)
   485  
   486  	configDetails := buildConfigDetails(dict)
   487  
   488  	_, err = Load(configDetails)
   489  	assert.NoError(t, err)
   490  
   491  	unsupported := GetUnsupportedProperties(configDetails)
   492  	assert.Equal(t, []string{"build", "links"}, unsupported)
   493  }
   494  
   495  func TestDeprecatedProperties(t *testing.T) {
   496  	dict, err := ParseYAML([]byte(`
   497  version: "3"
   498  services:
   499    web:
   500      image: web
   501      container_name: web
   502    db:
   503      image: db
   504      container_name: db
   505      expose: ["5434"]
   506  `))
   507  	assert.NoError(t, err)
   508  
   509  	configDetails := buildConfigDetails(dict)
   510  
   511  	_, err = Load(configDetails)
   512  	assert.NoError(t, err)
   513  
   514  	deprecated := GetDeprecatedProperties(configDetails)
   515  	assert.Equal(t, 2, len(deprecated))
   516  	assert.Contains(t, deprecated, "container_name")
   517  	assert.Contains(t, deprecated, "expose")
   518  }
   519  
   520  func TestForbiddenProperties(t *testing.T) {
   521  	_, err := loadYAML(`
   522  version: "3"
   523  services:
   524    foo:
   525      image: busybox
   526      volumes:
   527        - /data
   528      volume_driver: some-driver
   529    bar:
   530      extends:
   531        service: foo
   532  `)
   533  
   534  	assert.Error(t, err)
   535  	assert.IsType(t, &ForbiddenPropertiesError{}, err)
   536  	fmt.Println(err)
   537  	forbidden := err.(*ForbiddenPropertiesError).Properties
   538  
   539  	assert.Equal(t, 2, len(forbidden))
   540  	assert.Contains(t, forbidden, "volume_driver")
   541  	assert.Contains(t, forbidden, "extends")
   542  }
   543  
   544  func durationPtr(value time.Duration) *time.Duration {
   545  	return &value
   546  }
   547  
   548  func int64Ptr(value int64) *int64 {
   549  	return &value
   550  }
   551  
   552  func uint64Ptr(value uint64) *uint64 {
   553  	return &value
   554  }
   555  
   556  func TestFullExample(t *testing.T) {
   557  	bytes, err := ioutil.ReadFile("full-example.yml")
   558  	assert.NoError(t, err)
   559  
   560  	config, err := loadYAML(string(bytes))
   561  	if !assert.NoError(t, err) {
   562  		return
   563  	}
   564  
   565  	workingDir, err := os.Getwd()
   566  	assert.NoError(t, err)
   567  
   568  	homeDir := os.Getenv("HOME")
   569  	stopGracePeriod := time.Duration(20 * time.Second)
   570  
   571  	expectedServiceConfig := types.ServiceConfig{
   572  		Name: "foo",
   573  
   574  		CapAdd:        []string{"ALL"},
   575  		CapDrop:       []string{"NET_ADMIN", "SYS_ADMIN"},
   576  		CgroupParent:  "m-executor-abcd",
   577  		Command:       []string{"bundle", "exec", "thin", "-p", "3000"},
   578  		ContainerName: "my-web-container",
   579  		DependsOn:     []string{"db", "redis"},
   580  		Deploy: types.DeployConfig{
   581  			Mode:     "replicated",
   582  			Replicas: uint64Ptr(6),
   583  			Labels:   map[string]string{"FOO": "BAR"},
   584  			UpdateConfig: &types.UpdateConfig{
   585  				Parallelism:     uint64Ptr(3),
   586  				Delay:           time.Duration(10 * time.Second),
   587  				FailureAction:   "continue",
   588  				Monitor:         time.Duration(60 * time.Second),
   589  				MaxFailureRatio: 0.3,
   590  			},
   591  			Resources: types.Resources{
   592  				Limits: &types.Resource{
   593  					NanoCPUs:    "0.001",
   594  					MemoryBytes: 50 * 1024 * 1024,
   595  				},
   596  				Reservations: &types.Resource{
   597  					NanoCPUs:    "0.0001",
   598  					MemoryBytes: 20 * 1024 * 1024,
   599  				},
   600  			},
   601  			RestartPolicy: &types.RestartPolicy{
   602  				Condition:   "on_failure",
   603  				Delay:       durationPtr(5 * time.Second),
   604  				MaxAttempts: uint64Ptr(3),
   605  				Window:      durationPtr(2 * time.Minute),
   606  			},
   607  			Placement: types.Placement{
   608  				Constraints: []string{"node=foo"},
   609  			},
   610  		},
   611  		Devices:    []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
   612  		DNS:        []string{"8.8.8.8", "9.9.9.9"},
   613  		DNSSearch:  []string{"dc1.example.com", "dc2.example.com"},
   614  		DomainName: "foo.com",
   615  		Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
   616  		Environment: map[string]string{
   617  			"RACK_ENV":       "development",
   618  			"SHOW":           "true",
   619  			"SESSION_SECRET": "",
   620  			"FOO":            "1",
   621  			"BAR":            "2",
   622  			"BAZ":            "3",
   623  		},
   624  		EnvFile: []string{
   625  			"./example1.env",
   626  			"./example2.env",
   627  		},
   628  		Expose: []string{"3000", "8000"},
   629  		ExternalLinks: []string{
   630  			"redis_1",
   631  			"project_db_1:mysql",
   632  			"project_db_1:postgresql",
   633  		},
   634  		ExtraHosts: map[string]string{
   635  			"otherhost": "50.31.209.229",
   636  			"somehost":  "162.242.195.82",
   637  		},
   638  		HealthCheck: &types.HealthCheckConfig{
   639  			Test:     types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
   640  			Interval: "10s",
   641  			Timeout:  "1s",
   642  			Retries:  uint64Ptr(5),
   643  		},
   644  		Hostname: "foo",
   645  		Image:    "redis",
   646  		Ipc:      "host",
   647  		Labels: map[string]string{
   648  			"com.example.description": "Accounting webapp",
   649  			"com.example.number":      "42",
   650  			"com.example.empty-label": "",
   651  		},
   652  		Links: []string{
   653  			"db",
   654  			"db:database",
   655  			"redis",
   656  		},
   657  		Logging: &types.LoggingConfig{
   658  			Driver: "syslog",
   659  			Options: map[string]string{
   660  				"syslog-address": "tcp://192.168.0.42:123",
   661  			},
   662  		},
   663  		MacAddress:  "02:42:ac:11:65:43",
   664  		NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
   665  		Networks: map[string]*types.ServiceNetworkConfig{
   666  			"some-network": {
   667  				Aliases:     []string{"alias1", "alias3"},
   668  				Ipv4Address: "",
   669  				Ipv6Address: "",
   670  			},
   671  			"other-network": {
   672  				Ipv4Address: "172.16.238.10",
   673  				Ipv6Address: "2001:3984:3989::10",
   674  			},
   675  			"other-other-network": nil,
   676  		},
   677  		Pid: "host",
   678  		Ports: []types.ServicePortConfig{
   679  			//"3000",
   680  			{
   681  				Mode:     "ingress",
   682  				Target:   3000,
   683  				Protocol: "tcp",
   684  			},
   685  			//"3000-3005",
   686  			{
   687  				Mode:     "ingress",
   688  				Target:   3000,
   689  				Protocol: "tcp",
   690  			},
   691  			{
   692  				Mode:     "ingress",
   693  				Target:   3001,
   694  				Protocol: "tcp",
   695  			},
   696  			{
   697  				Mode:     "ingress",
   698  				Target:   3002,
   699  				Protocol: "tcp",
   700  			},
   701  			{
   702  				Mode:     "ingress",
   703  				Target:   3003,
   704  				Protocol: "tcp",
   705  			},
   706  			{
   707  				Mode:     "ingress",
   708  				Target:   3004,
   709  				Protocol: "tcp",
   710  			},
   711  			{
   712  				Mode:     "ingress",
   713  				Target:   3005,
   714  				Protocol: "tcp",
   715  			},
   716  			//"8000:8000",
   717  			{
   718  				Mode:      "ingress",
   719  				Target:    8000,
   720  				Published: 8000,
   721  				Protocol:  "tcp",
   722  			},
   723  			//"9090-9091:8080-8081",
   724  			{
   725  				Mode:      "ingress",
   726  				Target:    8080,
   727  				Published: 9090,
   728  				Protocol:  "tcp",
   729  			},
   730  			{
   731  				Mode:      "ingress",
   732  				Target:    8081,
   733  				Published: 9091,
   734  				Protocol:  "tcp",
   735  			},
   736  			//"49100:22",
   737  			{
   738  				Mode:      "ingress",
   739  				Target:    22,
   740  				Published: 49100,
   741  				Protocol:  "tcp",
   742  			},
   743  			//"127.0.0.1:8001:8001",
   744  			{
   745  				Mode:      "ingress",
   746  				Target:    8001,
   747  				Published: 8001,
   748  				Protocol:  "tcp",
   749  			},
   750  			//"127.0.0.1:5000-5010:5000-5010",
   751  			{
   752  				Mode:      "ingress",
   753  				Target:    5000,
   754  				Published: 5000,
   755  				Protocol:  "tcp",
   756  			},
   757  			{
   758  				Mode:      "ingress",
   759  				Target:    5001,
   760  				Published: 5001,
   761  				Protocol:  "tcp",
   762  			},
   763  			{
   764  				Mode:      "ingress",
   765  				Target:    5002,
   766  				Published: 5002,
   767  				Protocol:  "tcp",
   768  			},
   769  			{
   770  				Mode:      "ingress",
   771  				Target:    5003,
   772  				Published: 5003,
   773  				Protocol:  "tcp",
   774  			},
   775  			{
   776  				Mode:      "ingress",
   777  				Target:    5004,
   778  				Published: 5004,
   779  				Protocol:  "tcp",
   780  			},
   781  			{
   782  				Mode:      "ingress",
   783  				Target:    5005,
   784  				Published: 5005,
   785  				Protocol:  "tcp",
   786  			},
   787  			{
   788  				Mode:      "ingress",
   789  				Target:    5006,
   790  				Published: 5006,
   791  				Protocol:  "tcp",
   792  			},
   793  			{
   794  				Mode:      "ingress",
   795  				Target:    5007,
   796  				Published: 5007,
   797  				Protocol:  "tcp",
   798  			},
   799  			{
   800  				Mode:      "ingress",
   801  				Target:    5008,
   802  				Published: 5008,
   803  				Protocol:  "tcp",
   804  			},
   805  			{
   806  				Mode:      "ingress",
   807  				Target:    5009,
   808  				Published: 5009,
   809  				Protocol:  "tcp",
   810  			},
   811  			{
   812  				Mode:      "ingress",
   813  				Target:    5010,
   814  				Published: 5010,
   815  				Protocol:  "tcp",
   816  			},
   817  		},
   818  		Privileged: true,
   819  		ReadOnly:   true,
   820  		Restart:    "always",
   821  		SecurityOpt: []string{
   822  			"label=level:s0:c100,c200",
   823  			"label=type:svirt_apache_t",
   824  		},
   825  		StdinOpen:       true,
   826  		StopSignal:      "SIGUSR1",
   827  		StopGracePeriod: &stopGracePeriod,
   828  		Tmpfs:           []string{"/run", "/tmp"},
   829  		Tty:             true,
   830  		Ulimits: map[string]*types.UlimitsConfig{
   831  			"nproc": {
   832  				Single: 65535,
   833  			},
   834  			"nofile": {
   835  				Soft: 20000,
   836  				Hard: 40000,
   837  			},
   838  		},
   839  		User: "someone",
   840  		Volumes: []string{
   841  			"/var/lib/mysql",
   842  			"/opt/data:/var/lib/mysql",
   843  			fmt.Sprintf("%s:/code", workingDir),
   844  			fmt.Sprintf("%s/static:/var/www/html", workingDir),
   845  			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
   846  			"datavolume:/var/lib/mysql",
   847  		},
   848  		WorkingDir: "/code",
   849  	}
   850  
   851  	assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
   852  
   853  	expectedNetworkConfig := map[string]types.NetworkConfig{
   854  		"some-network": {},
   855  
   856  		"other-network": {
   857  			Driver: "overlay",
   858  			DriverOpts: map[string]string{
   859  				"foo": "bar",
   860  				"baz": "1",
   861  			},
   862  			Ipam: types.IPAMConfig{
   863  				Driver: "overlay",
   864  				Config: []*types.IPAMPool{
   865  					{Subnet: "172.16.238.0/24"},
   866  					{Subnet: "2001:3984:3989::/64"},
   867  				},
   868  			},
   869  		},
   870  
   871  		"external-network": {
   872  			External: types.External{
   873  				Name:     "external-network",
   874  				External: true,
   875  			},
   876  		},
   877  
   878  		"other-external-network": {
   879  			External: types.External{
   880  				Name:     "my-cool-network",
   881  				External: true,
   882  			},
   883  		},
   884  	}
   885  
   886  	assert.Equal(t, expectedNetworkConfig, config.Networks)
   887  
   888  	expectedVolumeConfig := map[string]types.VolumeConfig{
   889  		"some-volume": {},
   890  		"other-volume": {
   891  			Driver: "flocker",
   892  			DriverOpts: map[string]string{
   893  				"foo": "bar",
   894  				"baz": "1",
   895  			},
   896  		},
   897  		"external-volume": {
   898  			External: types.External{
   899  				Name:     "external-volume",
   900  				External: true,
   901  			},
   902  		},
   903  		"other-external-volume": {
   904  			External: types.External{
   905  				Name:     "my-cool-volume",
   906  				External: true,
   907  			},
   908  		},
   909  	}
   910  
   911  	assert.Equal(t, expectedVolumeConfig, config.Volumes)
   912  }
   913  
   914  func loadYAML(yaml string) (*types.Config, error) {
   915  	dict, err := ParseYAML([]byte(yaml))
   916  	if err != nil {
   917  		return nil, err
   918  	}
   919  
   920  	return Load(buildConfigDetails(dict))
   921  }
   922  
   923  func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
   924  	sort.Sort(servicesByName(services))
   925  	return services
   926  }
   927  
   928  type servicesByName []types.ServiceConfig
   929  
   930  func (sbn servicesByName) Len() int           { return len(sbn) }
   931  func (sbn servicesByName) Swap(i, j int)      { sbn[i], sbn[j] = sbn[j], sbn[i] }
   932  func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }
   933  
   934  func TestLoadAttachableNetwork(t *testing.T) {
   935  	config, err := loadYAML(`
   936  version: "3.1"
   937  networks:
   938    mynet1:
   939      driver: overlay
   940      attachable: true
   941    mynet2:
   942      driver: bridge
   943  `)
   944  	assert.NoError(t, err)
   945  
   946  	expected := map[string]types.NetworkConfig{
   947  		"mynet1": {
   948  			Driver:     "overlay",
   949  			Attachable: true,
   950  		},
   951  		"mynet2": {
   952  			Driver:     "bridge",
   953  			Attachable: false,
   954  		},
   955  	}
   956  
   957  	assert.Equal(t, expected, config.Networks)
   958  }
   959  
   960  func TestLoadExpandedPortFormat(t *testing.T) {
   961  	config, err := loadYAML(`
   962  version: "3.1"
   963  services:
   964    web:
   965      image: busybox
   966      ports:
   967        - "80-82:8080-8082"
   968        - "90-92:8090-8092/udp"
   969        - "85:8500"
   970        - 8600
   971        - protocol: udp
   972          target: 53
   973          published: 10053
   974        - mode: host
   975          target: 22
   976          published: 10022
   977  `)
   978  	assert.NoError(t, err)
   979  
   980  	expected := []types.ServicePortConfig{
   981  		{
   982  			Mode:      "ingress",
   983  			Target:    8080,
   984  			Published: 80,
   985  			Protocol:  "tcp",
   986  		},
   987  		{
   988  			Mode:      "ingress",
   989  			Target:    8081,
   990  			Published: 81,
   991  			Protocol:  "tcp",
   992  		},
   993  		{
   994  			Mode:      "ingress",
   995  			Target:    8082,
   996  			Published: 82,
   997  			Protocol:  "tcp",
   998  		},
   999  		{
  1000  			Mode:      "ingress",
  1001  			Target:    8090,
  1002  			Published: 90,
  1003  			Protocol:  "udp",
  1004  		},
  1005  		{
  1006  			Mode:      "ingress",
  1007  			Target:    8091,
  1008  			Published: 91,
  1009  			Protocol:  "udp",
  1010  		},
  1011  		{
  1012  			Mode:      "ingress",
  1013  			Target:    8092,
  1014  			Published: 92,
  1015  			Protocol:  "udp",
  1016  		},
  1017  		{
  1018  			Mode:      "ingress",
  1019  			Target:    8500,
  1020  			Published: 85,
  1021  			Protocol:  "tcp",
  1022  		},
  1023  		{
  1024  			Mode:      "ingress",
  1025  			Target:    8600,
  1026  			Published: 0,
  1027  			Protocol:  "tcp",
  1028  		},
  1029  		{
  1030  			Target:    53,
  1031  			Published: 10053,
  1032  			Protocol:  "udp",
  1033  		},
  1034  		{
  1035  			Mode:      "host",
  1036  			Target:    22,
  1037  			Published: 10022,
  1038  		},
  1039  	}
  1040  
  1041  	assert.Equal(t, 1, len(config.Services))
  1042  	assert.Equal(t, expected, config.Services[0].Ports)
  1043  }