package loader import ( "bytes" "io/ioutil" "os" "sort" "testing" "time" "github.com/docker/cli/cli/compose/types" "github.com/gotestyourself/gotestyourself/assert" is "github.com/gotestyourself/gotestyourself/assert/cmp" "github.com/sirupsen/logrus" ) func buildConfigDetails(source map[string]interface{}, env map[string]string) types.ConfigDetails { workingDir, err := os.Getwd() if err != nil { panic(err) } return types.ConfigDetails{ WorkingDir: workingDir, ConfigFiles: []types.ConfigFile{ {Filename: "filename.yml", Config: source}, }, Environment: env, } } func loadYAML(yaml string) (*types.Config, error) { return loadYAMLWithEnv(yaml, nil) } func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) { dict, err := ParseYAML([]byte(yaml)) if err != nil { return nil, err } return Load(buildConfigDetails(dict, env)) } var sampleYAML = ` version: "3" services: foo: image: busybox networks: with_me: bar: image: busybox environment: - FOO=1 networks: - with_ipam volumes: hello: driver: default driver_opts: beep: boop networks: default: driver: bridge driver_opts: beep: boop with_ipam: ipam: driver: default config: - subnet: 172.28.0.0/16 ` var sampleDict = map[string]interface{}{ "version": "3", "services": map[string]interface{}{ "foo": map[string]interface{}{ "image": "busybox", "networks": map[string]interface{}{"with_me": nil}, }, "bar": map[string]interface{}{ "image": "busybox", "environment": []interface{}{"FOO=1"}, "networks": []interface{}{"with_ipam"}, }, }, "volumes": map[string]interface{}{ "hello": map[string]interface{}{ "driver": "default", "driver_opts": map[string]interface{}{ "beep": "boop", }, }, }, "networks": map[string]interface{}{ "default": map[string]interface{}{ "driver": "bridge", "driver_opts": map[string]interface{}{ "beep": "boop", }, }, "with_ipam": map[string]interface{}{ "ipam": map[string]interface{}{ "driver": "default", "config": []interface{}{ map[string]interface{}{ "subnet": "172.28.0.0/16", }, }, }, }, }, } func strPtr(val string) *string { return &val } var sampleConfig = types.Config{ Version: "3.0", Services: []types.ServiceConfig{ { Name: "foo", Image: "busybox", Environment: map[string]*string{}, Networks: map[string]*types.ServiceNetworkConfig{ "with_me": nil, }, }, { Name: "bar", Image: "busybox", Environment: map[string]*string{"FOO": strPtr("1")}, Networks: map[string]*types.ServiceNetworkConfig{ "with_ipam": nil, }, }, }, Networks: map[string]types.NetworkConfig{ "default": { Driver: "bridge", DriverOpts: map[string]string{ "beep": "boop", }, }, "with_ipam": { Ipam: types.IPAMConfig{ Driver: "default", Config: []*types.IPAMPool{ { Subnet: "172.28.0.0/16", }, }, }, }, }, Volumes: map[string]types.VolumeConfig{ "hello": { Driver: "default", DriverOpts: map[string]string{ "beep": "boop", }, }, }, } func TestParseYAML(t *testing.T) { dict, err := ParseYAML([]byte(sampleYAML)) assert.NilError(t, err) assert.Check(t, is.DeepEqual(sampleDict, dict)) } func TestLoad(t *testing.T) { actual, err := Load(buildConfigDetails(sampleDict, nil)) assert.NilError(t, err) assert.Check(t, is.Equal(sampleConfig.Version, actual.Version)) assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) } func TestLoadV31(t *testing.T) { actual, err := loadYAML(` version: "3.1" services: foo: image: busybox secrets: [super] secrets: super: external: true `) assert.NilError(t, err) assert.Check(t, is.Len(actual.Services, 1)) assert.Check(t, is.Len(actual.Secrets, 1)) } func TestLoadV33(t *testing.T) { actual, err := loadYAML(` version: "3.3" services: foo: image: busybox credential_spec: File: "/foo" configs: [super] configs: super: external: true `) assert.NilError(t, err) assert.Assert(t, is.Len(actual.Services, 1)) assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.File, "/foo")) assert.Assert(t, is.Len(actual.Configs, 1)) } func TestParseAndLoad(t *testing.T) { actual, err := loadYAML(sampleYAML) assert.NilError(t, err) assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) } func TestInvalidTopLevelObjectType(t *testing.T) { _, err := loadYAML("1") assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Top-level object must be a mapping")) _, err = loadYAML("\"hello\"") assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Top-level object must be a mapping")) _, err = loadYAML("[\"hello\"]") assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Top-level object must be a mapping")) } func TestNonStringKeys(t *testing.T) { _, err := loadYAML(` version: "3" 123: foo: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Non-string key at top level: 123")) _, err = loadYAML(` version: "3" services: foo: image: busybox 123: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Non-string key in services: 123")) _, err = loadYAML(` version: "3" services: foo: image: busybox networks: default: ipam: config: - 123: oh dear `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")) _, err = loadYAML(` version: "3" services: dict-env: image: busybox environment: 1: FOO `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Non-string key in services.dict-env.environment: 1")) } func TestSupportedVersion(t *testing.T) { _, err := loadYAML(` version: "3" services: foo: image: busybox `) assert.NilError(t, err) _, err = loadYAML(` version: "3.0" services: foo: image: busybox `) assert.NilError(t, err) } func TestUnsupportedVersion(t *testing.T) { _, err := loadYAML(` version: "2" services: foo: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "version")) _, err = loadYAML(` version: "2.0" services: foo: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "version")) } func TestInvalidVersion(t *testing.T) { _, err := loadYAML(` version: 3 services: foo: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "version must be a string")) } func TestV1Unsupported(t *testing.T) { _, err := loadYAML(` foo: image: busybox `) assert.Check(t, is.ErrorContains(err, "")) } func TestNonMappingObject(t *testing.T) { _, err := loadYAML(` version: "3" services: - foo: image: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services must be a mapping")) _, err = loadYAML(` version: "3" services: foo: busybox `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.foo must be a mapping")) _, err = loadYAML(` version: "3" networks: - default: driver: bridge `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "networks must be a mapping")) _, err = loadYAML(` version: "3" networks: default: bridge `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "networks.default must be a mapping")) _, err = loadYAML(` version: "3" volumes: - data: driver: local `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "volumes must be a mapping")) _, err = loadYAML(` version: "3" volumes: data: local `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "volumes.data must be a mapping")) } func TestNonStringImage(t *testing.T) { _, err := loadYAML(` version: "3" services: foo: image: ["busybox", "latest"] `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.foo.image must be a string")) } func TestLoadWithEnvironment(t *testing.T) { config, err := loadYAMLWithEnv(` version: "3" services: dict-env: image: busybox environment: FOO: "1" BAR: 2 BAZ: 2.5 QUX: QUUX: list-env: image: busybox environment: - FOO=1 - BAR=2 - BAZ=2.5 - QUX= - QUUX `, map[string]string{"QUX": "qux"}) assert.Check(t, err) expected := types.MappingWithEquals{ "FOO": strPtr("1"), "BAR": strPtr("2"), "BAZ": strPtr("2.5"), "QUX": strPtr("qux"), "QUUX": nil, } assert.Check(t, is.Equal(2, len(config.Services))) for _, service := range config.Services { assert.Check(t, is.DeepEqual(expected, service.Environment)) } } func TestInvalidEnvironmentValue(t *testing.T) { _, err := loadYAML(` version: "3" services: dict-env: image: busybox environment: FOO: ["1"] `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.dict-env.environment.FOO must be a string, number or null")) } func TestInvalidEnvironmentObject(t *testing.T) { _, err := loadYAML(` version: "3" services: dict-env: image: busybox environment: "FOO=1" `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.dict-env.environment must be a mapping")) } func TestLoadWithEnvironmentInterpolation(t *testing.T) { home := "/home/foo" config, err := loadYAMLWithEnv(` version: "3" services: test: image: busybox labels: - home1=$HOME - home2=${HOME} - nonexistent=$NONEXISTENT - default=${NONEXISTENT-default} networks: test: driver: $HOME volumes: test: driver: $HOME `, map[string]string{ "HOME": home, "FOO": "foo", }) assert.NilError(t, err) expectedLabels := types.Labels{ "home1": home, "home2": home, "nonexistent": "", "default": "default", } assert.Check(t, is.DeepEqual(expectedLabels, config.Services[0].Labels)) assert.Check(t, is.Equal(home, config.Networks["test"].Driver)) assert.Check(t, is.Equal(home, config.Volumes["test"].Driver)) } func TestLoadWithInterpolationCastFull(t *testing.T) { dict, err := ParseYAML([]byte(` version: "3.4" services: web: configs: - source: appconfig mode: $theint secrets: - source: super mode: $theint healthcheck: retries: ${theint} disable: $thebool deploy: replicas: $theint update_config: parallelism: $theint max_failure_ratio: $thefloat restart_policy: max_attempts: $theint ports: - $theint - "34567" - target: $theint published: $theint ulimits: nproc: $theint nofile: hard: $theint soft: $theint privileged: $thebool read_only: $thebool stdin_open: ${thebool} tty: $thebool volumes: - source: data type: volume read_only: $thebool volume: nocopy: $thebool configs: appconfig: external: $thebool secrets: super: external: $thebool volumes: data: external: $thebool networks: front: external: $thebool internal: $thebool attachable: $thebool `)) assert.NilError(t, err) env := map[string]string{ "theint": "555", "thefloat": "3.14", "thebool": "true", } config, err := Load(buildConfigDetails(dict, env)) assert.NilError(t, err) expected := &types.Config{ Filename: "filename.yml", Version: "3.4", Services: []types.ServiceConfig{ { Name: "web", Configs: []types.ServiceConfigObjConfig{ { Source: "appconfig", Mode: uint32Ptr(555), }, }, Secrets: []types.ServiceSecretConfig{ { Source: "super", Mode: uint32Ptr(555), }, }, HealthCheck: &types.HealthCheckConfig{ Retries: uint64Ptr(555), Disable: true, }, Deploy: types.DeployConfig{ Replicas: uint64Ptr(555), UpdateConfig: &types.UpdateConfig{ Parallelism: uint64Ptr(555), MaxFailureRatio: 3.14, }, RestartPolicy: &types.RestartPolicy{ MaxAttempts: uint64Ptr(555), }, }, Ports: []types.ServicePortConfig{ {Target: 555, Mode: "ingress", Protocol: "tcp"}, {Target: 34567, Mode: "ingress", Protocol: "tcp"}, {Target: 555, Published: 555}, }, Ulimits: map[string]*types.UlimitsConfig{ "nproc": {Single: 555}, "nofile": {Hard: 555, Soft: 555}, }, Privileged: true, ReadOnly: true, StdinOpen: true, Tty: true, Volumes: []types.ServiceVolumeConfig{ { Source: "data", Type: "volume", ReadOnly: true, Volume: &types.ServiceVolumeVolume{NoCopy: true}, }, }, Environment: types.MappingWithEquals{}, }, }, Configs: map[string]types.ConfigObjConfig{ "appconfig": {External: types.External{External: true}, Name: "appconfig"}, }, Secrets: map[string]types.SecretConfig{ "super": {External: types.External{External: true}, Name: "super"}, }, Volumes: map[string]types.VolumeConfig{ "data": {External: types.External{External: true}, Name: "data"}, }, Networks: map[string]types.NetworkConfig{ "front": { External: types.External{External: true}, Name: "front", Internal: true, Attachable: true, }, }, } assert.Check(t, is.DeepEqual(expected, config)) } func TestUnsupportedProperties(t *testing.T) { dict, err := ParseYAML([]byte(` version: "3" services: web: image: web build: context: ./web links: - bar pid: host db: image: db build: context: ./db `)) assert.NilError(t, err) configDetails := buildConfigDetails(dict, nil) _, err = Load(configDetails) assert.NilError(t, err) unsupported := GetUnsupportedProperties(dict) assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported)) } func TestBuildProperties(t *testing.T) { dict, err := ParseYAML([]byte(` version: "3" services: web: image: web build: . links: - bar db: image: db build: context: ./db `)) assert.NilError(t, err) configDetails := buildConfigDetails(dict, nil) _, err = Load(configDetails) assert.NilError(t, err) } func TestDeprecatedProperties(t *testing.T) { dict, err := ParseYAML([]byte(` version: "3" services: web: image: web container_name: web db: image: db container_name: db expose: ["5434"] `)) assert.NilError(t, err) configDetails := buildConfigDetails(dict, nil) _, err = Load(configDetails) assert.NilError(t, err) deprecated := GetDeprecatedProperties(dict) assert.Check(t, is.Len(deprecated, 2)) assert.Check(t, is.Contains(deprecated, "container_name")) assert.Check(t, is.Contains(deprecated, "expose")) } func TestForbiddenProperties(t *testing.T) { _, err := loadYAML(` version: "3" services: foo: image: busybox volumes: - /data volume_driver: some-driver bar: extends: service: foo `) assert.Assert(t, is.ErrorContains(err, "")) forbidden, ok := err.(*ForbiddenPropertiesError) assert.Check(t, ok, "error type is %T instead of ForbiddenPropertiesError", err) props := forbidden.Properties assert.Check(t, is.Len(props, 2)) assert.Check(t, is.Contains(props, "volume_driver")) assert.Check(t, is.Contains(props, "extends")) } func TestInvalidResource(t *testing.T) { _, err := loadYAML(` version: "3" services: foo: image: busybox deploy: resources: impossible: x: 1 `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "Additional property impossible is not allowed")) } func TestInvalidExternalAndDriverCombination(t *testing.T) { _, err := loadYAML(` version: "3" volumes: external_volume: external: true driver: foobar `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "conflicting parameters \"external\" and \"driver\" specified for volume")) assert.Check(t, is.Contains(err.Error(), "external_volume")) } func TestInvalidExternalAndDirverOptsCombination(t *testing.T) { _, err := loadYAML(` version: "3" volumes: external_volume: external: true driver_opts: beep: boop `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "conflicting parameters \"external\" and \"driver_opts\" specified for volume")) assert.Check(t, is.Contains(err.Error(), "external_volume")) } func TestInvalidExternalAndLabelsCombination(t *testing.T) { _, err := loadYAML(` version: "3" volumes: external_volume: external: true labels: - beep=boop `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "conflicting parameters \"external\" and \"labels\" specified for volume")) assert.Check(t, is.Contains(err.Error(), "external_volume")) } func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) { _, err := loadYAML(` version: "3.4" volumes: external_volume: name: user_specified_name external: name: external_name `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "volume.external.name and volume.name conflict; only use volume.name")) assert.Check(t, is.Contains(err.Error(), "external_volume")) } func durationPtr(value time.Duration) *time.Duration { return &value } func uint64Ptr(value uint64) *uint64 { return &value } func uint32Ptr(value uint32) *uint32 { return &value } func TestFullExample(t *testing.T) { bytes, err := ioutil.ReadFile("full-example.yml") assert.NilError(t, err) homeDir := "/home/foo" env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} config, err := loadYAMLWithEnv(string(bytes), env) assert.NilError(t, err) workingDir, err := os.Getwd() assert.NilError(t, err) expectedConfig := fullExampleConfig(workingDir, homeDir) assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes)) } func TestLoadTmpfsVolume(t *testing.T) { config, err := loadYAML(` version: "3.6" services: tmpfs: image: nginx:latest volumes: - type: tmpfs target: /app tmpfs: size: 10000 `) assert.NilError(t, err) expected := types.ServiceVolumeConfig{ Target: "/app", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{ Size: int64(10000), }, } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.Len(config.Services[0].Volumes, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) } func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) { _, err := loadYAML(` version: "3.5" services: tmpfs: image: nginx:latest volumes: - type: tmpfs target: /app tmpfs: size: 10000 `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.tmpfs.volumes.0 Additional property tmpfs is not allowed")) } func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) { _, err := loadYAML(` version: "3.5" services: tmpfs: image: nginx:latest volumes: - type: bind target: /app `) assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`) } func TestLoadBindMountWithSource(t *testing.T) { config, err := loadYAML(` version: "3.5" services: bind: image: nginx:latest volumes: - type: bind target: /app source: "." `) assert.NilError(t, err) workingDir, err := os.Getwd() assert.NilError(t, err) expected := types.ServiceVolumeConfig{ Type: "bind", Source: workingDir, Target: "/app", } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.Len(config.Services[0].Volumes, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) } func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) { config, err := loadYAML(` version: "3.6" services: tmpfs: image: nginx:latest volumes: - type: tmpfs target: /app tmpfs: size: 0 `) assert.NilError(t, err) expected := types.ServiceVolumeConfig{ Target: "/app", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{}, } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.Len(config.Services[0].Volumes, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) } func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) { _, err := loadYAML(` version: "3.6" services: tmpfs: image: nginx:latest volumes: - type: tmpfs target: /app tmpfs: size: -1 `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")) } func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) { _, err := loadYAML(` version: "3.6" services: tmpfs: image: nginx:latest volumes: - type: tmpfs target: /app tmpfs: size: 0.0001 `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "services.tmpfs.volumes.0.tmpfs.size must be a integer")) } func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { sort.Sort(servicesByName(services)) return services } type servicesByName []types.ServiceConfig func (sbn servicesByName) Len() int { return len(sbn) } func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] } func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name } func TestLoadAttachableNetwork(t *testing.T) { config, err := loadYAML(` version: "3.2" networks: mynet1: driver: overlay attachable: true mynet2: driver: bridge `) assert.NilError(t, err) expected := map[string]types.NetworkConfig{ "mynet1": { Driver: "overlay", Attachable: true, }, "mynet2": { Driver: "bridge", Attachable: false, }, } assert.Check(t, is.DeepEqual(expected, config.Networks)) } func TestLoadExpandedPortFormat(t *testing.T) { config, err := loadYAML(` version: "3.2" services: web: image: busybox ports: - "80-82:8080-8082" - "90-92:8090-8092/udp" - "85:8500" - 8600 - protocol: udp target: 53 published: 10053 - mode: host target: 22 published: 10022 `) assert.NilError(t, err) expected := []types.ServicePortConfig{ { Mode: "ingress", Target: 8080, Published: 80, Protocol: "tcp", }, { Mode: "ingress", Target: 8081, Published: 81, Protocol: "tcp", }, { Mode: "ingress", Target: 8082, Published: 82, Protocol: "tcp", }, { Mode: "ingress", Target: 8090, Published: 90, Protocol: "udp", }, { Mode: "ingress", Target: 8091, Published: 91, Protocol: "udp", }, { Mode: "ingress", Target: 8092, Published: 92, Protocol: "udp", }, { Mode: "ingress", Target: 8500, Published: 85, Protocol: "tcp", }, { Mode: "ingress", Target: 8600, Published: 0, Protocol: "tcp", }, { Target: 53, Published: 10053, Protocol: "udp", }, { Mode: "host", Target: 22, Published: 10022, }, } assert.Check(t, is.Len(config.Services, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].Ports)) } func TestLoadExpandedMountFormat(t *testing.T) { config, err := loadYAML(` version: "3.2" services: web: image: busybox volumes: - type: volume source: foo target: /target read_only: true volumes: foo: {} `) assert.NilError(t, err) expected := types.ServiceVolumeConfig{ Type: "volume", Source: "foo", Target: "/target", ReadOnly: true, } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.Len(config.Services[0].Volumes, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) } func TestLoadExtraHostsMap(t *testing.T) { config, err := loadYAML(` version: "3.2" services: web: image: busybox extra_hosts: "zulu": "162.242.195.82" "alpha": "50.31.209.229" `) assert.NilError(t, err) expected := types.HostsList{ "alpha:50.31.209.229", "zulu:162.242.195.82", } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) } func TestLoadExtraHostsList(t *testing.T) { config, err := loadYAML(` version: "3.2" services: web: image: busybox extra_hosts: - "zulu:162.242.195.82" - "alpha:50.31.209.229" - "zulu:ff02::1" `) assert.NilError(t, err) expected := types.HostsList{ "zulu:162.242.195.82", "alpha:50.31.209.229", "zulu:ff02::1", } assert.Assert(t, is.Len(config.Services, 1)) assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) } func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) { buf, cleanup := patchLogrus() defer cleanup() source := map[string]interface{}{ "foo": map[string]interface{}{ "external": map[string]interface{}{ "name": "oops", }, }, } volumes, err := LoadVolumes(source, "3.4") assert.NilError(t, err) expected := map[string]types.VolumeConfig{ "foo": { Name: "oops", External: types.External{External: true}, }, } assert.Check(t, is.DeepEqual(expected, volumes)) assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated")) } func patchLogrus() (*bytes.Buffer, func()) { buf := new(bytes.Buffer) out := logrus.StandardLogger().Out logrus.SetOutput(buf) return buf, func() { logrus.SetOutput(out) } } func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) { buf, cleanup := patchLogrus() defer cleanup() source := map[string]interface{}{ "foo": map[string]interface{}{ "external": map[string]interface{}{ "name": "oops", }, }, } volumes, err := LoadVolumes(source, "3.3") assert.NilError(t, err) expected := map[string]types.VolumeConfig{ "foo": { Name: "oops", External: types.External{External: true}, }, } assert.Check(t, is.DeepEqual(expected, volumes)) assert.Check(t, is.Equal("", buf.String())) } func TestLoadV35(t *testing.T) { actual, err := loadYAML(` version: "3.5" services: foo: image: busybox isolation: process configs: foo: name: fooqux external: true bar: name: barqux file: ./example1.env secrets: foo: name: fooqux external: true bar: name: barqux file: ./full-example.yml `) assert.NilError(t, err) assert.Check(t, is.Len(actual.Services, 1)) assert.Check(t, is.Len(actual.Secrets, 2)) assert.Check(t, is.Len(actual.Configs, 2)) assert.Check(t, is.Equal("process", actual.Services[0].Isolation)) } func TestLoadV35InvalidIsolation(t *testing.T) { // validation should be done only on the daemon side actual, err := loadYAML(` version: "3.5" services: foo: image: busybox isolation: invalid configs: super: external: true `) assert.NilError(t, err) assert.Assert(t, is.Len(actual.Services, 1)) assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation)) } func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) { _, err := loadYAML(` version: "3.5" secrets: external_secret: name: user_specified_name external: name: external_name `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "secret.external.name and secret.name conflict; only use secret.name")) assert.Check(t, is.Contains(err.Error(), "external_secret")) } func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) { buf, cleanup := patchLogrus() defer cleanup() source := map[string]interface{}{ "foo": map[string]interface{}{ "external": map[string]interface{}{ "name": "oops", }, }, } details := types.ConfigDetails{ Version: "3.5", } secrets, err := LoadSecrets(source, details) assert.NilError(t, err) expected := map[string]types.SecretConfig{ "foo": { Name: "oops", External: types.External{External: true}, }, } assert.Check(t, is.DeepEqual(expected, secrets)) assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated")) } func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) { buf, cleanup := patchLogrus() defer cleanup() source := map[string]interface{}{ "foo": map[string]interface{}{ "external": map[string]interface{}{ "name": "oops", }, }, } networks, err := LoadNetworks(source, "3.5") assert.NilError(t, err) expected := map[string]types.NetworkConfig{ "foo": { Name: "oops", External: types.External{External: true}, }, } assert.Check(t, is.DeepEqual(expected, networks)) assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated")) } func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) { buf, cleanup := patchLogrus() defer cleanup() source := map[string]interface{}{ "foo": map[string]interface{}{ "external": map[string]interface{}{ "name": "oops", }, }, } networks, err := LoadNetworks(source, "3.4") assert.NilError(t, err) expected := map[string]types.NetworkConfig{ "foo": { Name: "oops", External: types.External{External: true}, }, } assert.Check(t, is.DeepEqual(expected, networks)) assert.Check(t, is.Equal("", buf.String())) } func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) { _, err := loadYAML(` version: "3.5" networks: foo: name: user_specified_name external: name: external_name `) assert.Assert(t, is.ErrorContains(err, "")) assert.Check(t, is.Contains(err.Error(), "network.external.name and network.name conflict; only use network.name")) assert.Check(t, is.Contains(err.Error(), "foo")) }