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