github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/loader/loader_test.go (about)

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