github.com/olljanat/moby@v1.13.1/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 := map[string]string{
   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 := map[string]string{
   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  		Expose: []string{"3000", "8000"},
   625  		ExternalLinks: []string{
   626  			"redis_1",
   627  			"project_db_1:mysql",
   628  			"project_db_1:postgresql",
   629  		},
   630  		ExtraHosts: map[string]string{
   631  			"otherhost": "50.31.209.229",
   632  			"somehost":  "162.242.195.82",
   633  		},
   634  		HealthCheck: &types.HealthCheckConfig{
   635  			Test: []string{
   636  				"CMD-SHELL",
   637  				"echo \"hello world\"",
   638  			},
   639  			Interval: "10s",
   640  			Timeout:  "1s",
   641  			Retries:  uint64Ptr(5),
   642  		},
   643  		Hostname: "foo",
   644  		Image:    "redis",
   645  		Ipc:      "host",
   646  		Labels: map[string]string{
   647  			"com.example.description": "Accounting webapp",
   648  			"com.example.number":      "42",
   649  			"com.example.empty-label": "",
   650  		},
   651  		Links: []string{
   652  			"db",
   653  			"db:database",
   654  			"redis",
   655  		},
   656  		Logging: &types.LoggingConfig{
   657  			Driver: "syslog",
   658  			Options: map[string]string{
   659  				"syslog-address": "tcp://192.168.0.42:123",
   660  			},
   661  		},
   662  		MacAddress:  "02:42:ac:11:65:43",
   663  		NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
   664  		Networks: map[string]*types.ServiceNetworkConfig{
   665  			"some-network": {
   666  				Aliases:     []string{"alias1", "alias3"},
   667  				Ipv4Address: "",
   668  				Ipv6Address: "",
   669  			},
   670  			"other-network": {
   671  				Ipv4Address: "172.16.238.10",
   672  				Ipv6Address: "2001:3984:3989::10",
   673  			},
   674  			"other-other-network": nil,
   675  		},
   676  		Pid: "host",
   677  		Ports: []string{
   678  			"3000",
   679  			"3000-3005",
   680  			"8000:8000",
   681  			"9090-9091:8080-8081",
   682  			"49100:22",
   683  			"127.0.0.1:8001:8001",
   684  			"127.0.0.1:5000-5010:5000-5010",
   685  		},
   686  		Privileged: true,
   687  		ReadOnly:   true,
   688  		Restart:    "always",
   689  		SecurityOpt: []string{
   690  			"label=level:s0:c100,c200",
   691  			"label=type:svirt_apache_t",
   692  		},
   693  		StdinOpen:       true,
   694  		StopSignal:      "SIGUSR1",
   695  		StopGracePeriod: &stopGracePeriod,
   696  		Tmpfs:           []string{"/run", "/tmp"},
   697  		Tty:             true,
   698  		Ulimits: map[string]*types.UlimitsConfig{
   699  			"nproc": {
   700  				Single: 65535,
   701  			},
   702  			"nofile": {
   703  				Soft: 20000,
   704  				Hard: 40000,
   705  			},
   706  		},
   707  		User: "someone",
   708  		Volumes: []string{
   709  			"/var/lib/mysql",
   710  			"/opt/data:/var/lib/mysql",
   711  			fmt.Sprintf("%s:/code", workingDir),
   712  			fmt.Sprintf("%s/static:/var/www/html", workingDir),
   713  			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
   714  			"datavolume:/var/lib/mysql",
   715  		},
   716  		WorkingDir: "/code",
   717  	}
   718  
   719  	assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
   720  
   721  	expectedNetworkConfig := map[string]types.NetworkConfig{
   722  		"some-network": {},
   723  
   724  		"other-network": {
   725  			Driver: "overlay",
   726  			DriverOpts: map[string]string{
   727  				"foo": "bar",
   728  				"baz": "1",
   729  			},
   730  			Ipam: types.IPAMConfig{
   731  				Driver: "overlay",
   732  				Config: []*types.IPAMPool{
   733  					{Subnet: "172.16.238.0/24"},
   734  					{Subnet: "2001:3984:3989::/64"},
   735  				},
   736  			},
   737  		},
   738  
   739  		"external-network": {
   740  			External: types.External{
   741  				Name:     "external-network",
   742  				External: true,
   743  			},
   744  		},
   745  
   746  		"other-external-network": {
   747  			External: types.External{
   748  				Name:     "my-cool-network",
   749  				External: true,
   750  			},
   751  		},
   752  	}
   753  
   754  	assert.Equal(t, expectedNetworkConfig, config.Networks)
   755  
   756  	expectedVolumeConfig := map[string]types.VolumeConfig{
   757  		"some-volume": {},
   758  		"other-volume": {
   759  			Driver: "flocker",
   760  			DriverOpts: map[string]string{
   761  				"foo": "bar",
   762  				"baz": "1",
   763  			},
   764  		},
   765  		"external-volume": {
   766  			External: types.External{
   767  				Name:     "external-volume",
   768  				External: true,
   769  			},
   770  		},
   771  		"other-external-volume": {
   772  			External: types.External{
   773  				Name:     "my-cool-volume",
   774  				External: true,
   775  			},
   776  		},
   777  	}
   778  
   779  	assert.Equal(t, expectedVolumeConfig, config.Volumes)
   780  }
   781  
   782  func loadYAML(yaml string) (*types.Config, error) {
   783  	dict, err := ParseYAML([]byte(yaml))
   784  	if err != nil {
   785  		return nil, err
   786  	}
   787  
   788  	return Load(buildConfigDetails(dict))
   789  }
   790  
   791  func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
   792  	sort.Sort(servicesByName(services))
   793  	return services
   794  }
   795  
   796  type servicesByName []types.ServiceConfig
   797  
   798  func (sbn servicesByName) Len() int           { return len(sbn) }
   799  func (sbn servicesByName) Swap(i, j int)      { sbn[i], sbn[j] = sbn[j], sbn[i] }
   800  func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }