github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/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 TestParseAndLoad(t *testing.T) {
   167  	actual, err := loadYAML(sampleYAML)
   168  	if !assert.NoError(t, err) {
   169  		return
   170  	}
   171  	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
   172  	assert.Equal(t, sampleConfig.Networks, actual.Networks)
   173  	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
   174  }
   175  
   176  func TestInvalidTopLevelObjectType(t *testing.T) {
   177  	_, err := loadYAML("1")
   178  	assert.Error(t, err)
   179  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   180  
   181  	_, err = loadYAML("\"hello\"")
   182  	assert.Error(t, err)
   183  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   184  
   185  	_, err = loadYAML("[\"hello\"]")
   186  	assert.Error(t, err)
   187  	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
   188  }
   189  
   190  func TestNonStringKeys(t *testing.T) {
   191  	_, err := loadYAML(`
   192  version: "3"
   193  123:
   194    foo:
   195      image: busybox
   196  `)
   197  	assert.Error(t, err)
   198  	assert.Contains(t, err.Error(), "Non-string key at top level: 123")
   199  
   200  	_, err = loadYAML(`
   201  version: "3"
   202  services:
   203    foo:
   204      image: busybox
   205    123:
   206      image: busybox
   207  `)
   208  	assert.Error(t, err)
   209  	assert.Contains(t, err.Error(), "Non-string key in services: 123")
   210  
   211  	_, err = loadYAML(`
   212  version: "3"
   213  services:
   214    foo:
   215      image: busybox
   216  networks:
   217    default:
   218      ipam:
   219        config:
   220          - 123: oh dear
   221  `)
   222  	assert.Error(t, err)
   223  	assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
   224  
   225  	_, err = loadYAML(`
   226  version: "3"
   227  services:
   228    dict-env:
   229      image: busybox
   230      environment:
   231        1: FOO
   232  `)
   233  	assert.Error(t, err)
   234  	assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
   235  }
   236  
   237  func TestSupportedVersion(t *testing.T) {
   238  	_, err := loadYAML(`
   239  version: "3"
   240  services:
   241    foo:
   242      image: busybox
   243  `)
   244  	assert.NoError(t, err)
   245  
   246  	_, err = loadYAML(`
   247  version: "3.0"
   248  services:
   249    foo:
   250      image: busybox
   251  `)
   252  	assert.NoError(t, err)
   253  }
   254  
   255  func TestUnsupportedVersion(t *testing.T) {
   256  	_, err := loadYAML(`
   257  version: "2"
   258  services:
   259    foo:
   260      image: busybox
   261  `)
   262  	assert.Error(t, err)
   263  	assert.Contains(t, err.Error(), "version")
   264  
   265  	_, err = loadYAML(`
   266  version: "2.0"
   267  services:
   268    foo:
   269      image: busybox
   270  `)
   271  	assert.Error(t, err)
   272  	assert.Contains(t, err.Error(), "version")
   273  }
   274  
   275  func TestInvalidVersion(t *testing.T) {
   276  	_, err := loadYAML(`
   277  version: 3
   278  services:
   279    foo:
   280      image: busybox
   281  `)
   282  	assert.Error(t, err)
   283  	assert.Contains(t, err.Error(), "version must be a string")
   284  }
   285  
   286  func TestV1Unsupported(t *testing.T) {
   287  	_, err := loadYAML(`
   288  foo:
   289    image: busybox
   290  `)
   291  	assert.Error(t, err)
   292  }
   293  
   294  func TestNonMappingObject(t *testing.T) {
   295  	_, err := loadYAML(`
   296  version: "3"
   297  services:
   298    - foo:
   299        image: busybox
   300  `)
   301  	assert.Error(t, err)
   302  	assert.Contains(t, err.Error(), "services must be a mapping")
   303  
   304  	_, err = loadYAML(`
   305  version: "3"
   306  services:
   307    foo: busybox
   308  `)
   309  	assert.Error(t, err)
   310  	assert.Contains(t, err.Error(), "services.foo must be a mapping")
   311  
   312  	_, err = loadYAML(`
   313  version: "3"
   314  networks:
   315    - default:
   316        driver: bridge
   317  `)
   318  	assert.Error(t, err)
   319  	assert.Contains(t, err.Error(), "networks must be a mapping")
   320  
   321  	_, err = loadYAML(`
   322  version: "3"
   323  networks:
   324    default: bridge
   325  `)
   326  	assert.Error(t, err)
   327  	assert.Contains(t, err.Error(), "networks.default must be a mapping")
   328  
   329  	_, err = loadYAML(`
   330  version: "3"
   331  volumes:
   332    - data:
   333        driver: local
   334  `)
   335  	assert.Error(t, err)
   336  	assert.Contains(t, err.Error(), "volumes must be a mapping")
   337  
   338  	_, err = loadYAML(`
   339  version: "3"
   340  volumes:
   341    data: local
   342  `)
   343  	assert.Error(t, err)
   344  	assert.Contains(t, err.Error(), "volumes.data must be a mapping")
   345  }
   346  
   347  func TestNonStringImage(t *testing.T) {
   348  	_, err := loadYAML(`
   349  version: "3"
   350  services:
   351    foo:
   352      image: ["busybox", "latest"]
   353  `)
   354  	assert.Error(t, err)
   355  	assert.Contains(t, err.Error(), "services.foo.image must be a string")
   356  }
   357  
   358  func TestValidEnvironment(t *testing.T) {
   359  	config, err := loadYAML(`
   360  version: "3"
   361  services:
   362    dict-env:
   363      image: busybox
   364      environment:
   365        FOO: "1"
   366        BAR: 2
   367        BAZ: 2.5
   368        QUUX:
   369    list-env:
   370      image: busybox
   371      environment:
   372        - FOO=1
   373        - BAR=2
   374        - BAZ=2.5
   375        - QUUX=
   376  `)
   377  	assert.NoError(t, err)
   378  
   379  	expected := map[string]string{
   380  		"FOO":  "1",
   381  		"BAR":  "2",
   382  		"BAZ":  "2.5",
   383  		"QUUX": "",
   384  	}
   385  
   386  	assert.Equal(t, 2, len(config.Services))
   387  
   388  	for _, service := range config.Services {
   389  		assert.Equal(t, expected, service.Environment)
   390  	}
   391  }
   392  
   393  func TestInvalidEnvironmentValue(t *testing.T) {
   394  	_, err := loadYAML(`
   395  version: "3"
   396  services:
   397    dict-env:
   398      image: busybox
   399      environment:
   400        FOO: ["1"]
   401  `)
   402  	assert.Error(t, err)
   403  	assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
   404  }
   405  
   406  func TestInvalidEnvironmentObject(t *testing.T) {
   407  	_, err := loadYAML(`
   408  version: "3"
   409  services:
   410    dict-env:
   411      image: busybox
   412      environment: "FOO=1"
   413  `)
   414  	assert.Error(t, err)
   415  	assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
   416  }
   417  
   418  func TestEnvironmentInterpolation(t *testing.T) {
   419  	config, err := loadYAML(`
   420  version: "3"
   421  services:
   422    test:
   423      image: busybox
   424      labels:
   425        - home1=$HOME
   426        - home2=${HOME}
   427        - nonexistent=$NONEXISTENT
   428        - default=${NONEXISTENT-default}
   429  networks:
   430    test:
   431      driver: $HOME
   432  volumes:
   433    test:
   434      driver: $HOME
   435  `)
   436  
   437  	assert.NoError(t, err)
   438  
   439  	home := os.Getenv("HOME")
   440  
   441  	expectedLabels := map[string]string{
   442  		"home1":       home,
   443  		"home2":       home,
   444  		"nonexistent": "",
   445  		"default":     "default",
   446  	}
   447  
   448  	assert.Equal(t, expectedLabels, config.Services[0].Labels)
   449  	assert.Equal(t, home, config.Networks["test"].Driver)
   450  	assert.Equal(t, home, config.Volumes["test"].Driver)
   451  }
   452  
   453  func TestUnsupportedProperties(t *testing.T) {
   454  	dict, err := ParseYAML([]byte(`
   455  version: "3"
   456  services:
   457    web:
   458      image: web
   459      build: ./web
   460      links:
   461        - bar
   462    db:
   463      image: db
   464      build: ./db
   465  `))
   466  	assert.NoError(t, err)
   467  
   468  	configDetails := buildConfigDetails(dict)
   469  
   470  	_, err = Load(configDetails)
   471  	assert.NoError(t, err)
   472  
   473  	unsupported := GetUnsupportedProperties(configDetails)
   474  	assert.Equal(t, []string{"build", "links"}, unsupported)
   475  }
   476  
   477  func TestDeprecatedProperties(t *testing.T) {
   478  	dict, err := ParseYAML([]byte(`
   479  version: "3"
   480  services:
   481    web:
   482      image: web
   483      container_name: web
   484    db:
   485      image: db
   486      container_name: db
   487      expose: ["5434"]
   488  `))
   489  	assert.NoError(t, err)
   490  
   491  	configDetails := buildConfigDetails(dict)
   492  
   493  	_, err = Load(configDetails)
   494  	assert.NoError(t, err)
   495  
   496  	deprecated := GetDeprecatedProperties(configDetails)
   497  	assert.Equal(t, 2, len(deprecated))
   498  	assert.Contains(t, deprecated, "container_name")
   499  	assert.Contains(t, deprecated, "expose")
   500  }
   501  
   502  func TestForbiddenProperties(t *testing.T) {
   503  	_, err := loadYAML(`
   504  version: "3"
   505  services:
   506    foo:
   507      image: busybox
   508      volumes:
   509        - /data
   510      volume_driver: some-driver
   511    bar:
   512      extends:
   513        service: foo
   514  `)
   515  
   516  	assert.Error(t, err)
   517  	assert.IsType(t, &ForbiddenPropertiesError{}, err)
   518  	fmt.Println(err)
   519  	forbidden := err.(*ForbiddenPropertiesError).Properties
   520  
   521  	assert.Equal(t, 2, len(forbidden))
   522  	assert.Contains(t, forbidden, "volume_driver")
   523  	assert.Contains(t, forbidden, "extends")
   524  }
   525  
   526  func durationPtr(value time.Duration) *time.Duration {
   527  	return &value
   528  }
   529  
   530  func int64Ptr(value int64) *int64 {
   531  	return &value
   532  }
   533  
   534  func uint64Ptr(value uint64) *uint64 {
   535  	return &value
   536  }
   537  
   538  func TestFullExample(t *testing.T) {
   539  	bytes, err := ioutil.ReadFile("full-example.yml")
   540  	assert.NoError(t, err)
   541  
   542  	config, err := loadYAML(string(bytes))
   543  	if !assert.NoError(t, err) {
   544  		return
   545  	}
   546  
   547  	workingDir, err := os.Getwd()
   548  	assert.NoError(t, err)
   549  
   550  	homeDir := os.Getenv("HOME")
   551  	stopGracePeriod := time.Duration(20 * time.Second)
   552  
   553  	expectedServiceConfig := types.ServiceConfig{
   554  		Name: "foo",
   555  
   556  		CapAdd:        []string{"ALL"},
   557  		CapDrop:       []string{"NET_ADMIN", "SYS_ADMIN"},
   558  		CgroupParent:  "m-executor-abcd",
   559  		Command:       []string{"bundle", "exec", "thin", "-p", "3000"},
   560  		ContainerName: "my-web-container",
   561  		DependsOn:     []string{"db", "redis"},
   562  		Deploy: types.DeployConfig{
   563  			Mode:     "replicated",
   564  			Replicas: uint64Ptr(6),
   565  			Labels:   map[string]string{"FOO": "BAR"},
   566  			UpdateConfig: &types.UpdateConfig{
   567  				Parallelism:     uint64Ptr(3),
   568  				Delay:           time.Duration(10 * time.Second),
   569  				FailureAction:   "continue",
   570  				Monitor:         time.Duration(60 * time.Second),
   571  				MaxFailureRatio: 0.3,
   572  			},
   573  			Resources: types.Resources{
   574  				Limits: &types.Resource{
   575  					NanoCPUs:    "0.001",
   576  					MemoryBytes: 50 * 1024 * 1024,
   577  				},
   578  				Reservations: &types.Resource{
   579  					NanoCPUs:    "0.0001",
   580  					MemoryBytes: 20 * 1024 * 1024,
   581  				},
   582  			},
   583  			RestartPolicy: &types.RestartPolicy{
   584  				Condition:   "on_failure",
   585  				Delay:       durationPtr(5 * time.Second),
   586  				MaxAttempts: uint64Ptr(3),
   587  				Window:      durationPtr(2 * time.Minute),
   588  			},
   589  			Placement: types.Placement{
   590  				Constraints: []string{"node=foo"},
   591  			},
   592  		},
   593  		Devices:    []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
   594  		DNS:        []string{"8.8.8.8", "9.9.9.9"},
   595  		DNSSearch:  []string{"dc1.example.com", "dc2.example.com"},
   596  		DomainName: "foo.com",
   597  		Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
   598  		Environment: map[string]string{
   599  			"RACK_ENV":       "development",
   600  			"SHOW":           "true",
   601  			"SESSION_SECRET": "",
   602  			"FOO":            "1",
   603  			"BAR":            "2",
   604  			"BAZ":            "3",
   605  		},
   606  		Expose: []string{"3000", "8000"},
   607  		ExternalLinks: []string{
   608  			"redis_1",
   609  			"project_db_1:mysql",
   610  			"project_db_1:postgresql",
   611  		},
   612  		ExtraHosts: map[string]string{
   613  			"otherhost": "50.31.209.229",
   614  			"somehost":  "162.242.195.82",
   615  		},
   616  		HealthCheck: &types.HealthCheckConfig{
   617  			Test: []string{
   618  				"CMD-SHELL",
   619  				"echo \"hello world\"",
   620  			},
   621  			Interval: "10s",
   622  			Timeout:  "1s",
   623  			Retries:  uint64Ptr(5),
   624  		},
   625  		Hostname: "foo",
   626  		Image:    "redis",
   627  		Ipc:      "host",
   628  		Labels: map[string]string{
   629  			"com.example.description": "Accounting webapp",
   630  			"com.example.number":      "42",
   631  			"com.example.empty-label": "",
   632  		},
   633  		Links: []string{
   634  			"db",
   635  			"db:database",
   636  			"redis",
   637  		},
   638  		Logging: &types.LoggingConfig{
   639  			Driver: "syslog",
   640  			Options: map[string]string{
   641  				"syslog-address": "tcp://192.168.0.42:123",
   642  			},
   643  		},
   644  		MacAddress:  "02:42:ac:11:65:43",
   645  		NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
   646  		Networks: map[string]*types.ServiceNetworkConfig{
   647  			"some-network": {
   648  				Aliases:     []string{"alias1", "alias3"},
   649  				Ipv4Address: "",
   650  				Ipv6Address: "",
   651  			},
   652  			"other-network": {
   653  				Ipv4Address: "172.16.238.10",
   654  				Ipv6Address: "2001:3984:3989::10",
   655  			},
   656  			"other-other-network": nil,
   657  		},
   658  		Pid: "host",
   659  		Ports: []string{
   660  			"3000",
   661  			"3000-3005",
   662  			"8000:8000",
   663  			"9090-9091:8080-8081",
   664  			"49100:22",
   665  			"127.0.0.1:8001:8001",
   666  			"127.0.0.1:5000-5010:5000-5010",
   667  		},
   668  		Privileged: true,
   669  		ReadOnly:   true,
   670  		Restart:    "always",
   671  		SecurityOpt: []string{
   672  			"label=level:s0:c100,c200",
   673  			"label=type:svirt_apache_t",
   674  		},
   675  		StdinOpen:       true,
   676  		StopSignal:      "SIGUSR1",
   677  		StopGracePeriod: &stopGracePeriod,
   678  		Tmpfs:           []string{"/run", "/tmp"},
   679  		Tty:             true,
   680  		Ulimits: map[string]*types.UlimitsConfig{
   681  			"nproc": {
   682  				Single: 65535,
   683  			},
   684  			"nofile": {
   685  				Soft: 20000,
   686  				Hard: 40000,
   687  			},
   688  		},
   689  		User: "someone",
   690  		Volumes: []string{
   691  			"/var/lib/mysql",
   692  			"/opt/data:/var/lib/mysql",
   693  			fmt.Sprintf("%s:/code", workingDir),
   694  			fmt.Sprintf("%s/static:/var/www/html", workingDir),
   695  			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
   696  			"datavolume:/var/lib/mysql",
   697  		},
   698  		WorkingDir: "/code",
   699  	}
   700  
   701  	assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
   702  
   703  	expectedNetworkConfig := map[string]types.NetworkConfig{
   704  		"some-network": {},
   705  
   706  		"other-network": {
   707  			Driver: "overlay",
   708  			DriverOpts: map[string]string{
   709  				"foo": "bar",
   710  				"baz": "1",
   711  			},
   712  			Ipam: types.IPAMConfig{
   713  				Driver: "overlay",
   714  				Config: []*types.IPAMPool{
   715  					{Subnet: "172.16.238.0/24"},
   716  					{Subnet: "2001:3984:3989::/64"},
   717  				},
   718  			},
   719  		},
   720  
   721  		"external-network": {
   722  			External: types.External{
   723  				Name:     "external-network",
   724  				External: true,
   725  			},
   726  		},
   727  
   728  		"other-external-network": {
   729  			External: types.External{
   730  				Name:     "my-cool-network",
   731  				External: true,
   732  			},
   733  		},
   734  	}
   735  
   736  	assert.Equal(t, expectedNetworkConfig, config.Networks)
   737  
   738  	expectedVolumeConfig := map[string]types.VolumeConfig{
   739  		"some-volume": {},
   740  		"other-volume": {
   741  			Driver: "flocker",
   742  			DriverOpts: map[string]string{
   743  				"foo": "bar",
   744  				"baz": "1",
   745  			},
   746  		},
   747  		"external-volume": {
   748  			External: types.External{
   749  				Name:     "external-volume",
   750  				External: true,
   751  			},
   752  		},
   753  		"other-external-volume": {
   754  			External: types.External{
   755  				Name:     "my-cool-volume",
   756  				External: true,
   757  			},
   758  		},
   759  	}
   760  
   761  	assert.Equal(t, expectedVolumeConfig, config.Volumes)
   762  }
   763  
   764  func loadYAML(yaml string) (*types.Config, error) {
   765  	dict, err := ParseYAML([]byte(yaml))
   766  	if err != nil {
   767  		return nil, err
   768  	}
   769  
   770  	return Load(buildConfigDetails(dict))
   771  }
   772  
   773  func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
   774  	sort.Sort(servicesByName(services))
   775  	return services
   776  }
   777  
   778  type servicesByName []types.ServiceConfig
   779  
   780  func (sbn servicesByName) Len() int           { return len(sbn) }
   781  func (sbn servicesByName) Swap(i, j int)      { sbn[i], sbn[j] = sbn[j], sbn[i] }
   782  func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }