github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/loader/merge_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  	"reflect"
     8  	"testing"
     9  
    10  	"dario.cat/mergo"
    11  	"github.com/khulnasoft/cli/cli/compose/types"
    12  	"gotest.tools/v3/assert"
    13  )
    14  
    15  func TestLoadTwoDifferentVersion(t *testing.T) {
    16  	configDetails := types.ConfigDetails{
    17  		ConfigFiles: []types.ConfigFile{
    18  			{Filename: "base.yml", Config: map[string]any{
    19  				"version": "3.1",
    20  			}},
    21  			{Filename: "override.yml", Config: map[string]any{
    22  				"version": "3.4",
    23  			}},
    24  		},
    25  	}
    26  	_, err := Load(configDetails)
    27  	assert.Error(t, err, "version mismatched between two composefiles : 3.1 and 3.4")
    28  }
    29  
    30  func TestLoadLogging(t *testing.T) {
    31  	loggingCases := []struct {
    32  		name            string
    33  		loggingBase     map[string]any
    34  		loggingOverride map[string]any
    35  		expected        *types.LoggingConfig
    36  	}{
    37  		{
    38  			name: "no_override_driver",
    39  			loggingBase: map[string]any{
    40  				"logging": map[string]any{
    41  					"driver": "json-file",
    42  					"options": map[string]any{
    43  						"frequency": "2000",
    44  						"timeout":   "23",
    45  					},
    46  				},
    47  			},
    48  			loggingOverride: map[string]any{
    49  				"logging": map[string]any{
    50  					"options": map[string]any{
    51  						"timeout":      "360",
    52  						"pretty-print": "on",
    53  					},
    54  				},
    55  			},
    56  			expected: &types.LoggingConfig{
    57  				Driver: "json-file",
    58  				Options: map[string]string{
    59  					"frequency":    "2000",
    60  					"timeout":      "360",
    61  					"pretty-print": "on",
    62  				},
    63  			},
    64  		},
    65  		{
    66  			name: "override_driver",
    67  			loggingBase: map[string]any{
    68  				"logging": map[string]any{
    69  					"driver": "json-file",
    70  					"options": map[string]any{
    71  						"frequency": "2000",
    72  						"timeout":   "23",
    73  					},
    74  				},
    75  			},
    76  			loggingOverride: map[string]any{
    77  				"logging": map[string]any{
    78  					"driver": "syslog",
    79  					"options": map[string]any{
    80  						"timeout":      "360",
    81  						"pretty-print": "on",
    82  					},
    83  				},
    84  			},
    85  			expected: &types.LoggingConfig{
    86  				Driver: "syslog",
    87  				Options: map[string]string{
    88  					"timeout":      "360",
    89  					"pretty-print": "on",
    90  				},
    91  			},
    92  		},
    93  		{
    94  			name: "no_base_driver",
    95  			loggingBase: map[string]any{
    96  				"logging": map[string]any{
    97  					"options": map[string]any{
    98  						"frequency": "2000",
    99  						"timeout":   "23",
   100  					},
   101  				},
   102  			},
   103  			loggingOverride: map[string]any{
   104  				"logging": map[string]any{
   105  					"driver": "json-file",
   106  					"options": map[string]any{
   107  						"timeout":      "360",
   108  						"pretty-print": "on",
   109  					},
   110  				},
   111  			},
   112  			expected: &types.LoggingConfig{
   113  				Driver: "json-file",
   114  				Options: map[string]string{
   115  					"frequency":    "2000",
   116  					"timeout":      "360",
   117  					"pretty-print": "on",
   118  				},
   119  			},
   120  		},
   121  		{
   122  			name: "no_driver",
   123  			loggingBase: map[string]any{
   124  				"logging": map[string]any{
   125  					"options": map[string]any{
   126  						"frequency": "2000",
   127  						"timeout":   "23",
   128  					},
   129  				},
   130  			},
   131  			loggingOverride: map[string]any{
   132  				"logging": map[string]any{
   133  					"options": map[string]any{
   134  						"timeout":      "360",
   135  						"pretty-print": "on",
   136  					},
   137  				},
   138  			},
   139  			expected: &types.LoggingConfig{
   140  				Options: map[string]string{
   141  					"frequency":    "2000",
   142  					"timeout":      "360",
   143  					"pretty-print": "on",
   144  				},
   145  			},
   146  		},
   147  		{
   148  			name: "no_override_options",
   149  			loggingBase: map[string]any{
   150  				"logging": map[string]any{
   151  					"driver": "json-file",
   152  					"options": map[string]any{
   153  						"frequency": "2000",
   154  						"timeout":   "23",
   155  					},
   156  				},
   157  			},
   158  			loggingOverride: map[string]any{
   159  				"logging": map[string]any{
   160  					"driver": "syslog",
   161  				},
   162  			},
   163  			expected: &types.LoggingConfig{
   164  				Driver: "syslog",
   165  			},
   166  		},
   167  		{
   168  			name:        "no_base",
   169  			loggingBase: map[string]any{},
   170  			loggingOverride: map[string]any{
   171  				"logging": map[string]any{
   172  					"driver": "json-file",
   173  					"options": map[string]any{
   174  						"frequency": "2000",
   175  					},
   176  				},
   177  			},
   178  			expected: &types.LoggingConfig{
   179  				Driver: "json-file",
   180  				Options: map[string]string{
   181  					"frequency": "2000",
   182  				},
   183  			},
   184  		},
   185  	}
   186  
   187  	for _, tc := range loggingCases {
   188  		t.Run(tc.name, func(t *testing.T) {
   189  			configDetails := types.ConfigDetails{
   190  				ConfigFiles: []types.ConfigFile{
   191  					{
   192  						Filename: "base.yml",
   193  						Config: map[string]any{
   194  							"version": "3.4",
   195  							"services": map[string]any{
   196  								"foo": tc.loggingBase,
   197  							},
   198  						},
   199  					},
   200  					{
   201  						Filename: "override.yml",
   202  						Config: map[string]any{
   203  							"version": "3.4",
   204  							"services": map[string]any{
   205  								"foo": tc.loggingOverride,
   206  							},
   207  						},
   208  					},
   209  				},
   210  			}
   211  			config, err := Load(configDetails)
   212  			assert.NilError(t, err)
   213  			assert.DeepEqual(t, &types.Config{
   214  				Filename: "base.yml",
   215  				Version:  "3.4",
   216  				Services: []types.ServiceConfig{
   217  					{
   218  						Name:        "foo",
   219  						Logging:     tc.expected,
   220  						Environment: types.MappingWithEquals{},
   221  					},
   222  				},
   223  				Networks: map[string]types.NetworkConfig{},
   224  				Volumes:  map[string]types.VolumeConfig{},
   225  				Secrets:  map[string]types.SecretConfig{},
   226  				Configs:  map[string]types.ConfigObjConfig{},
   227  			}, config)
   228  		})
   229  	}
   230  }
   231  
   232  func TestLoadMultipleServicePorts(t *testing.T) {
   233  	portsCases := []struct {
   234  		name         string
   235  		portBase     map[string]any
   236  		portOverride map[string]any
   237  		expected     []types.ServicePortConfig
   238  	}{
   239  		{
   240  			name: "no_override",
   241  			portBase: map[string]any{
   242  				"ports": []any{
   243  					"8080:80",
   244  				},
   245  			},
   246  			portOverride: map[string]any{},
   247  			expected: []types.ServicePortConfig{
   248  				{
   249  					Mode:      "ingress",
   250  					Published: 8080,
   251  					Target:    80,
   252  					Protocol:  "tcp",
   253  				},
   254  			},
   255  		},
   256  		{
   257  			name: "override_different_published",
   258  			portBase: map[string]any{
   259  				"ports": []any{
   260  					"8080:80",
   261  				},
   262  			},
   263  			portOverride: map[string]any{
   264  				"ports": []any{
   265  					"8081:80",
   266  				},
   267  			},
   268  			expected: []types.ServicePortConfig{
   269  				{
   270  					Mode:      "ingress",
   271  					Published: 8080,
   272  					Target:    80,
   273  					Protocol:  "tcp",
   274  				},
   275  				{
   276  					Mode:      "ingress",
   277  					Published: 8081,
   278  					Target:    80,
   279  					Protocol:  "tcp",
   280  				},
   281  			},
   282  		},
   283  		{
   284  			name: "override_same_published",
   285  			portBase: map[string]any{
   286  				"ports": []any{
   287  					"8080:80",
   288  				},
   289  			},
   290  			portOverride: map[string]any{
   291  				"ports": []any{
   292  					"8080:81",
   293  				},
   294  			},
   295  			expected: []types.ServicePortConfig{
   296  				{
   297  					Mode:      "ingress",
   298  					Published: 8080,
   299  					Target:    81,
   300  					Protocol:  "tcp",
   301  				},
   302  			},
   303  		},
   304  	}
   305  
   306  	for _, tc := range portsCases {
   307  		t.Run(tc.name, func(t *testing.T) {
   308  			configDetails := types.ConfigDetails{
   309  				ConfigFiles: []types.ConfigFile{
   310  					{
   311  						Filename: "base.yml",
   312  						Config: map[string]any{
   313  							"version": "3.4",
   314  							"services": map[string]any{
   315  								"foo": tc.portBase,
   316  							},
   317  						},
   318  					},
   319  					{
   320  						Filename: "override.yml",
   321  						Config: map[string]any{
   322  							"version": "3.4",
   323  							"services": map[string]any{
   324  								"foo": tc.portOverride,
   325  							},
   326  						},
   327  					},
   328  				},
   329  			}
   330  			config, err := Load(configDetails)
   331  			assert.NilError(t, err)
   332  			assert.DeepEqual(t, &types.Config{
   333  				Filename: "base.yml",
   334  				Version:  "3.4",
   335  				Services: []types.ServiceConfig{
   336  					{
   337  						Name:        "foo",
   338  						Ports:       tc.expected,
   339  						Environment: types.MappingWithEquals{},
   340  					},
   341  				},
   342  				Networks: map[string]types.NetworkConfig{},
   343  				Volumes:  map[string]types.VolumeConfig{},
   344  				Secrets:  map[string]types.SecretConfig{},
   345  				Configs:  map[string]types.ConfigObjConfig{},
   346  			}, config)
   347  		})
   348  	}
   349  }
   350  
   351  func TestLoadMultipleSecretsConfig(t *testing.T) {
   352  	portsCases := []struct {
   353  		name           string
   354  		secretBase     map[string]any
   355  		secretOverride map[string]any
   356  		expected       []types.ServiceSecretConfig
   357  	}{
   358  		{
   359  			name: "no_override",
   360  			secretBase: map[string]any{
   361  				"secrets": []any{
   362  					"my_secret",
   363  				},
   364  			},
   365  			secretOverride: map[string]any{},
   366  			expected: []types.ServiceSecretConfig{
   367  				{
   368  					Source: "my_secret",
   369  				},
   370  			},
   371  		},
   372  		{
   373  			name: "override_simple",
   374  			secretBase: map[string]any{
   375  				"secrets": []any{
   376  					"foo_secret",
   377  				},
   378  			},
   379  			secretOverride: map[string]any{
   380  				"secrets": []any{
   381  					"bar_secret",
   382  				},
   383  			},
   384  			expected: []types.ServiceSecretConfig{
   385  				{
   386  					Source: "bar_secret",
   387  				},
   388  				{
   389  					Source: "foo_secret",
   390  				},
   391  			},
   392  		},
   393  		{
   394  			name: "override_same_source",
   395  			secretBase: map[string]any{
   396  				"secrets": []any{
   397  					"foo_secret",
   398  					map[string]any{
   399  						"source": "bar_secret",
   400  						"target": "waw_secret",
   401  					},
   402  				},
   403  			},
   404  			secretOverride: map[string]any{
   405  				"secrets": []any{
   406  					map[string]any{
   407  						"source": "bar_secret",
   408  						"target": "bof_secret",
   409  					},
   410  					map[string]any{
   411  						"source": "baz_secret",
   412  						"target": "waw_secret",
   413  					},
   414  				},
   415  			},
   416  			expected: []types.ServiceSecretConfig{
   417  				{
   418  					Source: "bar_secret",
   419  					Target: "bof_secret",
   420  				},
   421  				{
   422  					Source: "baz_secret",
   423  					Target: "waw_secret",
   424  				},
   425  				{
   426  					Source: "foo_secret",
   427  				},
   428  			},
   429  		},
   430  	}
   431  
   432  	for _, tc := range portsCases {
   433  		t.Run(tc.name, func(t *testing.T) {
   434  			configDetails := types.ConfigDetails{
   435  				ConfigFiles: []types.ConfigFile{
   436  					{
   437  						Filename: "base.yml",
   438  						Config: map[string]any{
   439  							"version": "3.4",
   440  							"services": map[string]any{
   441  								"foo": tc.secretBase,
   442  							},
   443  						},
   444  					},
   445  					{
   446  						Filename: "override.yml",
   447  						Config: map[string]any{
   448  							"version": "3.4",
   449  							"services": map[string]any{
   450  								"foo": tc.secretOverride,
   451  							},
   452  						},
   453  					},
   454  				},
   455  			}
   456  			config, err := Load(configDetails)
   457  			assert.NilError(t, err)
   458  			assert.DeepEqual(t, &types.Config{
   459  				Filename: "base.yml",
   460  				Version:  "3.4",
   461  				Services: []types.ServiceConfig{
   462  					{
   463  						Name:        "foo",
   464  						Secrets:     tc.expected,
   465  						Environment: types.MappingWithEquals{},
   466  					},
   467  				},
   468  				Networks: map[string]types.NetworkConfig{},
   469  				Volumes:  map[string]types.VolumeConfig{},
   470  				Secrets:  map[string]types.SecretConfig{},
   471  				Configs:  map[string]types.ConfigObjConfig{},
   472  			}, config)
   473  		})
   474  	}
   475  }
   476  
   477  func TestLoadMultipleConfigobjsConfig(t *testing.T) {
   478  	portsCases := []struct {
   479  		name           string
   480  		configBase     map[string]any
   481  		configOverride map[string]any
   482  		expected       []types.ServiceConfigObjConfig
   483  	}{
   484  		{
   485  			name: "no_override",
   486  			configBase: map[string]any{
   487  				"configs": []any{
   488  					"my_config",
   489  				},
   490  			},
   491  			configOverride: map[string]any{},
   492  			expected: []types.ServiceConfigObjConfig{
   493  				{
   494  					Source: "my_config",
   495  				},
   496  			},
   497  		},
   498  		{
   499  			name: "override_simple",
   500  			configBase: map[string]any{
   501  				"configs": []any{
   502  					"foo_config",
   503  				},
   504  			},
   505  			configOverride: map[string]any{
   506  				"configs": []any{
   507  					"bar_config",
   508  				},
   509  			},
   510  			expected: []types.ServiceConfigObjConfig{
   511  				{
   512  					Source: "bar_config",
   513  				},
   514  				{
   515  					Source: "foo_config",
   516  				},
   517  			},
   518  		},
   519  		{
   520  			name: "override_same_source",
   521  			configBase: map[string]any{
   522  				"configs": []any{
   523  					"foo_config",
   524  					map[string]any{
   525  						"source": "bar_config",
   526  						"target": "waw_config",
   527  					},
   528  				},
   529  			},
   530  			configOverride: map[string]any{
   531  				"configs": []any{
   532  					map[string]any{
   533  						"source": "bar_config",
   534  						"target": "bof_config",
   535  					},
   536  					map[string]any{
   537  						"source": "baz_config",
   538  						"target": "waw_config",
   539  					},
   540  				},
   541  			},
   542  			expected: []types.ServiceConfigObjConfig{
   543  				{
   544  					Source: "bar_config",
   545  					Target: "bof_config",
   546  				},
   547  				{
   548  					Source: "baz_config",
   549  					Target: "waw_config",
   550  				},
   551  				{
   552  					Source: "foo_config",
   553  				},
   554  			},
   555  		},
   556  	}
   557  
   558  	for _, tc := range portsCases {
   559  		t.Run(tc.name, func(t *testing.T) {
   560  			configDetails := types.ConfigDetails{
   561  				ConfigFiles: []types.ConfigFile{
   562  					{
   563  						Filename: "base.yml",
   564  						Config: map[string]any{
   565  							"version": "3.4",
   566  							"services": map[string]any{
   567  								"foo": tc.configBase,
   568  							},
   569  						},
   570  					},
   571  					{
   572  						Filename: "override.yml",
   573  						Config: map[string]any{
   574  							"version": "3.4",
   575  							"services": map[string]any{
   576  								"foo": tc.configOverride,
   577  							},
   578  						},
   579  					},
   580  				},
   581  			}
   582  			config, err := Load(configDetails)
   583  			assert.NilError(t, err)
   584  			assert.DeepEqual(t, &types.Config{
   585  				Filename: "base.yml",
   586  				Version:  "3.4",
   587  				Services: []types.ServiceConfig{
   588  					{
   589  						Name:        "foo",
   590  						Configs:     tc.expected,
   591  						Environment: types.MappingWithEquals{},
   592  					},
   593  				},
   594  				Networks: map[string]types.NetworkConfig{},
   595  				Volumes:  map[string]types.VolumeConfig{},
   596  				Secrets:  map[string]types.SecretConfig{},
   597  				Configs:  map[string]types.ConfigObjConfig{},
   598  			}, config)
   599  		})
   600  	}
   601  }
   602  
   603  func TestLoadMultipleUlimits(t *testing.T) {
   604  	ulimitCases := []struct {
   605  		name           string
   606  		ulimitBase     map[string]any
   607  		ulimitOverride map[string]any
   608  		expected       map[string]*types.UlimitsConfig
   609  	}{
   610  		{
   611  			name: "no_override",
   612  			ulimitBase: map[string]any{
   613  				"ulimits": map[string]any{
   614  					"noproc": 65535,
   615  				},
   616  			},
   617  			ulimitOverride: map[string]any{},
   618  			expected: map[string]*types.UlimitsConfig{
   619  				"noproc": {
   620  					Single: 65535,
   621  				},
   622  			},
   623  		},
   624  		{
   625  			name: "override_simple",
   626  			ulimitBase: map[string]any{
   627  				"ulimits": map[string]any{
   628  					"noproc": 65535,
   629  				},
   630  			},
   631  			ulimitOverride: map[string]any{
   632  				"ulimits": map[string]any{
   633  					"noproc": 44444,
   634  				},
   635  			},
   636  			expected: map[string]*types.UlimitsConfig{
   637  				"noproc": {
   638  					Single: 44444,
   639  				},
   640  			},
   641  		},
   642  		{
   643  			name: "override_different_notation",
   644  			ulimitBase: map[string]any{
   645  				"ulimits": map[string]any{
   646  					"nofile": map[string]any{
   647  						"soft": 11111,
   648  						"hard": 99999,
   649  					},
   650  					"noproc": 44444,
   651  				},
   652  			},
   653  			ulimitOverride: map[string]any{
   654  				"ulimits": map[string]any{
   655  					"nofile": 55555,
   656  					"noproc": map[string]any{
   657  						"soft": 22222,
   658  						"hard": 33333,
   659  					},
   660  				},
   661  			},
   662  			expected: map[string]*types.UlimitsConfig{
   663  				"noproc": {
   664  					Soft: 22222,
   665  					Hard: 33333,
   666  				},
   667  				"nofile": {
   668  					Single: 55555,
   669  				},
   670  			},
   671  		},
   672  	}
   673  
   674  	for _, tc := range ulimitCases {
   675  		t.Run(tc.name, func(t *testing.T) {
   676  			configDetails := types.ConfigDetails{
   677  				ConfigFiles: []types.ConfigFile{
   678  					{
   679  						Filename: "base.yml",
   680  						Config: map[string]any{
   681  							"version": "3.4",
   682  							"services": map[string]any{
   683  								"foo": tc.ulimitBase,
   684  							},
   685  						},
   686  					},
   687  					{
   688  						Filename: "override.yml",
   689  						Config: map[string]any{
   690  							"version": "3.4",
   691  							"services": map[string]any{
   692  								"foo": tc.ulimitOverride,
   693  							},
   694  						},
   695  					},
   696  				},
   697  			}
   698  			config, err := Load(configDetails)
   699  			assert.NilError(t, err)
   700  			assert.DeepEqual(t, &types.Config{
   701  				Filename: "base.yml",
   702  				Version:  "3.4",
   703  				Services: []types.ServiceConfig{
   704  					{
   705  						Name:        "foo",
   706  						Ulimits:     tc.expected,
   707  						Environment: types.MappingWithEquals{},
   708  					},
   709  				},
   710  				Networks: map[string]types.NetworkConfig{},
   711  				Volumes:  map[string]types.VolumeConfig{},
   712  				Secrets:  map[string]types.SecretConfig{},
   713  				Configs:  map[string]types.ConfigObjConfig{},
   714  			}, config)
   715  		})
   716  	}
   717  }
   718  
   719  func TestLoadMultipleServiceNetworks(t *testing.T) {
   720  	networkCases := []struct {
   721  		name            string
   722  		networkBase     map[string]any
   723  		networkOverride map[string]any
   724  		expected        map[string]*types.ServiceNetworkConfig
   725  	}{
   726  		{
   727  			name: "no_override",
   728  			networkBase: map[string]any{
   729  				"networks": []any{
   730  					"net1",
   731  					"net2",
   732  				},
   733  			},
   734  			networkOverride: map[string]any{},
   735  			expected: map[string]*types.ServiceNetworkConfig{
   736  				"net1": nil,
   737  				"net2": nil,
   738  			},
   739  		},
   740  		{
   741  			name: "override_simple",
   742  			networkBase: map[string]any{
   743  				"networks": []any{
   744  					"net1",
   745  					"net2",
   746  				},
   747  			},
   748  			networkOverride: map[string]any{
   749  				"networks": []any{
   750  					"net1",
   751  					"net3",
   752  				},
   753  			},
   754  			expected: map[string]*types.ServiceNetworkConfig{
   755  				"net1": nil,
   756  				"net2": nil,
   757  				"net3": nil,
   758  			},
   759  		},
   760  		{
   761  			name: "override_with_aliases",
   762  			networkBase: map[string]any{
   763  				"networks": map[string]any{
   764  					"net1": map[string]any{
   765  						"aliases": []any{
   766  							"alias1",
   767  						},
   768  					},
   769  					"net2": nil,
   770  				},
   771  			},
   772  			networkOverride: map[string]any{
   773  				"networks": map[string]any{
   774  					"net1": map[string]any{
   775  						"aliases": []any{
   776  							"alias2",
   777  							"alias3",
   778  						},
   779  					},
   780  					"net3": map[string]any{},
   781  				},
   782  			},
   783  			expected: map[string]*types.ServiceNetworkConfig{
   784  				"net1": {
   785  					Aliases: []string{"alias2", "alias3"},
   786  				},
   787  				"net2": nil,
   788  				"net3": {},
   789  			},
   790  		},
   791  	}
   792  
   793  	for _, tc := range networkCases {
   794  		t.Run(tc.name, func(t *testing.T) {
   795  			configDetails := types.ConfigDetails{
   796  				ConfigFiles: []types.ConfigFile{
   797  					{
   798  						Filename: "base.yml",
   799  						Config: map[string]any{
   800  							"version": "3.4",
   801  							"services": map[string]any{
   802  								"foo": tc.networkBase,
   803  							},
   804  						},
   805  					},
   806  					{
   807  						Filename: "override.yml",
   808  						Config: map[string]any{
   809  							"version": "3.4",
   810  							"services": map[string]any{
   811  								"foo": tc.networkOverride,
   812  							},
   813  						},
   814  					},
   815  				},
   816  			}
   817  			config, err := Load(configDetails)
   818  			assert.NilError(t, err)
   819  			assert.DeepEqual(t, &types.Config{
   820  				Filename: "base.yml",
   821  				Version:  "3.4",
   822  				Services: []types.ServiceConfig{
   823  					{
   824  						Name:        "foo",
   825  						Networks:    tc.expected,
   826  						Environment: types.MappingWithEquals{},
   827  					},
   828  				},
   829  				Networks: map[string]types.NetworkConfig{},
   830  				Volumes:  map[string]types.VolumeConfig{},
   831  				Secrets:  map[string]types.SecretConfig{},
   832  				Configs:  map[string]types.ConfigObjConfig{},
   833  			}, config)
   834  		})
   835  	}
   836  }
   837  
   838  func TestLoadMultipleConfigs(t *testing.T) {
   839  	base := map[string]any{
   840  		"version": "3.4",
   841  		"services": map[string]any{
   842  			"foo": map[string]any{
   843  				"image": "foo",
   844  				"build": map[string]any{
   845  					"context":    ".",
   846  					"dockerfile": "bar.Dockerfile",
   847  				},
   848  				"ports": []any{
   849  					"8080:80",
   850  					"9090:90",
   851  				},
   852  				"labels": []any{
   853  					"foo=bar",
   854  				},
   855  				"cap_add": []any{
   856  					"NET_ADMIN",
   857  				},
   858  			},
   859  		},
   860  		"volumes":  map[string]any{},
   861  		"networks": map[string]any{},
   862  		"secrets":  map[string]any{},
   863  		"configs":  map[string]any{},
   864  	}
   865  	override := map[string]any{
   866  		"version": "3.4",
   867  		"services": map[string]any{
   868  			"foo": map[string]any{
   869  				"image": "baz",
   870  				"build": map[string]any{
   871  					"dockerfile": "foo.Dockerfile",
   872  					"args": []any{
   873  						"buildno=1",
   874  						"password=secret",
   875  					},
   876  				},
   877  				"ports": []any{
   878  					map[string]any{
   879  						"target":    81,
   880  						"published": 8080,
   881  					},
   882  				},
   883  				"labels": map[string]any{
   884  					"foo": "baz",
   885  				},
   886  				"cap_add": []any{
   887  					"SYS_ADMIN",
   888  				},
   889  			},
   890  			"bar": map[string]any{
   891  				"image": "bar",
   892  			},
   893  		},
   894  		"volumes":  map[string]any{},
   895  		"networks": map[string]any{},
   896  		"secrets":  map[string]any{},
   897  		"configs":  map[string]any{},
   898  	}
   899  	configDetails := types.ConfigDetails{
   900  		ConfigFiles: []types.ConfigFile{
   901  			{Filename: "base.yml", Config: base},
   902  			{Filename: "override.yml", Config: override},
   903  		},
   904  	}
   905  	config, err := Load(configDetails)
   906  	assert.NilError(t, err)
   907  	assert.DeepEqual(t, &types.Config{
   908  		Filename: "base.yml",
   909  		Version:  "3.4",
   910  		Services: []types.ServiceConfig{
   911  			{
   912  				Name:        "bar",
   913  				Image:       "bar",
   914  				Environment: types.MappingWithEquals{},
   915  			},
   916  			{
   917  				Name:  "foo",
   918  				Image: "baz",
   919  				Build: types.BuildConfig{
   920  					Context:    ".",
   921  					Dockerfile: "foo.Dockerfile",
   922  					Args: types.MappingWithEquals{
   923  						"buildno":  strPtr("1"),
   924  						"password": strPtr("secret"),
   925  					},
   926  				},
   927  				Ports: []types.ServicePortConfig{
   928  					{
   929  						Target:    81,
   930  						Published: 8080,
   931  					},
   932  					{
   933  						Mode:      "ingress",
   934  						Target:    90,
   935  						Published: 9090,
   936  						Protocol:  "tcp",
   937  					},
   938  				},
   939  				Labels: types.Labels{
   940  					"foo": "baz",
   941  				},
   942  				CapAdd:      []string{"NET_ADMIN", "SYS_ADMIN"},
   943  				Environment: types.MappingWithEquals{},
   944  			},
   945  		},
   946  		Networks: map[string]types.NetworkConfig{},
   947  		Volumes:  map[string]types.VolumeConfig{},
   948  		Secrets:  map[string]types.SecretConfig{},
   949  		Configs:  map[string]types.ConfigObjConfig{},
   950  	}, config)
   951  }
   952  
   953  // Issue#972
   954  func TestLoadMultipleNetworks(t *testing.T) {
   955  	base := map[string]any{
   956  		"version": "3.4",
   957  		"services": map[string]any{
   958  			"foo": map[string]any{
   959  				"image": "baz",
   960  			},
   961  		},
   962  		"volumes": map[string]any{},
   963  		"networks": map[string]any{
   964  			"hostnet": map[string]any{
   965  				"driver": "overlay",
   966  				"ipam": map[string]any{
   967  					"driver": "default",
   968  					"config": []any{
   969  						map[string]any{
   970  							"subnet": "10.0.0.0/20",
   971  						},
   972  					},
   973  				},
   974  			},
   975  		},
   976  		"secrets": map[string]any{},
   977  		"configs": map[string]any{},
   978  	}
   979  	override := map[string]any{
   980  		"version":  "3.4",
   981  		"services": map[string]any{},
   982  		"volumes":  map[string]any{},
   983  		"networks": map[string]any{
   984  			"hostnet": map[string]any{
   985  				"external": map[string]any{
   986  					"name": "host",
   987  				},
   988  			},
   989  		},
   990  		"secrets": map[string]any{},
   991  		"configs": map[string]any{},
   992  	}
   993  	configDetails := types.ConfigDetails{
   994  		ConfigFiles: []types.ConfigFile{
   995  			{Filename: "base.yml", Config: base},
   996  			{Filename: "override.yml", Config: override},
   997  		},
   998  	}
   999  	config, err := Load(configDetails)
  1000  	assert.NilError(t, err)
  1001  	assert.DeepEqual(t, &types.Config{
  1002  		Filename: "base.yml",
  1003  		Version:  "3.4",
  1004  		Services: []types.ServiceConfig{
  1005  			{
  1006  				Name:        "foo",
  1007  				Image:       "baz",
  1008  				Environment: types.MappingWithEquals{},
  1009  			},
  1010  		},
  1011  		Networks: map[string]types.NetworkConfig{
  1012  			"hostnet": {
  1013  				Name: "host",
  1014  				External: types.External{
  1015  					External: true,
  1016  				},
  1017  			},
  1018  		},
  1019  		Volumes: map[string]types.VolumeConfig{},
  1020  		Secrets: map[string]types.SecretConfig{},
  1021  		Configs: map[string]types.ConfigObjConfig{},
  1022  	}, config)
  1023  }
  1024  
  1025  func TestLoadMultipleServiceCommands(t *testing.T) {
  1026  	base := map[string]any{
  1027  		"version": "3.7",
  1028  		"services": map[string]any{
  1029  			"foo": map[string]any{
  1030  				"image":   "baz",
  1031  				"command": "foo bar",
  1032  			},
  1033  		},
  1034  		"volumes":  map[string]any{},
  1035  		"networks": map[string]any{},
  1036  		"secrets":  map[string]any{},
  1037  		"configs":  map[string]any{},
  1038  	}
  1039  	override := map[string]any{
  1040  		"version": "3.7",
  1041  		"services": map[string]any{
  1042  			"foo": map[string]any{
  1043  				"image":   "baz",
  1044  				"command": "foo baz",
  1045  			},
  1046  		},
  1047  		"volumes":  map[string]any{},
  1048  		"networks": map[string]any{},
  1049  		"secrets":  map[string]any{},
  1050  		"configs":  map[string]any{},
  1051  	}
  1052  	configDetails := types.ConfigDetails{
  1053  		ConfigFiles: []types.ConfigFile{
  1054  			{Filename: "base.yml", Config: base},
  1055  			{Filename: "override.yml", Config: override},
  1056  		},
  1057  	}
  1058  	config, err := Load(configDetails)
  1059  	assert.NilError(t, err)
  1060  	assert.DeepEqual(t, &types.Config{
  1061  		Filename: "base.yml",
  1062  		Version:  "3.7",
  1063  		Services: []types.ServiceConfig{
  1064  			{
  1065  				Name:        "foo",
  1066  				Image:       "baz",
  1067  				Command:     types.ShellCommand{"foo", "baz"},
  1068  				Environment: types.MappingWithEquals{},
  1069  			},
  1070  		},
  1071  		Volumes:  map[string]types.VolumeConfig{},
  1072  		Secrets:  map[string]types.SecretConfig{},
  1073  		Configs:  map[string]types.ConfigObjConfig{},
  1074  		Networks: map[string]types.NetworkConfig{},
  1075  	}, config)
  1076  }
  1077  
  1078  func TestLoadMultipleServiceVolumes(t *testing.T) {
  1079  	base := map[string]any{
  1080  		"version": "3.7",
  1081  		"services": map[string]any{
  1082  			"foo": map[string]any{
  1083  				"image": "baz",
  1084  				"volumes": []any{
  1085  					map[string]any{
  1086  						"type":   "volume",
  1087  						"source": "sourceVolume",
  1088  						"target": "/var/app",
  1089  					},
  1090  				},
  1091  			},
  1092  		},
  1093  		"volumes": map[string]any{
  1094  			"sourceVolume": map[string]any{},
  1095  		},
  1096  		"networks": map[string]any{},
  1097  		"secrets":  map[string]any{},
  1098  		"configs":  map[string]any{},
  1099  	}
  1100  	override := map[string]any{
  1101  		"version": "3.7",
  1102  		"services": map[string]any{
  1103  			"foo": map[string]any{
  1104  				"image": "baz",
  1105  				"volumes": []any{
  1106  					map[string]any{
  1107  						"type":   "volume",
  1108  						"source": "/local",
  1109  						"target": "/var/app",
  1110  					},
  1111  				},
  1112  			},
  1113  		},
  1114  		"volumes":  map[string]any{},
  1115  		"networks": map[string]any{},
  1116  		"secrets":  map[string]any{},
  1117  		"configs":  map[string]any{},
  1118  	}
  1119  	configDetails := types.ConfigDetails{
  1120  		ConfigFiles: []types.ConfigFile{
  1121  			{Filename: "base.yml", Config: base},
  1122  			{Filename: "override.yml", Config: override},
  1123  		},
  1124  	}
  1125  	config, err := Load(configDetails)
  1126  	assert.NilError(t, err)
  1127  	assert.DeepEqual(t, &types.Config{
  1128  		Filename: "base.yml",
  1129  		Version:  "3.7",
  1130  		Services: []types.ServiceConfig{
  1131  			{
  1132  				Name:        "foo",
  1133  				Image:       "baz",
  1134  				Environment: types.MappingWithEquals{},
  1135  				Volumes: []types.ServiceVolumeConfig{
  1136  					{
  1137  						Type:   "volume",
  1138  						Source: "/local",
  1139  						Target: "/var/app",
  1140  					},
  1141  				},
  1142  			},
  1143  		},
  1144  		Volumes: map[string]types.VolumeConfig{
  1145  			"sourceVolume": {},
  1146  		},
  1147  		Secrets:  map[string]types.SecretConfig{},
  1148  		Configs:  map[string]types.ConfigObjConfig{},
  1149  		Networks: map[string]types.NetworkConfig{},
  1150  	}, config)
  1151  }
  1152  
  1153  func TestMergeUlimitsConfig(t *testing.T) {
  1154  	specials := &specials{
  1155  		m: map[reflect.Type]func(dst, src reflect.Value) error{
  1156  			reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig,
  1157  		},
  1158  	}
  1159  	base := map[string]*types.UlimitsConfig{
  1160  		"override-single":                {Single: 100},
  1161  		"override-single-with-soft-hard": {Single: 200},
  1162  		"override-soft-hard":             {Soft: 300, Hard: 301},
  1163  		"override-soft-hard-with-single": {Soft: 400, Hard: 401},
  1164  		"dont-override":                  {Single: 500},
  1165  	}
  1166  	override := map[string]*types.UlimitsConfig{
  1167  		"override-single":                {Single: 110},
  1168  		"override-single-with-soft-hard": {Soft: 210, Hard: 211},
  1169  		"override-soft-hard":             {Soft: 310, Hard: 311},
  1170  		"override-soft-hard-with-single": {Single: 410},
  1171  		"add":                            {Single: 610},
  1172  	}
  1173  	err := mergo.Merge(&base, &override, mergo.WithOverride, mergo.WithTransformers(specials))
  1174  	assert.NilError(t, err)
  1175  	assert.DeepEqual(
  1176  		t,
  1177  		base,
  1178  		map[string]*types.UlimitsConfig{
  1179  			"override-single":                {Single: 110},
  1180  			"override-single-with-soft-hard": {Soft: 210, Hard: 211},
  1181  			"override-soft-hard":             {Soft: 310, Hard: 311},
  1182  			"override-soft-hard-with-single": {Single: 410},
  1183  			"dont-override":                  {Single: 500},
  1184  			"add":                            {Single: 610},
  1185  		},
  1186  	)
  1187  }
  1188  
  1189  func TestMergeServiceNetworkConfig(t *testing.T) {
  1190  	specials := &specials{
  1191  		m: map[reflect.Type]func(dst, src reflect.Value) error{
  1192  			reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
  1193  		},
  1194  	}
  1195  	base := map[string]*types.ServiceNetworkConfig{
  1196  		"override-aliases": {
  1197  			Aliases:     []string{"100", "101"},
  1198  			Ipv4Address: "127.0.0.1",
  1199  			Ipv6Address: "0:0:0:0:0:0:0:1",
  1200  		},
  1201  		"dont-override": {
  1202  			Aliases:     []string{"200", "201"},
  1203  			Ipv4Address: "127.0.0.2",
  1204  			Ipv6Address: "0:0:0:0:0:0:0:2",
  1205  		},
  1206  	}
  1207  	override := map[string]*types.ServiceNetworkConfig{
  1208  		"override-aliases": {
  1209  			Aliases:     []string{"110", "111"},
  1210  			Ipv4Address: "127.0.1.1",
  1211  			Ipv6Address: "0:0:0:0:0:0:1:1",
  1212  		},
  1213  		"add": {
  1214  			Aliases:     []string{"310", "311"},
  1215  			Ipv4Address: "127.0.3.1",
  1216  			Ipv6Address: "0:0:0:0:0:0:3:1",
  1217  		},
  1218  	}
  1219  	err := mergo.Merge(&base, &override, mergo.WithOverride, mergo.WithTransformers(specials))
  1220  	assert.NilError(t, err)
  1221  	assert.DeepEqual(
  1222  		t,
  1223  		base,
  1224  		map[string]*types.ServiceNetworkConfig{
  1225  			"override-aliases": {
  1226  				Aliases:     []string{"110", "111"},
  1227  				Ipv4Address: "127.0.1.1",
  1228  				Ipv6Address: "0:0:0:0:0:0:1:1",
  1229  			},
  1230  			"dont-override": {
  1231  				Aliases:     []string{"200", "201"},
  1232  				Ipv4Address: "127.0.0.2",
  1233  				Ipv6Address: "0:0:0:0:0:0:0:2",
  1234  			},
  1235  			"add": {
  1236  				Aliases:     []string{"310", "311"},
  1237  				Ipv4Address: "127.0.3.1",
  1238  				Ipv6Address: "0:0:0:0:0:0:3:1",
  1239  			},
  1240  		},
  1241  	)
  1242  }
  1243  
  1244  // issue #3293
  1245  func TestMergeServiceOverrideReplicasZero(t *testing.T) {
  1246  	base := types.ServiceConfig{
  1247  		Name: "someService",
  1248  		Deploy: types.DeployConfig{
  1249  			Replicas: uint64Ptr(3),
  1250  		},
  1251  	}
  1252  	override := types.ServiceConfig{
  1253  		Name: "someService",
  1254  		Deploy: types.DeployConfig{
  1255  			Replicas: uint64Ptr(0),
  1256  		},
  1257  	}
  1258  	services, err := mergeServices([]types.ServiceConfig{base}, []types.ServiceConfig{override})
  1259  	assert.NilError(t, err)
  1260  	assert.Equal(t, len(services), 1)
  1261  	actual := services[0]
  1262  	assert.DeepEqual(
  1263  		t,
  1264  		actual,
  1265  		types.ServiceConfig{
  1266  			Name: "someService",
  1267  			Deploy: types.DeployConfig{
  1268  				Replicas: uint64Ptr(0),
  1269  			},
  1270  		},
  1271  	)
  1272  }
  1273  
  1274  func TestMergeServiceOverrideReplicasNotNil(t *testing.T) {
  1275  	base := types.ServiceConfig{
  1276  		Name: "someService",
  1277  		Deploy: types.DeployConfig{
  1278  			Replicas: uint64Ptr(3),
  1279  		},
  1280  	}
  1281  	override := types.ServiceConfig{
  1282  		Name:   "someService",
  1283  		Deploy: types.DeployConfig{},
  1284  	}
  1285  	services, err := mergeServices([]types.ServiceConfig{base}, []types.ServiceConfig{override})
  1286  	assert.NilError(t, err)
  1287  	assert.Equal(t, len(services), 1)
  1288  	actual := services[0]
  1289  	assert.DeepEqual(
  1290  		t,
  1291  		actual,
  1292  		types.ServiceConfig{
  1293  			Name: "someService",
  1294  			Deploy: types.DeployConfig{
  1295  				Replicas: uint64Ptr(3),
  1296  			},
  1297  		},
  1298  	)
  1299  }