down_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "fmt"
  16. "os"
  17. "strings"
  18. "testing"
  19. "github.com/compose-spec/compose-go/v2/types"
  20. "github.com/containerd/errdefs"
  21. "github.com/docker/cli/cli/streams"
  22. "github.com/moby/moby/api/types/container"
  23. "github.com/moby/moby/api/types/image"
  24. "github.com/moby/moby/api/types/network"
  25. "github.com/moby/moby/api/types/volume"
  26. "github.com/moby/moby/client"
  27. "go.uber.org/mock/gomock"
  28. "gotest.tools/v3/assert"
  29. compose "github.com/docker/compose/v5/pkg/api"
  30. "github.com/docker/compose/v5/pkg/mocks"
  31. )
  32. func TestDown(t *testing.T) {
  33. mockCtrl := gomock.NewController(t)
  34. defer mockCtrl.Finish()
  35. api, cli := prepareMocks(mockCtrl)
  36. tested, err := NewComposeService(cli)
  37. assert.NilError(t, err)
  38. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  39. client.ContainerListResult{Items: []container.Summary{
  40. testContainer("service1", "123", false),
  41. testContainer("service2", "456", false),
  42. testContainer("service2", "789", false),
  43. testContainer("service_orphan", "321", true),
  44. }}, nil)
  45. api.EXPECT().VolumeList(
  46. gomock.Any(),
  47. client.VolumeListOptions{
  48. Filters: projectFilter(strings.ToLower(testProject)),
  49. }).
  50. Return(client.VolumeListResult{}, nil)
  51. // network names are not guaranteed to be unique, ensure Compose handles
  52. // cleanup properly if duplicates are inadvertently created
  53. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  54. Return(client.NetworkListResult{Items: []network.Summary{
  55. {Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  56. {Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  57. }}, nil)
  58. stopOptions := client.ContainerStopOptions{}
  59. api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(client.ContainerStopResult{}, nil)
  60. api.EXPECT().ContainerStop(gomock.Any(), "456", stopOptions).Return(client.ContainerStopResult{}, nil)
  61. api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(client.ContainerStopResult{}, nil)
  62. api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  63. api.EXPECT().ContainerRemove(gomock.Any(), "456", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  64. api.EXPECT().ContainerRemove(gomock.Any(), "789", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  65. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
  66. Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
  67. }).Return(client.NetworkListResult{Items: []network.Summary{
  68. {Network: network.Network{ID: "abc123", Name: "myProject_default"}},
  69. {Network: network.Network{ID: "def456", Name: "myProject_default"}},
  70. }}, nil)
  71. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{
  72. Network: network.Inspect{Network: network.Network{ID: "abc123"}},
  73. }, nil)
  74. api.EXPECT().NetworkInspect(gomock.Any(), "def456", gomock.Any()).Return(client.NetworkInspectResult{
  75. Network: network.Inspect{Network: network.Network{ID: "def456"}},
  76. }, nil)
  77. api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
  78. api.EXPECT().NetworkRemove(gomock.Any(), "def456", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
  79. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{})
  80. assert.NilError(t, err)
  81. }
  82. func TestDownWithGivenServices(t *testing.T) {
  83. mockCtrl := gomock.NewController(t)
  84. defer mockCtrl.Finish()
  85. api, cli := prepareMocks(mockCtrl)
  86. tested, err := NewComposeService(cli)
  87. assert.NilError(t, err)
  88. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(client.ContainerListResult{
  89. Items: []container.Summary{
  90. testContainer("service1", "123", false),
  91. testContainer("service2", "456", false),
  92. testContainer("service2", "789", false),
  93. testContainer("service_orphan", "321", true),
  94. },
  95. }, nil)
  96. api.EXPECT().VolumeList(
  97. gomock.Any(),
  98. client.VolumeListOptions{
  99. Filters: projectFilter(strings.ToLower(testProject)),
  100. }).
  101. Return(client.VolumeListResult{}, nil)
  102. // network names are not guaranteed to be unique, ensure Compose handles
  103. // cleanup properly if duplicates are inadvertently created
  104. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  105. Return(client.NetworkListResult{Items: []network.Summary{
  106. {Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  107. {Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  108. }}, nil)
  109. api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
  110. api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  111. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
  112. Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
  113. }).Return(client.NetworkListResult{Items: []network.Summary{
  114. {Network: network.Network{ID: "abc123", Name: "myProject_default"}},
  115. }}, nil)
  116. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{Network: network.Inspect{Network: network.Network{ID: "abc123"}}}, nil)
  117. api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
  118. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
  119. Services: []string{"service1", "not-running-service"},
  120. })
  121. assert.NilError(t, err)
  122. }
  123. func TestDownWithSpecifiedServiceButTheServicesAreNotRunning(t *testing.T) {
  124. mockCtrl := gomock.NewController(t)
  125. defer mockCtrl.Finish()
  126. api, cli := prepareMocks(mockCtrl)
  127. tested, err := NewComposeService(cli)
  128. assert.NilError(t, err)
  129. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(client.ContainerListResult{
  130. Items: []container.Summary{
  131. testContainer("service1", "123", false),
  132. testContainer("service2", "456", false),
  133. testContainer("service2", "789", false),
  134. testContainer("service_orphan", "321", true),
  135. },
  136. }, nil)
  137. api.EXPECT().VolumeList(
  138. gomock.Any(),
  139. client.VolumeListOptions{
  140. Filters: projectFilter(strings.ToLower(testProject)),
  141. }).
  142. Return(client.VolumeListResult{}, nil)
  143. // network names are not guaranteed to be unique, ensure Compose handles
  144. // cleanup properly if duplicates are inadvertently created
  145. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  146. Return(client.NetworkListResult{Items: []network.Summary{
  147. {Network: network.Network{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  148. {Network: network.Network{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}}},
  149. }}, nil)
  150. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
  151. Services: []string{"not-running-service1", "not-running-service2"},
  152. })
  153. assert.NilError(t, err)
  154. }
  155. func TestDownRemoveOrphans(t *testing.T) {
  156. mockCtrl := gomock.NewController(t)
  157. defer mockCtrl.Finish()
  158. api, cli := prepareMocks(mockCtrl)
  159. tested, err := NewComposeService(cli)
  160. assert.NilError(t, err)
  161. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return(
  162. client.ContainerListResult{
  163. Items: []container.Summary{
  164. testContainer("service1", "123", false),
  165. testContainer("service2", "789", false),
  166. testContainer("service_orphan", "321", true),
  167. },
  168. }, nil)
  169. api.EXPECT().VolumeList(
  170. gomock.Any(),
  171. client.VolumeListOptions{
  172. Filters: projectFilter(strings.ToLower(testProject)),
  173. }).
  174. Return(client.VolumeListResult{}, nil)
  175. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  176. Return(client.NetworkListResult{
  177. Items: []network.Summary{{
  178. Network: network.Network{
  179. Name: "myProject_default",
  180. Labels: map[string]string{compose.NetworkLabel: "default"},
  181. },
  182. }},
  183. }, nil)
  184. stopOptions := client.ContainerStopOptions{}
  185. api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(client.ContainerStopResult{}, nil)
  186. api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(client.ContainerStopResult{}, nil)
  187. api.EXPECT().ContainerStop(gomock.Any(), "321", stopOptions).Return(client.ContainerStopResult{}, nil)
  188. api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  189. api.EXPECT().ContainerRemove(gomock.Any(), "789", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  190. api.EXPECT().ContainerRemove(gomock.Any(), "321", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  191. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
  192. Filters: projectFilter(strings.ToLower(testProject)).Add("label", networkFilter("default")),
  193. }).Return(client.NetworkListResult{
  194. Items: []network.Summary{{Network: network.Network{ID: "abc123", Name: "myProject_default"}}},
  195. }, nil)
  196. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{
  197. Network: network.Inspect{Network: network.Network{ID: "abc123"}},
  198. }, nil)
  199. api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)
  200. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
  201. assert.NilError(t, err)
  202. }
  203. func TestDownRemoveVolumes(t *testing.T) {
  204. mockCtrl := gomock.NewController(t)
  205. defer mockCtrl.Finish()
  206. api, cli := prepareMocks(mockCtrl)
  207. tested, err := NewComposeService(cli)
  208. assert.NilError(t, err)
  209. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  210. client.ContainerListResult{
  211. Items: []container.Summary{testContainer("service1", "123", false)},
  212. }, nil)
  213. api.EXPECT().VolumeList(
  214. gomock.Any(),
  215. client.VolumeListOptions{
  216. Filters: projectFilter(strings.ToLower(testProject)),
  217. }).
  218. Return(client.VolumeListResult{
  219. Items: []volume.Volume{{Name: "myProject_volume"}},
  220. }, nil)
  221. api.EXPECT().VolumeInspect(gomock.Any(), "myProject_volume", gomock.Any()).
  222. Return(client.VolumeInspectResult{}, nil)
  223. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  224. Return(client.NetworkListResult{}, nil)
  225. api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
  226. api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(client.ContainerRemoveResult{}, nil)
  227. api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", client.VolumeRemoveOptions{Force: true}).Return(client.VolumeRemoveResult{}, nil)
  228. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
  229. assert.NilError(t, err)
  230. }
  231. func TestDownRemoveImages(t *testing.T) {
  232. mockCtrl := gomock.NewController(t)
  233. defer mockCtrl.Finish()
  234. opts := compose.DownOptions{
  235. Project: &types.Project{
  236. Name: strings.ToLower(testProject),
  237. Services: types.Services{
  238. "local-anonymous": {Name: "local-anonymous"},
  239. "local-named": {Name: "local-named", Image: "local-named-image"},
  240. "remote": {Name: "remote", Image: "remote-image"},
  241. "remote-tagged": {Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"},
  242. "no-images-anonymous": {Name: "no-images-anonymous"},
  243. "no-images-named": {Name: "no-images-named", Image: "missing-named-image"},
  244. },
  245. },
  246. }
  247. api, cli := prepareMocks(mockCtrl)
  248. tested, err := NewComposeService(cli)
  249. assert.NilError(t, err)
  250. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).
  251. Return(client.ContainerListResult{
  252. Items: []container.Summary{
  253. testContainer("service1", "123", false),
  254. },
  255. }, nil).
  256. AnyTimes()
  257. api.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{
  258. Filters: projectFilter(strings.ToLower(testProject)).Add("dangling", "false"),
  259. }).Return(client.ImageListResult{Items: []image.Summary{
  260. {
  261. Labels: types.Labels{compose.ServiceLabel: "local-anonymous"},
  262. RepoTags: []string{"testproject-local-anonymous:latest"},
  263. },
  264. {
  265. Labels: types.Labels{compose.ServiceLabel: "local-named"},
  266. RepoTags: []string{"local-named-image:latest"},
  267. },
  268. }}, nil).AnyTimes()
  269. imagesToBeInspected := map[string]bool{
  270. "testproject-local-anonymous": true,
  271. "local-named-image": true,
  272. "remote-image": true,
  273. "testproject-no-images-anonymous": false,
  274. "missing-named-image": false,
  275. }
  276. for img, exists := range imagesToBeInspected {
  277. var resp image.InspectResponse
  278. var err error
  279. if exists {
  280. resp.RepoTags = []string{img}
  281. } else {
  282. err = errdefs.ErrNotFound.WithMessage(fmt.Sprintf("test specified that image %q should not exist", img))
  283. }
  284. api.EXPECT().ImageInspect(gomock.Any(), img).
  285. Return(client.ImageInspectResult{InspectResponse: resp}, err).
  286. AnyTimes()
  287. }
  288. api.EXPECT().ImageInspect(gomock.Any(), "registry.example.com/remote-image-tagged:v1.0").
  289. Return(client.ImageInspectResult{InspectResponse: image.InspectResponse{RepoTags: []string{"registry.example.com/remote-image-tagged:v1.0"}}}, nil).
  290. AnyTimes()
  291. localImagesToBeRemoved := []string{
  292. "testproject-local-anonymous:latest",
  293. "local-named-image:latest",
  294. }
  295. for _, img := range localImagesToBeRemoved {
  296. // test calls down --rmi=local then down --rmi=all, so local images
  297. // get "removed" 2x, while other images are only 1x
  298. api.EXPECT().ImageRemove(gomock.Any(), img, client.ImageRemoveOptions{}).
  299. Return(client.ImageRemoveResult{}, nil).
  300. Times(2)
  301. }
  302. t.Log("-> docker compose down --rmi=local")
  303. opts.Images = "local"
  304. err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
  305. assert.NilError(t, err)
  306. otherImagesToBeRemoved := []string{
  307. "remote-image:latest",
  308. "registry.example.com/remote-image-tagged:v1.0",
  309. }
  310. for _, img := range otherImagesToBeRemoved {
  311. api.EXPECT().ImageRemove(gomock.Any(), img, client.ImageRemoveOptions{}).
  312. Return(client.ImageRemoveResult{}, nil).
  313. Times(1)
  314. }
  315. t.Log("-> docker compose down --rmi=all")
  316. opts.Images = "all"
  317. err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
  318. assert.NilError(t, err)
  319. }
  320. func TestDownRemoveImages_NoLabel(t *testing.T) {
  321. mockCtrl := gomock.NewController(t)
  322. defer mockCtrl.Finish()
  323. api, cli := prepareMocks(mockCtrl)
  324. tested, err := NewComposeService(cli)
  325. assert.NilError(t, err)
  326. ctr := testContainer("service1", "123", false)
  327. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  328. client.ContainerListResult{
  329. Items: []container.Summary{ctr},
  330. }, nil)
  331. api.EXPECT().VolumeList(
  332. gomock.Any(),
  333. client.VolumeListOptions{
  334. Filters: projectFilter(strings.ToLower(testProject)),
  335. }).
  336. Return(client.VolumeListResult{
  337. Items: []volume.Volume{{Name: "myProject_volume"}},
  338. }, nil)
  339. api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{Filters: projectFilter(strings.ToLower(testProject))}).
  340. Return(client.NetworkListResult{}, nil)
  341. // ImageList returns no images for the project since they were unlabeled
  342. // (created by an older version of Compose)
  343. api.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{
  344. Filters: projectFilter(strings.ToLower(testProject)).Add("dangling", "false"),
  345. }).Return(client.ImageListResult{}, nil)
  346. api.EXPECT().ImageInspect(gomock.Any(), "testproject-service1", gomock.Any()).Return(client.ImageInspectResult{}, nil)
  347. api.EXPECT().ContainerStop(gomock.Any(), "123", client.ContainerStopOptions{}).Return(client.ContainerStopResult{}, nil)
  348. api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)
  349. api.EXPECT().ImageRemove(gomock.Any(), "testproject-service1:latest", client.ImageRemoveOptions{}).Return(client.ImageRemoveResult{}, nil)
  350. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
  351. assert.NilError(t, err)
  352. }
  353. func prepareMocks(mockCtrl *gomock.Controller) (*mocks.MockAPIClient, *mocks.MockCli) {
  354. api := mocks.NewMockAPIClient(mockCtrl)
  355. cli := mocks.NewMockCli(mockCtrl)
  356. cli.EXPECT().Client().Return(api).AnyTimes()
  357. cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
  358. cli.EXPECT().Out().Return(streams.NewOut(os.Stdout)).AnyTimes()
  359. return api, cli
  360. }