github.com/ali-iotechsys/cli@v20.10.0+incompatible/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/v3/assert"
    16  	is "gotest.tools/v3/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.8"
   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        placement:
   608          max_replicas_per_node: $theint
   609      ports:
   610        - $theint
   611        - "34567"
   612        - target: $theint
   613          published: $theint
   614      ulimits:
   615        nproc: $theint
   616        nofile:
   617          hard: $theint
   618          soft: $theint
   619      privileged: $thebool
   620      read_only: $thebool
   621      stdin_open: ${thebool}
   622      tty: $thebool
   623      volumes:
   624        - source: data
   625          type: volume
   626          read_only: $thebool
   627          volume:
   628            nocopy: $thebool
   629  
   630  configs:
   631    appconfig:
   632      external: $thebool
   633  secrets:
   634    super:
   635      external: $thebool
   636  volumes:
   637    data:
   638      external: $thebool
   639  networks:
   640    front:
   641      external: $thebool
   642      internal: $thebool
   643      attachable: $thebool
   644  
   645  `))
   646  	assert.NilError(t, err)
   647  	env := map[string]string{
   648  		"theint":   "555",
   649  		"thefloat": "3.14",
   650  		"thebool":  "true",
   651  	}
   652  
   653  	config, err := Load(buildConfigDetails(dict, env))
   654  	assert.NilError(t, err)
   655  	expected := &types.Config{
   656  		Filename: "filename.yml",
   657  		Version:  "3.8",
   658  		Services: []types.ServiceConfig{
   659  			{
   660  				Name: "web",
   661  				Configs: []types.ServiceConfigObjConfig{
   662  					{
   663  						Source: "appconfig",
   664  						Mode:   uint32Ptr(555),
   665  					},
   666  				},
   667  				Secrets: []types.ServiceSecretConfig{
   668  					{
   669  						Source: "super",
   670  						Mode:   uint32Ptr(555),
   671  					},
   672  				},
   673  				HealthCheck: &types.HealthCheckConfig{
   674  					Retries: uint64Ptr(555),
   675  					Disable: true,
   676  				},
   677  				Deploy: types.DeployConfig{
   678  					Replicas: uint64Ptr(555),
   679  					UpdateConfig: &types.UpdateConfig{
   680  						Parallelism:     uint64Ptr(555),
   681  						MaxFailureRatio: 3.14,
   682  					},
   683  					RollbackConfig: &types.UpdateConfig{
   684  						Parallelism:     uint64Ptr(555),
   685  						MaxFailureRatio: 3.14,
   686  					},
   687  					RestartPolicy: &types.RestartPolicy{
   688  						MaxAttempts: uint64Ptr(555),
   689  					},
   690  					Placement: types.Placement{
   691  						MaxReplicas: 555,
   692  					},
   693  				},
   694  				Ports: []types.ServicePortConfig{
   695  					{Target: 555, Mode: "ingress", Protocol: "tcp"},
   696  					{Target: 34567, Mode: "ingress", Protocol: "tcp"},
   697  					{Target: 555, Published: 555},
   698  				},
   699  				Ulimits: map[string]*types.UlimitsConfig{
   700  					"nproc":  {Single: 555},
   701  					"nofile": {Hard: 555, Soft: 555},
   702  				},
   703  				Privileged: true,
   704  				ReadOnly:   true,
   705  				StdinOpen:  true,
   706  				Tty:        true,
   707  				Volumes: []types.ServiceVolumeConfig{
   708  					{
   709  						Source:   "data",
   710  						Type:     "volume",
   711  						ReadOnly: true,
   712  						Volume:   &types.ServiceVolumeVolume{NoCopy: true},
   713  					},
   714  				},
   715  				Environment: types.MappingWithEquals{},
   716  			},
   717  		},
   718  		Configs: map[string]types.ConfigObjConfig{
   719  			"appconfig": {External: types.External{External: true}, Name: "appconfig"},
   720  		},
   721  		Secrets: map[string]types.SecretConfig{
   722  			"super": {External: types.External{External: true}, Name: "super"},
   723  		},
   724  		Volumes: map[string]types.VolumeConfig{
   725  			"data": {External: types.External{External: true}, Name: "data"},
   726  		},
   727  		Networks: map[string]types.NetworkConfig{
   728  			"front": {
   729  				External:   types.External{External: true},
   730  				Name:       "front",
   731  				Internal:   true,
   732  				Attachable: true,
   733  			},
   734  		},
   735  	}
   736  
   737  	assert.Check(t, is.DeepEqual(expected, config))
   738  }
   739  
   740  func TestUnsupportedProperties(t *testing.T) {
   741  	dict, err := ParseYAML([]byte(`
   742  version: "3"
   743  services:
   744    web:
   745      image: web
   746      build:
   747       context: ./web
   748      links:
   749        - bar
   750      pid: host
   751    db:
   752      image: db
   753      build:
   754       context: ./db
   755  `))
   756  	assert.NilError(t, err)
   757  
   758  	configDetails := buildConfigDetails(dict, nil)
   759  
   760  	_, err = Load(configDetails)
   761  	assert.NilError(t, err)
   762  
   763  	unsupported := GetUnsupportedProperties(dict)
   764  	assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported))
   765  }
   766  
   767  func TestDiscardEnvFileOption(t *testing.T) {
   768  	dict, err := ParseYAML([]byte(`version: "3"
   769  services:
   770    web:
   771      image: nginx
   772      env_file:
   773       - example1.env
   774       - example2.env
   775  `))
   776  	expectedEnvironmentMap := types.MappingWithEquals{
   777  		"FOO": strPtr("foo_from_env_file"),
   778  		"BAZ": strPtr("baz_from_env_file"),
   779  		"BAR": strPtr("bar_from_env_file_2"), // Original value is overwritten by example2.env
   780  		"QUX": strPtr("quz_from_env_file_2"),
   781  	}
   782  	assert.NilError(t, err)
   783  	configDetails := buildConfigDetails(dict, nil)
   784  
   785  	// Default behavior keeps the `env_file` entries
   786  	configWithEnvFiles, err := Load(configDetails)
   787  	assert.NilError(t, err)
   788  	assert.DeepEqual(t, configWithEnvFiles.Services[0].EnvFile, types.StringList{"example1.env",
   789  		"example2.env"})
   790  	assert.DeepEqual(t, configWithEnvFiles.Services[0].Environment, expectedEnvironmentMap)
   791  
   792  	// Custom behavior removes the `env_file` entries
   793  	configWithoutEnvFiles, err := Load(configDetails, WithDiscardEnvFiles)
   794  	assert.NilError(t, err)
   795  	assert.DeepEqual(t, configWithoutEnvFiles.Services[0].EnvFile, types.StringList(nil))
   796  	assert.DeepEqual(t, configWithoutEnvFiles.Services[0].Environment, expectedEnvironmentMap)
   797  }
   798  
   799  func TestBuildProperties(t *testing.T) {
   800  	dict, err := ParseYAML([]byte(`
   801  version: "3"
   802  services:
   803    web:
   804      image: web
   805      build: .
   806      links:
   807        - bar
   808    db:
   809      image: db
   810      build:
   811       context: ./db
   812  `))
   813  	assert.NilError(t, err)
   814  	configDetails := buildConfigDetails(dict, nil)
   815  	_, err = Load(configDetails)
   816  	assert.NilError(t, err)
   817  }
   818  
   819  func TestDeprecatedProperties(t *testing.T) {
   820  	dict, err := ParseYAML([]byte(`
   821  version: "3"
   822  services:
   823    web:
   824      image: web
   825      container_name: web
   826    db:
   827      image: db
   828      container_name: db
   829      expose: ["5434"]
   830  `))
   831  	assert.NilError(t, err)
   832  
   833  	configDetails := buildConfigDetails(dict, nil)
   834  
   835  	_, err = Load(configDetails)
   836  	assert.NilError(t, err)
   837  
   838  	deprecated := GetDeprecatedProperties(dict)
   839  	assert.Check(t, is.Len(deprecated, 2))
   840  	assert.Check(t, is.Contains(deprecated, "container_name"))
   841  	assert.Check(t, is.Contains(deprecated, "expose"))
   842  }
   843  
   844  func TestForbiddenProperties(t *testing.T) {
   845  	_, err := loadYAML(`
   846  version: "3"
   847  services:
   848    foo:
   849      image: busybox
   850      volumes:
   851        - /data
   852      volume_driver: some-driver
   853    bar:
   854      extends:
   855        service: foo
   856  `)
   857  
   858  	assert.ErrorType(t, err, reflect.TypeOf(&ForbiddenPropertiesError{}))
   859  
   860  	props := err.(*ForbiddenPropertiesError).Properties
   861  	assert.Check(t, is.Len(props, 2))
   862  	assert.Check(t, is.Contains(props, "volume_driver"))
   863  	assert.Check(t, is.Contains(props, "extends"))
   864  }
   865  
   866  func TestInvalidResource(t *testing.T) {
   867  	_, err := loadYAML(`
   868          version: "3"
   869          services:
   870            foo:
   871              image: busybox
   872              deploy:
   873                resources:
   874                  impossible:
   875                    x: 1
   876  `)
   877  	assert.ErrorContains(t, err, "Additional property impossible is not allowed")
   878  }
   879  
   880  func TestInvalidExternalAndDriverCombination(t *testing.T) {
   881  	_, err := loadYAML(`
   882  version: "3"
   883  volumes:
   884    external_volume:
   885      external: true
   886      driver: foobar
   887  `)
   888  
   889  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver\" specified for volume")
   890  	assert.ErrorContains(t, err, "external_volume")
   891  }
   892  
   893  func TestInvalidExternalAndDirverOptsCombination(t *testing.T) {
   894  	_, err := loadYAML(`
   895  version: "3"
   896  volumes:
   897    external_volume:
   898      external: true
   899      driver_opts:
   900        beep: boop
   901  `)
   902  
   903  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver_opts\" specified for volume")
   904  	assert.ErrorContains(t, err, "external_volume")
   905  }
   906  
   907  func TestInvalidExternalAndLabelsCombination(t *testing.T) {
   908  	_, err := loadYAML(`
   909  version: "3"
   910  volumes:
   911    external_volume:
   912      external: true
   913      labels:
   914        - beep=boop
   915  `)
   916  
   917  	assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"labels\" specified for volume")
   918  	assert.ErrorContains(t, err, "external_volume")
   919  }
   920  
   921  func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) {
   922  	_, err := loadYAML(`
   923  version: "3.4"
   924  volumes:
   925    external_volume:
   926      name: user_specified_name
   927      external:
   928        name: external_name
   929  `)
   930  
   931  	assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name")
   932  	assert.ErrorContains(t, err, "external_volume")
   933  }
   934  
   935  func durationPtr(value time.Duration) *types.Duration {
   936  	result := types.Duration(value)
   937  	return &result
   938  }
   939  
   940  func uint64Ptr(value uint64) *uint64 {
   941  	return &value
   942  }
   943  
   944  func uint32Ptr(value uint32) *uint32 {
   945  	return &value
   946  }
   947  
   948  func TestFullExample(t *testing.T) {
   949  	bytes, err := ioutil.ReadFile("full-example.yml")
   950  	assert.NilError(t, err)
   951  
   952  	homeDir := "/home/foo"
   953  	env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
   954  	config, err := loadYAMLWithEnv(string(bytes), env)
   955  	assert.NilError(t, err)
   956  
   957  	workingDir, err := os.Getwd()
   958  	assert.NilError(t, err)
   959  
   960  	expectedConfig := fullExampleConfig(workingDir, homeDir)
   961  
   962  	assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services))
   963  	assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks))
   964  	assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes))
   965  	assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets))
   966  	assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs))
   967  	assert.Check(t, is.DeepEqual(expectedConfig.Extras, config.Extras))
   968  }
   969  
   970  func TestLoadTmpfsVolume(t *testing.T) {
   971  	config, err := loadYAML(`
   972  version: "3.6"
   973  services:
   974    tmpfs:
   975      image: nginx:latest
   976      volumes:
   977        - type: tmpfs
   978          target: /app
   979          tmpfs:
   980            size: 10000
   981  `)
   982  	assert.NilError(t, err)
   983  
   984  	expected := types.ServiceVolumeConfig{
   985  		Target: "/app",
   986  		Type:   "tmpfs",
   987  		Tmpfs: &types.ServiceVolumeTmpfs{
   988  			Size: int64(10000),
   989  		},
   990  	}
   991  
   992  	assert.Assert(t, is.Len(config.Services, 1))
   993  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
   994  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
   995  }
   996  
   997  func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) {
   998  	_, err := loadYAML(`
   999  version: "3.5"
  1000  services:
  1001    tmpfs:
  1002      image: nginx:latest
  1003      volumes:
  1004        - type: tmpfs
  1005          target: /app
  1006          tmpfs:
  1007            size: 10000
  1008  `)
  1009  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed")
  1010  }
  1011  
  1012  func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) {
  1013  	_, err := loadYAML(`
  1014  version: "3.5"
  1015  services:
  1016    tmpfs:
  1017      image: nginx:latest
  1018      volumes:
  1019        - type: bind
  1020          target: /app
  1021  `)
  1022  	assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`)
  1023  }
  1024  
  1025  func TestLoadBindMountSourceIsWindowsAbsolute(t *testing.T) {
  1026  	tests := []struct {
  1027  		doc      string
  1028  		yaml     string
  1029  		expected types.ServiceVolumeConfig
  1030  	}{
  1031  		{
  1032  			doc: "Z-drive lowercase",
  1033  			yaml: `
  1034  version: '3.3'
  1035  
  1036  services:
  1037    windows:
  1038      image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
  1039      volumes:
  1040        - type: bind
  1041          source: z:\
  1042          target: c:\data
  1043  `,
  1044  			expected: types.ServiceVolumeConfig{Type: "bind", Source: `z:\`, Target: `c:\data`},
  1045  		},
  1046  		{
  1047  			doc: "Z-drive uppercase",
  1048  			yaml: `
  1049  version: '3.3'
  1050  
  1051  services:
  1052    windows:
  1053      image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
  1054      volumes:
  1055        - type: bind
  1056          source: Z:\
  1057          target: C:\data
  1058  `,
  1059  			expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\`, Target: `C:\data`},
  1060  		},
  1061  		{
  1062  			doc: "Z-drive subdirectory",
  1063  			yaml: `
  1064  version: '3.3'
  1065  
  1066  services:
  1067    windows:
  1068      image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
  1069      volumes:
  1070        - type: bind
  1071          source: Z:\some-dir
  1072          target: C:\data
  1073  `,
  1074  			expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\some-dir`, Target: `C:\data`},
  1075  		},
  1076  		{
  1077  			doc: "forward-slashes",
  1078  			yaml: `
  1079  version: '3.3'
  1080  
  1081  services:
  1082    app:
  1083      image: app:latest
  1084      volumes:
  1085        - type: bind
  1086          source: /z/some-dir
  1087          target: /c/data
  1088  `,
  1089  			expected: types.ServiceVolumeConfig{Type: "bind", Source: `/z/some-dir`, Target: `/c/data`},
  1090  		},
  1091  	}
  1092  
  1093  	for _, tc := range tests {
  1094  		t.Run(tc.doc, func(t *testing.T) {
  1095  			config, err := loadYAML(tc.yaml)
  1096  			assert.NilError(t, err)
  1097  			assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1098  			assert.Check(t, is.DeepEqual(tc.expected, config.Services[0].Volumes[0]))
  1099  		})
  1100  	}
  1101  }
  1102  
  1103  func TestLoadBindMountWithSource(t *testing.T) {
  1104  	config, err := loadYAML(`
  1105  version: "3.5"
  1106  services:
  1107    bind:
  1108      image: nginx:latest
  1109      volumes:
  1110        - type: bind
  1111          target: /app
  1112          source: "."
  1113  `)
  1114  	assert.NilError(t, err)
  1115  
  1116  	workingDir, err := os.Getwd()
  1117  	assert.NilError(t, err)
  1118  
  1119  	expected := types.ServiceVolumeConfig{
  1120  		Type:   "bind",
  1121  		Source: workingDir,
  1122  		Target: "/app",
  1123  	}
  1124  
  1125  	assert.Assert(t, is.Len(config.Services, 1))
  1126  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1127  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
  1128  }
  1129  
  1130  func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) {
  1131  	config, err := loadYAML(`
  1132  version: "3.6"
  1133  services:
  1134    tmpfs:
  1135      image: nginx:latest
  1136      volumes:
  1137        - type: tmpfs
  1138          target: /app
  1139          tmpfs:
  1140            size: 0
  1141  `)
  1142  	assert.NilError(t, err)
  1143  
  1144  	expected := types.ServiceVolumeConfig{
  1145  		Target: "/app",
  1146  		Type:   "tmpfs",
  1147  		Tmpfs:  &types.ServiceVolumeTmpfs{},
  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 TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) {
  1156  	_, err := loadYAML(`
  1157  version: "3.6"
  1158  services:
  1159    tmpfs:
  1160      image: nginx:latest
  1161      volumes:
  1162        - type: tmpfs
  1163          target: /app
  1164          tmpfs:
  1165            size: -1
  1166  `)
  1167  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
  1168  }
  1169  
  1170  func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
  1171  	_, err := loadYAML(`
  1172  version: "3.6"
  1173  services:
  1174    tmpfs:
  1175      image: nginx:latest
  1176      volumes:
  1177        - type: tmpfs
  1178          target: /app
  1179          tmpfs:
  1180            size: 0.0001
  1181  `)
  1182  	assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer")
  1183  }
  1184  
  1185  func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
  1186  	sort.Slice(services, func(i, j int) bool {
  1187  		return services[i].Name < services[j].Name
  1188  	})
  1189  	return services
  1190  }
  1191  
  1192  func TestLoadAttachableNetwork(t *testing.T) {
  1193  	config, err := loadYAML(`
  1194  version: "3.2"
  1195  networks:
  1196    mynet1:
  1197      driver: overlay
  1198      attachable: true
  1199    mynet2:
  1200      driver: bridge
  1201  `)
  1202  	assert.NilError(t, err)
  1203  
  1204  	expected := map[string]types.NetworkConfig{
  1205  		"mynet1": {
  1206  			Driver:     "overlay",
  1207  			Attachable: true,
  1208  		},
  1209  		"mynet2": {
  1210  			Driver:     "bridge",
  1211  			Attachable: false,
  1212  		},
  1213  	}
  1214  
  1215  	assert.Check(t, is.DeepEqual(expected, config.Networks))
  1216  }
  1217  
  1218  func TestLoadExpandedPortFormat(t *testing.T) {
  1219  	config, err := loadYAML(`
  1220  version: "3.2"
  1221  services:
  1222    web:
  1223      image: busybox
  1224      ports:
  1225        - "80-82:8080-8082"
  1226        - "90-92:8090-8092/udp"
  1227        - "85:8500"
  1228        - 8600
  1229        - protocol: udp
  1230          target: 53
  1231          published: 10053
  1232        - mode: host
  1233          target: 22
  1234          published: 10022
  1235  `)
  1236  	assert.NilError(t, err)
  1237  
  1238  	assert.Check(t, is.Len(config.Services, 1))
  1239  	assert.Check(t, is.DeepEqual(samplePortsConfig, config.Services[0].Ports))
  1240  }
  1241  
  1242  func TestLoadExpandedMountFormat(t *testing.T) {
  1243  	config, err := loadYAML(`
  1244  version: "3.2"
  1245  services:
  1246    web:
  1247      image: busybox
  1248      volumes:
  1249        - type: volume
  1250          source: foo
  1251          target: /target
  1252          read_only: true
  1253  volumes:
  1254    foo: {}
  1255  `)
  1256  	assert.NilError(t, err)
  1257  
  1258  	expected := types.ServiceVolumeConfig{
  1259  		Type:     "volume",
  1260  		Source:   "foo",
  1261  		Target:   "/target",
  1262  		ReadOnly: true,
  1263  	}
  1264  
  1265  	assert.Assert(t, is.Len(config.Services, 1))
  1266  	assert.Check(t, is.Len(config.Services[0].Volumes, 1))
  1267  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0]))
  1268  }
  1269  
  1270  func TestLoadExtraHostsMap(t *testing.T) {
  1271  	config, err := loadYAML(`
  1272  version: "3.2"
  1273  services:
  1274    web:
  1275      image: busybox
  1276      extra_hosts:
  1277        "zulu": "162.242.195.82"
  1278        "alpha": "50.31.209.229"
  1279        "host.docker.internal": "host-gateway"
  1280  `)
  1281  	assert.NilError(t, err)
  1282  
  1283  	expected := types.HostsList{
  1284  		"alpha:50.31.209.229",
  1285  		"host.docker.internal:host-gateway",
  1286  		"zulu:162.242.195.82",
  1287  	}
  1288  
  1289  	assert.Assert(t, is.Len(config.Services, 1))
  1290  	assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts))
  1291  }
  1292  
  1293  func TestLoadExtraHostsList(t *testing.T) {
  1294  	config, err := loadYAML(`
  1295  version: "3.2"
  1296  services:
  1297    web:
  1298      image: busybox
  1299      extra_hosts:
  1300        - "zulu:162.242.195.82"
  1301        - "alpha:50.31.209.229"
  1302        - "zulu:ff02::1"
  1303        - "host.docker.internal:host-gateway"
  1304  `)
  1305  	assert.NilError(t, err)
  1306  
  1307  	expected := types.HostsList{
  1308  		"zulu:162.242.195.82",
  1309  		"alpha:50.31.209.229",
  1310  		"zulu:ff02::1",
  1311  		"host.docker.internal:host-gateway",
  1312  	}
  1313  
  1314  	assert.Assert(t, is.Len(config.Services, 1))
  1315  	assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts))
  1316  }
  1317  
  1318  func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
  1319  	buf, cleanup := patchLogrus()
  1320  	defer cleanup()
  1321  
  1322  	source := map[string]interface{}{
  1323  		"foo": map[string]interface{}{
  1324  			"external": map[string]interface{}{
  1325  				"name": "oops",
  1326  			},
  1327  		},
  1328  	}
  1329  	volumes, err := LoadVolumes(source, "3.4")
  1330  	assert.NilError(t, err)
  1331  	expected := map[string]types.VolumeConfig{
  1332  		"foo": {
  1333  			Name:     "oops",
  1334  			External: types.External{External: true},
  1335  		},
  1336  	}
  1337  	assert.Check(t, is.DeepEqual(expected, volumes))
  1338  	assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated"))
  1339  
  1340  }
  1341  
  1342  func patchLogrus() (*bytes.Buffer, func()) {
  1343  	buf := new(bytes.Buffer)
  1344  	out := logrus.StandardLogger().Out
  1345  	logrus.SetOutput(buf)
  1346  	return buf, func() { logrus.SetOutput(out) }
  1347  }
  1348  
  1349  func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) {
  1350  	buf, cleanup := patchLogrus()
  1351  	defer cleanup()
  1352  
  1353  	source := map[string]interface{}{
  1354  		"foo": map[string]interface{}{
  1355  			"external": map[string]interface{}{
  1356  				"name": "oops",
  1357  			},
  1358  		},
  1359  	}
  1360  	volumes, err := LoadVolumes(source, "3.3")
  1361  	assert.NilError(t, err)
  1362  	expected := map[string]types.VolumeConfig{
  1363  		"foo": {
  1364  			Name:     "oops",
  1365  			External: types.External{External: true},
  1366  		},
  1367  	}
  1368  	assert.Check(t, is.DeepEqual(expected, volumes))
  1369  	assert.Check(t, is.Equal("", buf.String()))
  1370  }
  1371  
  1372  func TestLoadV35(t *testing.T) {
  1373  	actual, err := loadYAML(`
  1374  version: "3.5"
  1375  services:
  1376    foo:
  1377      image: busybox
  1378      isolation: process
  1379  configs:
  1380    foo:
  1381      name: fooqux
  1382      external: true
  1383    bar:
  1384      name: barqux
  1385      file: ./example1.env
  1386  secrets:
  1387    foo:
  1388      name: fooqux
  1389      external: true
  1390    bar:
  1391      name: barqux
  1392      file: ./full-example.yml
  1393  `)
  1394  	assert.NilError(t, err)
  1395  	assert.Check(t, is.Len(actual.Services, 1))
  1396  	assert.Check(t, is.Len(actual.Secrets, 2))
  1397  	assert.Check(t, is.Len(actual.Configs, 2))
  1398  	assert.Check(t, is.Equal("process", actual.Services[0].Isolation))
  1399  }
  1400  
  1401  func TestLoadV35InvalidIsolation(t *testing.T) {
  1402  	// validation should be done only on the daemon side
  1403  	actual, err := loadYAML(`
  1404  version: "3.5"
  1405  services:
  1406    foo:
  1407      image: busybox
  1408      isolation: invalid
  1409  configs:
  1410    super:
  1411      external: true
  1412  `)
  1413  	assert.NilError(t, err)
  1414  	assert.Assert(t, is.Len(actual.Services, 1))
  1415  	assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation))
  1416  }
  1417  
  1418  func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) {
  1419  	_, err := loadYAML(`
  1420  version: "3.5"
  1421  secrets:
  1422    external_secret:
  1423      name: user_specified_name
  1424      external:
  1425        name: external_name
  1426  `)
  1427  
  1428  	assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name")
  1429  	assert.ErrorContains(t, err, "external_secret")
  1430  }
  1431  
  1432  func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
  1433  	buf, cleanup := patchLogrus()
  1434  	defer cleanup()
  1435  
  1436  	source := map[string]interface{}{
  1437  		"foo": map[string]interface{}{
  1438  			"external": map[string]interface{}{
  1439  				"name": "oops",
  1440  			},
  1441  		},
  1442  	}
  1443  	details := types.ConfigDetails{
  1444  		Version: "3.5",
  1445  	}
  1446  	secrets, err := LoadSecrets(source, details)
  1447  	assert.NilError(t, err)
  1448  	expected := map[string]types.SecretConfig{
  1449  		"foo": {
  1450  			Name:     "oops",
  1451  			External: types.External{External: true},
  1452  		},
  1453  	}
  1454  	assert.Check(t, is.DeepEqual(expected, secrets))
  1455  	assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated"))
  1456  }
  1457  
  1458  func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
  1459  	buf, cleanup := patchLogrus()
  1460  	defer cleanup()
  1461  
  1462  	source := map[string]interface{}{
  1463  		"foo": map[string]interface{}{
  1464  			"external": map[string]interface{}{
  1465  				"name": "oops",
  1466  			},
  1467  		},
  1468  	}
  1469  	networks, err := LoadNetworks(source, "3.5")
  1470  	assert.NilError(t, err)
  1471  	expected := map[string]types.NetworkConfig{
  1472  		"foo": {
  1473  			Name:     "oops",
  1474  			External: types.External{External: true},
  1475  		},
  1476  	}
  1477  	assert.Check(t, is.DeepEqual(expected, networks))
  1478  	assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated"))
  1479  
  1480  }
  1481  
  1482  func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
  1483  	buf, cleanup := patchLogrus()
  1484  	defer cleanup()
  1485  
  1486  	source := map[string]interface{}{
  1487  		"foo": map[string]interface{}{
  1488  			"external": map[string]interface{}{
  1489  				"name": "oops",
  1490  			},
  1491  		},
  1492  	}
  1493  	networks, err := LoadNetworks(source, "3.4")
  1494  	assert.NilError(t, err)
  1495  	expected := map[string]types.NetworkConfig{
  1496  		"foo": {
  1497  			Name:     "oops",
  1498  			External: types.External{External: true},
  1499  		},
  1500  	}
  1501  	assert.Check(t, is.DeepEqual(expected, networks))
  1502  	assert.Check(t, is.Equal("", buf.String()))
  1503  }
  1504  
  1505  func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) {
  1506  	_, err := loadYAML(`
  1507  version: "3.5"
  1508  networks:
  1509    foo:
  1510      name: user_specified_name
  1511      external:
  1512        name: external_name
  1513  `)
  1514  
  1515  	assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name")
  1516  	assert.ErrorContains(t, err, "foo")
  1517  }
  1518  
  1519  func TestLoadNetworkWithName(t *testing.T) {
  1520  	config, err := loadYAML(`
  1521  version: '3.5'
  1522  services:
  1523    hello-world:
  1524      image: redis:alpine
  1525      networks:
  1526        - network1
  1527        - network3
  1528  
  1529  networks:
  1530    network1:
  1531      name: network2
  1532    network3:
  1533  `)
  1534  	assert.NilError(t, err)
  1535  	expected := &types.Config{
  1536  		Filename: "filename.yml",
  1537  		Version:  "3.5",
  1538  		Services: types.Services{
  1539  			{
  1540  				Name:  "hello-world",
  1541  				Image: "redis:alpine",
  1542  				Networks: map[string]*types.ServiceNetworkConfig{
  1543  					"network1": nil,
  1544  					"network3": nil,
  1545  				},
  1546  			},
  1547  		},
  1548  		Networks: map[string]types.NetworkConfig{
  1549  			"network1": {Name: "network2"},
  1550  			"network3": {},
  1551  		},
  1552  	}
  1553  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1554  }
  1555  
  1556  func TestLoadInit(t *testing.T) {
  1557  	booleanTrue := true
  1558  	booleanFalse := false
  1559  
  1560  	var testcases = []struct {
  1561  		doc  string
  1562  		yaml string
  1563  		init *bool
  1564  	}{
  1565  		{
  1566  			doc: "no init defined",
  1567  			yaml: `
  1568  version: '3.7'
  1569  services:
  1570    foo:
  1571      image: alpine`,
  1572  		},
  1573  		{
  1574  			doc: "has true init",
  1575  			yaml: `
  1576  version: '3.7'
  1577  services:
  1578    foo:
  1579      image: alpine
  1580      init: true`,
  1581  			init: &booleanTrue,
  1582  		},
  1583  		{
  1584  			doc: "has false init",
  1585  			yaml: `
  1586  version: '3.7'
  1587  services:
  1588    foo:
  1589      image: alpine
  1590      init: false`,
  1591  			init: &booleanFalse,
  1592  		},
  1593  	}
  1594  	for _, testcase := range testcases {
  1595  		testcase := testcase
  1596  		t.Run(testcase.doc, func(t *testing.T) {
  1597  			config, err := loadYAML(testcase.yaml)
  1598  			assert.NilError(t, err)
  1599  			assert.Check(t, is.Len(config.Services, 1))
  1600  			assert.Check(t, is.DeepEqual(config.Services[0].Init, testcase.init))
  1601  		})
  1602  	}
  1603  }
  1604  
  1605  func TestLoadSysctls(t *testing.T) {
  1606  	config, err := loadYAML(`
  1607  version: "3.8"
  1608  services:
  1609    web:
  1610      image: busybox
  1611      sysctls:
  1612        - net.core.somaxconn=1024
  1613        - net.ipv4.tcp_syncookies=0
  1614        - testing.one.one=
  1615        - testing.one.two
  1616  `)
  1617  	assert.NilError(t, err)
  1618  
  1619  	expected := types.Mapping{
  1620  		"net.core.somaxconn":      "1024",
  1621  		"net.ipv4.tcp_syncookies": "0",
  1622  		"testing.one.one":         "",
  1623  		"testing.one.two":         "",
  1624  	}
  1625  
  1626  	assert.Assert(t, is.Len(config.Services, 1))
  1627  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))
  1628  
  1629  	config, err = loadYAML(`
  1630  version: "3.8"
  1631  services:
  1632    web:
  1633      image: busybox
  1634      sysctls:
  1635        net.core.somaxconn: 1024
  1636        net.ipv4.tcp_syncookies: 0
  1637        testing.one.one: ""
  1638        testing.one.two:
  1639  `)
  1640  	assert.NilError(t, err)
  1641  
  1642  	assert.Assert(t, is.Len(config.Services, 1))
  1643  	assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))
  1644  }
  1645  
  1646  func TestTransform(t *testing.T) {
  1647  	var source = []interface{}{
  1648  		"80-82:8080-8082",
  1649  		"90-92:8090-8092/udp",
  1650  		"85:8500",
  1651  		8600,
  1652  		map[string]interface{}{
  1653  			"protocol":  "udp",
  1654  			"target":    53,
  1655  			"published": 10053,
  1656  		},
  1657  		map[string]interface{}{
  1658  			"mode":      "host",
  1659  			"target":    22,
  1660  			"published": 10022,
  1661  		},
  1662  	}
  1663  	var ports []types.ServicePortConfig
  1664  	err := Transform(source, &ports)
  1665  	assert.NilError(t, err)
  1666  
  1667  	assert.Check(t, is.DeepEqual(samplePortsConfig, ports))
  1668  }
  1669  
  1670  func TestLoadTemplateDriver(t *testing.T) {
  1671  	config, err := loadYAML(`
  1672  version: '3.8'
  1673  services:
  1674    hello-world:
  1675      image: redis:alpine
  1676      secrets:
  1677        - secret
  1678      configs:
  1679        - config
  1680  
  1681  configs:
  1682    config:
  1683      name: config
  1684      external: true
  1685      template_driver: config-driver
  1686  
  1687  secrets:
  1688    secret:
  1689      name: secret
  1690      external: true
  1691      template_driver: secret-driver
  1692  `)
  1693  	assert.NilError(t, err)
  1694  	expected := &types.Config{
  1695  		Filename: "filename.yml",
  1696  		Version:  "3.8",
  1697  		Services: types.Services{
  1698  			{
  1699  				Name:  "hello-world",
  1700  				Image: "redis:alpine",
  1701  				Configs: []types.ServiceConfigObjConfig{
  1702  					{
  1703  						Source: "config",
  1704  					},
  1705  				},
  1706  				Secrets: []types.ServiceSecretConfig{
  1707  					{
  1708  						Source: "secret",
  1709  					},
  1710  				},
  1711  			},
  1712  		},
  1713  		Configs: map[string]types.ConfigObjConfig{
  1714  			"config": {
  1715  				Name:           "config",
  1716  				External:       types.External{External: true},
  1717  				TemplateDriver: "config-driver",
  1718  			},
  1719  		},
  1720  		Secrets: map[string]types.SecretConfig{
  1721  			"secret": {
  1722  				Name:           "secret",
  1723  				External:       types.External{External: true},
  1724  				TemplateDriver: "secret-driver",
  1725  			},
  1726  		},
  1727  	}
  1728  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1729  }
  1730  
  1731  func TestLoadSecretDriver(t *testing.T) {
  1732  	config, err := loadYAML(`
  1733  version: '3.8'
  1734  services:
  1735    hello-world:
  1736      image: redis:alpine
  1737      secrets:
  1738        - secret
  1739      configs:
  1740        - config
  1741  
  1742  configs:
  1743    config:
  1744      name: config
  1745      external: true
  1746  
  1747  secrets:
  1748    secret:
  1749      name: secret
  1750      driver: secret-bucket
  1751      driver_opts:
  1752        OptionA: value for driver option A
  1753        OptionB: value for driver option B
  1754  `)
  1755  	assert.NilError(t, err)
  1756  	expected := &types.Config{
  1757  		Filename: "filename.yml",
  1758  		Version:  "3.8",
  1759  		Services: types.Services{
  1760  			{
  1761  				Name:  "hello-world",
  1762  				Image: "redis:alpine",
  1763  				Configs: []types.ServiceConfigObjConfig{
  1764  					{
  1765  						Source: "config",
  1766  					},
  1767  				},
  1768  				Secrets: []types.ServiceSecretConfig{
  1769  					{
  1770  						Source: "secret",
  1771  					},
  1772  				},
  1773  			},
  1774  		},
  1775  		Configs: map[string]types.ConfigObjConfig{
  1776  			"config": {
  1777  				Name:     "config",
  1778  				External: types.External{External: true},
  1779  			},
  1780  		},
  1781  		Secrets: map[string]types.SecretConfig{
  1782  			"secret": {
  1783  				Name:   "secret",
  1784  				Driver: "secret-bucket",
  1785  				DriverOpts: map[string]string{
  1786  					"OptionA": "value for driver option A",
  1787  					"OptionB": "value for driver option B",
  1788  				},
  1789  			},
  1790  		},
  1791  	}
  1792  	assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
  1793  }