github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/cli/compose/loader/loader_test.go (about)

     1  package loader
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"os"
     7  	"reflect"
     8  	"sort"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/cli/cli/compose/types"
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/sirupsen/logrus"
    15  	"gotest.tools/assert"
    16  	is "gotest.tools/assert/cmp"
    17  )
    18  
    19  func buildConfigDetails(source map[string]interface{}, env map[string]string) types.ConfigDetails {
    20  	workingDir, err := os.Getwd()
    21  	if err != nil {
    22  		panic(err)
    23  	}
    24  
    25  	return types.ConfigDetails{
    26  		WorkingDir: workingDir,
    27  		ConfigFiles: []types.ConfigFile{
    28  			{Filename: "filename.yml", Config: source},
    29  		},
    30  		Environment: env,
    31  	}
    32  }
    33  
    34  func loadYAML(yaml string) (*types.Config, error) {
    35  	return loadYAMLWithEnv(yaml, nil)
    36  }
    37  
    38  func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) {
    39  	dict, err := ParseYAML([]byte(yaml))
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	return Load(buildConfigDetails(dict, env))
    45  }
    46  
    47  var sampleYAML = `
    48  version: "3"
    49  services:
    50    foo:
    51      image: busybox
    52      networks:
    53        with_me:
    54    bar:
    55      image: busybox
    56      environment:
    57        - FOO=1
    58      networks:
    59        - with_ipam
    60  volumes:
    61    hello:
    62      driver: default
    63      driver_opts:
    64        beep: boop
    65  networks:
    66    default:
    67      driver: bridge
    68      driver_opts:
    69        beep: boop
    70    with_ipam:
    71      ipam:
    72        driver: default
    73        config:
    74          - subnet: 172.28.0.0/16
    75  `
    76  
    77  var sampleDict = map[string]interface{}{
    78  	"version": "3",
    79  	"services": map[string]interface{}{
    80  		"foo": map[string]interface{}{
    81  			"image":    "busybox",
    82  			"networks": map[string]interface{}{"with_me": nil},
    83  		},
    84  		"bar": map[string]interface{}{
    85  			"image":       "busybox",
    86  			"environment": []interface{}{"FOO=1"},
    87  			"networks":    []interface{}{"with_ipam"},
    88  		},
    89  	},
    90  	"volumes": map[string]interface{}{
    91  		"hello": map[string]interface{}{
    92  			"driver": "default",
    93  			"driver_opts": map[string]interface{}{
    94  				"beep": "boop",
    95  			},
    96  		},
    97  	},
    98  	"networks": map[string]interface{}{
    99  		"default": map[string]interface{}{
   100  			"driver": "bridge",
   101  			"driver_opts": map[string]interface{}{
   102  				"beep": "boop",
   103  			},
   104  		},
   105  		"with_ipam": map[string]interface{}{
   106  			"ipam": map[string]interface{}{
   107  				"driver": "default",
   108  				"config": []interface{}{
   109  					map[string]interface{}{
   110  						"subnet": "172.28.0.0/16",
   111  					},
   112  				},
   113  			},
   114  		},
   115  	},
   116  }
   117  
   118  var samplePortsConfig = []types.ServicePortConfig{
   119  	{
   120  		Mode:      "ingress",
   121  		Target:    8080,
   122  		Published: 80,
   123  		Protocol:  "tcp",
   124  	},
   125  	{
   126  		Mode:      "ingress",
   127  		Target:    8081,
   128  		Published: 81,
   129  		Protocol:  "tcp",
   130  	},
   131  	{
   132  		Mode:      "ingress",
   133  		Target:    8082,
   134  		Published: 82,
   135  		Protocol:  "tcp",
   136  	},
   137  	{
   138  		Mode:      "ingress",
   139  		Target:    8090,
   140  		Published: 90,
   141  		Protocol:  "udp",
   142  	},
   143  	{
   144  		Mode:      "ingress",
   145  		Target:    8091,
   146  		Published: 91,
   147  		Protocol:  "udp",
   148  	},
   149  	{
   150  		Mode:      "ingress",
   151  		Target:    8092,
   152  		Published: 92,
   153  		Protocol:  "udp",
   154  	},
   155  	{
   156  		Mode:      "ingress",
   157  		Target:    8500,
   158  		Published: 85,
   159  		Protocol:  "tcp",
   160  	},
   161  	{
   162  		Mode:      "ingress",
   163  		Target:    8600,
   164  		Published: 0,
   165  		Protocol:  "tcp",
   166  	},
   167  	{
   168  		Target:    53,
   169  		Published: 10053,
   170  		Protocol:  "udp",
   171  	},
   172  	{
   173  		Mode:      "host",
   174  		Target:    22,
   175  		Published: 10022,
   176  	},
   177  }
   178  
   179  func strPtr(val string) *string {
   180  	return &val
   181  }
   182  
   183  var sampleConfig = types.Config{
   184  	Version: "3.0",
   185  	Services: []types.ServiceConfig{
   186  		{
   187  			Name:        "foo",
   188  			Image:       "busybox",
   189  			Environment: map[string]*string{},
   190  			Networks: map[string]*types.ServiceNetworkConfig{
   191  				"with_me": nil,
   192  			},
   193  		},
   194  		{
   195  			Name:        "bar",
   196  			Image:       "busybox",
   197  			Environment: map[string]*string{"FOO": strPtr("1")},
   198  			Networks: map[string]*types.ServiceNetworkConfig{
   199  				"with_ipam": nil,
   200  			},
   201  		},
   202  	},
   203  	Networks: map[string]types.NetworkConfig{
   204  		"default": {
   205  			Driver: "bridge",
   206  			DriverOpts: map[string]string{
   207  				"beep": "boop",
   208  			},
   209  		},
   210  		"with_ipam": {
   211  			Ipam: types.IPAMConfig{
   212  				Driver: "default",
   213  				Config: []*types.IPAMPool{
   214  					{
   215  						Subnet: "172.28.0.0/16",
   216  					},
   217  				},
   218  			},
   219  		},
   220  	},
   221  	Volumes: map[string]types.VolumeConfig{
   222  		"hello": {
   223  			Driver: "default",
   224  			DriverOpts: map[string]string{
   225  				"beep": "boop",
   226  			},
   227  		},
   228  	},
   229  }
   230  
   231  func TestParseYAML(t *testing.T) {
   232  	dict, err := ParseYAML([]byte(sampleYAML))
   233  	assert.NilError(t, err)
   234  	assert.Check(t, is.DeepEqual(sampleDict, dict))
   235  }
   236  
   237  func TestLoad(t *testing.T) {
   238  	actual, err := Load(buildConfigDetails(sampleDict, nil))
   239  	assert.NilError(t, err)
   240  	assert.Check(t, is.Equal(sampleConfig.Version, actual.Version))
   241  	assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services)))
   242  	assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks))
   243  	assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes))
   244  }
   245  
   246  func TestLoadExtras(t *testing.T) {
   247  	actual, err := loadYAML(`
   248  version: "3.7"
   249  services:
   250    foo:
   251      image: busybox
   252      x-foo: bar`)
   253  	assert.NilError(t, err)
   254  	assert.Check(t, is.Len(actual.Services, 1))
   255  	service := actual.Services[0]
   256  	assert.Check(t, is.Equal("busybox", service.Image))
   257  	extras := map[string]interface{}{
   258  		"x-foo": "bar",
   259  	}
   260  	assert.Check(t, is.DeepEqual(extras, service.Extras))
   261  }
   262  
   263  func TestLoadV31(t *testing.T) {
   264  	actual, err := loadYAML(`
   265  version: "3.1"
   266  services:
   267    foo:
   268      image: busybox
   269      secrets: [super]
   270  secrets:
   271    super:
   272      external: true
   273  `)
   274  	assert.NilError(t, err)
   275  	assert.Check(t, is.Len(actual.Services, 1))
   276  	assert.Check(t, is.Len(actual.Secrets, 1))
   277  }
   278  
   279  func TestLoadV33(t *testing.T) {
   280  	actual, err := loadYAML(`
   281  version: "3.3"
   282  services:
   283    foo:
   284      image: busybox
   285      credential_spec:
   286        file: "/foo"
   287      configs: [super]
   288  configs:
   289    super:
   290      external: true
   291  `)
   292  	assert.NilError(t, err)
   293  	assert.Assert(t, is.Len(actual.Services, 1))
   294  	assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.File, "/foo"))
   295  	assert.Assert(t, is.Len(actual.Configs, 1))
   296  }
   297  
   298  func TestLoadV38(t *testing.T) {
   299  	actual, err := loadYAML(`
   300  version: "3.8"
   301  services:
   302    foo:
   303      image: busybox
   304      credential_spec:
   305        config: "0bt9dmxjvjiqermk6xrop3ekq"
   306  `)
   307  	assert.NilError(t, err)
   308  	assert.Assert(t, is.Len(actual.Services, 1))
   309  	assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.Config, "0bt9dmxjvjiqermk6xrop3ekq"))
   310  }
   311  
   312  func TestParseAndLoad(t *testing.T) {
   313  	actual, err := loadYAML(sampleYAML)
   314  	assert.NilError(t, err)
   315  	assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services)))
   316  	assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks))
   317  	assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes))
   318  }
   319  
   320  func TestInvalidTopLevelObjectType(t *testing.T) {
   321  	_, err := loadYAML("1")
   322  	assert.ErrorContains(t, err, "Top-level object must be a mapping")
   323  
   324  	_, err = loadYAML("\"hello\"")
   325  	assert.ErrorContains(t, err, "Top-level object must be a mapping")
   326  
   327  	_, err = loadYAML("[\"hello\"]")
   328  	assert.ErrorContains(t, err, "Top-level object must be a mapping")
   329  }
   330  
   331  func TestNonStringKeys(t *testing.T) {
   332  	_, err := loadYAML(`
   333  version: "3"
   334  123:
   335    foo:
   336      image: busybox
   337  `)
   338  	assert.ErrorContains(t, err, "Non-string key at top level: 123")
   339  
   340  	_, err = loadYAML(`
   341  version: "3"
   342  services:
   343    foo:
   344      image: busybox
   345    123:
   346      image: busybox
   347  `)
   348  	assert.ErrorContains(t, err, "Non-string key in services: 123")
   349  
   350  	_, err = loadYAML(`
   351  version: "3"
   352  services:
   353    foo:
   354      image: busybox
   355  networks:
   356    default:
   357      ipam:
   358        config:
   359          - 123: oh dear
   360  `)
   361  	assert.ErrorContains(t, err, "Non-string key in networks.default.ipam.config[0]: 123")
   362  
   363  	_, err = loadYAML(`
   364  version: "3"
   365  services:
   366    dict-env:
   367      image: busybox
   368      environment:
   369        1: FOO
   370  `)
   371  	assert.ErrorContains(t, err, "Non-string key in services.dict-env.environment: 1")
   372  }
   373  
   374  func TestSupportedVersion(t *testing.T) {
   375  	_, err := loadYAML(`
   376  version: "3"
   377  services:
   378    foo:
   379      image: busybox
   380  `)
   381  	assert.NilError(t, err)
   382  
   383  	_, err = loadYAML(`
   384  version: "3.0"
   385  services:
   386    foo:
   387      image: busybox
   388  `)
   389  	assert.NilError(t, err)
   390  }
   391  
   392  func TestUnsupportedVersion(t *testing.T) {
   393  	_, err := loadYAML(`
   394  version: "2"
   395  services:
   396    foo:
   397      image: busybox
   398  `)
   399  	assert.ErrorContains(t, err, "version")
   400  
   401  	_, err = loadYAML(`
   402  version: "2.0"
   403  services:
   404    foo:
   405      image: busybox
   406  `)
   407  	assert.ErrorContains(t, err, "version")
   408  }
   409  
   410  func TestInvalidVersion(t *testing.T) {
   411  	_, err := loadYAML(`
   412  version: 3
   413  services:
   414    foo:
   415      image: busybox
   416  `)
   417  	assert.ErrorContains(t, err, "version must be a string")
   418  }
   419  
   420  func TestV1Unsupported(t *testing.T) {
   421  	_, err := loadYAML(`
   422  foo:
   423    image: busybox
   424  `)
   425  	assert.ErrorContains(t, err, "unsupported Compose file version: 1.0")
   426  }
   427  
   428  func TestNonMappingObject(t *testing.T) {
   429  	_, err := loadYAML(`
   430  version: "3"
   431  services:
   432    - foo:
   433        image: busybox
   434  `)
   435  	assert.ErrorContains(t, err, "services must be a mapping")
   436  
   437  	_, err = loadYAML(`
   438  version: "3"
   439  services:
   440    foo: busybox
   441  `)
   442  	assert.ErrorContains(t, err, "services.foo must be a mapping")
   443  
   444  	_, err = loadYAML(`
   445  version: "3"
   446  networks:
   447    - default:
   448        driver: bridge
   449  `)
   450  	assert.ErrorContains(t, err, "networks must be a mapping")
   451  
   452  	_, err = loadYAML(`
   453  version: "3"
   454  networks:
   455    default: bridge
   456  `)
   457  	assert.ErrorContains(t, err, "networks.default must be a mapping")
   458  
   459  	_, err = loadYAML(`
   460  version: "3"
   461  volumes:
   462    - data:
   463        driver: local
   464  `)
   465  	assert.ErrorContains(t, err, "volumes must be a mapping")
   466  
   467  	_, err = loadYAML(`
   468  version: "3"
   469  volumes:
   470    data: local
   471  `)
   472  	assert.ErrorContains(t, err, "volumes.data must be a mapping")
   473  }
   474  
   475  func TestNonStringImage(t *testing.T) {
   476  	_, err := loadYAML(`
   477  version: "3"
   478  services:
   479    foo:
   480      image: ["busybox", "latest"]
   481  `)
   482  	assert.ErrorContains(t, err, "services.foo.image must be a string")
   483  }
   484  
   485  func TestLoadWithEnvironment(t *testing.T) {
   486  	config, err := loadYAMLWithEnv(`
   487  version: "3"
   488  services:
   489    dict-env:
   490      image: busybox
   491      environment:
   492        FOO: "1"
   493        BAR: 2
   494        BAZ: 2.5
   495        QUX:
   496        QUUX:
   497    list-env:
   498      image: busybox
   499      environment:
   500        - FOO=1
   501        - BAR=2
   502        - BAZ=2.5
   503        - QUX=
   504        - QUUX
   505  `, map[string]string{"QUX": "qux"})
   506  	assert.NilError(t, err)
   507  
   508  	expected := types.MappingWithEquals{
   509  		"FOO":  strPtr("1"),
   510  		"BAR":  strPtr("2"),
   511  		"BAZ":  strPtr("2.5"),
   512  		"QUX":  strPtr("qux"),
   513  		"QUUX": nil,
   514  	}
   515  
   516  	assert.Check(t, is.Equal(2, len(config.Services)))
   517  
   518  	for _, service := range config.Services {
   519  		assert.Check(t, is.DeepEqual(expected, service.Environment))
   520  	}
   521  }
   522  
   523  func TestInvalidEnvironmentValue(t *testing.T) {
   524  	_, err := loadYAML(`
   525  version: "3"
   526  services:
   527    dict-env:
   528      image: busybox
   529      environment:
   530        FOO: ["1"]
   531  `)
   532  	assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number or null")
   533  }
   534  
   535  func TestInvalidEnvironmentObject(t *testing.T) {
   536  	_, err := loadYAML(`
   537  version: "3"
   538  services:
   539    dict-env:
   540      image: busybox
   541      environment: "FOO=1"
   542  `)
   543  	assert.ErrorContains(t, err, "services.dict-env.environment must be a mapping")
   544  }
   545  
   546  func TestLoadWithEnvironmentInterpolation(t *testing.T) {
   547  	home := "/home/foo"
   548  	config, err := loadYAMLWithEnv(`
   549  version: "3"
   550  services:
   551    test:
   552      image: busybox
   553      labels:
   554        - home1=$HOME
   555        - home2=${HOME}
   556        - nonexistent=$NONEXISTENT
   557        - default=${NONEXISTENT-default}
   558  networks:
   559    test:
   560      driver: $HOME
   561  volumes:
   562    test:
   563      driver: $HOME
   564  `, map[string]string{
   565  		"HOME": home,
   566  		"FOO":  "foo",
   567  	})
   568  
   569  	assert.NilError(t, err)
   570  
   571  	expectedLabels := types.Labels{
   572  		"home1":       home,
   573  		"home2":       home,
   574  		"nonexistent": "",
   575  		"default":     "default",
   576  	}
   577  
   578  	assert.Check(t, is.DeepEqual(expectedLabels, config.Services[0].Labels))
   579  	assert.Check(t, is.Equal(home, config.Networks["test"].Driver))
   580  	assert.Check(t, is.Equal(home, config.Volumes["test"].Driver))
   581  }
   582  
   583  func TestLoadWithInterpolationCastFull(t *testing.T) {
   584  	dict, err := ParseYAML([]byte(`
   585  version: "3.7"
   586  services:
   587    web:
   588      configs:
   589        - source: appconfig
   590          mode: $theint
   591      secrets:
   592        - source: super
   593          mode: $theint
   594      healthcheck:
   595        retries: ${theint}
   596        disable: $thebool
   597      deploy:
   598        replicas: $theint
   599        update_config:
   600          parallelism: $theint
   601          max_failure_ratio: $thefloat
   602        rollback_config:
   603          parallelism: $theint
   604          max_failure_ratio: $thefloat
   605        restart_policy:
   606          max_attempts: $theint
   607      ports:
   608        - $theint
   609        - "34567"
   610        - target: $theint
   611          published: $theint
   612      ulimits:
   613        nproc: $theint
   614        nofile:
   615          hard: $theint
   616          soft: $theint
   617      privileged: $thebool
   618      read_only: $thebool
   619      stdin_open: ${thebool}
   620      tty: $thebool
   621      volumes:
   622        - source: data
   623          type: volume
   624          read_only: $thebool
   625          volume:
   626            nocopy: $thebool
   627  
   628  configs:
   629    appconfig:
   630      external: $thebool
   631  secrets:
   632    super:
   633      external: $thebool
   634  volumes:
   635    data:
   636      external: $thebool
   637  networks:
   638    front:
   639      external: $thebool
   640      internal: $thebool
   641      attachable: $thebool
   642  
   643  `))
   644  	assert.NilError(t, err)
   645  	env := map[string]string{
   646  		"theint":   "555",
   647  		"thefloat": "3.14",
   648  		"thebool":  "true",
   649  	}
   650  
   651  	config, err := Load(buildConfigDetails(dict, env))
   652  	assert.NilError(t, err)
   653  	expected := &types.Config{
   654  		Filename: "filename.yml",
   655  		Version:  "3.7",
   656  		Services: []types.ServiceConfig{
   657  			{
   658  				Name: "web",
   659  				Configs: []types.ServiceConfigObjConfig{
   660  					{
   661  						Source: "appconfig",
   662  						Mode:   uint32Ptr(555),
   663  					},
   664  				},
   665  				Secrets: []types.ServiceSecretConfig{
   666  					{
   667  						Source: "super",
   668  						Mode:   uint32Ptr(555),
   669  					},
   670  				},
   671  				HealthCheck: &types.HealthCheckConfig{
   672  					Retries: uint64Ptr(555),
   673  					Disable: true,
   674  				},
   675  				Deploy: types.DeployConfig{
   676  					Replicas: uint64Ptr(555),
   677  					UpdateConfig: &types.UpdateConfig{
   678  						Parallelism:     uint64Ptr(555),
   679  						MaxFailureRatio: 3.14,
   680  					},
   681  					RollbackConfig: &types.UpdateConfig{
   682  						Parallelism:     uint64Ptr(555),
   683  						MaxFailureRatio: 3.14,
   684  					},
   685  					RestartPolicy: &types.RestartPolicy{
   686  						MaxAttempts: uint64Ptr(555),
   687  					},
   688  				},
   689  				Ports: []types.ServicePortConfig{
   690  					{Target: 555, Mode: "ingress", Protocol: "tcp"},
   691  					{Target: 34567, Mode: "ingress", Protocol: "tcp"},
   692  					{Target: 555, Published: 555},
   693  				},
   694  				Ulimits: map[string]*types.UlimitsConfig{
   695  					"nproc":  {Single: 555},
   696  					"nofile": {Hard: 555, Soft: 555},
   697  				},
   698  				Privileged: true,
   699  				ReadOnly:   true,
   700  				StdinOpen:  true,
   701  				Tty:        true,
   702  				Volumes: []types.ServiceVolumeConfig{
   703  					{
   704  						Source:   "data",
   705  						Type:     "volume",
   706  						ReadOnly: true,
   707  						Volume:   &types.ServiceVolumeVolume{NoCopy: true},
   708  					},
   709  				},
   710  				Environment: types.MappingWithEquals{},
   711  			},
   712  		},
   713  		Configs: map[string]types.ConfigObjConfig{
   714  			"appconfig": {External: types.External{External: true}, Name: "appconfig"},
   715  		},
   716  		Secrets: map[string]types.SecretConfig{
   717  			"super": {External: types.External{External: true}, Name: "super"},
   718  		},
   719  		Volumes: map[string]types.VolumeConfig{
   720  			"data": {External: types.External{External: true}, Name: "data"},
   721  		},
   722  		Networks: map[string]types.NetworkConfig{
   723  			"front": {
   724  				External:   types.External{External: true},
   725  				Name:       "front",
   726  				Internal:   true,
   727  				Attachable: true,
   728  			},
   729  		},
   730  	}
   731  
   732  	assert.Check(t, is.DeepEqual(expected, config))
   733  }
   734  
   735  func TestUnsupportedProperties(t *testing.T) {
   736  	dict, err := ParseYAML([]byte(`
   737  version: "3"
   738  services:
   739    web:
   740      image: web
   741      build:
   742       context: ./web
   743      links:
   744        - bar
   745      pid: host
   746    db:
   747      image: db
   748      build:
   749       context: ./db
   750  `))
   751  	assert.NilError(t, err)
   752  
   753  	configDetails := buildConfigDetails(dict, nil)
   754  
   755  	_, err = Load(configDetails)
   756  	assert.NilError(t, err)
   757  
   758  	unsupported := GetUnsupportedProperties(dict)
   759  	assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported))
   760  }
   761  
   762  func TestBuildProperties(t *testing.T) {
   763  	dict, err := ParseYAML([]byte(`
   764  version: "3"
   765  services:
   766    web:
   767      image: web
   768      build: .
   769      links:
   770        - bar
   771    db:
   772      image: db
   773      build:
   774       context: ./db
   775  `))
   776  	assert.NilError(t, err)
   777  	configDetails := buildConfigDetails(dict, nil)
   778  	_, err = Load(configDetails)
   779  	assert.NilError(t, err)
   780  }
   781  
   782  func TestDeprecatedProperties(t *testing.T) {
   783  	dict, err := ParseYAML([]byte(`
   784  version: "3"
   785  services:
   786    web:
   787      image: web
   788      container_name: web
   789    db:
   790      image: db
   791      container_name: db
   792      expose: ["5434"]
   793  `))
   794  	assert.NilError(t, err)
   795  
   796  	configDetails := buildConfigDetails(dict, nil)
   797  
   798  	_, err = Load(configDetails)
   799  	assert.NilError(t, err)
   800  
   801  	deprecated := GetDeprecatedProperties(dict)
   802  	assert.Check(t, is.Len(deprecated, 2))
   803  	assert.Check(t, is.Contains(deprecated, "container_name"))
   804  	assert.Check(t, is.Contains(deprecated, "expose"))
   805  }
   806  
   807  func TestForbiddenProperties(t *testing.T) {
   808  	_, err := loadYAML(`
   809  version: "3"
   810  services:
   811    foo:
   812      image: busybox
   813      volumes:
   814        - /data
   815      volume_driver: some-driver
   816    bar:
   817      extends:
   818        service: foo
   819  `)
   820  
   821  	assert.ErrorType(t, err, reflect.TypeOf(&ForbiddenPropertiesError{}))
   822  
   823  	props := err.(*ForbiddenPropertiesError).Properties
   824  	assert.Check(t, is.Len(props, 2))
   825  	assert.Check(t, is.Contains(props, "volume_driver"))
   826  	assert.Check(t, is.Contains(props, "extends"))
   827  }
   828  
   829  func TestInvalidResource(t *testing.T) {
   830  	_, err := loadYAML(`
   831          version: "3"
   832          services:
   833            foo:
   834              image: busybox
   835              deploy:
   836                resources:
   837                  impossible:
   838                    x: 1
   839  `)
   840  	assert.ErrorContains(t, err, "Additional property impossible is not allowed")
   841  }
   842  
   843  func TestInvalidExternalAndDriverCombination(t *testing.T) {
   844  	_, err := loadYAML(`
   845  version: "3"
   846  volumes:
   847    external_volume:
   848      external: true
   849      driver: foobar
   850  `)
   851  
   852  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver\" specified for volume")
   853  	assert.ErrorContains(t, err, "external_volume")
   854  }
   855  
   856  func TestInvalidExternalAndDirverOptsCombination(t *testing.T) {
   857  	_, err := loadYAML(`
   858  version: "3"
   859  volumes:
   860    external_volume:
   861      external: true
   862      driver_opts:
   863        beep: boop
   864  `)
   865  
   866  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver_opts\" specified for volume")
   867  	assert.ErrorContains(t, err, "external_volume")
   868  }
   869  
   870  func TestInvalidExternalAndLabelsCombination(t *testing.T) {
   871  	_, err := loadYAML(`
   872  version: "3"
   873  volumes:
   874    external_volume:
   875      external: true
   876      labels:
   877        - beep=boop
   878  `)
   879  
   880  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"labels\" specified for volume")
   881  	assert.ErrorContains(t, err, "external_volume")
   882  }
   883  
   884  func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) {
   885  	_, err := loadYAML(`
   886  version: "3.4"
   887  volumes:
   888    external_volume:
   889      name: user_specified_name
   890      external:
   891        name: external_name
   892  `)
   893  
   894  	assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name")
   895  	assert.ErrorContains(t, err, "external_volume")
   896  }
   897  
   898  func durationPtr(value time.Duration) *types.Duration {
   899  	result := types.Duration(value)
   900  	return &result
   901  }
   902  
   903  func uint64Ptr(value uint64) *uint64 {
   904  	return &value
   905  }
   906  
   907  func uint32Ptr(value uint32) *uint32 {
   908  	return &value
   909  }
   910  
   911  func TestFullExample(t *testing.T) {
   912  	bytes, err := ioutil.ReadFile("full-example.yml")
   913  	assert.NilError(t, err)
   914  
   915  	homeDir := "/home/foo"
   916  	env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
   917  	config, err := loadYAMLWithEnv(string(bytes), env)
   918  	assert.NilError(t, err)
   919  
   920  	workingDir, err := os.Getwd()
   921  	assert.NilError(t, err)
   922  
   923  	expectedConfig := fullExampleConfig(workingDir, homeDir)
   924  
   925  	assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services))
   926  	assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks))
   927  	assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes))
   928  	assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets))
   929  	assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs))
   930  	assert.Check(t, is.DeepEqual(expectedConfig.Extras, config.Extras))
   931  }
   932  
   933  func TestLoadTmpfsVolume(t *testing.T) {
   934  	config, err := loadYAML(`
   935  version: "3.6"
   936  services:
   937    tmpfs:
   938      image: nginx:latest
   939      volumes:
   940        - type: tmpfs
   941          target: /app
   942          tmpfs:
   943            size: 10000
   944  `)
   945  	assert.NilError(t, err)
   946  
   947  	expected := types.ServiceVolumeConfig{
   948  		Target: "/app",
   949  		Type:   "tmpfs",
   950  		Tmpfs: &types.ServiceVolumeTmpfs{
   951  			Size: int64(10000),
   952  		},
   953  	}
   954  
   955  	assert.Assert(t, is.Len(config.Services, 1))
   956  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
   957  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
   958  }
   959  
   960  func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) {
   961  	_, err := loadYAML(`
   962  version: "3.5"
   963  services:
   964    tmpfs:
   965      image: nginx:latest
   966      volumes:
   967        - type: tmpfs
   968          target: /app
   969          tmpfs:
   970            size: 10000
   971  `)
   972  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed")
   973  }
   974  
   975  func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) {
   976  	_, err := loadYAML(`
   977  version: "3.5"
   978  services:
   979    tmpfs:
   980      image: nginx:latest
   981      volumes:
   982        - type: bind
   983          target: /app
   984  `)
   985  	assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`)
   986  }
   987  
   988  func TestLoadBindMountWithSource(t *testing.T) {
   989  	config, err := loadYAML(`
   990  version: "3.5"
   991  services:
   992    bind:
   993      image: nginx:latest
   994      volumes:
   995        - type: bind
   996          target: /app
   997          source: "."
   998  `)
   999  	assert.NilError(t, err)
  1000  
  1001  	workingDir, err := os.Getwd()
  1002  	assert.NilError(t, err)
  1003  
  1004  	expected := types.ServiceVolumeConfig{
  1005  		Type:   "bind",
  1006  		Source: workingDir,
  1007  		Target: "/app",
  1008  	}
  1009  
  1010  	assert.Assert(t, is.Len(config.Services, 1))
  1011  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1012  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
  1013  }
  1014  
  1015  func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) {
  1016  	config, err := loadYAML(`
  1017  version: "3.6"
  1018  services:
  1019    tmpfs:
  1020      image: nginx:latest
  1021      volumes:
  1022        - type: tmpfs
  1023          target: /app
  1024          tmpfs:
  1025            size: 0
  1026  `)
  1027  	assert.NilError(t, err)
  1028  
  1029  	expected := types.ServiceVolumeConfig{
  1030  		Target: "/app",
  1031  		Type:   "tmpfs",
  1032  		Tmpfs:  &types.ServiceVolumeTmpfs{},
  1033  	}
  1034  
  1035  	assert.Assert(t, is.Len(config.Services, 1))
  1036  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1037  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
  1038  }
  1039  
  1040  func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) {
  1041  	_, err := loadYAML(`
  1042  version: "3.6"
  1043  services:
  1044    tmpfs:
  1045      image: nginx:latest
  1046      volumes:
  1047        - type: tmpfs
  1048          target: /app
  1049          tmpfs:
  1050            size: -1
  1051  `)
  1052  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
  1053  }
  1054  
  1055  func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
  1056  	_, err := loadYAML(`
  1057  version: "3.6"
  1058  services:
  1059    tmpfs:
  1060      image: nginx:latest
  1061      volumes:
  1062        - type: tmpfs
  1063          target: /app
  1064          tmpfs:
  1065            size: 0.0001
  1066  `)
  1067  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer")
  1068  }
  1069  
  1070  func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
  1071  	sort.Slice(services, func(i, j int) bool {
  1072  		return services[i].Name < services[j].Name
  1073  	})
  1074  	return services
  1075  }
  1076  
  1077  func TestLoadAttachableNetwork(t *testing.T) {
  1078  	config, err := loadYAML(`
  1079  version: "3.2"
  1080  networks:
  1081    mynet1:
  1082      driver: overlay
  1083      attachable: true
  1084    mynet2:
  1085      driver: bridge
  1086  `)
  1087  	assert.NilError(t, err)
  1088  
  1089  	expected := map[string]types.NetworkConfig{
  1090  		"mynet1": {
  1091  			Driver:     "overlay",
  1092  			Attachable: true,
  1093  		},
  1094  		"mynet2": {
  1095  			Driver:     "bridge",
  1096  			Attachable: false,
  1097  		},
  1098  	}
  1099  
  1100  	assert.Check(t, is.DeepEqual(expected, config.Networks))
  1101  }
  1102  
  1103  func TestLoadExpandedPortFormat(t *testing.T) {
  1104  	config, err := loadYAML(`
  1105  version: "3.2"
  1106  services:
  1107    web:
  1108      image: busybox
  1109      ports:
  1110        - "80-82:8080-8082"
  1111        - "90-92:8090-8092/udp"
  1112        - "85:8500"
  1113        - 8600
  1114        - protocol: udp
  1115          target: 53
  1116          published: 10053
  1117        - mode: host
  1118          target: 22
  1119          published: 10022
  1120  `)
  1121  	assert.NilError(t, err)
  1122  
  1123  	assert.Check(t, is.Len(config.Services, 1))
  1124  	assert.Check(t, is.DeepEqual(samplePortsConfig, config.Services[0].Ports))
  1125  }
  1126  
  1127  func TestLoadExpandedMountFormat(t *testing.T) {
  1128  	config, err := loadYAML(`
  1129  version: "3.2"
  1130  services:
  1131    web:
  1132      image: busybox
  1133      volumes:
  1134        - type: volume
  1135          source: foo
  1136          target: /target
  1137          read_only: true
  1138  volumes:
  1139    foo: {}
  1140  `)
  1141  	assert.NilError(t, err)
  1142  
  1143  	expected := types.ServiceVolumeConfig{
  1144  		Type:     "volume",
  1145  		Source:   "foo",
  1146  		Target:   "/target",
  1147  		ReadOnly: true,
  1148  	}
  1149  
  1150  	assert.Assert(t, is.Len(config.Services, 1))
  1151  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1152  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
  1153  }
  1154  
  1155  func TestLoadExtraHostsMap(t *testing.T) {
  1156  	config, err := loadYAML(`
  1157  version: "3.2"
  1158  services:
  1159    web:
  1160      image: busybox
  1161      extra_hosts:
  1162        "zulu": "162.242.195.82"
  1163        "alpha": "50.31.209.229"
  1164  `)
  1165  	assert.NilError(t, err)
  1166  
  1167  	expected := types.HostsList{
  1168  		"alpha:50.31.209.229",
  1169  		"zulu:162.242.195.82",
  1170  	}
  1171  
  1172  	assert.Assert(t, is.Len(config.Services, 1))
  1173  	assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts))
  1174  }
  1175  
  1176  func TestLoadExtraHostsList(t *testing.T) {
  1177  	config, err := loadYAML(`
  1178  version: "3.2"
  1179  services:
  1180    web:
  1181      image: busybox
  1182      extra_hosts:
  1183        - "zulu:162.242.195.82"
  1184        - "alpha:50.31.209.229"
  1185        - "zulu:ff02::1"
  1186  `)
  1187  	assert.NilError(t, err)
  1188  
  1189  	expected := types.HostsList{
  1190  		"zulu:162.242.195.82",
  1191  		"alpha:50.31.209.229",
  1192  		"zulu:ff02::1",
  1193  	}
  1194  
  1195  	assert.Assert(t, is.Len(config.Services, 1))
  1196  	assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts))
  1197  }
  1198  
  1199  func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
  1200  	buf, cleanup := patchLogrus()
  1201  	defer cleanup()
  1202  
  1203  	source := map[string]interface{}{
  1204  		"foo": map[string]interface{}{
  1205  			"external": map[string]interface{}{
  1206  				"name": "oops",
  1207  			},
  1208  		},
  1209  	}
  1210  	volumes, err := LoadVolumes(source, "3.4")
  1211  	assert.NilError(t, err)
  1212  	expected := map[string]types.VolumeConfig{
  1213  		"foo": {
  1214  			Name:     "oops",
  1215  			External: types.External{External: true},
  1216  		},
  1217  	}
  1218  	assert.Check(t, is.DeepEqual(expected, volumes))
  1219  	assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated"))
  1220  
  1221  }
  1222  
  1223  func patchLogrus() (*bytes.Buffer, func()) {
  1224  	buf := new(bytes.Buffer)
  1225  	out := logrus.StandardLogger().Out
  1226  	logrus.SetOutput(buf)
  1227  	return buf, func() { logrus.SetOutput(out) }
  1228  }
  1229  
  1230  func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) {
  1231  	buf, cleanup := patchLogrus()
  1232  	defer cleanup()
  1233  
  1234  	source := map[string]interface{}{
  1235  		"foo": map[string]interface{}{
  1236  			"external": map[string]interface{}{
  1237  				"name": "oops",
  1238  			},
  1239  		},
  1240  	}
  1241  	volumes, err := LoadVolumes(source, "3.3")
  1242  	assert.NilError(t, err)
  1243  	expected := map[string]types.VolumeConfig{
  1244  		"foo": {
  1245  			Name:     "oops",
  1246  			External: types.External{External: true},
  1247  		},
  1248  	}
  1249  	assert.Check(t, is.DeepEqual(expected, volumes))
  1250  	assert.Check(t, is.Equal("", buf.String()))
  1251  }
  1252  
  1253  func TestLoadV35(t *testing.T) {
  1254  	actual, err := loadYAML(`
  1255  version: "3.5"
  1256  services:
  1257    foo:
  1258      image: busybox
  1259      isolation: process
  1260  configs:
  1261    foo:
  1262      name: fooqux
  1263      external: true
  1264    bar:
  1265      name: barqux
  1266      file: ./example1.env
  1267  secrets:
  1268    foo:
  1269      name: fooqux
  1270      external: true
  1271    bar:
  1272      name: barqux
  1273      file: ./full-example.yml
  1274  `)
  1275  	assert.NilError(t, err)
  1276  	assert.Check(t, is.Len(actual.Services, 1))
  1277  	assert.Check(t, is.Len(actual.Secrets, 2))
  1278  	assert.Check(t, is.Len(actual.Configs, 2))
  1279  	assert.Check(t, is.Equal("process", actual.Services[0].Isolation))
  1280  }
  1281  
  1282  func TestLoadV35InvalidIsolation(t *testing.T) {
  1283  	// validation should be done only on the daemon side
  1284  	actual, err := loadYAML(`
  1285  version: "3.5"
  1286  services:
  1287    foo:
  1288      image: busybox
  1289      isolation: invalid
  1290  configs:
  1291    super:
  1292      external: true
  1293  `)
  1294  	assert.NilError(t, err)
  1295  	assert.Assert(t, is.Len(actual.Services, 1))
  1296  	assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation))
  1297  }
  1298  
  1299  func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) {
  1300  	_, err := loadYAML(`
  1301  version: "3.5"
  1302  secrets:
  1303    external_secret:
  1304      name: user_specified_name
  1305      external:
  1306        name: external_name
  1307  `)
  1308  
  1309  	assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name")
  1310  	assert.ErrorContains(t, err, "external_secret")
  1311  }
  1312  
  1313  func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
  1314  	buf, cleanup := patchLogrus()
  1315  	defer cleanup()
  1316  
  1317  	source := map[string]interface{}{
  1318  		"foo": map[string]interface{}{
  1319  			"external": map[string]interface{}{
  1320  				"name": "oops",
  1321  			},
  1322  		},
  1323  	}
  1324  	details := types.ConfigDetails{
  1325  		Version: "3.5",
  1326  	}
  1327  	secrets, err := LoadSecrets(source, details)
  1328  	assert.NilError(t, err)
  1329  	expected := map[string]types.SecretConfig{
  1330  		"foo": {
  1331  			Name:     "oops",
  1332  			External: types.External{External: true},
  1333  		},
  1334  	}
  1335  	assert.Check(t, is.DeepEqual(expected, secrets))
  1336  	assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated"))
  1337  }
  1338  
  1339  func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
  1340  	buf, cleanup := patchLogrus()
  1341  	defer cleanup()
  1342  
  1343  	source := map[string]interface{}{
  1344  		"foo": map[string]interface{}{
  1345  			"external": map[string]interface{}{
  1346  				"name": "oops",
  1347  			},
  1348  		},
  1349  	}
  1350  	networks, err := LoadNetworks(source, "3.5")
  1351  	assert.NilError(t, err)
  1352  	expected := map[string]types.NetworkConfig{
  1353  		"foo": {
  1354  			Name:     "oops",
  1355  			External: types.External{External: true},
  1356  		},
  1357  	}
  1358  	assert.Check(t, is.DeepEqual(expected, networks))
  1359  	assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated"))
  1360  
  1361  }
  1362  
  1363  func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
  1364  	buf, cleanup := patchLogrus()
  1365  	defer cleanup()
  1366  
  1367  	source := map[string]interface{}{
  1368  		"foo": map[string]interface{}{
  1369  			"external": map[string]interface{}{
  1370  				"name": "oops",
  1371  			},
  1372  		},
  1373  	}
  1374  	networks, err := LoadNetworks(source, "3.4")
  1375  	assert.NilError(t, err)
  1376  	expected := map[string]types.NetworkConfig{
  1377  		"foo": {
  1378  			Name:     "oops",
  1379  			External: types.External{External: true},
  1380  		},
  1381  	}
  1382  	assert.Check(t, is.DeepEqual(expected, networks))
  1383  	assert.Check(t, is.Equal("", buf.String()))
  1384  }
  1385  
  1386  func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) {
  1387  	_, err := loadYAML(`
  1388  version: "3.5"
  1389  networks:
  1390    foo:
  1391      name: user_specified_name
  1392      external:
  1393        name: external_name
  1394  `)
  1395  
  1396  	assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name")
  1397  	assert.ErrorContains(t, err, "foo")
  1398  }
  1399  
  1400  func TestLoadNetworkWithName(t *testing.T) {
  1401  	config, err := loadYAML(`
  1402  version: '3.5'
  1403  services:
  1404    hello-world:
  1405      image: redis:alpine
  1406      networks:
  1407        - network1
  1408        - network3
  1409  
  1410  networks:
  1411    network1:
  1412      name: network2
  1413    network3:
  1414  `)
  1415  	assert.NilError(t, err)
  1416  	expected := &types.Config{
  1417  		Filename: "filename.yml",
  1418  		Version:  "3.5",
  1419  		Services: types.Services{
  1420  			{
  1421  				Name:  "hello-world",
  1422  				Image: "redis:alpine",
  1423  				Networks: map[string]*types.ServiceNetworkConfig{
  1424  					"network1": nil,
  1425  					"network3": nil,
  1426  				},
  1427  			},
  1428  		},
  1429  		Networks: map[string]types.NetworkConfig{
  1430  			"network1": {Name: "network2"},
  1431  			"network3": {},
  1432  		},
  1433  	}
  1434  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1435  }
  1436  
  1437  func TestLoadInit(t *testing.T) {
  1438  	booleanTrue := true
  1439  	booleanFalse := false
  1440  
  1441  	var testcases = []struct {
  1442  		doc  string
  1443  		yaml string
  1444  		init *bool
  1445  	}{
  1446  		{
  1447  			doc: "no init defined",
  1448  			yaml: `
  1449  version: '3.7'
  1450  services:
  1451    foo:
  1452      image: alpine`,
  1453  		},
  1454  		{
  1455  			doc: "has true init",
  1456  			yaml: `
  1457  version: '3.7'
  1458  services:
  1459    foo:
  1460      image: alpine
  1461      init: true`,
  1462  			init: &booleanTrue,
  1463  		},
  1464  		{
  1465  			doc: "has false init",
  1466  			yaml: `
  1467  version: '3.7'
  1468  services:
  1469    foo:
  1470      image: alpine
  1471      init: false`,
  1472  			init: &booleanFalse,
  1473  		},
  1474  	}
  1475  	for _, testcase := range testcases {
  1476  		t.Run(testcase.doc, func(t *testing.T) {
  1477  			config, err := loadYAML(testcase.yaml)
  1478  			assert.NilError(t, err)
  1479  			assert.Check(t, is.Len(config.Services, 1))
  1480  			assert.Check(t, is.DeepEqual(config.Services[0].Init, testcase.init))
  1481  		})
  1482  	}
  1483  }
  1484  
  1485  func TestLoadSysctls(t *testing.T) {
  1486  	config, err := loadYAML(`
  1487  version: "3.8"
  1488  services:
  1489    web:
  1490      image: busybox
  1491      sysctls:
  1492        - net.core.somaxconn=1024
  1493        - net.ipv4.tcp_syncookies=0
  1494        - testing.one.one=
  1495        - testing.one.two
  1496  `)
  1497  	assert.NilError(t, err)
  1498  
  1499  	expected := types.Mapping{
  1500  		"net.core.somaxconn":      "1024",
  1501  		"net.ipv4.tcp_syncookies": "0",
  1502  		"testing.one.one":         "",
  1503  		"testing.one.two":         "",
  1504  	}
  1505  
  1506  	assert.Assert(t, is.Len(config.Services, 1))
  1507  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))
  1508  
  1509  	config, err = loadYAML(`
  1510  version: "3.8"
  1511  services:
  1512    web:
  1513      image: busybox
  1514      sysctls:
  1515        net.core.somaxconn: 1024
  1516        net.ipv4.tcp_syncookies: 0
  1517        testing.one.one: ""
  1518        testing.one.two:
  1519  `)
  1520  	assert.NilError(t, err)
  1521  
  1522  	assert.Assert(t, is.Len(config.Services, 1))
  1523  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))
  1524  }
  1525  
  1526  func TestTransform(t *testing.T) {
  1527  	var source = []interface{}{
  1528  		"80-82:8080-8082",
  1529  		"90-92:8090-8092/udp",
  1530  		"85:8500",
  1531  		8600,
  1532  		map[string]interface{}{
  1533  			"protocol":  "udp",
  1534  			"target":    53,
  1535  			"published": 10053,
  1536  		},
  1537  		map[string]interface{}{
  1538  			"mode":      "host",
  1539  			"target":    22,
  1540  			"published": 10022,
  1541  		},
  1542  	}
  1543  	var ports []types.ServicePortConfig
  1544  	err := Transform(source, &ports)
  1545  	assert.NilError(t, err)
  1546  
  1547  	assert.Check(t, is.DeepEqual(samplePortsConfig, ports))
  1548  }
  1549  
  1550  func TestLoadTemplateDriver(t *testing.T) {
  1551  	config, err := loadYAML(`
  1552  version: '3.8'
  1553  services:
  1554    hello-world:
  1555      image: redis:alpine
  1556      secrets:
  1557        - secret
  1558      configs:
  1559        - config
  1560  
  1561  configs:
  1562    config:
  1563      name: config
  1564      external: true
  1565      template_driver: config-driver
  1566  
  1567  secrets:
  1568    secret:
  1569      name: secret
  1570      external: true
  1571      template_driver: secret-driver
  1572  `)
  1573  	assert.NilError(t, err)
  1574  	expected := &types.Config{
  1575  		Filename: "filename.yml",
  1576  		Version:  "3.8",
  1577  		Services: types.Services{
  1578  			{
  1579  				Name:  "hello-world",
  1580  				Image: "redis:alpine",
  1581  				Configs: []types.ServiceConfigObjConfig{
  1582  					{
  1583  						Source: "config",
  1584  					},
  1585  				},
  1586  				Secrets: []types.ServiceSecretConfig{
  1587  					{
  1588  						Source: "secret",
  1589  					},
  1590  				},
  1591  			},
  1592  		},
  1593  		Configs: map[string]types.ConfigObjConfig{
  1594  			"config": {
  1595  				Name:           "config",
  1596  				External:       types.External{External: true},
  1597  				TemplateDriver: "config-driver",
  1598  			},
  1599  		},
  1600  		Secrets: map[string]types.SecretConfig{
  1601  			"secret": {
  1602  				Name:           "secret",
  1603  				External:       types.External{External: true},
  1604  				TemplateDriver: "secret-driver",
  1605  			},
  1606  		},
  1607  	}
  1608  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1609  }
  1610  
  1611  func TestLoadSecretDriver(t *testing.T) {
  1612  	config, err := loadYAML(`
  1613  version: '3.8'
  1614  services:
  1615    hello-world:
  1616      image: redis:alpine
  1617      secrets:
  1618        - secret
  1619      configs:
  1620        - config
  1621  
  1622  configs:
  1623    config:
  1624      name: config
  1625      external: true
  1626  
  1627  secrets:
  1628    secret:
  1629      name: secret
  1630      driver: secret-bucket
  1631      driver_opts:
  1632        OptionA: value for driver option A
  1633        OptionB: value for driver option B
  1634  `)
  1635  	assert.NilError(t, err)
  1636  	expected := &types.Config{
  1637  		Filename: "filename.yml",
  1638  		Version:  "3.8",
  1639  		Services: types.Services{
  1640  			{
  1641  				Name:  "hello-world",
  1642  				Image: "redis:alpine",
  1643  				Configs: []types.ServiceConfigObjConfig{
  1644  					{
  1645  						Source: "config",
  1646  					},
  1647  				},
  1648  				Secrets: []types.ServiceSecretConfig{
  1649  					{
  1650  						Source: "secret",
  1651  					},
  1652  				},
  1653  			},
  1654  		},
  1655  		Configs: map[string]types.ConfigObjConfig{
  1656  			"config": {
  1657  				Name:     "config",
  1658  				External: types.External{External: true},
  1659  			},
  1660  		},
  1661  		Secrets: map[string]types.SecretConfig{
  1662  			"secret": {
  1663  				Name:   "secret",
  1664  				Driver: "secret-bucket",
  1665  				DriverOpts: map[string]string{
  1666  					"OptionA": "value for driver option A",
  1667  					"OptionB": "value for driver option B",
  1668  				},
  1669  			},
  1670  		},
  1671  	}
  1672  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1673  }