github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/compose/loader/loader_test.go (about)

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