loader_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. package loader
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "os"
  6. "sort"
  7. "testing"
  8. "time"
  9. "github.com/docker/docker/cli/compose/types"
  10. "github.com/stretchr/testify/assert"
  11. )
  12. func buildConfigDetails(source types.Dict) types.ConfigDetails {
  13. workingDir, err := os.Getwd()
  14. if err != nil {
  15. panic(err)
  16. }
  17. return types.ConfigDetails{
  18. WorkingDir: workingDir,
  19. ConfigFiles: []types.ConfigFile{
  20. {Filename: "filename.yml", Config: source},
  21. },
  22. Environment: nil,
  23. }
  24. }
  25. var sampleYAML = `
  26. version: "3"
  27. services:
  28. foo:
  29. image: busybox
  30. networks:
  31. with_me:
  32. bar:
  33. image: busybox
  34. environment:
  35. - FOO=1
  36. networks:
  37. - with_ipam
  38. volumes:
  39. hello:
  40. driver: default
  41. driver_opts:
  42. beep: boop
  43. networks:
  44. default:
  45. driver: bridge
  46. driver_opts:
  47. beep: boop
  48. with_ipam:
  49. ipam:
  50. driver: default
  51. config:
  52. - subnet: 172.28.0.0/16
  53. `
  54. var sampleDict = types.Dict{
  55. "version": "3",
  56. "services": types.Dict{
  57. "foo": types.Dict{
  58. "image": "busybox",
  59. "networks": types.Dict{"with_me": nil},
  60. },
  61. "bar": types.Dict{
  62. "image": "busybox",
  63. "environment": []interface{}{"FOO=1"},
  64. "networks": []interface{}{"with_ipam"},
  65. },
  66. },
  67. "volumes": types.Dict{
  68. "hello": types.Dict{
  69. "driver": "default",
  70. "driver_opts": types.Dict{
  71. "beep": "boop",
  72. },
  73. },
  74. },
  75. "networks": types.Dict{
  76. "default": types.Dict{
  77. "driver": "bridge",
  78. "driver_opts": types.Dict{
  79. "beep": "boop",
  80. },
  81. },
  82. "with_ipam": types.Dict{
  83. "ipam": types.Dict{
  84. "driver": "default",
  85. "config": []interface{}{
  86. types.Dict{
  87. "subnet": "172.28.0.0/16",
  88. },
  89. },
  90. },
  91. },
  92. },
  93. }
  94. var sampleConfig = types.Config{
  95. Services: []types.ServiceConfig{
  96. {
  97. Name: "foo",
  98. Image: "busybox",
  99. Environment: map[string]string{},
  100. Networks: map[string]*types.ServiceNetworkConfig{
  101. "with_me": nil,
  102. },
  103. },
  104. {
  105. Name: "bar",
  106. Image: "busybox",
  107. Environment: map[string]string{"FOO": "1"},
  108. Networks: map[string]*types.ServiceNetworkConfig{
  109. "with_ipam": nil,
  110. },
  111. },
  112. },
  113. Networks: map[string]types.NetworkConfig{
  114. "default": {
  115. Driver: "bridge",
  116. DriverOpts: map[string]string{
  117. "beep": "boop",
  118. },
  119. },
  120. "with_ipam": {
  121. Ipam: types.IPAMConfig{
  122. Driver: "default",
  123. Config: []*types.IPAMPool{
  124. {
  125. Subnet: "172.28.0.0/16",
  126. },
  127. },
  128. },
  129. },
  130. },
  131. Volumes: map[string]types.VolumeConfig{
  132. "hello": {
  133. Driver: "default",
  134. DriverOpts: map[string]string{
  135. "beep": "boop",
  136. },
  137. },
  138. },
  139. }
  140. func TestParseYAML(t *testing.T) {
  141. dict, err := ParseYAML([]byte(sampleYAML))
  142. if !assert.NoError(t, err) {
  143. return
  144. }
  145. assert.Equal(t, sampleDict, dict)
  146. }
  147. func TestLoad(t *testing.T) {
  148. actual, err := Load(buildConfigDetails(sampleDict))
  149. if !assert.NoError(t, err) {
  150. return
  151. }
  152. assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
  153. assert.Equal(t, sampleConfig.Networks, actual.Networks)
  154. assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
  155. }
  156. func TestParseAndLoad(t *testing.T) {
  157. actual, err := loadYAML(sampleYAML)
  158. if !assert.NoError(t, err) {
  159. return
  160. }
  161. assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
  162. assert.Equal(t, sampleConfig.Networks, actual.Networks)
  163. assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
  164. }
  165. func TestInvalidTopLevelObjectType(t *testing.T) {
  166. _, err := loadYAML("1")
  167. assert.Error(t, err)
  168. assert.Contains(t, err.Error(), "Top-level object must be a mapping")
  169. _, err = loadYAML("\"hello\"")
  170. assert.Error(t, err)
  171. assert.Contains(t, err.Error(), "Top-level object must be a mapping")
  172. _, err = loadYAML("[\"hello\"]")
  173. assert.Error(t, err)
  174. assert.Contains(t, err.Error(), "Top-level object must be a mapping")
  175. }
  176. func TestNonStringKeys(t *testing.T) {
  177. _, err := loadYAML(`
  178. version: "3"
  179. 123:
  180. foo:
  181. image: busybox
  182. `)
  183. assert.Error(t, err)
  184. assert.Contains(t, err.Error(), "Non-string key at top level: 123")
  185. _, err = loadYAML(`
  186. version: "3"
  187. services:
  188. foo:
  189. image: busybox
  190. 123:
  191. image: busybox
  192. `)
  193. assert.Error(t, err)
  194. assert.Contains(t, err.Error(), "Non-string key in services: 123")
  195. _, err = loadYAML(`
  196. version: "3"
  197. services:
  198. foo:
  199. image: busybox
  200. networks:
  201. default:
  202. ipam:
  203. config:
  204. - 123: oh dear
  205. `)
  206. assert.Error(t, err)
  207. assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
  208. _, err = loadYAML(`
  209. version: "3"
  210. services:
  211. dict-env:
  212. image: busybox
  213. environment:
  214. 1: FOO
  215. `)
  216. assert.Error(t, err)
  217. assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
  218. }
  219. func TestSupportedVersion(t *testing.T) {
  220. _, err := loadYAML(`
  221. version: "3"
  222. services:
  223. foo:
  224. image: busybox
  225. `)
  226. assert.NoError(t, err)
  227. _, err = loadYAML(`
  228. version: "3.0"
  229. services:
  230. foo:
  231. image: busybox
  232. `)
  233. assert.NoError(t, err)
  234. }
  235. func TestUnsupportedVersion(t *testing.T) {
  236. _, err := loadYAML(`
  237. version: "2"
  238. services:
  239. foo:
  240. image: busybox
  241. `)
  242. assert.Error(t, err)
  243. assert.Contains(t, err.Error(), "version")
  244. _, err = loadYAML(`
  245. version: "2.0"
  246. services:
  247. foo:
  248. image: busybox
  249. `)
  250. assert.Error(t, err)
  251. assert.Contains(t, err.Error(), "version")
  252. }
  253. func TestInvalidVersion(t *testing.T) {
  254. _, err := loadYAML(`
  255. version: 3
  256. services:
  257. foo:
  258. image: busybox
  259. `)
  260. assert.Error(t, err)
  261. assert.Contains(t, err.Error(), "version must be a string")
  262. }
  263. func TestV1Unsupported(t *testing.T) {
  264. _, err := loadYAML(`
  265. foo:
  266. image: busybox
  267. `)
  268. assert.Error(t, err)
  269. }
  270. func TestNonMappingObject(t *testing.T) {
  271. _, err := loadYAML(`
  272. version: "3"
  273. services:
  274. - foo:
  275. image: busybox
  276. `)
  277. assert.Error(t, err)
  278. assert.Contains(t, err.Error(), "services must be a mapping")
  279. _, err = loadYAML(`
  280. version: "3"
  281. services:
  282. foo: busybox
  283. `)
  284. assert.Error(t, err)
  285. assert.Contains(t, err.Error(), "services.foo must be a mapping")
  286. _, err = loadYAML(`
  287. version: "3"
  288. networks:
  289. - default:
  290. driver: bridge
  291. `)
  292. assert.Error(t, err)
  293. assert.Contains(t, err.Error(), "networks must be a mapping")
  294. _, err = loadYAML(`
  295. version: "3"
  296. networks:
  297. default: bridge
  298. `)
  299. assert.Error(t, err)
  300. assert.Contains(t, err.Error(), "networks.default must be a mapping")
  301. _, err = loadYAML(`
  302. version: "3"
  303. volumes:
  304. - data:
  305. driver: local
  306. `)
  307. assert.Error(t, err)
  308. assert.Contains(t, err.Error(), "volumes must be a mapping")
  309. _, err = loadYAML(`
  310. version: "3"
  311. volumes:
  312. data: local
  313. `)
  314. assert.Error(t, err)
  315. assert.Contains(t, err.Error(), "volumes.data must be a mapping")
  316. }
  317. func TestNonStringImage(t *testing.T) {
  318. _, err := loadYAML(`
  319. version: "3"
  320. services:
  321. foo:
  322. image: ["busybox", "latest"]
  323. `)
  324. assert.Error(t, err)
  325. assert.Contains(t, err.Error(), "services.foo.image must be a string")
  326. }
  327. func TestValidEnvironment(t *testing.T) {
  328. config, err := loadYAML(`
  329. version: "3"
  330. services:
  331. dict-env:
  332. image: busybox
  333. environment:
  334. FOO: "1"
  335. BAR: 2
  336. BAZ: 2.5
  337. QUUX:
  338. list-env:
  339. image: busybox
  340. environment:
  341. - FOO=1
  342. - BAR=2
  343. - BAZ=2.5
  344. - QUUX=
  345. `)
  346. assert.NoError(t, err)
  347. expected := map[string]string{
  348. "FOO": "1",
  349. "BAR": "2",
  350. "BAZ": "2.5",
  351. "QUUX": "",
  352. }
  353. assert.Equal(t, 2, len(config.Services))
  354. for _, service := range config.Services {
  355. assert.Equal(t, expected, service.Environment)
  356. }
  357. }
  358. func TestInvalidEnvironmentValue(t *testing.T) {
  359. _, err := loadYAML(`
  360. version: "3"
  361. services:
  362. dict-env:
  363. image: busybox
  364. environment:
  365. FOO: ["1"]
  366. `)
  367. assert.Error(t, err)
  368. assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
  369. }
  370. func TestInvalidEnvironmentObject(t *testing.T) {
  371. _, err := loadYAML(`
  372. version: "3"
  373. services:
  374. dict-env:
  375. image: busybox
  376. environment: "FOO=1"
  377. `)
  378. assert.Error(t, err)
  379. assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
  380. }
  381. func TestEnvironmentInterpolation(t *testing.T) {
  382. config, err := loadYAML(`
  383. version: "3"
  384. services:
  385. test:
  386. image: busybox
  387. labels:
  388. - home1=$HOME
  389. - home2=${HOME}
  390. - nonexistent=$NONEXISTENT
  391. - default=${NONEXISTENT-default}
  392. networks:
  393. test:
  394. driver: $HOME
  395. volumes:
  396. test:
  397. driver: $HOME
  398. `)
  399. assert.NoError(t, err)
  400. home := os.Getenv("HOME")
  401. expectedLabels := map[string]string{
  402. "home1": home,
  403. "home2": home,
  404. "nonexistent": "",
  405. "default": "default",
  406. }
  407. assert.Equal(t, expectedLabels, config.Services[0].Labels)
  408. assert.Equal(t, home, config.Networks["test"].Driver)
  409. assert.Equal(t, home, config.Volumes["test"].Driver)
  410. }
  411. func TestUnsupportedProperties(t *testing.T) {
  412. dict, err := ParseYAML([]byte(`
  413. version: "3"
  414. services:
  415. web:
  416. image: web
  417. build: ./web
  418. links:
  419. - bar
  420. db:
  421. image: db
  422. build: ./db
  423. `))
  424. assert.NoError(t, err)
  425. configDetails := buildConfigDetails(dict)
  426. _, err = Load(configDetails)
  427. assert.NoError(t, err)
  428. unsupported := GetUnsupportedProperties(configDetails)
  429. assert.Equal(t, []string{"build", "links"}, unsupported)
  430. }
  431. func TestDeprecatedProperties(t *testing.T) {
  432. dict, err := ParseYAML([]byte(`
  433. version: "3"
  434. services:
  435. web:
  436. image: web
  437. container_name: web
  438. db:
  439. image: db
  440. container_name: db
  441. expose: ["5434"]
  442. `))
  443. assert.NoError(t, err)
  444. configDetails := buildConfigDetails(dict)
  445. _, err = Load(configDetails)
  446. assert.NoError(t, err)
  447. deprecated := GetDeprecatedProperties(configDetails)
  448. assert.Equal(t, 2, len(deprecated))
  449. assert.Contains(t, deprecated, "container_name")
  450. assert.Contains(t, deprecated, "expose")
  451. }
  452. func TestForbiddenProperties(t *testing.T) {
  453. _, err := loadYAML(`
  454. version: "3"
  455. services:
  456. foo:
  457. image: busybox
  458. volumes:
  459. - /data
  460. volume_driver: some-driver
  461. bar:
  462. extends:
  463. service: foo
  464. `)
  465. assert.Error(t, err)
  466. assert.IsType(t, &ForbiddenPropertiesError{}, err)
  467. fmt.Println(err)
  468. forbidden := err.(*ForbiddenPropertiesError).Properties
  469. assert.Equal(t, 2, len(forbidden))
  470. assert.Contains(t, forbidden, "volume_driver")
  471. assert.Contains(t, forbidden, "extends")
  472. }
  473. func durationPtr(value time.Duration) *time.Duration {
  474. return &value
  475. }
  476. func int64Ptr(value int64) *int64 {
  477. return &value
  478. }
  479. func uint64Ptr(value uint64) *uint64 {
  480. return &value
  481. }
  482. func TestFullExample(t *testing.T) {
  483. bytes, err := ioutil.ReadFile("full-example.yml")
  484. assert.NoError(t, err)
  485. config, err := loadYAML(string(bytes))
  486. if !assert.NoError(t, err) {
  487. return
  488. }
  489. workingDir, err := os.Getwd()
  490. assert.NoError(t, err)
  491. homeDir := os.Getenv("HOME")
  492. stopGracePeriod := time.Duration(20 * time.Second)
  493. expectedServiceConfig := types.ServiceConfig{
  494. Name: "foo",
  495. CapAdd: []string{"ALL"},
  496. CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
  497. CgroupParent: "m-executor-abcd",
  498. Command: []string{"bundle", "exec", "thin", "-p", "3000"},
  499. ContainerName: "my-web-container",
  500. DependsOn: []string{"db", "redis"},
  501. Deploy: types.DeployConfig{
  502. Mode: "replicated",
  503. Replicas: uint64Ptr(6),
  504. Labels: map[string]string{"FOO": "BAR"},
  505. UpdateConfig: &types.UpdateConfig{
  506. Parallelism: uint64Ptr(3),
  507. Delay: time.Duration(10 * time.Second),
  508. FailureAction: "continue",
  509. Monitor: time.Duration(60 * time.Second),
  510. MaxFailureRatio: 0.3,
  511. },
  512. Resources: types.Resources{
  513. Limits: &types.Resource{
  514. NanoCPUs: "0.001",
  515. MemoryBytes: 50 * 1024 * 1024,
  516. },
  517. Reservations: &types.Resource{
  518. NanoCPUs: "0.0001",
  519. MemoryBytes: 20 * 1024 * 1024,
  520. },
  521. },
  522. RestartPolicy: &types.RestartPolicy{
  523. Condition: "on_failure",
  524. Delay: durationPtr(5 * time.Second),
  525. MaxAttempts: uint64Ptr(3),
  526. Window: durationPtr(2 * time.Minute),
  527. },
  528. Placement: types.Placement{
  529. Constraints: []string{"node=foo"},
  530. },
  531. },
  532. Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
  533. DNS: []string{"8.8.8.8", "9.9.9.9"},
  534. DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
  535. DomainName: "foo.com",
  536. Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
  537. Environment: map[string]string{
  538. "RACK_ENV": "development",
  539. "SHOW": "true",
  540. "SESSION_SECRET": "",
  541. "FOO": "1",
  542. "BAR": "2",
  543. "BAZ": "3",
  544. },
  545. Expose: []string{"3000", "8000"},
  546. ExternalLinks: []string{
  547. "redis_1",
  548. "project_db_1:mysql",
  549. "project_db_1:postgresql",
  550. },
  551. ExtraHosts: map[string]string{
  552. "otherhost": "50.31.209.229",
  553. "somehost": "162.242.195.82",
  554. },
  555. HealthCheck: &types.HealthCheckConfig{
  556. Test: []string{
  557. "CMD-SHELL",
  558. "echo \"hello world\"",
  559. },
  560. Interval: "10s",
  561. Timeout: "1s",
  562. Retries: uint64Ptr(5),
  563. },
  564. Hostname: "foo",
  565. Image: "redis",
  566. Ipc: "host",
  567. Labels: map[string]string{
  568. "com.example.description": "Accounting webapp",
  569. "com.example.number": "42",
  570. "com.example.empty-label": "",
  571. },
  572. Links: []string{
  573. "db",
  574. "db:database",
  575. "redis",
  576. },
  577. Logging: &types.LoggingConfig{
  578. Driver: "syslog",
  579. Options: map[string]string{
  580. "syslog-address": "tcp://192.168.0.42:123",
  581. },
  582. },
  583. MacAddress: "02:42:ac:11:65:43",
  584. NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
  585. Networks: map[string]*types.ServiceNetworkConfig{
  586. "some-network": {
  587. Aliases: []string{"alias1", "alias3"},
  588. Ipv4Address: "",
  589. Ipv6Address: "",
  590. },
  591. "other-network": {
  592. Ipv4Address: "172.16.238.10",
  593. Ipv6Address: "2001:3984:3989::10",
  594. },
  595. "other-other-network": nil,
  596. },
  597. Pid: "host",
  598. Ports: []string{
  599. "3000",
  600. "3000-3005",
  601. "8000:8000",
  602. "9090-9091:8080-8081",
  603. "49100:22",
  604. "127.0.0.1:8001:8001",
  605. "127.0.0.1:5000-5010:5000-5010",
  606. },
  607. Privileged: true,
  608. ReadOnly: true,
  609. Restart: "always",
  610. SecurityOpt: []string{
  611. "label=level:s0:c100,c200",
  612. "label=type:svirt_apache_t",
  613. },
  614. StdinOpen: true,
  615. StopSignal: "SIGUSR1",
  616. StopGracePeriod: &stopGracePeriod,
  617. Tmpfs: []string{"/run", "/tmp"},
  618. Tty: true,
  619. Ulimits: map[string]*types.UlimitsConfig{
  620. "nproc": {
  621. Single: 65535,
  622. },
  623. "nofile": {
  624. Soft: 20000,
  625. Hard: 40000,
  626. },
  627. },
  628. User: "someone",
  629. Volumes: []string{
  630. "/var/lib/mysql",
  631. "/opt/data:/var/lib/mysql",
  632. fmt.Sprintf("%s:/code", workingDir),
  633. fmt.Sprintf("%s/static:/var/www/html", workingDir),
  634. fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
  635. "datavolume:/var/lib/mysql",
  636. },
  637. WorkingDir: "/code",
  638. }
  639. assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
  640. expectedNetworkConfig := map[string]types.NetworkConfig{
  641. "some-network": {},
  642. "other-network": {
  643. Driver: "overlay",
  644. DriverOpts: map[string]string{
  645. "foo": "bar",
  646. "baz": "1",
  647. },
  648. Ipam: types.IPAMConfig{
  649. Driver: "overlay",
  650. Config: []*types.IPAMPool{
  651. {Subnet: "172.16.238.0/24"},
  652. {Subnet: "2001:3984:3989::/64"},
  653. },
  654. },
  655. },
  656. "external-network": {
  657. External: types.External{
  658. Name: "external-network",
  659. External: true,
  660. },
  661. },
  662. "other-external-network": {
  663. External: types.External{
  664. Name: "my-cool-network",
  665. External: true,
  666. },
  667. },
  668. }
  669. assert.Equal(t, expectedNetworkConfig, config.Networks)
  670. expectedVolumeConfig := map[string]types.VolumeConfig{
  671. "some-volume": {},
  672. "other-volume": {
  673. Driver: "flocker",
  674. DriverOpts: map[string]string{
  675. "foo": "bar",
  676. "baz": "1",
  677. },
  678. },
  679. "external-volume": {
  680. External: types.External{
  681. Name: "external-volume",
  682. External: true,
  683. },
  684. },
  685. "other-external-volume": {
  686. External: types.External{
  687. Name: "my-cool-volume",
  688. External: true,
  689. },
  690. },
  691. }
  692. assert.Equal(t, expectedVolumeConfig, config.Volumes)
  693. }
  694. func loadYAML(yaml string) (*types.Config, error) {
  695. dict, err := ParseYAML([]byte(yaml))
  696. if err != nil {
  697. return nil, err
  698. }
  699. return Load(buildConfigDetails(dict))
  700. }
  701. func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
  702. sort.Sort(servicesByName(services))
  703. return services
  704. }
  705. type servicesByName []types.ServiceConfig
  706. func (sbn servicesByName) Len() int { return len(sbn) }
  707. func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] }
  708. func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }